web3.py
是基于python的以太坊庫川蒙,內(nèi)部封裝了對于etereum的rpc請求,這篇文章介紹了使用web3.py v6
與以太坊或以太坊系列的區(qū)塊鏈網(wǎng)絡進行交互的常用方式。
安裝web3.py
pip install web3
加載以太坊節(jié)點:
from web3 import Web3
w3 = Web3(Web3.HTTPProvider('https://<your-provider-url>'))
如果是本地節(jié)點盗扇,例如ganache,地址為http://localhost:8545
如果是線上節(jié)點,一般使用一些節(jié)點服務商提供的節(jié)點地址,例如Infura
夺脾、quicknode
、alchemy
等茉继。小狐貍metamast只是個錢包入口咧叭,它不提供節(jié)點api,它本身使用的也是infur提供的節(jié)點烁竭。智能合約開發(fā)框架truffle
菲茬、truffle
都提供了本地用于測試的私鏈,同樣可以通過節(jié)點連接派撕。
獲取區(qū)塊詳情
w3.eth.get_block('latest')
這個方法可以使用'latset'獲取最新塊婉弹,也可以指定高度、區(qū)塊哈希獲取指定高度终吼,返回一堆區(qū)塊頭數(shù)據(jù)
{'difficulty': 1,
'gasLimit': 6283185,
'gasUsed': 0,
'hash': HexBytes('0x0'),
'logsBloom': HexBytes('0x000'),
'miner': '0x0',
'mixHash': HexBytes('0x0'),
'nonce': HexBytes('0x0'),
'number': 0,
'parentHash': HexBytes('0x000'),
'proofOfAuthorityData': HexBytes('0x000'),
'receiptsRoot': HexBytes('0x0'),
'sha3Uncles': HexBytes('0x0'),
'size': 622,
'stateRoot': HexBytes('0x0'),
'timestamp': 0,
'totalDifficulty': 1,
'transactions': [],
'transactionsRoot': HexBytes('0x0'),
'uncles': []}
常用函數(shù)和工具方法
- 獲取當前gasprice
w3.eth.gase_price
镀赌,經(jīng)常需要使用 - 獲取當前區(qū)塊高度
w3.eth.block_number
- 獲取當前鏈ID
w3.eth.chain_id
- 獲取某賬戶的余額
w3.eth.get_balance(account='0x0',block_identifier=111111)
注意是eth余額,不是token的.注意第二個參數(shù)block_identifier
可以指定區(qū)塊高度际跪,或者區(qū)塊哈希 -
Web3.toChecksumAddress(address)
:將地址(字符串類型)轉(zhuǎn)換為校驗和形式商佛,以提高地址的安全性蛙粘。 -
Web3.keccak(text)
:計算一個字符串的 Keccak-256 哈希值。在 Solidity 中威彰,也可以使用keccak256
函數(shù)計算一個字符串的哈希值出牧。 -
Web3.is_address(address)
:判斷一個地址是否為合法的以太坊地址。 -
Web3.to_wei(1,'ether')
數(shù)值轉(zhuǎn)化,將ether轉(zhuǎn)為wei -
Web3.from_wei(1000000000000000000, 'ether')
:將單位wei轉(zhuǎn)化為其他單位
以太坊原生交易
受益于以太坊的常規(guī)賬戶模型歇盼,轉(zhuǎn)賬的邏輯要比BTC的UTXO模型要簡潔清晰得多舔痕,在以太坊系統(tǒng)中從A向B轉(zhuǎn)賬,就是把A賬戶中的余額扣除豹缀,把B賬戶中的余額增加伯复。以下是轉(zhuǎn)賬的基本流程代碼:
from eth_account.account import Account, LocalAccount, SignedTransaction
from web3 import Web3, HTTPProvider
# 以太坊原生交易s
sender_addr = "0x1111111111111111111111111111..."
sender_private_key = "0xffffffffffffffffffffffff..."
to_address = "0xB1476dFdAD625D787A006Ed362EBa5872a88Ae1A"
w3 = Web3(HTTPProvider("http://127.0.0.1:8545"))
## 1、構(gòu)建交易數(shù)據(jù)
tx = {
'from': sender_addr, # 如果傳入的地址不是發(fā)送者的地址邢笙,那么會出現(xiàn)typeerror的錯誤
'to': to_address,
'value': w3.to_wei(1, 'ether'), # 這里的單位是wei,10**18 wei = 1 ether
'gas': 21000,
'gasPrice': w3.eth.gas_price,
'nonce': w3.eth.get_transaction_count(sender_addr)
}
## 2啸如、簽名交易
account: LocalAccount = Account.from_key(sender_private_key)
sign_tx: SignedTransaction = account.sign_transaction(tx)
## 3、發(fā)送交易
tx_hash = w3.eth.send_raw_transaction(sign_tx.rawTransaction)
## 4氮惯、阻塞等待交易結(jié)果
result = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60)
print(result)
注意:
- 交易數(shù)據(jù)中
from
這個字段可以不傳的叮雳,但如果使用,必須保證地址與使用的私鑰匹配妇汗,否則無法進行簽名 - 交易能否執(zhí)行成功與gas有關(guān)帘不,gas過低會造成以太坊虛擬機無法執(zhí)行事務,高了會返回多余gas杨箭,原生交易固定為21000寞焙,其他交易可以預估
web3.eth.estimate_gas(tx)
,但一般直接上限寫死即可互婿,比如100000 - 能否上鏈被礦工打包與gas無關(guān)捣郊,與
gasPrice
相關(guān),gas價格太低了礦工嫌棄你慈参,太高了你嫌棄礦工呛牲,web3.py提供了預估當前最佳gasprice的方法w3.eth.gase_price
,你和礦工都滿意 -
value
這個字段是交易的值懂牧,單位是Wei侈净,對于原生交易,這個值表示從A轉(zhuǎn)給B的錢僧凤,對于合約交易,這個值一般為0元扔。 -
nonce
這個值是記錄交易發(fā)起者交易序列的躯保,每次轉(zhuǎn)賬都比上一次的nonce加1,使用wb3.eth.get_transaction_count(sender)
獲取sender的nonce值(注意澎语,這個值已經(jīng)進行了加1) - web3.py的簽名過程不同版本還不一樣途事,這里使用的是
v6.1.0
- 這里使用的是eth_account提供的Account進行簽名验懊,對于原生交易和合約交易效果一樣,但可能對于一些新EIP的交易類型不支持
- 簽名的兩個參數(shù)尸变,一個是需要簽名的交易數(shù)據(jù)义图,一個是私鑰,注意私鑰的保密
- 簽名完成后會生成Raw交易數(shù)據(jù)召烂,結(jié)構(gòu)如下碱工,實際上最后用到的只有
rawTransaction
這個字段
{
'hash': HexBytes('0x126431f2a7fda003aada7c2ce52b0ce3cbdbb1896230d3333b9eea24f42d15b0'),
'r': 110093478023675319011132687961420618950720745285952062287904334878381994888509,
'rawTransaction':HexBytes('0x........'),
's': 33674551144139401179914073499472892825822542092106065756005379322302694600392,
'v': 0}
注意,發(fā)送過程是異步的奏夫,以太坊從交易發(fā)送到節(jié)點確認需要一定的時間怕篷,主網(wǎng)一般10~15秒,goerli
這類測試網(wǎng)要快一點酗昼,如arbitrum
這類L2的網(wǎng)絡最快基本上1~3秒廊谓。
發(fā)送交易后可以等待交易確認再執(zhí)行后續(xù)操作,使用方法:
resp = w3.eth.wait_for_transaction_receipt(tx_hash)
這個方法其實是一個死循環(huán)在請求節(jié)點獲取交易哈希狀態(tài)結(jié)果麻削,默認超時120秒
可以將上述流程封裝成函數(shù)蒸痹,方便調(diào)用
from eth_account.account import Account, LocalAccount, SignedTransaction
from hexbytes import HexBytes
from web3 import Web3, HTTPProvider
def transfer(w3: Web3, account: LocalAccount, to: str, value: int, gas=21000) -> HexBytes:
"""
交易、普通轉(zhuǎn)賬
:param w3: 以太坊客戶端Web3實例
:param account: 以太坊賬戶呛哟,由eth-account提供
:param to: 接收地址
:param value: 轉(zhuǎn)賬金額,單位時wei,1 ether=10**18 wei
:param gas: gas消耗电抚,默認21000,不能比這個低
:return: 交易哈希 字節(jié)數(shù)組
"""
## 1竖共、構(gòu)建交易數(shù)據(jù)
tx = {
'from': account.address,
'to': to,
'value': value,
'gas': gas,
'gasPrice': w3.eth.gas_price,
'nonce': w3.eth.get_transaction_count(account.address)
}
## 2蝙叛、簽名
sign_tx: SignedTransaction = account.sign_transaction(tx)
## 3、發(fā)送
return w3.eth.send_raw_transaction(sign_tx.rawTransaction)
if __name__ == '__main__':
w3 = Web3(HTTPProvider(endpoint_uri='http://127.0.0.1:8545'))
account = Account.from_key("<你的私鑰>")
tx_hash = transfer(w3, account, '<接收地址>', 1*10**17)
print(f"Success Transfer Hash:{tx_hash.hex()}")
result = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f"Transfer Receipt:{result}")
調(diào)用合約方法
智能合約一旦部署就不允許再修改公给,后續(xù)對于合約的使用絕大部分集中在合約方法調(diào)用和過濾合約日志
- 智能合約的方法從外部調(diào)用的角度可以分為兩種借帘,一種是view方法,一種是非view方法淌铐。
- view方法只是從區(qū)塊鏈上查詢數(shù)據(jù)肺然,不會進行數(shù)據(jù)更改,不需要調(diào)用者進行簽名操作
- 非view方法需要修改區(qū)塊鏈上數(shù)據(jù)腿准,需要調(diào)用者進行簽名操作
- 調(diào)用合約需要生成一個合約實例际起,這取決于兩個東西,一是合約
address
吐葱,二是合約ABI
在本文中街望,需要部署一個簡單的合約到區(qū)塊鏈上:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.9;
contract Demo {
mapping(address => uint) private _ages;
function setAge(uint age) public {
_ages[msg.sender] = age;
}
function getAge(address user) public view returns (uint) {
return _ages[user];
}
}
在上面的合約中,提供了一個狀態(tài)變量_ages
來記錄地址和年齡的映射關(guān)系弟跑,并提供了兩個方法灾前,setAge(uint age)
設置調(diào)用者的年齡,getAge(address user)
獲取指定地址的年齡孟辑。
調(diào)用合約的View方法:
對于View方法哎甲,調(diào)用方法分為幾步:
- 加載Web3實例
- 加載合約實例
- 獲取合約方法對象
- 使用
call()
方法調(diào)用
from web3 import Web3, HTTPProvider
# 連接以太坊節(jié)點
w3 = Web3(HTTPProvider(endpoint_uri='http://127.0.0.1:8545'))
# 獲取合約實例
contract_address = '<合約地址>'
contract_abi = [] # 合約abi,這是一個list結(jié)構(gòu)
contract_instance = w3.eth.contract(address=contract_address, abi=contract_abi)
# 獲取View函數(shù)對象并調(diào)用
func = getattr(contract_instance.functions, 'getAge')(user='0xC3845C061D0e0929744B01CbB6c23c31402B3E3a')
result=func.call()
print(f"result={result})
調(diào)用合約非View方法
對于非View方法蔫敲,則分為下面幾步:
- 加載Web3實例
- 加載合約實例
- 獲取合約方法對象
- 將方法對象綁定到交易數(shù)據(jù)中的
data
字段中 - 對交易數(shù)據(jù)進行簽名
- 發(fā)送簽名后的tx
from eth_account.account import Account, SignedTransaction
from web3 import Web3, HTTPProvider
# 調(diào)用者地址
sender_address = '<調(diào)用者地址>'
sender_privatekey = '<調(diào)用者私鑰>'
# 連接以太坊節(jié)點
w3 = Web3(HTTPProvider(endpoint_uri='http://127.0.0.1:8545'))
# 獲取合約實例
contract_address = '0x6EC21D47FCF6F06eeE9b36Ee85Ae25417b44697a'
contract_abi = [] # 合約ABI,這是一個List結(jié)構(gòu)
contract_instance = w3.eth.contract(address=contract_address, abi=contract_abi)
# 獲取調(diào)用函數(shù)對象
func = getattr(contract_instance.functions, 'setAge')(age=18)
# 構(gòu)造交易數(shù)據(jù)
tx = func.build_transaction({
'nonce': w3.eth.get_transaction_count(sender_address),
'value': 0,
'gas': 100000,
'gasPrice': w3.eth.gas_price})
sign_tx: SignedTransaction = Account.sign_transaction(tx, sender_privatekey)
tx_hash = w3.eth.send_raw_transaction(sign_tx.rawTransaction)
print(tx_hash.hex())
同樣炭玫,可以將上述過程封裝成函數(shù)奈嘿,方便調(diào)用:
from eth_account.account import Account, LocalAccount, SignedTransaction
from web3 import Web3, HTTPProvider
def call_contract_sign_func(w3: Web3, account: LocalAccount, address: str, abi: list, func_name: str,
gas=200000, value=0, **kwargs):
"""
調(diào)用合約非View方法
:param w3: Web3 實例
:param account: 賬戶實例
:param address: 合約地址
:param abi: 合約ABI
:param func_name: 合約方法名
:param gas: 燃氣,默認200000
:param value: 附帶轉(zhuǎn)賬金額,默認0
:param kwargs: 方法參數(shù)
:return: 交易哈希字節(jié)數(shù)組
"""
contract_instance = w3.eth.contract(address=address, abi=abi)
func = getattr(contract_instance.functions, func_name)(**kwargs)
# 構(gòu)造交易數(shù)據(jù)
tx = func.build_transaction({
'nonce': w3.eth.get_transaction_count(account.address),
'value': value,
'gas': gas,
'gasPrice': w3.eth.gas_price})
# 簽名
sign_tx: SignedTransaction = account.sign_transaction(tx)
# 發(fā)送
return w3.eth.send_raw_transaction(sign_tx.rawTransaction)
def call_contract_view_func(w3, address, abi, func_name, **kwargs):
"""
調(diào)用合約View方法
:param w3:Web3實例
:param address:合約地址
:param abi:合約ABI
:param func_name:合約方法名稱
:param kwargs:合約方法參數(shù)
:return:函數(shù)執(zhí)行結(jié)果
"""
contract_instance = w3.eth.contract(address=address, abi=abi)
func = getattr(contract_instance.functions, func_name)(**kwargs)
return func.call()
if __name__ == '__main__':
w = Web3(HTTPProvider(endpoint_uri='http://127.0.0.1:8545'))
contract_address = '<合約地址>'
contract_abi = [] # 合約abi
caller: LocalAccount = Account.from_key('<賬戶私鑰>')
## 調(diào)用view方法
result = call_contract_view_func(w3=w, address=contract_address, abi=contract_abi, func_name='getAge',
user='0xC3845C061D0e0929744B01CbB6c23c31402B3E3a')
print(result)
## 調(diào)用非View方法
tx_hash = call_contract_sign_func(w3=w, account=caller, address=contract_address, abi=contract_abi,
func_name='setAge',
age=8877)
print(tx_hash.hex())
## 等待并返回調(diào)用結(jié)果
resp = w.eth.wait_for_transaction_receipt(tx_hash)
print(resp)
上面的代碼中可以看出調(diào)用合約非view方法實際上就是一次轉(zhuǎn)賬交易吞加,在交易中把對合約方法的調(diào)用信息寫入交易數(shù)據(jù)的data
字段中裙犹,剩下的就是evm需要考慮的事情了。
過濾合約日志
日志的過濾是合約使用的另一個重要方面榴鼎,為了便于日志演示伯诬,對上面使用的合約進行簡單的改造,加上event:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.9;
contract Demo {
// 日志
event SetAge(address user, uint age);
mapping(address => uint) private _ages;
function setAge(uint age) public {
_ages[msg.sender] = age;
// 發(fā)射日志
emit SetAge(msg.sender, age);
}
function getAge(address user) public view returns (uint) {
return _ages[user];
}
}
過濾合約日志分如下幾步:
- 加載web3實例
- 加載合約實例
- 獲取合約事件對象
- 創(chuàng)建事件過濾器
- 檢索過濾后的全部事件
from web3 import Web3, HTTPProvider
contract_address = '<合約地址>'
contract_abi = [] # 合約ABI
# 加載Web3實例
w3 = Web3(HTTPProvider(endpoint_uri='http://127.0.0.1:8545'))
# 加載合約實例
contract_instance = w3.eth.contract(address=contract_address, abi=contract_abi)
# 獲取event對象
my_event = contract_instance.events.SetAge()
# 創(chuàng)建事件過濾器巫财,
my_filter = my_event.create_filter(fromBlock=1, toBlock=28)
# 檢索事件
for event in my_filter.get_all_entries():
print(dict(event))
返回的event數(shù)據(jù)如下盗似,包含了日志的基本信息:
- 區(qū)塊信息:區(qū)塊高度、哈希
- 交易哈希平项、交易哈希索引
- 日志索引
- event名
- event 數(shù)據(jù)信息
{
'args':AttributeDict({'user': '0xC3845C061D0e0929744B01CbB6c23c31402B3E3a', 'age': 8877}),
'event': 'SetAge',
'logIndex': 0,
'transactionIndex': 0,
'transactionHash': HexBytes('0x0975e324106b995512a331f0fa29e8af1ef607e41753c84cacf5a6009dda34e7'),
'address': '0x9d95b127281335A846E4f36e36D45963e452d3D4',
'blockHash': HexBytes('0x1b67007d4f40c93c1e881a32299541cffe837b8acc64724ac093ae7d9bc4e832'),
'blockNumber': 28
}
上面的代碼中并沒有體現(xiàn)log的topic信息赫舒,這里需要首先說明 Log 、event闽瓢、topic的關(guān)系接癌,以太坊的Log數(shù)據(jù)結(jié)構(gòu)如下:
```go
type Log struct {
// 日志所屬的區(qū)塊高度
BlockNumber uint64 `json:"blockNumber"`
// 日志所屬的交易哈希
BlockHash common.Hash `json:"blockHash"`
// 日志所屬的區(qū)塊哈希
TxHash common.Hash `json:"transactionHash" gencodec:"required"`
// 日志所屬的交易序列號
TxIndex uint `json:"transactionIndex"`
// 日志在交易中的序列,一個交易可以包含多個日志扣讼,這個字段表示日志在交易中的索引
Index uint `json:"logIndex"`
// 是否被移除缺猛,注意如果鏈進行了分叉,那么本字段會變成true椭符,如果為true荔燎,那么不應該識別為正確的log
Removed bool `json:"removed"`
// 發(fā)送出日志的智能合約地址,注意销钝,只有合約賬戶才能發(fā)送event
Address common.Address `json:"address" gencodec:"required"`
// 日志主題列表有咨,這個字段是為了方便查詢時進行過濾
Topics []common.Hash `json:"topics" gencodec:"required"`
// event攜帶的數(shù)據(jù)字節(jié)數(shù)組
Data []byte `json:"data" gencodec:"required"`
}
}
```
Log 是以太坊中的一個事件記錄,是一個 key-value 對的集合蒸健,其中 key 是字符串類型的 topics座享,value 是一個任意長度的字節(jié)數(shù)組 data。Log 可以被合約 emit似忧,也可以由以太坊系統(tǒng)自動生成渣叛,例如在轉(zhuǎn)賬時生成的 Transfer Log。每筆交易都有可能會產(chǎn)生 Log橡娄。
Event 是合約中的一類特殊函數(shù)诗箍,它允許合約在執(zhí)行過程中,向外部環(huán)境廣播某些內(nèi)容(包括 Log 和數(shù)據(jù))挽唉。Event 可以定義多個參數(shù)滤祖,這些參數(shù)可以是基本類型(如 uint256、string 等)瓶籽,也可以是自定義類型匠童。Event 可以被觸發(fā)多次,并且每次觸發(fā)都會生成一個獨立的 Log塑顺。
Topic 是 Log 中的一個結(jié)構(gòu)汤求。在 Emit Event 的時候,每個 Event 參數(shù)可能會被標記為 indexed 或不 indexed严拒,indexed 的參數(shù)會被記錄在 Event 的 Topic 中扬绪,不 indexed 的參數(shù)則會被記錄在 Log 的 Data 中。具體來說裤唠,Event 的 keccak 哈希會成為 Topic 的第一個元素挤牛,Events 中 indexed 的參數(shù)會按照順序在后面添加。
上面過濾日志的代碼中种蘸,只是通過合約地址和區(qū)塊范圍進行過濾墓赴,如果要過濾event和或者過濾方法參數(shù)那么需要在構(gòu)造過濾器時添加topics字段,
每個log都有一個topic集合航瞭,每個event至少有一個topic诫硕,即event方法的簽名
topic=w3.keccak(text="SetAge(address,uint256)").hex()
如果event的參數(shù)被indexed
修飾,那么也可以生成topic
topic=w3.keccak(text="user(address)").hex()
在構(gòu)建過濾器時添加topics
my_filter = my_event.create_filter(fromBlock=1, toBlock=28, topics=[w3.keccak(text="SetAge(address,uint256)").hex()])
topic
相當于給日志打標簽刊侯,通過標簽可以提高過濾效率章办,但每次要計算哈希還是挺麻煩的,如果logs數(shù)量本身不大滨彻,可以直接全量過濾后再通過event name藕届。
Web3.py 還提供了很多其他有用的工具,比如用于解析和生成合約 ABI 編碼的方法疮绷。但合約的部署翰舌、測試一般由hardhat、truffle這類框架完成冬骚,使用的是js
或者ts
,這方面python的存在感比較低椅贱。
參考: