Brownie is a Python-based development and testing framework for smart contracts targeting the Ethereum Virtual Machine.
Brownie文档

安装Brownie:pip install eth-brownie
安装Ganache:npm install ganache --global

创建新项目

  • 有两种方式可以初始化一个新项目
    1. 创建一个空项目:brownie init
    2. 创建一个有模板的项目,如:brownie bake react

项目结构

  • 初始化项目之后,文件夹会生成以下几个文件夹

    文件夹 内容
    contracts 智能合约源代码。每次运行Brownie,都会自动检查该文件夹中是否存在变动的文件,如果有,就会自动重新编译
    interfaces 接口源代码
    scripts 部署和交互脚本。脚本通过brownie run执行
    tests 测试脚本。脚本通过brownie test执行;使用的测试框架为Pytest
    build(不应修改或删除其中的内容) 项目数据,如编译产物和单元测试结果
    reports(不应修改或删除其中的内容) 用于GUI的JSON报告

编译合约

当运行brownie compie时,Brownie会自动检查contracts文件夹中是否存在文件发生变动,若没有文件发生变动,则不会进行该次编译,如果要强制进行编译,则可以使用:brownie compile --all

只要contracts文件夹中存在无法编译的文件,就无法运行brownie,如果希望将某个文件夹或者某个文件排除在编译之外,就在其名称前加_

  • Brownie支持Solidity(>=0.4.22)和Vyper(>=0.1.0-beta.16),文件的扩展名会决定使用哪种编译器

    • Solidity: .sol
    • Vyper: .vy
  • interfaces文件夹在以下两种情况特别有用:

    1. 使用Vyper时,接口不一定是可编译的源代码,因此不能包含在contracts文件夹中。
    2. 在同一个项目中使用Solidity和Vyper时,或使用Solidity的多个版本时,兼容性问题会阻止合约直接相互引用。

Interfaces文件夹支持Solidity(.sol)和Vyper(.vy).Vyper合约也可以直接导入JSON编码的ABI(.json)文件

可以在brownie-config.yaml文件中对编译进行相关设置,如果未设置或设置无法生效,Brownie会采用默认设置,有关编译的默认设置如下:

1
2
3
4
5
6
7
8
9
compiler:
evm_version: null
solc:
version: null
optimizer:
enabled: true
runs: 200
vyper:
version: null

修改编译设置后,会使得项目全部重新编译

如果没有在配置文件中设定编译器版本,Brownie会自动根据合约的version pragma来选择编译器版本

EVM Version:当使用>=0.5.13的Solidity或者使用Vyper时,Brownie会自动将EVM version设置为istanbul,当然,也可以手动在brownie-config.yaml文件中进行设置

Solidity编译器允许路径重新映射,Brownie通过配置文件中的compiler.solc.remappings字段进行设置,案例如下:

1
2
3
4
5
compiler:
solc:
remappings:
- zeppelin=/usr/local/lib/open-zeppelin/contracts/
- github.com/ethereum/dapp-bin/=/usr/local/lib/dapp-bin/

Brownie不会检测项目文件夹以外的文件是否发生变动,如果被引用的文件在项目文件夹之外,那么当该文件发生变动时,需要手动重新编译

如果使用了如下映射:

1
2
3
4
compiler:
solc:
remappings:
- "@openzeppelin=OpenZeppelin/openzeppelin-contracts@3.0.0"

那么以下两种导包方式都将是同样的:

1
2
import "@openzeppelin/contracts/math/SafeMath.sol";
import "OpenZeppelin/openzeppelin-contracts@3.0.0/contracts/math/SafeMath.sol";

可以在brownie console或者python代码中安装不同版本的编译器:

1
2
3
4
5
from brownie.project.compiler import install_solc
install_solc("0.5.10")

from brownie.project.compiler.vyper import install_vyper
install_vyper("0.2.4")

包管理器

可以通过以下命令调用包管理器:brownie pm
查看当前安装的包:brownie pm list
将包的内容复制到另一个文件夹中:brownie pm clone [package] [path],如果不指定路径,则会复制到当前目录,例如:brownie pm clone aragon/aragonOS@4.0.0

Brownie支持从ethPM和Github安装包

要从Github安装包,您必须使用包ID,包ID由组织名称、存储库和版本标签组成,包ID不区分大小写,类似这样:
[ORGANIZATION]/[REPOSITORY]@[VERSION]

如果要安装OpenZeppelin contracts的3.0.0版本:brownie pm install OpenZeppelin/openzeppelin-contracts@3.0.0

ethPM(以太坊包管理器):是一个去中心化的包管理器,用于分发EVM智能合约和项目。

要获得ethPM软件包,您必须知道软件包名称和可用的注册表地址。此信息通过注册表URI传递。注册表URI使用以下格式:
ethpm://[CONTRACT_ADDRESS]:[CHAIN_ID]/[PACKAGE_NAME]@[VERSION]

安装由SnakeCharmersZeppelin注册表提供的OpenZeppelin的Math包:
brownie pm install ethpm://zeppelin.snakecharmers.eth:1/math@1.0.0

导入已经安装的包,例如从OpenZeppelin contracts导入SafeMath:import "OpenZeppelin/openzeppelin-contracts@3.0.0/contracts/math/SafeMath.sol";

可以在配置文件中先声明依赖,Brownie会在编译项目之前尝试安装任何列出的依赖项,例如:

1
2
3
dependencies:
- aragon/aragonOS@4.0.0
- defi.snakecharmers.eth/compound@1.1.0

部署

部署完成后,Brownie将在build/deployment/文件夹中维护一个map.json文件,该文件列出了实时网络上所有已部署的合约,并按链和合约名称排序。每个合约的列表按部署的区块编号排序,最新的部署排在最前面

Brownie会保存有关实时网络上合约部署的信息。部署合约后,生成的ProjectContract实例在未来的Brownie会话中仍然可用

  • 以下操作不会删除本地存储的部署数据:
    • 断开并重新连接到同一网络
    • 关闭并重新加载项目
    • 退出并重新加载Brownie
    • 修改合约的源代码 - Brownie仍然保留已部署版本的源代码
  • 以下操作将删除项目中本地存储的部署数据:
    • 调用ContractContainer.remove将删除已删除的ProjectContract实例的部署信息
    • 删除或重命名项目中的合同源文件将导致Brownie删除已删除合同的所有部署信息
    • 删除build/deployments/目录将删除有关已部署合约的所有信息

Brownie为etherscan支持的所有网络上的solidity合约提供自动源代码验证功能。要在部署时验证合约,请添加publish_source=True参数,如:Token.deploy("My Real Token", "RLT", 18, 1e28, {'from': acct}, publish_source=True)

交互

Brownie支持对已经部署的合约进行交互,交互手段分为控制台交互和脚本交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 进入控制台交互模式
brownie console
brownie console --network goerli
# 退出控制台交互模式
exit()
# 部署合约
contract = contract_name.deploy({"from": account})
# 获取已经部署的合约
contract = Contract(contract_address)
contract = Contract.from_explorer(contract_address)
contract = Contract.from_abi(contract_name, contract_address, contract_abi)
contract = contract_name.at(contract_address)
contract = contract_name[-1]
# 随消息发送ETH
contract.contract_function({"from": account, "value": value})

网络

  • Brownie可用于开发和实时环境
    • 开发环境是用于测试和调试的本地临时网络。Brownie使用Ganache作为开发环境
    • 实时环境是一个非本地的、持久的区块链。该术语用于指代以太坊主网和测试网

常用的网络命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 获取帮助
brownie networks -h
# 查看网络
brownie networks list
brownie networks list true
# 修改网络
brownie networks modify ...
# 新增网络
brownie networks add ...
brownie networks add Ethereum localchain host=http://localhost:7545 chainid=1337
brownie networks add development mainnet-fork-dev cmd=ganache-cli host=http://localhost fork='https://infura.io/v3/$WEB3_INFURA_PROJECT_ID' accounts=10 mnemonic=brownie port=8545
# 默认情况下,每次加载Brownie时都会启动并连接到ganache-cli
# 要连接到不同的网络,请使用--network
brownie run scripts\deploy.py --network goerli
# 查看节点提供商
brownie networks list_providers
brownie networks list_providers true
# 修改节点提供商
brownie networks set_provider alchemy

如果要修改默认网络,可以在配置文件中修改,具体如下:

1
2
networks:
default: goerli

要与实时网络交互,必须连接到一个节点。可以运行自己的节点,也可以连接到托管节点。Alchemy和Infura提供了对以太坊节点的公共访问

Ganache允许您通过从实时网络分叉来创建开发网络

账户

常用的账户命令:

1
2
3
4
5
6
7
8
# 获取帮助
brownie accounts -h
# 查看账户
brownie accounts list
# 根据私钥导入账户
brownie accounts new <id>
# 修改账户
brownie accounts modify <id>

在代码中加载账户:

1
2
3
4
5
6
# 加载brownie账户列表里的账户
accounts.load("account_id")
# 加载私钥为账户
accounts.add("PRIVATE_KEY")
# 使用os库加载环境变量中的私钥
os.environ.get("PRIVATE_KEY")

配置文件

需要先在项目根目录创建brownie-config.yaml,然后就可以通过编辑该配置文件来修改Brownie的默认行为

要引用brownie-config.yaml中配置的参数,需要先导包,然后用索引的方式得到

1
2
3
from brownie import config

current_network = config['networks'][network.show_active()]

可以在配置文件中加载环境变量,如:

1
2
wallets:
from_key: ${PRIVATE_KEY}

测试

文件命名规则:test_.py或者test.py
函数命名规则:test
*

测试分为三个步骤:Arrange,Act,Assert,一个测试函数可以进行两次assert

1
2
3
4
5
6
7
# 进行测试
brownie test
brownie test tests/test_transfer.py
brownie test -s
brownie test --pdb
brownie test --network mychain
brownie test -k test_only_owner_can_withdraw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 案例1
def test_deploy():
# Arrange
account = accounts[0]
simple_storage = SimpleStorage.deploy({"from": account})
# Act
starting_value = simple_storage.retrieve()
expected = 0
# Assert
assert starting_value == expected

# 案例2:两次assert
def test_can_fund_and_withdraw():
# Arrange
account = get_account()
fund_me = deploy_fund_me()
# Act
entrance_fee = fund_me.getEntranceFee()
tx = fund_me.fund({"from": account, "value": entrance_fee})
tx.wait(1)
# Assert
assert fund_me.addressToAmountFunded(account.address) == entrance_fee
# Act
tx2 = fund_me.withdraw({"from": account})
tx2.wait(1)
# Assert
assert fund_me.addressToAmountFunded(account.address) == 0

# 案例3:与pytest联动
def test_only_owner_can_withdraw():
if network.show_active() != "mychain":
pytest.skip("only for local testing")