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 }