編輯嚴選
深入了解區塊鏈(二) - 如何使用Python來和區塊鏈進行互動

前言

上一篇文章中,有和您分享了使用Ganache模擬了一個以太坊主網。想必您已經迫不及待的想對它進行更深入的了解了。本篇預計會使用Python來和您建立好的模擬網路進行互動。在開發套件的細節上我們並不會很深入的地為您進行解釋,而會透過一些簡單的例子來引導一個學習的方向。
本篇會需要您先準備好:
  1. Python 3環境 (有Jupyter環境更佳)
  2. 啟動上一篇所設定好的Ganache服務
  3. 安裝 web3.py 套件
PS. 若您是Node.js開發者,您也可以選用web3.js來進行開發,依您的開發習慣為主,web3.py和web3.js應都可以找到對應的方法使用。

讓我們開始吧

在上一篇的文章中,各位是否還記得我們使用了Metamask到Uniswap進行了Token交易?我們是否可以不使用Metamask,直接透過我們自己開發的程式(DApp)來和區塊鏈互動?接下來我們就會使用web3.py來一步一步達成這項功能。
  • (CLI) 啟動Ganache
ganache --fork.network mainnet --chain.networkId 1 --accounts 10 \
--wallet.seed 1337 --port 8545
  • (CLI) 安裝web3.py
pip install web3
以上就準備好基本環境了,首先我們先要知道我們在Uniswap交易使用的智能合約的地址為何?我們可以從Uniswap提供的文件得知這個地址
我們可以到etherscan.io中找到這個合約的資訊,它是一個Uniswap Router合約 (這個合約會根據我們的交易目的決定要跟那些下游合約作互動)
PS. 剛剛開啟的etherscan.io頁面建議保留著,等等還會用到
我們的目標是使用ETH換得1000個USDT,這筆交易的流程將會是以下:
  1. 將價值1000USDT的ETH換成WETH (將ETH封裝成ERC20 Token)
  2. 將這些等價的WETH換成1000 USDT (ERC20 Token pair互換)
  • 首先,開始一些Python的初始化工作
from web3 import HTTPProvider, Web3
import json
import time
#連線到剛啟動的Ganache RPC Server
web3 = Web3(HTTPProvider('http://127.0.0.1:8545'))
block = web3.eth.get_block('latest')
#可以看到目前的區塊高度等資訊
print(block)
  • 將一些關鍵的地址寫在變數中待用
#userAcc和userKey是此範例中區塊鍊新手的錢包地址和私鑰
#!IMPORTANT! 若和您生成的不一致時請自行修改
userAcc = web3.toChecksumAddress('0xC459cc3AC9f8462Be2EF00aAD02fAc7af2e97b14')
userKey = '0xda09f8cdec20b7c8334ce05b27e6797bef01c1ad79c59381666467552c5012e3'
#此為Uniswap router的合約地址,可以用來做Token的交換
UNI_ROUTER_ADDR = web3.toChecksumAddress('0x7a250d5630b4cf539739df2c5dacb4c659f2488d')
#此為封裝以太(WETH)的合約地址
WETH_ADDR = web3.toChecksumAddress('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2')
#此為USDT的合約地址
USDT_ADDR = web3.toChecksumAddress('0xdac17f958d2ee523a2206206994597c13d831ec7')
  • 可以檢查一下目前錢包的餘額
balance_eth = web3.eth.get_balance(userAcc)
print('ETH:', balance_eth, 'wei')
ETH: 1000000000000000000000 wei
接下來,我們就要正式來跟合約互動了。首先我們必須取得Uniswap Router合約的ABI,它是智能合約的介面,讓其他應用可以對這些定義好的介面進行存取。您可以將它簡單的想像成它是智能合約各個Function的定義表,告訴您這個合約有哪些Function可以用,要傳入那些參數...等等。
您可以在剛剛開啟的etherscan.io頁面中找到這項資訊
在Contract → Code中可以找到合約相關資訊
您可以使用此按鈕來複製ABI內容
  • 找到ABI後,複製它的內容,並將它貼到Python的程式中,並以JSON格式讀取
    (您也可以將它寫到一個檔案中方便未來管理)
#首先我們需要先設定Uniswap Router的合約
#ABI介面可以從Etherscan得知: https://etherscan.io/address/0x7a250d5630b4cf539739df2c5dacb4c659f2488d#code
#將ABI字串貼到abi_txt變數裡
abi_txt = '''[{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"amountADesired","type":"uint256"},{"internalType":"uint256","name":"amountBDesired","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"addLiquidity","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"},{"internalType":"uint256","name":"liquidity","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountTokenDesired","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"addLiquidityETH","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"},{"internalType":"uint256","name":"liquidity","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"reserveIn","type":"uint256"},{"internalType":"uint256","name":"reserveOut","type":"uint256"}],"name":"getAmountIn","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"reserveIn","type":"uint256"},{"internalType":"uint256","name":"reserveOut","type":"uint256"}],"name":"getAmountOut","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsIn","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsOut","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"reserveA","type":"uint256"},{"internalType":"uint256","name":"reserveB","type":"uint256"}],"name":"quote","outputs":[{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidity","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidityETH","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidityETHSupportingFeeOnTransferTokens","outputs":[{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityETHWithPermit","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityETHWithPermitSupportingFeeOnTransferTokens","outputs":[{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityWithPermit","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapETHForExactTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactETHForTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactETHForTokensSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForETH","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForETHSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForTokensSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapTokensForExactETH","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapTokensForExactTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]'''
#以JSON格式讀取
abi = json.loads(abi_txt)
  • 接著我們可以使用web3.eth.contract()來實例化這個合約物件
uni_contract = web3.eth.contract(address=UNI_ROUTER_ADDR, abi=abi)
  • 在Uniswap Router合約中,有一個名為swapETHForExactTokens的function,它可以協助我們換取指定的Token
swap = uni_contract.functions.swapETHForExactTokens(
    1000 * 1000000, #想要購買的Token數量,1USDT=1,000,000單位
    [WETH_ADDR, USDT_ADDR], #交易的過程是 ETH → WETH → USDT,可以接受兩種以上的Token交換
    userAcc, #購買者地址
    (int(time.time()) + 10000) #交易等待的最長時間
)
  • 建立交易內容,nonce需要根據交易次數+1
    (實務上要根據token/gas時價進行精確計算,不過是模擬環境就先隨意設定)
#建立TXN內容
txn = swap.buildTransaction({
    'from': web3.toChecksumAddress(userAcc),
    'value': web3.toWei(1,'ether'), #因為要用ETH買ERC20幣,所以要發送足夠的ETH
    'gasPrice': web3.toWei('30','gwei'),
    "gas": 3000000,
    'nonce': web3.eth.get_transaction_count(userAcc), #User的歷史交易次數,從0開始遞增
})
  • 用私鑰簽屬該筆交易後,即可送出交易。Ganache預設會立即處理這筆交易,立即出一個新Block
#用私鑰簽屬交易並送出交易,Ganache預設會立即出塊
signed_txn = web3.eth.account.sign_transaction(txn, private_key=userKey)
tx_token = web3.eth.send_raw_transaction(signed_txn.rawTransaction)
print(web3.toHex(tx_token))
  • 交易成功,我們可以回到Metamask中察看交易結果是不是符合預期呢?
成功換到了1000USDT

後記

在這篇文章中,和大家分享了透過其他語言來和智能合約互動的方式。若您是初次接觸這種互動方式,也恭喜您跨出了DApp開發的第一步。透過Ganache模擬出來的網路,可以放心的做任何嘗試,不用擔心成本問題。未來若有機會開發Solidity智能合約,您也可以將合約上到Ganache中來進行測試,比使用testnet來得快速許多。
感謝您閱讀了這系列第二篇的文章,後續仍會進一步探討Ganache還能做那些有趣的功能,以及那些資料的分析。期待與您的再次相見。

作者Steve目前任職於趨勢科技區塊鏈安全研究小組,專注於區塊鏈合約安全與與相關技術,如果喜歡我的文章,或是想獲得更多區塊鏈詐騙事件懶人包,歡迎關注我的帳號
另外,我已經加入由趨勢科技防詐達人所成立的方格子專題-《區塊鏈生存守則》,在那裡我會跟其他優質的創作者一起帶大家深入瞭解區塊鏈,並隨時向大家更新區塊鏈資安事件
> 追蹤《區塊鏈生存守則》學習如何在區塊鏈的世界保護自己
> 關注防詐達人獲得其他最新詐騙情報
27會員
176內容數
我們整理了web3相關的熱門資安問題,包含加密貨幣投資詐騙、盜版NFT、空投釣魚和區塊鏈重大資安事件懶人包等等,並提供最完整的辨識方法教學,讓大家從0到1學習如何保護自己
留言0
查看全部
發表第一個留言支持創作者!