Commit c9123af4 authored by smartcontracts's avatar smartcontracts Committed by GitHub

Merge pull request #1979 from ethereum-optimism/sc/sdk-wait-for-receipt

feat(sdk): implement waitForMessageReceipt
parents 9195dcf2 0b6eca57
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
TransactionReceipt, TransactionReceipt,
} from '@ethersproject/abstract-provider' } from '@ethersproject/abstract-provider'
import { ethers, BigNumber, Event } from 'ethers' import { ethers, BigNumber, Event } from 'ethers'
import { sleep } from '@eth-optimism/core-utils'
import { import {
ICrossChainProvider, ICrossChainProvider,
OEContracts, OEContracts,
...@@ -395,15 +396,27 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -395,15 +396,27 @@ export class CrossChainProvider implements ICrossChainProvider {
return null return null
} }
public async waitForMessageReciept( public async waitForMessageReceipt(
message: MessageLike, message: MessageLike,
opts?: { opts: {
confirmations?: number confirmations?: number
pollIntervalMs?: number pollIntervalMs?: number
timeoutMs?: number timeoutMs?: number
} } = {}
): Promise<MessageReceipt> { ): Promise<MessageReceipt> {
throw new Error('Not implemented') let totalTimeMs = 0
while (totalTimeMs < (opts.timeoutMs || Infinity)) {
const tick = Date.now()
const receipt = await this.getMessageReceipt(message)
if (receipt !== null) {
return receipt
} else {
await sleep(opts.pollIntervalMs || 4000)
totalTimeMs += Date.now() - tick
}
}
throw new Error(`timed out waiting for message receipt`)
} }
public async estimateL2MessageGasLimit( public async estimateL2MessageGasLimit(
......
...@@ -186,7 +186,7 @@ export interface ICrossChainProvider { ...@@ -186,7 +186,7 @@ export interface ICrossChainProvider {
* @returns CrossChainMessage receipt including receipt of the transaction that relayed the * @returns CrossChainMessage receipt including receipt of the transaction that relayed the
* given message. * given message.
*/ */
waitForMessageReciept( waitForMessageReceipt(
message: MessageLike, message: MessageLike,
opts?: { opts?: {
confirmations?: number confirmations?: number
......
/* eslint-disable @typescript-eslint/no-empty-function */
import './setup' import './setup'
describe('CrossChainERC20Pair', () => { describe('CrossChainERC20Pair', () => {
describe('construction', () => { describe('construction', () => {
it('should have a messenger', () => {}) it('should have a messenger')
describe('when the token is a standard bridge token', () => { describe('when the token is a standard bridge token', () => {
it('should resolve the correct bridge', () => {}) it('should resolve the correct bridge')
}) })
describe('when the token is SNX', () => { describe('when the token is SNX', () => {
it('should resolve the correct bridge', () => {}) it('should resolve the correct bridge')
}) })
describe('when the token is DAI', () => { describe('when the token is DAI', () => {
it('should resolve the correct bridge', () => {}) it('should resolve the correct bridge')
}) })
describe('when a custom adapter is provided', () => { describe('when a custom adapter is provided', () => {
it('should use the custom adapter', () => {}) it('should use the custom adapter')
}) })
}) })
describe('deposit', () => { describe('deposit', () => {
describe('when the user has enough balance and allowance', () => { describe('when the user has enough balance and allowance', () => {
describe('when the token is a standard bridge token', () => { describe('when the token is a standard bridge token', () => {
it('should trigger a token deposit', () => {}) it('should trigger a token deposit')
}) })
describe('when the token is ETH', () => { describe('when the token is ETH', () => {
it('should trigger a token deposit', () => {}) it('should trigger a token deposit')
}) })
describe('when the token is SNX', () => { describe('when the token is SNX', () => {
it('should trigger a token deposit', () => {}) it('should trigger a token deposit')
}) })
describe('when the token is DAI', () => { describe('when the token is DAI', () => {
it('should trigger a token deposit', () => {}) it('should trigger a token deposit')
}) })
}) })
describe('when the user does not have enough balance', () => { describe('when the user does not have enough balance', () => {
it('should throw an error', () => {}) it('should throw an error')
}) })
describe('when the user has not given enough allowance to the bridge', () => { describe('when the user has not given enough allowance to the bridge', () => {
it('should throw an error', () => {}) it('should throw an error')
}) })
}) })
describe('withdraw', () => { describe('withdraw', () => {
describe('when the user has enough balance', () => { describe('when the user has enough balance', () => {
describe('when the token is a standard bridge token', () => { describe('when the token is a standard bridge token', () => {
it('should trigger a token withdrawal', () => {}) it('should trigger a token withdrawal')
}) })
describe('when the token is ETH', () => { describe('when the token is ETH', () => {
it('should trigger a token withdrawal', () => {}) it('should trigger a token withdrawal')
}) })
describe('when the token is SNX', () => { describe('when the token is SNX', () => {
it('should trigger a token withdrawal', () => {}) it('should trigger a token withdrawal')
}) })
describe('when the token is DAI', () => { describe('when the token is DAI', () => {
it('should trigger a token withdrawal', () => {}) it('should trigger a token withdrawal')
}) })
}) })
describe('when the user does not have enough balance', () => { describe('when the user does not have enough balance', () => {
it('should throw an error', () => {}) it('should throw an error')
}) })
}) })
describe('populateTransaction', () => { describe('populateTransaction', () => {
describe('deposit', () => { describe('deposit', () => {
it('should populate the transaction with the correct values', () => {}) it('should populate the transaction with the correct values')
}) })
describe('withdraw', () => { describe('withdraw', () => {
it('should populate the transaction with the correct values', () => {}) it('should populate the transaction with the correct values')
}) })
}) })
describe('estimateGas', () => { describe('estimateGas', () => {
describe('deposit', () => { describe('deposit', () => {
it('should estimate gas required for the transaction', () => {}) it('should estimate gas required for the transaction')
}) })
describe('withdraw', () => { describe('withdraw', () => {
it('should estimate gas required for the transaction', () => {}) it('should estimate gas required for the transaction')
}) })
}) })
}) })
/* eslint-disable @typescript-eslint/no-empty-function */
import './setup' import './setup'
describe('CrossChainMessenger', () => { describe('CrossChainMessenger', () => {
describe('sendMessage', () => { describe('sendMessage', () => {
describe('when no l2GasLimit is provided', () => { describe('when no l2GasLimit is provided', () => {
it('should send a message with an estimated l2GasLimit', () => {}) it('should send a message with an estimated l2GasLimit')
}) })
describe('when an l2GasLimit is provided', () => { describe('when an l2GasLimit is provided', () => {
it('should send a message with the provided l2GasLimit', () => {}) it('should send a message with the provided l2GasLimit')
}) })
}) })
describe('resendMessage', () => { describe('resendMessage', () => {
describe('when the message being resent exists', () => { describe('when the message being resent exists', () => {
it('should resend the message with the new gas limit', () => {}) it('should resend the message with the new gas limit')
}) })
describe('when the message being resent does not exist', () => { describe('when the message being resent does not exist', () => {
it('should throw an error', () => {}) it('should throw an error')
}) })
}) })
describe('finalizeMessage', () => { describe('finalizeMessage', () => {
describe('when the message being finalized exists', () => { describe('when the message being finalized exists', () => {
describe('when the message is ready to be finalized', () => { describe('when the message is ready to be finalized', () => {
it('should finalize the message', () => {}) it('should finalize the message')
}) })
describe('when the message is not ready to be finalized', () => { describe('when the message is not ready to be finalized', () => {
it('should throw an error', () => {}) it('should throw an error')
}) })
describe('when the message has already been finalized', () => { describe('when the message has already been finalized', () => {
it('should throw an error', () => {}) it('should throw an error')
}) })
}) })
describe('when the message being finalized does not exist', () => { describe('when the message being finalized does not exist', () => {
it('should throw an error', () => {}) it('should throw an error')
}) })
}) })
}) })
/* eslint-disable @typescript-eslint/no-empty-function */
import { expect } from './setup' import { expect } from './setup'
import { Provider } from '@ethersproject/abstract-provider' import { Provider } from '@ethersproject/abstract-provider'
import { Contract } from 'ethers' import { Contract } from 'ethers'
...@@ -383,24 +382,26 @@ describe('CrossChainProvider', () => { ...@@ -383,24 +382,26 @@ describe('CrossChainProvider', () => {
describe('getMessagesByAddress', () => { describe('getMessagesByAddress', () => {
describe('when the address has sent messages', () => { describe('when the address has sent messages', () => {
describe('when no direction is specified', () => { describe('when no direction is specified', () => {
it('should find all messages sent by the address', () => {}) it('should find all messages sent by the address')
}) })
describe('when a direction is specified', () => { describe('when a direction is specified', () => {
it('should find all messages only in the given direction', () => {}) it('should find all messages only in the given direction')
}) })
describe('when a block range is specified', () => { describe('when a block range is specified', () => {
it('should find all messages within the block range', () => {}) it('should find all messages within the block range')
}) })
describe('when both a direction and a block range are specified', () => { describe('when both a direction and a block range are specified', () => {
it('should find all messages only in the given direction and within the block range', () => {}) it(
'should find all messages only in the given direction and within the block range'
)
}) })
}) })
describe('when the address has not sent messages', () => { describe('when the address has not sent messages', () => {
it('should find nothing', () => {}) it('should find nothing')
}) })
}) })
...@@ -784,44 +785,44 @@ describe('CrossChainProvider', () => { ...@@ -784,44 +785,44 @@ describe('CrossChainProvider', () => {
describe('getMessageStatus', () => { describe('getMessageStatus', () => {
describe('when the message is an L1 => L2 message', () => { describe('when the message is an L1 => L2 message', () => {
describe('when the message has not been executed on L2 yet', () => { describe('when the message has not been executed on L2 yet', () => {
it('should return a status of UNCONFIRMED_L1_TO_L2_MESSAGE', () => {}) it('should return a status of UNCONFIRMED_L1_TO_L2_MESSAGE')
}) })
describe('when the message has been executed on L2', () => { describe('when the message has been executed on L2', () => {
it('should return a status of RELAYED', () => {}) it('should return a status of RELAYED')
}) })
describe('when the message has been executed but failed', () => { describe('when the message has been executed but failed', () => {
it('should return a status of FAILED_L1_TO_L2_MESSAGE', () => {}) it('should return a status of FAILED_L1_TO_L2_MESSAGE')
}) })
}) })
describe('when the message is an L2 => L1 message', () => { describe('when the message is an L2 => L1 message', () => {
describe('when the message state root has not been published', () => { describe('when the message state root has not been published', () => {
it('should return a status of STATE_ROOT_NOT_PUBLISHED', () => {}) it('should return a status of STATE_ROOT_NOT_PUBLISHED')
}) })
describe('when the message state root is still in the challenge period', () => { describe('when the message state root is still in the challenge period', () => {
it('should return a status of IN_CHALLENGE_PERIOD', () => {}) it('should return a status of IN_CHALLENGE_PERIOD')
}) })
describe('when the message is no longer in the challenge period', () => { describe('when the message is no longer in the challenge period', () => {
describe('when the message has been relayed successfully', () => { describe('when the message has been relayed successfully', () => {
it('should return a status of RELAYED', () => {}) it('should return a status of RELAYED')
}) })
describe('when the message has been relayed but the relay failed', () => { describe('when the message has been relayed but the relay failed', () => {
it('should return a status of READY_FOR_RELAY', () => {}) it('should return a status of READY_FOR_RELAY')
}) })
describe('when the message has not been relayed', () => { describe('when the message has not been relayed', () => {
it('should return a status of READY_FOR_RELAY', () => {}) it('should return a status of READY_FOR_RELAY')
}) })
}) })
}) })
describe('when the message does not exist', () => { describe('when the message does not exist', () => {
it('should throw an error', () => {}) it('should throw an error')
}) })
}) })
...@@ -982,64 +983,157 @@ describe('CrossChainProvider', () => { ...@@ -982,64 +983,157 @@ describe('CrossChainProvider', () => {
// track of // track of
}) })
describe('waitForMessageReciept', () => { describe('waitForMessageReceipt', () => {
let l2Messenger: Contract
let provider: CrossChainProvider
beforeEach(async () => {
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 31337,
contracts: {
l2: {
L2CrossDomainMessenger: l2Messenger.address,
},
},
})
})
describe('when the message receipt already exists', () => { describe('when the message receipt already exists', () => {
it('should immediately return the receipt', () => {}) it('should immediately return the receipt', async () => {
const message = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
const tx = await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
const messageReceipt = await provider.waitForMessageReceipt(message)
expect(messageReceipt.receiptStatus).to.equal(1)
expect(
omit(messageReceipt.transactionReceipt, 'confirmations')
).to.deep.equal(
omit(
await ethers.provider.getTransactionReceipt(tx.hash),
'confirmations'
)
)
})
}) })
describe('when the message receipt does not exist already', () => { describe('when the message receipt does not exist already', () => {
describe('when no extra options are provided', () => { describe('when no extra options are provided', () => {
it('should wait for the receipt to be published', () => {}) it('should wait for the receipt to be published', async () => {
it('should wait forever for the receipt if the receipt is never published', () => {}) const message = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
setTimeout(async () => {
await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
}, 5000)
const tick = Date.now()
const messageReceipt = await provider.waitForMessageReceipt(message)
const tock = Date.now()
expect(messageReceipt.receiptStatus).to.equal(1)
expect(tock - tick).to.be.greaterThan(5000)
})
it('should wait forever for the receipt if the receipt is never published', () => {
// Not sure how to easily test this without introducing some sort of cancellation token
// I don't want the promise to loop forever and make the tests never finish.
})
}) })
describe('when a timeout is provided', () => { describe('when a timeout is provided', () => {
it('should throw an error if the timeout is reached', () => {}) it('should throw an error if the timeout is reached', async () => {
}) const message = {
}) direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
describe('when the message does not exist', () => { await expect(
it('should throw an error', () => {}) provider.waitForMessageReceipt(message, {
timeoutMs: 10000,
})
).to.be.rejectedWith('timed out waiting for message receipt')
})
})
}) })
}) })
describe('estimateL2MessageGasLimit', () => { describe('estimateL2MessageGasLimit', () => {
it('should perform a gas estimation of the L2 action', () => {}) it('should perform a gas estimation of the L2 action')
}) })
describe('estimateMessageWaitTimeBlocks', () => { describe('estimateMessageWaitTimeBlocks', () => {
describe('when the message exists', () => { describe('when the message exists', () => {
describe('when the message is an L1 => L2 message', () => { describe('when the message is an L1 => L2 message', () => {
describe('when the message has not been executed on L2 yet', () => { describe('when the message has not been executed on L2 yet', () => {
it('should return the estimated blocks until the message will be confirmed on L2', () => {}) it(
'should return the estimated blocks until the message will be confirmed on L2'
)
}) })
describe('when the message has been executed on L2', () => { describe('when the message has been executed on L2', () => {
it('should return 0', () => {}) it('should return 0')
}) })
}) })
describe('when the message is an L2 => L1 message', () => { describe('when the message is an L2 => L1 message', () => {
describe('when the state root has not been published', () => { describe('when the state root has not been published', () => {
it('should return the estimated blocks until the state root will be published and pass the challenge period', () => {}) it(
'should return the estimated blocks until the state root will be published and pass the challenge period'
)
}) })
describe('when the state root is within the challenge period', () => { describe('when the state root is within the challenge period', () => {
it('should return the estimated blocks until the state root passes the challenge period', () => {}) it(
'should return the estimated blocks until the state root passes the challenge period'
)
}) })
describe('when the state root passes the challenge period', () => { describe('when the state root passes the challenge period', () => {
it('should return 0', () => {}) it('should return 0')
}) })
}) })
}) })
describe('when the message does not exist', () => { describe('when the message does not exist', () => {
it('should throw an error', () => {}) it('should throw an error')
}) })
}) })
describe('estimateMessageWaitTimeSeconds', () => { describe('estimateMessageWaitTimeSeconds', () => {
it('should be the result of estimateMessageWaitTimeBlocks multiplied by the L1 block time', () => {}) it(
'should be the result of estimateMessageWaitTimeBlocks multiplied by the L1 block time'
)
}) })
}) })
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