github.com/ethereum-optimism/optimism@v1.7.2/packages/core-utils/src/optimism/deposit-transaction.ts (about)

     1  import { getAddress } from '@ethersproject/address'
     2  import { ContractReceipt, Event } from '@ethersproject/contracts'
     3  import { BigNumber, BigNumberish } from '@ethersproject/bignumber'
     4  import { keccak256 } from '@ethersproject/keccak256'
     5  import { Zero } from '@ethersproject/constants'
     6  import * as RLP from '@ethersproject/rlp'
     7  import {
     8    arrayify,
     9    BytesLike,
    10    hexDataSlice,
    11    stripZeros,
    12    hexConcat,
    13    zeroPad,
    14  } from '@ethersproject/bytes'
    15  
    16  const formatBoolean = (value: boolean): Uint8Array => {
    17    return value ? new Uint8Array([1]) : new Uint8Array([])
    18  }
    19  
    20  const formatNumber = (value: BigNumberish, name: string): Uint8Array => {
    21    const result = stripZeros(BigNumber.from(value).toHexString())
    22    if (result.length > 32) {
    23      throw new Error(`invalid length for ${name}`)
    24    }
    25    return result
    26  }
    27  
    28  const handleBoolean = (value: string): boolean => {
    29    if (value === '0x') {
    30      return false
    31    }
    32    if (value === '0x01') {
    33      return true
    34    }
    35    throw new Error(`invalid boolean RLP hex value ${value}`)
    36  }
    37  
    38  const handleNumber = (value: string): BigNumber => {
    39    if (value === '0x') {
    40      return Zero
    41    }
    42    return BigNumber.from(value)
    43  }
    44  
    45  const handleAddress = (value: string): string => {
    46    if (value === '0x') {
    47      // @ts-ignore
    48      return null
    49    }
    50    return getAddress(value)
    51  }
    52  
    53  export enum SourceHashDomain {
    54    UserDeposit = 0,
    55    L1InfoDeposit = 1,
    56  }
    57  
    58  interface DepositTxOpts {
    59    sourceHash?: string
    60    from: string
    61    to: string | null
    62    mint: BigNumberish
    63    value: BigNumberish
    64    gas: BigNumberish
    65    isSystemTransaction: boolean
    66    data: string
    67    domain?: SourceHashDomain
    68    l1BlockHash?: string
    69    logIndex?: BigNumberish
    70    sequenceNumber?: BigNumberish
    71  }
    72  
    73  interface DepositTxExtraOpts {
    74    domain?: SourceHashDomain
    75    l1BlockHash?: string
    76    logIndex?: BigNumberish
    77    sequenceNumber?: BigNumberish
    78  }
    79  
    80  export class DepositTx {
    81    public type = 0x7e
    82    public version = 0x00
    83    private _sourceHash?: string
    84    public from: string
    85    public to: string | null
    86    public mint: BigNumberish
    87    public value: BigNumberish
    88    public gas: BigNumberish
    89    public isSystemTransaction: boolean
    90    public data: BigNumberish
    91  
    92    public domain?: SourceHashDomain
    93    public l1BlockHash?: string
    94    public logIndex?: BigNumberish
    95    public sequenceNumber?: BigNumberish
    96  
    97    constructor(opts: Partial<DepositTxOpts> = {}) {
    98      this._sourceHash = opts.sourceHash
    99      this.from = opts.from!
   100      this.to = opts.to!
   101      this.mint = opts.mint!
   102      this.value = opts.value!
   103      this.gas = opts.gas!
   104      this.isSystemTransaction = opts.isSystemTransaction || false
   105      this.data = opts.data!
   106      this.domain = opts.domain
   107      this.l1BlockHash = opts.l1BlockHash
   108      this.logIndex = opts.logIndex
   109      this.sequenceNumber = opts.sequenceNumber
   110    }
   111  
   112    hash() {
   113      const encoded = this.encode()
   114      return keccak256(encoded)
   115    }
   116  
   117    sourceHash() {
   118      if (!this._sourceHash) {
   119        let marker: string
   120        switch (this.domain) {
   121          case SourceHashDomain.UserDeposit:
   122            marker = BigNumber.from(this.logIndex).toHexString()
   123            break
   124          case SourceHashDomain.L1InfoDeposit:
   125            marker = BigNumber.from(this.sequenceNumber).toHexString()
   126            break
   127          default:
   128            throw new Error(`Unknown domain: ${this.domain}`)
   129        }
   130  
   131        if (!this.l1BlockHash) {
   132          throw new Error('Need l1BlockHash to compute sourceHash')
   133        }
   134  
   135        const l1BlockHash = this.l1BlockHash
   136        const input = hexConcat([l1BlockHash, zeroPad(marker, 32)])
   137        const depositIDHash = keccak256(input)
   138        const domain = BigNumber.from(this.domain).toHexString()
   139        const domainInput = hexConcat([zeroPad(domain, 32), depositIDHash])
   140        this._sourceHash = keccak256(domainInput)
   141      }
   142      return this._sourceHash
   143    }
   144  
   145    encode() {
   146      const fields: any = [
   147        this.sourceHash() || '0x',
   148        getAddress(this.from) || '0x',
   149        this.to != null ? getAddress(this.to) : '0x',
   150        formatNumber(this.mint || 0, 'mint'),
   151        formatNumber(this.value || 0, 'value'),
   152        formatNumber(this.gas || 0, 'gas'),
   153        formatBoolean(this.isSystemTransaction),
   154        this.data || '0x',
   155      ]
   156  
   157      return hexConcat([
   158        BigNumber.from(this.type).toHexString(),
   159        RLP.encode(fields),
   160      ])
   161    }
   162  
   163    decode(raw: BytesLike, extra: DepositTxExtraOpts = {}) {
   164      const payload = arrayify(raw)
   165      if (payload[0] !== this.type) {
   166        throw new Error(`Invalid type ${payload[0]}`)
   167      }
   168      this.version = payload[1]
   169      const transaction = RLP.decode(payload.slice(1))
   170      this._sourceHash = transaction[0]
   171      this.from = handleAddress(transaction[1])
   172      this.to = handleAddress(transaction[2])
   173      this.mint = handleNumber(transaction[3])
   174      this.value = handleNumber(transaction[4])
   175      this.gas = handleNumber(transaction[5])
   176      this.isSystemTransaction = handleBoolean(transaction[6])
   177      this.data = transaction[7]
   178  
   179      if ('l1BlockHash' in extra) {
   180        this.l1BlockHash = extra.l1BlockHash
   181      }
   182      if ('domain' in extra) {
   183        this.domain = extra.domain
   184      }
   185      if ('logIndex' in extra) {
   186        this.logIndex = extra.logIndex
   187      }
   188      if ('sequenceNumber' in extra) {
   189        this.sequenceNumber = extra.sequenceNumber
   190      }
   191      return this
   192    }
   193  
   194    static decode(raw: BytesLike, extra?: DepositTxExtraOpts): DepositTx {
   195      return new this().decode(raw, extra)
   196    }
   197  
   198    fromL1Receipt(receipt: ContractReceipt, index: number): DepositTx {
   199      if (!receipt.events) {
   200        throw new Error('cannot parse receipt')
   201      }
   202      const event = receipt.events[index]
   203      if (!event) {
   204        throw new Error(`event index ${index} does not exist`)
   205      }
   206      return this.fromL1Event(event)
   207    }
   208  
   209    static fromL1Receipt(receipt: ContractReceipt, index: number): DepositTx {
   210      return new this({}).fromL1Receipt(receipt, index)
   211    }
   212  
   213    fromL1Event(event: Event): DepositTx {
   214      if (event.event !== 'TransactionDeposited') {
   215        throw new Error(`incorrect event type: ${event.event}`)
   216      }
   217      if (typeof event.args === 'undefined') {
   218        throw new Error('no event args')
   219      }
   220      if (typeof event.args.from === 'undefined') {
   221        throw new Error('"from" undefined')
   222      }
   223      this.from = event.args.from
   224      if (typeof event.args.to === 'undefined') {
   225        throw new Error('"to" undefined')
   226      }
   227      if (typeof event.args.version === 'undefined') {
   228        throw new Error(`"verison" undefined`)
   229      }
   230      if (!event.args.version.eq(0)) {
   231        throw new Error(`Unsupported version ${event.args.version.toString()}`)
   232      }
   233      if (typeof event.args.opaqueData === 'undefined') {
   234        throw new Error(`"opaqueData" undefined`)
   235      }
   236  
   237      const opaqueData = event.args.opaqueData
   238      if (opaqueData.length < 32 + 32 + 8 + 1) {
   239        throw new Error(`invalid opaqueData size: ${opaqueData.length}`)
   240      }
   241  
   242      let offset = 0
   243      this.mint = BigNumber.from(hexDataSlice(opaqueData, offset, offset + 32))
   244      offset += 32
   245      this.value = BigNumber.from(hexDataSlice(opaqueData, offset, offset + 32))
   246      offset += 32
   247      this.gas = BigNumber.from(hexDataSlice(opaqueData, offset, offset + 8))
   248      offset += 8
   249      const isCreation = BigNumber.from(opaqueData[offset]).eq(1)
   250      offset += 1
   251      this.to = isCreation === true ? null : event.args.to
   252      const length = opaqueData.length - offset
   253      this.isSystemTransaction = false
   254      this.data = hexDataSlice(opaqueData, offset, offset + length)
   255      this.domain = SourceHashDomain.UserDeposit
   256      this.l1BlockHash = event.blockHash
   257      this.logIndex = event.logIndex
   258      return this
   259    }
   260  
   261    static fromL1Event(event: Event): DepositTx {
   262      return new this({}).fromL1Event(event)
   263    }
   264  }