github.com/ethereum-optimism/optimism@v1.7.2/packages/sdk/src/cross-chain-messenger.ts (about)

     1  /* eslint-disable @typescript-eslint/no-unused-vars */
     2  import {
     3    Provider,
     4    BlockTag,
     5    TransactionReceipt,
     6    TransactionResponse,
     7    TransactionRequest,
     8  } from '@ethersproject/abstract-provider'
     9  import { Signer } from '@ethersproject/abstract-signer'
    10  import {
    11    ethers,
    12    BigNumber,
    13    Overrides,
    14    CallOverrides,
    15    PayableOverrides,
    16  } from 'ethers'
    17  import {
    18    sleep,
    19    remove0x,
    20    toHexString,
    21    toRpcHexString,
    22    encodeCrossDomainMessageV0,
    23    encodeCrossDomainMessageV1,
    24    BedrockOutputData,
    25    BedrockCrossChainMessageProof,
    26    decodeVersionedNonce,
    27    encodeVersionedNonce,
    28    getChainId,
    29    hashCrossDomainMessagev0,
    30    hashCrossDomainMessagev1,
    31  } from '@eth-optimism/core-utils'
    32  import { getContractInterface, predeploys } from '@eth-optimism/contracts'
    33  import * as rlp from 'rlp'
    34  import semver from 'semver'
    35  
    36  import {
    37    OEContracts,
    38    OEContractsLike,
    39    MessageLike,
    40    MessageRequestLike,
    41    TransactionLike,
    42    AddressLike,
    43    NumberLike,
    44    SignerOrProviderLike,
    45    CrossChainMessage,
    46    CrossChainMessageRequest,
    47    CrossChainMessageProof,
    48    MessageDirection,
    49    MessageStatus,
    50    TokenBridgeMessage,
    51    MessageReceipt,
    52    MessageReceiptStatus,
    53    BridgeAdapterData,
    54    BridgeAdapters,
    55    StateRoot,
    56    StateRootBatch,
    57    IBridgeAdapter,
    58    ProvenWithdrawal,
    59    LowLevelMessage,
    60  } from './interfaces'
    61  import {
    62    toSignerOrProvider,
    63    toNumber,
    64    toTransactionHash,
    65    DeepPartial,
    66    getAllOEContracts,
    67    getBridgeAdapters,
    68    makeMerkleTreeProof,
    69    makeStateTrieProof,
    70    hashLowLevelMessage,
    71    migratedWithdrawalGasLimit,
    72    DEPOSIT_CONFIRMATION_BLOCKS,
    73    CHAIN_BLOCK_TIMES,
    74    hashMessageHash,
    75    getContractInterfaceBedrock,
    76    toJsonRpcProvider,
    77  } from './utils'
    78  
    79  export class CrossChainMessenger {
    80    /**
    81     * Provider connected to the L1 chain.
    82     */
    83    public l1SignerOrProvider: Signer | Provider
    84  
    85    /**
    86     * Provider connected to the L2 chain.
    87     */
    88    public l2SignerOrProvider: Signer | Provider
    89  
    90    /**
    91     * Chain ID for the L1 network.
    92     */
    93    public l1ChainId: number
    94  
    95    /**
    96     * Chain ID for the L2 network.
    97     */
    98    public l2ChainId: number
    99  
   100    /**
   101     * Contract objects attached to their respective providers and addresses.
   102     */
   103    public contracts: OEContracts
   104  
   105    /**
   106     * List of custom bridges for the given network.
   107     */
   108    public bridges: BridgeAdapters
   109  
   110    /**
   111     * Number of blocks before a deposit is considered confirmed.
   112     */
   113    public depositConfirmationBlocks: number
   114  
   115    /**
   116     * Estimated average L1 block time in seconds.
   117     */
   118    public l1BlockTimeSeconds: number
   119  
   120    /**
   121     * Whether or not Bedrock compatibility is enabled.
   122     */
   123    public bedrock: boolean
   124  
   125    /**
   126     * Cache for output root validation. Output roots are expensive to verify, so we cache them.
   127     */
   128    private _outputCache: Array<{ root: string; valid: boolean }> = []
   129  
   130    /**
   131     * Creates a new CrossChainProvider instance.
   132     *
   133     * @param opts Options for the provider.
   134     * @param opts.l1SignerOrProvider Signer or Provider for the L1 chain, or a JSON-RPC url.
   135     * @param opts.l2SignerOrProvider Signer or Provider for the L2 chain, or a JSON-RPC url.
   136     * @param opts.l1ChainId Chain ID for the L1 chain.
   137     * @param opts.l2ChainId Chain ID for the L2 chain.
   138     * @param opts.depositConfirmationBlocks Optional number of blocks before a deposit is confirmed.
   139     * @param opts.l1BlockTimeSeconds Optional estimated block time in seconds for the L1 chain.
   140     * @param opts.contracts Optional contract address overrides.
   141     * @param opts.bridges Optional bridge address list.
   142     * @param opts.bedrock Whether or not to enable Bedrock compatibility.
   143     */
   144    constructor(opts: {
   145      l1SignerOrProvider: SignerOrProviderLike
   146      l2SignerOrProvider: SignerOrProviderLike
   147      l1ChainId: NumberLike
   148      l2ChainId: NumberLike
   149      depositConfirmationBlocks?: NumberLike
   150      l1BlockTimeSeconds?: NumberLike
   151      contracts?: DeepPartial<OEContractsLike>
   152      bridges?: BridgeAdapterData
   153      bedrock?: boolean
   154    }) {
   155      this.bedrock = opts.bedrock ?? true
   156  
   157      this.l1SignerOrProvider = toSignerOrProvider(opts.l1SignerOrProvider)
   158      this.l2SignerOrProvider = toSignerOrProvider(opts.l2SignerOrProvider)
   159  
   160      try {
   161        this.l1ChainId = toNumber(opts.l1ChainId)
   162      } catch (err) {
   163        throw new Error(`L1 chain ID is missing or invalid: ${opts.l1ChainId}`)
   164      }
   165  
   166      try {
   167        this.l2ChainId = toNumber(opts.l2ChainId)
   168      } catch (err) {
   169        throw new Error(`L2 chain ID is missing or invalid: ${opts.l2ChainId}`)
   170      }
   171  
   172      this.depositConfirmationBlocks =
   173        opts?.depositConfirmationBlocks !== undefined
   174          ? toNumber(opts.depositConfirmationBlocks)
   175          : DEPOSIT_CONFIRMATION_BLOCKS[this.l2ChainId] || 0
   176  
   177      this.l1BlockTimeSeconds =
   178        opts?.l1BlockTimeSeconds !== undefined
   179          ? toNumber(opts.l1BlockTimeSeconds)
   180          : CHAIN_BLOCK_TIMES[this.l1ChainId] || 1
   181  
   182      this.contracts = getAllOEContracts(this.l2ChainId, {
   183        l1SignerOrProvider: this.l1SignerOrProvider,
   184        l2SignerOrProvider: this.l2SignerOrProvider,
   185        overrides: opts.contracts,
   186      })
   187  
   188      this.bridges = getBridgeAdapters(this.l2ChainId, this, {
   189        overrides: opts.bridges,
   190        contracts: opts.contracts,
   191      })
   192    }
   193  
   194    /**
   195     * Provider connected to the L1 chain.
   196     */
   197    get l1Provider(): Provider {
   198      if (Provider.isProvider(this.l1SignerOrProvider)) {
   199        return this.l1SignerOrProvider
   200      } else {
   201        return this.l1SignerOrProvider.provider
   202      }
   203    }
   204  
   205    /**
   206     * Provider connected to the L2 chain.
   207     */
   208    get l2Provider(): Provider {
   209      if (Provider.isProvider(this.l2SignerOrProvider)) {
   210        return this.l2SignerOrProvider
   211      } else {
   212        return this.l2SignerOrProvider.provider
   213      }
   214    }
   215  
   216    /**
   217     * Signer connected to the L1 chain.
   218     */
   219    get l1Signer(): Signer {
   220      if (Provider.isProvider(this.l1SignerOrProvider)) {
   221        throw new Error(`messenger has no L1 signer`)
   222      } else {
   223        return this.l1SignerOrProvider
   224      }
   225    }
   226  
   227    /**
   228     * Signer connected to the L2 chain.
   229     */
   230    get l2Signer(): Signer {
   231      if (Provider.isProvider(this.l2SignerOrProvider)) {
   232        throw new Error(`messenger has no L2 signer`)
   233      } else {
   234        return this.l2SignerOrProvider
   235      }
   236    }
   237  
   238    /**
   239     * Uses portal version to determine if the messenger is using fpac contracts. Better not to cache
   240     * this value as it will change during the fpac upgrade and we want clients to automatically
   241     * begin using the new logic without throwing any errors.
   242     *
   243     * @returns Whether or not the messenger is using fpac contracts.
   244     */
   245    public async fpac(): Promise<boolean> {
   246      if (
   247        this.contracts.l1.OptimismPortal.address === ethers.constants.AddressZero
   248      ) {
   249        // Only really relevant for certain SDK tests where the portal is not deployed. We should
   250        // probably just update the tests so the portal gets deployed but feels like it's out of
   251        // scope for the FPAC changes.
   252        return false
   253      } else {
   254        return semver.gte(
   255          await this.contracts.l1.OptimismPortal.version(),
   256          '3.0.0'
   257        )
   258      }
   259    }
   260  
   261    /**
   262     * Retrieves all cross chain messages sent within a given transaction.
   263     *
   264     * @param transaction Transaction hash or receipt to find messages from.
   265     * @param opts Options object.
   266     * @param opts.direction Direction to search for messages in. If not provided, will attempt to
   267     * automatically search both directions under the assumption that a transaction hash will only
   268     * exist on one chain. If the hash exists on both chains, will throw an error.
   269     * @returns All cross chain messages sent within the transaction.
   270     */
   271    public async getMessagesByTransaction(
   272      transaction: TransactionLike,
   273      opts: {
   274        direction?: MessageDirection
   275      } = {}
   276    ): Promise<CrossChainMessage[]> {
   277      // Wait for the transaction receipt if the input is waitable.
   278      await (transaction as TransactionResponse).wait?.()
   279  
   280      // Convert the input to a transaction hash.
   281      const txHash = toTransactionHash(transaction)
   282  
   283      let receipt: TransactionReceipt
   284      if (opts.direction !== undefined) {
   285        // Get the receipt for the requested direction.
   286        if (opts.direction === MessageDirection.L1_TO_L2) {
   287          receipt = await this.l1Provider.getTransactionReceipt(txHash)
   288        } else {
   289          receipt = await this.l2Provider.getTransactionReceipt(txHash)
   290        }
   291      } else {
   292        // Try both directions, starting with L1 => L2.
   293        receipt = await this.l1Provider.getTransactionReceipt(txHash)
   294        if (receipt) {
   295          opts.direction = MessageDirection.L1_TO_L2
   296        } else {
   297          receipt = await this.l2Provider.getTransactionReceipt(txHash)
   298          opts.direction = MessageDirection.L2_TO_L1
   299        }
   300      }
   301  
   302      if (!receipt) {
   303        throw new Error(`unable to find transaction receipt for ${txHash}`)
   304      }
   305  
   306      // By this point opts.direction will always be defined.
   307      const messenger =
   308        opts.direction === MessageDirection.L1_TO_L2
   309          ? this.contracts.l1.L1CrossDomainMessenger
   310          : this.contracts.l2.L2CrossDomainMessenger
   311  
   312      return receipt.logs
   313        .filter((log) => {
   314          // Only look at logs emitted by the messenger address
   315          return log.address === messenger.address
   316        })
   317        .filter((log) => {
   318          // Only look at SentMessage logs specifically
   319          const parsed = messenger.interface.parseLog(log)
   320          return parsed.name === 'SentMessage'
   321        })
   322        .map((log) => {
   323          // Try to pull out the value field, but only if the very next log is a SentMessageExtension1
   324          // event which was introduced in the Bedrock upgrade.
   325          let value = ethers.BigNumber.from(0)
   326          const next = receipt.logs.find((l) => {
   327            return (
   328              l.logIndex === log.logIndex + 1 && l.address === messenger.address
   329            )
   330          })
   331          if (next) {
   332            const nextParsed = messenger.interface.parseLog(next)
   333            if (nextParsed.name === 'SentMessageExtension1') {
   334              value = nextParsed.args.value
   335            }
   336          }
   337  
   338          // Convert each SentMessage log into a message object
   339          const parsed = messenger.interface.parseLog(log)
   340          return {
   341            direction: opts.direction,
   342            target: parsed.args.target,
   343            sender: parsed.args.sender,
   344            message: parsed.args.message,
   345            messageNonce: parsed.args.messageNonce,
   346            value,
   347            minGasLimit: parsed.args.gasLimit,
   348            logIndex: log.logIndex,
   349            blockNumber: log.blockNumber,
   350            transactionHash: log.transactionHash,
   351          }
   352        })
   353    }
   354  
   355    /**
   356     * Transforms a legacy message into its corresponding Bedrock representation.
   357     *
   358     * @param message Legacy message to transform.
   359     * @param messageIndex The index of the message, if multiple exist from multicall
   360     * @returns Bedrock representation of the message.
   361     */
   362    public async toBedrockCrossChainMessage(
   363      message: MessageLike,
   364      messageIndex = 0
   365    ): Promise<CrossChainMessage> {
   366      const resolved = await this.toCrossChainMessage(message, messageIndex)
   367  
   368      // Bedrock messages are already in the correct format.
   369      const { version } = decodeVersionedNonce(resolved.messageNonce)
   370      if (version.eq(1)) {
   371        return resolved
   372      }
   373  
   374      let value = BigNumber.from(0)
   375      if (
   376        resolved.direction === MessageDirection.L2_TO_L1 &&
   377        resolved.sender === this.contracts.l2.L2StandardBridge.address &&
   378        resolved.target === this.contracts.l1.L1StandardBridge.address
   379      ) {
   380        try {
   381          ;[, , value] =
   382            this.contracts.l1.L1StandardBridge.interface.decodeFunctionData(
   383              'finalizeETHWithdrawal',
   384              resolved.message
   385            )
   386        } catch (err) {
   387          // No problem, not a message with value.
   388        }
   389      }
   390  
   391      return {
   392        ...resolved,
   393        value,
   394        minGasLimit: BigNumber.from(0),
   395        messageNonce: encodeVersionedNonce(
   396          BigNumber.from(0),
   397          resolved.messageNonce
   398        ),
   399      }
   400    }
   401  
   402    /**
   403     * Transforms a CrossChainMessenger message into its low-level representation inside the
   404     * L2ToL1MessagePasser contract on L2.
   405     *
   406     * @param message Message to transform.
   407     * @param messageIndex The index of the message, if multiple exist from multicall
   408     * @return Transformed message.
   409     */
   410    public async toLowLevelMessage(
   411      message: MessageLike,
   412      messageIndex = 0
   413    ): Promise<LowLevelMessage> {
   414      const resolved = await this.toCrossChainMessage(message, messageIndex)
   415      if (resolved.direction === MessageDirection.L1_TO_L2) {
   416        throw new Error(`can only convert L2 to L1 messages to low level`)
   417      }
   418  
   419      // We may have to update the message if it's a legacy message.
   420      const { version } = decodeVersionedNonce(resolved.messageNonce)
   421      let updated: CrossChainMessage
   422      if (version.eq(0)) {
   423        updated = await this.toBedrockCrossChainMessage(resolved, messageIndex)
   424      } else {
   425        updated = resolved
   426      }
   427  
   428      // Encode the updated message, we need this for legacy messages.
   429      const encoded = encodeCrossDomainMessageV1(
   430        updated.messageNonce,
   431        updated.sender,
   432        updated.target,
   433        updated.value,
   434        updated.minGasLimit,
   435        updated.message
   436      )
   437  
   438      // EVERYTHING following here is basically repeating the logic from getMessagesByTransaction
   439      // consider cleaning this up
   440      // We need to figure out the final withdrawal data that was used to compute the withdrawal hash
   441      // inside the L2ToL1Message passer contract. Exact mechanism here depends on whether or not
   442      // this is a legacy message or a new Bedrock message.
   443      let gasLimit: BigNumber
   444      let messageNonce: BigNumber
   445      if (version.eq(0)) {
   446        const chainID = await getChainId(this.l2Provider)
   447        gasLimit = migratedWithdrawalGasLimit(encoded, chainID)
   448        messageNonce = resolved.messageNonce
   449      } else {
   450        const receipt = await this.l2Provider.getTransactionReceipt(
   451          (
   452            await this.toCrossChainMessage(message)
   453          ).transactionHash
   454        )
   455  
   456        const withdrawals: ethers.utils.Result[] = []
   457        for (const log of receipt.logs) {
   458          if (log.address === this.contracts.l2.BedrockMessagePasser.address) {
   459            const decoded =
   460              this.contracts.l2.L2ToL1MessagePasser.interface.parseLog(log)
   461            if (decoded.name === 'MessagePassed') {
   462              withdrawals.push(decoded.args)
   463            }
   464          }
   465        }
   466  
   467        // Should not happen.
   468        if (withdrawals.length === 0) {
   469          throw new Error(`no withdrawals found in receipt`)
   470        }
   471  
   472        const withdrawal = withdrawals[messageIndex]
   473        if (!withdrawal) {
   474          throw new Error(
   475            `withdrawal index ${messageIndex} out of bounds there are ${withdrawals.length} withdrawals`
   476          )
   477        }
   478        messageNonce = withdrawal.nonce
   479        gasLimit = withdrawal.gasLimit
   480      }
   481  
   482      return {
   483        messageNonce,
   484        sender: this.contracts.l2.L2CrossDomainMessenger.address,
   485        target: this.contracts.l1.L1CrossDomainMessenger.address,
   486        value: updated.value,
   487        minGasLimit: gasLimit,
   488        message: encoded,
   489      }
   490    }
   491  
   492    // public async getMessagesByAddress(
   493    //   address: AddressLike,
   494    //   opts?: {
   495    //     direction?: MessageDirection
   496    //     fromBlock?: NumberLike
   497    //     toBlock?: NumberLike
   498    //   }
   499    // ): Promise<CrossChainMessage[]> {
   500    //   throw new Error(`
   501    //     The function getMessagesByAddress is currently not enabled because the sender parameter of
   502    //     the SentMessage event is not indexed within the CrossChainMessenger contracts.
   503    //     getMessagesByAddress will be enabled by plugging in an Optimism Indexer (coming soon).
   504    //     See the following issue on GitHub for additional context:
   505    //     https://github.com/ethereum-optimism/optimism/issues/2129
   506    //   `)
   507    // }
   508  
   509    /**
   510     * Finds the appropriate bridge adapter for a given L1<>L2 token pair. Will throw if no bridges
   511     * support the token pair or if more than one bridge supports the token pair.
   512     *
   513     * @param l1Token L1 token address.
   514     * @param l2Token L2 token address.
   515     * @returns The appropriate bridge adapter for the given token pair.
   516     */
   517    public async getBridgeForTokenPair(
   518      l1Token: AddressLike,
   519      l2Token: AddressLike
   520    ): Promise<IBridgeAdapter> {
   521      const bridges: IBridgeAdapter[] = []
   522      for (const bridge of Object.values(this.bridges)) {
   523        try {
   524          if (await bridge.supportsTokenPair(l1Token, l2Token)) {
   525            bridges.push(bridge)
   526          }
   527        } catch (err) {
   528          if (
   529            !err?.message?.toString().includes('CALL_EXCEPTION') &&
   530            !err?.stack?.toString().includes('execution reverted')
   531          ) {
   532            console.error('Unexpected error when checking bridge', err)
   533          }
   534        }
   535      }
   536  
   537      if (bridges.length === 0) {
   538        throw new Error(`no supported bridge for token pair`)
   539      }
   540  
   541      if (bridges.length > 1) {
   542        throw new Error(`found more than one bridge for token pair`)
   543      }
   544  
   545      return bridges[0]
   546    }
   547  
   548    /**
   549     * Gets all deposits for a given address.
   550     *
   551     * @param address Address to search for messages from.
   552     * @param opts Options object.
   553     * @param opts.fromBlock Block to start searching for messages from. If not provided, will start
   554     * from the first block (block #0).
   555     * @param opts.toBlock Block to stop searching for messages at. If not provided, will stop at the
   556     * latest known block ("latest").
   557     * @returns All deposit token bridge messages sent by the given address.
   558     */
   559    public async getDepositsByAddress(
   560      address: AddressLike,
   561      opts: {
   562        fromBlock?: BlockTag
   563        toBlock?: BlockTag
   564      } = {}
   565    ): Promise<TokenBridgeMessage[]> {
   566      return (
   567        await Promise.all(
   568          Object.values(this.bridges).map(async (bridge) => {
   569            return bridge.getDepositsByAddress(address, opts)
   570          })
   571        )
   572      )
   573        .reduce((acc, val) => {
   574          return acc.concat(val)
   575        }, [])
   576        .sort((a, b) => {
   577          // Sort descending by block number
   578          return b.blockNumber - a.blockNumber
   579        })
   580    }
   581  
   582    /**
   583     * Gets all withdrawals for a given address.
   584     *
   585     * @param address Address to search for messages from.
   586     * @param opts Options object.
   587     * @param opts.fromBlock Block to start searching for messages from. If not provided, will start
   588     * from the first block (block #0).
   589     * @param opts.toBlock Block to stop searching for messages at. If not provided, will stop at the
   590     * latest known block ("latest").
   591     * @returns All withdrawal token bridge messages sent by the given address.
   592     */
   593    public async getWithdrawalsByAddress(
   594      address: AddressLike,
   595      opts: {
   596        fromBlock?: BlockTag
   597        toBlock?: BlockTag
   598      } = {}
   599    ): Promise<TokenBridgeMessage[]> {
   600      return (
   601        await Promise.all(
   602          Object.values(this.bridges).map(async (bridge) => {
   603            return bridge.getWithdrawalsByAddress(address, opts)
   604          })
   605        )
   606      )
   607        .reduce((acc, val) => {
   608          return acc.concat(val)
   609        }, [])
   610        .sort((a, b) => {
   611          // Sort descending by block number
   612          return b.blockNumber - a.blockNumber
   613        })
   614    }
   615  
   616    /**
   617     * Resolves a MessageLike into a CrossChainMessage object.
   618     * Unlike other coercion functions, this function is stateful and requires making additional
   619     * requests. For now I'm going to keep this function here, but we could consider putting a
   620     * similar function inside of utils/coercion.ts if people want to use this without having to
   621     * create an entire CrossChainProvider object.
   622     *
   623     * @param message MessageLike to resolve into a CrossChainMessage.
   624     * @param messageIndex The index of the message, if multiple exist from multicall
   625     * @returns Message coerced into a CrossChainMessage.
   626     */
   627    public async toCrossChainMessage(
   628      message: MessageLike,
   629      messageIndex = 0
   630    ): Promise<CrossChainMessage> {
   631      if (!message) {
   632        throw new Error('message is undefined')
   633      }
   634      // TODO: Convert these checks into proper type checks.
   635      if ((message as CrossChainMessage).message) {
   636        return message as CrossChainMessage
   637      } else if (
   638        (message as TokenBridgeMessage).l1Token &&
   639        (message as TokenBridgeMessage).l2Token &&
   640        (message as TokenBridgeMessage).transactionHash
   641      ) {
   642        const messages = await this.getMessagesByTransaction(
   643          (message as TokenBridgeMessage).transactionHash
   644        )
   645  
   646        // The `messages` object corresponds to a list of SentMessage events that were triggered by
   647        // the same transaction. We want to find the specific SentMessage event that corresponds to
   648        // the TokenBridgeMessage (either a ETHDepositInitiated, ERC20DepositInitiated, or
   649        // WithdrawalInitiated event). We expect the behavior of bridge contracts to be that these
   650        // TokenBridgeMessage events are triggered and then a SentMessage event is triggered. Our
   651        // goal here is therefore to find the first SentMessage event that comes after the input
   652        // event.
   653        const found = messages
   654          .sort((a, b) => {
   655            // Sort all messages in ascending order by log index.
   656            return a.logIndex - b.logIndex
   657          })
   658          .find((m) => {
   659            return m.logIndex > (message as TokenBridgeMessage).logIndex
   660          })
   661  
   662        if (!found) {
   663          throw new Error(`could not find SentMessage event for message`)
   664        }
   665  
   666        return found
   667      } else {
   668        // TODO: Explicit TransactionLike check and throw if not TransactionLike
   669        const messages = await this.getMessagesByTransaction(
   670          message as TransactionLike
   671        )
   672  
   673        const out = messages[messageIndex]
   674        if (!out) {
   675          throw new Error(
   676            `withdrawal index ${messageIndex} out of bounds. There are ${messages.length} withdrawals`
   677          )
   678        }
   679        return out
   680      }
   681    }
   682  
   683    /**
   684     * Retrieves the status of a particular message as an enum.
   685     *
   686     * @param message Cross chain message to check the status of.
   687     * @param messageIndex The index of the message, if multiple exist from multicall
   688     * @param fromBlockOrBlockHash The start block to use for the query filter on the RECEIVING chain
   689     * @param toBlockOrBlockHash The end block to use for the query filter on the RECEIVING chain
   690     * @returns Status of the message.
   691     */
   692    public async getMessageStatus(
   693      message: MessageLike,
   694      // consider making this an options object next breaking release
   695      messageIndex = 0,
   696      fromBlockOrBlockHash?: BlockTag,
   697      toBlockOrBlockHash?: BlockTag
   698    ): Promise<MessageStatus> {
   699      const resolved = await this.toCrossChainMessage(message, messageIndex)
   700      // legacy withdrawals relayed prebedrock are v1
   701      const messageHashV0 = hashCrossDomainMessagev0(
   702        resolved.target,
   703        resolved.sender,
   704        resolved.message,
   705        resolved.messageNonce
   706      )
   707      // bedrock withdrawals are v1
   708      // legacy withdrawals relayed postbedrock are v1
   709      // there is no good way to differentiate between the two types of legacy
   710      // so what we will check for both
   711      const messageHashV1 = hashCrossDomainMessagev1(
   712        resolved.messageNonce,
   713        resolved.sender,
   714        resolved.target,
   715        resolved.value,
   716        resolved.minGasLimit,
   717        resolved.message
   718      )
   719  
   720      // Here we want the messenger that will receive the message, not the one that sent it.
   721      const messenger =
   722        resolved.direction === MessageDirection.L1_TO_L2
   723          ? this.contracts.l2.L2CrossDomainMessenger
   724          : this.contracts.l1.L1CrossDomainMessenger
   725  
   726      const success =
   727        (await messenger.successfulMessages(messageHashV0)) ||
   728        (await messenger.successfulMessages(messageHashV1))
   729  
   730      // Avoid the extra query if we already know the message was successful.
   731      if (success) {
   732        return MessageStatus.RELAYED
   733      }
   734  
   735      const failure =
   736        (await messenger.failedMessages(messageHashV0)) ||
   737        (await messenger.failedMessages(messageHashV1))
   738  
   739      if (resolved.direction === MessageDirection.L1_TO_L2) {
   740        if (failure) {
   741          return MessageStatus.FAILED_L1_TO_L2_MESSAGE
   742        } else {
   743          return MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE
   744        }
   745      } else {
   746        if (failure) {
   747          return MessageStatus.READY_FOR_RELAY
   748        } else {
   749          let timestamp: number
   750          if (this.bedrock) {
   751            const output = await this.getMessageBedrockOutput(
   752              resolved,
   753              messageIndex
   754            )
   755            if (output === null) {
   756              return MessageStatus.STATE_ROOT_NOT_PUBLISHED
   757            }
   758  
   759            // Convert the message to the low level message that was proven.
   760            const withdrawal = await this.toLowLevelMessage(
   761              resolved,
   762              messageIndex
   763            )
   764  
   765            // Pick portal based on FPAC compatibility.
   766            const portal = (await this.fpac())
   767              ? this.contracts.l1.OptimismPortal2
   768              : this.contracts.l1.OptimismPortal
   769  
   770            // Attempt to fetch the proven withdrawal.
   771            const provenWithdrawal = await portal.provenWithdrawals(
   772              hashLowLevelMessage(withdrawal)
   773            )
   774  
   775            // If the withdrawal hash has not been proven on L1,
   776            // return `READY_TO_PROVE`
   777            if (provenWithdrawal.timestamp.eq(BigNumber.from(0))) {
   778              return MessageStatus.READY_TO_PROVE
   779            }
   780  
   781            // Set the timestamp to the provenWithdrawal's timestamp
   782            timestamp = provenWithdrawal.timestamp.toNumber()
   783          } else {
   784            const stateRoot = await this.getMessageStateRoot(
   785              resolved,
   786              messageIndex
   787            )
   788            if (stateRoot === null) {
   789              return MessageStatus.STATE_ROOT_NOT_PUBLISHED
   790            }
   791  
   792            const bn = stateRoot.batch.blockNumber
   793            const block = await this.l1Provider.getBlock(bn)
   794            timestamp = block.timestamp
   795          }
   796  
   797          if (await this.fpac()) {
   798            // Convert the message to the low level message that was proven.
   799            const withdrawal = await this.toLowLevelMessage(
   800              resolved,
   801              messageIndex
   802            )
   803  
   804            // Get the withdrawal hash.
   805            const withdrawalHash = hashLowLevelMessage(withdrawal)
   806  
   807            // Grab the proven withdrawal data.
   808            const provenWithdrawal =
   809              await this.contracts.l1.OptimismPortal2.provenWithdrawals(
   810                withdrawalHash
   811              )
   812  
   813            // Attach to the FaultDisputeGame.
   814            const game = new ethers.Contract(
   815              provenWithdrawal.disputeGameProxy,
   816              getContractInterfaceBedrock('FaultDisputeGame'),
   817              this.l1SignerOrProvider
   818            )
   819  
   820            // Check if the game resolved to status 1 = "CHALLENGER_WINS". If so, the withdrawal was
   821            // proven against a proposal that was invalidated and will need to be reproven. We throw
   822            // an error here instead of creating a new status mostly because it's easier to integrate
   823            // into the SDK.
   824            const status = await game.status()
   825            if (status === 1) {
   826              throw new Error(`withdrawal proposal was invalidated, must reprove`)
   827            }
   828  
   829            try {
   830              // If this doesn't revert then we should be fine to relay.
   831              await this.contracts.l1.OptimismPortal2.checkWithdrawal(
   832                hashLowLevelMessage(withdrawal)
   833              )
   834  
   835              return MessageStatus.READY_FOR_RELAY
   836            } catch (err) {
   837              return MessageStatus.IN_CHALLENGE_PERIOD
   838            }
   839          } else {
   840            const challengePeriod = await this.getChallengePeriodSeconds()
   841            const latestBlock = await this.l1Provider.getBlock('latest')
   842  
   843            if (timestamp + challengePeriod > latestBlock.timestamp) {
   844              return MessageStatus.IN_CHALLENGE_PERIOD
   845            } else {
   846              return MessageStatus.READY_FOR_RELAY
   847            }
   848          }
   849        }
   850      }
   851    }
   852  
   853    /**
   854     * Finds the receipt of the transaction that executed a particular cross chain message.
   855     *
   856     * @param message Message to find the receipt of.
   857     * @param messageIndex The index of the message, if multiple exist from multicall
   858     * @param fromBlockOrBlockHash The start block to use for the query filter on the RECEIVING chain
   859     * @param toBlockOrBlockHash The end block to use for the query filter on the RECEIVING chain
   860     * @returns CrossChainMessage receipt including receipt of the transaction that relayed the
   861     * given message.
   862     */
   863    public async getMessageReceipt(
   864      message: MessageLike,
   865      messageIndex = 0,
   866      fromBlockOrBlockHash?: BlockTag,
   867      toBlockOrHash?: BlockTag
   868    ): Promise<MessageReceipt> {
   869      const resolved = await this.toCrossChainMessage(message, messageIndex)
   870      // legacy withdrawals relayed prebedrock are v1
   871      const messageHashV0 = hashCrossDomainMessagev0(
   872        resolved.target,
   873        resolved.sender,
   874        resolved.message,
   875        resolved.messageNonce
   876      )
   877      // bedrock withdrawals are v1
   878      // legacy withdrawals relayed postbedrock are v1
   879      // there is no good way to differentiate between the two types of legacy
   880      // so what we will check for both
   881      const messageHashV1 = hashCrossDomainMessagev1(
   882        resolved.messageNonce,
   883        resolved.sender,
   884        resolved.target,
   885        resolved.value,
   886        resolved.minGasLimit,
   887        resolved.message
   888      )
   889  
   890      // Here we want the messenger that will receive the message, not the one that sent it.
   891      const messenger =
   892        resolved.direction === MessageDirection.L1_TO_L2
   893          ? this.contracts.l2.L2CrossDomainMessenger
   894          : this.contracts.l1.L1CrossDomainMessenger
   895  
   896      // this is safe because we can guarantee only one of these filters max will return something
   897      const relayedMessageEvents = [
   898        ...(await messenger.queryFilter(
   899          messenger.filters.RelayedMessage(messageHashV0),
   900          fromBlockOrBlockHash,
   901          toBlockOrHash
   902        )),
   903        ...(await messenger.queryFilter(
   904          messenger.filters.RelayedMessage(messageHashV1),
   905          fromBlockOrBlockHash,
   906          toBlockOrHash
   907        )),
   908      ]
   909  
   910      // Great, we found the message. Convert it into a transaction receipt.
   911      if (relayedMessageEvents.length === 1) {
   912        return {
   913          receiptStatus: MessageReceiptStatus.RELAYED_SUCCEEDED,
   914          transactionReceipt:
   915            await relayedMessageEvents[0].getTransactionReceipt(),
   916        }
   917      } else if (relayedMessageEvents.length > 1) {
   918        // Should never happen!
   919        throw new Error(`multiple successful relays for message`)
   920      }
   921  
   922      // We didn't find a transaction that relayed the message. We now attempt to find
   923      // FailedRelayedMessage events instead.
   924      const failedRelayedMessageEvents = [
   925        ...(await messenger.queryFilter(
   926          messenger.filters.FailedRelayedMessage(messageHashV0),
   927          fromBlockOrBlockHash,
   928          toBlockOrHash
   929        )),
   930        ...(await messenger.queryFilter(
   931          messenger.filters.FailedRelayedMessage(messageHashV1),
   932          fromBlockOrBlockHash,
   933          toBlockOrHash
   934        )),
   935      ]
   936  
   937      // A transaction can fail to be relayed multiple times. We'll always return the last
   938      // transaction that attempted to relay the message.
   939      // TODO: Is this the best way to handle this?
   940      if (failedRelayedMessageEvents.length > 0) {
   941        return {
   942          receiptStatus: MessageReceiptStatus.RELAYED_FAILED,
   943          transactionReceipt: await failedRelayedMessageEvents[
   944            failedRelayedMessageEvents.length - 1
   945          ].getTransactionReceipt(),
   946        }
   947      }
   948  
   949      // TODO: If the user doesn't provide enough gas then there's a chance that FailedRelayedMessage
   950      // will never be triggered. We should probably fix this at the contract level by requiring a
   951      // minimum amount of input gas and designing the contracts such that the gas will always be
   952      // enough to trigger the event. However, for now we need a temporary way to find L1 => L2
   953      // transactions that fail but don't alert us because they didn't provide enough gas.
   954      // TODO: Talk with the systems and protocol team about coordinating a hard fork that fixes this
   955      // on both L1 and L2.
   956  
   957      // Just return null if we didn't find a receipt. Slightly nicer than throwing an error.
   958      return null
   959    }
   960  
   961    /**
   962     * Waits for a message to be executed and returns the receipt of the transaction that executed
   963     * the given message.
   964     *
   965     * @param message Message to wait for.
   966     * @param opts Options to pass to the waiting function.
   967     * @param opts.confirmations Number of transaction confirmations to wait for before returning.
   968     * @param opts.pollIntervalMs Number of milliseconds to wait between polling for the receipt.
   969     * @param opts.timeoutMs Milliseconds to wait before timing out.
   970     * @param opts.fromBlockOrBlockHash The start block to use for the query filter on the RECEIVING chain
   971     * @param opts.toBlockOrBlockHash The end block to use for the query filter on the RECEIVING chain
   972     * @param messageIndex The index of the message, if multiple exist from multicall
   973     * @returns CrossChainMessage receipt including receipt of the transaction that relayed the
   974     * given message.
   975     */
   976    public async waitForMessageReceipt(
   977      message: MessageLike,
   978      opts: {
   979        fromBlockOrBlockHash?: BlockTag
   980        toBlockOrHash?: BlockTag
   981        confirmations?: number
   982        pollIntervalMs?: number
   983        timeoutMs?: number
   984      } = {},
   985  
   986      /**
   987       * The index of the withdrawal if multiple are made with multicall
   988       */
   989      messageIndex = 0
   990    ): Promise<MessageReceipt> {
   991      // Resolving once up-front is slightly more efficient.
   992      const resolved = await this.toCrossChainMessage(message, messageIndex)
   993  
   994      let totalTimeMs = 0
   995      while (totalTimeMs < (opts.timeoutMs || Infinity)) {
   996        const tick = Date.now()
   997        const receipt = await this.getMessageReceipt(
   998          resolved,
   999          messageIndex,
  1000          opts.fromBlockOrBlockHash,
  1001          opts.toBlockOrHash
  1002        )
  1003        if (receipt !== null) {
  1004          return receipt
  1005        } else {
  1006          await sleep(opts.pollIntervalMs || 4000)
  1007          totalTimeMs += Date.now() - tick
  1008        }
  1009      }
  1010  
  1011      throw new Error(`timed out waiting for message receipt`)
  1012    }
  1013  
  1014    /**
  1015     * Waits until the status of a given message changes to the expected status. Note that if the
  1016     * status of the given message changes to a status that implies the expected status, this will
  1017     * still return. If the status of the message changes to a status that exclues the expected
  1018     * status, this will throw an error.
  1019     *
  1020     * @param message Message to wait for.
  1021     * @param status Expected status of the message.
  1022     * @param opts Options to pass to the waiting function.
  1023     * @param opts.pollIntervalMs Number of milliseconds to wait when polling.
  1024     * @param opts.timeoutMs Milliseconds to wait before timing out.
  1025     * @param opts.fromBlockOrBlockHash The start block to use for the query filter on the RECEIVING chain
  1026     * @param opts.toBlockOrBlockHash The end block to use for the query filter on the RECEIVING chain
  1027     * @param messageIndex The index of the message, if multiple exist from multicall
  1028     */
  1029    public async waitForMessageStatus(
  1030      message: MessageLike,
  1031      status: MessageStatus,
  1032      opts: {
  1033        fromBlockOrBlockHash?: BlockTag
  1034        toBlockOrBlockHash?: BlockTag
  1035        pollIntervalMs?: number
  1036        timeoutMs?: number
  1037      } = {},
  1038      messageIndex = 0
  1039    ): Promise<void> {
  1040      // Resolving once up-front is slightly more efficient.
  1041      const resolved = await this.toCrossChainMessage(message, messageIndex)
  1042  
  1043      let totalTimeMs = 0
  1044      while (totalTimeMs < (opts.timeoutMs || Infinity)) {
  1045        const tick = Date.now()
  1046        const currentStatus = await this.getMessageStatus(
  1047          resolved,
  1048          messageIndex,
  1049          opts.fromBlockOrBlockHash,
  1050          opts.toBlockOrBlockHash
  1051        )
  1052  
  1053        // Handle special cases for L1 to L2 messages.
  1054        if (resolved.direction === MessageDirection.L1_TO_L2) {
  1055          // If we're at the expected status, we're done.
  1056          if (currentStatus === status) {
  1057            return
  1058          }
  1059  
  1060          if (
  1061            status === MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE &&
  1062            currentStatus > status
  1063          ) {
  1064            // Anything other than UNCONFIRMED_L1_TO_L2_MESSAGE implies that the message was at one
  1065            // point "unconfirmed", so we can stop waiting.
  1066            return
  1067          }
  1068  
  1069          if (
  1070            status === MessageStatus.FAILED_L1_TO_L2_MESSAGE &&
  1071            currentStatus === MessageStatus.RELAYED
  1072          ) {
  1073            throw new Error(
  1074              `incompatible message status, expected FAILED_L1_TO_L2_MESSAGE got RELAYED`
  1075            )
  1076          }
  1077  
  1078          if (
  1079            status === MessageStatus.RELAYED &&
  1080            currentStatus === MessageStatus.FAILED_L1_TO_L2_MESSAGE
  1081          ) {
  1082            throw new Error(
  1083              `incompatible message status, expected RELAYED got FAILED_L1_TO_L2_MESSAGE`
  1084            )
  1085          }
  1086        }
  1087  
  1088        // Handle special cases for L2 to L1 messages.
  1089        if (resolved.direction === MessageDirection.L2_TO_L1) {
  1090          if (currentStatus >= status) {
  1091            // For L2 to L1 messages, anything after the expected status implies the previous status,
  1092            // so we can safely return if the current status enum is larger than the expected one.
  1093            return
  1094          }
  1095        }
  1096  
  1097        await sleep(opts.pollIntervalMs || 4000)
  1098        totalTimeMs += Date.now() - tick
  1099      }
  1100  
  1101      throw new Error(`timed out waiting for message status change`)
  1102    }
  1103  
  1104    /**
  1105     * Estimates the amount of gas required to fully execute a given message on L2. Only applies to
  1106     * L1 => L2 messages. You would supply this gas limit when sending the message to L2.
  1107     *
  1108     * @param message Message get a gas estimate for.
  1109     * @param opts Options object.
  1110     * @param opts.bufferPercent Percentage of gas to add to the estimate. Defaults to 20.
  1111     * @param opts.from Address to use as the sender.
  1112     * @returns Estimates L2 gas limit.
  1113     */
  1114    public async estimateL2MessageGasLimit(
  1115      message: MessageRequestLike,
  1116      opts?: {
  1117        bufferPercent?: number
  1118        from?: string
  1119      },
  1120      messageIndex = 0
  1121    ): Promise<BigNumber> {
  1122      let resolved: CrossChainMessage | CrossChainMessageRequest
  1123      let from: string
  1124      if ((message as CrossChainMessage).messageNonce === undefined) {
  1125        resolved = message as CrossChainMessageRequest
  1126        from = opts?.from
  1127      } else {
  1128        resolved = await this.toCrossChainMessage(
  1129          message as MessageLike,
  1130          messageIndex
  1131        )
  1132        from = opts?.from || (resolved as CrossChainMessage).sender
  1133      }
  1134  
  1135      // L2 message gas estimation is only used for L1 => L2 messages.
  1136      if (resolved.direction === MessageDirection.L2_TO_L1) {
  1137        throw new Error(`cannot estimate gas limit for L2 => L1 message`)
  1138      }
  1139  
  1140      const estimate = await this.l2Provider.estimateGas({
  1141        from,
  1142        to: resolved.target,
  1143        data: resolved.message,
  1144      })
  1145  
  1146      // Return the estimate plus a buffer of 20% just in case.
  1147      const bufferPercent = opts?.bufferPercent || 20
  1148      return estimate.mul(100 + bufferPercent).div(100)
  1149    }
  1150  
  1151    /**
  1152     * Returns the estimated amount of time before the message can be executed. When this is a
  1153     * message being sent to L1, this will return the estimated time until the message will complete
  1154     * its challenge period. When this is a message being sent to L2, this will return the estimated
  1155     * amount of time until the message will be picked up and executed on L2.
  1156     *
  1157     * @param message Message to estimate the time remaining for.
  1158     * @param messageIndex The index of the message, if multiple exist from multicall
  1159     * @param opts.fromBlockOrBlockHash The start block to use for the query filter on the RECEIVING chain
  1160     * @param opts.toBlockOrBlockHash The end block to use for the query filter on the RECEIVING chain
  1161     * @returns Estimated amount of time remaining (in seconds) before the message can be executed.
  1162     */
  1163    public async estimateMessageWaitTimeSeconds(
  1164      message: MessageLike,
  1165      // consider making this an options object next breaking release
  1166      messageIndex = 0,
  1167      fromBlockOrBlockHash?: BlockTag,
  1168      toBlockOrBlockHash?: BlockTag
  1169    ): Promise<number> {
  1170      const resolved = await this.toCrossChainMessage(message, messageIndex)
  1171      const status = await this.getMessageStatus(
  1172        resolved,
  1173        messageIndex,
  1174        fromBlockOrBlockHash,
  1175        toBlockOrBlockHash
  1176      )
  1177      if (resolved.direction === MessageDirection.L1_TO_L2) {
  1178        if (
  1179          status === MessageStatus.RELAYED ||
  1180          status === MessageStatus.FAILED_L1_TO_L2_MESSAGE
  1181        ) {
  1182          // Transactions that are relayed or failed are considered completed, so the wait time is 0.
  1183          return 0
  1184        } else {
  1185          // Otherwise we need to estimate the number of blocks left until the transaction will be
  1186          // considered confirmed by the Layer 2 system. Then we multiply this by the estimated
  1187          // average L1 block time.
  1188          const receipt = await this.l1Provider.getTransactionReceipt(
  1189            resolved.transactionHash
  1190          )
  1191          const blocksLeft = Math.max(
  1192            this.depositConfirmationBlocks - receipt.confirmations,
  1193            0
  1194          )
  1195          return blocksLeft * this.l1BlockTimeSeconds
  1196        }
  1197      } else {
  1198        if (
  1199          status === MessageStatus.RELAYED ||
  1200          status === MessageStatus.READY_FOR_RELAY
  1201        ) {
  1202          // Transactions that are relayed or ready for relay are considered complete.
  1203          return 0
  1204        } else if (status === MessageStatus.STATE_ROOT_NOT_PUBLISHED) {
  1205          // If the state root hasn't been published yet, just assume it'll be published relatively
  1206          // quickly and return the challenge period for now. In the future we could use more
  1207          // advanced techniques to figure out average time between transaction execution and
  1208          // state root publication.
  1209          return this.getChallengePeriodSeconds()
  1210        } else if (status === MessageStatus.IN_CHALLENGE_PERIOD) {
  1211          // If the message is still within the challenge period, then we need to estimate exactly
  1212          // the amount of time left until the challenge period expires. The challenge period starts
  1213          // when the state root is published.
  1214          const stateRoot = await this.getMessageStateRoot(resolved, messageIndex)
  1215          const challengePeriod = await this.getChallengePeriodSeconds()
  1216          const targetBlock = await this.l1Provider.getBlock(
  1217            stateRoot.batch.blockNumber
  1218          )
  1219          const latestBlock = await this.l1Provider.getBlock('latest')
  1220          return Math.max(
  1221            challengePeriod - (latestBlock.timestamp - targetBlock.timestamp),
  1222            0
  1223          )
  1224        } else {
  1225          // Should not happen
  1226          throw new Error(`unexpected message status`)
  1227        }
  1228      }
  1229    }
  1230  
  1231    /**
  1232     * Queries the current challenge period in seconds from the StateCommitmentChain.
  1233     *
  1234     * @returns Current challenge period in seconds.
  1235     */
  1236    public async getChallengePeriodSeconds(): Promise<number> {
  1237      if (!this.bedrock) {
  1238        return (
  1239          await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW()
  1240        ).toNumber()
  1241      }
  1242  
  1243      const oracleVersion = await this.contracts.l1.L2OutputOracle.version()
  1244      const challengePeriod =
  1245        oracleVersion === '1.0.0'
  1246          ? // The ABI in the SDK does not contain FINALIZATION_PERIOD_SECONDS
  1247            // in OptimismPortal, so making an explicit call instead.
  1248            BigNumber.from(
  1249              await this.contracts.l1.OptimismPortal.provider.call({
  1250                to: this.contracts.l1.OptimismPortal.address,
  1251                data: '0xf4daa291', // FINALIZATION_PERIOD_SECONDS
  1252              })
  1253            )
  1254          : await this.contracts.l1.L2OutputOracle.FINALIZATION_PERIOD_SECONDS()
  1255      return challengePeriod.toNumber()
  1256    }
  1257  
  1258    /**
  1259     * Queries the OptimismPortal contract's `provenWithdrawals` mapping
  1260     * for a ProvenWithdrawal that matches the passed withdrawalHash
  1261     *
  1262     * @bedrock
  1263     * Note: This function is bedrock-specific.
  1264     *
  1265     * @returns A ProvenWithdrawal object
  1266     */
  1267    public async getProvenWithdrawal(
  1268      withdrawalHash: string
  1269    ): Promise<ProvenWithdrawal> {
  1270      if (!this.bedrock) {
  1271        throw new Error('message proving only applies after the bedrock upgrade')
  1272      }
  1273  
  1274      return this.contracts.l1.OptimismPortal.provenWithdrawals(withdrawalHash)
  1275    }
  1276  
  1277    /**
  1278     * Returns the Bedrock output root that corresponds to the given message.
  1279     *
  1280     * @param message Message to get the Bedrock output root for.
  1281     * @param messageIndex The index of the message, if multiple exist from multicall
  1282     * @returns Bedrock output root.
  1283     */
  1284    public async getMessageBedrockOutput(
  1285      message: MessageLike,
  1286      messageIndex = 0
  1287    ): Promise<BedrockOutputData | null> {
  1288      const resolved = await this.toCrossChainMessage(message, messageIndex)
  1289  
  1290      // Outputs are only a thing for L2 to L1 messages.
  1291      if (resolved.direction === MessageDirection.L1_TO_L2) {
  1292        throw new Error(`cannot get a state root for an L1 to L2 message`)
  1293      }
  1294  
  1295      let proposal: any
  1296      let l2OutputIndex: BigNumber
  1297      if (await this.fpac()) {
  1298        // Get the respected game type from the portal.
  1299        const gameType =
  1300          await this.contracts.l1.OptimismPortal2.respectedGameType()
  1301  
  1302        // Get the total game count from the DisputeGameFactory since that will give us the end of
  1303        // the array that we're searching over. We'll then use that to find the latest games.
  1304        const gameCount = await this.contracts.l1.DisputeGameFactory.gameCount()
  1305  
  1306        // Find the latest 100 games (or as many as we can up to 100).
  1307        const latestGames =
  1308          await this.contracts.l1.DisputeGameFactory.findLatestGames(
  1309            gameType,
  1310            Math.max(0, gameCount.sub(1).toNumber()),
  1311            Math.min(100, gameCount.toNumber())
  1312          )
  1313  
  1314        // Find all games that are for proposals about blocks newer than the message block.
  1315        const matches: any[] = []
  1316        for (const game of latestGames) {
  1317          try {
  1318            const [blockNumber] = ethers.utils.defaultAbiCoder.decode(
  1319              ['uint256'],
  1320              game.extraData
  1321            )
  1322            if (blockNumber.gte(resolved.blockNumber)) {
  1323              matches.push({
  1324                ...game,
  1325                l2BlockNumber: blockNumber,
  1326              })
  1327            }
  1328          } catch (err) {
  1329            // If we can't decode the extra data then we just skip this game.
  1330            continue
  1331          }
  1332        }
  1333  
  1334        // Shuffle the list of matches. We shuffle here to avoid potential DoS vectors where the
  1335        // latest games are all invalid and the SDK would be forced to make a bunch of archive calls.
  1336        for (let i = matches.length - 1; i > 0; i--) {
  1337          const j = Math.floor(Math.random() * (i + 1))
  1338          ;[matches[i], matches[j]] = [matches[j], matches[i]]
  1339        }
  1340  
  1341        // Now we verify the proposals in the matches array.
  1342        let match: any
  1343        for (const option of matches) {
  1344          // Use the cache if we can.
  1345          const cached = this._outputCache.find((other) => {
  1346            return other.root === option.rootClaim
  1347          })
  1348  
  1349          // Skip if we can use the cached.
  1350          if (cached) {
  1351            if (cached.valid) {
  1352              match = option
  1353              break
  1354            } else {
  1355              continue
  1356            }
  1357          }
  1358  
  1359          // If the cache ever gets to 10k elements, clear out the first half. Works well enough
  1360          // since the cache will generally tend to be used in a FIFO manner.
  1361          if (this._outputCache.length > 10000) {
  1362            this._outputCache = this._outputCache.slice(5000)
  1363          }
  1364  
  1365          // We didn't hit the cache so we're going to have to do the work.
  1366          try {
  1367            // Make sure this is a JSON RPC provider.
  1368            const provider = toJsonRpcProvider(this.l2Provider)
  1369  
  1370            // Grab the block and storage proof at the same time.
  1371            const [block, proof] = await Promise.all([
  1372              provider.send('eth_getBlockByNumber', [
  1373                toRpcHexString(option.l2BlockNumber),
  1374                false,
  1375              ]),
  1376              makeStateTrieProof(
  1377                provider,
  1378                option.l2BlockNumber,
  1379                this.contracts.l2.OVM_L2ToL1MessagePasser.address,
  1380                ethers.constants.HashZero
  1381              ),
  1382            ])
  1383  
  1384            // Compute the output.
  1385            const output = ethers.utils.solidityKeccak256(
  1386              ['bytes32', 'bytes32', 'bytes32', 'bytes32'],
  1387              [
  1388                ethers.constants.HashZero,
  1389                block.stateRoot,
  1390                proof.storageRoot,
  1391                block.hash,
  1392              ]
  1393            )
  1394  
  1395            // If the output matches the proposal then we're good.
  1396            if (output === option.rootClaim) {
  1397              this._outputCache.push({ root: option.rootClaim, valid: true })
  1398              match = option
  1399              break
  1400            } else {
  1401              this._outputCache.push({ root: option.rootClaim, valid: false })
  1402            }
  1403          } catch (err) {
  1404            // Just skip this option, whatever. If it was a transient error then we'll try again in
  1405            // the next loop iteration. If it was a permanent error then we'll get the same thing.
  1406            continue
  1407          }
  1408        }
  1409  
  1410        // If there's no match then we can't prove the message to the portal.
  1411        if (!match) {
  1412          return null
  1413        }
  1414  
  1415        // Put the result into the same format as the old logic for now to reduce added code.
  1416        l2OutputIndex = match.index
  1417        proposal = {
  1418          outputRoot: match.rootClaim,
  1419          timestamp: match.timestamp,
  1420          l2BlockNumber: match.l2BlockNumber,
  1421        }
  1422      } else {
  1423        // Try to find the output index that corresponds to the block number attached to the message.
  1424        // We'll explicitly handle "cannot get output" errors as a null return value, but anything else
  1425        // needs to get thrown. Might need to revisit this in the future to be a little more robust
  1426        // when connected to RPCs that don't return nice error messages.
  1427        try {
  1428          l2OutputIndex =
  1429            await this.contracts.l1.L2OutputOracle.getL2OutputIndexAfter(
  1430              resolved.blockNumber
  1431            )
  1432        } catch (err) {
  1433          if (err.message.includes('L2OutputOracle: cannot get output')) {
  1434            return null
  1435          } else {
  1436            throw err
  1437          }
  1438        }
  1439  
  1440        // Now pull the proposal out given the output index. Should always work as long as the above
  1441        // codepath completed successfully.
  1442        proposal = await this.contracts.l1.L2OutputOracle.getL2Output(
  1443          l2OutputIndex
  1444        )
  1445      }
  1446  
  1447      // Format everything and return it nicely.
  1448      return {
  1449        outputRoot: proposal.outputRoot,
  1450        l1Timestamp: proposal.timestamp.toNumber(),
  1451        l2BlockNumber: proposal.l2BlockNumber.toNumber(),
  1452        l2OutputIndex: l2OutputIndex.toNumber(),
  1453      }
  1454    }
  1455  
  1456    /**
  1457     * Returns the state root that corresponds to a given message. This is the state root for the
  1458     * block in which the transaction was included, as published to the StateCommitmentChain. If the
  1459     * state root for the given message has not been published yet, this function returns null.
  1460     *
  1461     * @param message Message to find a state root for.
  1462     * @param messageIndex The index of the message, if multiple exist from multicall
  1463     * @returns State root for the block in which the message was created.
  1464     */
  1465    public async getMessageStateRoot(
  1466      message: MessageLike,
  1467      messageIndex = 0
  1468    ): Promise<StateRoot | null> {
  1469      const resolved = await this.toCrossChainMessage(message, messageIndex)
  1470  
  1471      // State roots are only a thing for L2 to L1 messages.
  1472      if (resolved.direction === MessageDirection.L1_TO_L2) {
  1473        throw new Error(`cannot get a state root for an L1 to L2 message`)
  1474      }
  1475  
  1476      // We need the block number of the transaction that triggered the message so we can look up the
  1477      // state root batch that corresponds to that block number.
  1478      const messageTxReceipt = await this.l2Provider.getTransactionReceipt(
  1479        resolved.transactionHash
  1480      )
  1481  
  1482      // Every block has exactly one transaction in it. Since there's a genesis block, the
  1483      // transaction index will always be one less than the block number.
  1484      const messageTxIndex = messageTxReceipt.blockNumber - 1
  1485  
  1486      // Pull down the state root batch, we'll try to pick out the specific state root that
  1487      // corresponds to our message.
  1488      const stateRootBatch = await this.getStateRootBatchByTransactionIndex(
  1489        messageTxIndex
  1490      )
  1491  
  1492      // No state root batch, no state root.
  1493      if (stateRootBatch === null) {
  1494        return null
  1495      }
  1496  
  1497      // We have a state root batch, now we need to find the specific state root for our transaction.
  1498      // First we need to figure out the index of the state root within the batch we found. This is
  1499      // going to be the original transaction index offset by the total number of previous state
  1500      // roots.
  1501      const indexInBatch =
  1502        messageTxIndex - stateRootBatch.header.prevTotalElements.toNumber()
  1503  
  1504      // Just a sanity check.
  1505      if (stateRootBatch.stateRoots.length <= indexInBatch) {
  1506        // Should never happen!
  1507        throw new Error(`state root does not exist in batch`)
  1508      }
  1509  
  1510      return {
  1511        stateRoot: stateRootBatch.stateRoots[indexInBatch],
  1512        stateRootIndexInBatch: indexInBatch,
  1513        batch: stateRootBatch,
  1514      }
  1515    }
  1516  
  1517    /**
  1518     * Returns the StateBatchAppended event that was emitted when the batch with a given index was
  1519     * created. Returns null if no such event exists (the batch has not been submitted).
  1520     *
  1521     * @param batchIndex Index of the batch to find an event for.
  1522     * @returns StateBatchAppended event for the batch, or null if no such batch exists.
  1523     */
  1524    public async getStateBatchAppendedEventByBatchIndex(
  1525      batchIndex: number
  1526    ): Promise<ethers.Event | null> {
  1527      const events = await this.contracts.l1.StateCommitmentChain.queryFilter(
  1528        this.contracts.l1.StateCommitmentChain.filters.StateBatchAppended(
  1529          batchIndex
  1530        )
  1531      )
  1532  
  1533      if (events.length === 0) {
  1534        return null
  1535      } else if (events.length > 1) {
  1536        // Should never happen!
  1537        throw new Error(`found more than one StateBatchAppended event`)
  1538      } else {
  1539        return events[0]
  1540      }
  1541    }
  1542  
  1543    /**
  1544     * Returns the StateBatchAppended event for the batch that includes the transaction with the
  1545     * given index. Returns null if no such event exists.
  1546     *
  1547     * @param transactionIndex Index of the L2 transaction to find an event for.
  1548     * @returns StateBatchAppended event for the batch that includes the given transaction by index.
  1549     */
  1550    public async getStateBatchAppendedEventByTransactionIndex(
  1551      transactionIndex: number
  1552    ): Promise<ethers.Event | null> {
  1553      const isEventHi = (event: ethers.Event, index: number) => {
  1554        const prevTotalElements = event.args._prevTotalElements.toNumber()
  1555        return index < prevTotalElements
  1556      }
  1557  
  1558      const isEventLo = (event: ethers.Event, index: number) => {
  1559        const prevTotalElements = event.args._prevTotalElements.toNumber()
  1560        const batchSize = event.args._batchSize.toNumber()
  1561        return index >= prevTotalElements + batchSize
  1562      }
  1563  
  1564      const totalBatches: ethers.BigNumber =
  1565        await this.contracts.l1.StateCommitmentChain.getTotalBatches()
  1566      if (totalBatches.eq(0)) {
  1567        return null
  1568      }
  1569  
  1570      let lowerBound = 0
  1571      let upperBound = totalBatches.toNumber() - 1
  1572      let batchEvent: ethers.Event | null =
  1573        await this.getStateBatchAppendedEventByBatchIndex(upperBound)
  1574  
  1575      // Only happens when no batches have been submitted yet.
  1576      if (batchEvent === null) {
  1577        return null
  1578      }
  1579  
  1580      if (isEventLo(batchEvent, transactionIndex)) {
  1581        // Upper bound is too low, means this transaction doesn't have a corresponding state batch yet.
  1582        return null
  1583      } else if (!isEventHi(batchEvent, transactionIndex)) {
  1584        // Upper bound is not too low and also not too high. This means the upper bound event is the
  1585        // one we're looking for! Return it.
  1586        return batchEvent
  1587      }
  1588  
  1589      // Binary search to find the right event. The above checks will guarantee that the event does
  1590      // exist and that we'll find it during this search.
  1591      while (lowerBound < upperBound) {
  1592        const middleOfBounds = Math.floor((lowerBound + upperBound) / 2)
  1593        batchEvent = await this.getStateBatchAppendedEventByBatchIndex(
  1594          middleOfBounds
  1595        )
  1596  
  1597        if (isEventHi(batchEvent, transactionIndex)) {
  1598          upperBound = middleOfBounds
  1599        } else if (isEventLo(batchEvent, transactionIndex)) {
  1600          lowerBound = middleOfBounds
  1601        } else {
  1602          break
  1603        }
  1604      }
  1605  
  1606      return batchEvent
  1607    }
  1608  
  1609    /**
  1610     * Returns information about the state root batch that included the state root for the given
  1611     * transaction by index. Returns null if no such state root has been published yet.
  1612     *
  1613     * @param transactionIndex Index of the L2 transaction to find a state root batch for.
  1614     * @returns State root batch for the given transaction index, or null if none exists yet.
  1615     */
  1616    public async getStateRootBatchByTransactionIndex(
  1617      transactionIndex: number
  1618    ): Promise<StateRootBatch | null> {
  1619      const stateBatchAppendedEvent =
  1620        await this.getStateBatchAppendedEventByTransactionIndex(transactionIndex)
  1621      if (stateBatchAppendedEvent === null) {
  1622        return null
  1623      }
  1624  
  1625      const stateBatchTransaction = await stateBatchAppendedEvent.getTransaction()
  1626      const [stateRoots] =
  1627        this.contracts.l1.StateCommitmentChain.interface.decodeFunctionData(
  1628          'appendStateBatch',
  1629          stateBatchTransaction.data
  1630        )
  1631  
  1632      return {
  1633        blockNumber: stateBatchAppendedEvent.blockNumber,
  1634        stateRoots,
  1635        header: {
  1636          batchIndex: stateBatchAppendedEvent.args._batchIndex,
  1637          batchRoot: stateBatchAppendedEvent.args._batchRoot,
  1638          batchSize: stateBatchAppendedEvent.args._batchSize,
  1639          prevTotalElements: stateBatchAppendedEvent.args._prevTotalElements,
  1640          extraData: stateBatchAppendedEvent.args._extraData,
  1641        },
  1642      }
  1643    }
  1644  
  1645    /**
  1646     * Generates the proof required to finalize an L2 to L1 message.
  1647     *
  1648     * @param message Message to generate a proof for.
  1649     * @param messageIndex The index of the message, if multiple exist from multicall
  1650     * @returns Proof that can be used to finalize the message.
  1651     */
  1652    public async getMessageProof(
  1653      message: MessageLike,
  1654      messageIndex = 0
  1655    ): Promise<CrossChainMessageProof> {
  1656      const resolved = await this.toCrossChainMessage(message, messageIndex)
  1657      if (resolved.direction === MessageDirection.L1_TO_L2) {
  1658        throw new Error(`can only generate proofs for L2 to L1 messages`)
  1659      }
  1660  
  1661      const stateRoot = await this.getMessageStateRoot(resolved, messageIndex)
  1662      if (stateRoot === null) {
  1663        throw new Error(`state root for message not yet published`)
  1664      }
  1665  
  1666      // We need to calculate the specific storage slot that demonstrates that this message was
  1667      // actually included in the L2 chain. The following calculation is based on the fact that
  1668      // messages are stored in the following mapping on L2:
  1669      // https://github.com/ethereum-optimism/optimism/blob/c84d3450225306abbb39b4e7d6d82424341df2be/packages/contracts/contracts/L2/predeploys/OVM_L2ToL1MessagePasser.sol#L23
  1670      // You can read more about how Solidity storage slots are computed for mappings here:
  1671      // https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
  1672      const messageSlot = ethers.utils.keccak256(
  1673        ethers.utils.keccak256(
  1674          encodeCrossDomainMessageV0(
  1675            resolved.target,
  1676            resolved.sender,
  1677            resolved.message,
  1678            resolved.messageNonce
  1679          ) + remove0x(this.contracts.l2.L2CrossDomainMessenger.address)
  1680        ) + '00'.repeat(32)
  1681      )
  1682  
  1683      const stateTrieProof = await makeStateTrieProof(
  1684        toJsonRpcProvider(this.l2Provider),
  1685        resolved.blockNumber,
  1686        this.contracts.l2.OVM_L2ToL1MessagePasser.address,
  1687        messageSlot
  1688      )
  1689  
  1690      return {
  1691        stateRoot: stateRoot.stateRoot,
  1692        stateRootBatchHeader: stateRoot.batch.header,
  1693        stateRootProof: {
  1694          index: stateRoot.stateRootIndexInBatch,
  1695          siblings: makeMerkleTreeProof(
  1696            stateRoot.batch.stateRoots,
  1697            stateRoot.stateRootIndexInBatch
  1698          ),
  1699        },
  1700        stateTrieWitness: toHexString(rlp.encode(stateTrieProof.accountProof)),
  1701        storageTrieWitness: toHexString(rlp.encode(stateTrieProof.storageProof)),
  1702      }
  1703    }
  1704  
  1705    /**
  1706     * Generates the bedrock proof required to finalize an L2 to L1 message.
  1707     *
  1708     * @param message Message to generate a proof for.
  1709     * @param messageIndex The index of the message, if multiple exist from multicall
  1710     * @returns Proof that can be used to finalize the message.
  1711     */
  1712    public async getBedrockMessageProof(
  1713      message: MessageLike,
  1714      messageIndex = 0
  1715    ): Promise<BedrockCrossChainMessageProof> {
  1716      const resolved = await this.toCrossChainMessage(message, messageIndex)
  1717      if (resolved.direction === MessageDirection.L1_TO_L2) {
  1718        throw new Error(`can only generate proofs for L2 to L1 messages`)
  1719      }
  1720  
  1721      const output = await this.getMessageBedrockOutput(resolved, messageIndex)
  1722      if (output === null) {
  1723        throw new Error(`state root for message not yet published`)
  1724      }
  1725  
  1726      const withdrawal = await this.toLowLevelMessage(resolved, messageIndex)
  1727      const hash = hashLowLevelMessage(withdrawal)
  1728      const messageSlot = hashMessageHash(hash)
  1729  
  1730      const provider = toJsonRpcProvider(this.l2Provider)
  1731  
  1732      const stateTrieProof = await makeStateTrieProof(
  1733        provider,
  1734        output.l2BlockNumber,
  1735        this.contracts.l2.BedrockMessagePasser.address,
  1736        messageSlot
  1737      )
  1738  
  1739      const block = await provider.send('eth_getBlockByNumber', [
  1740        toRpcHexString(output.l2BlockNumber),
  1741        false,
  1742      ])
  1743  
  1744      return {
  1745        outputRootProof: {
  1746          version: ethers.constants.HashZero,
  1747          stateRoot: block.stateRoot,
  1748          messagePasserStorageRoot: stateTrieProof.storageRoot,
  1749          latestBlockhash: block.hash,
  1750        },
  1751        withdrawalProof: stateTrieProof.storageProof,
  1752        l2OutputIndex: output.l2OutputIndex,
  1753      }
  1754    }
  1755  
  1756    /**
  1757     * Sends a given cross chain message. Where the message is sent depends on the direction attached
  1758     * to the message itself.
  1759     *
  1760     * @param message Cross chain message to send.
  1761     * @param opts Additional options.
  1762     * @param opts.signer Optional signer to use to send the transaction.
  1763     * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
  1764     * @param opts.overrides Optional transaction overrides.
  1765     * @returns Transaction response for the message sending transaction.
  1766     */
  1767    public async sendMessage(
  1768      message: CrossChainMessageRequest,
  1769      opts?: {
  1770        signer?: Signer
  1771        l2GasLimit?: NumberLike
  1772        overrides?: Overrides
  1773      }
  1774    ): Promise<TransactionResponse> {
  1775      const tx = await this.populateTransaction.sendMessage(message, opts)
  1776      if (message.direction === MessageDirection.L1_TO_L2) {
  1777        return (opts?.signer || this.l1Signer).sendTransaction(tx)
  1778      } else {
  1779        return (opts?.signer || this.l2Signer).sendTransaction(tx)
  1780      }
  1781    }
  1782  
  1783    /**
  1784     * Resends a given cross chain message with a different gas limit. Only applies to L1 to L2
  1785     * messages. If provided an L2 to L1 message, this function will throw an error.
  1786     *
  1787     * @param message Cross chain message to resend.
  1788     * @param messageGasLimit New gas limit to use for the message.
  1789     * @param opts Additional options.
  1790     * @param opts.signer Optional signer to use to send the transaction.
  1791     * @param opts.overrides Optional transaction overrides.
  1792     * @returns Transaction response for the message resending transaction.
  1793     */
  1794    public async resendMessage(
  1795      message: MessageLike,
  1796      messageGasLimit: NumberLike,
  1797      opts?: {
  1798        signer?: Signer
  1799        overrides?: Overrides
  1800      }
  1801    ): Promise<TransactionResponse> {
  1802      return (opts?.signer || this.l1Signer).sendTransaction(
  1803        await this.populateTransaction.resendMessage(
  1804          message,
  1805          messageGasLimit,
  1806          opts
  1807        )
  1808      )
  1809    }
  1810  
  1811    /**
  1812     * Proves a cross chain message that was sent from L2 to L1. Only applicable for L2 to L1
  1813     * messages.
  1814     *
  1815     * @param message Message to finalize.
  1816     * @param opts Additional options.
  1817     * @param opts.signer Optional signer to use to send the transaction.
  1818     * @param opts.overrides Optional transaction overrides.
  1819     * @returns Transaction response for the finalization transaction.
  1820     */
  1821    public async proveMessage(
  1822      message: MessageLike,
  1823      opts?: {
  1824        signer?: Signer
  1825        overrides?: Overrides
  1826      },
  1827      /**
  1828       * The index of the withdrawal if multiple are made with multicall
  1829       */
  1830      messageIndex: number = 0
  1831    ): Promise<TransactionResponse> {
  1832      const tx = await this.populateTransaction.proveMessage(
  1833        message,
  1834        opts,
  1835        messageIndex
  1836      )
  1837      return (opts?.signer || this.l1Signer).sendTransaction(tx)
  1838    }
  1839  
  1840    /**
  1841     * Finalizes a cross chain message that was sent from L2 to L1. Only applicable for L2 to L1
  1842     * messages. Will throw an error if the message has not completed its challenge period yet.
  1843     *
  1844     * @param message Message to finalize.
  1845     * @param opts Additional options.
  1846     * @param opts.signer Optional signer to use to send the transaction.
  1847     * @param opts.overrides Optional transaction overrides.
  1848     * @param messageIndex The index of the message, if multiple exist from multicall
  1849     * @returns Transaction response for the finalization transaction.
  1850     */
  1851    public async finalizeMessage(
  1852      message: MessageLike,
  1853      opts?: {
  1854        signer?: Signer
  1855        overrides?: PayableOverrides
  1856      },
  1857      messageIndex = 0
  1858    ): Promise<TransactionResponse> {
  1859      return (opts?.signer || this.l1Signer).sendTransaction(
  1860        await this.populateTransaction.finalizeMessage(
  1861          message,
  1862          opts,
  1863          messageIndex
  1864        )
  1865      )
  1866    }
  1867  
  1868    /**
  1869     * Deposits some ETH into the L2 chain.
  1870     *
  1871     * @param amount Amount of ETH to deposit (in wei).
  1872     * @param opts Additional options.
  1873     * @param opts.signer Optional signer to use to send the transaction.
  1874     * @param opts.recipient Optional address to receive the funds on L2. Defaults to sender.
  1875     * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
  1876     * @param opts.overrides Optional transaction overrides.
  1877     * @returns Transaction response for the deposit transaction.
  1878     */
  1879    public async depositETH(
  1880      amount: NumberLike,
  1881      opts?: {
  1882        recipient?: AddressLike
  1883        signer?: Signer
  1884        l2GasLimit?: NumberLike
  1885        overrides?: Overrides
  1886      }
  1887    ): Promise<TransactionResponse> {
  1888      return (opts?.signer || this.l1Signer).sendTransaction(
  1889        await this.populateTransaction.depositETH(amount, opts)
  1890      )
  1891    }
  1892  
  1893    /**
  1894     * Withdraws some ETH back to the L1 chain.
  1895     *
  1896     * @param amount Amount of ETH to withdraw.
  1897     * @param opts Additional options.
  1898     * @param opts.signer Optional signer to use to send the transaction.
  1899     * @param opts.recipient Optional address to receive the funds on L1. Defaults to sender.
  1900     * @param opts.overrides Optional transaction overrides.
  1901     * @returns Transaction response for the withdraw transaction.
  1902     */
  1903    public async withdrawETH(
  1904      amount: NumberLike,
  1905      opts?: {
  1906        recipient?: AddressLike
  1907        signer?: Signer
  1908        overrides?: Overrides
  1909      }
  1910    ): Promise<TransactionResponse> {
  1911      return (opts?.signer || this.l2Signer).sendTransaction(
  1912        await this.populateTransaction.withdrawETH(amount, opts)
  1913      )
  1914    }
  1915  
  1916    /**
  1917     * Queries the account's approval amount for a given L1 token.
  1918     *
  1919     * @param l1Token The L1 token address.
  1920     * @param l2Token The L2 token address.
  1921     * @param opts Additional options.
  1922     * @param opts.signer Optional signer to get the approval for.
  1923     * @returns Amount of tokens approved for deposits from the account.
  1924     */
  1925    public async approval(
  1926      l1Token: AddressLike,
  1927      l2Token: AddressLike,
  1928      opts?: {
  1929        signer?: Signer
  1930      }
  1931    ): Promise<BigNumber> {
  1932      const bridge = await this.getBridgeForTokenPair(l1Token, l2Token)
  1933      return bridge.approval(l1Token, l2Token, opts?.signer || this.l1Signer)
  1934    }
  1935  
  1936    /**
  1937     * Approves a deposit into the L2 chain.
  1938     *
  1939     * @param l1Token The L1 token address.
  1940     * @param l2Token The L2 token address.
  1941     * @param amount Amount of the token to approve.
  1942     * @param opts Additional options.
  1943     * @param opts.signer Optional signer to use to send the transaction.
  1944     * @param opts.overrides Optional transaction overrides.
  1945     * @returns Transaction response for the approval transaction.
  1946     */
  1947    public async approveERC20(
  1948      l1Token: AddressLike,
  1949      l2Token: AddressLike,
  1950      amount: NumberLike,
  1951      opts?: {
  1952        signer?: Signer
  1953        overrides?: Overrides
  1954      }
  1955    ): Promise<TransactionResponse> {
  1956      return (opts?.signer || this.l1Signer).sendTransaction(
  1957        await this.populateTransaction.approveERC20(
  1958          l1Token,
  1959          l2Token,
  1960          amount,
  1961          opts
  1962        )
  1963      )
  1964    }
  1965  
  1966    /**
  1967     * Deposits some ERC20 tokens into the L2 chain.
  1968     *
  1969     * @param l1Token Address of the L1 token.
  1970     * @param l2Token Address of the L2 token.
  1971     * @param amount Amount to deposit.
  1972     * @param opts Additional options.
  1973     * @param opts.signer Optional signer to use to send the transaction.
  1974     * @param opts.recipient Optional address to receive the funds on L2. Defaults to sender.
  1975     * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
  1976     * @param opts.overrides Optional transaction overrides.
  1977     * @returns Transaction response for the deposit transaction.
  1978     */
  1979    public async depositERC20(
  1980      l1Token: AddressLike,
  1981      l2Token: AddressLike,
  1982      amount: NumberLike,
  1983      opts?: {
  1984        recipient?: AddressLike
  1985        signer?: Signer
  1986        l2GasLimit?: NumberLike
  1987        overrides?: CallOverrides
  1988      }
  1989    ): Promise<TransactionResponse> {
  1990      return (opts?.signer || this.l1Signer).sendTransaction(
  1991        await this.populateTransaction.depositERC20(
  1992          l1Token,
  1993          l2Token,
  1994          amount,
  1995          opts
  1996        )
  1997      )
  1998    }
  1999  
  2000    /**
  2001     * Withdraws some ERC20 tokens back to the L1 chain.
  2002     *
  2003     * @param l1Token Address of the L1 token.
  2004     * @param l2Token Address of the L2 token.
  2005     * @param amount Amount to withdraw.
  2006     * @param opts Additional options.
  2007     * @param opts.signer Optional signer to use to send the transaction.
  2008     * @param opts.recipient Optional address to receive the funds on L1. Defaults to sender.
  2009     * @param opts.overrides Optional transaction overrides.
  2010     * @returns Transaction response for the withdraw transaction.
  2011     */
  2012    public async withdrawERC20(
  2013      l1Token: AddressLike,
  2014      l2Token: AddressLike,
  2015      amount: NumberLike,
  2016      opts?: {
  2017        recipient?: AddressLike
  2018        signer?: Signer
  2019        overrides?: Overrides
  2020      }
  2021    ): Promise<TransactionResponse> {
  2022      return (opts?.signer || this.l2Signer).sendTransaction(
  2023        await this.populateTransaction.withdrawERC20(
  2024          l1Token,
  2025          l2Token,
  2026          amount,
  2027          opts
  2028        )
  2029      )
  2030    }
  2031  
  2032    /**
  2033     * Object that holds the functions that generate transactions to be signed by the user.
  2034     * Follows the pattern used by ethers.js.
  2035     */
  2036    populateTransaction = {
  2037      /**
  2038       * Generates a transaction that sends a given cross chain message. This transaction can be signed
  2039       * and executed by a signer.
  2040       *
  2041       * @param message Cross chain message to send.
  2042       * @param opts Additional options.
  2043       * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
  2044       * @param opts.overrides Optional transaction overrides.
  2045       * @returns Transaction that can be signed and executed to send the message.
  2046       */
  2047      sendMessage: async (
  2048        message: CrossChainMessageRequest,
  2049        opts?: {
  2050          l2GasLimit?: NumberLike
  2051          overrides?: Overrides
  2052        }
  2053      ): Promise<TransactionRequest> => {
  2054        if (message.direction === MessageDirection.L1_TO_L2) {
  2055          return this.contracts.l1.L1CrossDomainMessenger.populateTransaction.sendMessage(
  2056            message.target,
  2057            message.message,
  2058            opts?.l2GasLimit || (await this.estimateL2MessageGasLimit(message)),
  2059            opts?.overrides || {}
  2060          )
  2061        } else {
  2062          return this.contracts.l2.L2CrossDomainMessenger.populateTransaction.sendMessage(
  2063            message.target,
  2064            message.message,
  2065            0, // Gas limit goes unused when sending from L2 to L1
  2066            opts?.overrides || {}
  2067          )
  2068        }
  2069      },
  2070  
  2071      /**
  2072       * Generates a transaction that resends a given cross chain message. Only applies to L1 to L2
  2073       * messages. This transaction can be signed and executed by a signer.
  2074       *
  2075       * @param message Cross chain message to resend.
  2076       * @param messageGasLimit New gas limit to use for the message.
  2077       * @param opts Additional options.
  2078       * @param opts.overrides Optional transaction overrides.
  2079       * @returns Transaction that can be signed and executed to resend the message.
  2080       */
  2081      resendMessage: async (
  2082        message: MessageLike,
  2083        messageGasLimit: NumberLike,
  2084        opts?: {
  2085          overrides?: Overrides
  2086        },
  2087        /**
  2088         * The index of the withdrawal if multiple are made with multicall
  2089         */
  2090        messageIndex = 0
  2091      ): Promise<TransactionRequest> => {
  2092        const resolved = await this.toCrossChainMessage(message, messageIndex)
  2093        if (resolved.direction === MessageDirection.L2_TO_L1) {
  2094          throw new Error(`cannot resend L2 to L1 message`)
  2095        }
  2096  
  2097        if (this.bedrock) {
  2098          return this.populateTransaction.finalizeMessage(
  2099            resolved,
  2100            {
  2101              ...(opts || {}),
  2102              overrides: {
  2103                ...opts?.overrides,
  2104                gasLimit: messageGasLimit,
  2105              },
  2106            },
  2107            messageIndex
  2108          )
  2109        } else {
  2110          const legacyL1XDM = new ethers.Contract(
  2111            this.contracts.l1.L1CrossDomainMessenger.address,
  2112            getContractInterface('L1CrossDomainMessenger'),
  2113            this.l1SignerOrProvider
  2114          )
  2115          return legacyL1XDM.populateTransaction.replayMessage(
  2116            resolved.target,
  2117            resolved.sender,
  2118            resolved.message,
  2119            resolved.messageNonce,
  2120            resolved.minGasLimit,
  2121            messageGasLimit,
  2122            opts?.overrides || {}
  2123          )
  2124        }
  2125      },
  2126  
  2127      /**
  2128       * Generates a message proving transaction that can be signed and executed. Only
  2129       * applicable for L2 to L1 messages.
  2130       *
  2131       * @param message Message to generate the proving transaction for.
  2132       * @param opts Additional options.
  2133       * @param opts.overrides Optional transaction overrides.
  2134       * @param messageIndex The index of the message, if multiple exist from multicall
  2135       * @returns Transaction that can be signed and executed to prove the message.
  2136       */
  2137      proveMessage: async (
  2138        message: MessageLike,
  2139        opts?: {
  2140          overrides?: PayableOverrides
  2141        },
  2142        messageIndex = 0
  2143      ): Promise<TransactionRequest> => {
  2144        const resolved = await this.toCrossChainMessage(message, messageIndex)
  2145        if (resolved.direction === MessageDirection.L1_TO_L2) {
  2146          throw new Error('cannot finalize L1 to L2 message')
  2147        }
  2148  
  2149        if (!this.bedrock) {
  2150          throw new Error(
  2151            'message proving only applies after the bedrock upgrade'
  2152          )
  2153        }
  2154  
  2155        const withdrawal = await this.toLowLevelMessage(resolved, messageIndex)
  2156        const proof = await this.getBedrockMessageProof(resolved, messageIndex)
  2157  
  2158        const args = [
  2159          [
  2160            withdrawal.messageNonce,
  2161            withdrawal.sender,
  2162            withdrawal.target,
  2163            withdrawal.value,
  2164            withdrawal.minGasLimit,
  2165            withdrawal.message,
  2166          ],
  2167          proof.l2OutputIndex,
  2168          [
  2169            proof.outputRootProof.version,
  2170            proof.outputRootProof.stateRoot,
  2171            proof.outputRootProof.messagePasserStorageRoot,
  2172            proof.outputRootProof.latestBlockhash,
  2173          ],
  2174          proof.withdrawalProof,
  2175          opts?.overrides || {},
  2176        ] as const
  2177  
  2178        return this.contracts.l1.OptimismPortal.populateTransaction.proveWithdrawalTransaction(
  2179          ...args
  2180        )
  2181      },
  2182  
  2183      /**
  2184       * Generates a message finalization transaction that can be signed and executed. Only
  2185       * applicable for L2 to L1 messages. Will throw an error if the message has not completed
  2186       * its challenge period yet.
  2187       *
  2188       * @param message Message to generate the finalization transaction for.
  2189       * @param opts Additional options.
  2190       * @param opts.overrides Optional transaction overrides.
  2191       * @param messageIndex The index of the message, if multiple exist from multicall
  2192       * @returns Transaction that can be signed and executed to finalize the message.
  2193       */
  2194      finalizeMessage: async (
  2195        message: MessageLike,
  2196        opts?: {
  2197          overrides?: PayableOverrides
  2198        },
  2199        messageIndex = 0
  2200      ): Promise<TransactionRequest> => {
  2201        const resolved = await this.toCrossChainMessage(message, messageIndex)
  2202        if (resolved.direction === MessageDirection.L1_TO_L2) {
  2203          throw new Error(`cannot finalize L1 to L2 message`)
  2204        }
  2205  
  2206        if (this.bedrock) {
  2207          const withdrawal = await this.toLowLevelMessage(resolved, messageIndex)
  2208          return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction(
  2209            [
  2210              withdrawal.messageNonce,
  2211              withdrawal.sender,
  2212              withdrawal.target,
  2213              withdrawal.value,
  2214              withdrawal.minGasLimit,
  2215              withdrawal.message,
  2216            ],
  2217            opts?.overrides || {}
  2218          )
  2219        } else {
  2220          // L1CrossDomainMessenger relayMessage is the only method that isn't fully backwards
  2221          // compatible, so we need to use the legacy interface. When we fully upgrade to Bedrock we
  2222          // should be able to remove this code.
  2223          const proof = await this.getMessageProof(resolved, messageIndex)
  2224          const legacyL1XDM = new ethers.Contract(
  2225            this.contracts.l1.L1CrossDomainMessenger.address,
  2226            getContractInterface('L1CrossDomainMessenger'),
  2227            this.l1SignerOrProvider
  2228          )
  2229          return legacyL1XDM.populateTransaction.relayMessage(
  2230            resolved.target,
  2231            resolved.sender,
  2232            resolved.message,
  2233            resolved.messageNonce,
  2234            proof,
  2235            opts?.overrides || {}
  2236          )
  2237        }
  2238      },
  2239  
  2240      /**
  2241       * Generates a transaction for depositing some ETH into the L2 chain.
  2242       *
  2243       * @param amount Amount of ETH to deposit.
  2244       * @param opts Additional options.
  2245       * @param opts.recipient Optional address to receive the funds on L2. Defaults to sender.
  2246       * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
  2247       * @param opts.overrides Optional transaction overrides.
  2248       * @returns Transaction that can be signed and executed to deposit the ETH.
  2249       */
  2250      depositETH: async (
  2251        amount: NumberLike,
  2252        opts?: {
  2253          recipient?: AddressLike
  2254          l2GasLimit?: NumberLike
  2255          overrides?: PayableOverrides
  2256        },
  2257        isEstimatingGas: boolean = false
  2258      ): Promise<TransactionRequest> => {
  2259        const getOpts = async () => {
  2260          if (isEstimatingGas) {
  2261            return opts
  2262          }
  2263          const gasEstimation = await this.estimateGas.depositETH(amount, opts)
  2264          return {
  2265            ...opts,
  2266            overrides: {
  2267              ...opts?.overrides,
  2268              gasLimit: gasEstimation.add(gasEstimation.div(2)),
  2269            },
  2270          }
  2271        }
  2272        return this.bridges.ETH.populateTransaction.deposit(
  2273          ethers.constants.AddressZero,
  2274          predeploys.OVM_ETH,
  2275          amount,
  2276          await getOpts()
  2277        )
  2278      },
  2279  
  2280      /**
  2281       * Generates a transaction for withdrawing some ETH back to the L1 chain.
  2282       *
  2283       * @param amount Amount of ETH to withdraw.
  2284       * @param opts Additional options.
  2285       * @param opts.recipient Optional address to receive the funds on L1. Defaults to sender.
  2286       * @param opts.overrides Optional transaction overrides.
  2287       * @returns Transaction that can be signed and executed to withdraw the ETH.
  2288       */
  2289      withdrawETH: async (
  2290        amount: NumberLike,
  2291        opts?: {
  2292          recipient?: AddressLike
  2293          overrides?: Overrides
  2294        }
  2295      ): Promise<TransactionRequest> => {
  2296        return this.bridges.ETH.populateTransaction.withdraw(
  2297          ethers.constants.AddressZero,
  2298          predeploys.OVM_ETH,
  2299          amount,
  2300          opts
  2301        )
  2302      },
  2303  
  2304      /**
  2305       * Generates a transaction for approving some tokens to deposit into the L2 chain.
  2306       *
  2307       * @param l1Token The L1 token address.
  2308       * @param l2Token The L2 token address.
  2309       * @param amount Amount of the token to approve.
  2310       * @param opts Additional options.
  2311       * @param opts.overrides Optional transaction overrides.
  2312       * @returns Transaction response for the approval transaction.
  2313       */
  2314      approveERC20: async (
  2315        l1Token: AddressLike,
  2316        l2Token: AddressLike,
  2317        amount: NumberLike,
  2318        opts?: {
  2319          overrides?: Overrides
  2320        }
  2321      ): Promise<TransactionRequest> => {
  2322        const bridge = await this.getBridgeForTokenPair(l1Token, l2Token)
  2323        return bridge.populateTransaction.approve(l1Token, l2Token, amount, opts)
  2324      },
  2325  
  2326      /**
  2327       * Generates a transaction for depositing some ERC20 tokens into the L2 chain.
  2328       *
  2329       * @param l1Token Address of the L1 token.
  2330       * @param l2Token Address of the L2 token.
  2331       * @param amount Amount to deposit.
  2332       * @param opts Additional options.
  2333       * @param opts.recipient Optional address to receive the funds on L2. Defaults to sender.
  2334       * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
  2335       * @param opts.overrides Optional transaction overrides.
  2336       * @returns Transaction that can be signed and executed to deposit the tokens.
  2337       */
  2338      depositERC20: async (
  2339        l1Token: AddressLike,
  2340        l2Token: AddressLike,
  2341        amount: NumberLike,
  2342        opts?: {
  2343          recipient?: AddressLike
  2344          l2GasLimit?: NumberLike
  2345          overrides?: CallOverrides
  2346        },
  2347        isEstimatingGas: boolean = false
  2348      ): Promise<TransactionRequest> => {
  2349        const bridge = await this.getBridgeForTokenPair(l1Token, l2Token)
  2350        // we need extra buffer for gas limit
  2351        const getOpts = async () => {
  2352          if (isEstimatingGas) {
  2353            return opts
  2354          }
  2355          // if we don't include the users address the estimation will fail from lack of allowance
  2356          if (!ethers.Signer.isSigner(this.l1SignerOrProvider)) {
  2357            throw new Error('unable to deposit without an l1 signer')
  2358          }
  2359          const from = (this.l1SignerOrProvider as Signer).getAddress()
  2360          const gasEstimation = await this.estimateGas.depositERC20(
  2361            l1Token,
  2362            l2Token,
  2363            amount,
  2364            {
  2365              ...opts,
  2366              overrides: {
  2367                ...opts?.overrides,
  2368                from: opts?.overrides?.from ?? from,
  2369              },
  2370            }
  2371          )
  2372          return {
  2373            ...opts,
  2374            overrides: {
  2375              ...opts?.overrides,
  2376              gasLimit: gasEstimation.add(gasEstimation.div(2)),
  2377              from: opts?.overrides?.from ?? from,
  2378            },
  2379          }
  2380        }
  2381        return bridge.populateTransaction.deposit(
  2382          l1Token,
  2383          l2Token,
  2384          amount,
  2385          await getOpts()
  2386        )
  2387      },
  2388  
  2389      /**
  2390       * Generates a transaction for withdrawing some ERC20 tokens back to the L1 chain.
  2391       *
  2392       * @param l1Token Address of the L1 token.
  2393       * @param l2Token Address of the L2 token.
  2394       * @param amount Amount to withdraw.
  2395       * @param opts Additional options.
  2396       * @param opts.recipient Optional address to receive the funds on L1. Defaults to sender.
  2397       * @param opts.overrides Optional transaction overrides.
  2398       * @returns Transaction that can be signed and executed to withdraw the tokens.
  2399       */
  2400      withdrawERC20: async (
  2401        l1Token: AddressLike,
  2402        l2Token: AddressLike,
  2403        amount: NumberLike,
  2404        opts?: {
  2405          recipient?: AddressLike
  2406          overrides?: Overrides
  2407        }
  2408      ): Promise<TransactionRequest> => {
  2409        const bridge = await this.getBridgeForTokenPair(l1Token, l2Token)
  2410        return bridge.populateTransaction.withdraw(l1Token, l2Token, amount, opts)
  2411      },
  2412    }
  2413  
  2414    /**
  2415     * Object that holds the functions that estimates the gas required for a given transaction.
  2416     * Follows the pattern used by ethers.js.
  2417     */
  2418    estimateGas = {
  2419      /**
  2420       * Estimates gas required to send a cross chain message.
  2421       *
  2422       * @param message Cross chain message to send.
  2423       * @param opts Additional options.
  2424       * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
  2425       * @param opts.overrides Optional transaction overrides.
  2426       * @returns Gas estimate for the transaction.
  2427       */
  2428      sendMessage: async (
  2429        message: CrossChainMessageRequest,
  2430        opts?: {
  2431          l2GasLimit?: NumberLike
  2432          overrides?: CallOverrides
  2433        }
  2434      ): Promise<BigNumber> => {
  2435        const tx = await this.populateTransaction.sendMessage(message, opts)
  2436        if (message.direction === MessageDirection.L1_TO_L2) {
  2437          return this.l1Provider.estimateGas(tx)
  2438        } else {
  2439          return this.l2Provider.estimateGas(tx)
  2440        }
  2441      },
  2442  
  2443      /**
  2444       * Estimates gas required to resend a cross chain message. Only applies to L1 to L2 messages.
  2445       *
  2446       * @param message Cross chain message to resend.
  2447       * @param messageGasLimit New gas limit to use for the message.
  2448       * @param opts Additional options.
  2449       * @param opts.overrides Optional transaction overrides.
  2450       * @returns Gas estimate for the transaction.
  2451       */
  2452      resendMessage: async (
  2453        message: MessageLike,
  2454        messageGasLimit: NumberLike,
  2455        opts?: {
  2456          overrides?: CallOverrides
  2457        }
  2458      ): Promise<BigNumber> => {
  2459        return this.l1Provider.estimateGas(
  2460          await this.populateTransaction.resendMessage(
  2461            message,
  2462            messageGasLimit,
  2463            opts
  2464          )
  2465        )
  2466      },
  2467  
  2468      /**
  2469       * Estimates gas required to prove a cross chain message. Only applies to L2 to L1 messages.
  2470       *
  2471       * @param message Message to generate the proving transaction for.
  2472       * @param opts Additional options.
  2473       * @param opts.overrides Optional transaction overrides.
  2474       * @param messageIndex The index of the message, if multiple exist from multicall
  2475       * @returns Gas estimate for the transaction.
  2476       */
  2477      proveMessage: async (
  2478        message: MessageLike,
  2479        opts?: {
  2480          overrides?: CallOverrides
  2481        },
  2482        messageIndex = 0
  2483      ): Promise<BigNumber> => {
  2484        return this.l1Provider.estimateGas(
  2485          await this.populateTransaction.proveMessage(message, opts, messageIndex)
  2486        )
  2487      },
  2488  
  2489      /**
  2490       * Estimates gas required to finalize a cross chain message. Only applies to L2 to L1 messages.
  2491       *
  2492       * @param message Message to generate the finalization transaction for.
  2493       * @param opts Additional options.
  2494       * @param opts.overrides Optional transaction overrides.
  2495       * @param messageIndex The index of the message, if multiple exist from multicall
  2496       * @returns Gas estimate for the transaction.
  2497       */
  2498      finalizeMessage: async (
  2499        message: MessageLike,
  2500        opts?: {
  2501          overrides?: CallOverrides
  2502        },
  2503        messageIndex = 0
  2504      ): Promise<BigNumber> => {
  2505        return this.l1Provider.estimateGas(
  2506          await this.populateTransaction.finalizeMessage(
  2507            message,
  2508            opts,
  2509            messageIndex
  2510          )
  2511        )
  2512      },
  2513  
  2514      /**
  2515       * Estimates gas required to deposit some ETH into the L2 chain.
  2516       *
  2517       * @param amount Amount of ETH to deposit.
  2518       * @param opts Additional options.
  2519       * @param opts.recipient Optional address to receive the funds on L2. Defaults to sender.
  2520       * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
  2521       * @param opts.overrides Optional transaction overrides.
  2522       * @returns Gas estimate for the transaction.
  2523       */
  2524      depositETH: async (
  2525        amount: NumberLike,
  2526        opts?: {
  2527          recipient?: AddressLike
  2528          l2GasLimit?: NumberLike
  2529          overrides?: CallOverrides
  2530        }
  2531      ): Promise<BigNumber> => {
  2532        return this.l1Provider.estimateGas(
  2533          await this.populateTransaction.depositETH(amount, opts, true)
  2534        )
  2535      },
  2536  
  2537      /**
  2538       * Estimates gas required to withdraw some ETH back to the L1 chain.
  2539       *
  2540       * @param amount Amount of ETH to withdraw.
  2541       * @param opts Additional options.
  2542       * @param opts.recipient Optional address to receive the funds on L1. Defaults to sender.
  2543       * @param opts.overrides Optional transaction overrides.
  2544       * @returns Gas estimate for the transaction.
  2545       */
  2546      withdrawETH: async (
  2547        amount: NumberLike,
  2548        opts?: {
  2549          recipient?: AddressLike
  2550          overrides?: CallOverrides
  2551        }
  2552      ): Promise<BigNumber> => {
  2553        return this.l2Provider.estimateGas(
  2554          await this.populateTransaction.withdrawETH(amount, opts)
  2555        )
  2556      },
  2557  
  2558      /**
  2559       * Estimates gas required to approve some tokens to deposit into the L2 chain.
  2560       *
  2561       * @param l1Token The L1 token address.
  2562       * @param l2Token The L2 token address.
  2563       * @param amount Amount of the token to approve.
  2564       * @param opts Additional options.
  2565       * @param opts.overrides Optional transaction overrides.
  2566       * @returns Transaction response for the approval transaction.
  2567       */
  2568      approveERC20: async (
  2569        l1Token: AddressLike,
  2570        l2Token: AddressLike,
  2571        amount: NumberLike,
  2572        opts?: {
  2573          overrides?: CallOverrides
  2574        }
  2575      ): Promise<BigNumber> => {
  2576        return this.l1Provider.estimateGas(
  2577          await this.populateTransaction.approveERC20(
  2578            l1Token,
  2579            l2Token,
  2580            amount,
  2581            opts
  2582          )
  2583        )
  2584      },
  2585  
  2586      /**
  2587       * Estimates gas required to deposit some ERC20 tokens into the L2 chain.
  2588       *
  2589       * @param l1Token Address of the L1 token.
  2590       * @param l2Token Address of the L2 token.
  2591       * @param amount Amount to deposit.
  2592       * @param opts Additional options.
  2593       * @param opts.recipient Optional address to receive the funds on L2. Defaults to sender.
  2594       * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
  2595       * @param opts.overrides Optional transaction overrides.
  2596       * @returns Gas estimate for the transaction.
  2597       */
  2598      depositERC20: async (
  2599        l1Token: AddressLike,
  2600        l2Token: AddressLike,
  2601        amount: NumberLike,
  2602        opts?: {
  2603          recipient?: AddressLike
  2604          l2GasLimit?: NumberLike
  2605          overrides?: CallOverrides
  2606        }
  2607      ): Promise<BigNumber> => {
  2608        return this.l1Provider.estimateGas(
  2609          await this.populateTransaction.depositERC20(
  2610            l1Token,
  2611            l2Token,
  2612            amount,
  2613            opts,
  2614            true
  2615          )
  2616        )
  2617      },
  2618  
  2619      /**
  2620       * Estimates gas required to withdraw some ERC20 tokens back to the L1 chain.
  2621       *
  2622       * @param l1Token Address of the L1 token.
  2623       * @param l2Token Address of the L2 token.
  2624       * @param amount Amount to withdraw.
  2625       * @param opts Additional options.
  2626       * @param opts.recipient Optional address to receive the funds on L1. Defaults to sender.
  2627       * @param opts.overrides Optional transaction overrides.
  2628       * @returns Gas estimate for the transaction.
  2629       */
  2630      withdrawERC20: async (
  2631        l1Token: AddressLike,
  2632        l2Token: AddressLike,
  2633        amount: NumberLike,
  2634        opts?: {
  2635          recipient?: AddressLike
  2636          overrides?: CallOverrides
  2637        }
  2638      ): Promise<BigNumber> => {
  2639        return this.l2Provider.estimateGas(
  2640          await this.populateTransaction.withdrawERC20(
  2641            l1Token,
  2642            l2Token,
  2643            amount,
  2644            opts
  2645          )
  2646        )
  2647      },
  2648    }
  2649  }