代币
官方文档中对代币的解释是代币可以再以太坊中表示任何东西。比如在线平台中的信誉积、游戏中一个角色的技能、彩票卷、金融资产类似于公司股份的资产、像美元一样的法定货币、一盎司黄金及更多…
由于以太坊这么强大的特点,就需要一个强的标准去规范和处理。ERC-20标准允许开发者构建可与其他产品和服务互相操作的代币应用程序。
ERC-20
ERC-20 的功能示例包括:
1.将代币从一个帐户转到另一个帐户
2.获取帐户的当前代币余额
3.获取网络上可用代币的总供应量
4.批准一个帐户中一定的代币金额由第三方帐户使用
智能合约设计与部署
跟之前的以太坊环境搭建的操作一样,
首先初始化节点,
然后启动节点,注意一下这里的http.addr和http.port,
最后连接节点,
私有节点就创建好了。
设计合约
合约的注意事项参考之前的以太坊环境搭建文档。
创建三个sol文件,
第一个sol文件为IECR20.sol,代码如下,
// SPDX-License-Identifier: MIT
//file IERC20.sol
pragma solidity ^0.8.0;
interface IERC20 {
// 总发行量
function totalSupply() external view returns (uint256);
// 查看地址余额
function balanceOf(address account) external view returns (uint256);
/// 从自己帐户给指定地址转账
function transfer(address account, uint256 amount) external returns (bool);
// 查看被授权人还可以使用的代币余额
function allowance(address owner, address spender) external view returns (uint256);
// 授权指定帐户使用你拥有的代币
function approve(address spender, uint256 amount) external returns (bool);
// 从一个地址转账至另一个地址,该函数只能是通过approver授权的用户可以调用
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
/// 定义事件,发生代币转移时触发
event Transfer(address indexed from, address indexed to, uint256 value);
/// 定义事件 授权时触发
event Approval(address indexed owner, address indexed spender, uint256 value);
}
第二个sol文件为IERC20Metadata.sol,代码如下
// SPDX-License-Identifier: MIT
//file IERC20Metadata.sol
pragma solidity ^0.8.0;
import "./IERC20.sol";
interface IERC20Metadata is IERC20 {
// 代币名称, 如:BitCoin
function name() external view returns (string memory);
// 代币符号或简称, 如:BTC
function symbol() external view returns (string memory);
// 代币支持的小数点后位数,若无特别需求,我们一般默认采用18位。
function decimals() external view returns (uint8);
}
第三个sol文件为IERC20Metadata.sol,代码如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IERC20.sol";
import "./IERC20Metadata.sol";
contract ERC20 is IERC20, IERC20Metadata {
// 地址余额
mapping(address => uint256) private _balances;
// 授权地址余额
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
// 设定代币名称符号,并初始化铸造了10000000000代币在发布者帐号下。
constructor() {
_name = "HarryToken";
_symbol = "HYT";
_mint(msg.sender, 10000000000);
}
function name() public view virtual override returns (string memory) {
return _name;
}
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
/// 小数点位数一般为 18
function decimals() public view virtual override returns (uint8) {
return 18;
}
// 返回当前流通代币的总量
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
// 查询指定帐号地址余额
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
// 转帐功能
function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = msg.sender;
_transfer(owner, to, amount);
return true;
}
// 获取被授权者可使用授权帐号的可使用余额
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
// 授权指定帐事情可使用自己一定额度的帐户余额。
// 授权spender, 可将自己余额。使用可使用的余额的总量为amount
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = msg.sender;
_approve(owner, spender, amount);
return true;
}
//approve函数中的spender调用,将授权人 from 帐户中的代币转入to 帐户中
function transferFrom(
address from,
address to,
uint256 amount
) public virtual override returns (bool) {
address spender = msg.sender;
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
address owner = msg.sender;
_approve(owner, spender, _allowances[owner][spender] + addedValue);
return true;
}
function decreaseAllowance(address spender, uint256 substractedValue) public virtual returns (bool) {
address owner = msg.sender;
uint256 currentAllowance = _allowances[owner][spender];
require(currentAllowance >= substractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - substractedValue);
}
return true;
}
function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
}
_balances[to] += amount;
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply += amount;
_balances[account] += amount;
emit Transfer(address(0), account, amount);
_afterTokenTransfer(address(0), account, amount);
}
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
}
_totalSupply -= amount;
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
}
function _approve(
address owner,
address spender,
uint256 amount
) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}
}
以上代码可以参考官方文档自己写,也可以网上参考很多ERC-20合约代码。
部署合约
现在我们部署一下智能合约,选择合适的编译器版本,我这里是0.8.0,
然后环境选择MetaMask,
然后就会自动弹出(前提是你下载了MetaMask插件),
如果没有下载,点击火狐浏览器右上角这个碎片,
然后点击管理扩展,
搜索metamask,
然后自己点进去下载,
紧接上文,登录之后就会自动填入现在正在用的账户(前提是你连接了私网),
==注意:==如果没有自动填入正在用户,也就是没有连私网。为什么要连私网,不连测试网络,说实话没有啥区别,那个测试网络一开始用户没有测试币,不能支付挖矿,连合约都部署不了,私网效果差不多的。
点击下面红色方框区域,进行网络选择。
然后添加网络,
点击这个手动添加网络,
填写私网信息,
下面是我的填写,
然后点击保存,等待连接吧。
然后我们选择我们需要的账户,就是之前创建节点我们自己personal.newAccount生成的账户,
这里我选择这一个账户,
然后重新开启一个控制台,进入之前创建好的节点文件里面的keystore文件,里面存放这我们的私钥信息,然后选择对应的用户私钥,查看信息。这里的信息不是说就是私钥,而是一些算法和变量,通过这些才能计算得到私钥,是私钥的象征。
然后复制这一段到你自己知道的文件目录下,并把这段字符放在新创建的json文件里面。
保存之后进入MetaMask,点击切换用户,
然后可以自己进行在控制台创建好的用户导入,选择添加账户
选择导入账户。
选择JSON文件,
选择浏览文件,
选择之前在桌面创建好的json文件,
然后输入创建用户时我们设置的密码,
出现下面变化,用户切换成功,
可以对比一下控制台的用户地址,发现是一样的,说明我们导入成功。
注意这里是当前用户的地址,如果你没有连接MetaMask那么这里默认分配的是虚拟用户,没有啥用,
下面这里我们是重新进行过连接的,如果一开始不是你想要的用户,那么退出浏览器然后再进入remix重新编译和连接一下,
我们点击部署按钮之后,我们的交易就被放入交易池中悬挂起来,等待别人挖矿,然后将交易信息存入区块中。
然后我就开始模拟挖矿。
可以发现,挖矿之后合约部署这个交易就被确认了。
==注意:==如果如果,因为我这里遇到了,你挖矿很慢,首先研究一下你自己节点设置的挖矿难度是不是很高,或者那就自己上网查查怎么Ubuntu怎么加快,比如改改内存之类的。
调用合约
接下来可以使用部署好的合约里的方法进行其他交易,
这里可以进行转账交易。输入转入的地址和金额,转出方为一开始我们导入的用户。
然后可以看到交易并没有被处理,仍然在交易池里。
挖矿。
交易被确认,意味着转账成功,当前用于的金额此时才真的转到了转入用户的钱包里。
可以发现,状态由待处理变成了已确认。
然后通过balanceOf可以发现代币比之前少了12个。
发行代币
下面是我的代币的名字和简称,在ERC20.sol合约的构造器里面。
下面这个地址是合约地址,如果你创建了代币,可以通过该合约地址来发现你创建的代币。
复制上面的地址到下面的代币合约地址一栏中,下面的代币符号和代币小数就会自动填充。
点击导入。
就可以在MetaMask中看到自己创建的代币了。
DAPP开发
接下来我们将设计一个简单的web页面来实现交互,就是最简单的HTML和JS,想要搞厉害的可以去网上看看vue的,也挺方便,我懒得弄了。
先看看最终结果展示,
授权账户:主要是用来请求MetaMask的授权,指定你目前使用的账户是哪个。
查看余额:就是查看钱包里还有多少代币。
转账:就是完成一个账户到另一个账户的代币转移。
交易历史:就是查看你一共发生了哪些交易,这些交易的交易哈希是多少,你自己拿这些交易哈希去干一些你想干的事情。
代码你可以放在某个文件夹下,也可以就放在桌面。
我就放在桌面的,
HTML代码如下,
<!DOCTYPE html>
<html>
<head>
<title>Hello World DApp</title>
<meta charset="UTF-8">
</head>
<body>
<h1>Fan's Wallet</h1>
<div class="table-responsive">
<button onclick="getCurrentAccounts()">授权账户</button>
<div style="margin-bottom:50rpx">代币余额:<span id="variableValue"></span> <button onclick="seeMyLeaveMoney()">点击查看余额</button></div>
<div style="margin-bottom:50rpx">转账:
我要转账给地址为:<input type="text" id="toId"/>的账户,转账金额为<input type="text" id="account"/><button onclick="confirmTransfer()">确认转账</button>
</div>
<div style="margin-bottom:50rpx">交易历史<button onclick="seeHistory()">点击查看交易历史</button></div>
<ul id="historya">
</ul>
</div>
</body>
<script src="jquery.min.js"></script>
<script src="web3.min.js"></script>
<script src="blockchain.js"></script>
</html>
注意Js文件我这里是放在和html文件一个目录下,都放在桌面的,
Js代码如下,你根据需要自己改改,我页面太low了
const web3=new Web3("HTTP://0.0.0.0:9049");
web3.eth.defaultAccount ='0x8b4f61dbd48285d73fd8a57064d7407faff2a74a';
const abi = JSON.parse('[{"inputs":[],"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"},{"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":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","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":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"substractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","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"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]');
const contractAddress='0xe23DbC79aC17249fcCD8DAAaD35697CD2b1ffd08';
const contractInstance =new web3.eth.Contract(abi,contractAddress);
var logss=[];
async function getCurrentAccounts() {
const accounts = await web3.eth.getAccounts();
console.log(accounts);
ethereum.request({ method: 'eth_requestAccounts' }).then(nowaccount => {
console.log(nowaccount);
}).catch((error) => {
console.error(error);
});
}
async function seeMyLeaveMoney() {
//document.write('12345');
//console.log(contractInstance.methods.balanceOf("0x8b4f61dbd48285d73fd8a57064d7407faff2a74a").call());
const leavemoney=await contractInstance.methods.balanceOf("0x8b4f61dbd48285d73fd8a57064d7407faff2a74a").call();
console.log(leavemoney);
//var myVariable = "Hello, World!";
document.getElementById('variableValue').innerHTML = leavemoney ;
}
async function confirmTransfer(){
console.log(contractInstance);
var to=document.getElementById('toId').value;
console.log(to);
var account=document.getElementById('account').value;
console.log(account);
var from="0x8b4f61dbd48285d73fd8a57064d7407faff2a74a";
const isTransferOk=await contractInstance.methods.transfer(to,account).call();
if(isTransferOk){
alert("转账成功!");
}
}
async function seeHistory() {
// 使用web3.eth.subscribe订阅'logs'事件来监听新的日志
const subscription = web3.eth.subscribe('logs', {
fromBlock: '0',
address: contractAddress
}, function(error, result) {
if (!error) {
console.log(result);
}
})
.on("data", function(log) {
console.log(log);
})
.on("changed", function(log) {
// 当某些情况下,一个log被移除时会触发这个事件
});
// 使用web3.eth.getPastLogs获取历史日志
web3.eth.getPastLogs({
fromBlock: '0',
toBlock: 'latest',
address: contractAddress
})
.then(logs => {
logs.forEach(log => {
logss.push(log)
console.log(log.transactionHash);
// console.log(JSON.stringify(log));
// document.getElementById('historya').textContent = log ;
});
})
.catch(err => console.error(err));
console.log(logss);
var html='';
for(let i=0;i<logss.length;i++){
html+='<li>'+'第'+i+'次交易的交易哈希为:'+logss[i].transactionHash+'</li>';
}
document.getElementById('historya').innerHTML = html;
}
其他js代码自己理解一下,交易历史这里我不知道是查询什么交易历史,我这里得到交易哈希,可以通过交易哈希得到交易的详细信息。
==注意一下:==html不止引入了一个js文件,还有另外两个必须的web3.min.js以及jquery.min.js,
这两引入官网给的是用网址的方式引入,但是如果你虚拟机没有科学上网的话,你访问不到,然后你想科学上网的话,还要去搞个虚拟机VPN,有点麻烦,所以我们将官网给的那个网址,下载到本地引用。
这里注意选择版本,以及科学上网访问该网站,下载web3.min.js文件到本地。
还有jquery.min.js文件。
集成与测试
在html文件和js文件所在目录运行python服务器(这里版本是3.x),我的这里在桌面运行,得到默认端口8000,之后,在浏览器打开这个链接,进入网页。
python3 -m http.server
授权账户
点击授权账户,第一次授权会和MetaMask进行连接,
控制台打印被授权的账户,并且显示授权成功。
查看余额
点击查看余额按钮,可以看到代币余额。
转账
输入转入的账户地址和转账余额,点击确认转账,
然后回提示转账成功。
查看交易历史
点击查看交易历史,得到每次交易的交易哈希值,你可以通过交易哈希再去查看交易信息。
实验总结
合约设计的理由
设计这样的合约有几个关键理由:
标准化: ERC20提供了一组标准的接口,使得不同的代币能在各种应用中通用,如钱包、交易所等。
简化交互: 开发者可以通过标准接口与代币进行交互,而无需了解代币的具体实现细节。
降低复杂性: 通过实现标准接口,新的代币合约可以更容易地被社区接受和集成。
增强兼容性: 标准化的合约能够确保与现有的基础设施和服务的兼容性。
安全性: 遵循已被广泛审查和测试的标准可以减少安全漏洞。
此外,合约中的 mint 函数在构造函数中被调用,以初始化代币供应量,这是启动新代币项目的常见做法。name和symbol变量分别用于设置代币的名称和符号,这对于代币的识别和使用至关重要。整体来说,这样的设计旨在确保代币的互操作性、安全性和易用性。
遇到的挑战以及解决方案
1.不能挖矿。
一开始是这样的,每次重启虚拟机之后,立刻挖矿,还能挖出来。但是只要我一打开remix和MetaMask,就根本挖不了一点。所以这肯定是虚拟机本身的原因,不是你自己区块链的问题。解决方案就是首先看看自己创世文件里面的挖矿难度是不是设置太高了,如果你设置比较低还是挖不动。那就更改一下虚拟机配置,增大一下内存什么的。
2.无法使用测试网络。
也不是不能使用,就是我部署合约都报错insufficient allowance,好像是这个,就是你这个账户里面没钱,但是MetaMask又不能buy测试币(有可能是我没有找到方法)。解决方案就是使用私网,自己搭建一个私有节点,然后连接到MetaMask上面去,可能用测试网络还更不好挖矿,因为你网络延迟大的话,挖矿需要不断地发送和接受数据包,就会导致你挖不动。
3.DAPP的开发。
我只能说对html和js语言太陌生了,语言太原始了。解决方案就是自己遇到问题上网查询语言的代码规范,注意细节,如果能用比较好的开发环境就用比如vscode啥的。
心得体会
首先是没有完整的适合学习者看的开发文档,因此对于区块链的探索我们学习的步伐被阻遏住了,但是一些兴趣就是在这样的过程中一步一步建立起来的,所有的问题都有答案,只是答案可能没有人去主动给出。然后是对于Ubuntu虚拟机环境的逐步适应,由于这个环境的开发能够被提供的资源更加全面,因此我始终使用这个环境,所以当遇到问题的时候,想一想windows遇到的困难更多,心里就会好受不少。最后感觉能够坚持去完成去探索,是一件不容易的事,也是为数不多的自己能够有兴趣去做的方向。