ethers.js: Enable EIP- 2831 Signer transaction.wait() style rejections.

When a Signer sends a transaction, the TransactionResponse object has a .wait() method. Currently that method rejects a CALL_EXCEPTION if the transaction reverts, otherwise it resolves with the TransactionReceipt, once the transaction is mined, based on its hash.

This feature will enable an additional rejection case, along with a new error in the Logger, TRANSACTION_REPLACED. This will occur if the transaction is replaced (i.e. the from and nonce match the transaction but the hash is different), with a field indicating the reason for the replacement:

  • repriced; the data and to match, but the gas price has changed
  • cancelled; the data has been changed to 0x and the to changed to the from
  • replaced; any other change

This must occur in a minor version bump.

In v6 we may change repriced to also resolve (instead of reject), but this requires more thought and would not be backwards compatible within v5.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 19
  • Comments: 30 (8 by maintainers)

Commits related to this issue

Most upvoted comments

This is now available in 5.2.0. Please check out this article for more details. ๐Ÿ˜ƒ

If you wait on a transaction hash it will eventually resolve to the receipt or throw an exception which includes the receipt of the replacement transaction. tx.wait() will eventually finish, one way or the other.

Prior to this feature, tx.wait() would never proceed if the transaction was replaced. If you want to replicate this functionality, you can use the above .catch(), but there are probably very few cases you would want to do that.

Does that make sense?

Sorry Iโ€™m a bit confused, my case is when I donโ€™t have access to the transactionResponse object but only the hash.

So Iโ€™m waiting on the transaction via the provider, say

await web3Provider.waitForTransaction(transactionHash)

In this case, replaceable param is always passed down to _waitFortransaction() as null

    async waitForTransaction(transactionHash: string, confirmations?: number, timeout?: number): Promise<TransactionReceipt> {
        return this._waitForTransaction(transactionHash, (confirmations == null) ? 1: confirmations, timeout || 0, null);
    }

@aysuio, not sure if you found an answer to it, but generally I was using template code from this update: https://blog.ricmoo.com/highlights-ethers-js-may-2021-2826e858277d

 } catch (error) {

        console.debug(error, 'error in the first catch wait await')

        if (error.code === Logger.errors.TRANSACTION_REPLACED) {
          if (error.cancelled) {
            // The transaction was replaced  :'(
            console.debug(error.replacement, 'The transaction was replaced  ')
        
          } else {
            // The user used "speed up" or something similar
            // in their client, but we now have the updated info
            console.debug(error.replacement, 'error.replacement');
            console.debug(error.receipt, 'error.receipt')
         
          }
        }

Let me know if you managed to test this feature on your end. ๐Ÿ˜ƒ I still cannot get it to work because I cannot test it.

tried TRANSACTION_REPLACED, it generated error params as stated in ethers.js enum ErrorCode

There are bunch of other codes like UNPREDICTABLE_GAS_LIMIT, which are well defined in the ErrorCode.

@aysuio, not sure if you found an answer to it, but generally I was using template code from this update: https://blog.ricmoo.com/highlights-ethers-js-may-2021-2826e858277d

 } catch (error) {

        console.debug(error, 'error in the first catch wait await')

        if (error.code === Logger.errors.TRANSACTION_REPLACED) {
          if (error.cancelled) {
            // The transaction was replaced  :'(
            console.debug(error.replacement, 'The transaction was replaced  ')
        
          } else {
            // The user used "speed up" or something similar
            // in their client, but we now have the updated info
            console.debug(error.replacement, 'error.replacement');
            console.debug(error.receipt, 'error.receipt')
         
          }
        }

Let me know if you managed to test this feature on your end. ๐Ÿ˜ƒ I still cannot get it to work because I cannot test it.

How would you recommend I test then the transaction replacement on Polygon?

If you use a private node, then you should be able to send an intentionally underpriced transaction and it would be detected alright. If you use a public rpc, you need to be fast and get lucky.

However, I would be interested in the following: is the new support for replaced transactions provided by ethers library working for you?

Yes, all in all itโ€™s working. Not working only in the case of:

  1. On polygon with public rpc
  2. And I purposefully send a low-priced transaction

There are definitely problems with both Polygon and BSC when it comes to receipts, that are still being tracked down; they seem to emit data before they complete indexing (and therefore responding to requests).

So, the result.transactionHash != tx.hash? Are either left undefined? Do the nonce and from match?