watcher.ts 4.97 KB
Newer Older
1 2
/* External Imports */
import { ethers } from 'ethers'
3
import { Provider, TransactionReceipt } from '@ethersproject/abstract-provider'
4

5 6 7 8
const SENT_MESSAGE = ethers.utils.id('SentMessage(bytes)')
const RELAYED_MESSAGE = ethers.utils.id(`RelayedMessage(bytes32)`)
const FAILED_RELAYED_MESSAGE = ethers.utils.id(`FailedRelayedMessage(bytes32)`)

9 10 11
export interface Layer {
  provider: Provider
  messengerAddress: string
12
  blocksToFetch?: number
13 14 15 16 17
}

export interface WatcherOptions {
  l1: Layer
  l2: Layer
18
  pollInterval?: number
19
  blocksToFetch?: number
20
  pollForPending?: boolean
21 22 23 24 25
}

export class Watcher {
  public l1: Layer
  public l2: Layer
26
  public pollInterval = 3000
27
  public blocksToFetch = 1500
28
  public pollForPending = true
29 30 31 32

  constructor(opts: WatcherOptions) {
    this.l1 = opts.l1
    this.l2 = opts.l2
33
    if (typeof opts.pollInterval === 'number') {
34 35
      this.pollInterval = opts.pollInterval
    }
36
    if (typeof opts.blocksToFetch === 'number') {
37 38
      this.blocksToFetch = opts.blocksToFetch
    }
39 40 41
    if (typeof opts.pollForPending === 'boolean') {
      this.pollForPending = opts.pollForPending
    }
42 43 44
  }

  public async getMessageHashesFromL1Tx(l1TxHash: string): Promise<string[]> {
45
    return this.getMessageHashesFromTx(this.l1, l1TxHash)
46 47
  }
  public async getMessageHashesFromL2Tx(l2TxHash: string): Promise<string[]> {
48
    return this.getMessageHashesFromTx(this.l2, l2TxHash)
49 50 51 52
  }

  public async getL1TransactionReceipt(
    l2ToL1MsgHash: string,
53
    pollForPending?
54
  ): Promise<TransactionReceipt> {
55
    return this.getTransactionReceipt(this.l1, l2ToL1MsgHash, pollForPending)
56 57 58 59
  }

  public async getL2TransactionReceipt(
    l1ToL2MsgHash: string,
60
    pollForPending?
61 62
  ): Promise<TransactionReceipt> {
    return this.getTransactionReceipt(this.l2, l1ToL2MsgHash, pollForPending)
63 64
  }

65 66
  public async getMessageHashesFromTx(
    layer: Layer,
67 68 69 70 71 72 73 74
    txHash: string
  ): Promise<string[]> {
    const receipt = await layer.provider.getTransactionReceipt(txHash)
    if (!receipt) {
      return []
    }

    const msgHashes = []
elenadimitrova's avatar
elenadimitrova committed
75 76 77
    const sentMessageEventId = ethers.utils.id(
      'SentMessage(address,address,bytes,uint256,uint256)'
    )
78 79 80 81 82 83
    const l2CrossDomainMessengerRelayAbi = [
      'function relayMessage(address _target,address _sender,bytes memory _message,uint256 _messageNonce)',
    ]
    const l2CrossDomainMessengerRelayinterface = new ethers.utils.Interface(
      l2CrossDomainMessengerRelayAbi
    )
84 85 86
    for (const log of receipt.logs) {
      if (
        log.address === layer.messengerAddress &&
elenadimitrova's avatar
elenadimitrova committed
87
        log.topics[0] === sentMessageEventId
88
      ) {
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
        const [sender, message, messageNonce] =
          ethers.utils.defaultAbiCoder.decode(
            ['address', 'bytes', 'uint256'],
            log.data
          )

        const [target] = ethers.utils.defaultAbiCoder.decode(
          ['address'],
          log.topics[1]
        )

        const encodedMessage =
          l2CrossDomainMessengerRelayinterface.encodeFunctionData(
            'relayMessage',
            [target, sender, message, messageNonce]
          )

        msgHashes.push(
          ethers.utils.solidityKeccak256(['bytes'], [encodedMessage])
108 109 110 111 112 113
        )
      }
    }
    return msgHashes
  }

114 115
  public async getTransactionReceipt(
    layer: Layer,
116
    msgHash: string,
117
    pollForPending?
118
  ): Promise<TransactionReceipt> {
119 120 121 122
    if (typeof pollForPending !== 'boolean') {
      pollForPending = this.pollForPending
    }

123 124
    let matches: ethers.providers.Log[] = []

125 126 127 128 129
    let blocksToFetch = layer.blocksToFetch
    if (typeof blocksToFetch !== 'number') {
      blocksToFetch = this.blocksToFetch
    }

130 131 132
    // scan for transaction with specified message
    while (matches.length === 0) {
      const blockNumber = await layer.provider.getBlockNumber()
133
      const startingBlock = Math.max(blockNumber - blocksToFetch, 0)
134 135
      const successFilter: ethers.providers.Filter = {
        address: layer.messengerAddress,
136
        topics: [RELAYED_MESSAGE],
137
        fromBlock: startingBlock,
138 139 140
      }
      const failureFilter: ethers.providers.Filter = {
        address: layer.messengerAddress,
141
        topics: [FAILED_RELAYED_MESSAGE],
142
        fromBlock: startingBlock,
143 144 145 146
      }
      const successLogs = await layer.provider.getLogs(successFilter)
      const failureLogs = await layer.provider.getLogs(failureFilter)
      const logs = successLogs.concat(failureLogs)
147 148 149
      matches = logs.filter(
        (log: ethers.providers.Log) => log.topics[1] === msgHash
      )
150

151 152 153 154
      // exit loop after first iteration if not polling
      if (!pollForPending) {
        break
      }
155 156

      // pause awhile before trying again
157
      await new Promise((r) => setTimeout(r, this.pollInterval))
158
    }
159 160 161 162 163 164 165 166 167

    // Message was relayed in the past
    if (matches.length > 0) {
      if (matches.length > 1) {
        throw Error(
          'Found multiple transactions relaying the same message hash.'
        )
      }
      return layer.provider.getTransactionReceipt(matches[0].transactionHash)
168
    } else {
169 170 171 172
      return Promise.resolve(undefined)
    }
  }
}