简介
上篇文章,使用了 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 两种。
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"]
至此,智能合约的编译流程就结束了,然后我们通过 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 中输入 brownie
与 ganache-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 的文档是很好的学习资料。