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官方手册....

参考链接

点赞 3
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
退学写码
退学写码
江湖只有他的大名,没有他的介绍。