以太坊(Ethereum)是一个去中心化的开源的有智能合约功能的公共区块链平台。以太币(ETH 或 Ξ)是以太坊的原生加密货币。截至2021年12月,以太币是市值第二高的加密货币,仅次于比特币。以太坊是使用最多的区块链。
以太坊的概念首次在2013至2014年间由程序员维塔利克·布特林受比特币启发后提出,大意为“下一代加密货币与去中心化应用平台”,在2014年通过ICO众筹得以开始发展。以太坊亦被称为“第二代的区块链平台”,仅次于比特币。(维基百科)
以太坊官网
以太坊作为区块链技术的一个开源平台,允许任何人在区块链上构建和使用去中心化的应用程序和加密货币。
以太坊的主要特点包括:
以太坊代币标准是指在以太坊上发行代币需要遵循的一系列技术规范,以方便以太坊开发者在以太坊区块链上发行和交易加密数字资产,常见的代币标准有 ERC-20、ERC-721、ERC-777、ERC-1155、ERC-4626等。代币标准的实施有助于在项目的不同实现中保持统一的兼容性(例如以太坊客户和钱包),并确保智能合约和 dapps 仍保持兼容。(参考以太坊文档)
(图片来自以太坊官方网站)
本文介绍基于 MetaMask + Ganache + Web3.js 的 同质化代币 Dapp 搭建。
MetaMask 官网
MetaMask(小狐狸钱包),是用于与以太坊区块链进行交互的软件加密货币钱包。它可以通过浏览器扩展程序或移动应用程序让用户访问其以太坊钱包,与去中心化应用进行交互。
MetaMask 由 ConsenSys Software Inc. 开发运营,主要专注于以太坊为基础的工具及基础设施。(维基百科)
进入 MetaMask 官网直接点击下载 MetaMask 的浏览器扩展,第一次使用需要自己注册账号(注意保存好私钥)。
Ganache 官网
Ganache 是一个能够在本地快速部署以太坊私有链的模拟应用。能够帮助开发者快速、简单、安全地测试和调试他们的去中心化应用(DApp),并在发布到生产环境之前预览其性能和功能。通过 Ganache,开发者可以快速查看所有账户的当前状态,包括他们的地址、私钥、交易和余额。
同样的,可以在 Ganache 官网点击下载 windows 版本的 Ganache ,操作简单,开箱即用。
ERC-20 代币标准
ERC-20(以太坊意见征求 20)由 Fabian Vogelsteller 提出于 2015 年 11 月。这是一个能实现智能合约中代币的应用程序接口标准。
ERC-20 的功能示例包括:
如果智能合约实施了下列方法和事件,它可以被称为 ERC-20 代币合约,一旦部署,将负责跟踪以太坊上创建的代币。(以太坊官方文档)
在这里我们用 solidity 实现 ERC-20 代币协议:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
// ERC-20 代币合约接口
interface IERC20 {
// 转账事件
event Transfer(address indexed _from, address indexed _to, uint256 _value);
// 授权事件
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
// 代币名称
function name() external view returns (string memory);
// 代币符号
function symbol() external view returns (string memory);
// 小数位数
function decimals() external view returns (uint8);
// 总供给量
function totalSupply() external view returns (uint256);
// 余额查询
function balanceOf(address _owner) external view returns (uint256 balance);
// 转账
function transfer(address _to, uint256 _value) external returns (bool success);
// 从 from 账户转账
function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);
// 授权转账
function approve(address _spender, uint256 _value) external returns (bool success);
// 查询授权余额
function allowance(address _owner, address _spender) external view returns (uint256 remaining);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "./IERC-20.sol";
// ERC-20 代币实现
contract ERC20Token is IERC20 {
event Forge(address indexed account, uint256 amount); // 锻造代币事件
event Destroy(address indexed account, uint amount); // 销毁代币事件
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
uint256 public totalSupply;
string public name;
string public symbol;
uint8 public decimals;
// 构造函数
constructor(string memory name_, string memory symbol_, uint8 decimals_) {
name = name_;
symbol = symbol_;
decimals = decimals_;
}
// 锻造代币
function forgeToken(address account, uint amount) external {
require(amount > 0, "ERC-20: amount less than or equal to 0.");
require(account != address(0), "ERC-20: forge to zero address.");
balanceOf[account] += amount;
totalSupply += amount;
emit Forge(account, amount);
}
// 销毁代币
function destroyToken(address account, uint amount) external {
require(amount > 0, "ERC-20: amount less than or equal to 0.");
require(account != address(0), "ERC-20: destroy to zero address.");
uint256 accountBalance = balanceOf[account];
require(accountBalance >= amount, "ERC-20: destroy amount exceeds balance.");
balanceOf[account] = accountBalance - amount;
totalSupply -= amount;
emit Destroy(account, amount);
}
// 转账
function transfer(address _to, uint256 _value) external returns (bool success){
_transfer(msg.sender, _to, _value);
return true;
}
// 从 _from 账户转账 _value 到 _to 账户
function transferFrom(address _from, address _to, uint256 _value) external returns (bool success) {
uint256 currentAllowance = allowance[_from][msg.sender]; // 查询授权额度
require(currentAllowance >= _value, "ERC-20: transfer amount exceeds allowances."); // 是否超过授权额度
_transfer(_from, _to, _value);
_approve(_from, msg.sender, currentAllowance - _value);
return true;
}
function _transfer(address _from, address _to, uint256 _value) internal {
require(_value > 0, "ERC-20: value less than or equal to 0.");
require(_from != address(0), "ERC-20: transfer from the zero address.");
require(_to != address(0), "ERC-20: transfer to zero address.");
uint256 senderBalance = balanceOf[_from];
require(senderBalance >= _value, "ERC-20: sender amount exceeds balance.");
balanceOf[_from] = senderBalance - _value;
balanceOf[_to] += _value;
emit Transfer(_from, _to, _value);
}
// 进行授权
function approve(address _spender, uint256 _value) external returns (bool success){
_approve(msg.sender, _spender, _value);
return true;
}
function _approve(address _from, address _to, uint256 _value) internal {
require(_value >= 0, "ERC-20: value less than or equal to 0.");
require(_from != address(0), "ERC-20: approve from the zero address.");
require(_to != address(0), "ERC-20: approve to the zero address.");
allowance[_from][_to] = _value;
emit Approval(_from, _to, _value);
}
}
使用 bootstrap3.3.7 框架编写一个简单的前端界面
index.html 源码
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>同质化代币Dapptitle>
<link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet' type='text/css'>
<style>
.jumbotron{
background-color: #EDF0F5;
}
style>
head>
<body>
<div class="container">
<div class="row clearfix">
<div class="col-md-12 column">
<h1 style="text-align: center;">同质化代币 Dapp 搭建h1>
<div class="jumbotron" >
<div class="row clearfix">
<div class="col-md-3 column">
<h3 id="tokenName">h3>
div>
<div class="col-md-3 column">
<h3 id="tokenSymbol">h3>
div>
<div class="col-md-3 column">
<h3 id="tokenDecimals">h3>
div>
<div class="col-md-3 column">
<h3 id="totalSupply">h3>
div>
div>
div>
<div class="row clearfix">
<div class="col-md-4 column">
<span style="font-size: 20px;"><b>授权b>span>
<form class="form-horizontal" role="form">
<div class="form-group">
<label for="approveSpender" class="col-sm-3 control-label">_spender:label>
<div class="col-sm-9">
<input type="text" class="form-control" id="approveSpender" placeholder="address"/>
div>
div>
<div class="form-group">
<label for="approveValue" class="col-sm-3 control-label">_value:label>
<div class="col-sm-9">
<input type="text" class="form-control" id="approveValue" placeholder="uint256"/>
div>
div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<a class="btn btn-primary" onclick="homoTokens('approve')">确定a>
<font id="approveResult" style="float: right; font-size:medium;">font>
div>
div>
form>
div>
<div class="col-md-4 column">
<span style="font-size: 20px;"><b>允许b>span>
<form class="form-horizontal" role="form">
<div class="form-group">
<label for="allowanceOwner" class="col-sm-3 control-label">_owner:label>
<div class="col-sm-9">
<input type="text" class="form-control" id="allowanceOwner" placeholder="address" />
div>
div>
<div class="form-group">
<label for="allowanceSpender" class="col-sm-3 control-label">_spender:label>
<div class="col-sm-9">
<input type="text" class="form-control" id="allowanceSpender" placeholder="address" />
div>
div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<a class="btn btn-success" onclick="homoTokens('allownace')">查询a>
<font id="allownaceResult" style="float: right; font-size:medium;">font>
div>
div>
form>
div>
<div class="col-md-4 column">
<span style="font-size: 20px;"><b>转账b>span>
<form class="form-horizontal" role="form">
<div class="form-group">
<label for="transferTo" class="col-sm-3 control-label">_to:label>
<div class="col-sm-9">
<input type="text" class="form-control" id="transferTo" placeholder="address" />
div>
div>
<div class="form-group">
<label for="transferValue" class="col-sm-3 control-label">_value:label>
<div class="col-sm-9">
<input type="text" class="form-control" id="transferValue" placeholder="uint256" />
div>
div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<a class="btn btn-primary" onclick="homoTokens('transfer')">确定a>
<font id="transferResult" style="float: right; font-size:medium;">font>
div>
div>
form>
div>
div>
<div class="row clearfix">
<div class="col-md-4 column">
<span style="font-size: 20px;"><b>从...转账b>span>
<form class="form-horizontal" role="form">
<div class="form-group">
<label for="transferFromFrom" class="col-sm-3 control-label">_from:label>
<div class="col-sm-9">
<input type="text" class="form-control" id="transferFromFrom" placeholder="address" />
div>
div>
<div class="form-group">
<label for="transferFromTo" class="col-sm-3 control-label">_to:label>
<div class="col-sm-9">
<input type="text" class="form-control" id="transferFromTo" placeholder="address" />
div>
div>
<div class="form-group">
<label for="transferFromValue" class="col-sm-3 control-label">_value:label>
<div class="col-sm-9">
<input type="text" class="form-control" id="transferFromValue" placeholder="uint256" />
div>
div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<a class="btn btn-primary" onclick="homoTokens('transferFrom')">确定a>
<font id="transferFromResult" style="float: right; font-size:medium;">font>
div>
div>
form>
div>
<div class="col-md-4 column">
<span style="font-size: 20px;"><b>余额b>span>
<form class="form-horizontal" role="form">
<div class="form-group">
<label for="balanceOfOwner" class="col-sm-3 control-label">_owner:label>
<div class="col-sm-9">
<input type="text" class="form-control" id="balanceOfOwner" placeholder="address" />
div>
div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<a class="btn btn-success" onclick="homoTokens('balanceOf')">查询a>
<font id="balanceOfResult" style="float: right; font-size:medium;">font>
div>
div>
form>
div>
div>
<hr/>
div>
div>
div>
body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/web3.js">script>
<script src="./index.js">script>
html>
index.js 源码
const web3 = new Web3(new Web3.providers.HttpProvider("HTTP://127.0.0.1:7545"));
const abi = JSON.parse('[{"inputs":[{"internalType":"string","name":"name_","type":"string"},{"internalType":"string","name":"symbol_","type":"string"},{"internalType":"uint8","name":"decimals_","type":"uint8"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_owner","type":"address"},{"indexed":true,"internalType":"address","name":"_spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"_value","type":"uint256"}],"name":"Approval","type":"event"},{"inputs":[{"internalType":"address","name":"_spender","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Destroy","type":"event"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"destroyToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Forge","type":"event"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"forgeToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_from","type":"address"},{"indexed":true,"internalType":"address","name":"_to","type":"address"},{"indexed":false,"internalType":"uint256","name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]')
const token20Contract = web3.eth.contract(abi);
const contractInstance = token20Contract.at('0xe95D98B5975B4D54Fe6d6F555A55009F1AEec13B'); //部署的合约地址
const fromAccount = web3.eth.accounts[0];
console.log(web3.eth.accounts);
// 页面加载完执行
window.onload = function () {
const name = contractInstance.name.call().toString();
const symbol = contractInstance.symbol.call().toString();
const decimals = contractInstance.decimals.call().toString();
const totalSupply = contractInstance.totalSupply.call().toString();
setElementText('tokenName', "名称: " + name);
setElementText('tokenSymbol', "符号: " + symbol);
setElementText('tokenDecimals', "小数位: " + decimals);
setElementText('totalSupply', "总供给量: " + totalSupply);
};
// 设置元素文本
function setElementText(eleId, text) {
const element = document.getElementById(eleId);
element.innerHTML = text;
}
// 获取元素值
function getElementValues(eleIds) {
const result = [];
for (const id of eleIds) {
const inputValue = document.getElementById(id).value;
if (!inputValue) {
alert("缺少必要的参数。");
return;
}
result.push(inputValue);
}
return result;
}
// 同质化代币
function homoTokens(id) {
if (id == "approve") {
const argument = getElementValues(['approveSpender', 'approveValue']);
if (argument) {
// 返回交易的哈希值
const transactionHash = contractInstance.approve(argument[0], argument[1], { from: fromAccount });
if (transactionHash) setElementText("approveResult", 'true');
else setElementText("approveResult", 'error');
}
} else if (id == "allownace") {
const argument = getElementValues(['allowanceOwner', 'allowanceSpender']);
if (argument) {
const result = contractInstance.allowance.call(argument[0], argument[1]).toString();
setElementText("allownaceResult", result);
}
} else if (id == "transfer") {
const argument = getElementValues(['transferTo', 'transferValue']);
if (argument) {
const transactionHash = contractInstance.transfer(argument[0], argument[1], { from: fromAccount });
if (transactionHash) setElementText("transferResult", 'true');
else setElementText("transferResult", 'error');
}
} else if (id == "transferFrom") {
const argument = getElementValues(['transferFromFrom', 'transferFromTo', 'transferFromValue']);
if (argument) {
const transactionHash = contractInstance.transferFrom(argument[0], argument[1],
argument[2], { from: web3.eth.accounts[1] });
if (transactionHash) setElementText("transferFromResult", 'true');
else setElementText("transferFromResult", 'error');
}
} else {
const argument = getElementValues(['balanceOfOwner']);
if (argument) {
const result = contractInstance.balanceOf.call(argument[0]).toString();
setElementText("balanceOfResult", result);
}
}
}
首先需要在 MetaMask 中导入 Ganache 私有链上的账户,以第一个为例,
复制第一个地址的私钥导入 MetaMask 中。
在这里还需要在 MetaMask 的设置中手动添加 Ganache 的私有链网络,基本的信息都可以在 Ganache 首界面找到,添加好切换至新添加的网络。
打开在线版的 Remix 导入前面示例的 solidity 代码, 连接刚刚创建好的 MetaMask 账户。
需要在 Remix 的编译选项中把 Advanced Configurations 中的 EVM VERSION 改为跟 Ganache 的配置一致才能运行智能合约。
否则会报错(Gas estimation errored with the following message (see below). The transactionexecution will likely fail. Do you want to force sending?)
输入初始化信息进行部署
复制部署好的合约地址 和 abi 到 web3.js 的代码中, 如下所示
给 fromAccount 账户锻造 200 个代币用于功能的正常运行
在前端页面输入 fromAccount 账户地址可以看到显示刚才锻造的 200 个代币。
至此一个简单的 Dapp 的搭建和相关操作就完成了。