上篇文章,使用了 Remix 在线 IDE,个人感觉 Remix 在入门智能合约开发时,是很好的上手工具,因为 Remix 帮我们处理好了编译、部署的过程,并且还通过 JavaScript VM 准备好了本地区块链方便我们测试,可谓开箱即用,但毕竟是线上 IDE,功能还是有限。
这里我们使用 Brownie 框架来开发智能合约,Brownie 框架是基于 Python 编写的智能合约开发框架,它可以帮我们快速完成编译、部署、测试等智能合约开发的全流程。
文档:https://eth-brownie.readthedocs.io/en/stable/
因为 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")
部署的流程比较简单,直接给出代码:
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
部署完后,我们获得了合约部署后的地址,使用该地址,可以构建出合约对象,然后我们就可以调用合约里的方法了。回顾一开始我们编写的合约,其实只有 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,程序才知道当前的智能合约提供什么功能。
通过上面的操作,我们已经可以将智能合约部署到测试网络中了,那如何部署到测试网络中?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/
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。
上面通过 Web3.py 实现了智能合约的部署与交互,可以发现还是比较麻烦的,每次触发交易时,都需要进行签名操作等,Brownie 框架基于 Web3.py,它将很多步骤都帮我们静默完成了,如果你不了解 Web3.py,直接上 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 中输入 brownie
与 ganache-cli
都可以正常使用则表示安装成功。
创建名为【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 中通过 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=
然后通过 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 的文档是很好的学习资料。