Brownie + Ganache Fork 合约开发
使用ganache的主网fork技术 搭配python友好的brownie框架,高效对合约进行开发测试。
简介
Brownie 是一个基于python语言智能合约开发框架,与hardhat类似。
Ganache的前身是TestRPC,Ganache可以帮助我们快速启动一个以太坊私链来做开发测试、执行命令、探测区块链状态等。Ganache模拟的是内存中的区块链,它在执行交易时是实时返回,而不等待默认的出块时间,这样我们就可以快速验证代码。它同时还是一个支持自动化测试的功能强大的客户端。
Brownie安装
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
Ganache安装
npm install ganache --global
如果安装失败提示"Permission issue",加上"sudo"
安装需要依赖 node 环境
Brownie基本使用原则
创建文件夹后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
- contracts 目录下存放合约源码
- build中存放合约的abi json,deployments中存放对应chainid的abi json
- interfaces 存放合约接口定义
- scripts 存放执行的python脚本
- tests 存放用于测试的python脚本
brownie compile 命令编译所写的合约 (**注意:**这个命令会自动安装对应版本的solc编译器,但是amd64架构在mac不能run)
brownie networks list 显示已有的以太坊系区块链网络 (注意 :经测试,其中brownie自带的Development中bsc-main-fork的主网fork功能是无法正常使用的,有些地址的链上数据无法找到,因此建议使用下文的方法自行添加节点进行fork)
brownie console 可以启动一个brownie命令行 解释执行python脚本
基于主网fork的brownie开发 测试 部署教程
主网fork功能
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添加网络
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命令行
在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" }, ... -
更多详细的命令,可以查看brownie官方手册....