Commit b66e3131 authored by Kelvin Fichter's avatar Kelvin Fichter

feat(sdk): add fn to wait for message status

Adds a function to the SDK (waitForMessageStatus) which will wait until
the status of a particular message matches the target status.
waitForMessageStatus also handles certain special cases where different
status messages are incompatible (e.g., the RELAYED and
FAILED_L1_TO_L2_MESSAGE status for L1 to L2 messages). Useful to avoid
including looping logic in your own application.
parent 66df4cd4
---
'@eth-optimism/sdk': patch
---
Add a function for waiting for a particular message status
...@@ -74,11 +74,10 @@ describe('Basic L1<>L2 Communication', async () => { ...@@ -74,11 +74,10 @@ describe('Basic L1<>L2 Communication', async () => {
} }
) )
let status: MessageStatus await env.messenger.waitForMessageStatus(
while (status !== MessageStatus.READY_FOR_RELAY) { transaction,
status = await env.messenger.getMessageStatus(transaction) MessageStatus.READY_FOR_RELAY
await sleep(1000) )
}
await env.messenger.finalizeMessage(transaction) await env.messenger.finalizeMessage(transaction)
await env.messenger.waitForMessageReceipt(transaction) await env.messenger.waitForMessageReceipt(transaction)
......
...@@ -3,7 +3,6 @@ import { BigNumber, Contract, ContractFactory, utils, Wallet } from 'ethers' ...@@ -3,7 +3,6 @@ import { BigNumber, Contract, ContractFactory, utils, Wallet } from 'ethers'
import { ethers } from 'hardhat' import { ethers } from 'hardhat'
import { getContractFactory } from '@eth-optimism/contracts' import { getContractFactory } from '@eth-optimism/contracts'
import { MessageStatus } from '@eth-optimism/sdk' import { MessageStatus } from '@eth-optimism/sdk'
import { sleep } from '@eth-optimism/core-utils'
/* Imports: Internal */ /* Imports: Internal */
import { expect } from './shared/setup' import { expect } from './shared/setup'
...@@ -108,12 +107,10 @@ describe('Bridged tokens', () => { ...@@ -108,12 +107,10 @@ describe('Bridged tokens', () => {
500 500
) )
// TODO: Maybe this should be built into the SDK await env.messenger.waitForMessageStatus(
let status: MessageStatus tx,
while (status !== MessageStatus.READY_FOR_RELAY) { MessageStatus.READY_FOR_RELAY
status = await env.messenger.getMessageStatus(tx) )
await sleep(1000)
}
await env.messenger.finalizeMessage(tx) await env.messenger.finalizeMessage(tx)
await env.messenger.waitForMessageReceipt(tx) await env.messenger.waitForMessageReceipt(tx)
...@@ -139,12 +136,10 @@ describe('Bridged tokens', () => { ...@@ -139,12 +136,10 @@ describe('Bridged tokens', () => {
} }
) )
// TODO: Maybe this should be built into the SDK await env.messenger.waitForMessageStatus(
let status: MessageStatus tx,
while (status !== MessageStatus.READY_FOR_RELAY) { MessageStatus.READY_FOR_RELAY
status = await env.messenger.getMessageStatus(tx) )
await sleep(1000)
}
await env.messenger.finalizeMessage(tx) await env.messenger.finalizeMessage(tx)
await env.messenger.waitForMessageReceipt(tx) await env.messenger.waitForMessageReceipt(tx)
...@@ -201,12 +196,10 @@ describe('Bridged tokens', () => { ...@@ -201,12 +196,10 @@ describe('Bridged tokens', () => {
} }
) )
// TODO: Maybe this should be built into the SDK await env.messenger.waitForMessageStatus(
let status: MessageStatus withdrawalTx,
while (status !== MessageStatus.READY_FOR_RELAY) { MessageStatus.READY_FOR_RELAY
status = await env.messenger.getMessageStatus(withdrawalTx) )
await sleep(1000)
}
await env.messenger.finalizeMessage(withdrawalTx) await env.messenger.finalizeMessage(withdrawalTx)
await env.messenger.waitForMessageReceipt(withdrawalTx) await env.messenger.waitForMessageReceipt(withdrawalTx)
......
...@@ -129,14 +129,10 @@ export class OptimismEnv { ...@@ -129,14 +129,10 @@ export class OptimismEnv {
} }
for (const message of messages) { for (const message of messages) {
let status: MessageStatus await this.messenger.waitForMessageStatus(
while ( message,
status !== MessageStatus.READY_FOR_RELAY && MessageStatus.READY_FOR_RELAY
status !== MessageStatus.RELAYED )
) {
status = await this.messenger.getMessageStatus(message)
await sleep(1000)
}
let relayed = false let relayed = false
while (!relayed) { while (!relayed) {
......
...@@ -453,10 +453,13 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -453,10 +453,13 @@ export class CrossChainMessenger implements ICrossChainMessenger {
timeoutMs?: number timeoutMs?: number
} = {} } = {}
): Promise<MessageReceipt> { ): Promise<MessageReceipt> {
// Resolving once up-front is slightly more efficient.
const resolved = await this.toCrossChainMessage(message)
let totalTimeMs = 0 let totalTimeMs = 0
while (totalTimeMs < (opts.timeoutMs || Infinity)) { while (totalTimeMs < (opts.timeoutMs || Infinity)) {
const tick = Date.now() const tick = Date.now()
const receipt = await this.getMessageReceipt(message) const receipt = await this.getMessageReceipt(resolved)
if (receipt !== null) { if (receipt !== null) {
return receipt return receipt
} else { } else {
...@@ -468,6 +471,73 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -468,6 +471,73 @@ export class CrossChainMessenger implements ICrossChainMessenger {
throw new Error(`timed out waiting for message receipt`) throw new Error(`timed out waiting for message receipt`)
} }
public async waitForMessageStatus(
message: MessageLike,
status: MessageStatus,
opts: {
pollIntervalMs?: number
timeoutMs?: number
} = {}
): Promise<void> {
// Resolving once up-front is slightly more efficient.
const resolved = await this.toCrossChainMessage(message)
let totalTimeMs = 0
while (totalTimeMs < (opts.timeoutMs || Infinity)) {
const tick = Date.now()
const currentStatus = await this.getMessageStatus(resolved)
// Handle special cases for L1 to L2 messages.
if (resolved.direction === MessageDirection.L1_TO_L2) {
// If we're at the expected status, we're done.
if (currentStatus === status) {
return
}
if (
status === MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE &&
currentStatus > status
) {
// Anything other than UNCONFIRMED_L1_TO_L2_MESSAGE implies that the message was at one
// point "unconfirmed", so we can stop waiting.
return
}
if (
status === MessageStatus.FAILED_L1_TO_L2_MESSAGE &&
currentStatus === MessageStatus.RELAYED
) {
throw new Error(
`incompatible message status, expected FAILED_L1_TO_L2_MESSAGE got RELAYED`
)
}
if (
status === MessageStatus.RELAYED &&
currentStatus === MessageStatus.FAILED_L1_TO_L2_MESSAGE
) {
throw new Error(
`incompatible message status, expected RELAYED got FAILED_L1_TO_L2_MESSAGE`
)
}
}
// Handle special cases for L2 to L1 messages.
if (resolved.direction === MessageDirection.L2_TO_L1) {
if (currentStatus >= status) {
// For L2 to L1 messages, anything after the expected status implies the previous status,
// so we can safely return if the current status enum is larger than the expected one.
return
}
}
await sleep(opts.pollIntervalMs || 4000)
totalTimeMs += Date.now() - tick
}
throw new Error(`timed out waiting for message status change`)
}
public async estimateL2MessageGasLimit( public async estimateL2MessageGasLimit(
message: MessageRequestLike, message: MessageRequestLike,
opts?: { opts?: {
......
...@@ -226,6 +226,27 @@ export interface ICrossChainMessenger { ...@@ -226,6 +226,27 @@ export interface ICrossChainMessenger {
} }
): Promise<MessageReceipt> ): Promise<MessageReceipt>
/**
* Waits until the status of a given message changes to the expected status. Note that if the
* status of the given message changes to a status that implies the expected status, this will
* still return. If the status of the message changes to a status that exclues the expected
* status, this will throw an error.
*
* @param message Message to wait for.
* @param status Expected status of the message.
* @param opts Options to pass to the waiting function.
* @param opts.pollIntervalMs Number of milliseconds to wait when polling.
* @param opts.timeoutMs Milliseconds to wait before timing out.
*/
waitForMessageStatus(
message: MessageLike,
status: MessageStatus,
opts?: {
pollIntervalMs?: number
timeoutMs?: number
}
): Promise<void>
/** /**
* Estimates the amount of gas required to fully execute a given message on L2. Only applies to * Estimates the amount of gas required to fully execute a given message on L2. Only applies to
* L1 => L2 messages. You would supply this gas limit when sending the message to L2. * L1 => L2 messages. You would supply this gas limit when sending the message to L2.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment