使用web3.py在Base Layer 2执行代币购买交易时遭遇Execution Reverted问题
我正在尝试用Python和web3.py写一个服务,在Base链的某个合约上执行“购买”代币的交易。奇怪的是,UI上执行相同的购买操作完全没问题,但我代码里发的交易虽然能拿到哈希,却总是被回滚。我用的测试钱包已经有几笔失败的交易记录了。
我的代码如下(交易发送成功但被回滚,我试过把gas值转成十六进制(和网站上的交易格式一致),也试过移除value字段,但都没用):
from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from .models import Holding import os import sys from web3 import Web3 from eth_account import Account from .config import w3, CONTRACT_ADDRESS, GAS_LIMIT, VIRTUALS_CA from .contract_abi import CONTRACT_ABI from .virtual_protocol_contract import VIRTUAL_ABI from .utils import estimate_gas_price class BuyTokenView(APIView): def post(self, request): data = request.data # Validate input if 'contract_address' not in data: return Response({"error": "Missing required fields"}, status=status.HTTP_400_BAD_REQUEST) ca = data['contract_address'] amount = data['amount'] private_key = load_environment() account = setup_account(private_key) print(f"Connected with address: {account.address}") # Execute transaction execute_buy_transaction(account, ca, float(amount)) return Response({"Message": f"Buying Token {ca}"}, status=status.HTTP_200_OK) def load_environment(): """Load environment variables.""" private_key = os.getenv('PRIVATE_KEY') if not private_key: print("Error: PRIVATE_KEY not found in environment variables") sys.exit(1) return private_key def setup_account(private_key): """Setup account from private key.""" try: account = Account.from_key(private_key) return account except ValueError as e: print(f"Error setting up account: {e}") sys.exit(1) def execute_buy_transaction(account, token_address, amount): """Execute token purchase transaction.""" contract = w3.eth.contract(address=CONTRACT_ADDRESS, abi=CONTRACT_ABI) virtuals_contract = w3.eth.contract(address=VIRTUALS_CA, abi=VIRTUAL_ABI) float_value = float(amount) approve_tokens(account, virtuals_contract, int(amount)) # Prepare transaction transaction = contract.functions.buy(amountIn=int(amount), tokenAddress=token_address).build_transaction({ 'from': account.address, 'gas': hex(GAS_LIMIT), 'maxFeePerGas': hex(estimate_gas_price(w3)), 'maxPriorityFeePerGas': hex(w3.to_wei('2', 'gwei')), 'nonce': hex(w3.eth.get_transaction_count(account.address)), }) transaction.pop('value', None) # Sign and send transaction try: signed_txn = w3.eth.account.sign_transaction(transaction, account.key) tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction) print(f"Transaction sent. Hash: {tx_hash.hex()}") # Wait for transaction receipt print("Waiting for transaction confirmation...") receipt = w3.eth.wait_for_transaction_receipt(tx_hash) return receipt except Exception as e: print(f"Error executing transaction: {e}") return None # Function to approve tokens def approve_tokens(account, contract, amount): nonce = w3.eth.get_transaction_count(account.address) tx = { 'nonce': nonce, 'to': contract.address, 'value': w3.to_wei(0, 'ether'), 'gas': 200000, 'gasPrice': w3.eth.gas_price, 'data': contract.encode_abi("approve", args=[account.address, amount]), 'chainId': w3.eth.chain_id } try: signed_tx = w3.eth.account.sign_transaction(tx, account.key) tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction) print(f"Approve Transaction sent. Hash: {tx_hash.hex()}") # Wait for transaction receipt print("Waiting for transaction confirmation...") receipt = w3.eth.wait_for_transaction_receipt(tx_hash) return receipt except Exception as e: print(f"Error approving tokens: {e}") return None
可能的问题排查方向及解决方案
作为踩过不少web3交易回滚坑的开发者,给你几个优先排查的点:
授权对象完全错了!
看你的approve_tokens函数,你把代币授权给了自己的钱包地址account.address,但实际购买逻辑里,是需要把代币授权给负责执行购买的合约(也就是CONTRACT_ADDRESS)啊!UI上的交易肯定是授权给了这个合约地址,你这里参数写反了,改成args=[CONTRACT_ADDRESS, amount]试试——这大概率是最核心的问题,合约没权限动你的代币,直接就回滚了。金额单位是否匹配?
你传入buy函数的amountIn是int(amount),要确认这个数值的精度是不是合约要求的。比如如果是USDC这类6位小数的代币,你直接传用户输入的100,就会变成100 wei(也就是0.0000001 USDC),这点钱肯定买不到东西,合约直接因余额不足回滚。要根据代币的小数位数转换,比如USDC就用int(amount * 10**6),或者用w3.to_wei(amount, 'ether')对应18位小数的代币。Gas参数别瞎转十六进制
web3.py的build_transaction接受整数类型的gas参数,你转成hex反而可能出问题。把hex(GAS_LIMIT)、hex(estimate_gas_price(w3))这些都改成直接传整数,比如:transaction = contract.functions.buy(amountIn=int(amount), tokenAddress=token_address).build_transaction({ 'from': account.address, 'gas': GAS_LIMIT, 'maxFeePerGas': estimate_gas_price(w3), 'maxPriorityFeePerGas': w3.to_wei('2', 'gwei'), 'nonce': w3.eth.get_transaction_count(account.address), 'chainId': w3.eth.chain_id })顺便加上
chainId字段,避免链ID不匹配的隐性问题。提前模拟交易排查错误
别等交易发出去回滚才看问题,用call()方法模拟交易,直接在本地拿到错误信息:try: # 模拟购买交易,提前发现问题 contract.functions.buy(amountIn=int(amount), tokenAddress=token_address).call({ 'from': account.address, }) except Exception as e: print(f"模拟交易失败,具体错误:{e}")比如如果是授权不足,这里会直接抛出
Insufficient allowance的错误,比查区块浏览器还快。
总结
先改授权的spender地址,这是最可能的元凶,然后检查金额单位,最后用模拟交易提前排错,应该能解决你的问题。
备注:内容来源于stack exchange,提问作者degenTy

