From f7984ef38458820e1e440478a70e5ce37035b73f Mon Sep 17 00:00:00 2001 From: Eric <5089238+emizzle@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:44:16 +1000 Subject: [PATCH] cancel transaction after estimateGas failure --- ethers/contract.nim | 2 +- ethers/signer.nim | 45 ++++++++++++++++++++++++++++++++++-- testmodule/testContracts.nim | 3 +++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/ethers/contract.nim b/ethers/contract.nim index 84d5d8f..f362581 100644 --- a/ethers/contract.nim +++ b/ethers/contract.nim @@ -123,7 +123,7 @@ proc send(contract: Contract, Future[?TransactionResponse] {.async.} = if signer =? contract.signer: let transaction = createTransaction(contract, function, parameters, overrides) - let populated = await signer.populateTransaction(transaction) + let populated = await signer.populateTransaction(transaction, cancelOnEstimateGasError = true) let txResp = await signer.sendTransaction(populated) return txResp.some else: diff --git a/ethers/signer.nim b/ethers/signer.nim index e22b118..cd04dc3 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -1,7 +1,12 @@ import ./basics import ./provider +import pkg/chronicles export basics +export chronicles + +logScope: + topics = "ethers signer" type Signer* = ref object of RootObj @@ -64,8 +69,37 @@ method updateNonce*(signer: Signer, nonce: ?UInt256) {.base, gcsafe.} = if nonce > lastSeen: signer.lastSeenNonce = some nonce +method cancelTransaction( + signer: Signer, + tx: Transaction +): Future[TransactionResponse] {.async, base.} = + # cancels a transaction by sending with a 0-valued transaction to ourselves + # with the failed tx's nonce + + without sender =? tx.sender: + raiseSignerError "transaction must have sender" + if sender != await signer.getAddress(): + raiseSignerError "can only cancel a tx this signer has sent" + without nonce =? tx.nonce: + raiseSignerError "transaction must have nonce" + + var cancelTx = tx + cancelTx.to = sender + cancelTx.value = 0.u256 + cancelTx.nonce = some nonce + try: + cancelTx.gasLimit = some(await signer.estimateGas(cancelTx)) + except ProviderError: + warn "failed to estimate gas for cancellation tx, sending anyway", + tx = $cancelTx + discard + + trace "cancelling transaction to prevent stuck transactions", nonce + return await signer.sendTransaction(cancelTx) + method populateTransaction*(signer: Signer, - transaction: Transaction): + transaction: Transaction, + cancelOnEstimateGasError = false): Future[Transaction] {.base, async.} = if sender =? transaction.sender and sender != await signer.getAddress(): @@ -84,6 +118,13 @@ method populateTransaction*(signer: Signer, if transaction.gasPrice.isNone and (transaction.maxFee.isNone or transaction.maxPriorityFee.isNone): populated.gasPrice = some(await signer.getGasPrice()) if transaction.gasLimit.isNone: - populated.gasLimit = some(await signer.estimateGas(populated)) + try: + populated.gasLimit = some(await signer.estimateGas(populated)) + except ProviderError as e: + # send a 0-valued transaction with the errored nonce to prevent stuck txs + discard await signer.cancelTransaction(populated) + raiseSignerError "estimateGas failed. *A cancellation transaction " & + "(0-valued tx to ourselves with the estimateGas nonce) has been sent " & + "to prevent stuck transactions.* Error: " & e.msg return populated diff --git a/testmodule/testContracts.nim b/testmodule/testContracts.nim index 954e94e..d9cbf19 100644 --- a/testmodule/testContracts.nim +++ b/testmodule/testContracts.nim @@ -1,4 +1,6 @@ import std/json +import std/options +import std/strutils import pkg/asynctest import pkg/questionable import pkg/stint @@ -7,6 +9,7 @@ import pkg/ethers/erc20 import ./hardhat import ./miner import ./mocks +import ./examples type TestToken = ref object of Erc20Token