Ethereum Smart Contract Development
Table of Contents
- 1. 智能合约开发
- 2. web3.js
- 3. Truffle
- 4. OpenZeppelin
- 5. Golang
- 6. Foundry
- 6.1. cast
- 6.1.1. 指定 rpc 的不同方式
- 6.1.2. call and send(调用合约)
- 6.1.3. 本地模拟执行 Tx(cast call --trace)
- 6.1.4. balance(查询 ETH/ERC20 余额)
- 6.1.5. mktx(构造原始交易)
- 6.1.6. publish(发送原始交易)
- 6.1.7. rpc(调用 rpc 方法)
- 6.1.8. decode-tx(解码交易)
- 6.1.9. calldata and decode-calldata(编码和解码 calldata,带 4byte)
- 6.1.10. wallet
- 6.1.11. sig and 4byte(函数 4byte 签名的计算和反查)
- 6.1.12. keccak(计算 keccak 哈希)
- 6.1.13. address-zero
- 6.1.14. to-checksum(可简写 ta,转换为 checksum 地址)
- 6.1.15. to-uint256 and to-dec(十进制和 uint256 之前进行转换)
- 6.1.16. to-ascii and from-utf8(字符串和 hex data 转换)
- 6.1.17. to-unit(在 ether, gwei, wei 之间任意转换)
- 6.1. cast
- 7. Hardhat
- 8. Remix
1. 智能合约开发
开发 Ethereum 智能合约 的基本流程如下:
- 启动一个以太坊节点 (例如 geth 或者 testrpc),或者直接使用第三方(如 Infura)提供的服务。
- 编写智能合约,以太坊的智能合约一般使用 Solidity 语言(语法类似 JavaScript 的高级语言)来编写。
- 使用
solc编译智能合约,可以获得部署合约时所要使用的 Ethereum Virtual Machine (EVM) bytecode 和 abi。 - 将编译好的合约部署到测试网络。
- 调用和测试合约。
后面将分别介绍使用 web3.js、Truffle、OpenZeppelin、Golang 等进行 Ethereum 智能合约开发的基本过程。
1.1. Ganache 测试链
在开发合约时,我们可以部署代码到以太坊的测试网上。我们还有更好的办法,直接在本地搭建完整的测试以太链。Ganache 就是这样一个项目,使用它可以非常地容易搭建一个测试链。
第一步,安装 ganache 命令行工具:
$ npm install --save-dev ganache-cli
第二步,启动 ganache 测试链:
$ npx ganache-cli --deterministic
Ganache CLI v6.9.1 (ganache-core: 2.10.2)
Available Accounts
==================
(0) 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 (100 ETH)
(1) 0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0 (100 ETH)
(2) 0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b (100 ETH)
(3) 0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d (100 ETH)
(4) 0xd03ea8624C8C5987235048901fB614fDcA89b117 (100 ETH)
(5) 0x95cED938F7991cd0dFcb48F0a06a40FA1aF46EBC (100 ETH)
(6) 0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9 (100 ETH)
(7) 0x28a8746e75304c0780E011BEd21C72cD78cd535E (100 ETH)
(8) 0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E (100 ETH)
(9) 0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e (100 ETH)
Private Keys
==================
(0) 0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
(1) 0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1
(2) 0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c
(3) 0x646f1ce2fdad0e6deeeb5c7e8e5543bdde65e86029e2fd9fc169899c440a7913
(4) 0xadd53f9a7e588d003326d1cbf9e4a43c061aadd9bc938c843a79e7b4fd2ad743
(5) 0x395df67f0c2d2d9fe1ad08d1bc8b6627011959b79c53d7dd6a3536a33ab8a4fd
(6) 0xe485d098507f54e7733a205420dfddbe58db035fa577fc294ebd14db90767a52
(7) 0xa453611d9419d0e56f499079478fd72c37b251a94bfde4d19872c44cf65386e3
(8) 0x829e924fdf021ba3dbbc4225edfece9aca04b929d6e75613329ca6f1d31c0bb4
(9) 0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773
HD Wallet
==================
Mnemonic: myth like bonus scare over problem client lizard pioneer submit female collect
Base HD Path: m/44'/60'/0'/0/{account_index}
Gas Price
==================
20000000000
Gas Limit
==================
6721975
Call Gas Limit
==================
9007199254740991
Listening on 127.0.0.1:8545
启动后,服务默认监听在“127.0.0.1:8545”(通过选项 -h 和 -p 可以修改监听了地址和端口)。默认创建了十个帐户,且每个帐户里有 100 个以太。
2. web3.js
web3.js 是一个 Javascript 库,使用它可以和以太节点进行交互。
2.1. 编写合约
下面是以太坊智能合约的一个例子(假设文件名为 test.sol):
pragma solidity >=0.4.0 <0.6.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}
2.2. 编译合约
使用 web3 来部署智能合约时,需要先获得合约的 EVM bytecode 以及合约的 abi。可以直接在网络 https://ethereum.github.io/browser-solidity/ 上进行 online 编译;也可以安装 solc 后自己编译,安装 solc 的步骤:
$ npm install -g solc # 成功安装后,会得到可执行程序solcjs
下面是使用 solcjs 编译上节例子中的 test.sol,得到其 EVM bytecode 以及 abi 的例子:
$ solcjs --bin test.sol #### 获得EVM bytecode,保存在文件test_sol_SimpleStorage.bin中
$ cat test_sol_SimpleStorage.bin
608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a72305820fc40facace75159ce9071fb0b9c2ef08afa7650019cf3276f911c3a1cc29eb390029
$ solcjs --abi test.sol #### 获的abi,保存在文件test_sol_SimpleStorage.abi中
$ cat test_sol_SimpleStorage.abi
[{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
2.3. 部署合约
在 web3.js 1.0 前,调用合约的 new()方法就可以部署合约;在 web3.js 1.0 中,部署合约分为两步:
1、创建合约(新建一个 web3.eth.Contract 类的实例);
2、发布合约。
在发布合约时要调用 myContract.deploy().send()。对于 deploy() 而言,如果你的合约的构造函数是带参数的,则调用 deploy() 函数也要把参数带上。调用 send() 的时候需要指定从哪个地址扣费(from 选项),以及燃料多少(gas 选项)。 send() 函数比较有意思的是,它返回的不光是一个 Promise,还是一个 EventEmiter,因而可以监听发布过程中的一些事件:
- transactionHash - 生成了交易 hash 的事件,一般只会触发一次;
- receipt - 合约已经打包到区块上了,一般只触发一次,这时就已经可以调用这个合约了;
- confirmation - 合约已经打包到区块链上并收到了确认,每次确认都会触发一次,最多到第 24 次确认;
- error - 发布合约失败。
下面是部署前面合约的例子:
var Web3 = require("web3");
var web3 = new Web3();
// 连接到以太坊节点
web3.setProvider(new Web3.providers.HttpProvider("http://localhost:8545"));
console.log('web3 version=' + web3.version);
// 下面contractAbi是通过solcjs编译得到的
contractAbi = [{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
// 下面contractBytecode是通过solcjs编译得到的
contractBytecode = '0x' + '608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a72305820fc40facace75159ce9071fb0b9c2ef08afa7650019cf3276f911c3a1cc29eb390029'
// 创建合约
const myContract = new web3.eth.Contract(contractAbi, {data: contractBytecode})
// 部署合约的账号(部署合约时需要消耗gas,会从这个账号中扣除)
const deployAddr = '0xe4F394093E0Dc30d28A216dAEce79A6D0D537042'
// 上面账户的密码(这个密码是生成或导入帐号时指定的)
const password = '12345678'
// 解锁账号
try {
web3.eth.personal.unlockAccount(deployAddr, password, 600);
} catch(e) {
console.log(e);
return;
}
// 发布合约
myContract.deploy()
.send({from: deployAddr, // 指定部署合约的帐号
gasPrice: 6000000000, // 指定gas price
gas: 112149 // 指定gas值
}) // send() returns a Promise & EventEmit
.on('transactionHash', function(transactionHash){
console.log("deploy transaction hash: ", transactionHash)
})
.on('receipt', function(receipt){
console.log("deploy receipt: ", receipt)
})
.on('confirmation', function(confirmationNum, receipt){
console.log("got confirmations number: ", confirmationNum)
})
.then(function(myContractInstance){
console.log("deployed successfully.")
console.log("myContractInstance.options.address = ", myContractInstance.options.address)
})
.catch(err => {
console.log("Error: failed to deploy, detail:", err)
})
运行部署合约的程序,可能得到下面输出:
web3 version=1.0.0-beta.36
deploy transaction hash: 0xad3b569955ce8ca4be421adb2c5cba366791d2ba9944f4e0966fd54f708f4ebe
got confirmations number: 0
deploy receipt: { blockHash: '0x95875d8749353dd22ddd51ee005e23a8bce75503272615c286c520547b2dc852',
blockNumber: 4241261,
contractAddress: '0x92021f848472af2fD85c0b8Ef6Ea89CeDc6294B0',
cumulativeGasUsed: 225499,
from: '0xe4f394093e0dc30d28a216daece79a6d0d537042',
gasUsed: 112149,
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
status: true,
to: null,
transactionHash: '0xad3b569955ce8ca4be421adb2c5cba366791d2ba9944f4e0966fd54f708f4ebe',
transactionIndex: 4,
events: {} }
deployed successfully.
myContactInstance.options.address = 0x92021f848472af2fD85c0b8Ef6Ea89CeDc6294B0
got confirmations number: 1
got confirmations number: 2
got confirmations number: 3
got confirmations number: 4
got confirmations number: 5
got confirmations number: 6
got confirmations number: 7
got confirmations number: 8
got confirmations number: 9
got confirmations number: 10
got confirmations number: 11
got confirmations number: 12
got confirmations number: 13
got confirmations number: 14
got confirmations number: 15
got confirmations number: 16
got confirmations number: 17
got confirmations number: 18
got confirmations number: 19
got confirmations number: 20
got confirmations number: 21
got confirmations number: 22
got confirmations number: 23
got confirmations number: 24
在上面输出中,我们可以看到新合约的地址为: 0x92021f848472af2fD85c0b8Ef6Ea89CeDc6294B0 ,后面会用到这个地址。
注意:前面代码中,设置了 gasPrice 为 6000000000,gas 为 112149;但这两个值并不是固定的,可以通过下面代码获得:
web3.eth.getGasPrice().
then((averageGasPrice) => {
console.log("Average gas price: " + averageGasPrice); // 输出实例:6000000000
}).
catch(function(err) {
console.log(err);
});
myContract.deploy().estimateGas().
then((estimatedGas) => {
console.log("Estimated gas: " + estimatedGas); // 输出实例:112149
}).
catch(function(err) {
console.log(err);
});
2.4. 调用合约
myContract.methods.myMethod([param1[, param2[, ...]]]).call(options[, callback]) myContract.methods.myMethod([param1[, param2[, ...]]]).send(options[, callback])
其中 call() 适用于 constant 类型的方法(如前面例子中的 get 方法),而后者 send() 适用于其他方法(有交易或会修改 storage 的方法,如前面例子中的 set 方法)。
call() 的返回值(Promise 解决后)就是合约中方法的返回值,然而 send() 的返回值(Promise 解决后)是这次交易的信息,包括 transactionHash 和 blockHash 等,即使合约中方法有返回值也将被忽略。所以,非 constant 类型的方法建议不要有返回值,出错了就直接抛异常挂掉;如果真的想返回什么,建议通过事件的方式来通知调用者。
2.4.1. send(适用于非 constant 类型的方法)
下面是调用前面合约中 set 方法(它不是 constant 类型的方法,需要使用 send() 来调用)的实例:
var Web3 = require("web3");
var web3 = new Web3();
// 连接到以太坊节点
web3.setProvider(new Web3.providers.HttpProvider("http://localhost:8545"));
contractAbi = [{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
// 合约地址
const contractAddress = '0x92021f848472af2fD85c0b8Ef6Ea89CeDc6294B0'
// 创建合约(使用已经存在的合约,需要指定abi和合约地址)
const myContract = new web3.eth.Contract(contractAbi, contractAddress)
// 调用合约的账号(调用合约时需要消耗gas,会从这个账号中扣除)
const userAddr = '0xe4F394093E0Dc30d28A216dAEce79A6D0D537042'
const password = '12345678'
// 解锁账号
try {
web3.eth.personal.unlockAccount(userAddr, password, 600);
} catch(e) {
console.log(e);
return;
}
// 调用合约myContract的set方法
myContract.methods.set(20)
.send({from: userAddr,
gasPrice: 6000000000, // 指定gas price
gas: 112149 // 指定gas值
}) // send() returns a Promise & EventEmit
.on('transactionHash', function(transactionHash){
console.log("transaction hash: ", transactionHash)
})
.on('receipt', function(receipt){
console.log("receipt: ", receipt)
})
.on('confirmation', function(confirmationNum, receipt){
console.log("got confirmations number: ", confirmationNum)
})
.then(function(myContractInstance) {
console.log(myContractInstance)
})
.catch(err => {
console.log("Error: failed to call set method, detail:", err)
})
2.4.2. call(适用于 constant 类型的方法)
下面是调用前面合约中 get 方法(它是 constant 类型的方法,可以使用 call() 来调用)的实例:
var Web3 = require("web3");
var web3 = new Web3();
// 连接到以太坊节点
web3.setProvider(new Web3.providers.HttpProvider("http://localhost:8545"));
contractAbi = [{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
// 合约地址
const contractAddress = '0x92021f848472af2fD85c0b8Ef6Ea89CeDc6294B0'
// 创建合约(使用已经存在的合约,需要指定abi和合约地址)
const myContract = new web3.eth.Contract(contractAbi, contractAddress)
// 调用合约myContract的get方法
myContract.methods.get()
.call()
.then(console.log) // 输出get方法的返回值,如20
.catch(err => {
console.log("Error: failed to call method get, detail:", err)
})
3. Truffle
3.1. 初始化工程
执行 truffle init 可以初始化 truffle 工程:
$ truffle init
命令成功执行完后,会生成下面目录:
contracts/: Directory for Solidity contracts migrations/: Directory for scriptable deployment files test/: Directory for test files for testing your application and contracts truffle-config.js: Truffle configuration file
3.2. 编写合约
按照 truffle 工程的规范,合约文件需要保存在 contracts 子目录中。
假设有合约 contracts/Counter.sol,代码如下:
pragma solidity >=0.4.21 <0.7.0;
contract Counter {
uint256 public value;
function increase(uint256 amount) public {
value += amount;
}
}
3.3. 编译合约
执行 truffle compile 可以编译合约。默认是增量编译,增加选项 --all 可以进行全新编译,如:
$ truffle compile --all Compiling your contracts... =========================== > Compiling ./contracts/Counter.sol > Compiling ./contracts/Migrations.sol > Artifacts written to /Users/cig01/proj/dapp/build/contracts > Compiled successfully using: - solc: 0.5.16+commit.9c3226ce.Emscripten.clang
3.4. 部署合约
在部署合约前,首先需要有个以太测试节点。可以使用 Ganache(参考节 1.1)在本地搭建,或者使用 Infura 的服务连接到 Ropsten 等测试网。
执行 Truffle 的 migrate (或者它的别名 deploy )子命令可以依次执行子目录 migrations 中的脚本:
$ truffle migrate # 连接 127.0.0.1:7545,请检查 truffle-config.js 中的配置 $ truffle deploy # 同上。注 deploy 是 migrate 的别名 $ truffle deploy --network ropsten # 需要在 truffle-config.js 中配置网络
下面是在 truffle-config.js 中增加 ropsten 网络配置的例子:
const HDWalletProvider = require('@truffle/hdwallet-provider');
module.exports = {
networks: {
......
ropsten: {
provider: () => new HDWalletProvider(mnemonic or privateKey, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),
network_id: 3, // Ropsten's id
gas: 5500000, // Ropsten has a lower block limit than mainnet
confirmations: 2, // # of confs to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
}
},
......
}
3.4.1. 为新合约编写部署脚本
truffle deploy 只是执行子目录 migrations 中的脚本来完成部署。所以对于新增加的合约(如 Counter.sol),需要编写相应的脚本。
在初始化工程时,会自动生成 contracts/Migrations.sol 和 migrations/1_initial_migration.js。
const Migrations = artifacts.require("Migrations");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};
以 migrations/1_initial_migration.js 为模板,我们创建文件 migrations/2_initial_counter.js,内容如下:
const Counter = artifacts.require("Counter");
module.exports = async function(deployer, network, accounts) {
const deployAccount = accounts[0];
console.log("Deploying from account:" + deployAccount, 'network:', network);
await deployer.deploy(Counter);
console.log('The contract address:', Counter.address);
};
这样,在执行 truffle deploy 时,就可以完成对新合约 contracts/Counter.sol 的部署了。
参考:https://www.trufflesuite.com/docs/truffle/getting-started/running-migrations
3.4.2. 指定执行某个部署脚本
目录 migrations 中的部署脚本都以数字为前缀命名的,通过指定 --f <num> 和 --to <num> 可以分别指定开始和结束脚本的数字前缀。如,仅执行以 3 为前缀的脚本:
$ truffle deploy --f 3 --to 3 --network ropsten
3.5. 调用合约
使用 truffle console 和 truffle develop 都可以启动一个交互式控制台,它们的区别在于 truffle console 是连接一个已存在的网络,而 truffle develop 会自己启动一个测试网络(即它可以代替 Ganache 等)。
打开交互式控制台:
$ truffle console $ truffle console --network ropsten # 需要在 truffle-config.js 中配置网络
在控制台中得到 Counter 合约的实例:
$ truffle(ganache)> const instance = await Counter.deployed()
在控制台中调用 Counter 合约的 increase() 方法,如:
$ truffle(ganache)> instance.increase(2)
{ tx:
'0xb21f4745b6daaef1af7d4f2e9df57f5863127a025958bce0d0fd028e114de202',
receipt:
{ transactionHash:
'0xb21f4745b6daaef1af7d4f2e9df57f5863127a025958bce0d0fd028e114de202',
transactionIndex: 0,
blockHash:
'0x4a055728a3060db29ad0712555d010a6b3be825b2ab4563f475343b977e83ef8',
blockNumber: 5,
from: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1',
to: '0xcfeb869f69431e42cdb54a4f4f105c19c080a601',
gasUsed: 42243,
cumulativeGasUsed: 42243,
contractAddress: null,
logs: [],
status: true,
logsBloom:
'0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
rawLogs: [] },
logs: [] }
在控制台中调用 Counter 合约的 value() 方法,如:
$ truffle(ganache)> instance.value() <BN: 2>
参考:https://www.trufflesuite.com/docs/truffle/getting-started/using-truffle-develop-and-the-console
3.6. 调试合约(truffle debug)
执行 truffle debug 可以调试一个已经执行过的 tx,如:
$ truffle debug 0xb21f4745b6daaef1af7d4f2e9df57f5863127a025958bce0d0fd028e114de202
Starting Truffle Debugger...
✓ Gathering information about your project and the transaction...
Addresses called: (not created)
0xCfEB869F69431e42cdB54A4F4f105C19C080A601 - Counter
Commands:
(enter) last command entered (step next)
(o) step over, (i) step into, (u) step out, (n) step next
(;) step instruction (include number to step multiple)
(p) print instruction & state (can specify locations, e.g. p mem; see docs)
(l) print additional source context, (s) print stacktrace, (h) print this help
(q) quit, (r) reset, (t) load new transaction, (T) unload transaction
(b) add breakpoint, (B) remove breakpoint, (c) continue until breakpoint
(+) add watch expression (`+:<expr>`), (-) remove watch expression (-:<expr>)
(?) list existing watch expressions and breakpoints
(v) print variables and values, (:) evaluate expression - see `v`
Counter.sol:
1: pragma solidity >=0.4.21 <0.7.0;
2:
3: contract Counter {
^^^^^^^^^^^^^^^^^^
在上面输出中显示了调试器支持的命令。常用的 debug 命令如表 1 所示。
| Command | 含义 |
|---|---|
| n | 执行下一条语句 |
| v | 查看变量和对应的值 |
| r | 重新执行 tx |
| h | 查询帮助 |
| q | 退出调试器 |
下面演示了基本的调试过程:
debug(development:0xb21f4745...)> n # n 执行下一条语句
Counter.sol:
4: uint256 public value;
5:
6: function increase(uint256 amount) public {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # ^ 表示正在执行的语句
....... # 这里省略好几个 n 命令
debug(development:0xb21f4745...)> v # v 查看变量和对应的值
amount: 2
value: 2
msg: { data:
hex'30f3f0db0000000000000000000000000000000000000000000000000000000000000002',
sig: 0x30f3f0db,
sender:
0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1,
value: 0 }
tx: { origin:
0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1,
gasprice: 20000000000 }
block: { coinbase:
0x0000000000000000000000000000000000000000,
difficulty: 0,
gaslimit: 6721975,
number: 5,
timestamp: 1595384174 }
this: 0xCfEB869F69431e42cdB54A4F4f105C19C080A601 (Counter)
now: 1595384174
参考:https://www.trufflesuite.com/docs/truffle/getting-started/debugging-your-contracts
4. OpenZeppelin
OpenZeppelin 提供了一些安全的智能合约库(如 Safemath )及命令行工具,帮助开发者更快速地开发合约。
4.1. 安装命令行工具
OpenZeppelin 提供命令行工具帮助开发者快速地编译部署合约。
安装 OpenZeppelin 命令行工具:
$ npm install @openzeppelin/cli
4.2. 初始化工程
执行 npx oz init 可以初始化工程:
$ npx openzeppelin init # 或者简写为 npx oz init
交互式地输入完工程名称和版本后,会创建 .openzeppelin 目录,以及 networks.js 文件。
networks.js 保存着网络的配置,其默认内容如下:
module.exports = {
networks: {
development: {
protocol: 'http',
host: 'localhost',
port: 8545,
gas: 5000000,
gasPrice: 5e9,
networkId: '*',
},
},
};
上面定义了一个名为 development 的网络,默认配置(“localhost:8545”)适用于 ganache 命令行启动的测试链。
4.2.1. 连接公开测试网
除了使用 ganache 构造的本地测试链外,我们也可以使用公开的以太测试链。把测试链相关信息增加到 networks.js 文件中即可,如增加 rinkeby 和 kovan 测试链:
const { projectId, mnemonic } = require('./secrets.json');
const HDWalletProvider = require('@truffle/hdwallet-provider');
module.exports = {
networks: {
development: {
protocol: 'http',
host: 'localhost',
port: 8545,
gas: 5000000,
gasPrice: 5e9,
networkId: '*',
},
rinkeby: {
provider: () => new HDWalletProvider(
mnemonic, `https://rinkeby.infura.io/v3/${projectId}`
),
networkId: 4, // for rinkeby
gasPrice: 10e9
},
kovan: {
provider: () => new HDWalletProvider(
mnemonic, `https://kovan.infura.io/v3/${projectId}`
),
networkId: 42, // for kovan
gasPrice: 10e9
},
},
};
其中,文件 secrets.json 保存着帐户助记词和 Infura 的 projectId,其例子如下:
{
"mnemonic": "mistake pelican mention moment ...",
"projectId": "305c13705054a8d918ad77549e402c72"
}
参考:https://docs.openzeppelin.com/learn/connecting-to-public-test-networks
4.3. 编写合约
项目中创建文件 contracts/Counter.sol,内容如下:
// contracts/Counter.sol
pragma solidity ^0.6.0;
contract Counter {
uint256 public value;
function increase() public {
value++;
}
}
4.4. 编译合约
执行 npx oz compile 可以编译合约,如:
$ npx oz compile # 编译合约 ✓ Compiled contracts with solc 0.6.3 (commit.8dda9521)
4.5. 部署合约
执行 npx oz create 可以部署合约,执行时会询问你部署哪个合约,以及部署到哪个网络(networks.js 中配置的网络都可以选择)中,如:
$ npx oz create # 部署合约 Nothing to compile, all contracts are up to date. ? Pick a contract to instantiate Counter ? Pick a network development ✓ Added contract Counter ✓ Contract Counter deployed All implementations have been deployed ? Call a function to initialize the instance after creating it? No ✓ Setting everything up to create contract instances ✓ Instance created at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601 0xCfEB869F69431e42cdB54A4F4f105C19C080A601
注:OpenZeppelin 支持“升级”合约,上面部署过程中包含了三个合约,一个是用户的业务合约(也就是 Counter.sol),其它两个则是 OpenZeppelin 的辅助合约。部署成功后显示的合约地址是 OpenZeppelin 辅助合约的地址(这个地址在业务合约升级后也是不变的)。
4.6. 调用合约
执行 npx oz send-tx 可以发送交易,调用合约中的函数,下面例子将调用合约 Counter 中的函数 increase() :
$ npx oz send-tx ? Pick a network development ? Pick an instance Counter at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601 ? Select which function increase() ✓ Transaction successful. Transaction hash: 0xd4defba8d14018c00a484473552f3b9f5ba290c1c2d57759f6c99527816f3f57
执行 npx oz call 可以调用合约中的查询函数,下面例子将调用合约 Counter 中的函数 value() :
$ npx oz call ? Pick a network development ? Pick an instance Counter at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601 ? Select which function value() ✓ Method 'value()' returned: 1 1
4.7. 修改合约
下面我们把合约的 increase() 函数“增强”一下,让其可以接收一个参数,如下:
// contracts/Counter.sol
pragma solidity ^0.6.0;
contract Counter {
uint256 public value;
function increase(uint256 amount) public {
value += amount;
}
}
4.8. 升级合约
执行 npx oz upgrade 可以升级合约,如:
$ npx oz upgrade ? Pick a network: development ✓ Compiled contracts with solc 0.6.3 (commit.8dda9521) ✓ Contract Counter deployed ? Which proxies would you like to upgrade?: All proxies Instance upgraded at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601.
下面调用 increase(10) 和 value() 以验证其升级成功:
$ npx oz send-tx ? Pick a network: development ? Pick an instance: Counter at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601 ? Select which function: increase(amount: uint256) ? amount (uint256): 10 Transaction successful: 0x9c84faf32a87a33f517b424518712f1dc5ba0bdac4eae3a67ca80a393c555ece $ npx oz call ? Pick a network: development ? Pick an instance: Counter at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601 ? Select which function: value() Returned "11"
5. Golang
这里简单介绍下用 Go 进行以太坊开发的基本过程。
5.1. 编写合约
项目中创建文件 contracts/Counter.sol,内容如下:
pragma solidity >=0.4.21 <0.7.0;
contract Counter {
uint256 public value;
function increase(uint256 amount) public {
value += amount;
}
}
5.2. 编译合约
$ npm install -g solc # 安装 solidity 编译器 solcjs $ go install 'github.com/ethereum/go-ethereum/cmd/abigen@latest' # 安装 abigen
第一步,使用 solidity 编译器 solc(solcjs)编译合约,同时得到对应的 abi 文件:
$ cd contracts $ solcjs --abi --bin --optimize Counter.sol $ ls Counter.sol Counter_sol_Counter.abi Counter_sol_Counter.bin
第二步,利用 abigen,根据上一步生成的 Counter_sol_Counter.abi 和 Counter_sol_Counter.bin 生成 golang 辅助文件 counter.go:
$ abigen --abi=Counter_sol_Counter.abi --bin=Counter_sol_Counter.bin --pkg=contracts --type=Counter --out=counter.go
文件 counter.go 中包含了开发所要用到的方法,后面介绍的部署合约、加载合约、调用合约都会用到 counter.go 中生成的方法。
5.3. 部署合约
生成的 Go 合约文件 counter.go 中提供了部署方法。 部署方法名称始终以单词 Deploy 开头,后跟合约名称,在这个例子中部署合约的方法为 DeployCounter。
下面是部署 Counter 合约的关键代码:
client, err := ethclient.Dial("https://rinkeby.infura.io/v3/your-project-id")
auth := bind.NewKeyedTransactor(privateKey)
address, tx, instance, err := contracts.DeployCounter(auth, client)
5.4. 加载合约
当我们要调用合约方法前,先要得到合约实例,这个过程称为加载合约。
生成的 Go 合约文件 counter.go 中提供了加载合约的方法。 总是以单词 New 开头,后跟合约名称,这个例子中为 NewCounter。
加载合约时,需要提供合约的部署地址,这样调用合约时才知道调用的是哪个地址下的合约。
下面是加载 Counter 合约的关键代码,假设合约的部署地址为 0x147B8eb97fD247D06C4006D269c90C1908Fb5D54:
client, err := ethclient.Dial("https://rinkeby.infura.io/v3/your-project-id")
address := common.HexToAddress("0x147B8eb97fD247D06C4006D269c90C1908Fb5D54")
instance, err := contracts.NewCounter(address, client)
5.5. 调用合约
加载合约后,有了合约实例 instance,直接调用 instance 上的方法即可调用合约中的方法。
下面是调用 Counter 合约方法的关键代码:
// 调用合约中的 Value 方法(这个方法不会改变合约的状态) value, err := instance.Value(nil) // 调用合约中的 Increase 方法(这个方法变改变合约的状态) auth := bind.NewKeyedTransactor(privateKey) tx, err := instance.Increase(auth, big.NewInt(10))
5.6. 完整代码
整个项目结构如下:
example ├── contracts │ ├── Counter.sol │ ├── Counter_sol_Counter.abi │ ├── Counter_sol_Counter.bin │ └── counter.go ├── go.mod ├── go.sum └── main.go
文件 go.mod 为:
$ cat go.mod module example go 1.13 require github.com/ethereum/go-ethereum v1.9.6
前面介绍的部署、加载、调用合约的完整 golang 代码(main.go)如下:
package main
import (
"context"
"crypto/ecdsa"
"fmt"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"log"
"math/big"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"example/contracts" // example 是当前 go module 的名称,导入 example 的 contracts 子目录
)
func checkErr(err error) {
if err != nil {
panic(err)
}
}
func deployCounter(client *ethclient.Client, privateKey *ecdsa.PrivateKey) string {
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
checkErr(err)
gasPrice, err := client.SuggestGasPrice(context.Background())
checkErr(err)
auth := bind.NewKeyedTransactor(privateKey)
auth.Nonce = big.NewInt(int64(nonce))
auth.Value = big.NewInt(0) // in wei
auth.GasLimit = uint64(300000) // in units
auth.GasPrice = gasPrice
// 部署合约
address, tx, _, err := contracts.DeployCounter(auth, client)
checkErr(err)
log.Printf("deploy at address %v", address.Hex()) // 0x147B8eb97fD247D06C4006D269c90C1908Fb5D54
log.Printf("tx (deploy): %v", tx.Hash().Hex())
// 检测是否已经被打包
err = checkTx(client, tx)
checkErr(err)
return address.String()
}
func loadCounter(client *ethclient.Client, address common.Address) *contracts.Counter {
instance, err := contracts.NewCounter(address, client)
checkErr(err)
return instance
}
func callValue(instance *contracts.Counter) {
// 查询合约
// 调用合约中的 Value 方法(这个方法不会改变合约的状态)
value, err := instance.Value(nil)
checkErr(err)
log.Printf("value: %v", value)
}
func callIncrease(client *ethclient.Client, privateKey *ecdsa.PrivateKey, instance *contracts.Counter) {
auth := bind.NewKeyedTransactor(privateKey)
// 写入合约(发送交易)
// 调用合约中的 Increase 方法(这个方法变改变合约的状态)
tx, err := instance.Increase(auth, big.NewInt(10))
checkErr(err)
log.Printf("tx (call increase): %s", tx.Hash().Hex())
// 检测是否已经被打包
err = checkTx(client, tx)
checkErr(err)
}
// checkTx 查询交易是否已经被节点打包
func checkTx(client *ethclient.Client, tx *types.Transaction) error {
retry:
rp, err := client.TransactionReceipt(context.Background(), tx.Hash())
if err != nil {
if err == ethereum.NotFound {
// 可能交易暂时未被节点打包(pending),这里需要重新查询
log.Printf("tx %v not found, check it later", tx.Hash().String())
time.Sleep(1 * time.Second)
goto retry
} else {
log.Fatalf("TransactionReceipt fail: %s", err)
return err
}
}
if rp.Status != types.ReceiptStatusSuccessful {
log.Fatalf("TransactionReceipt fail, status is not successful")
return fmt.Errorf("TransactionReceipt fail, status is not successful")
}
log.Printf("tx %v confirmed by node", tx.Hash().String())
return nil
}
func main() {
client, err := ethclient.Dial("https://rinkeby.infura.io/v3/your-project-id") // change it !
checkErr(err)
privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19") // change it !
checkErr(err)
// 部署 Counter 合约
addressHexStr := deployCounter(client, privateKey)
// 加载合约
address := common.HexToAddress(addressHexStr)
instance := loadCounter(client, address)
// 写入合约,调用 Counter 合约中的方法 Increase
callIncrease(client, privateKey, instance)
// 查询合约,调用 Counter 合约中的方法 Value
callValue(instance)
}
运行上面代码前,先需要正确地生成 contracts/counter.go 文件,参考节 5.2
6. Foundry
Foundry 是一个 Ethereum 智能合约开发框架,可用于构建、测试、调试和部署 Solidity 智能合约。Foundry 的优势是把 Solidity 当作第一公民,使用 Solidity 可直接编写测试用例。
Foundry 有 4 个组件:
- forge:合约开发框架,相当于 Truffle/Hardhat/DappTools;
- cast:链上交互工具;
- anvil:本地节点;
- chisel:交互式的 Solidity 环境。
安装 Foundry 开发工具:
$ curl -L https://foundry.paradigm.xyz | bash # 安装 foundryup $ foundryup # 通过 foundryup 安装工具 forge, cast, anvil, and chisel
常用 forge 命令如表 2 所示。
| 命令 | 作用 |
|---|---|
| forge init hello_foundry | 创建名为 hello_foundry 的项目 |
| forge build | 编译项目 |
| forge test | 运行项目测试用例 |
| forge test --match-test testFunc1 | 运行项目中的指定测试函数 |
| forge install | 安装依赖 |
| forge remove | 删除依赖 |
参考:https://book.getfoundry.sh/
下面是使用 forge init 初始化工程的示例:
$ forge init hello_foundry # 使用 init 初始化一个名为 hello_foundry 的项目 $ cd hello_foundry # 进入项目根目录 $ find . ./foundry.toml ./test/Counter.t.sol ./script/Counter.s.sol ./lib/forge-std ./src/Counter.sol ./README.md ./lib/forge-std/ # 这个目录中有很多文件,这里省略了
下面是使用 forge install 安装依赖的示例:
$ forge install OpenZeppelin/openzeppelin-contracts
6.1. cast
cast 是一个和 EVM 链进行交互的命令行工具。
6.1.1. 指定 rpc 的不同方式
有下面几种方式都可以指定 rpc:
方式一:通过环境变量 ETH_RPC_URL 指定,比如 export ETH_RPC_URL=https://ethereum-rpc.publicnode.com ;
方式一:通过命令行参数 --rpc-url https://ethereum-rpc.publicnode.com 直接指定;
方式二:rpc 具体保存在 foundry.toml 文件的 rpc_endpoints 配置中,而 --rpc-url 仅指定配置项的名称,比如 --rpc-url mainnet ,
下面是 foundry.toml 文件的例子:
[profile.default] src = "src" out = "out" libs = ["lib"] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options solc = "0.8.28" via_ir = true optimizer = true optimizer-runs = 200 [rpc_endpoints] mainnet = "https://ethereum-rpc.publicnode.com" base = "https://mainnet.base.org"
6.1.2. call and send(调用合约)
使用命令 cast call/send 可以分别调用合约的读/写方法:
cast call [OPTIONS] [TO] [SIG] [ARGS]... [COMMAND] # 调用合约的读方法 cast send [OPTIONS] [TO] [SIG] [ARGS]... [COMMAND] # 调用合约的写方法,需要指定 --private-key 等参数
比如:
$ cast call 0xdac17f958d2ee523a2206206994597c13d831ec7 'balanceOf(address)(uint256)' 0x5041ed759Dd4aFc3a72b8192C143F72f4724081A --rpc-url https://ethereum-rpc.publicnode.com 882397126783508 [8.823e14]
6.1.2.1. send(转移 ETH)
使用命令 cast send 可以转移 ETH。比如下面命令可以转移 0.1 ETH 到 0x8F2c5FA3487F72AA4363E507487D62FC50e57E0D:
$ cast send --value 0.1ether 0x8F2c5FA3487F72AA4363E507487D62FC50e57E0D --rpc-url https://ethereum-rpc.publicnode.com --private-key 0xf126953d34431b813f3e5c16d9fb7e46891d80ed9218f183dc56e2a564166a10
6.1.3. 本地模拟执行 Tx(cast call --trace)
执行命令 cast call 时如果指定 --trace 选项后,可以实现“Forks the remote rpc, executes the transaction locally and prints a trace”。
下面是 Tx 0x27423a01daf15560c395a1f2290d00e229f09519eec59c259fc180a470deede0 的重新模拟执行的例子:
$ cast call 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
--trace \
--block 21917352 \
--from 0x7713974908Be4BEd47172370115e8b1219F4A5f0 \
--data 0xa9059cbb000000000000000000000000e6158b784b7b43923773e08bfd07da2855f3f3b4000000000000000000000000000000000000000000000000000000003b9aca00 \
--rpc-url https://eth-mainnet.public.blastapi.io
Traces:
[23552] 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48::transfer(0xe6158b784b7b43923773e08BFd07dA2855F3F3B4, 1000000000 [1e9])
├─ [16263] 0x43506849D7C04F9138D1A2050bbF3A0c054402dd::transfer(0xe6158b784b7b43923773e08BFd07dA2855F3F3B4, 1000000000 [1e9]) [delegatecall]
│ ├─ emit Transfer(param0: 0x7713974908Be4BEd47172370115e8b1219F4A5f0, param1: 0xe6158b784b7b43923773e08BFd07dA2855F3F3B4, param2: 1000000000 [1e9])
│ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
└─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
Transaction successfully executed.
Gas used: 45148
下面是修改上面 Tx,人为提高 ERC20 的转帐数量后再执行的例子:
$ cast call 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
--trace \
--from 0x7713974908Be4BEd47172370115e8b1219F4A5f0 \
--block 21917352 \
--data 0xa9059cbb000000000000000000000000e6158b784b7b43923773e08bfd07da2855f3f3b4ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff \
--rpc-url https://eth-mainnet.public.blastapi.io
Traces:
[14669] 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48::transfer(0xe6158b784b7b43923773e08BFd07dA2855F3F3B4, 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
├─ [7361] 0x43506849D7C04F9138D1A2050bbF3A0c054402dd::transfer(0xe6158b784b7b43923773e08BFd07dA2855F3F3B4, 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]) [delegatecall]
│ └─ ← [Revert] revert: ERC20: transfer amount exceeds balance
└─ ← [Revert] revert: ERC20: transfer amount exceeds balance
Error: Transaction failed.
Gas used: 36613
6.1.4. balance(查询 ETH/ERC20 余额)
使用命令 cast balance 可以查询余额,比如:
$ cast balance --ether 0x00000000219ab540356cbb839cbe05303d7705fa --rpc-url https://ethereum-rpc.publicnode.com 58318831.414528837550514795
通过添加选项 --erc20 <contract-address> 可以查询 ERC20 余额,比如:
$ cast balance --erc20 0xdac17f958d2ee523a2206206994597c13d831ec7 0x00000000219ab540356cbb839cbe05303d7705fa --rpc-url https://ethereum-rpc.publicnode.com 4933853283 [4.933e9]
6.1.5. mktx(构造原始交易)
使用命令 mktx 可以构造原始交易,比如:
$ cast mktx 0x7E690CDc39F3a89110127646C81AEc47b444AeC4 'transfer(uint256)' 123 --private-key 0x3a7c119ac07652b119c6212259ec5dd58b29e71322274f6e9807684d269c4c30 --chain 56 --nonce 0 --gas-price 5gwei --gas-limit 21000 0xf8898085012a05f200825208947e690cdc39f3a89110127646c81aec47b444aec480a412514bba000000000000000000000000000000000000000000000000000000000000007b8194a048a1a98f29429a9aac70a4fe9305c570df534d40e028989c1714e174d47d03a1a0761ffb42e689cf74cd812ca95a664871bb17ebed24543196c29744562417eaea
6.1.6. publish(发送原始交易)
使用命令 cast publish 可以提交已经签名的交易,比如:
$ cast publish --rpc-url $RPC $TX
6.1.7. rpc(调用 rpc 方法)
使用命令 cast rpc 可以调用 ethereum 的节点 rpc 方法,比如:
$ cast rpc eth_getBlockByNumber "latest" "false" # 调用 rpc 方法 eth_getBlockByNumber $ cast rpc eth_getBlockByNumber --raw '["latest", false]' # 指定 --raw 后,参数会原封不动地作为 JSON RPC 的 "params" $ cast rpc eth_getTransactionByHash 0x2642e960d3150244e298d52b5b0f024782253e6d0b2c9a01dd4858f7b4665a3f
6.1.8. decode-tx(解码交易)
使用命令 cast decode-tx 可以解码交易,比如:
$ cast decode-tx 0xf86c808504e3b2920082520894428cf082d321d435ff0e1f8a994e01f976f19c118809b5552f5abade008026a00a27decf27241dca4e5d82bd5b7c1fbcc3f09c35a2a05cb967f2983d148ad6aba0596e9baa40ab157f5b1b0d66746472550ba9000d4154e3faa43ccce00b030452
{
"type": "0x0",
"chainId": "0x1",
"nonce": "0x0",
"gasPrice": "0x4e3b29200",
"gas": "0x5208",
"to": "0x428cf082d321d435ff0e1f8a994e01f976f19c11",
"value": "0x9b5552f5abade00",
"input": "0x",
"r": "0xa27decf27241dca4e5d82bd5b7c1fbcc3f09c35a2a05cb967f2983d148ad6ab",
"s": "0x596e9baa40ab157f5b1b0d66746472550ba9000d4154e3faa43ccce00b030452",
"v": "0x26",
"hash": "0xa8208564aa36d095973ce979df5bda03568ae0fb55f76517f1d91438bba84390"
}
6.1.9. calldata and decode-calldata(编码和解码 calldata,带 4byte)
使用命令 cast calldata 可以编码 calldata,比如:
$ cast calldata "transfer(address receiver, uint256 amt)" 0x41730Dd07F909c69708B9FEF3914A98F7f58D042 1 0xa9059cbb00000000000000000000000041730dd07f909c69708b9fef3914a98f7f58d0420000000000000000000000000000000000000000000000000000000000000001
使用命令 cast decode-calldata 可以解码 calldata,比如:
$ cast decode-calldata "transfer(address receiver, uint256 amt)" 0xa9059cbb00000000000000000000000041730dd07f909c69708b9fef3914a98f7f58d0420000000000000000000000000000000000000000000000000000000000000001 0x41730Dd07F909c69708B9FEF3914A98F7f58D042 1
6.1.9.1. abi-encode and decode-abi(无 4byte)
使用命令 cast abi-encode 可以进行 abi 编码(无前面 4byte),比如:
$ cast abi-encode "transfer(address receiver, uint256 amt)" 0x41730Dd07F909c69708B9FEF3914A98F7f58D042 1 0x00000000000000000000000041730dd07f909c69708b9fef3914a98f7f58d0420000000000000000000000000000000000000000000000000000000000000001
使用命令 cast decode-abi 可以进行 abi 解码(不需要提供前面 4byte),比如:
$ cast decode-abi "transfer(address receiver, uint256 amt)" --input 0x00000000000000000000000041730dd07f909c69708b9fef3914a98f7f58d0420000000000000000000000000000000000000000000000000000000000000001 0x41730Dd07F909c69708B9FEF3914A98F7f58D042 1
6.1.9.2. pretty-calldata(可简写 pc)
使用命令 cast pc 可以以更加易读的形式输出 calldata,比如:
$ cast pc 0xa9059cbb000000000000000000000000e78388b4ce79068e89bf8aa7f218ef6b9ab0e9d00000000000000000000000000000000000000000000000000174b37380cea000 Method: a9059cbb ------------ [000]: 000000000000000000000000e78388b4ce79068e89bf8aa7f218ef6b9ab0e9d0 [020]: 0000000000000000000000000000000000000000000000000174b37380cea000
6.1.10. wallet
6.1.10.1. wallet nm(创建助记词钱包)
使用命令 cast wallet nm 可以创建助记词钱包,比如:
$ cast wallet nm # nm 是 new-mnemonic 的简写 Generating mnemonic from provided entropy... Successfully generated a new mnemonic. Phrase: fly forest rib peace hazard slab enable blade forest hedgehog ordinary simple Accounts: - Account 0: Address: 0x41730Dd07F909c69708B9FEF3914A98F7f58D042 Private key: 0x3a7c119ac07652b119c6212259ec5dd58b29e71322274f6e9807684d269c4c30
6.1.10.2. wallet pk(从助记词导出私钥)
使用命令 cast wallet pk 可以从助记词导出私钥,比如:
$ cast wallet pk 'fly forest rib peace hazard slab enable blade forest hedgehog ordinary simple' 0x3a7c119ac07652b119c6212259ec5dd58b29e71322274f6e9807684d269c4c30
6.1.10.3. wallet addr(从助记词或私钥导出地址)
使用命令 cast wallet addr 可以从助记词或私钥导出地址,比如:
$ cast wallet addr --mnemonic 'fly forest rib peace hazard slab enable blade forest hedgehog ordinary simple' 0x41730Dd07F909c69708B9FEF3914A98F7f58D042 $ cast wallet addr 0x3a7c119ac07652b119c6212259ec5dd58b29e71322274f6e9807684d269c4c30 0x41730Dd07F909c69708B9FEF3914A98F7f58D042
6.1.11. sig and 4byte(函数 4byte 签名的计算和反查)
使用命令 cast sig 可以计算函数的 4byte 签名,比如:
$ cast sig 'function transfer(address receiver, uint256 amt) public returns (bool)' 0xa9059cbb $ cast sig 'transfer(address receiver, uint256 amt)' # 省略 function 关键字,函数返回值等等都是可以的 0xa9059cbb
已知函数的 4byte 签名时,可以使用命令 cast 4byte 反查出函数的完整签名,比如:
$ cast 4byte 0xa9059cbb transfer(address,uint256)
注:这种反查使用的是 openchain.xyz 提供的 API,比如上面反查 0xa9059cbb 对应的完整签名时其实是访问的接口:https://api.openchain.xyz/signature-database/v1/lookup?function=0xa9059cbb
6.1.11.1. sig-event and 4byte-event
Event 签名的哈希是 topic 的首个元素,使用命令 cast sig-event 可以计算 Event 签名的哈希,比如:
$ cast sig-event 'event Transfer(address indexed from, address indexed to, uint256 amt)' 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef $ cast sig-event 'Transfer(address,address,uint256)' # 省略 event 关键字,event 字段名称等等都是可以的 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
已知 Event 签名的哈希时,可以使用命令 cast 4byte-event 反查出 Event 的完整签名,比如:
$ cast 4byte-event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef Transfer(address,address,uint256)
注:这种反查使用的是 openchain.xyz 提供的 API,比如上面反查 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef 对应的 Event 完整签名时其实是访问的接口:https://api.openchain.xyz/signature-database/v1/lookup?event=0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
6.1.12. keccak(计算 keccak 哈希)
使用命令 cast keccak 可以计算 keccak 哈希。支持输入字符串,也支持输入 hex 数据,比如:
$ cast keccak hello 0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8 $ cast keccak 0x68656c6c6f # 输入是 0x 开头,则当作 hex data 处理 0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8
6.1.13. address-zero
使用命令 cast address-zero 可以输出全零地址,比如:
$ cast address-zero # 可简写为 cast az 0x0000000000000000000000000000000000000000
6.1.14. to-checksum(可简写 ta,转换为 checksum 地址)
使用命令 cast to-checksum 可以把全小写或者全大写的地址转换为大小写混合的 checksum 地址,比如:
$ cast to-checksum 0xaaa1183a90438d8aac8ad800f601f4d4cc7d20fa 0xaAA1183a90438d8aAc8AD800f601f4d4Cc7d20FA $ cast to-checksum 0xAAA1183A90438D8AAC8AD800F601F4D4CC7D20FA 0xaAA1183a90438d8aAc8AD800f601f4d4Cc7d20FA $ cast ta 0xaaa1183a90438d8aac8ad800f601f4d4cc7d20fa 0xaAA1183a90438d8aAc8AD800f601f4d4Cc7d20FA $ cast ta 0xAAA1183A90438D8AAC8AD800F601F4D4CC7D20FA 0xaAA1183a90438d8aAc8AD800f601f4d4Cc7d20FA
6.1.15. to-uint256 and to-dec(十进制和 uint256 之前进行转换)
使用命令 cast to-uint256 可以把十进制数转换为 uint256,比如:
$ cast to-uint256 100000000000 # 十进制数 100000000000 转换为 uint256 0x000000000000000000000000000000000000000000000000000000174876e800
使用命令 cast to-dec 可以把 hex data 转换为十进制数,比如:
$ cast to-dec 0x174876e800 # 把 0x174876e800 转换为十进制数 100000000000 $ cast to-dec 0x000000000000000000000000000000000000000000000000000000174876e800 # 同上 100000000000
6.1.16. to-ascii and from-utf8(字符串和 hex data 转换)
使用命令 cast to-ascii 可以把 hex data 转换为 ASCII 字符串,比如:
$ cast to-ascii 0x68656c6c6f # hex data -> ASCII hello
使用命令 cast from-utf8 可以把 UTF8 字符串转换 hex data,比如:
$ cast from-utf8 hello # UTF8 -> hex data 0x68656c6c6f
6.1.17. to-unit(在 ether, gwei, wei 之间任意转换)
使用命令 cast to-unit 可以在 ether, gwei, wei 之间任意转换,比如:
$ cast to-unit 1.23ether wei # 1.23 ether 转换为 wei 1230000000000000000 $ cast to-unit 1.23ether gwei # 1.23 ether 转换为 gwei 1230000000 $ cast to-unit 1230000000000000000 ether # 1230000000000000000 wei 转换为 ether 1.230000000000000000 $ cast to-unit 1230000000000000000 gwei # 1230000000000000000 wei 转换为 gwei 1230000000 $ cast to-unit 1230000000gwei ether # 1230000000 gwei 转换为 ether 1230000000000000000 $ cast to-unit 1230000000gwei wei # 1230000000 gwei 转换为 wei 1230000000000000000
6.1.17.1. to-wei and from-wei(ether 和 wei 之前单位转换)
使用命令 cast to-wei 和 cast from-wei 可以在 ether 和 wei 之前进行单位转换,比如:
$ cast to-wei 1.23 # 把 ether 转换为 wei 1230000000000000000 $ cast from-wei 1230000000000000000 # 把 wei 转换为 ether 1.230000000000000000
7. Hardhat
Hardhat 是一个和 Truffle 类似的 Ethereum 智能合约开发环境。
$ npm install --save-dev hardhat # 安装 hardhat 依赖 $ npx hardhat # 当前目录中没有 hardhat 项目时,会提示你创建一个 hardhat 项目 $ npx hardhat --help # 查看 hardhat 帮助 Hardhat version 2.12.4 Usage: hardhat [GLOBAL OPTIONS] <TASK> [TASK OPTIONS] GLOBAL OPTIONS: --config A Hardhat config file. --emoji Use emoji in messages. --flamegraph Generate a flamegraph of your Hardhat tasks --help Shows this message, or a task's help if its name is provided --max-memory The maximum amount of memory that Hardhat can use. --network The network to connect to. --show-stack-traces Show stack traces (always enabled on CI servers). --tsconfig A TypeScript config file. --typecheck Enable TypeScript type-checking of your scripts/tests --verbose Enables Hardhat verbose logging --version Shows hardhat's version. AVAILABLE TASKS: check Check whatever you need clean Clears the cache and deletes all artifacts compile Compiles the entire project, building all artifacts console Opens a hardhat console coverage Generates a code coverage report for tests deploy Deploy contracts etherscan-verify submit contract source code to etherscan export export contract deployment of the specified network into one file export-artifacts flatten Flattens and prints contracts and their dependencies help Prints this message node Starts a JSON-RPC server on top of Hardhat EVM run Runs a user-defined script after compiling the project sourcify submit contract source code to sourcify (https://sourcify.dev) test Runs mocha tests typechain Generate Typechain typings for compiled contracts verify Verifies contract on Etherscan To get help for a specific task run: npx hardhat help [task]
运行 npx hardhat 命令时默认会创建一个示例合约 contracts/Greeter.sol,其内容如下:
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.7.0;
import "hardhat/console.sol";
contract Greeter {
string greeting;
constructor(string memory _greeting) {
console.log("Deploying a Greeter with greeting:", _greeting);
greeting = _greeting;
}
function greet() public view returns (string memory) {
return greeting;
}
function setGreeting(string memory _greeting) public {
console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
greeting = _greeting;
}
}
这个示例合约中使用了 console.log ,用它可以很方便地进行代码调试。
运行 npx hardhat 命令时,默认也会创建一个测试用例 test/sample-test.js,其内容如下:
const { expect } = require("chai");
describe("Greeter", function() {
it("Should return the new greeting once it's changed", async function() {
const Greeter = await ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, world!");
await greeter.deployed();
expect(await greeter.greet()).to.equal("Hello, world!");
await greeter.setGreeting("Hola, mundo!");
expect(await greeter.greet()).to.equal("Hola, mundo!");
});
});
下面是使用 hardhat 编译合约和运行测试用例:
$ npx hardhat compile # 编译合约
$ npx hardhat test # 运行测试用例
Greeter
Deploying a Greeter with greeting: Hello, world! <----- 合约中的 console.log 输出
Changing greeting from 'Hello, world!' to 'Hola, mundo!' <----- 合约中的 console.log 输出
✓ Should return the new greeting once it's changed (1225ms)
1 passing (1s)
可见,合约中的 console.log 输出到控制台了,非常方便。
下面是启动 hardhat 本地节点:
$ npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/
Accounts
========
Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
Account #1: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
......
web3_clientVersion
eth_chainId
eth_accounts
eth_chainId
eth_estimateGas
eth_gasPrice
eth_sendTransaction
Contract deployment: Greeter
Contract address: 0x5fbdb2315678afecb367f032d93f642f64180aa3
Transaction: 0x28101d672a659feb8e5105b61a97b9c01b9ae4a5a01c20c1a39a0bad10a53144
From: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Value: 0 ETH
Gas used: 496037 of 496037
Block #1: 0xf171828ebbcac72873ee528e2778365f63a9c6f2c11af43045d64ab937ffedd4
console.log:
Deploying a Greeter with greeting: Hello, Hardhat!
这个控制台中也会输出合约中 console.log 的输出,非常方便。
7.1. 部署时去掉 console.log
使用插件 hardhat-preprocessor ,可以在真正部署时去掉 console.log 相关代码。
安装插件:
$ npm install hardhat-preprocessor
在 hardhat.config.js 中增加下面内容即可:
import {removeConsoleLog} from 'hardhat-preprocessor';
export default {
preprocess: {
eachLine: removeConsoleLog((bre) => bre.network.name !== 'hardhat' && bre.network.name !== 'localhost'),
},
};
8. Remix
Remix 是一个基于浏览器的 Ethereum 智能合约开发环境,支持合约的编译、部署、调试等等。可以在本地搭建 Remix 环境,也可以通过网站 https://remix.ethereum.org/ 直接使用。