2018年9月6日 星期四

使用 Python 存取 Ethereum:透過 web3.py

想要透過 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 上查找這筆交易了。

    參考資料
    1. web3.py
    2. pyethereum
    3. web3.py on readthedoc
    4. infura.io
    5. 【Ethereum 智能合約開發筆記】深入智能合約 ABI
    6. Contract ERC20 ABI
    7. Working with Local Private Keys

    沒有留言: