ethers.js: tx.wait() never resolves

I’m facing a strange issue when testing with ganache and increasing my blockTime to greater than 0 to simulate network congestion and failures.

The following code works just fine (meaning it fails and returns the reason for failure) when ganache’s blockTime is 0:

try {
     let tx = await contractWithSigner.contractFunction(someData, { gasLimit: 1 });
     await tx.wait();
} catch (e) { console.log(e); }	

As soon as I increase ganache’s blockTime to any arbitrary number: ganache-cli -b 5, the same code never resolves the tx.wait() and ethers is stuck in a loop of sending out a call for transactionReceipt forever.

I can see in ganache’s output that it does indeed send out the failed transaction receipt message in the block immediately following the contract call. Is my assumption on how this should work incorrect?

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 8
  • Comments: 58 (16 by maintainers)

Most upvoted comments

issue still relevant

@fafrd If this is the only reason tx.wait hangs, this is great news. We can just run a Promise race to account for that, something like

async function waitForTransaction(provider, tx){
    let finished = false;
    const result = await Promise.race([
        tx.wait(),
        (async () => {
            while (!finished) {
                await util.waitMs(3000);
                const mempoolTx = await provider.getTransaction(tx.hash);
                if (!mempoolTx){
                    return null;
                } 
            }
        })()
    ]);
    finished = true;
    if (!result){
        throw `Transaction ${tx.hash} failed`;
    }
    return result;
}

Use provider.getTransactionReceipt(tx.hash); to fetch the receipt. Put the whole thing into a loop and poll for the result.

This is a scary way to do it. That should already be what the internal .wait is doing, except the .wait enforces exponential back-off and will use additional hints from the provider to optimize the network usage.

If you need an immediate solution, I would recommend using the provider.waitForTransaction(hash, confirms?, timeout?) which allows a timeout.

I’m looking into this now, and will let you know what I find shortly. 😃

@ricmoo I’m able to recreate this issue! It can occur with low-fee transactions that drop out of the mempool.

I’m using a JsonRpcProvider, testing on rinkeby testnet with infura https:// endpoint.

To recreate:

  • Create an arbitrary transaction (such as sending ether to an account)
  • Set the priority fee and basefee to be very low; I tested with 0.000000001 gwei (1 wei) but I think this would work with any low fee that causes the tx to drop from the mempool
  • Submit the transaction on-chain. Then run the following snippet:
> txhash = "0xbd1cc2908fa8640b0b6730be79cf01e0624452445a0fe2fb16451c18248b7e0a"
'0xbd1cc2908fa8640b0b6730be79cf01e0624452445a0fe2fb16451c18248b7e0a'
> tx = await provider.getTransaction(txhash) // tx shows as expected!
{
  hash: '0xbd1cc2908fa8640b0b6730be79cf01e0624452445a0fe2fb16451c18248b7e0a',
  type: 2,
  accessList: [],
  blockHash: null,
  blockNumber: null,
  transactionIndex: null,
  confirmations: 0,
  from: '0x271c5832de95de950C112E8A71BB0496448e719c',
  gasPrice: BigNumber { _hex: '0x01', _isBigNumber: true },
  maxPriorityFeePerGas: BigNumber { _hex: '0x01', _isBigNumber: true },
  maxFeePerGas: BigNumber { _hex: '0x01', _isBigNumber: true },
  gasLimit: BigNumber { _hex: '0x5208', _isBigNumber: true },
  to: '0x5295b474F3A0bB39418456c96D6FCf13901A4aA1',
  value: BigNumber { _hex: '0x01b4fbd92b5f8000', _isBigNumber: true },
  nonce: 134,
  data: '0x',
  r: '0x97c538fad503b6a59ab13167e8698d74dcb2e6157ddea0a1d38b61d2aeb801a7',
  s: '0x12a671ddeb8f67e091b91f14942e3273fce594ca9b510facbe6b843f706ae405',
  v: 1,
  creates: null,
  chainId: 4,
  wait: [Function (anonymous)]
}
> await tx.wait(confirms=1)
// hang...

In a separate repl, after some time we see that the transaction dropped from the mempool altogether:

> txhash = "0xbd1cc2908fa8640b0b6730be79cf01e0624452445a0fe2fb16451c18248b7e0a"
'0xbd1cc2908fa8640b0b6730be79cf01e0624452445a0fe2fb16451c18248b7e0a'
> tx = await provider.getTransaction(txhash)
null

But the wait() is still hanging! Furthermore, if I ctrl-c kill the wait(), then restart it (against the same TransactionReceipt object) it will still hang.


EDIT: After reading more closely, I see ricmoo mentioned that tx.wait() will not resolve if the tx is never mined:

If the transaction is never mined, tx.wait() shouldn’t resolve. tx.wait() only resolves once the transaction is mined.

Thus, tx.wait() is not an appropriate function to use if there is concern that the tx will never be mined. I guess it should be used in parallel with some kind of polling mechanism to check that the tx is still present in the mempool at all.

Use provider.getTransactionReceipt(tx.hash); to fetch the receipt. Put the whole thing into a loop and poll for the result.

Break the loop once the tx receipt is received.

Snippet:

const tx = await router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
 ....
 ....
 ...
);


    console.log('txn hash', tx.hash);
    console.log(`Fetching txn receipt....`);

    let receipt = null;

    while (receipt === null) {
      try {
        receipt = await provider.getTransactionReceipt(tx.hash);

        if (receipt === null) {
          console.log(`Trying again to fetch txn receipt....`);

          continue;
        }

        console.log(`Receipt confirmations:`, receipt.confirmations);

        console.info(
          `Transaction receipt : https://www.bscscan.com/tx/${receipt.logs[1].transactionHash}`
        );
      } catch (e) {
        console.log(`Receipt error:`, e);
        break;
      }
    }

Facing the same issue of tx.wait() not resolving or returning an error when a transaction is not picked up by the mining pool on Polygon/Matic.

Ideally, for a transaction that is submitted to nodes but isn’t mined in a certain amount of time, we would want tx.wait() to return an error of some kind. Right now it appears to just get stuck. @ricmoo any ideas on a temporary fix?

Guys, check pollingInterval value in your providers. I was having pollingInterval=1200000(I set it for some test purpose and forgot about it) and my wait() didn’t work. But after changing it to 2000 - it start to work. I didn’t think that it’s common case issue, but anyway it can help in some cases.

Facing the same issue of tx.wait() not resolving indefinitely. But it only happens infrequently, not always.

Does tx.wait() accept a timeout, or is it only available for provider.waitForTransaction()? Tagging a related issue: #1775

Would be nice if tx.wait() had the timeout too.

Error logs says the following:

Error
PollingBlockTracker - encountered an error while attempting to update latest block:
undefined

./node_modules/eth-block-tracker/src/polling.js in e.exports._performSync at line 51:24

 while (this._isRunning) {
      try {
        await this._updateLatestBlock()
        await timeout(this._pollingInterval, !this._keepEventLoopActive)
      } catch (err) {
        >> const newErr = new Error(`PollingBlockTracker - encountered an error while attempting to update latest block:\n${err.stack}`)
        try {
          this.emit('error', newErr)
        } catch (emitErr) {
          console.error(newErr)
        }

What might be causes for the PollingBlockTracker to error out?

Hey all, I figured out the issue and its very noob of me 🙈 the account that I was doing the transaction on had no currency, so it couldn’t pay the gas fees.

I still think this is somewhat of an issue because without a manual timeout catch, you would have no way of picking up this error. So a good suggestion for a PR is to check the accounts balance first and return an error, or to use a timeout. (I am now manually checking balance first which is also actually okay - main point is still that a useful error message is better than an infinite wait)

One of xDai chain users also reported about infinite tx.wait():

I am trying interact with a contract on xDai, but whenever I make the contract call through ethers.js, I end up getting a tx that never gets submitted. To be clear, the tx does not error (I get a valid tx object with a hash after calling contract.functions.methodName()), its just that it never ends up on the network (e.g. calling await tx.wait() stalls forever).

For context, we are using the rpc.xdaichain.com endpoint (tried both https and wss)

Ah, when using the websocket we actually get a return error after a while:

{"jsonrpc":"2.0","error":{"code":-32017,"message":"Timeout","data":"Nethermind.JsonRpc.Modules.ModuleRentalTimeoutException: Unable to rent an instance of IEthRpcModule. Too many concurrent requests.\\n   at Nethermind.JsonRpc.Modules.BoundedModulePool`1.SlowPath() in /src/Nethermind/Nethermind.JsonRpc/Modules/BoundedModulePool.cs:line 58\\n   at Nethermind.JsonRpc.Modules.RpcModuleProvider.<>c__DisplayClass15_0`1.<<Register>b__0>d.MoveNext() in /src/Nethermind/Nethermind.JsonRpc/Modules/RpcModuleProvider.cs:line 74\\n--- End of stack trace from previous location ---\\n   at Nethermind.JsonRpc.JsonRpcService.ExecuteAsync(JsonRpcRequest request, String methodName, ValueTuple`2 method, JsonRpcContext context) in /src/Nethermind/Nethermind.JsonRpc/JsonRpcService.cs:line 162\\n   at Nethermind.JsonRpc.JsonRpcService.ExecuteRequestAsync(JsonRpcRequest rpcRequest, JsonRpcContext context) in /src/Nethermind/Nethermind.JsonRpc/JsonRpcService.cs:line 115\\n   at Nethermind.JsonRpc.JsonRpcService.SendRequestAsync(JsonRpcRequest rpcRequest, JsonRpcContext context) in /src/Nethermind/Nethermind.JsonRpc/JsonRpcService.cs:line 105"},"id":2}

The error Timeout is returned by Nethermind client when the transaction cannot be handled in time. But tx.wait() stalls. Can the reason be in the Timeout error message? I guess, ethers.js should handle any errors.

Can you give advice on how to set a timeout on ethers.js level? For example, in web3 we have transactionBlockTimeout, transactionConfirmationBlocks, and transactionPollingTimeout options.

@ricmoo I changed from ankr to morails (i’m on bsc blockchain). I only changed the ws:// link and the code worked fine. Maybe the problem is on ankr api.

Hello everyone, I’m having the same issue as stated on the title. My tx.wait() never resolves, even thought the transaction has been successfuly mined.

try {
        const tx = await router.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            [data.WBNB, token],
            data.recipient,
            Date.now() + 1000 * 60 * 5, // 5 minutes
            {
                gasLimit: data.gasLimit,
                gasPrice: ethers.utils.parseUnits(`${data.gasPrice}`, "gwei"),
                nonce: null,
            }
        );
        receipt = await tx.wait();
        if (receipt.status) {
            console.log(`Transaction receipt : https://www.bscscan.com/tx/${receipt.logs[1].transactionHash}\n`);
        }
    } catch (err) {
        let error = JSON.parse(JSON.stringify(err));
        [...]

I’m using the BSC testnet to try out the script. If I go to the bscscan testnet site I can see that my transaction was successful. I don’t know what else to do.

@TeaH4nd have you been able to solve this problem? It happens to me too. The transaction went through but tx.wait() just never resolves.

PS: I’m on mainnet.

I’ve been having the same issue but I believe I have a more realistic gasPrice and the transaction still never goes through. Just hangs indefinitely on tx.wait():

const gasPrice = 50000000000;
    const swap1 = await wallet.sendTransaction({
      data: tx1.data,
      chainId: tx1.chainId,
      from: tx1.from,
      gasLimit: 350449, 
      gasPrice: gasPrice,
      value: '0x' + new BigNumber(tx1.value).toString(16),
      to: tx1.to,
      nonce: nonce,
    });

Thanks @Benjythebee, I have this issue opened in a tab to investigate once v5.2 is released, so this is good to know. 😃