web3.py调用0x API在Ropsten测试网执行代币兑换失败问题排查
Ropsten测试网0x API兑换交易失败问题
问题描述
- 操作场景:在Ropsten测试网调用0x API获取最优报价,执行链上USDC到DAI的代币兑换
- 故障现象:approve授权交易可正常上链确认,但实际兑换交易始终执行失败,Etherscan浏览器未返回明确失败提示,反复核查代码未定位根因,已查阅0x官方文档、Stack Overflow相关公开问题未找到解决方案
- 相关交易记录:
- 已确认成功的授权交易:Etherscan approval tx
- 执行失败的兑换交易:Etherscan swap tx
复现代码
import requests import json import time from web3 import Web3 walletAddress = 'mywalletaddress' wpk = 'myprivatekey' # Setup Ropsten provider w3 = Web3(Web3.HTTPProvider('https://ropsten.infura.io/v3/')) # Get the nonce to prevent from sending the transaction twice nonce = w3.eth.getTransactionCount(walletAddress) print('Nonce:', nonce) # Store the generic ERC20 abi for later use ABI = json.load(open('abi.json')) # ROPSTEN ADDRESSES WETH = Web3.toChecksumAddress('0xc778417e063141139fce010982780140aa0cd5ab') DAI = Web3.toChecksumAddress('0xad6d458402f60fd3bd25163575031acdce07538d') USDC = Web3.toChecksumAddress('0x07865c6e87b9f70255377e024ace6630c1eaa37f') def calcAmount(initialAmount, contractAddress): contract = w3.eth.contract(address=contractAddress, abi=ABI) decimals = contract.functions.decimals().call() amount = int(initialAmount * 10**decimals) return amount print(decimals, amount) def getTokenSymbol(contractAddress): contract = w3.eth.contract(address=contractAddress, abi=ABI) tokenSymbol = contract.functions.symbol().call() return tokenSymbol print('Symbol:', tokenSymbol) # Define query parameters initialAmount = 100 slippage = '0.01' # 0.01 = 1% slippage sellAmount = calcAmount(initialAmount, USDC) print('Sell:', sellAmount) def approve(token, spenderAddress, walletAddress, wpk): token = Web3.toChecksumAddress(token) contract = w3.eth.contract(address=token, abi=ABI) spender = Web3.toChecksumAddress(spenderAddress) max_amount = Web3.toWei(2**64 - 1, 'ether') nonce = w3.eth.getTransactionCount(walletAddress) tx = contract.functions.approve(spender, max_amount).buildTransaction({ 'from': walletAddress, 'nonce': nonce }) signed_tx = w3.eth.account.signTransaction(tx, wpk) tx_hash = w3.eth.sendRawTransaction(signed_tx.rawTransaction) print("Approve", w3.toHex(tx_hash)) w3.eth.wait_for_transaction_receipt(tx_hash) def getQuote(buyToken, slippage, sellToken, sellAmount): # Make the query https://ropsten.api.0x.org/ query = 'https://ropsten.api.0x.org/swap/v1/quote?buyToken={}&slippage={}&sellToken={}&sellAmount={}'.format(buyToken, slippage, sellToken, sellAmount) # Get the query and store the response in data response = requests.get(query) data = response.json() # Print a prettiefied version of the response (optional) print(json.dumps(data, indent=2)) # Convert the buyAmmount to integer buyAmount = int(data['buyAmount']) # Convert the buyAmount to a readable number finalPrice = calcAmount(buyAmount, buyToken) # Get the symbol of the 2 tokens buyTokenSymbol = getTokenSymbol(buyToken) sellTokenSymbol = getTokenSymbol(sellToken) print(sellTokenSymbol, '->', buyTokenSymbol) approve(data['sellTokenAddress'], data['allowanceTarget'], walletAddress, wpk) tx = { "from": walletAddress, "to": Web3.toChecksumAddress(data['to']), "data": data['data'], "value": 0, "gas": int(data['gas']), "gasPrice": int(data['gasPrice']), "nonce": nonce + 1, "chainId": data['chainId'] } # sign the transaction signed_tx = w3.eth.account.sign_transaction(tx, wpk) # # send transaction tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction) # # get transaction hash print(w3.toHex(tx_hash)) # Use only for testing the query getQuote(DAI, slippage, USDC, sellAmount)
问题根因及修复方案
代码存在4处直接导致交易失败的隐患,按影响优先级排序:
- 交易value字段硬编码为0
0x API返回的报价结果中自带value字段,代表交易需要附带的原生币数量,即使是ERC20代币间兑换,部分路由逻辑(如中间经过原生币中转、WETH wrap/unwrap)也需要传递对应value值,硬编码为0会直接触发合约回滚。修复方式:直接将tx结构体中的value设置为int(data['value']),不要手动赋值。 - Gas参数未留冗余
API返回的gas是链上模拟执行的最低gas消耗值,测试网出块时状态波动、节点gas估算偏差都可能导致实际执行gas超过预估值,触发gas不足失败。修复方式:在返回gas基础上增加20%~30%冗余,例如gas: int(int(data['gas'])*1.2)。 - Nonce获取逻辑错误
代码仅在脚本启动时全局获取一次地址nonce,后续approve交易发送完成后,构造兑换交易时使用的nonce是启动时的旧值加1,若approve交易占用了该nonce、或脚本启动后地址有其他交易操作,会直接导致nonce不匹配,交易被节点拒绝或上链失败。修复方式:在构造兑换交易前,重新通过w3.eth.getTransactionCount(walletAddress)拉取最新链上nonce使用,不要依赖全局缓存的旧nonce。 - 无限授权金额计算逻辑不兼容非18位小数代币
当前授权逻辑用Web3.toWei(2**64 - 1, 'ether')计算最大授权额,该逻辑仅适配18位小数的代币,USDC为6位小数,计算出的授权额不符合合约预期,可能导致路由合约无法正常划转代币。修复方式:无限授权直接使用固定最大值2**256 -1即可,无需通过toWei转换,兼容所有小数位的ERC20代币。
注:代码中calcAmount和getTokenSymbol函数里return语句后的print代码永远不会执行,属于无效代码,不影响交易但建议清理;另外API返回的buyAmount已经是对应代币最小单位的数值,传入calcAmount再次乘10^decimals会导致金额计算错误,仅影响本地打印结果,不会导致链上交易失败。
内容的提问来源于stack exchange,提问作者icelandnode
相关产品推荐
相关产品推荐

