diff --git a/src/common/tx-manager.ts b/src/common/tx-manager.ts index f12e5af..d7eed7c 100644 --- a/src/common/tx-manager.ts +++ b/src/common/tx-manager.ts @@ -22,7 +22,7 @@ export class TxManager { constructor( contract: ethers.Contract, chainId: number, - constOptions?: { + constOptions: { blockTimeSec?: number gasLimitRatio?: number rbfTimes?: number @@ -35,12 +35,12 @@ export class TxManager { this.contract = contract this.price = null this.cancel = false - this.blockTimeSec = constOptions?.blockTimeSec || 3 - this.gasLimitRatio = constOptions?.gasLimitRatio || 1.2 - this.rbfTimes = constOptions?.rbfTimes || 3 - this.boardcastTimes = constOptions?.boardcastTimes || 3 - this.waitDistance = constOptions?.waitDistance || 10 - this.minRBFRatio = constOptions?.minRBFRatio || 1.3 + this.blockTimeSec = constOptions.blockTimeSec || 3 + this.gasLimitRatio = constOptions.gasLimitRatio || 1.2 + this.rbfTimes = constOptions.rbfTimes || 3 + this.boardcastTimes = constOptions.boardcastTimes || 3 + this.waitDistance = constOptions.waitDistance || 10 + this.minRBFRatio = constOptions.minRBFRatio || 1.3 } async FreshBaseGas(): Promise { @@ -57,7 +57,7 @@ export class TxManager { const signer = this.contract.signer let nonce = await this.getNonce() this._deltaCount++ - console.log("clearPendingNonce", nonce, num) + console.error("clearPendingNonce", nonce, num) let price = await this.FreshBaseGas() let txs = [] for (let i = 0; i < num; i++) { @@ -67,22 +67,19 @@ export class TxManager { gasLimit: 21000, type: 2, chainId: this.chainId, - maxFeePerGas: price! - .maxFeePerGas!.mul((rbfRatio * 100) | 0) - .div(100), + maxFeePerGas: price!.maxFeePerGas!.mul((rbfRatio * 100) | 0).div(100), maxPriorityFeePerGas: price! .maxPriorityFeePerGas!.mul((rbfRatio * 100) | 0) .div(100), }) - txs.push(res) + txs.push(res.then((tx) => tx.wait())) } await Promise.all(txs) } async getNonce(): Promise { if (!this._initialPromise) { - this._initialPromise = - this.contract.signer.getTransactionCount("pending") + this._initialPromise = this.contract.signer.getTransactionCount("pending") } const deltaCount = this._deltaCount this._deltaCount++ @@ -91,20 +88,19 @@ export class TxManager { async SendCall(_method: string, _args: any[]): Promise { const nonce = await this.getNonce() + if (this.queueCurrNonce < 0) this.queueCurrNonce = nonce - let unsignedTx = await this.contract.populateTransaction[_method]( - ..._args - ) + let unsignedTx = await this.contract.populateTransaction[_method](..._args) unsignedTx.nonce = nonce unsignedTx.chainId = this.chainId // estimateGas check let gasLimit = await this.contract.provider.estimateGas(unsignedTx) - unsignedTx.gasLimit = gasLimit - .mul((this.gasLimitRatio * 100) | 0) - .div(100) + unsignedTx.gasLimit = gasLimit.mul((this.gasLimitRatio * 100) | 0).div(100) let retryRBF = this.rbfTimes - + let rbfCount = 0 let lastPrice = null + + let waitTxs: Record = {} while (retryRBF > 0 && !this.cancel) { // set gas price let price @@ -113,15 +109,13 @@ export class TxManager { } catch (e) { price = this.price } finally { - if ( - !price || - !price.maxFeePerGas || - !price.maxPriorityFeePerGas - ) { + if (!price || !price.maxFeePerGas || !price.maxPriorityFeePerGas) { throw new Error("get fee data failed") } } if (lastPrice) { + // RBF + console.error("[tx-manager] RBF", "nonce:", nonce, "cnt:", rbfCount) let maxFeePerGasMin = lastPrice .maxFeePerGas!.mul((this.minRBFRatio * 100) | 0) .div(100) @@ -145,24 +139,18 @@ export class TxManager { } // sign - let signedTx = await this.contract.signer.signTransaction( - unsignedTx - ) + let signedTx = await this.contract.signer.signTransaction(unsignedTx) let retryBoardcast = this.boardcastTimes - let txRes = null + let txRes: ethers.providers.TransactionResponse | null = null while (retryBoardcast > 0 && !this.cancel) { - if ( - this.queueCurrNonce < 0 || - this.queueCurrNonce + 1 == nonce - ) { + if (nonce <= this.queueCurrNonce + 1) { // Arrive in line retryBoardcast-- } else if (nonce - this.queueCurrNonce > this.waitDistance) { // Too far away don't boardcast, waitTime = int(distance / groupSize) * blockTime + 1s const waitTime = - (((nonce - this.queueCurrNonce) / this.waitDistance) | - 0) * + (((nonce - this.queueCurrNonce) / this.waitDistance) | 0) * this.blockTimeSec * 1000 + 1000 @@ -174,62 +162,93 @@ export class TxManager { try { // send - txRes = await this.contract.provider.sendTransaction( - signedTx - ) - await new Promise((r) => - setTimeout(r, (this.blockTimeSec / 2) * 1000) - ) + txRes = await this.contract.provider.sendTransaction(signedTx) + await new Promise((r) => setTimeout(r, (this.blockTimeSec / 2) * 1000)) } catch (e: Error | any) { if (e.code == ethers.errors.NONCE_EXPIRED) { // ignore if tx already in mempool } else if (e.code == ethers.errors.SERVER_ERROR) { // ignore if tx already in mempool + } else if (e.code == ethers.errors.REPLACEMENT_UNDERPRICED) { + // gas price too low, rbf++ but total rbf times < rbfTimes*2 + if (rbfCount < this.rbfTimes * 2) { + retryRBF++ + } + } else if (e.code == ethers.errors.INSUFFICIENT_FUNDS) { + console.error("insufficient funds!") + throw Error("insufficient funds!") } else { - console.error( - "[tx-manager] sendTransaction", - nonce, - e.code, - e.message - ) + console.error("[tx-manager] sendTransaction", nonce, e.code, e.message) } + // console.error( + // "[tx-manager] sendTransaction", + // nonce, + // this.queueCurrNonce, + // e.code, + // e.message + // ) } if (txRes) { // wait - try { - let receipt = - await this.contract.provider.waitForTransaction( - txRes.hash, - 1, - this.blockTimeSec * 1000 + 1000 - ) - if (receipt) { - this.queueCurrNonce = - txRes.nonce > this.queueCurrNonce - ? txRes.nonce - : this.queueCurrNonce - return receipt + waitTxs[txRes.hash] = txRes + let done = new Promise((resolve, reject) => { + let errCnt = 0 + let reportError = (e: Error) => { + errCnt++ + if (errCnt >= Object.keys(waitTxs).length) { + reject(new Error("all timeout")) + } } - } catch (e: Error | any) { - if (e.code == ethers.errors.TIMEOUT) { - // ignore timeout - } else { - console.error( - "[tx-manager] waitForTransaction", - nonce, - txRes.hash, - e.code, - e.reason - ) + for (let hash of Object.keys(waitTxs)) { + // console.error("wait start", nonce, hash) + this.contract.provider + .waitForTransaction(hash, 1, this.blockTimeSec * 1000 + 1000) + .then((receipt) => { + resolve(receipt) + return receipt + }) + .catch((e) => { + if (e.code == ethers.errors.TIMEOUT) { + // ignore timeout + } else { + console.error( + "[tx-manager] waitForTransaction", + nonce, + hash, + e.code, + e.reason + ) + } + // console.error( + // "[tx-manager] waitForTransaction", + // nonce, + // hash, + // e.code, + // e.reason + // ) + // console.error("wait", nonce, hash, e.code) + reportError(e) + }) } + }) + try { + let receipt = await done + this.queueCurrNonce = + nonce > this.queueCurrNonce ? nonce : this.queueCurrNonce + // console.error("wait done", nonce) + return receipt + } catch (e) { + // ignore } } else { + // send first time failed, wait 1s then try again await new Promise((r) => setTimeout(r, 1000)) } } retryRBF-- + rbfCount++ } - throw new Error("send tx failed") + throw new Error(`send tx failed: ${nonce}`) } } diff --git a/src/config/evm-network.ts b/src/config/evm-network.ts index 58b7b61..fdc2616 100644 --- a/src/config/evm-network.ts +++ b/src/config/evm-network.ts @@ -25,6 +25,26 @@ const evmNetworks: Record = { }, contracts: { git3: "" }, }, + 5: { + name: "Goerli", + rpc: ["https://eth-goerli.g.alchemy.com/v2/asrXwNuiK9my-cZJYZ_ooo4q-lDw8HLm"], + nativeCurrency: { + name: "Goerli Ether", + symbol: "ETH", + decimals: 18, + }, + explorers: [ + { + name: "etherscan-goerli", + url: "https://goerli.etherscan.io", + standard: "EIP3091", + }, + ], + txConst: { + blockTimeSec: 15, + }, + contracts: { git3: "0x80F4b977F9C1d21FF6fDDd56C3CA59eeD5745B58" }, + }, 3334: { name: "Web3Q Galileo", nativeCurrency: { @@ -41,7 +61,7 @@ const evmNetworks: Record = { }, ], txConst: { - blockTimeSec: 6, + blockTimeSec: 7, }, contracts: { git3: "0x59ef6b2dbfE86CcAaD84E2d8e78177f528521Da9" }, }, @@ -65,6 +85,7 @@ const evmNetworks: Record = { ], txConst: { blockTimeSec: 30, + boardcastTimes: 5, }, contracts: { git3: "0xF56A1dd941667911896B9B872AC79E56cfc6a3dB" }, }, diff --git a/src/config/name-services.ts b/src/config/name-services.ts index 8b8ba81..6d83b5f 100644 --- a/src/config/name-services.ts +++ b/src/config/name-services.ts @@ -11,6 +11,10 @@ const ns: Record = { chainId: 3141, resolver: "", }, + goerli: { + chainId: 5, + resolver: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", + }, } export default ns diff --git a/src/git3/index.ts b/src/git3/index.ts index 4e54380..bb2c1a0 100644 --- a/src/git3/index.ts +++ b/src/git3/index.ts @@ -211,7 +211,7 @@ program .description("clear pending nonce") .argument("", "ex: default@git3.w3q") .argument("[num]", "number of pending nonce to clear", 1) - .action(async (uri,num) => { + .action(async (uri, num) => { if (!uri.startsWith("git3://")) { uri = "git3://" + uri } @@ -224,6 +224,8 @@ program let nonce = await protocol.wallet.getTransactionCount() console.log(`current nonce: ${nonce}`) await txManager.clearPendingNonce(num) + nonce = await protocol.wallet.getTransactionCount() + console.log(`current nonce: ${nonce}`) }) // Todo: set-wallet temporarily useless