无交易收据时如何确认Avalanche子网ERC20转账状态?
问题
我是Alice,需要通过部署在Avalanche子网的ERC20智能合约将N枚代币转给客户Bob,使用Rust结合ethers-rs调用合约transfer函数的代码如下:
let tx = self.contract.transfer(addr_to, amount).tx; let _res: TypedTransaction = tx; if let Some(receipt) = self .contract .transfer(addr_to, amount) .send() .await .map_err(|e| EthersTransferError(e.to_string()))? .await .map_err(|e| EthersTransferError(e.to_string()))? { return Ok(receipt); }
交易约5秒完成并返回包含交易哈希、状态的收据,但如果程序在这5秒内崩溃(刚好在收到收据前终止),我仅持有from地址、to地址、转账金额和合约地址,无法确认交易是否成功,且需要确保代币仅转账一次。
我曾尝试通过抓取区块事件查询交易,代码如下:
async fn find_tx(&self, to: Address, amount: U256, from_block: BlockNumber) -> Result<()> { let filter = Filter::new() .address(self.contract_addr) .event("Transfer(address,address,uint256)") .topic2(H256::from(to)) .from_block(from_block); let mut interval = time::interval(time::Duration::from_secs(600)); loop { interval.tick().await; let logs = self .client .get_logs(&filter) .await .map_err(|e| AvalancheGetLogsError(e.to_string()))?; for log in logs.iter() { if self.client.address() == Address::from(log.topics[1]) && to == Address::from(log.topics[2]) && amount == U256::from(log.data.as_bytes_ref()) { return Ok(()); } } } }
但该方案存在两个问题:一是部分链(如Fuji测试网)限制过滤的最大区块数(2048块),交易可能超出该范围;二是若交易失败,代码会陷入无限循环,需等待多天才能确认交易未发生。
请问在仅拥有有限信息的情况下,最优的程序化解决方案是什么?是否需要依赖子网托管服务的API、索引器或浏览器?是否要扩展智能合约功能?Geth是否具备相关能力?
最优解决方案
1. 优化事件查询逻辑,解决区块范围限制与无限循环问题
针对现有事件查询的缺陷,做两点关键改进:
- 分块查询日志:将大区块范围拆分为多个2048块的小批次,循环遍历每个批次查询,避免触发链的区块数限制。比如从
from_block开始,每次查询[current_block, current_block+2047],直到当前区块达到链上最新高度。 - 设置超时终止条件:定义合理的超时窗口(比如Avalanche子网出块时间约2秒,可设置30分钟超时),若超过该时间仍未找到目标Transfer事件,则判定交易未发生,终止循环。
示例优化代码片段:
async fn find_tx(&self, to: Address, amount: U256, from_block: u64) -> Result<bool> { let mut latest_block = self.client.get_block_number().await?; let batch_size = 2048u64; let timeout = time::Duration::from_secs(1800); // 30分钟超时 let start_time = time::Instant::now(); let mut current_block = from_block; while current_block <= latest_block && start_time.elapsed() < timeout { let end_block = std::cmp::min(current_block + batch_size - 1, latest_block); let filter = Filter::new() .address(self.contract_addr) .event("Transfer(address,address,uint256)") .topic1(H256::from(self.client.address())) // 明确from地址作为topic1 .topic2(H256::from(to)) .from_block(BlockNumber::Number(current_block.into())) .to_block(BlockNumber::Number(end_block.into())); let logs = self.client.get_logs(&filter).await?; for log in logs { let parsed_amount = U256::from(log.data.as_bytes_ref()); if parsed_amount == amount { return Ok(true); // 找到匹配交易,返回成功 } } // 更新当前区块,同时轮询最新区块高度 current_block = end_block + 1; latest_block = self.client.get_block_number().await?; time::sleep(time::Duration::from_secs(2)).await; // 等待新区块生成 } // 超时或遍历完所有区块仍未找到,返回未发生 Ok(false) }
2. 利用账户交易历史查询补充验证
除了事件日志,还可以查询from地址的交易历史,筛选与目标合约交互的交易,再验证交易详情:
- 调用
get_transaction_count获取from地址的交易总数,然后遍历每个交易哈希(通过get_transaction_by_index),筛选出目标合约地址的交易。 - 对筛选出的交易,解析输入数据(ERC20的
transfer函数签名是0xa9059cbb),验证接收地址和转账金额是否匹配,再通过get_transaction_receipt确认交易是否成功。
这种方式可以直接定位到具体交易,避免事件日志可能的遗漏(比如交易成功但未触发Transfer事件的极端情况)。
3. 扩展智能合约添加幂等性控制(推荐长期方案)
如果需要从根源上避免重复转账问题,可以给ERC20合约添加幂等转账接口:
- 新增一个
transferWithId函数,要求传入唯一的transferId(比如UUID哈希),合约先检查该ID是否已被使用,未使用则执行转账并标记ID为已用。 - 这样即使程序崩溃,后续可以通过查询合约中
transferId的状态,直接判断该笔转账是否已执行,无需依赖链上交易日志。
示例合约片段(Solidity):
contract IdempotentERC20 is ERC20 { mapping(bytes32 => bool) public executedTransfers; function transferWithId(address to, uint256 amount, bytes32 transferId) external returns (bool) { require(!executedTransfers[transferId], "Transfer already executed"); executedTransfers[transferId] = true; return transfer(to, amount); } }
关于依赖与工具的说明
- 子网托管服务/索引器:使用Avalanche子网的托管服务(比如AvalancheGo节点的API)即可通过原生RPC接口实现查询,无需额外依赖第三方索引器;若追求更高查询效率,可使用预索引Transfer事件的索引器服务,避免分块遍历的开销。
- Geth能力:Geth支持所有标准以太坊RPC接口,包括
getLogs、getTransactionByIndex等,完全可以用来实现上述查询逻辑,无需额外特殊功能。 - 浏览器API:不建议依赖区块链浏览器的公开API,其速率限制和稳定性无法保障程序化查询需求,优先使用节点原生RPC或合规索引器服务。
内容的提问来源于stack exchange,提问作者akolechko
相关产品推荐
相关产品推荐

