前言
在
上一篇文章中,有和您分享了使用Ganache模擬了一個以太坊主網。想必您已經迫不及待的想對它進行更深入的了解了。本篇預計會使用Python來和您建立好的模擬網路進行互動。在開發套件的細節上我們並不會很深入的地為您進行解釋,而會透過一些簡單的例子來引導一個學習的方向。
本篇會需要您先準備好:
- Python 3環境 (有Jupyter環境更佳)
- 啟動上一篇所設定好的Ganache服務
- 安裝 web3.py 套件
PS. 若您是Node.js開發者,您也可以選用web3.js來進行開發,依您的開發習慣為主,web3.py和web3.js應都可以找到對應的方法使用。
讓我們開始吧
在上一篇的文章中,各位是否還記得我們使用了Metamask到
Uniswap進行了Token交易?我們是否可以不使用Metamask,直接透過我們自己開發的程式(DApp)來和區塊鏈互動?接下來我們就會使用web3.py來一步一步達成這項功能。
ganache --fork.network mainnet --chain.networkId 1 --accounts 10 \
--wallet.seed 1337 --port 8545
pip install web3
以上就準備好基本環境了,首先我們先要知道我們在Uniswap交易使用的智能合約的地址為何?我們可以從
Uniswap提供的文件得知這個地址
我們可以到etherscan.io中找到這個
合約的資訊,它是一個Uniswap Router合約 (這個合約會根據我們的交易目的決定要跟那些下游合約作互動)
PS. 剛剛開啟的etherscan.io頁面建議保留著,等等還會用到
我們的目標是使用ETH換得1000個USDT,這筆交易的流程將會是以下:
- 將價值1000USDT的ETH換成WETH (將ETH封裝成ERC20 Token)
- 將這些等價的WETH換成1000 USDT (ERC20 Token pair互換)
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後,複製它的內容,並將它貼到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中察看交易結果是不是符合預期呢?
後記
在這篇文章中,和大家分享了透過其他語言來和智能合約互動的方式。若您是初次接觸這種互動方式,也恭喜您跨出了DApp開發的第一步。透過Ganache模擬出來的網路,可以放心的做任何嘗試,不用擔心成本問題。未來若有機會開發Solidity智能合約,您也可以將合約上到Ganache中來進行測試,比使用testnet來得快速許多。
感謝您閱讀了這系列第二篇的文章,後續仍會進一步探討Ganache還能做那些有趣的功能,以及那些資料的分析。期待與您的再次相見。
作者Steve目前任職於趨勢科技區塊鏈安全研究小組,專注於區塊鏈合約安全與與相關技術,如果喜歡我的文章,或是想獲得更多區塊鏈詐騙事件懶人包,歡迎關注
我的帳號!
另外,我已經加入由
趨勢科技防詐達人所成立的方格子專題-《區塊鏈生存守則》,在那裡我會跟其他優質的創作者一起帶大家深入瞭解區塊鏈,並隨時向大家更新區塊鏈資安事件