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 }