You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

web3.py调用0x API在Ropsten测试网执行代币兑换失败问题排查

Ropsten测试网0x API兑换交易失败问题

问题描述

  • 操作场景:在Ropsten测试网调用0x API获取最优报价,执行链上USDC到DAI的代币兑换
  • 故障现象:approve授权交易可正常上链确认,但实际兑换交易始终执行失败,Etherscan浏览器未返回明确失败提示,反复核查代码未定位根因,已查阅0x官方文档、Stack Overflow相关公开问题未找到解决方案
  • 相关交易记录:

复现代码

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处直接导致交易失败的隐患,按影响优先级排序:

  1. 交易value字段硬编码为0
    0x API返回的报价结果中自带value字段,代表交易需要附带的原生币数量,即使是ERC20代币间兑换,部分路由逻辑(如中间经过原生币中转、WETH wrap/unwrap)也需要传递对应value值,硬编码为0会直接触发合约回滚。修复方式:直接将tx结构体中的value设置为int(data['value']),不要手动赋值。
  2. Gas参数未留冗余
    API返回的gas是链上模拟执行的最低gas消耗值,测试网出块时状态波动、节点gas估算偏差都可能导致实际执行gas超过预估值,触发gas不足失败。修复方式:在返回gas基础上增加20%~30%冗余,例如gas: int(int(data['gas'])*1.2)。
  3. Nonce获取逻辑错误
    代码仅在脚本启动时全局获取一次地址nonce,后续approve交易发送完成后,构造兑换交易时使用的nonce是启动时的旧值加1,若approve交易占用了该nonce、或脚本启动后地址有其他交易操作,会直接导致nonce不匹配,交易被节点拒绝或上链失败。修复方式:在构造兑换交易前,重新通过w3.eth.getTransactionCount(walletAddress)拉取最新链上nonce使用,不要依赖全局缓存的旧nonce。
  4. 无限授权金额计算逻辑不兼容非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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 22:24:18