使用ganache的主网fork技术 搭配python友好的brownie框架,高效对合约进行开发测试。
Brownie 是一个基于python语言智能合约开发框架,与hardhat类似。
Ganache的前身是TestRPC,Ganache可以帮助我们快速启动一个以太坊私链来做开发测试、执行命令、探测区块链状态等。Ganache模拟的是内存中的区块链,它在执行交易时是实时返回,而不等待默认的出块时间,这样我们就可以快速验证代码。它同时还是一个支持自动化测试的功能强大的客户端。
python3 -m pip install eth-brownie
# brownie version 1.18.1
vim ~/.zshrc # or .bashrc
# add python bin to env
export PATH=/Users/[your username]/Library/Python/3.8/bin:$PATH
npm install ganache --global
如果安装失败提示"Permission issue",加上"sudo"
安装需要依赖 node 环境
创建文件夹后brownie init 命令生成目录
项目目录结构如下:
.
├── build
│ ├── contracts
│ │ ├── ERC20.json
│ │ ├── IERC20.json
│ │ ├── IERC20Metadata.json
│ ├── deployments
│ │ ├── 1337
│ │ │ └── 0x58014f69691c46eB3a077E5e90C0F6BFeF5FE46a.json
│ │ └── map.json
│ └── interfaces
├── contracts
│ └── M_token.sol
├── interfaces
├── reports
├── scripts
└── tests
brownie compile 命令编译所写的合约 (注意:这个命令会自动安装对应版本的solc编译器,但是amd64架构在mac不能run)
brownie networks list 显示已有的以太坊系区块链网络 (注意 :经测试,其中brownie自带的Development中bsc-main-fork的主网fork功能是无法正常使用的,有些地址的链上数据无法找到,因此建议使用下文的方法自行添加节点进行fork)
brownie console 可以启动一个brownie命令行 解释执行python脚本
Ganache新增了一个非常好用的功能是一键fork主网当前的某个区块高度的状态到本地,进行私人开发。(这个功能在hardhat上也有)
首先我们先注册一个RPC Endpoint节点,我使用的是https://moralis.io/的节点服务,注册登录后,选择“Speedy Nodes”,然后点击BSC Network(以BSC主网fork为例),会出现很多个节点链接的弹窗,选择”Mainnet Archive“下的链接进行复制。
然后在本机命令行中,输入命令开启主网fork
ganache-cli --fork https://speedy-nodes-nyc.moralis.io/your_id/bsc/mainnet/archive
该fork功能有更多参数(助记词、区块高度、账户数等)可以参见--help进行查看
执行成功后 部分返回结果如下:
==================
Location: https://speedy-nodes-nyc.moralis.io/your_id/bsc/mainnet/archive
Block: 17020664
Network ID: 56
Time: Sun Apr 17 2022 02:03:54 GMT-0700 (Pacific Daylight Time)
Chain Id
==================
1337
RPC Listening on 127.0.0.1:8545
可以看到block height、network id、chainid、host等信息。
brownie添加网络
brownie networks add Ethereum local-bsc-fork host=http://127.0.0.1:8545 chainid=1337
其中local-bsc-fork是自定义命名,Ethereum是网络分类,host和chainid分别按照上条ganache命令的返回结果填入
添加成功后,brownie networks list命令查看是否在网络列表中
输入brownie console --network local-bsc-fork命令可以开启brownie命令行
也可以使用 brownie run your_script --network local-bsc-fork 命令连接本地环境跑脚本
在brownie命令行中可以与自己的合约和区块链环境进行交互。
注意:交互时注意本机的代理模式,科学上网有可能导致区块链无法正常交互
区块链账号交互 :
命令 accounts或者a可以访问本地帐号
>>> accounts
['0xC0BcE0346d4d93e30008A1FE83a2Cf8CfB9Ed301','0xf414d65808f5f59aE156E51B97f98094888e7d92']
>>> accounts[0]
<Account object '0xC0BcE0346d4d93e30008A1FE83a2Cf8CfB9Ed301'>
accounts.add() 增加账号
>>> accounts.add()
mnemonic: 'rice cement vehicle ladder end engine tiger gospel toy inspire steel teach'
<LocalAccount '0x7f1eCD32aF08635A3fB3128108F6Eb0956Efd532'>
也可以通过accounts.add(your_private_key) 来导入特定账户
>>> accounts.add('0xca751356c37a98109fd969d8e79b42d768587efc6ba35e878bc8c093ed95d8a9')
<LocalAccount '0xf6c0182eFD54830A87e4020E13B8E4C82e2f60f0'>
在测试环境中,如果没有某账户的私钥,也可以通过accounts.at方法强行引入,适合冒充其他特定账户行为进行测试
交易替换 transaction.replace() 使用更高gas price替换正在pending的交易
>>> tx = accounts[0].transfer(accounts[1], 100, required_confs=0, gas_price="1 gwei")
Transaction sent: 0xc1aab54599d7875fc1fe8d3e375abb0f490cbb80d5b7f48cedaa95fa726f29be
Gas price: 13.0 gwei Gas limit: 21000 Nonce: 3
<Transaction object '0xc1aab54599d7875fc1fe8d3e375abb0f490cbb80d5b7f48cedaa95fa726f29be'>
>>> tx.replace(1.1)
Transaction sent: 0x9a525e42b326c3cd57e889ad8c5b29c88108227a35f9763af33dccd522375212
Gas price: 14.3 gwei Gas limit: 21000 Nonce: 3
<Transaction '0x9a525e42b326c3cd57e889ad8c5b29c88108227a35f9763af33dccd522375212'>
合约交互 :
合约部署
>>> t = Token.deploy("Test Token", "TST", 18, 1e23, {'from': accounts[1]})
Transaction sent: 0x2e3cab83342edda14141714ced002e1326ecd8cded4cd0cf14b2f037b690b976
Transaction confirmed - block: 1 gas spent: 594186
Contract deployed at: 0x5419710735c2D6c3e4db8F30EF2d361F70a4b380
<Token Contract object '0x5419710735c2D6c3e4db8F30EF2d361F70a4b380'>
>>> t
<Token Contract object '0x5419710735c2D6c3e4db8F30EF2d361F70a4b380'>
>>> Token
[<Token Contract object '0x5419710735c2D6c3e4db8F30EF2d361F70a4b380'>]
合约调用 合约调用有两种方式,一种是上链消耗gas的交易,一种是静态调用,返回结果是 TransactionReceipt对象,代码示例如下
>>> Token[0].transfer(accounts[1], 1e18, {'from': accounts[0]})
Transaction sent: 0x6e557594e657faf1270235bf4b3f27be7f5a3cb8a9c981cfffb12133cbaa165e
Token.transfer confirmed - block: 4 gas used: 51019 (33.78%)
<Transaction object '0x6e557594e657faf1270235bf4b3f27be7f5a3cb8a9c981cfffb12133cbaa165e'>
>>> Token[0].transfer.call(accounts[1], 1e18, {'from': accounts[0]})
True # 交易结果
Transaction参数,除了from,其他参数具体可参见https://eth-brownie.readthedocs.io/en/stable/core-contracts.html
另外如果调用的函数是不发生状态改变的静态函数,使用上述方法只返回结果,均不消耗gas,也不上链。如果有上链需求,使用ContractCall.transact
方法,示例如下
# 不上链,静态调用
>>> Token[0].balanceOf(accounts[0])
1000000000000000000000
# 上链
>>> tx = Token[0].balanceOf.transact(accounts[0])
Transaction sent: 0xe803698b0ade1598c594b2c73ad6a656560a4a4292cc7211b53ffda4a1dbfbe8
Token.balanceOf confirmed - block: 3 gas used: 23222 (18.85%)
<Transaction object '0xe803698b0ade1598c594b2c73ad6a656560a4a4292cc7211b53ffda4a1dbfbe8'>
>>> tx.return_value
1000000000000000000000
利用接口实例化合约对象 只要在contracts和interfaces中声明的合约接口,都可以通过interface对象进行调用和实例化。 示例如下
>>> interface.Dai
<InterfaceConstructor 'Dai'>
# 填入地址参数,可以实例化Dai合约
>>> interface.Dai("0x6B175474E89094C44Da98b954EedeAC495271d0F")
<Dai Contract object '0x6B175474E89094C44Da98b954EedeAC495271d0F'>
也可以通过abi实例化合约,使用Contract.from_abi
方法
>>> Contract.from_abi("Token", "0x79447c97b6543F6eFBC91613C655977806CB18b0", abi)
<Token Contract object '0x79447c97b6543F6eFBC91613C655977806CB18b0'>
对于ethereum上的合约可以通过Contract.from_explorer
方法示例化合约
>>> Contract.from_explorer("0x6b175474e89094c44da98b954eedeac495271d0f")
Fetching source of 0x6B175474E89094C44Da98b954EedeAC495271d0F from api.etherscan.io...
<Dai Contract '0x6B175474E89094C44Da98b954EedeAC495271d0F'>
交易Inspecting & Debugging
brownie中发送一个交易后会返回一个TransactionReceipt
对象,这个交易收据对象有很多用于inspect和debug的函数。
info方法 查看交易信息
>>> tx = Token[0].transfer(accounts[1], 1e18, {'from': accounts[0]})
Transaction sent: 0xa7616a96ef571f1791586f570017b37f4db9decb1a5f7888299a035653e8b44b
Token.transfer confirmed - block: 2 gas used: 51019 (33.78%)
>>> tx.info()
Transaction was Mined
---------------------
Tx Hash: 0xa7616a96ef571f1791586f570017b37f4db9decb1a5f7888299a035653e8b44b
From: 0x4FE357AdBdB4C6C37164C54640851D6bff9296C8
To: 0xDd18d6475A7C71Ee33CEBE730a905DbBd89945a1
Value: 0
Function: Token.transfer
Block: 2
Gas Used: 51019 / 151019 (33.8%)
Events In This Transaction
--------------------------
Transfer
from: 0x4fe357adbdb4c6c37164c54640851d6bff9296c8
to: 0xfae9bc8a468ee0d8c84ec00c8345377710e0f0bb
value: 1000000000000000000
events成员 查看交易触发的事件
>>> tx.events
{
'CountryModified': [
{
'country': 1,
'limits': (0, 0, 0, 0, 0, 0, 0, 0),
'minrating': 1,
'permitted': True
},
{
'country': 2,
'limits': (0, 0, 0, 0, 0, 0, 0, 0),
'minrating': 1,
'permitted': True
}
],
'MultiSigCallApproved': [
{
'callHash': "0x0013ae2e37373648c5161d81ca78d84e599f6207ad689693d6e5938c3ae4031d",
'callSignature': "0xa513efa4",
'caller': "0xF9c1fd2f0452FA1c60B15f29cA3250DfcB1081b9",
'id': "0x8be1198d7f1848ebeddb3f807146ce7d26e63d3b6715f27697428ddb52db9b63"
}
]
}
events可以list或者dict的方式去访问。
查看内部转账和合约创建
TransactionReceipt.internal_transfers
查看内部ether(bnb)转账
>>> tx.internal_transfers
[
{
"from": "0x79447c97b6543F6eFBC91613C655977806CB18b0",
"to": "0x21b42413bA931038f35e7A5224FaDb065d297Ba3",
"value": 100
}
]
TransactionReceipt.new_contracts
查看新合约创建
>>> tx = deployer.deployNewContract() # 调用deployer合约的deployNewContract方法
Transaction sent: 0x6c3183e41670101c4ab5d732bfe385844815f67ae26d251c3bd175a28604da92
Gas price: 0.0 gwei Gas limit: 79781
Deployer.deployNewContract confirmed - Block: 4 Gas used: 79489 (99.63%)
>>> tx.new_contracts
["0x1262567B3e2e03f918875370636dE250f01C528c"]
>>> Token.at(tx.new_contracts[0]) # 实例化合约
<Token Contract object '0x1262567B3e2e03f918875370636dE250f01C528c'>
交易debug
TransactionReceipt.revert_msg
查看交易revert信息
>>> tx.revert_msg
'Insufficient Balance'
TransactionReceipt.traceback
以类python风格显示报错点
>>> tx.traceback()
Traceback for '0xd31c1c8db46a5bf2d3be822778c767e1b12e0257152fcc14dcf7e4a942793cb4':
Trace step 169, program counter 3659:
File "contracts/SecurityToken.sol", line 156, in SecurityToken.transfer:
_transfer(msg.sender, [msg.sender, _to], _value);
Trace step 5070, program counter 5666:
File "contracts/SecurityToken.sol", lines 230-234, in SecurityToken._transfer:
_addr = _checkTransfer(
_authID,
_id,
_addr
);
Trace step 5197, program counter 9719:
File "contracts/SecurityToken.sol", line 136, in SecurityToken._checkTransfer:
require(balances[_addr[SENDER]] >= _value, "Insufficient Balance")
TransactionReceipt.call_trace
可以展示该失败交易的整个过程中的跳转map
>>> tx.call_trace()
Call trace for '0x7824c6032966ca2349d6a14ec3174d48d546d0fb3020a71b08e50c7b31c1bcb1':
Initial call cost [21228 gas]
LiquidityGauge.deposit 0:3103 [64010 / 128030 gas]
├── LiquidityGauge._checkpoint 83:1826 [-6420 / 7698 gas]
│ ├── GaugeController.get_period_timestamp [STATICCALL] 119:384 [2511 gas]
│ ├── ERC20CRV.start_epoch_time_write [CALL] 411:499 [1832 gas]
│ ├── GaugeController.gauge_relative_weight_write [CALL] 529:1017 [3178 / 7190 gas]
│ │ └── GaugeController.change_epoch 697:953 [2180 / 4012 gas]
│ │ └── ERC20CRV.start_epoch_time_write [CALL] 718:806 [1832 gas]
│ └── GaugeController.period [STATICCALL] 1043:1336 [2585 gas]
├── LiquidityGauge._update_liquidity_limit 1929:2950 [45242 / 54376 gas]
│ ├── VotingEscrow.balanceOf [STATICCALL] 1957:2154 [2268 gas]
│ └── VotingEscrow.totalSupply [STATICCALL] 2180:2768 [6029 / 6866 gas]
│ └── VotingEscrow.supply_at 2493:2748 [837 gas]
└── ERC20LP.transferFrom [CALL] 2985:3098 [1946 gas]
每一行的显示规则为
ContractName.functionName (external call opcode) start:stop [internal / total gas used]
如果该方法中填入True参数,可以展示更详细的视图
>>> history[-1].call_trace(True)
Call trace for '0x7824c6032966ca2349d6a14ec3174d48d546d0fb3020a71b08e50c7b31c1bcb1':
Initial call cost [21228 gas]
LiquidityGauge.deposit 0:3103 [64010 / 128030 gas]
├── LiquidityGauge._checkpoint 83:1826 [-6420 / 7698 gas]
│ │
│ ├── GaugeController.get_period_timestamp [STATICCALL] 119:384 [2511 gas]
│ │ ├── address: 0x0C41Fc429cC21BC3c826efB3963929AEdf1DBb8e
│ │ ├── input arguments:
│ │ │ └── p: 0
│ │ └── return value: 1594574319
...
通过访问TransactionReceipt.subcalls
成员可以展示子调用的详细信息
>>> history[-1].subcalls
[
{
'from': "0x5AE569698C5F986665018B6e1d92A71be71DEF9a",
'function': "get_period_timestamp(int128)",
'inputs': {
'p': 0
},
'op': "STATICCALL",
'return_value': (1594574319,),
'to': "0x0C41Fc429cC21BC3c826efB3963929AEdf1DBb8e"
},
...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!