Brownie 开发智能合约(入门使用)

Posted 懒编程-二两

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Brownie 开发智能合约(入门使用)相关的知识,希望对你有一定的参考价值。

简介

上篇文章,使用了 Remix 在线 IDE,个人感觉 Remix 在入门智能合约开发时,是很好的上手工具,因为 Remix 帮我们处理好了编译、部署的过程,并且还通过 javascript VM 准备好了本地区块链方便我们测试,可谓开箱即用,但毕竟是线上 IDE,功能还是有限。

这里我们使用 Brownie 框架来开发智能合约,Brownie 框架是基于 Python 编写的智能合约开发框架,它可以帮我们快速完成编译、部署、测试等智能合约开发的全流程。

文档:https://eth-brownie.readthedocs.io/en/stable/

Web3.py 基础

因为 Brownie 主要基于 Web3.py 这个库开发而来,在从 Python 角度了解以太坊

编写简单的智能合约

首先,通过 Solidity 编写一个简单智能合约,没错,我们并不能通过 Python 来编写智能合约,利用 Python,只是为了让这个过程更加自动化与工程化,智能合约代码如下:

// SPDX-License-Identifier: MIT

pragma solidity ^0.6.0;

contract Storage {
    struct People {
        string name;
        uint256 age;
    }
    People[] public people;

    function addPerson(string memory _name, uint256 _age) public {
        people.push(People(_name, _age));
    }
}

上述代码中,通过 struct 关键字定义了一个名为 People 的对象,该对象中有 name 与 age 两个属性,然后基于 People 对象,实例化了 people 数组,然后定义了 addPerson 函数,该方法会接收_name 与_age 参数,然后实例化 People 对象,最后将 People 对象添加到数组中。

这里有个细节,就是参数_name 是字符串,所以需要使用 memory 关键字标注一下。Solidity 中,存储变量的方式有 storage 与 memory 两种。

  • storage 变量:永久存储在区块链中的变量

  • memory 变量:临时的,当外部函数对某合约调用完成时,内存型变量即被移除

Solidity 中的 string 的本质是字符数组(Char Array),如果你不通过 memory 声明,就算_name 是函数参数,Solidity 也会通过storage持续存储它。

编译智能合约与连接本地区块链网络

创建名为【web3py_storage】的文件夹,然后在其中创建 Storage.sol 文件并将智能合约代码复制到文件中。

通过 vscode 打开 webpy_simple_storage 文件夹,创建 base.py,在 base.py 实现对智能合约的编译以及连接上区块链网络的操作。

阅读 web3.py 智能合约相关的文档:https://web3py.readthedocs.io/en/stable/contracts.html

通过文档可知,web3.py 不支持 solidity 的编译,文档中建议我们安装 py-solc-x 库来实现 solidity 的编译,简单安装一下,然后通过 install_solc 方法来下载对应版本的 solidity 编译器。

因为我们的智能合约使用了 Solidity ^0.6.0,所以下载 0.6.0 版本的 solidity 编译器则可,然后按文档的方式设置编译 Solidity 时的配置则可,相关代码如下:

import os
import json
from web3 import Web3

# 编译 solidity
# https://github.com/iamdefinitelyahuman/py-solc-x
from solcx import compile_standard, install_solc

with open('./Storage.sol', 'r', encoding='utf-8') as f:
    storage_file = f.read()

# 下载0.6.0版本的Solidity编译器
install_solc('0.6.0')

# 编译Solidity
compiled_sol = compile_standard(
    {
        "language": "Solidity",
        # Solidity文件
        "sources": {"Storage.sol": {"content": storage_file}},
        "settings": {
            "outputSelection": {
                "*": {
                    # 编译后产生的内容
                    "*": ["abi", "metadata", "evm.bytecode", "evm.bytecode.sourceMap"]
                }
            }
        },
    },
    # 版本,与编写智能合约时Solidity使用的版本对应
    solc_version="0.6.0",
)

# 编译后的结果写入文件
with open('compiled_code.json', 'w') as f:
    json.dump(compiled_sol, f)

compile_standard 方法编译后的结果写入 compiled_code.json,将其格式化,如下图:

从上图可知,Solidity 编译后的字节码也在 compiled_code.json 中,将 json 文件中重要的数据读取出来,代码如下:

# 智能合约编译后的字节码(上链的数据)
bytecode = compiled_sol["contracts"]["Storage.sol"]["Storage"]["evm"][
    "bytecode"
]["object"]

# ABI (Application Binary Interface),用于与智能合约中的方法进行交互的接口
abi = json.loads(
    compiled_sol["contracts"]["Storage.sol"]["Storage"]["metadata"]
)["output"]["abi"]
  • bytecode:智能合约编译后的字节码,智能合约上链其实就是将这部分数据存储到区块链中。

  • abi:我们的程序与智能合约交互的接口,它定义了我们的程序可以怎么与当前这个智能合约交互。

至此,智能合约的编译流程就结束了,然后我们通过 web3.py 连接到以太坊中。

与 Remix IDE 不同,web3.py 没有通过 JavaScript VM 实现的本地区块链网络,虽然有 web3 [tester],但不够完善,这里我们通过 Genache 来实现本地网络。

Genache:https://www.trufflesuite.com/ganache

下载好后,直接运行,然后点击【QUICKSTART】,选择【ETHEREUM】。

Ganache 会在本地快速创建区块链网络:

从上图中,可以看出,Ganache 会为我们创建 10 个账号,创建出的网络可以通过 http://127.0.0.1:7545 连接。

要实现连接,还需要一个信息,那就是 Ganache 创建的区块链网络,其 chain id 是多少?图中只展示了 NETWORK ID(5777),查阅文档,可知 chain id 为 1337(https://ethereum.stackexchange.com/questions/91072/setup-ganache-with-metamask-what-and-where-is-a-chain-id)。

通常,我们不会将这些常量硬编码到代码中,而是通过配置文件或环境变量的形式引入,这里使用环境变量的形式。Python 中使用环境变量比较好的方式是使用 python-dotenv 这个库,pip 安装一下,然后再项目根目录中创建名为.env 的文件,写入如下内容:

RINKEBY_RPC_URL=http://127.0.0.1:7545
ACCOUNT_ADDRESS=0x4A151d2855eEFba23Eb9B7943253D29E061cFeFD
PRIVATE_KEY=0xc6ba82d2e7bc2ab41f578a57b8822767b9875e339d2f93d3fe8eef25f5cb39aa

然后代码里使用一下:

from dotenv import load_dotenv

load_dotenv()

w3 = Web3(Web3.HTTPProvider(os.getenv("RINKEBY_RPC_URL")))
chain_id = 1337

my_address = os.getenv("ACCOUNT_ADDRESS")
private_key = os.getenv("PRIVATE_KEY")

Web3.py 部署智能合约

部署的流程比较简单,直接给出代码:

from base import *

# 构建智能合约对象
storage = w3.eth.contract(abi=abi, bytecode=bytecode)
# 当前区块链中最后一个交易的nonce
nonce = w3.eth.get_transaction_count(my_address)

# 部署智能合约 - 创建交易
transaction = storage.constructor().buildTransaction(
    {"chainId": chain_id, "from": my_address, "nonce": nonce}
)
# 签名当前交易 - 证明是你发起的交易
signed_txn = w3.eth.account.sign_transaction(transaction, private_key=private_key)
print("Deploying Contract!")

# 开始部署 - 发送交易
tx_hash = w3.eth.send_raw_transaction(signed_txn.rawTransaction)
print('Waiting for deploy transaction to finish...')
# 等待智能合约部署结果,部署完后,会获得合约的地址
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print('Deployed Done!')
print(f'contract address: {tx_receipt.contractAddress}')

上述代码中,一开始通过 w3.eth.contract 方法实例化合约对象,需要传入 abi 与 bytecode(base.py 提供了)。

然后对合约进行部署,部署的过程其实也是在创建交易,这就涉及到:

  • 创建交易对象

  • 签名交易

  • 发送交易

  • 等待交易完成

上述代码刚好就是这几个步骤,需要注意的点是 nonce,每个交易都需要 nonce,这个 nonce 是顺序的,所有我们需要获取最后一个交易的 nonce,运行代码,结果如下图:

部署后,智能合约的地址:0x8395Fd53331cea813e3838F6bB42B9668BEBf0C2

Web3.py 调用部署的智能合约

部署完后,我们获得了合约部署后的地址,使用该地址,可以构建出合约对象,然后我们就可以调用合约里的方法了。回顾一开始我们编写的合约,其实只有 addPerson 这一个方法,该方法会将传入方法的数据存到区块链网络中,这改变了区块链的状态,所以算是一次交易操作,凡是交易操作就需要签名,从而证明这个操作是你做的。

完整代码如下:

from base import *

# 调用deploy.py会获得contract_address
contract_address = '0x5071ad6611B322647B88ACF5CBeBCA71Bead0c6f'

nonce = w3.eth.get_transaction_count(my_address)

# 实例化合约对象
storage = w3.eth.contract(address=contract_address, abi=abi)
# 调用addPerson方法
transaction = storage.functions.addPerson('二两', 28).buildTransaction({
    "chainId": chain_id,
    "from": my_address,
    "nonce": nonce
})
# 签名
signed_transaction = w3.eth.account.sign_transaction(transaction, private_key=private_key)
# 发送交易
tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction)
print('add new Person to contract...')
# 等待交易完成
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
# 获得people数组中存储的值
result = storage.functions.people(0).call()
print(f'get person info: {result}')

因为 addPerson 方法会改变区块链,即需要消耗 Gas 的交易行为,这类行为都需要使用私钥进行签名,然后才能发送交易,调用完 addPerson 函数后,再从 people 数组获取下标为 0 的数据。

这里提一下 ABI,让大家有更直观的理解,在上述代码中,为啥可以调用 addPerson 函数和 people 数组?因为编译后获得的智能合约的 ABI 中存在 addPerson 与 people,复制 compiled_code.json 中 abi 的内容:

"abi": [
                    {
                        "inputs": [
                            {
                                "internalType": "string",
                                "name": "_name",
                                "type": "string"
                            },
                            {
                                "internalType": "uint256",
                                "name": "_age",
                                "type": "uint256"
                            }
                        ],
                        "name": "addPerson",
                        "outputs": [],
                        "stateMutability": "nonpayable",
                        "type": "function"
                    },
                    {
                        "inputs": [
                            {
                                "internalType": "uint256",
                                "name": "",
                                "type": "uint256"
                            }
                        ],
                        "name": "people",
                        "outputs": [
                            {
                                "internalType": "string",
                                "name": "name",
                                "type": "string"
                            },
                            {
                                "internalType": "uint256",
                                "name": "age",
                                "type": "uint256"
                            }
                        ],
                        "stateMutability": "view",
                        "type": "function"
                    }
                ],

以 addPerson 函数为例,其 type 为 function,name 为 addPerson,inputs 表示调用该方法需传入的参数,也给出了 type,通过 abi,程序才知道当前的智能合约提供什么功能。

部署到 Rinkeby 测试网络

通过上面的操作,我们已经可以将智能合约部署到测试网络中了,那如何部署到测试网络中?Web3.py 不像 Remix IDE 提供 Inject Web3 的功能,要部署到测试网络,我们需要借助第三方服务,与之相关的服务有:alchemy、infura 等。

简单而言,在这些服务对应的网站上,注册账号,创建应用,然后拿到开发用的 key,然后使用这个 key 与这些服务交互,我们会连接到这些服务上,然后服务会为我们将应用发布到测试网络或以太坊主网上,与我们平时使用百度、高德等 API 平台没啥差别,都是创建应用获得 key。

这里我们使用 infura 服务,infura 服务大体的工作方式如下图,简单来说,我们不需要将自己本地的计算机加入到以太坊网络中,成为其中的节点(挺麻烦的,要拉数据、足够的网速和足够的硬盘空间),而是直接通过 infura 服务连接(本质是使用 infura 的节点)。

从图中可知,我们通过 infura 提供的 ITX API 便可以与以太坊网络交互了,然后你创建应用,在应用的设置页,可以看到相应的信息,需要注意的是,【ENDPOINTS】处需要选择 rinkeby 测试网络,如下图:

有了这些设置后,我们修改一下.env 文件中的内容:

RINKEBY_RPC_URL=https://rinkeby.infura.io/v3/<project_id>
ACCOUNT_ADDRESS=<账号地址>
PRIVATE_KEY=<对应的私钥>
CHAIN_ID=4

RINKEBY_RPC_URL 给我 Infura 给的 http 地址,ACCOUNT_ADDRES 与 PRIVATE_KEY 可以在 MetaMask 钱包中获取(获取 Rinkeby 上的),为了方便,我将 CHAIN_ID 也放到.env 中了,不同的链具有通过的 CHAIN_ID,可以通过 https://chainlist.org/ 查询:

代码中连接网络的方式不需要改变,只是我们将 CHAIN_ID 抽到.env 中了,getenv 函数会返回字符串格式,需要强转一下。

w3 = Web3(Web3.HTTPProvider(os.getenv("RINKEBY_RPC_URL")))
chain_id = int(os.getenv("CHAIN_ID"))

然后我们部署,然后调用合约中的方法,使用 play_storage.py 时,因为合约地址变了,所以你需要同步修改一下 contract_address 变量,调用后,可以通过 etherscan 查看:

项目代码:GitHub - ayuLiao/web3py_storage: use web3.py play ethereum contract

如果你在使用 Infura 时,发现总是 403,可以尝试删除掉原本的 project,创建一个新的 project。

Brownie 基础

上面通过 Web3.py 实现了智能合约的部署与交互,可以发现还是比较麻烦的,每次触发交易时,都需要进行签名操作等,Brownie 框架基于 Web3.py,它将很多步骤都帮我们静默完成了,如果你不了解 Web3.py,直接上 Brownie 框架,个人感觉也不好,因为会显得比较黑盒。

安装 Brownie

我们通过 pip 安装一下 Brownie,阅读文档,会发现 Brownie 建议使用 pipx 来安装,pipx 会在全局创建一个虚拟环境,然后将 Brownie 安装在虚拟环境中,研究后发现,这是因为 Brownie 依赖比较多,安装过程比较慢,如果你通过 venv 方式,每个项目都要来一次,挺费时间的,因我 Windows 的环境问题,我懒得折腾,我自己管理员开 Terminal 直接 pip 安装:

pip install eth-brownie

安装完后,根据文档,我们还需要安装一下 ganache-cli(github.com/trufflesuite/ganache),命令行版的 ganache,npm 全局安装一下则可。

npm install ganache-cli@latest --global

在 Terminal 中输入 brownieganache-cli 都可以正常使用则表示安装成功。

快速使用 Brownie

创建名为【brownie_storage】的文件夹,进入该文件夹,然后通过 brownie init 初始化项目,会获得如下结构,每个文件夹的作用也标准了:

C:\\USERS\\AYU\\WORKPLACE\\BLOCKCHAIN\\BROWNIE_STORAGE
├───build                # 编译、部署等结果存放目录
│   ├───contracts
│   ├───deployments
│   └───interfaces
├───contracts            # 智能合约的目录
├───interfaces           # 接口的目录
├───reports              # JSON报告文件的目录(使用GUI的用户才会使用)
├───scripts              # 脚本的目录
└───tests                # 测试脚本目录

在使用 Brownie 编写代码前,先使用 ganache-cli 启动本地的以太坊网络,方便测试:

然后,我们将 Storage.sol 复制到 contracts 目录中,通过 brownie compile 命令编译智能合约,该命令会将 contracts 目录下所有的智能合约都进行编译,编译完成后,在 build/contracts 会出现同名的 json 文件,与 Web3.py 类似,这里记录着智能合约的 bytecode、abi 等信息。

完成编译后,接着进行部署,在 scripts 目录下创建 deploy.py,其代码如下:

from brownie import accounts, config, network, Storage


def deploy_storage():
    account = get_account()
    # Instantiate Storage contract
    storage = Storage.deploy({"from": account})
    # call addPerson function
    transaction = storage.addPerson('二两', 28, {"from": account})
    # wait transaction finish
    transaction.wait(1)
    # call people function to get data from people array
    result = storage.people(0)
    print('result: ', result)


def get_account():
    if network.show_active() == 'development':
        return accounts[0]
    else:
        # add new account to brownie accounts
        # account config data from brownie-config.yaml
        return accounts.add(config['wallets']['account_key'])

def main():
    deploy_storage()

在 Windows 中,brownie 不支持 python 中有中文注释,估计是没有兼容好。

相比于 Web3.py,brownie 简单了很多,你只需导入 Storage,然后调用其 deploy 方法则可,因为 Storage 其实是动态载入的,brownie 本身并没有这个类,所以我们不可以直接通过 python 去运行 deploy.py 文件,而是需要使用 brownie run .\\scripts\\deploy.py 命令去运行:

上述代码中,定义了 get_account 函数,该函数会判断当前处于哪个区块链,从而使用想要的方式获得 account,brownie 默认处于 development(本地开发网络),如果不处于 development,则通过 brownie 提供的 accounts.add 函数添加账户对象,比如后面我们会部署到 Rinkeby,就需要从钱包里拿私钥(账号公钥信息可以通过私钥推导获得),这里为了方便,直接放在配置文件中。

brownie 提供的 config 模型,会自动从项目根目录的 brownie-config.yaml 中获取,在这里,该文件内容如下:

dotenv: .env
wallets:
  from_key: ${PRIVATE_KEY}

因为私钥比较重要,也规范一些,这里通过 ${PRIVATE_KEY} 导入项目根目录下.env 文件中的内容。

此外,我们还可以使用 brownie console,进入 brownie 提供的交互式命令环境,在该环境里,你可以使用 brownie 中的任何功能。

> brownie console

Brownie v1.17.0 - Python development framework for Ethereum

BrownieStorageProject is the active project.
c:\\program files\\python37\\lib\\site-packages\\brownie\\network\\main.py:46: BrownieEnvironmentWarning: Development network has a block height of 6
  BrownieEnvironmentWarning,
Attached to local RPC client listening at '127.0.0.1:8545'...
Brownie environment is ready.
>>> from brownie import network
>>> network.show_active()
'development'
>>> from brownie import accounts
>>> account = accounts[0]
>>> from brownie import Storage
>>> storage = Storage.deploy({"from": account})
Transaction sent: 0xd7269730fb3ee3a642391c338234f9cb63993b7bd991316971c89ca6406cebe7
  Gas price: 0.0 gwei   Gas limit: 6721975   Nonce: 6
  Storage.constructor confirmed   Block: 7   Gas used: 243848 (3.63%)
  Storage deployed at: 0x500F5EDceE38597164c26606E93e92D059853a46

>>> transaction = storage.addPerson('二两', 28, {"from": account})
Transaction sent: 0x2aa19410ddc316413f54a6e1c25f6a5878b7a0877fa65a5bec80f380ba3c64aa
  Gas price: 0.0 gwei   Gas limit: 6721975   Nonce: 7
  Storage.addPerson confirmed   Block: 8   Gas used: 84259 (1.25%)

>>> transaction.wait(1)
  Storage.addPerson confirmed   Block: 8   Gas used: 84259 (1.25%)

进行单元测试

智能合约通常与钱相关,做好测试是非常有必要的。brownie 使用 pytest 来实现单元测试,至于 pytest,用过的都说好),在 tests 目录创建名为 test_storage.py 的文件,代码如下:

from brownie import Storage, accounts

def test_deploy():
    account = accounts[0]
    storage = Storage.deploy({"from": account})
    transaction = storage.addPerson('二两', 28, {"from": account})
    transaction.wait(1)
    # call people function to get data from people array
    result = storage.people(0)
    assert result == ('二两', 28)

很常规的单元测试代码,可以将智能合约部署的过程与 CI/CD 流程结合,每次部署都过一遍所有的单元测试,从而让合约更加健硕。

使用 Brownie 将合约部署到测试网络

阅读文档发现,在 Brownie 中通过 Infura 服务进行合约的部署,只需要配置一下则可,文档内容:https://eth-brownie.readthedocs.io/en/latest/network-management.html#using-infura

除了可以通过 export 的方式添加 WEB3_INFURA_PROJECT_ID 环境变量,我们还可以将 WEB3_INFURA_PROJECT_ID 直接添加到.env 中(文档里没写)。

WEB3_INFURA_PROJECT_ID 就是 Infura 为你提供的 Project ID,此外,因为要连接测试网络,所以部署时需要连接测试网络中的账号,你需要将你账号的私钥也放到.env 中。

PRIVATE_KEY= <你账号的私钥>
WEB3_INFURA_PROJECT_ID=<Infura中的Project ID>

然后通过 brownie run .\\scripts\\deploy.py --network rinkeby 运行则可完成部署。

brownie 提供了多种网络,所以我们部署时不需要做额外操作,直接指定对应的网络则可。

当然,后续开发时,我们还可以 brownie networks add 命令添加新的网络。

项目代码:https://github.com/ayuLiao/brownie_storage

结尾

这篇文章只是简单的介绍了 Brownie 的一些操作,Brownie 还具有很多高级功能,比如 Mock、Fork 一个区块链到本地进行开发、又比如 Brownie 提供了 Debug Tools 供你进行调试开发,后续的文章会分享这些内容。

最后提一嘴,Brownie 的文档是很好的学习资料。

以上是关于Brownie 开发智能合约(入门使用)的主要内容,如果未能解决你的问题,请参考以下文章

用基于 Python 的开发框架 Brownie 部署以太坊智能合约

用基于 Python 的开发框架 Brownie 部署以太坊智能合约

brownie部署与测试智能合约

区块链入门教程--开发编译部署调用HelloWorld合约

智能合约从入门到精通:Solidity汇编语言

智能合约从入门到精通:Solidity语言的开发规范和开发流程