如何使用ethers处理NestJS中区块链连续交易的nonce问题?
问题场景
你在NestJS项目中实现了activateCompany函数,会连续调用addController和increaseAllowance两个区块链操作,但每个操作都重新创建ethers.Wallet实例,导致多交易并发时出现nonce值过低的错误,手动传入布尔值控制nonce的方式在并发场景下失效。
你的核心代码如下:
activateCompany函数
async activateCompany(data: CompanyActivateRequest): Promise<void> { (...) await this.blockchain.addController(newWallet.address); await this.blockchain.increaseAllowance(newWallet.address,data.monthlyBalance); (...) }
addController函数
async addController(walletAddress: string): Promise<void> { try { const wallet = new ethers.Wallet(process.env.OWNER_PRIVATE_KEY,this._provider); const contract = new ethers.Contract(process.env.CONTRACT_ADDRESS, this._abi, wallet); await contract.addController(walletAddress); } catch (error) { throw new InternalServerErrorException(['blockchain-error']); } }
increaseAllowance函数
async increaseAllowance(walletAddress: string, amount: number, increaseNonce: boolean): Promise<void> { try { const wallet = new ethers.Wallet(process.env.OWNER_PRIVATE_KEY, this._provider); const contract = new ethers.Contract(process.env.CONTRACT_ADDRESS, this._abi, wallet); await contract.increaseAllowance(walletAddress, amount); } catch (error) { throw new InternalServerErrorException(['blockchain-error']); } }
问题根源
每次调用区块链操作时都重新创建ethers.Wallet实例,会导致每个实例独立获取nonce值,并发场景下多个实例可能拿到相同的nonce,后发送的交易会因为nonce过低被拒绝。手动修改nonce的方式无法应对动态变化的交易队列,自然失效。
解决方案
1. 复用Wallet与Contract实例
在Blockchain服务的生命周期内只初始化一次Wallet和Contract,让ethers自动管理nonce——每次发送交易后,Wallet会自动递增nonce,并发场景下会将交易加入队列按顺序发送,从根本上避免nonce冲突。
修改后的Blockchain服务示例:
import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { ethers } from 'ethers'; @Injectable() export class BlockchainService { private _provider: ethers.providers.JsonRpcProvider; private readonly wallet: ethers.Wallet; private readonly contract: ethers.Contract; constructor() { // 初始化Provider(匹配你的Hardhat本地RPC地址) this._provider = new ethers.providers.JsonRpcProvider(process.env.HARDHAT_RPC_URL || 'http://localhost:8545'); // 仅初始化一次Wallet this.wallet = new ethers.Wallet(process.env.OWNER_PRIVATE_KEY, this._provider); // 仅初始化一次Contract this.contract = new ethers.Contract(process.env.CONTRACT_ADDRESS, this._abi, this.wallet); } async addController(walletAddress: string): Promise<void> { try { await this.contract.addController(walletAddress); } catch (error) { this.handleNonceError(error, () => this.contract.addController(walletAddress)); } } async increaseAllowance(walletAddress: string, amount: number): Promise<void> { try { // 注意:如果合约使用wei单位,需将金额转换为wei(示例假设是18位小数的代币) const weiAmount = ethers.utils.parseUnits(amount.toString(), 18); await this.contract.increaseAllowance(walletAddress, weiAmount); } catch (error) { this.handleNonceError(error, () => this.contract.increaseAllowance(walletAddress, ethers.utils.parseUnits(amount.toString(), 18))); } } /** * 统一处理nonce过低错误,自动重试 */ private async handleNonceError(error: any, retryFn: () => Promise<any>) { if (error.message?.includes('nonce too low')) { // 获取当前钱包的pending状态交易数,重置Wallet的nonce const currentNonce = await this._provider.getTransactionCount(this.wallet.address, 'pending'); this.wallet.nonce = currentNonce; // 重试交易 await retryFn(); } else { throw new InternalServerErrorException(['blockchain-error']); } } }
2. 优化并发交易处理
复用Wallet实例后,ethers会自动将并发的交易加入内部队列,按顺序发送并维护nonce的连续性,无需手动干预。如果遇到极端场景下的nonce同步问题,上述代码中的handleNonceError方法会自动重置nonce并重试交易。
3. 注意金额单位转换
你的increaseAllowance函数中直接传入amount数值,如果合约要求的是wei单位(绝大多数以太坊生态合约都是如此),需要用ethers.utils.parseUnits将金额转换为对应的wei值,否则会出现金额错误或交易失败。
额外提示
- 避免手动修改nonce:除非有特殊业务需求,否则不要手动干预Wallet的nonce值,依赖ethers的自动管理即可。
- 测试并发场景:可以用Postman或编写单元测试模拟多请求并发,验证修改后的代码是否解决nonce问题。
内容的提问来源于stack exchange,提问作者Tomás Martínez

