想要透過 Python 存取 Ethereum,從 Ethereum 官方的 Github 中可以看到有兩種套件可以達成:web3.py [1] 和 pyethereum [2]。就我目前的理解來說,兩者的差別在於 web3.py 主要是作為外部存取 Ethereum 的客戶端,也就是說 web3.py 函式庫本身不會成為區塊鏈節點,也不會進行區塊鏈同步,而是連接一個區塊鏈上的節點,把區塊鏈當成像是外部資料庫一樣取用而已;而 pyethereum 則比較像是 geth 那樣,是用來把自己做成一個區塊鏈節點,會正常進行區塊同步,也可以作為礦工開始挖礦。
在本篇當中,因為是想要一個輕量級的客戶端來與區塊鏈互動,並不想要準備龐大的儲存空間來存放區塊鏈的資料,因此會以 web3.py 為主。
設定 web3.py 執行環境
web3.py 可以直接透過 pip 安裝。
pip install web3
需注意的是,在 Windows 上想安裝時,會需要事先安裝 Visual C++ Builder,否則在安裝的最後階段會因為無法編譯而失敗。
透過 web3.py 連結 Ethereum 節點
web3.py 因為自身不會作為一個區塊鏈的節點存在,因此它需要有一個節點用來存取區塊鏈上的資料。一般來說最安全的方式應該是自己使用 geth 或者 parity 來自建節點,不過如果在不想要自建節點的狀況時,可以考慮看看 infura [4] 提供的 HTTP 節點服務。
以 infura 現在的 API 來說,如果要連結 Ropsten 測試鏈,連結的網址是 https://ropsten.infura.io/v3/api_key,其中 api_key 要去註冊帳號才取得。以下的程式碼仿照了 web3.py 內建的 auto.infura 的作法,會從環境變數讀取 INFURA_API_KEY 這個參數來組出 infura.io 的 HTTP 位址,用來建立跟 Ropsten 測試鏈的連線。
import os from web3 import ( HTTPProvider, Web3, ) INFURA_ROPSTEN_BASE_URL = 'https://ropsten.infura.io/v3' def load_infura_url(): key = os.environ.get('INFURA_API_KEY', '') return "%s/%s" % (INFURA_ROPSTEN_BASE_URL, key) w3 = Web3(HTTPProvider(load_infura_url()))
存取區塊鏈上的 ERC20 合約
在開始存取合約之前,需要先談談什麼是 ABI [5]。在 Ethereum 中,因為合約都是以編譯過的 binary code 形式存在,因此其實函式庫沒辦法直接知道合約傳輸的內容到底是什麼,因為合約的回傳值全都是 binary。因此在操作合約之前,需要提供一份 ABI 文件,告訴函式庫如何使用合約。
# Assume the contract we're going to invoke is a standard ERC20 contract. with open("erc20.abi.json") as f: erc20_abi = json.load(f) # Web3 accept only checksum address. So we should ensure the given address is a # checksum address before accessing the corresponding contract. contract_addr = w3.toChecksumAddress('0x4e470dc7321e84ca96fcaedd0c8abcebbaeb68c6'); erc20_contract = w3.eth.contract(address=contract_addr, abi=erc20_abi) for func in erc20_contract.all_functions(): logger.debug('contract functions: %s', func) logger.debug("Name of the token: %s", erc20_contract.functions.name().call())
這裡假設我們想存取 Ropsten 測試鏈上位址是 0x4e470dc7321e84ca96fcaedd0c8abcebbaeb68c6 的智能合約。這個合約是透過 etherscan 隨便找的某個 ERC20 的合約,因此可以用標準的 ERC20 的 ABI [6] 來存取它。我們在建立這個合約的 instance 時,先跑一個迴圈印出合約內所有的 function(這個步驟其實是在列出 ABI 上的資訊),接著試著呼叫合約中的 name() 來取得這個合約宣告的代幣名稱。最後輸出的內容如下:
2018-09-07 15:02:53,815 | __main__ | DEBUG | contract functions: <Function name()> 2018-09-07 15:02:53,816 | __main__ | DEBUG | contract functions: <Function approve(address,uint256)> 2018-09-07 15:02:53,824 | __main__ | DEBUG | contract functions: <Function totalSupply()> 2018-09-07 15:02:53,824 | __main__ | DEBUG | contract functions: <Function transferFrom(address,address,uint256)> 2018-09-07 15:02:53,824 | __main__ | DEBUG | contract functions: <Function decimals()> 2018-09-07 15:02:53,824 | __main__ | DEBUG | contract functions: <Function balanceOf(address)> 2018-09-07 15:02:53,824 | __main__ | DEBUG | contract functions: <Function symbol()> 2018-09-07 15:02:53,825 | __main__ | DEBUG | contract functions: <Function transfer(address,uint256)> 2018-09-07 15:02:53,825 | __main__ | DEBUG | contract functions: <Function allowance(address,address)> 2018-09-07 15:02:54,359 | __main__ | DEBUG | Name of the token: KyberNetwork
簽署並送出交易
在上面的例子中,呼叫智能合約時是直接呼叫合約裡的 function,但這一般只能用在讀取區塊鏈上的資料的狀況。如果是想要透過呼叫智能合約來寫入資料到區塊鏈,就必須要用另一種方式 [7] 來呼叫合約,也就是必須先簽署交易,然後付 gas 去執行這個交易。
假設我們一樣是要呼叫一個 ERC20 的合約,要執行合約上的 transferFrom() 這個函式。transferFrom() 需要三個參數 _from、_to、_value,表示要從 _from 帳號轉帳給 _to 帳號,轉帳金額是 _value。
# Set the account which makes the transaction. account = w3.toChecksumAddress(os.environ.get('ETHEREUM_ACCOUNT', '')) w3.eth.defaultAccount = account # Web3 accept only checksum address. So we should ensure the given address is a # checksum address before accessing the corresponding contract. contract_address = w3.toChecksumAddress('0x4e470dc7321e84ca96fcaedd0c8abcebbaeb68c6') contract = w3.eth.contract(address=contract_address, abi=contract_abi) # Prepare the necessary parameters for making a transaction on the blockchain. estimate_gas = contract.functions.transferFrom(account, account, w3.toWei('1', 'eth')).estimateGas() nonce = w3.eth.getTransactionCount(account) # Build the transaction. txn = contract.functions.transferFrom(account, account, w3.toWei('1', 'eth')).buildTransaction({ 'chainId': 3, 'gas': estimate_gas, 'gasPrice': w3.toWei('1', 'gwei'), 'nonce': nonce }) logger.debug('Transaction: %s', txn) # Sign the transaction. private_key = bytes.fromhex(os.environ.get('ETHEREUM_ACCOUNT_PKEY', '')) signed_txn = w3.eth.account.signTransaction(txn, private_key=private_key) tx_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction) logger.debug('Txhash: 0x%s', bytes.hex(tx_hash))
在上面的程式碼中,首先第 2 ~ 3 行先從環境變數中讀取我們要使用的帳號,這個帳號將會用來發送交易,當然要付 gas 時也會從這個帳號扣。第 10 ~ 20 行建立一個原始交易(raw transaction),這個交易中因為我們需要自行指定包括 gas、nonce 等參數,因此需要在前面 11 ~ 12 行確認參數要設定多少。然後最重要的第 25 ~ 26 行就是讀取私鑰,並且用私鑰去簽署交易。這裡假設私鑰的組成會是用 Hex 編碼的文字,所以使用 bytes.fromhex 把 Hex 編碼轉回成 byte 格式。簽好以後就送出交易,送出交易時 API 會回傳 byte 格式的交易的 transaction hash,可以把它編碼後印出來,之後就可以去 etherscan 上查找這筆交易了。
沒有留言:
張貼留言