以太坊官方开发文档:https://ethereum.org/zh/
以太坊小游戏代码学习:https://cryptozombies.io/
本文介绍了以下内容,文章比较长,涉及内容较多,如果对你有帮助,麻烦点赞、收藏,感谢支持
以太坊开发环境(之一Hardhat):https://hardhat.org/
以太坊合约调用(之一Web3.js也可以编译、部署合约):https://web3js.readthedocs.io/
Hardhat是依赖Node环境的,先安装Node.js
下载长期维护版,并进行安装即可(各系统的安装流程,环境变量配置此处不做介绍,非本文核心内容),安装完成后打开终端(cmd、PowerShell),如下测试node环境有效性,正常输出版本信息即可
node -v
v12.14.0
(Windows版node环境)Hardhat环境搭建:
npm init
npm install --save-dev hardhat
npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-truffle5 @nomiclabs/hardhat-web3 web3
npx hardhat
hardhat.config.js
文件的networks
如下,其他配置可参考:HardHat网络参考module.exports = {
defaultNetwork: "hardhat",
networks: {
hardhat: {
// 在这种情况下,将在 3 到 6 秒的随机延迟后挖掘一个新块。
// 例如,第一个区块可以在 4 秒后开采,第二个区块在 5.5 秒后开采,依此类推。
mining: {
auto: false,
interval: [3000, 6000]
}
}
},
solidity: "0.8.4",
};
npx hardhat node
看到输出Account #1、Private Key等信息,说明本地网络启动成功,该网络每次启动都是新的,但创世用户都是固定的,仅用于开发使用,并非真正意义上的区块链网络,本地开发测试发布都需要依赖这个网络环境
新建或导入MetaMask钱包账户,此文不介绍该内容,钱包信息注意妥善保管
私钥,填写本地HardHat启动时,输出的任何一个Account下面的Private Key,点击导入后如图,才可以使用该账户进行后续操作,如果是私链,则使用私链中的用户,就是一定要使用对应网络中的用户,本地HardHat环境查看创世用户可以使用命令npm hardhat account
所涉及的代码都需要有,因为ERC20是代币本身的规范,但是对代币合约的操作以及其他方法都是需要自己开发的,我这里增加了给用户发币等一些方法,为了良好的体验建议跟着往下走
EIPs/eip-20.md
点击链接查看以太坊官方发布的ERC20标准说明
ERC代表“Etuereum Request for Comment”。
ERC20标准定义了一些函数接口,规定了各个代币的基本功能,它可以快速发币,而且使用又方便,因此空投币和大部分基于以太坊合约的代币基本上就是利用ERC-20标准开发的。
如果简单理解的话就是,ERC20是以太坊的一种标准,所有符合这个标准的数字货币都可以成为ERC20代币,自然也就能存入支持以太币的钱包中了,但是这些货币的交易本质上还是在消耗以太坊的资源,所以交易费都还是以太币。
这里的标准其实就是一种智能合约模版,只要满足这个模版的代币都称之为ERC20代币。你可以理解为电脑主板上的内存插条,只要符合一定的尺寸和接线标准的内存品牌都可以使用。这就等于说以太坊订立了一个标准,其他代币就不需要另起炉灶,全部从头到尾再搞一遍,只要满足这个标准就可以在以太坊公链上运行了,极大降低了开发难度。很多代币就是只关注自己的业务逻辑,底层全部依赖以太坊。
转自:https://xw.qq.com/cmsid/20210512A03DDN00
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256){
uint256 c = a * b;
assert(a == 0 || c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256){
assert(b > 0);
uint256 c = a / b;
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256){
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256){
uint256 c = a + b;
assert(c >= a);
return c;
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
interface ERC20 {
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);
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);
event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
/**
* @title Roles
* @dev Library for managing addresses assigned to a Role.
*/
library Roles {
struct Role {
mapping (address => bool) bearer;
}
/**
* @dev give an account access to this role
*/
function add(Role storage role, address account) internal {
require(account != address(0));
role.bearer[account] = true;
}
/**
* @dev remove an account's access to this role
*/
function remove(Role storage role, address account) internal {
require(account != address(0));
role.bearer[account] = false;
}
/**
* @dev check if an account has this role
* @return bool
*/
function has(Role storage role, address account)
internal
view
returns (bool)
{
require(account != address(0));
return role.bearer[account];
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
import "./Roles.sol";
contract Ownable {
using Roles for Roles.Role;
event MinterAdded(address indexed account);
event MinterRemoved(address indexed account);
Roles.Role private minters;
constructor() {
minters.add(msg.sender);
}
modifier onlyMinter() {
require(isMinter(msg.sender));
_;
}
function isMinter(address account) public view returns (bool) {
return minters.has(account);
}
function addMinter(address account) public onlyMinter {
minters.add(account);
emit MinterAdded(account);
}
function renounceMinter() public {
minters.remove(msg.sender);
}
function _removeMinter(address account) internal {
minters.remove(account);
emit MinterRemoved(account);
}
}
需要注意的是,发行的数量需要相对token小数点来设置。
例如如果token的小数点是0位,你要发行1000个token,那么发行数量_totalSupply的值是1000。
但是如果token的小数点是18位,你要发行1000个token,那么发行数量_totalSupply的值是1000000000000000000000(1000后面加上18个0)。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
import { SafeMath } from "../library/SafeMath.sol";
import { ERC20 } from "../library/ERC20.sol";
/// @title PET 私有区块链代币合约
/// @author Aubrey
/// @notice Explain to an end user what this does
/// @dev Explain to a developer any extra details
contract TokenPET is ERC20 {
// 变量
string private _name = 'PETurrency';
string private _symbol = 'PET';
// 8表示将令牌数量除以100000000得到其用户表示。
uint8 private _decimals = 8;
uint256 private _totalSupply;
uint256 constant private MAX_UINT256 = 2**256 - 1;
uint256 private maxMintBlock = 0;
// 使用 SafeMath 函数库
using SafeMath for uint256;
// 类比二维数组
mapping (address => mapping (address => uint256)) private allowed;
// 类比一维数组
mapping (address => uint256) private balances;
// 令牌的名称
function name() public view returns (string memory) {
return _name;
}
// 令牌的符号
function symbol() public view returns (string memory) {
return _symbol;
}
// 令牌使用的小数位数 - 例如8,表示将令牌数量除以100000000得到其用户表示。
function decimals() public view returns (uint8) {
return _decimals;
}
// 返回总代币供应量
function totalSupply() public override view returns (uint256) {
return _totalSupply;
}
// 获取账户余额
function balanceOf(address _owner) public override view returns (uint256 balance) {
return balances[_owner];
}
// 给账户转账
function transfer(address _to, uint256 _value) public override returns (bool success) {
assert(0 < _value);
require(balances[msg.sender] >= _value);
require(_to != address(0));
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
emit Transfer(msg.sender, _to, _value);
return true;
}
// 从账户转账到账户
function transferFrom(address _from, address _to, uint256 _value) public override returns (bool success) {
uint256 _allowance = allowed[_from][msg.sender];
assert (balances[_from] >= _value);
assert (_allowance >= _value);
assert (_value > 0);
require(_to != address(0));
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = _allowance.sub(_value);
emit Transfer(_from, _to, _value);
return true;
}
// 允许 _spender 多次取回您的帐户,最高达 _value 金额; 如果再次调用此函数,它将用 _value 的当前值覆盖的 allowance 值。
function approve(address _spender, uint256 _value) public override returns (bool success) {
require(_spender != address(0));
require((_value == 0) || (allowed[msg.sender][_spender] == 0));
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
// 返回 _spender 仍然被允许从 _owner 提取的金额。
function allowance(address _owner, address _spender) public override view returns (uint256 remaining) {
return allowed[_owner][_spender];
}
// 内部方法 给地址 _to 初始化数量 _amount 数量的 tokens,注意 onlyOwner 修饰,只有合约创建者才有权限分配 分配会增加可发行总代币量,如果代币总量为0也可以增量发行
function _mint(address _to, uint256 _amount) internal {
require(_to != address(0));
_totalSupply = _totalSupply.add(_amount);
balances[_to] = balances[_to].add(_amount);
emit Transfer(address(0), _to, _amount);
}
// 内部方法 销毁一定数量的令牌
function _burn(address account, uint256 amount) internal {
require(account != address(0));
require(amount <= balances[account]);
_totalSupply = _totalSupply.sub(amount);
balances[account] = balances[account].sub(amount);
emit Transfer(account, address(0), amount);
}
// 内部方法 从津贴中销毁一定数量的令牌
function _burnFrom(address account, uint256 amount) internal {
require(amount <= allowed[account][msg.sender]);
// Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
// this function needs to emit an event with the updated approval.
allowed[account][msg.sender] = allowed[account][msg.sender].sub(amount);
_burn(account, amount);
}
}
上方代码有三个内部方法,因为方法的操作内容比较敏感,所以控制为内部方法,然后使用权限控制调用是否允许,下面编写内部方法的对外调用接口
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
import { TokenPET } from "../contracts/TokenPET.sol";
import { Ownable } from "../library/Ownable.sol";
contract TokenPETMintable is TokenPET, Ownable {
event MintingFinished();
bool private _mintingFinished = false;
modifier onlyBeforeMintingFinished() {
require(!_mintingFinished);
_;
}
/**
* @return true if the minting is finished.
*/
function mintingFinished() public view returns(bool) {
return _mintingFinished;
}
/**
* @dev Function to mint tokens
* @param to The address that will receive the minted tokens.
* @param amount The amount of tokens to mint.
* @return A boolean that indicates if the operation was successful.
*/
function mint(
address to,
uint256 amount
)
public
onlyMinter
onlyBeforeMintingFinished
returns (bool)
{
_mint(to, amount);
return true;
}
/**
* @dev Function to stop minting new tokens.
* @return True if the operation was successful.
*/
function finishMinting()
public
onlyMinter
onlyBeforeMintingFinished
returns (bool)
{
_mintingFinished = true;
emit MintingFinished();
return true;
}
}
npx hardhat compile
---------------------
# 编译成功最后输出如下
> Compilation finished successfully
const { expect } = require("chai");
// 这里的PET是测试实例名
describe("PET", function() {
// 测试描述
it("Should return the new PET once it's changed", async function() {
// 获取编译的智能合约TokenPET,这个名是sol合约文件的类名,就是contracts后面的名字,不是文件名
const TokenPET = await ethers.getContractFactory("TokenPET");
const pet = await TokenPET.deploy();
await pet.deployed();
// 断言判断
expect(await pet.name()).to.equal("PETurrency");
expect(await pet.symbol()).to.equal("PET");
});
});
npx hardhat test
-------------------------
# 输出如下说明成功
PET
√ Should return the new PET once it's changed (166ms)
1 passing (901ms)
hre.ethers.getContractFactory
的参数是合同名,就是智能合约代码中contracts后的名,不是合约文件名const hre = require("hardhat");
async function main() {
const TokenPET = await hre.ethers.getContractFactory("TokenPETMintable");
const pet = await TokenPET.deploy();
await pet.deployed();
console.log("TokenPET symbol is:", await pet.symbol());
console.log("TokenPET deployed to:", pet.address);
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
# scripts/pet-deploy-script.js 为写好的部署合同脚本,想运行哪个执行哪个
# --network localhost 指发布到本地网络,之后讲发布到测试链或私链
npx hardhat run scripts/pet-deploy-script.js --network localhost
TokenPET symbol is: PET
TokenPET deployed to: 0x9fE46736679d2D9a65F0992F2……
pet-using.js
//初始化过程
var Web3 = require('web3');
var Contract = require('web3-eth-contract');
// 编译后的文件 自行定位文件修改路径
var TokenPETMintable = require("../artifacts/contracts/TokenPETMintable.sol/TokenPETMintable.json");
var abi = TokenPETMintable.abi;
// 发布后合约的地址
var address = '0xa85233C63b9Ee964Add6F2cffe00……';
// 合约发布的链网络
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
// 创建合同对象
var contract = new web3.eth.Contract(abi, address);
// 方法调用
contract.methods.totalSupply().call().then(function(totalSupply){
console.log('totalSupply result', totalSupply)
});
// 查询发币之前的账户余额 钱包中所选的创世用户也可以
contract.methods.balanceOf('0xf39Fd6e51aad88F6F4ce6aB88272……').call().then(function(balanceOf){
console.log('Before balanceOf result', balanceOf)
});
// 调用mint方法向账户发行代币
var mint = contract.methods.mint('0xf39Fd6e51aad88F6F4ce6aB88272……', 999);
// from需要是所有者,因为发布的时候没有选择用户,默认是创世用户第一个,才可以向想合约申请给用户发币
mint.send({from: '0xf39Fd6e51aad88F6F4ce6aB88272……'}).then(function(receipt){
// var result = mint.call().then(function(receipt) {
console.log('call result', receipt)
// 查询发币之后的账户余额 钱包中所选的创世用户也可以
contract.methods.balanceOf('0xf39Fd6e51aad88F6F4ce6aB88272……').call().then(function(balanceOf){
console.log('After balanceOf result', balanceOf)
});
}).catch((error, receipt) => {
console.error('error === ', error);
process.exit(1);
});
npx hardhat run .\scripts\pet-using.js
# Before balanceOf result 0
# call result中status: true则为交易成功
# After balanceOf result 999
到此,您已经在本地创建了一个项目,运行了一个HardHat任务,开发、编译了一个智能合约,编写并运行了一个测试,并部署了一个合同,远程调用了已部署合约的方法,对合约及账户进行了操作。
下面说将该代币发布到私有、公开区块链网络。
以下发布流程需要是本地测试、部署没有问题再进行,本文介绍的只是其中一种方式,并非唯一发布方式,使用其他方式的如果有疑问可以留言
访问合约发布网站myetherwallet(需要准备好前文说的MetaMask)
授权成功后,钱包管理网页应跳转至如下,这个页面左侧用户信息,注意要是私链或测试链中的用户地址信息,并且有一定的ETH,部署合约需要花费ETH,测试链可以在响应链的水龙头申请,然后之后的操作保证MetaMask选择用户是有ETH的这个用户
填写构建的合约信息(构建流程见上文),查看artifacts->contracts->TokenPETMintable.sol->TokenPETMintable.json文件内容,按照下图对应关系填入
然后MetaMask会提示确认交易(需要由报酬,确认账户中有ETH,可以在水龙头申请一些ETH用来测试,否则个人的私网也没法处理本次工作,私链就自己发币就行了),点击确认,点击提交,在metamask 插件中或myetherwallet可以看到处理状态,然后等待交易完成
点击交易查看详情,可以跳转到以太坊区块链浏览器查看信息,如果是私链则线上的区块链浏览器无法查看,因为区块链浏览器没有你私链的连接信息和api(可能需要)
在metamask连接私链或测试链,是刚刚发布代币的链就可以,选择添加代币,和之前说的添加本地网络代币步骤一样,就是代币合约地址使用上一步查看的合约地址,然后这里就多了另一个刚刚发布的币,可以进行交易
18. 交易成功后,到MetaMask查看用户的PET余额,可以看到已经有余额了,这里因为我设置的合约小数点为8位,所以交易金额都是以小数点后8位开始
GitHub源码参考:https://github.com/Aubrey-J/PET-ERC20