Commit 77bd8dfc authored by Kelvin Fichter's avatar Kelvin Fichter

feat(sdk): refactor into single messenger class

parent ceea12f3
......@@ -45,7 +45,7 @@ export class DAIBridgeAdapter extends StandardBridgeAdapter {
type: 'function',
},
],
this.provider.l1Provider
this.messenger.l1Provider
)
const allowedL1Token = await l1Bridge.l1Token()
......
......@@ -10,7 +10,7 @@ import { hexStringEquals } from '@eth-optimism/core-utils'
import {
IBridgeAdapter,
ICrossChainProvider,
ICrossChainMessenger,
NumberLike,
AddressLike,
TokenBridgeMessage,
......@@ -22,7 +22,7 @@ import { toAddress } from '../utils'
* Bridge adapter for any token bridge that uses the standard token bridge interface.
*/
export class StandardBridgeAdapter implements IBridgeAdapter {
public provider: ICrossChainProvider
public messenger: ICrossChainMessenger
public l1Bridge: Contract
public l2Bridge: Contract
......@@ -30,25 +30,25 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
* Creates a StandardBridgeAdapter instance.
*
* @param opts Options for the adapter.
* @param opts.provider Provider used to make queries related to cross-chain interactions.
* @param opts.messenger Provider used to make queries related to cross-chain interactions.
* @param opts.l1Bridge L1 bridge contract.
* @param opts.l2Bridge L2 bridge contract.
*/
constructor(opts: {
provider: ICrossChainProvider
messenger: ICrossChainMessenger
l1Bridge: AddressLike
l2Bridge: AddressLike
}) {
this.provider = opts.provider
this.messenger = opts.messenger
this.l1Bridge = new Contract(
toAddress(opts.l1Bridge),
getContractInterface('L1StandardBridge'),
this.provider.l1Provider
this.messenger.l1Provider
)
this.l2Bridge = new Contract(
toAddress(opts.l2Bridge),
getContractInterface('IL2ERC20Bridge'),
this.provider.l2Provider
this.messenger.l2Provider
)
}
......@@ -167,7 +167,7 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
const contract = new Contract(
toAddress(l2Token),
getContractInterface('L2StandardERC20'),
this.provider.l2Provider
this.messenger.l2Provider
)
// Don't support ETH deposits or withdrawals via this bridge.
......@@ -287,7 +287,7 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l1Provider.estimateGas(
return this.messenger.l1Provider.estimateGas(
await this.populateTransaction.deposit(l1Token, l2Token, amount, opts)
)
},
......@@ -300,7 +300,7 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l2Provider.estimateGas(
return this.messenger.l2Provider.estimateGas(
await this.populateTransaction.withdraw(l1Token, l2Token, amount, opts)
)
},
......
import { ethers, Overrides, Signer, BigNumber } from 'ethers'
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
TransactionRequest,
Provider,
BlockTag,
TransactionReceipt,
TransactionResponse,
TransactionRequest,
} from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { ethers, BigNumber, Overrides } from 'ethers'
import { sleep, remove0x } from '@eth-optimism/core-utils'
import { predeploys } from '@eth-optimism/contracts'
import {
CrossChainMessageRequest,
ICrossChainMessenger,
ICrossChainProvider,
OEContracts,
OEContractsLike,
MessageLike,
NumberLike,
MessageRequestLike,
TransactionLike,
AddressLike,
NumberLike,
SignerOrProviderLike,
CrossChainMessage,
CrossChainMessageRequest,
CrossChainMessageProof,
MessageDirection,
MessageStatus,
TokenBridgeMessage,
MessageReceipt,
MessageReceiptStatus,
BridgeAdapterData,
BridgeAdapters,
StateRoot,
StateRootBatch,
IBridgeAdapter,
} from './interfaces'
import {
toSignerOrProvider,
toBigNumber,
toTransactionHash,
DeepPartial,
getAllOEContracts,
getBridgeAdapters,
hashCrossChainMessage,
makeMerkleTreeProof,
makeStateTrieProof,
encodeCrossChainMessage,
} from './utils'
export class CrossChainMessenger implements ICrossChainMessenger {
provider: ICrossChainProvider
l1Signer: Signer
l2Signer: Signer
public l1SignerOrProvider: Signer | Provider
public l2SignerOrProvider: Signer | Provider
public l1ChainId: number
public contracts: OEContracts
public bridges: BridgeAdapters
/**
* Creates a new CrossChainMessenger instance.
* Creates a new CrossChainProvider instance.
*
* @param opts Options for the messenger.
* @param opts.provider CrossChainProvider to use to send messages.
* @param opts.l1Signer Signer to use to send messages on L1.
* @param opts.l2Signer Signer to use to send messages on L2.
* @param opts Options for the provider.
* @param opts.l1SignerOrProvider Signer or Provider for the L1 chain, or a JSON-RPC url.
* @param opts.l1SignerOrProvider Signer or Provider for the L2 chain, or a JSON-RPC url.
* @param opts.l1ChainId Chain ID for the L1 chain.
* @param opts.contracts Optional contract address overrides.
* @param opts.bridges Optional bridge address list.
*/
constructor(opts: {
provider: ICrossChainProvider
l1Signer: Signer
l2Signer: Signer
l1SignerOrProvider: SignerOrProviderLike
l2SignerOrProvider: SignerOrProviderLike
l1ChainId: NumberLike
contracts?: DeepPartial<OEContractsLike>
bridges?: BridgeAdapterData
}) {
this.provider = opts.provider
this.l1Signer = opts.l1Signer
this.l2Signer = opts.l2Signer
this.l1SignerOrProvider = toSignerOrProvider(opts.l1SignerOrProvider)
this.l2SignerOrProvider = toSignerOrProvider(opts.l2SignerOrProvider)
this.l1ChainId = toBigNumber(opts.l1ChainId).toNumber()
this.contracts = getAllOEContracts(this.l1ChainId, {
l1SignerOrProvider: this.l1SignerOrProvider,
l2SignerOrProvider: this.l2SignerOrProvider,
overrides: opts.contracts,
})
this.bridges = getBridgeAdapters(this.l1ChainId, this, {
overrides: opts.bridges,
})
}
get l1Provider(): Provider {
if (Provider.isProvider(this.l1SignerOrProvider)) {
return this.l1SignerOrProvider
} else {
return this.l1SignerOrProvider.provider
}
}
get l2Provider(): Provider {
if (Provider.isProvider(this.l2SignerOrProvider)) {
return this.l2SignerOrProvider
} else {
return this.l2SignerOrProvider.provider
}
}
get l1Signer(): Signer {
if (Provider.isProvider(this.l1SignerOrProvider)) {
throw new Error(`messenger has no L1 signer`)
} else {
return this.l1SignerOrProvider
}
}
get l2Signer(): Signer {
if (Provider.isProvider(this.l2SignerOrProvider)) {
throw new Error(`messenger has no L2 signer`)
} else {
return this.l2SignerOrProvider
}
}
public async getMessagesByTransaction(
transaction: TransactionLike,
opts: {
direction?: MessageDirection
} = {}
): Promise<CrossChainMessage[]> {
const txHash = toTransactionHash(transaction)
let receipt: TransactionReceipt
if (opts.direction !== undefined) {
// Get the receipt for the requested direction.
if (opts.direction === MessageDirection.L1_TO_L2) {
receipt = await this.l1Provider.getTransactionReceipt(txHash)
} else {
receipt = await this.l2Provider.getTransactionReceipt(txHash)
}
} else {
// Try both directions, starting with L1 => L2.
receipt = await this.l1Provider.getTransactionReceipt(txHash)
if (receipt) {
opts.direction = MessageDirection.L1_TO_L2
} else {
receipt = await this.l2Provider.getTransactionReceipt(txHash)
opts.direction = MessageDirection.L2_TO_L1
}
}
if (!receipt) {
throw new Error(`unable to find transaction receipt for ${txHash}`)
}
// By this point opts.direction will always be defined.
const messenger =
opts.direction === MessageDirection.L1_TO_L2
? this.contracts.l1.L1CrossDomainMessenger
: this.contracts.l2.L2CrossDomainMessenger
return receipt.logs
.filter((log) => {
// Only look at logs emitted by the messenger address
return log.address === messenger.address
})
.filter((log) => {
// Only look at SentMessage logs specifically
const parsed = messenger.interface.parseLog(log)
return parsed.name === 'SentMessage'
})
.map((log) => {
// Convert each SentMessage log into a message object
const parsed = messenger.interface.parseLog(log)
return {
direction: opts.direction,
target: parsed.args.target,
sender: parsed.args.sender,
message: parsed.args.message,
messageNonce: parsed.args.messageNonce,
gasLimit: parsed.args.gasLimit,
logIndex: log.logIndex,
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
}
})
}
public async getMessagesByAddress(
address: AddressLike,
opts?: {
direction?: MessageDirection
fromBlock?: NumberLike
toBlock?: NumberLike
}
): Promise<CrossChainMessage[]> {
throw new Error('Not implemented')
}
public async getBridgeForTokenPair(
l1Token: AddressLike,
l2Token: AddressLike
): Promise<IBridgeAdapter> {
const bridges: IBridgeAdapter[] = []
for (const bridge of Object.values(this.bridges)) {
if (await bridge.supportsTokenPair(l1Token, l2Token)) {
bridges.push(bridge)
}
}
if (bridges.length === 0) {
throw new Error(`no supported bridge for token pair`)
}
if (bridges.length > 1) {
throw new Error(`found more than one bridge for token pair`)
}
return bridges[0]
}
public async getTokenBridgeMessagesByAddress(
address: AddressLike,
opts: {
direction?: MessageDirection
} = {}
): Promise<TokenBridgeMessage[]> {
return (
await Promise.all(
Object.values(this.bridges).map(async (bridge) => {
return bridge.getTokenBridgeMessagesByAddress(address, opts)
})
)
).reduce((acc, val) => {
return acc.concat(val)
}, [])
}
public async getDepositsByAddress(
address: AddressLike,
opts: {
fromBlock?: BlockTag
toBlock?: BlockTag
} = {}
): Promise<TokenBridgeMessage[]> {
return (
await Promise.all(
Object.values(this.bridges).map(async (bridge) => {
return bridge.getDepositsByAddress(address, opts)
})
)
).reduce((acc, val) => {
return acc.concat(val)
}, [])
}
public async getWithdrawalsByAddress(
address: AddressLike,
opts: {
fromBlock?: BlockTag
toBlock?: BlockTag
} = {}
): Promise<TokenBridgeMessage[]> {
return (
await Promise.all(
Object.values(this.bridges).map(async (bridge) => {
return bridge.getWithdrawalsByAddress(address, opts)
})
)
).reduce((acc, val) => {
return acc.concat(val)
}, [])
}
public async toCrossChainMessage(
message: MessageLike
): Promise<CrossChainMessage> {
// TODO: Convert these checks into proper type checks.
if ((message as CrossChainMessage).message) {
return message as CrossChainMessage
} else if (
(message as TokenBridgeMessage).l1Token &&
(message as TokenBridgeMessage).l2Token &&
(message as TokenBridgeMessage).transactionHash
) {
const messages = await this.getMessagesByTransaction(
(message as TokenBridgeMessage).transactionHash
)
// The `messages` object corresponds to a list of SentMessage events that were triggered by
// the same transaction. We want to find the specific SentMessage event that corresponds to
// the TokenBridgeMessage (either a ETHDepositInitiated, ERC20DepositInitiated, or
// WithdrawalInitiated event). We expect the behavior of bridge contracts to be that these
// TokenBridgeMessage events are triggered and then a SentMessage event is triggered. Our
// goal here is therefore to find the first SentMessage event that comes after the input
// event.
const found = messages
.sort((a, b) => {
// Sort all messages in ascending order by log index.
return a.logIndex - b.logIndex
})
.find((m) => {
return m.logIndex > (message as TokenBridgeMessage).logIndex
})
if (!found) {
throw new Error(`could not find SentMessage event for message`)
}
return found
} else {
// TODO: Explicit TransactionLike check and throw if not TransactionLike
const messages = await this.getMessagesByTransaction(
message as TransactionLike
)
// We only want to treat TransactionLike objects as MessageLike if they only emit a single
// message (very common). It's unintuitive to treat a TransactionLike as a MessageLike if
// they emit more than one message (which message do you pick?), so we throw an error.
if (messages.length !== 1) {
throw new Error(`expected 1 message, got ${messages.length}`)
}
return messages[0]
}
}
public async getMessageStatus(message: MessageLike): Promise<MessageStatus> {
const resolved = await this.toCrossChainMessage(message)
const receipt = await this.getMessageReceipt(resolved)
if (resolved.direction === MessageDirection.L1_TO_L2) {
if (receipt === null) {
return MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE
} else {
if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) {
return MessageStatus.RELAYED
} else {
return MessageStatus.FAILED_L1_TO_L2_MESSAGE
}
}
} else {
if (receipt === null) {
const stateRoot = await this.getMessageStateRoot(resolved)
if (stateRoot === null) {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
} else {
const challengePeriod = await this.getChallengePeriodSeconds()
const targetBlock = await this.l1Provider.getBlock(
stateRoot.batch.blockNumber
)
const latestBlock = await this.l1Provider.getBlock('latest')
if (targetBlock.timestamp + challengePeriod > latestBlock.timestamp) {
return MessageStatus.IN_CHALLENGE_PERIOD
} else {
return MessageStatus.READY_FOR_RELAY
}
}
} else {
if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) {
return MessageStatus.RELAYED
} else {
return MessageStatus.READY_FOR_RELAY
}
}
}
}
public async getMessageReceipt(
message: MessageLike
): Promise<MessageReceipt> {
const resolved = await this.toCrossChainMessage(message)
const messageHash = hashCrossChainMessage(resolved)
// Here we want the messenger that will receive the message, not the one that sent it.
const messenger =
resolved.direction === MessageDirection.L1_TO_L2
? this.contracts.l2.L2CrossDomainMessenger
: this.contracts.l1.L1CrossDomainMessenger
const relayedMessageEvents = await messenger.queryFilter(
messenger.filters.RelayedMessage(messageHash)
)
// Great, we found the message. Convert it into a transaction receipt.
if (relayedMessageEvents.length === 1) {
return {
receiptStatus: MessageReceiptStatus.RELAYED_SUCCEEDED,
transactionReceipt:
await relayedMessageEvents[0].getTransactionReceipt(),
}
} else if (relayedMessageEvents.length > 1) {
// Should never happen!
throw new Error(`multiple successful relays for message`)
}
// We didn't find a transaction that relayed the message. We now attempt to find
// FailedRelayedMessage events instead.
const failedRelayedMessageEvents = await messenger.queryFilter(
messenger.filters.FailedRelayedMessage(messageHash)
)
// A transaction can fail to be relayed multiple times. We'll always return the last
// transaction that attempted to relay the message.
// TODO: Is this the best way to handle this?
if (failedRelayedMessageEvents.length > 0) {
return {
receiptStatus: MessageReceiptStatus.RELAYED_FAILED,
transactionReceipt: await failedRelayedMessageEvents[
failedRelayedMessageEvents.length - 1
].getTransactionReceipt(),
}
}
// TODO: If the user doesn't provide enough gas then there's a chance that FailedRelayedMessage
// will never be triggered. We should probably fix this at the contract level by requiring a
// minimum amount of input gas and designing the contracts such that the gas will always be
// enough to trigger the event. However, for now we need a temporary way to find L1 => L2
// transactions that fail but don't alert us because they didn't provide enough gas.
// TODO: Talk with the systems and protocol team about coordinating a hard fork that fixes this
// on both L1 and L2.
// Just return null if we didn't find a receipt. Slightly nicer than throwing an error.
return null
}
public async waitForMessageReceipt(
message: MessageLike,
opts: {
confirmations?: number
pollIntervalMs?: number
timeoutMs?: number
} = {}
): Promise<MessageReceipt> {
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(
message: MessageRequestLike,
opts?: {
bufferPercent?: number
from?: string
}
): Promise<BigNumber> {
let resolved: CrossChainMessage | CrossChainMessageRequest
let from: string
if ((message as CrossChainMessage).messageNonce === undefined) {
resolved = message as CrossChainMessageRequest
from = opts?.from
} else {
resolved = await this.toCrossChainMessage(message as MessageLike)
from = opts?.from || (resolved as CrossChainMessage).sender
}
// L2 message gas estimation is only used for L1 => L2 messages.
if (resolved.direction === MessageDirection.L2_TO_L1) {
throw new Error(`cannot estimate gas limit for L2 => L1 message`)
}
const estimate = await this.l2Provider.estimateGas({
from,
to: resolved.target,
data: resolved.message,
})
// Return the estimate plus a buffer of 20% just in case.
const bufferPercent = opts?.bufferPercent || 20
return estimate.mul(100 + bufferPercent).div(100)
}
public async estimateMessageWaitTimeSeconds(
message: MessageLike
): Promise<number> {
throw new Error('Not implemented')
}
public async getChallengePeriodSeconds(): Promise<number> {
const challengePeriod =
await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW()
return challengePeriod.toNumber()
}
public async getMessageStateRoot(
message: MessageLike
): Promise<StateRoot | null> {
const resolved = await this.toCrossChainMessage(message)
// State roots are only a thing for L2 to L1 messages.
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`cannot get a state root for an L1 to L2 message`)
}
// We need the block number of the transaction that triggered the message so we can look up the
// state root batch that corresponds to that block number.
const messageTxReceipt = await this.l2Provider.getTransactionReceipt(
resolved.transactionHash
)
// Every block has exactly one transaction in it. Since there's a genesis block, the
// transaction index will always be one less than the block number.
const messageTxIndex = messageTxReceipt.blockNumber - 1
// Pull down the state root batch, we'll try to pick out the specific state root that
// corresponds to our message.
const stateRootBatch = await this.getStateRootBatchByTransactionIndex(
messageTxIndex
)
// No state root batch, no state root.
if (stateRootBatch === null) {
return null
}
// We have a state root batch, now we need to find the specific state root for our transaction.
// First we need to figure out the index of the state root within the batch we found. This is
// going to be the original transaction index offset by the total number of previous state
// roots.
const indexInBatch =
messageTxIndex - stateRootBatch.header.prevTotalElements.toNumber()
// Just a sanity check.
if (stateRootBatch.stateRoots.length <= indexInBatch) {
// Should never happen!
throw new Error(`state root does not exist in batch`)
}
return {
stateRoot: stateRootBatch.stateRoots[indexInBatch],
stateRootIndexInBatch: indexInBatch,
batch: stateRootBatch,
}
}
public async getStateBatchAppendedEventByBatchIndex(
batchIndex: number
): Promise<ethers.Event | null> {
const events = await this.contracts.l1.StateCommitmentChain.queryFilter(
this.contracts.l1.StateCommitmentChain.filters.StateBatchAppended(
batchIndex
)
)
if (events.length === 0) {
return null
} else if (events.length > 1) {
// Should never happen!
throw new Error(`found more than one StateBatchAppended event`)
} else {
return events[0]
}
}
public async getStateBatchAppendedEventByTransactionIndex(
transactionIndex: number
): Promise<ethers.Event | null> {
const isEventHi = (event: ethers.Event, index: number) => {
const prevTotalElements = event.args._prevTotalElements.toNumber()
return index < prevTotalElements
}
const isEventLo = (event: ethers.Event, index: number) => {
const prevTotalElements = event.args._prevTotalElements.toNumber()
const batchSize = event.args._batchSize.toNumber()
return index >= prevTotalElements + batchSize
}
const totalBatches: ethers.BigNumber =
await this.contracts.l1.StateCommitmentChain.getTotalBatches()
if (totalBatches.eq(0)) {
return null
}
let lowerBound = 0
let upperBound = totalBatches.toNumber() - 1
let batchEvent: ethers.Event | null =
await this.getStateBatchAppendedEventByBatchIndex(upperBound)
if (isEventLo(batchEvent, transactionIndex)) {
// Upper bound is too low, means this transaction doesn't have a corresponding state batch yet.
return null
} else if (!isEventHi(batchEvent, transactionIndex)) {
// Upper bound is not too low and also not too high. This means the upper bound event is the
// one we're looking for! Return it.
return batchEvent
}
// Binary search to find the right event. The above checks will guarantee that the event does
// exist and that we'll find it during this search.
while (lowerBound < upperBound) {
const middleOfBounds = Math.floor((lowerBound + upperBound) / 2)
batchEvent = await this.getStateBatchAppendedEventByBatchIndex(
middleOfBounds
)
if (isEventHi(batchEvent, transactionIndex)) {
upperBound = middleOfBounds
} else if (isEventLo(batchEvent, transactionIndex)) {
lowerBound = middleOfBounds
} else {
break
}
}
return batchEvent
}
public async getStateRootBatchByTransactionIndex(
transactionIndex: number
): Promise<StateRootBatch | null> {
const stateBatchAppendedEvent =
await this.getStateBatchAppendedEventByTransactionIndex(transactionIndex)
if (stateBatchAppendedEvent === null) {
return null
}
const stateBatchTransaction = await stateBatchAppendedEvent.getTransaction()
const [stateRoots] =
this.contracts.l1.StateCommitmentChain.interface.decodeFunctionData(
'appendStateBatch',
stateBatchTransaction.data
)
return {
blockNumber: stateBatchAppendedEvent.blockNumber,
stateRoots,
header: {
batchIndex: stateBatchAppendedEvent.args._batchIndex,
batchRoot: stateBatchAppendedEvent.args._batchRoot,
batchSize: stateBatchAppendedEvent.args._batchSize,
prevTotalElements: stateBatchAppendedEvent.args._prevTotalElements,
extraData: stateBatchAppendedEvent.args._extraData,
},
}
}
public async getMessageProof(
message: MessageLike
): Promise<CrossChainMessageProof> {
const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`can only generate proofs for L2 to L1 messages`)
}
const stateRoot = await this.getMessageStateRoot(resolved)
if (stateRoot === null) {
throw new Error(`state root for message not yet published`)
}
// We need to calculate the specific storage slot that demonstrates that this message was
// actually included in the L2 chain. The following calculation is based on the fact that
// messages are stored in the following mapping on L2:
// https://github.com/ethereum-optimism/optimism/blob/c84d3450225306abbb39b4e7d6d82424341df2be/packages/contracts/contracts/L2/predeploys/OVM_L2ToL1MessagePasser.sol#L23
// You can read more about how Solidity storage slots are computed for mappings here:
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
const messageSlot = ethers.utils.keccak256(
ethers.utils.keccak256(
encodeCrossChainMessage(resolved) +
remove0x(this.contracts.l2.L2CrossDomainMessenger.address)
) + '00'.repeat(32)
)
const stateTrieProof = await makeStateTrieProof(
this.l2Provider as any,
resolved.blockNumber,
this.contracts.l2.OVM_L2ToL1MessagePasser.address,
messageSlot
)
return {
stateRoot: stateRoot.stateRoot,
stateRootBatchHeader: stateRoot.batch.header,
stateRootProof: {
index: stateRoot.stateRootIndexInBatch,
siblings: makeMerkleTreeProof(
stateRoot.batch.stateRoots,
stateRoot.stateRootIndexInBatch
),
},
stateTrieWitness: stateTrieProof.accountProof,
storageTrieWitness: stateTrieProof.storageProof,
}
}
public async sendMessage(
message: CrossChainMessageRequest,
opts?: {
signer?: Signer
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> {
const tx = await this.populateTransaction.sendMessage(message, opts)
if (message.direction === MessageDirection.L1_TO_L2) {
return this.l1Signer.sendTransaction(tx)
return (opts?.signer || this.l1Signer).sendTransaction(tx)
} else {
return this.l2Signer.sendTransaction(tx)
return (opts?.signer || this.l2Signer).sendTransaction(tx)
}
}
......@@ -57,10 +708,11 @@ export class CrossChainMessenger implements ICrossChainMessenger {
message: MessageLike,
messageGasLimit: NumberLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse> {
return this.l1Signer.sendTransaction(
return (opts?.signer || this.l1Signer).sendTransaction(
await this.populateTransaction.resendMessage(
message,
messageGasLimit,
......@@ -72,10 +724,11 @@ export class CrossChainMessenger implements ICrossChainMessenger {
public async finalizeMessage(
message: MessageLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse> {
return this.l1Signer.sendTransaction(
return (opts?.signer || this.l1Signer).sendTransaction(
await this.populateTransaction.finalizeMessage(message, opts)
)
}
......@@ -83,11 +736,12 @@ export class CrossChainMessenger implements ICrossChainMessenger {
public async depositETH(
amount: NumberLike,
opts?: {
signer?: Signer
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> {
return this.l1Signer.sendTransaction(
return (opts?.signer || this.l1Signer).sendTransaction(
await this.populateTransaction.depositETH(amount, opts)
)
}
......@@ -95,10 +749,11 @@ export class CrossChainMessenger implements ICrossChainMessenger {
public async withdrawETH(
amount: NumberLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse> {
return this.l2Signer.sendTransaction(
return (opts?.signer || this.l2Signer).sendTransaction(
await this.populateTransaction.withdrawETH(amount, opts)
)
}
......@@ -108,11 +763,12 @@ export class CrossChainMessenger implements ICrossChainMessenger {
l2Token: AddressLike,
amount: NumberLike,
opts?: {
signer?: Signer
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> {
return this.l1Signer.sendTransaction(
return (opts?.signer || this.l1Signer).sendTransaction(
await this.populateTransaction.depositERC20(
l1Token,
l2Token,
......@@ -127,10 +783,11 @@ export class CrossChainMessenger implements ICrossChainMessenger {
l2Token: AddressLike,
amount: NumberLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse> {
return this.l2Signer.sendTransaction(
return (opts?.signer || this.l2Signer).sendTransaction(
await this.populateTransaction.withdrawERC20(
l1Token,
l2Token,
......@@ -149,15 +806,14 @@ export class CrossChainMessenger implements ICrossChainMessenger {
}
): Promise<TransactionRequest> => {
if (message.direction === MessageDirection.L1_TO_L2) {
return this.provider.contracts.l1.L1CrossDomainMessenger.populateTransaction.sendMessage(
return this.contracts.l1.L1CrossDomainMessenger.populateTransaction.sendMessage(
message.target,
message.message,
opts?.l2GasLimit ||
(await this.provider.estimateL2MessageGasLimit(message)),
opts?.l2GasLimit || (await this.estimateL2MessageGasLimit(message)),
opts?.overrides || {}
)
} else {
return this.provider.contracts.l2.L2CrossDomainMessenger.populateTransaction.sendMessage(
return this.contracts.l2.L2CrossDomainMessenger.populateTransaction.sendMessage(
message.target,
message.message,
0, // Gas limit goes unused when sending from L2 to L1
......@@ -173,12 +829,12 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
const resolved = await this.provider.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L2_TO_L1) {
throw new Error(`cannot resend L2 to L1 message`)
}
return this.provider.contracts.l1.L1CrossDomainMessenger.populateTransaction.replayMessage(
return this.contracts.l1.L1CrossDomainMessenger.populateTransaction.replayMessage(
resolved.target,
resolved.sender,
resolved.message,
......@@ -195,13 +851,13 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
const resolved = await this.provider.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`cannot finalize L1 to L2 message`)
}
const proof = await this.provider.getMessageProof(resolved)
return this.provider.contracts.l1.L1CrossDomainMessenger.populateTransaction.relayMessage(
const proof = await this.getMessageProof(resolved)
return this.contracts.l1.L1CrossDomainMessenger.populateTransaction.relayMessage(
resolved.target,
resolved.sender,
resolved.message,
......@@ -218,7 +874,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
return this.provider.bridges.ETH.populateTransaction.deposit(
return this.bridges.ETH.populateTransaction.deposit(
ethers.constants.AddressZero,
predeploys.OVM_ETH,
amount,
......@@ -232,7 +888,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
return this.provider.bridges.ETH.populateTransaction.withdraw(
return this.bridges.ETH.populateTransaction.withdraw(
ethers.constants.AddressZero,
predeploys.OVM_ETH,
amount,
......@@ -249,7 +905,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
const bridge = await this.provider.getBridgeForTokenPair(l1Token, l2Token)
const bridge = await this.getBridgeForTokenPair(l1Token, l2Token)
return bridge.populateTransaction.deposit(l1Token, l2Token, amount, opts)
},
......@@ -261,7 +917,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
const bridge = await this.provider.getBridgeForTokenPair(l1Token, l2Token)
const bridge = await this.getBridgeForTokenPair(l1Token, l2Token)
return bridge.populateTransaction.withdraw(l1Token, l2Token, amount, opts)
},
}
......@@ -276,9 +932,9 @@ export class CrossChainMessenger implements ICrossChainMessenger {
): Promise<BigNumber> => {
const tx = await this.populateTransaction.sendMessage(message, opts)
if (message.direction === MessageDirection.L1_TO_L2) {
return this.provider.l1Provider.estimateGas(tx)
return this.l1Provider.estimateGas(tx)
} else {
return this.provider.l2Provider.estimateGas(tx)
return this.l2Provider.estimateGas(tx)
}
},
......@@ -289,7 +945,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l1Provider.estimateGas(
return this.l1Provider.estimateGas(
await this.populateTransaction.resendMessage(
message,
messageGasLimit,
......@@ -304,7 +960,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l1Provider.estimateGas(
return this.l1Provider.estimateGas(
await this.populateTransaction.finalizeMessage(message, opts)
)
},
......@@ -316,7 +972,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l1Provider.estimateGas(
return this.l1Provider.estimateGas(
await this.populateTransaction.depositETH(amount, opts)
)
},
......@@ -327,7 +983,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l2Provider.estimateGas(
return this.l2Provider.estimateGas(
await this.populateTransaction.withdrawETH(amount, opts)
)
},
......@@ -341,7 +997,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l1Provider.estimateGas(
return this.l1Provider.estimateGas(
await this.populateTransaction.depositERC20(
l1Token,
l2Token,
......@@ -359,7 +1015,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l2Provider.estimateGas(
return this.l2Provider.estimateGas(
await this.populateTransaction.withdrawERC20(
l1Token,
l2Token,
......
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
Provider,
BlockTag,
TransactionReceipt,
} from '@ethersproject/abstract-provider'
import { ethers, BigNumber } from 'ethers'
import { sleep, remove0x } from '@eth-optimism/core-utils'
import {
ICrossChainProvider,
OEContracts,
OEContractsLike,
MessageLike,
MessageRequestLike,
TransactionLike,
AddressLike,
NumberLike,
ProviderLike,
CrossChainMessage,
CrossChainMessageRequest,
CrossChainMessageProof,
MessageDirection,
MessageStatus,
TokenBridgeMessage,
MessageReceipt,
MessageReceiptStatus,
BridgeAdapterData,
BridgeAdapters,
StateRoot,
StateRootBatch,
IBridgeAdapter,
} from './interfaces'
import {
toProvider,
toBigNumber,
toTransactionHash,
DeepPartial,
getAllOEContracts,
getBridgeAdapters,
hashCrossChainMessage,
makeMerkleTreeProof,
makeStateTrieProof,
encodeCrossChainMessage,
} from './utils'
export class CrossChainProvider implements ICrossChainProvider {
public l1Provider: Provider
public l2Provider: Provider
public l1ChainId: number
public contracts: OEContracts
public bridges: BridgeAdapters
/**
* Creates a new CrossChainProvider instance.
*
* @param opts Options for the provider.
* @param opts.l1Provider Provider for the L1 chain, or a JSON-RPC url.
* @param opts.l2Provider Provider for the L2 chain, or a JSON-RPC url.
* @param opts.l1ChainId Chain ID for the L1 chain.
* @param opts.contracts Optional contract address overrides.
* @param opts.bridges Optional bridge address list.
*/
constructor(opts: {
l1Provider: ProviderLike
l2Provider: ProviderLike
l1ChainId: NumberLike
contracts?: DeepPartial<OEContractsLike>
bridges?: BridgeAdapterData
}) {
this.l1Provider = toProvider(opts.l1Provider)
this.l2Provider = toProvider(opts.l2Provider)
this.l1ChainId = toBigNumber(opts.l1ChainId).toNumber()
this.contracts = getAllOEContracts(this.l1ChainId, {
l1SignerOrProvider: this.l1Provider,
l2SignerOrProvider: this.l2Provider,
overrides: opts.contracts,
})
this.bridges = getBridgeAdapters(this.l1ChainId, this, {
overrides: opts.bridges,
})
}
public async getMessagesByTransaction(
transaction: TransactionLike,
opts: {
direction?: MessageDirection
} = {}
): Promise<CrossChainMessage[]> {
const txHash = toTransactionHash(transaction)
let receipt: TransactionReceipt
if (opts.direction !== undefined) {
// Get the receipt for the requested direction.
if (opts.direction === MessageDirection.L1_TO_L2) {
receipt = await this.l1Provider.getTransactionReceipt(txHash)
} else {
receipt = await this.l2Provider.getTransactionReceipt(txHash)
}
} else {
// Try both directions, starting with L1 => L2.
receipt = await this.l1Provider.getTransactionReceipt(txHash)
if (receipt) {
opts.direction = MessageDirection.L1_TO_L2
} else {
receipt = await this.l2Provider.getTransactionReceipt(txHash)
opts.direction = MessageDirection.L2_TO_L1
}
}
if (!receipt) {
throw new Error(`unable to find transaction receipt for ${txHash}`)
}
// By this point opts.direction will always be defined.
const messenger =
opts.direction === MessageDirection.L1_TO_L2
? this.contracts.l1.L1CrossDomainMessenger
: this.contracts.l2.L2CrossDomainMessenger
return receipt.logs
.filter((log) => {
// Only look at logs emitted by the messenger address
return log.address === messenger.address
})
.filter((log) => {
// Only look at SentMessage logs specifically
const parsed = messenger.interface.parseLog(log)
return parsed.name === 'SentMessage'
})
.map((log) => {
// Convert each SentMessage log into a message object
const parsed = messenger.interface.parseLog(log)
return {
direction: opts.direction,
target: parsed.args.target,
sender: parsed.args.sender,
message: parsed.args.message,
messageNonce: parsed.args.messageNonce,
gasLimit: parsed.args.gasLimit,
logIndex: log.logIndex,
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
}
})
}
public async getMessagesByAddress(
address: AddressLike,
opts?: {
direction?: MessageDirection
fromBlock?: NumberLike
toBlock?: NumberLike
}
): Promise<CrossChainMessage[]> {
throw new Error('Not implemented')
}
public async getBridgeForTokenPair(
l1Token: AddressLike,
l2Token: AddressLike
): Promise<IBridgeAdapter> {
const bridges: IBridgeAdapter[] = []
for (const bridge of Object.values(this.bridges)) {
if (await bridge.supportsTokenPair(l1Token, l2Token)) {
bridges.push(bridge)
}
}
if (bridges.length === 0) {
throw new Error(`no supported bridge for token pair`)
}
if (bridges.length > 1) {
throw new Error(`found more than one bridge for token pair`)
}
return bridges[0]
}
public async getTokenBridgeMessagesByAddress(
address: AddressLike,
opts: {
direction?: MessageDirection
} = {}
): Promise<TokenBridgeMessage[]> {
return (
await Promise.all(
Object.values(this.bridges).map(async (bridge) => {
return bridge.getTokenBridgeMessagesByAddress(address, opts)
})
)
).reduce((acc, val) => {
return acc.concat(val)
}, [])
}
public async getDepositsByAddress(
address: AddressLike,
opts: {
fromBlock?: BlockTag
toBlock?: BlockTag
} = {}
): Promise<TokenBridgeMessage[]> {
return (
await Promise.all(
Object.values(this.bridges).map(async (bridge) => {
return bridge.getDepositsByAddress(address, opts)
})
)
).reduce((acc, val) => {
return acc.concat(val)
}, [])
}
public async getWithdrawalsByAddress(
address: AddressLike,
opts: {
fromBlock?: BlockTag
toBlock?: BlockTag
} = {}
): Promise<TokenBridgeMessage[]> {
return (
await Promise.all(
Object.values(this.bridges).map(async (bridge) => {
return bridge.getWithdrawalsByAddress(address, opts)
})
)
).reduce((acc, val) => {
return acc.concat(val)
}, [])
}
public async toCrossChainMessage(
message: MessageLike
): Promise<CrossChainMessage> {
// TODO: Convert these checks into proper type checks.
if ((message as CrossChainMessage).message) {
return message as CrossChainMessage
} else if (
(message as TokenBridgeMessage).l1Token &&
(message as TokenBridgeMessage).l2Token &&
(message as TokenBridgeMessage).transactionHash
) {
const messages = await this.getMessagesByTransaction(
(message as TokenBridgeMessage).transactionHash
)
// The `messages` object corresponds to a list of SentMessage events that were triggered by
// the same transaction. We want to find the specific SentMessage event that corresponds to
// the TokenBridgeMessage (either a ETHDepositInitiated, ERC20DepositInitiated, or
// WithdrawalInitiated event). We expect the behavior of bridge contracts to be that these
// TokenBridgeMessage events are triggered and then a SentMessage event is triggered. Our
// goal here is therefore to find the first SentMessage event that comes after the input
// event.
const found = messages
.sort((a, b) => {
// Sort all messages in ascending order by log index.
return a.logIndex - b.logIndex
})
.find((m) => {
return m.logIndex > (message as TokenBridgeMessage).logIndex
})
if (!found) {
throw new Error(`could not find SentMessage event for message`)
}
return found
} else {
// TODO: Explicit TransactionLike check and throw if not TransactionLike
const messages = await this.getMessagesByTransaction(
message as TransactionLike
)
// We only want to treat TransactionLike objects as MessageLike if they only emit a single
// message (very common). It's unintuitive to treat a TransactionLike as a MessageLike if
// they emit more than one message (which message do you pick?), so we throw an error.
if (messages.length !== 1) {
throw new Error(`expected 1 message, got ${messages.length}`)
}
return messages[0]
}
}
public async getMessageStatus(message: MessageLike): Promise<MessageStatus> {
const resolved = await this.toCrossChainMessage(message)
const receipt = await this.getMessageReceipt(resolved)
if (resolved.direction === MessageDirection.L1_TO_L2) {
if (receipt === null) {
return MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE
} else {
if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) {
return MessageStatus.RELAYED
} else {
return MessageStatus.FAILED_L1_TO_L2_MESSAGE
}
}
} else {
if (receipt === null) {
const stateRoot = await this.getMessageStateRoot(resolved)
if (stateRoot === null) {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
} else {
const challengePeriod = await this.getChallengePeriodSeconds()
const targetBlock = await this.l1Provider.getBlock(
stateRoot.batch.blockNumber
)
const latestBlock = await this.l1Provider.getBlock('latest')
if (targetBlock.timestamp + challengePeriod > latestBlock.timestamp) {
return MessageStatus.IN_CHALLENGE_PERIOD
} else {
return MessageStatus.READY_FOR_RELAY
}
}
} else {
if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) {
return MessageStatus.RELAYED
} else {
return MessageStatus.READY_FOR_RELAY
}
}
}
}
public async getMessageReceipt(
message: MessageLike
): Promise<MessageReceipt> {
const resolved = await this.toCrossChainMessage(message)
const messageHash = hashCrossChainMessage(resolved)
// Here we want the messenger that will receive the message, not the one that sent it.
const messenger =
resolved.direction === MessageDirection.L1_TO_L2
? this.contracts.l2.L2CrossDomainMessenger
: this.contracts.l1.L1CrossDomainMessenger
const relayedMessageEvents = await messenger.queryFilter(
messenger.filters.RelayedMessage(messageHash)
)
// Great, we found the message. Convert it into a transaction receipt.
if (relayedMessageEvents.length === 1) {
return {
receiptStatus: MessageReceiptStatus.RELAYED_SUCCEEDED,
transactionReceipt:
await relayedMessageEvents[0].getTransactionReceipt(),
}
} else if (relayedMessageEvents.length > 1) {
// Should never happen!
throw new Error(`multiple successful relays for message`)
}
// We didn't find a transaction that relayed the message. We now attempt to find
// FailedRelayedMessage events instead.
const failedRelayedMessageEvents = await messenger.queryFilter(
messenger.filters.FailedRelayedMessage(messageHash)
)
// A transaction can fail to be relayed multiple times. We'll always return the last
// transaction that attempted to relay the message.
// TODO: Is this the best way to handle this?
if (failedRelayedMessageEvents.length > 0) {
return {
receiptStatus: MessageReceiptStatus.RELAYED_FAILED,
transactionReceipt: await failedRelayedMessageEvents[
failedRelayedMessageEvents.length - 1
].getTransactionReceipt(),
}
}
// TODO: If the user doesn't provide enough gas then there's a chance that FailedRelayedMessage
// will never be triggered. We should probably fix this at the contract level by requiring a
// minimum amount of input gas and designing the contracts such that the gas will always be
// enough to trigger the event. However, for now we need a temporary way to find L1 => L2
// transactions that fail but don't alert us because they didn't provide enough gas.
// TODO: Talk with the systems and protocol team about coordinating a hard fork that fixes this
// on both L1 and L2.
// Just return null if we didn't find a receipt. Slightly nicer than throwing an error.
return null
}
public async waitForMessageReceipt(
message: MessageLike,
opts: {
confirmations?: number
pollIntervalMs?: number
timeoutMs?: number
} = {}
): Promise<MessageReceipt> {
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(
message: MessageRequestLike,
opts?: {
bufferPercent?: number
from?: string
}
): Promise<BigNumber> {
let resolved: CrossChainMessage | CrossChainMessageRequest
let from: string
if ((message as CrossChainMessage).messageNonce === undefined) {
resolved = message as CrossChainMessageRequest
from = opts?.from
} else {
resolved = await this.toCrossChainMessage(message as MessageLike)
from = opts?.from || (resolved as CrossChainMessage).sender
}
// L2 message gas estimation is only used for L1 => L2 messages.
if (resolved.direction === MessageDirection.L2_TO_L1) {
throw new Error(`cannot estimate gas limit for L2 => L1 message`)
}
const estimate = await this.l2Provider.estimateGas({
from,
to: resolved.target,
data: resolved.message,
})
// Return the estimate plus a buffer of 20% just in case.
const bufferPercent = opts?.bufferPercent || 20
return estimate.mul(100 + bufferPercent).div(100)
}
public async estimateMessageWaitTimeSeconds(
message: MessageLike
): Promise<number> {
throw new Error('Not implemented')
}
public async getChallengePeriodSeconds(): Promise<number> {
const challengePeriod =
await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW()
return challengePeriod.toNumber()
}
public async getMessageStateRoot(
message: MessageLike
): Promise<StateRoot | null> {
const resolved = await this.toCrossChainMessage(message)
// State roots are only a thing for L2 to L1 messages.
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`cannot get a state root for an L1 to L2 message`)
}
// We need the block number of the transaction that triggered the message so we can look up the
// state root batch that corresponds to that block number.
const messageTxReceipt = await this.l2Provider.getTransactionReceipt(
resolved.transactionHash
)
// Every block has exactly one transaction in it. Since there's a genesis block, the
// transaction index will always be one less than the block number.
const messageTxIndex = messageTxReceipt.blockNumber - 1
// Pull down the state root batch, we'll try to pick out the specific state root that
// corresponds to our message.
const stateRootBatch = await this.getStateRootBatchByTransactionIndex(
messageTxIndex
)
// No state root batch, no state root.
if (stateRootBatch === null) {
return null
}
// We have a state root batch, now we need to find the specific state root for our transaction.
// First we need to figure out the index of the state root within the batch we found. This is
// going to be the original transaction index offset by the total number of previous state
// roots.
const indexInBatch =
messageTxIndex - stateRootBatch.header.prevTotalElements.toNumber()
// Just a sanity check.
if (stateRootBatch.stateRoots.length <= indexInBatch) {
// Should never happen!
throw new Error(`state root does not exist in batch`)
}
return {
stateRoot: stateRootBatch.stateRoots[indexInBatch],
stateRootIndexInBatch: indexInBatch,
batch: stateRootBatch,
}
}
public async getStateBatchAppendedEventByBatchIndex(
batchIndex: number
): Promise<ethers.Event | null> {
const events = await this.contracts.l1.StateCommitmentChain.queryFilter(
this.contracts.l1.StateCommitmentChain.filters.StateBatchAppended(
batchIndex
)
)
if (events.length === 0) {
return null
} else if (events.length > 1) {
// Should never happen!
throw new Error(`found more than one StateBatchAppended event`)
} else {
return events[0]
}
}
public async getStateBatchAppendedEventByTransactionIndex(
transactionIndex: number
): Promise<ethers.Event | null> {
const isEventHi = (event: ethers.Event, index: number) => {
const prevTotalElements = event.args._prevTotalElements.toNumber()
return index < prevTotalElements
}
const isEventLo = (event: ethers.Event, index: number) => {
const prevTotalElements = event.args._prevTotalElements.toNumber()
const batchSize = event.args._batchSize.toNumber()
return index >= prevTotalElements + batchSize
}
const totalBatches: ethers.BigNumber =
await this.contracts.l1.StateCommitmentChain.getTotalBatches()
if (totalBatches.eq(0)) {
return null
}
let lowerBound = 0
let upperBound = totalBatches.toNumber() - 1
let batchEvent: ethers.Event | null =
await this.getStateBatchAppendedEventByBatchIndex(upperBound)
if (isEventLo(batchEvent, transactionIndex)) {
// Upper bound is too low, means this transaction doesn't have a corresponding state batch yet.
return null
} else if (!isEventHi(batchEvent, transactionIndex)) {
// Upper bound is not too low and also not too high. This means the upper bound event is the
// one we're looking for! Return it.
return batchEvent
}
// Binary search to find the right event. The above checks will guarantee that the event does
// exist and that we'll find it during this search.
while (lowerBound < upperBound) {
const middleOfBounds = Math.floor((lowerBound + upperBound) / 2)
batchEvent = await this.getStateBatchAppendedEventByBatchIndex(
middleOfBounds
)
if (isEventHi(batchEvent, transactionIndex)) {
upperBound = middleOfBounds
} else if (isEventLo(batchEvent, transactionIndex)) {
lowerBound = middleOfBounds
} else {
break
}
}
return batchEvent
}
public async getStateRootBatchByTransactionIndex(
transactionIndex: number
): Promise<StateRootBatch | null> {
const stateBatchAppendedEvent =
await this.getStateBatchAppendedEventByTransactionIndex(transactionIndex)
if (stateBatchAppendedEvent === null) {
return null
}
const stateBatchTransaction = await stateBatchAppendedEvent.getTransaction()
const [stateRoots] =
this.contracts.l1.StateCommitmentChain.interface.decodeFunctionData(
'appendStateBatch',
stateBatchTransaction.data
)
return {
blockNumber: stateBatchAppendedEvent.blockNumber,
stateRoots,
header: {
batchIndex: stateBatchAppendedEvent.args._batchIndex,
batchRoot: stateBatchAppendedEvent.args._batchRoot,
batchSize: stateBatchAppendedEvent.args._batchSize,
prevTotalElements: stateBatchAppendedEvent.args._prevTotalElements,
extraData: stateBatchAppendedEvent.args._extraData,
},
}
}
public async getMessageProof(
message: MessageLike
): Promise<CrossChainMessageProof> {
const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`can only generate proofs for L2 to L1 messages`)
}
const stateRoot = await this.getMessageStateRoot(resolved)
if (stateRoot === null) {
throw new Error(`state root for message not yet published`)
}
// We need to calculate the specific storage slot that demonstrates that this message was
// actually included in the L2 chain. The following calculation is based on the fact that
// messages are stored in the following mapping on L2:
// https://github.com/ethereum-optimism/optimism/blob/c84d3450225306abbb39b4e7d6d82424341df2be/packages/contracts/contracts/L2/predeploys/OVM_L2ToL1MessagePasser.sol#L23
// You can read more about how Solidity storage slots are computed for mappings here:
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
const messageSlot = ethers.utils.keccak256(
ethers.utils.keccak256(
encodeCrossChainMessage(resolved) +
remove0x(this.contracts.l2.L2CrossDomainMessenger.address)
) + '00'.repeat(32)
)
const stateTrieProof = await makeStateTrieProof(
this.l2Provider as any,
resolved.blockNumber,
this.contracts.l2.OVM_L2ToL1MessagePasser.address,
messageSlot
)
return {
stateRoot: stateRoot.stateRoot,
stateRootBatchHeader: stateRoot.batch.header,
stateRootProof: {
index: stateRoot.stateRootIndexInBatch,
siblings: makeMerkleTreeProof(
stateRoot.batch.stateRoots,
stateRoot.stateRootIndexInBatch
),
},
stateTrieWitness: stateTrieProof.accountProof,
storageTrieWitness: stateTrieProof.storageProof,
}
}
}
export * from './interfaces'
export * from './utils'
export * from './cross-chain-provider'
export * from './cross-chain-messenger'
export * from './adapters'
......@@ -11,7 +11,7 @@ import {
MessageDirection,
TokenBridgeMessage,
} from './types'
import { ICrossChainProvider } from './cross-chain-provider'
import { ICrossChainMessenger } from './cross-chain-messenger'
/**
* Represents an adapter for an L1<>L2 token bridge. Each custom bridge currently needs its own
......@@ -21,7 +21,7 @@ export interface IBridgeAdapter {
/**
* Provider used to make queries related to cross-chain interactions.
*/
provider: ICrossChainProvider
messenger: ICrossChainMessenger
/**
* L1 bridge contract.
......
import { Overrides, Signer, BigNumber } from 'ethers'
import { Event, BigNumber, Overrides } from 'ethers'
import {
Provider,
BlockTag,
TransactionRequest,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import {
MessageLike,
MessageRequestLike,
TransactionLike,
AddressLike,
NumberLike,
CrossChainMessage,
CrossChainMessageRequest,
AddressLike,
CrossChainMessageProof,
MessageDirection,
MessageStatus,
TokenBridgeMessage,
OEContracts,
MessageReceipt,
StateRoot,
StateRootBatch,
BridgeAdapters,
} from './types'
import { ICrossChainProvider } from './cross-chain-provider'
import { IBridgeAdapter } from './bridge-adapter'
/**
* Represents a utility class for making L1/L2 cross-chain transactions.
* Handles L1/L2 interactions.
*/
export interface ICrossChainMessenger {
/**
* Provider that will be used to interact with the L1/L2 system.
* Provider connected to the L1 chain.
*/
l1SignerOrProvider: Signer | Provider
/**
* Provider connected to the L2 chain.
*/
l2SignerOrProvider: Signer | Provider
/**
* Chain ID for the L1 network.
*/
l1ChainId: number
/**
* Contract objects attached to their respective providers and addresses.
*/
contracts: OEContracts
/**
* List of custom bridges for the given network.
*/
provider: ICrossChainProvider
bridges: BridgeAdapters
/**
* Signer that will carry out L1 transactions.
* Provider connected to the L1 chain.
*/
l1Provider: Provider
/**
* Provider connected to the L2 chain.
*/
l2Provider: Provider
/**
* Signer connected to the L1 chain.
*/
l1Signer: Signer
/**
* Signer that will carry out L2 transactions.
* Signer connected to the L2 chain.
*/
l2Signer: Signer
/**
* Retrieves all cross chain messages sent within a given transaction.
*
* @param transaction Transaction hash or receipt to find messages from.
* @param opts Options object.
* @param opts.direction Direction to search for messages in. If not provided, will attempt to
* automatically search both directions under the assumption that a transaction hash will only
* exist on one chain. If the hash exists on both chains, will throw an error.
* @returns All cross chain messages sent within the transaction.
*/
getMessagesByTransaction(
transaction: TransactionLike,
opts?: {
direction?: MessageDirection
}
): Promise<CrossChainMessage[]>
/**
* Retrieves all cross chain messages sent by a particular address.
*
* @param address Address to search for messages from.
* @param opts Options object.
* @param opts.direction Direction to search for messages in. If not provided, will attempt to
* find all messages in both directions.
* @param opts.fromBlock Block to start searching for messages from. If not provided, will start
* from the first block (block #0).
* @param opts.toBlock Block to stop searching for messages at. If not provided, will stop at the
* latest known block ("latest").
* @returns All cross chain messages sent by the particular address.
*/
getMessagesByAddress(
address: AddressLike,
opts?: {
direction?: MessageDirection
fromBlock?: NumberLike
toBlock?: NumberLike
}
): Promise<CrossChainMessage[]>
/**
* Finds the appropriate bridge adapter for a given L1<>L2 token pair. Will throw if no bridges
* support the token pair or if more than one bridge supports the token pair.
*
* @param l1Token L1 token address.
* @param l2Token L2 token address.
* @returns The appropriate bridge adapter for the given token pair.
*/
getBridgeForTokenPair(
l1Token: AddressLike,
l2Token: AddressLike
): Promise<IBridgeAdapter>
/**
* Finds all cross chain messages that correspond to token deposits or withdrawals sent by a
* particular address. Useful for finding deposits/withdrawals because the sender of the message
* will appear to be the StandardBridge contract and not the actual end user.
*
* @param address Address to search for messages from.
* @param opts Options object.
* @param opts.direction Direction to search for messages in. If not provided, will attempt to
* find all messages in both directions.
* @returns All token bridge messages sent by the given address.
*/
getTokenBridgeMessagesByAddress(
address: AddressLike,
opts?: {
direction?: MessageDirection
fromBlock?: BlockTag
toBlock?: BlockTag
}
): Promise<TokenBridgeMessage[]>
/**
* Alias for getTokenBridgeMessagesByAddress with a drection of L1_TO_L2.
*
* @param address Address to search for messages from.
* @param opts Options object.
* @param opts.fromBlock Block to start searching for messages from. If not provided, will start
* from the first block (block #0).
* @param opts.toBlock Block to stop searching for messages at. If not provided, will stop at the
* latest known block ("latest").
* @returns All deposit token bridge messages sent by the given address.
*/
getDepositsByAddress(
address: AddressLike,
opts?: {
fromBlock?: BlockTag
toBlock?: BlockTag
}
): Promise<TokenBridgeMessage[]>
/**
* Alias for getTokenBridgeMessagesByAddress with a drection of L2_TO_L1.
*
* @param address Address to search for messages from.
* @param opts Options object.
* @param opts.fromBlock Block to start searching for messages from. If not provided, will start
* from the first block (block #0).
* @param opts.toBlock Block to stop searching for messages at. If not provided, will stop at the
* latest known block ("latest").
* @returns All withdrawal token bridge messages sent by the given address.
*/
getWithdrawalsByAddress(
address: AddressLike,
opts?: {
fromBlock?: BlockTag
toBlock?: BlockTag
}
): Promise<TokenBridgeMessage[]>
/**
* Resolves a MessageLike into a CrossChainMessage object.
* Unlike other coercion functions, this function is stateful and requires making additional
* requests. For now I'm going to keep this function here, but we could consider putting a
* similar function inside of utils/coercion.ts if people want to use this without having to
* create an entire CrossChainProvider object.
*
* @param message MessageLike to resolve into a CrossChainMessage.
* @returns Message coerced into a CrossChainMessage.
*/
toCrossChainMessage(message: MessageLike): Promise<CrossChainMessage>
/**
* Retrieves the status of a particular message as an enum.
*
* @param message Cross chain message to check the status of.
* @returns Status of the message.
*/
getMessageStatus(message: MessageLike): Promise<MessageStatus>
/**
* Finds the receipt of the transaction that executed a particular cross chain message.
*
* @param message Message to find the receipt of.
* @returns CrossChainMessage receipt including receipt of the transaction that relayed the
* given message.
*/
getMessageReceipt(message: MessageLike): Promise<MessageReceipt>
/**
* Waits for a message to be executed and returns the receipt of the transaction that executed
* the given message.
*
* @param message Message to wait for.
* @param opts Options to pass to the waiting function.
* @param opts.confirmations Number of transaction confirmations to wait for before returning.
* @param opts.pollIntervalMs Number of milliseconds to wait between polling for the receipt.
* @param opts.timeoutMs Milliseconds to wait before timing out.
* @returns CrossChainMessage receipt including receipt of the transaction that relayed the
* given message.
*/
waitForMessageReceipt(
message: MessageLike,
opts?: {
confirmations?: number
pollIntervalMs?: number
timeoutMs?: number
}
): Promise<MessageReceipt>
/**
* 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.
*
* @param message Message get a gas estimate for.
* @param opts Options object.
* @param opts.bufferPercent Percentage of gas to add to the estimate. Defaults to 20.
* @param opts.from Address to use as the sender.
* @returns Estimates L2 gas limit.
*/
estimateL2MessageGasLimit(
message: MessageRequestLike,
opts?: {
bufferPercent?: number
from?: string
}
): Promise<BigNumber>
/**
* Returns the estimated amount of time before the message can be executed. When this is a
* message being sent to L1, this will return the estimated time until the message will complete
* its challenge period. When this is a message being sent to L2, this will return the estimated
* amount of time until the message will be picked up and executed on L2.
*
* @param message Message to estimate the time remaining for.
* @returns Estimated amount of time remaining (in seconds) before the message can be executed.
*/
estimateMessageWaitTimeSeconds(message: MessageLike): Promise<number>
/**
* Queries the current challenge period in seconds from the StateCommitmentChain.
*
* @returns Current challenge period in seconds.
*/
getChallengePeriodSeconds(): Promise<number>
/**
* Returns the state root that corresponds to a given message. This is the state root for the
* block in which the transaction was included, as published to the StateCommitmentChain. If the
* state root for the given message has not been published yet, this function returns null.
*
* @param message Message to find a state root for.
* @returns State root for the block in which the message was created.
*/
getMessageStateRoot(message: MessageLike): Promise<StateRoot | null>
/**
* Returns the StateBatchAppended event that was emitted when the batch with a given index was
* created. Returns null if no such event exists (the batch has not been submitted).
*
* @param batchIndex Index of the batch to find an event for.
* @returns StateBatchAppended event for the batch, or null if no such batch exists.
*/
getStateBatchAppendedEventByBatchIndex(
batchIndex: number
): Promise<Event | null>
/**
* Returns the StateBatchAppended event for the batch that includes the transaction with the
* given index. Returns null if no such event exists.
*
* @param transactionIndex Index of the L2 transaction to find an event for.
* @returns StateBatchAppended event for the batch that includes the given transaction by index.
*/
getStateBatchAppendedEventByTransactionIndex(
transactionIndex: number
): Promise<Event | null>
/**
* Returns information about the state root batch that included the state root for the given
* transaction by index. Returns null if no such state root has been published yet.
*
* @param transactionIndex Index of the L2 transaction to find a state root batch for.
* @returns State root batch for the given transaction index, or null if none exists yet.
*/
getStateRootBatchByTransactionIndex(
transactionIndex: number
): Promise<StateRootBatch | null>
/**
* Generates the proof required to finalize an L2 to L1 message.
*
* @param message Message to generate a proof for.
* @returns Proof that can be used to finalize the message.
*/
getMessageProof(message: MessageLike): Promise<CrossChainMessageProof>
/**
* Sends a given cross chain message. Where the message is sent depends on the direction attached
* to the message itself.
*
* @param message Cross chain message to send.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the message sending transaction.
......@@ -44,6 +337,7 @@ export interface ICrossChainMessenger {
sendMessage(
message: CrossChainMessageRequest,
opts?: {
signer?: Signer
l2GasLimit?: NumberLike
overrides?: Overrides
}
......@@ -56,6 +350,7 @@ export interface ICrossChainMessenger {
* @param message Cross chain message to resend.
* @param messageGasLimit New gas limit to use for the message.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the message resending transaction.
*/
......@@ -63,6 +358,7 @@ export interface ICrossChainMessenger {
message: MessageLike,
messageGasLimit: NumberLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse>
......@@ -73,12 +369,14 @@ export interface ICrossChainMessenger {
*
* @param message Message to finalize.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the finalization transaction.
*/
finalizeMessage(
message: MessageLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse>
......@@ -88,6 +386,7 @@ export interface ICrossChainMessenger {
*
* @param amount Amount of ETH to deposit (in wei).
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the deposit transaction.
......@@ -95,6 +394,7 @@ export interface ICrossChainMessenger {
depositETH(
amount: NumberLike,
opts?: {
signer?: Signer
l2GasLimit?: NumberLike
overrides?: Overrides
}
......@@ -105,12 +405,14 @@ export interface ICrossChainMessenger {
*
* @param amount Amount of ETH to withdraw.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the withdraw transaction.
*/
withdrawETH(
amount: NumberLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse>
......@@ -122,6 +424,7 @@ export interface ICrossChainMessenger {
* @param l2Token Address of the L2 token.
* @param amount Amount to deposit.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the deposit transaction.
......@@ -131,6 +434,7 @@ export interface ICrossChainMessenger {
l2Token: AddressLike,
amount: NumberLike,
opts?: {
signer?: Signer
l2GasLimit?: NumberLike
overrides?: Overrides
}
......@@ -143,6 +447,7 @@ export interface ICrossChainMessenger {
* @param l2Token Address of the L2 token.
* @param amount Amount to withdraw.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the withdraw transaction.
*/
......@@ -151,6 +456,7 @@ export interface ICrossChainMessenger {
l2Token: AddressLike,
amount: NumberLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse>
......
import { Event, BigNumber } from 'ethers'
import { Provider, BlockTag } from '@ethersproject/abstract-provider'
import {
MessageLike,
MessageRequestLike,
TransactionLike,
AddressLike,
NumberLike,
CrossChainMessage,
CrossChainMessageProof,
MessageDirection,
MessageStatus,
TokenBridgeMessage,
OEContracts,
MessageReceipt,
StateRoot,
StateRootBatch,
BridgeAdapters,
} from './types'
import { IBridgeAdapter } from './bridge-adapter'
/**
* Represents the L1/L2 connection. Only handles read requests. If you want to send messages, use
* the CrossChainMessenger contract which takes a CrossChainProvider and a signer as inputs.
*/
export interface ICrossChainProvider {
/**
* Provider connected to the L1 chain.
*/
l1Provider: Provider
/**
* Provider connected to the L2 chain.
*/
l2Provider: Provider
/**
* Chain ID for the L1 network.
*/
l1ChainId: number
/**
* Contract objects attached to their respective providers and addresses.
*/
contracts: OEContracts
/**
* List of custom bridges for the given network.
*/
bridges: BridgeAdapters
/**
* Retrieves all cross chain messages sent within a given transaction.
*
* @param transaction Transaction hash or receipt to find messages from.
* @param opts Options object.
* @param opts.direction Direction to search for messages in. If not provided, will attempt to
* automatically search both directions under the assumption that a transaction hash will only
* exist on one chain. If the hash exists on both chains, will throw an error.
* @returns All cross chain messages sent within the transaction.
*/
getMessagesByTransaction(
transaction: TransactionLike,
opts?: {
direction?: MessageDirection
}
): Promise<CrossChainMessage[]>
/**
* Retrieves all cross chain messages sent by a particular address.
*
* @param address Address to search for messages from.
* @param opts Options object.
* @param opts.direction Direction to search for messages in. If not provided, will attempt to
* find all messages in both directions.
* @param opts.fromBlock Block to start searching for messages from. If not provided, will start
* from the first block (block #0).
* @param opts.toBlock Block to stop searching for messages at. If not provided, will stop at the
* latest known block ("latest").
* @returns All cross chain messages sent by the particular address.
*/
getMessagesByAddress(
address: AddressLike,
opts?: {
direction?: MessageDirection
fromBlock?: NumberLike
toBlock?: NumberLike
}
): Promise<CrossChainMessage[]>
/**
* Finds the appropriate bridge adapter for a given L1<>L2 token pair. Will throw if no bridges
* support the token pair or if more than one bridge supports the token pair.
*
* @param l1Token L1 token address.
* @param l2Token L2 token address.
* @returns The appropriate bridge adapter for the given token pair.
*/
getBridgeForTokenPair(
l1Token: AddressLike,
l2Token: AddressLike
): Promise<IBridgeAdapter>
/**
* Finds all cross chain messages that correspond to token deposits or withdrawals sent by a
* particular address. Useful for finding deposits/withdrawals because the sender of the message
* will appear to be the StandardBridge contract and not the actual end user.
*
* @param address Address to search for messages from.
* @param opts Options object.
* @param opts.direction Direction to search for messages in. If not provided, will attempt to
* find all messages in both directions.
* @returns All token bridge messages sent by the given address.
*/
getTokenBridgeMessagesByAddress(
address: AddressLike,
opts?: {
direction?: MessageDirection
fromBlock?: BlockTag
toBlock?: BlockTag
}
): Promise<TokenBridgeMessage[]>
/**
* Alias for getTokenBridgeMessagesByAddress with a drection of L1_TO_L2.
*
* @param address Address to search for messages from.
* @param opts Options object.
* @param opts.fromBlock Block to start searching for messages from. If not provided, will start
* from the first block (block #0).
* @param opts.toBlock Block to stop searching for messages at. If not provided, will stop at the
* latest known block ("latest").
* @returns All deposit token bridge messages sent by the given address.
*/
getDepositsByAddress(
address: AddressLike,
opts?: {
fromBlock?: BlockTag
toBlock?: BlockTag
}
): Promise<TokenBridgeMessage[]>
/**
* Alias for getTokenBridgeMessagesByAddress with a drection of L2_TO_L1.
*
* @param address Address to search for messages from.
* @param opts Options object.
* @param opts.fromBlock Block to start searching for messages from. If not provided, will start
* from the first block (block #0).
* @param opts.toBlock Block to stop searching for messages at. If not provided, will stop at the
* latest known block ("latest").
* @returns All withdrawal token bridge messages sent by the given address.
*/
getWithdrawalsByAddress(
address: AddressLike,
opts?: {
fromBlock?: BlockTag
toBlock?: BlockTag
}
): Promise<TokenBridgeMessage[]>
/**
* Resolves a MessageLike into a CrossChainMessage object.
* Unlike other coercion functions, this function is stateful and requires making additional
* requests. For now I'm going to keep this function here, but we could consider putting a
* similar function inside of utils/coercion.ts if people want to use this without having to
* create an entire CrossChainProvider object.
*
* @param message MessageLike to resolve into a CrossChainMessage.
* @returns Message coerced into a CrossChainMessage.
*/
toCrossChainMessage(message: MessageLike): Promise<CrossChainMessage>
/**
* Retrieves the status of a particular message as an enum.
*
* @param message Cross chain message to check the status of.
* @returns Status of the message.
*/
getMessageStatus(message: MessageLike): Promise<MessageStatus>
/**
* Finds the receipt of the transaction that executed a particular cross chain message.
*
* @param message Message to find the receipt of.
* @returns CrossChainMessage receipt including receipt of the transaction that relayed the
* given message.
*/
getMessageReceipt(message: MessageLike): Promise<MessageReceipt>
/**
* Waits for a message to be executed and returns the receipt of the transaction that executed
* the given message.
*
* @param message Message to wait for.
* @param opts Options to pass to the waiting function.
* @param opts.confirmations Number of transaction confirmations to wait for before returning.
* @param opts.pollIntervalMs Number of milliseconds to wait between polling for the receipt.
* @param opts.timeoutMs Milliseconds to wait before timing out.
* @returns CrossChainMessage receipt including receipt of the transaction that relayed the
* given message.
*/
waitForMessageReceipt(
message: MessageLike,
opts?: {
confirmations?: number
pollIntervalMs?: number
timeoutMs?: number
}
): Promise<MessageReceipt>
/**
* 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.
*
* @param message Message get a gas estimate for.
* @param opts Options object.
* @param opts.bufferPercent Percentage of gas to add to the estimate. Defaults to 20.
* @param opts.from Address to use as the sender.
* @returns Estimates L2 gas limit.
*/
estimateL2MessageGasLimit(
message: MessageRequestLike,
opts?: {
bufferPercent?: number
from?: string
}
): Promise<BigNumber>
/**
* Returns the estimated amount of time before the message can be executed. When this is a
* message being sent to L1, this will return the estimated time until the message will complete
* its challenge period. When this is a message being sent to L2, this will return the estimated
* amount of time until the message will be picked up and executed on L2.
*
* @param message Message to estimate the time remaining for.
* @returns Estimated amount of time remaining (in seconds) before the message can be executed.
*/
estimateMessageWaitTimeSeconds(message: MessageLike): Promise<number>
/**
* Queries the current challenge period in seconds from the StateCommitmentChain.
*
* @returns Current challenge period in seconds.
*/
getChallengePeriodSeconds(): Promise<number>
/**
* Returns the state root that corresponds to a given message. This is the state root for the
* block in which the transaction was included, as published to the StateCommitmentChain. If the
* state root for the given message has not been published yet, this function returns null.
*
* @param message Message to find a state root for.
* @returns State root for the block in which the message was created.
*/
getMessageStateRoot(message: MessageLike): Promise<StateRoot | null>
/**
* Returns the StateBatchAppended event that was emitted when the batch with a given index was
* created. Returns null if no such event exists (the batch has not been submitted).
*
* @param batchIndex Index of the batch to find an event for.
* @returns StateBatchAppended event for the batch, or null if no such batch exists.
*/
getStateBatchAppendedEventByBatchIndex(
batchIndex: number
): Promise<Event | null>
/**
* Returns the StateBatchAppended event for the batch that includes the transaction with the
* given index. Returns null if no such event exists.
*
* @param transactionIndex Index of the L2 transaction to find an event for.
* @returns StateBatchAppended event for the batch that includes the given transaction by index.
*/
getStateBatchAppendedEventByTransactionIndex(
transactionIndex: number
): Promise<Event | null>
/**
* Returns information about the state root batch that included the state root for the given
* transaction by index. Returns null if no such state root has been published yet.
*
* @param transactionIndex Index of the L2 transaction to find a state root batch for.
* @returns State root batch for the given transaction index, or null if none exists yet.
*/
getStateRootBatchByTransactionIndex(
transactionIndex: number
): Promise<StateRootBatch | null>
/**
* Generates the proof required to finalize an L2 to L1 message.
*
* @param message Message to generate a proof for.
* @returns Proof that can be used to finalize the message.
*/
getMessageProof(message: MessageLike): Promise<CrossChainMessageProof>
}
export * from './bridge-adapter'
export * from './cross-chain-messenger'
export * from './cross-chain-provider'
export * from './l2-provider'
export * from './types'
......@@ -6,7 +6,7 @@ import {
import { Signer } from '@ethersproject/abstract-signer'
import { Contract, BigNumber } from 'ethers'
import { ICrossChainProvider } from './cross-chain-provider'
import { ICrossChainMessenger } from './cross-chain-messenger'
import { IBridgeAdapter } from './bridge-adapter'
/**
......@@ -76,7 +76,7 @@ export interface OEContractsLike {
export interface BridgeAdapterData {
[name: string]: {
Adapter: new (opts: {
provider: ICrossChainProvider
messenger: ICrossChainMessenger
l1Bridge: AddressLike
l2Bridge: AddressLike
}) => IBridgeAdapter
......
......@@ -5,27 +5,32 @@ import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { ethers, BigNumber } from 'ethers'
import {
ProviderLike,
SignerOrProviderLike,
TransactionLike,
NumberLike,
AddressLike,
} from '../interfaces'
/**
* Converts a ProviderLike into a provider. Assumes that if the ProviderLike is a string then
* it is a JSON-RPC url.
* Converts a SignerOrProviderLike into a Signer or a Provider. Assumes that if the input is a
* string then it is a JSON-RPC url.
*
* @param provider ProviderLike to turn into a provider.
* @returns ProviderLike as a provider.
* @param signerOrProvider SignerOrProviderLike to turn into a Signer or Provider.
* @returns Input as a Signer or Provider.
*/
export const toProvider = (provider: ProviderLike): Provider => {
if (typeof provider === 'string') {
return new ethers.providers.JsonRpcProvider(provider)
} else if (Provider.isProvider(provider)) {
return provider
export const toSignerOrProvider = (
signerOrProvider: SignerOrProviderLike
): Signer | Provider => {
if (typeof signerOrProvider === 'string') {
return new ethers.providers.JsonRpcProvider(signerOrProvider)
} else if (Provider.isProvider(signerOrProvider)) {
return signerOrProvider as Provider
} else if (Signer.isSigner(signerOrProvider)) {
return signerOrProvider as Signer
} else {
throw new Error('Invalid provider')
}
......
......@@ -12,7 +12,7 @@ import {
AddressLike,
BridgeAdapters,
BridgeAdapterData,
ICrossChainProvider,
ICrossChainMessenger,
} from '../interfaces'
import {
StandardBridgeAdapter,
......@@ -291,14 +291,14 @@ export const getAllOEContracts = (
* Gets a series of bridge adapters for the given L1 chain ID.
*
* @param l1ChainId L1 chain ID for the L1 network where the custom bridges are deployed.
* @param provider Cross chain provider to connect to the bridge adapters
* @param messenger Cross chain messenger to connect to the bridge adapters
* @param opts Additional options for connecting to the custom bridges.
* @param opts.overrides Custom bridge adapters.
* @returns An object containing all bridge adapters
*/
export const getBridgeAdapters = (
l1ChainId: number,
provider: ICrossChainProvider,
messenger: ICrossChainMessenger,
opts?: {
overrides?: BridgeAdapterData
}
......@@ -309,7 +309,7 @@ export const getBridgeAdapters = (
...(opts?.overrides || {}),
})) {
adapters[bridgeName] = new bridgeData.Adapter({
provider,
messenger,
l1Bridge: bridgeData.l1Bridge,
l2Bridge: bridgeData.l2Bridge,
})
......
import { Provider } from '@ethersproject/abstract-provider'
import { expectApprox } from '@eth-optimism/core-utils'
import { predeploys } from '@eth-optimism/contracts'
import { Contract } from 'ethers'
import { ethers } from 'hardhat'
import { predeploys } from '@eth-optimism/contracts'
import { expect } from './setup'
import {
CrossChainProvider,
CrossChainMessenger,
MessageDirection,
ETHBridgeAdapter,
} from '../src'
import { expect } from './setup'
import {
MessageDirection,
CONTRACT_ADDRESSES,
hashCrossChainMessage,
omit,
MessageStatus,
CrossChainMessage,
CrossChainMessenger,
StandardBridgeAdapter,
ETHBridgeAdapter,
} from '../src'
import { DUMMY_MESSAGE } from './helpers'
describe('CrossChainMessenger', () => {
let l1Signer: any
let l2Signer: any
before(async () => {
;[l1Signer, l2Signer] = await ethers.getSigners()
})
describe('construction', () => {
describe('when given an ethers provider for the L1 provider', () => {
it('should use the provider as the L1 provider', () => {
const messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: ethers.provider,
l1ChainId: 1,
})
expect(messenger.l1Provider).to.equal(ethers.provider)
})
})
describe('when given an ethers provider for the L2 provider', () => {
it('should use the provider as the L2 provider', () => {
const messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: ethers.provider,
l1ChainId: 1,
})
expect(messenger.l2Provider).to.equal(ethers.provider)
})
})
describe('when given a string as the L1 provider', () => {
it('should create a JSON-RPC provider for the L1 provider', () => {
const messenger = new CrossChainMessenger({
l1SignerOrProvider: 'https://localhost:8545',
l2SignerOrProvider: ethers.provider,
l1ChainId: 1,
})
expect(Provider.isProvider(messenger.l1Provider)).to.be.true
})
})
describe('when given a string as the L2 provider', () => {
it('should create a JSON-RPC provider for the L2 provider', () => {
const messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: 'https://localhost:8545',
l1ChainId: 1,
})
expect(Provider.isProvider(messenger.l2Provider)).to.be.true
})
})
describe('when no custom contract addresses are provided', () => {
describe('when given a known chain ID', () => {
it('should use the contract addresses for the known chain ID', () => {
const messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: 'https://localhost:8545',
l1ChainId: 1,
})
const addresses = CONTRACT_ADDRESSES[1]
for (const [contractName, contractAddress] of Object.entries(
addresses.l1
)) {
const contract = messenger.contracts.l1[contractName]
expect(contract.address).to.equal(contractAddress)
}
for (const [contractName, contractAddress] of Object.entries(
addresses.l2
)) {
const contract = messenger.contracts.l2[contractName]
expect(contract.address).to.equal(contractAddress)
}
})
})
describe('when given an unknown chain ID', () => {
it('should throw an error', () => {
expect(() => {
new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: 'https://localhost:8545',
l1ChainId: 1234,
})
}).to.throw()
})
})
})
describe('when custom contract addresses are provided', () => {
describe('when given a known chain ID', () => {
it('should use known addresses except where custom addresses are given', () => {
const overrides = {
l1: {
L1CrossDomainMessenger: '0x' + '11'.repeat(20),
},
l2: {
L2CrossDomainMessenger: '0x' + '22'.repeat(20),
},
}
const messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: 'https://localhost:8545',
l1ChainId: 1,
contracts: overrides,
})
const addresses = CONTRACT_ADDRESSES[1]
for (const [contractName, contractAddress] of Object.entries(
addresses.l1
)) {
if (overrides.l1[contractName]) {
const contract = messenger.contracts.l1[contractName]
expect(contract.address).to.equal(overrides.l1[contractName])
} else {
const contract = messenger.contracts.l1[contractName]
expect(contract.address).to.equal(contractAddress)
}
}
for (const [contractName, contractAddress] of Object.entries(
addresses.l2
)) {
if (overrides.l2[contractName]) {
const contract = messenger.contracts.l2[contractName]
expect(contract.address).to.equal(overrides.l2[contractName])
} else {
const contract = messenger.contracts.l2[contractName]
expect(contract.address).to.equal(contractAddress)
}
}
})
})
describe('when given an unknown chain ID', () => {
describe('when all L1 addresses are provided', () => {
it('should use custom addresses where provided', () => {
const overrides = {
l1: {
AddressManager: '0x' + '11'.repeat(20),
L1CrossDomainMessenger: '0x' + '12'.repeat(20),
L1StandardBridge: '0x' + '13'.repeat(20),
StateCommitmentChain: '0x' + '14'.repeat(20),
CanonicalTransactionChain: '0x' + '15'.repeat(20),
BondManager: '0x' + '16'.repeat(20),
},
l2: {
L2CrossDomainMessenger: '0x' + '22'.repeat(20),
},
}
const messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: 'https://localhost:8545',
l1ChainId: 1234,
contracts: overrides,
})
const addresses = CONTRACT_ADDRESSES[1]
for (const [contractName, contractAddress] of Object.entries(
addresses.l1
)) {
if (overrides.l1[contractName]) {
const contract = messenger.contracts.l1[contractName]
expect(contract.address).to.equal(overrides.l1[contractName])
} else {
const contract = messenger.contracts.l1[contractName]
expect(contract.address).to.equal(contractAddress)
}
}
for (const [contractName, contractAddress] of Object.entries(
addresses.l2
)) {
if (overrides.l2[contractName]) {
const contract = messenger.contracts.l2[contractName]
expect(contract.address).to.equal(overrides.l2[contractName])
} else {
const contract = messenger.contracts.l2[contractName]
expect(contract.address).to.equal(contractAddress)
}
}
})
})
describe('when not all L1 addresses are provided', () => {
it('should throw an error', () => {
expect(() => {
new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: 'https://localhost:8545',
l1ChainId: 1234,
contracts: {
l1: {
// Missing some required L1 addresses
AddressManager: '0x' + '11'.repeat(20),
L1CrossDomainMessenger: '0x' + '12'.repeat(20),
L1StandardBridge: '0x' + '13'.repeat(20),
},
l2: {
L2CrossDomainMessenger: '0x' + '22'.repeat(20),
},
},
})
}).to.throw()
})
})
})
})
})
describe('getMessagesByTransaction', () => {
let l1Messenger: Contract
let l2Messenger: Contract
let messenger: CrossChainMessenger
beforeEach(async () => {
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
},
},
})
})
describe('when a direction is specified', () => {
describe('when the transaction exists', () => {
describe('when the transaction has messages', () => {
for (const n of [1, 2, 4, 8]) {
it(`should find ${n} messages when the transaction emits ${n} messages`, async () => {
const messages = [...Array(n)].map(() => {
return DUMMY_MESSAGE
})
const tx = await l1Messenger.triggerSentMessageEvents(messages)
const found = await messenger.getMessagesByTransaction(tx, {
direction: MessageDirection.L1_TO_L2,
})
expect(found).to.deep.equal(
messages.map((message, i) => {
return {
direction: MessageDirection.L1_TO_L2,
sender: message.sender,
target: message.target,
message: message.message,
messageNonce: ethers.BigNumber.from(message.messageNonce),
gasLimit: ethers.BigNumber.from(message.gasLimit),
logIndex: i,
blockNumber: tx.blockNumber,
transactionHash: tx.hash,
}
})
)
})
}
})
describe('when the transaction has no messages', () => {
it('should find nothing', async () => {
const tx = await l1Messenger.doNothing()
const found = await messenger.getMessagesByTransaction(tx, {
direction: MessageDirection.L1_TO_L2,
})
expect(found).to.deep.equal([])
})
})
})
describe('when the transaction does not exist in the specified direction', () => {
it('should throw an error', async () => {
await expect(
messenger.getMessagesByTransaction('0x' + '11'.repeat(32), {
direction: MessageDirection.L1_TO_L2,
})
).to.be.rejectedWith('unable to find transaction receipt')
})
})
})
describe('when a direction is not specified', () => {
describe('when the transaction exists only on L1', () => {
describe('when the transaction has messages', () => {
for (const n of [1, 2, 4, 8]) {
it(`should find ${n} messages when the transaction emits ${n} messages`, async () => {
const messages = [...Array(n)].map(() => {
return DUMMY_MESSAGE
})
const tx = await l1Messenger.triggerSentMessageEvents(messages)
const found = await messenger.getMessagesByTransaction(tx)
expect(found).to.deep.equal(
messages.map((message, i) => {
return {
direction: MessageDirection.L1_TO_L2,
sender: message.sender,
target: message.target,
message: message.message,
messageNonce: ethers.BigNumber.from(message.messageNonce),
gasLimit: ethers.BigNumber.from(message.gasLimit),
logIndex: i,
blockNumber: tx.blockNumber,
transactionHash: tx.hash,
}
})
)
})
}
})
describe('when the transaction has no messages', () => {
it('should find nothing', async () => {
const tx = await l1Messenger.doNothing()
const found = await messenger.getMessagesByTransaction(tx)
expect(found).to.deep.equal([])
})
})
})
describe('when the transaction exists only on L2', () => {
describe('when the transaction has messages', () => {
for (const n of [1, 2, 4, 8]) {
it(`should find ${n} messages when the transaction emits ${n} messages`, () => {
// TODO: Need support for simulating more than one network.
})
}
})
describe('when the transaction has no messages', () => {
it('should find nothing', () => {
// TODO: Need support for simulating more than one network.
})
})
})
describe('when the transaction does not exist', () => {
it('should throw an error', async () => {
await expect(
messenger.getMessagesByTransaction('0x' + '11'.repeat(32))
).to.be.rejectedWith('unable to find transaction receipt')
})
})
describe('when the transaction exists on both L1 and L2', () => {
it('should throw an error', async () => {
// TODO: Need support for simulating more than one network.
})
})
})
})
describe('getMessagesByAddress', () => {
describe('when the address has sent messages', () => {
describe('when no direction is specified', () => {
it('should find all messages sent by the address')
})
describe('when a direction is specified', () => {
it('should find all messages only in the given direction')
})
describe('when a block range is specified', () => {
it('should find all messages within the block range')
})
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'
)
})
})
describe('when the address has not sent messages', () => {
it('should find nothing')
})
})
describe('getTokenBridgeMessagesByAddress', () => {
let l1Bridge: Contract
let l2Bridge: Contract
let l1Messenger: Contract
let l2Messenger: Contract
let messenger: CrossChainMessenger
beforeEach(async () => {
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l1Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l1Messenger.address)) as any
l2Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l2Messenger.address)) as any
messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
L1StandardBridge: l1Bridge.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
L2StandardBridge: l2Bridge.address,
},
},
bridges: {
Standard: {
Adapter: StandardBridgeAdapter,
l1Bridge: l1Bridge.address,
l2Bridge: l2Bridge.address,
},
},
})
})
describe('when the address has made deposits or withdrawals', () => {
describe('when a direction of L1 => L2 is specified', () => {
it('should find all deposits made by the address', async () => {
const from = '0x' + '99'.repeat(20)
const deposit = {
l1Token: '0x' + '11'.repeat(20),
l2Token: '0x' + '22'.repeat(20),
from,
to: '0x' + '44'.repeat(20),
amount: ethers.BigNumber.from(1234),
data: '0x1234',
}
const withdrawal = {
l1Token: '0x' + '12'.repeat(20),
l2Token: '0x' + '23'.repeat(20),
from,
to: '0x' + '45'.repeat(20),
amount: ethers.BigNumber.from(5678),
data: '0x5678',
}
await l1Bridge.emitERC20DepositInitiated(deposit)
await l2Bridge.emitWithdrawalInitiated(withdrawal)
const found = await messenger.getTokenBridgeMessagesByAddress(from, {
direction: MessageDirection.L1_TO_L2,
})
expect(found.length).to.equal(1)
expect(found[0].amount).to.deep.equal(deposit.amount)
expect(found[0].data).to.deep.equal(deposit.data)
expect(found[0].direction).to.equal(MessageDirection.L1_TO_L2)
expect(found[0].l1Token).to.deep.equal(deposit.l1Token)
expect(found[0].l2Token).to.deep.equal(deposit.l2Token)
expect(found[0].from).to.deep.equal(deposit.from)
expect(found[0].to).to.deep.equal(deposit.to)
})
})
describe('when a direction of L2 => L1 is specified', () => {
it('should find all withdrawals made by the address', async () => {
const from = '0x' + '99'.repeat(20)
const deposit = {
l1Token: '0x' + '11'.repeat(20),
l2Token: '0x' + '22'.repeat(20),
from,
to: '0x' + '44'.repeat(20),
amount: ethers.BigNumber.from(1234),
data: '0x1234',
}
const withdrawal = {
l1Token: '0x' + '12'.repeat(20),
l2Token: '0x' + '23'.repeat(20),
from,
to: '0x' + '45'.repeat(20),
amount: ethers.BigNumber.from(5678),
data: '0x5678',
}
await l1Bridge.emitERC20DepositInitiated(deposit)
await l2Bridge.emitWithdrawalInitiated(withdrawal)
const found = await messenger.getTokenBridgeMessagesByAddress(from, {
direction: MessageDirection.L2_TO_L1,
})
expect(found.length).to.equal(1)
expect(found[0].amount).to.deep.equal(withdrawal.amount)
expect(found[0].data).to.deep.equal(withdrawal.data)
expect(found[0].direction).to.equal(MessageDirection.L2_TO_L1)
expect(found[0].l1Token).to.deep.equal(withdrawal.l1Token)
expect(found[0].l2Token).to.deep.equal(withdrawal.l2Token)
expect(found[0].from).to.deep.equal(withdrawal.from)
expect(found[0].to).to.deep.equal(withdrawal.to)
})
})
describe('when no direction is specified', () => {
it('should find all deposits and withdrawals made by the address', async () => {
const from = '0x' + '99'.repeat(20)
const deposit = {
l1Token: '0x' + '11'.repeat(20),
l2Token: '0x' + '22'.repeat(20),
from,
to: '0x' + '44'.repeat(20),
amount: ethers.BigNumber.from(1234),
data: '0x1234',
}
const withdrawal = {
l1Token: '0x' + '12'.repeat(20),
l2Token: '0x' + '23'.repeat(20),
from,
to: '0x' + '45'.repeat(20),
amount: ethers.BigNumber.from(5678),
data: '0x5678',
}
await l1Bridge.emitERC20DepositInitiated(deposit)
await l2Bridge.emitWithdrawalInitiated(withdrawal)
const found = await messenger.getTokenBridgeMessagesByAddress(from)
expect(found.length).to.equal(2)
// Check the deposit (deposits get searched first)
expect(found[0].amount).to.deep.equal(deposit.amount)
expect(found[0].data).to.deep.equal(deposit.data)
expect(found[0].direction).to.equal(MessageDirection.L1_TO_L2)
expect(found[0].l1Token).to.deep.equal(deposit.l1Token)
expect(found[0].l2Token).to.deep.equal(deposit.l2Token)
expect(found[0].from).to.deep.equal(deposit.from)
expect(found[0].to).to.deep.equal(deposit.to)
// Check the withdrawal
expect(found[1].amount).to.deep.equal(withdrawal.amount)
expect(found[1].data).to.deep.equal(withdrawal.data)
expect(found[1].direction).to.equal(MessageDirection.L2_TO_L1)
expect(found[1].l1Token).to.deep.equal(withdrawal.l1Token)
expect(found[1].l2Token).to.deep.equal(withdrawal.l2Token)
expect(found[1].from).to.deep.equal(withdrawal.from)
expect(found[1].to).to.deep.equal(withdrawal.to)
})
})
})
describe('when the address has not made any deposits or withdrawals', () => {
it('should find nothing', async () => {
const from = '0x' + '99'.repeat(20)
const found = await messenger.getTokenBridgeMessagesByAddress(from)
expect(found).to.deep.equal([])
})
})
})
describe('toCrossChainMessage', () => {
let l1Bridge: Contract
let l2Bridge: Contract
let l1Messenger: Contract
let l2Messenger: Contract
let messenger: CrossChainMessenger
beforeEach(async () => {
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l1Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l1Messenger.address)) as any
l2Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l2Messenger.address)) as any
messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
L1StandardBridge: l1Bridge.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
L2StandardBridge: l2Bridge.address,
},
},
bridges: {
Standard: {
Adapter: StandardBridgeAdapter,
l1Bridge: l1Bridge.address,
l2Bridge: l2Bridge.address,
},
},
})
})
describe('when the input is a CrossChainMessage', () => {
it('should return the input', 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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
expect(await messenger.toCrossChainMessage(message)).to.deep.equal(
message
)
})
})
describe('when the input is a TokenBridgeMessage', () => {
// TODO: There are some edge cases here with custom bridges that conform to the interface but
// not to the behavioral spec. Possibly worth testing those. For now this is probably
// sufficient.
it('should return the sent message event that came after the deposit or withdrawal', async () => {
const from = '0x' + '99'.repeat(20)
const deposit = {
l1Token: '0x' + '11'.repeat(20),
l2Token: '0x' + '22'.repeat(20),
from,
to: '0x' + '44'.repeat(20),
amount: ethers.BigNumber.from(1234),
data: '0x1234',
}
const tx = await l1Bridge.emitERC20DepositInitiated(deposit)
const foundCrossChainMessages =
await messenger.getMessagesByTransaction(tx)
const foundTokenBridgeMessages =
await messenger.getTokenBridgeMessagesByAddress(from)
const resolved = await messenger.toCrossChainMessage(
foundTokenBridgeMessages[0]
)
expect(resolved).to.deep.equal(foundCrossChainMessages[0])
})
})
describe('when the input is a TransactionLike', () => {
describe('when the transaction sent exactly one message', () => {
it('should return the CrossChainMessage sent in the transaction', async () => {
const tx = await l1Messenger.triggerSentMessageEvents([DUMMY_MESSAGE])
const foundCrossChainMessages =
await messenger.getMessagesByTransaction(tx)
const resolved = await messenger.toCrossChainMessage(tx)
expect(resolved).to.deep.equal(foundCrossChainMessages[0])
})
})
describe('when the transaction sent more than one message', () => {
it('should throw an error', async () => {
const messages = [...Array(2)].map(() => {
return DUMMY_MESSAGE
})
const tx = await l1Messenger.triggerSentMessageEvents(messages)
await expect(messenger.toCrossChainMessage(tx)).to.be.rejectedWith(
'expected 1 message, got 2'
)
})
})
describe('when the transaction sent no messages', () => {
it('should throw an error', async () => {
const tx = await l1Messenger.triggerSentMessageEvents([])
await expect(messenger.toCrossChainMessage(tx)).to.be.rejectedWith(
'expected 1 message, got 0'
)
})
})
})
})
describe('getMessageStatus', () => {
let scc: Contract
let l1Messenger: Contract
let l2Messenger: Contract
let messenger: CrossChainMessenger
beforeEach(async () => {
// TODO: Get rid of the nested awaits here. Could be a good first issue for someone.
scc = (await (await ethers.getContractFactory('MockSCC')).deploy()) as any
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
StateCommitmentChain: scc.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
},
},
})
})
const sendAndGetDummyMessage = async (direction: MessageDirection) => {
const mockMessenger =
direction === MessageDirection.L1_TO_L2 ? l1Messenger : l2Messenger
const tx = await mockMessenger.triggerSentMessageEvents([DUMMY_MESSAGE])
return (
await messenger.getMessagesByTransaction(tx, {
direction,
})
)[0]
}
const submitStateRootBatchForMessage = async (
message: CrossChainMessage
) => {
await scc.setSBAParams({
batchIndex: 0,
batchRoot: ethers.constants.HashZero,
batchSize: 1,
prevTotalElements: message.blockNumber,
extraData: '0x',
})
await scc.appendStateBatch([ethers.constants.HashZero], 0)
}
describe('when the message is an L1 => L2 message', () => {
describe('when the message has not been executed on L2 yet', () => {
it('should return a status of UNCONFIRMED_L1_TO_L2_MESSAGE', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L1_TO_L2
)
expect(await messenger.getMessageStatus(message)).to.equal(
MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE
)
})
})
describe('when the message has been executed on L2', () => {
it('should return a status of RELAYED', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L1_TO_L2
)
await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
expect(await messenger.getMessageStatus(message)).to.equal(
MessageStatus.RELAYED
)
})
})
describe('when the message has been executed but failed', () => {
it('should return a status of FAILED_L1_TO_L2_MESSAGE', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L1_TO_L2
)
await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
])
expect(await messenger.getMessageStatus(message)).to.equal(
MessageStatus.FAILED_L1_TO_L2_MESSAGE
)
})
})
})
describe('when the message is an L2 => L1 message', () => {
describe('when the message state root has not been published', () => {
it('should return a status of STATE_ROOT_NOT_PUBLISHED', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
expect(await messenger.getMessageStatus(message)).to.equal(
MessageStatus.STATE_ROOT_NOT_PUBLISHED
)
})
})
describe('when the message state root is still in the challenge period', () => {
it('should return a status of IN_CHALLENGE_PERIOD', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
describe('CrossChainMessenger', () => {
let l1Signer: any
let l2Signer: any
before(async () => {
;[l1Signer, l2Signer] = await ethers.getSigners()
await submitStateRootBatchForMessage(message)
expect(await messenger.getMessageStatus(message)).to.equal(
MessageStatus.IN_CHALLENGE_PERIOD
)
})
})
describe('sendMessage', () => {
describe('when the message is no longer in the challenge period', () => {
describe('when the message has been relayed successfully', () => {
it('should return a status of RELAYED', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
await submitStateRootBatchForMessage(message)
const challengePeriod = await messenger.getChallengePeriodSeconds()
ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
ethers.provider.send('evm_mine', [])
await l1Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
expect(await messenger.getMessageStatus(message)).to.equal(
MessageStatus.RELAYED
)
})
})
describe('when the message has been relayed but the relay failed', () => {
it('should return a status of READY_FOR_RELAY', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
await submitStateRootBatchForMessage(message)
const challengePeriod = await messenger.getChallengePeriodSeconds()
ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
ethers.provider.send('evm_mine', [])
await l1Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
])
expect(await messenger.getMessageStatus(message)).to.equal(
MessageStatus.READY_FOR_RELAY
)
})
})
describe('when the message has not been relayed', () => {
it('should return a status of READY_FOR_RELAY', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
await submitStateRootBatchForMessage(message)
const challengePeriod = await messenger.getChallengePeriodSeconds()
ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
ethers.provider.send('evm_mine', [])
expect(await messenger.getMessageStatus(message)).to.equal(
MessageStatus.READY_FOR_RELAY
)
})
})
})
})
describe('when the message does not exist', () => {
// TODO: Figure out if this is the correct behavior. Mark suggests perhaps returning null.
it('should throw an error')
})
})
describe('getMessageReceipt', () => {
let l1Bridge: Contract
let l2Bridge: Contract
let l1Messenger: Contract
let l2Messenger: Contract
let provider: CrossChainProvider
let messenger: CrossChainMessenger
beforeEach(async () => {
l1Messenger = (await (
......@@ -29,25 +933,422 @@ describe('CrossChainMessenger', () => {
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l1Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l1Messenger.address)) as any
l2Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l2Messenger.address)) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
L1StandardBridge: l1Bridge.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
L2StandardBridge: l2Bridge.address,
},
},
})
})
describe('when the message has been relayed', () => {
describe('when the relay was successful', () => {
it('should return the receipt of the transaction that relayed the message', 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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
const tx = await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
const messageReceipt = await messenger.getMessageReceipt(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 relay failed', () => {
it('should return the receipt of the transaction that attempted to relay the message', 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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
const tx = await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
])
const messageReceipt = await messenger.getMessageReceipt(message)
expect(messageReceipt.receiptStatus).to.equal(0)
expect(
omit(messageReceipt.transactionReceipt, 'confirmations')
).to.deep.equal(
omit(
await ethers.provider.getTransactionReceipt(tx.hash),
'confirmations'
)
)
})
})
describe('when the relay failed more than once', () => {
it('should return the receipt of the last transaction that attempted to relay the message', 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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
])
const tx = await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
])
const messageReceipt = await messenger.getMessageReceipt(message)
expect(messageReceipt.receiptStatus).to.equal(0)
expect(
omit(messageReceipt.transactionReceipt, 'confirmations')
).to.deep.equal(
omit(
await ethers.provider.getTransactionReceipt(tx.hash),
'confirmations'
)
)
})
})
})
describe('when the message has not been relayed', () => {
it('should return null', 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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
await l2Messenger.doNothing()
const messageReceipt = await messenger.getMessageReceipt(message)
expect(messageReceipt).to.equal(null)
})
})
// TODO: Go over all of these tests and remove the empty functions so we can accurately keep
// track of
})
describe('waitForMessageReceipt', () => {
let l2Messenger: Contract
let messenger: CrossChainMessenger
beforeEach(async () => {
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: ethers.provider,
l1ChainId: 31337,
contracts: {
l2: {
L2CrossDomainMessenger: l2Messenger.address,
},
},
})
})
describe('when the message receipt already exists', () => {
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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
const tx = await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
const messageReceipt = await messenger.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 no extra options are provided', () => {
it('should wait for the receipt to be published', 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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
setTimeout(async () => {
await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
}, 5000)
const tick = Date.now()
const messageReceipt = await messenger.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', () => {
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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
await expect(
messenger.waitForMessageReceipt(message, {
timeoutMs: 10000,
})
).to.be.rejectedWith('timed out waiting for message receipt')
})
})
})
})
describe('estimateL2MessageGasLimit', () => {
let messenger: CrossChainMessenger
beforeEach(async () => {
messenger = new CrossChainMessenger({
l1SignerOrProvider: ethers.provider,
l2SignerOrProvider: ethers.provider,
l1ChainId: 31337,
})
})
describe('when the message is an L1 to L2 message', () => {
it('should return an accurate gas estimate plus a ~20% buffer', 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 estimate = await ethers.provider.estimateGas({
to: message.target,
from: message.sender,
data: message.message,
})
// Approximately 20% greater than the estimate, +/- 1%.
expectApprox(
await messenger.estimateL2MessageGasLimit(message),
estimate.mul(120).div(100),
{
percentUpperDeviation: 1,
percentLowerDeviation: 1,
}
)
})
it('should return an accurate gas estimate when a custom buffer is provided', 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 estimate = await ethers.provider.estimateGas({
to: message.target,
from: message.sender,
data: message.message,
})
// Approximately 30% greater than the estimate, +/- 1%.
expectApprox(
await messenger.estimateL2MessageGasLimit(message, {
bufferPercent: 30,
}),
estimate.mul(130).div(100),
{
percentUpperDeviation: 1,
percentLowerDeviation: 1,
}
)
})
})
describe('when the message is an L2 to L1 message', () => {
it('should throw an error', async () => {
const message = {
direction: MessageDirection.L2_TO_L1,
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),
}
await expect(messenger.estimateL2MessageGasLimit(message)).to.be
.rejected
})
})
})
describe('estimateMessageWaitTimeBlocks', () => {
describe('when the message exists', () => {
describe('when the message is an L1 => L2 message', () => {
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'
)
})
describe('when the message has been executed on L2', () => {
it('should return 0')
})
})
describe('when the message is an L2 => L1 message', () => {
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'
)
})
describe('when the state root is within 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', () => {
it('should return 0')
})
})
})
describe('when the message does not exist', () => {
it('should throw an error')
})
})
describe('estimateMessageWaitTimeSeconds', () => {
it(
'should be the result of estimateMessageWaitTimeBlocks multiplied by the L1 block time'
)
})
describe('sendMessage', () => {
let l1Messenger: Contract
let l2Messenger: Contract
let messenger: CrossChainMessenger
beforeEach(async () => {
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
messenger = new CrossChainMessenger({
provider,
l1Signer,
l2Signer,
l1SignerOrProvider: l1Signer,
l2SignerOrProvider: l2Signer,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
},
},
})
})
......@@ -60,7 +1361,7 @@ describe('CrossChainMessenger', () => {
message: '0x' + '22'.repeat(32),
}
const estimate = await provider.estimateL2MessageGasLimit(message)
const estimate = await messenger.estimateL2MessageGasLimit(message)
await expect(messenger.sendMessage(message))
.to.emit(l1Messenger, 'SentMessage')
.withArgs(
......@@ -122,7 +1423,6 @@ describe('CrossChainMessenger', () => {
describe('resendMessage', () => {
let l1Messenger: Contract
let l2Messenger: Contract
let provider: CrossChainProvider
let messenger: CrossChainMessenger
beforeEach(async () => {
l1Messenger = (await (
......@@ -132,9 +1432,9 @@ describe('CrossChainMessenger', () => {
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
messenger = new CrossChainMessenger({
l1SignerOrProvider: l1Signer,
l2SignerOrProvider: l2Signer,
l1ChainId: 31337,
contracts: {
l1: {
......@@ -145,12 +1445,6 @@ describe('CrossChainMessenger', () => {
},
},
})
messenger = new CrossChainMessenger({
provider,
l1Signer,
l2Signer,
})
})
describe('when resending an L1 to L2 message', () => {
......@@ -219,7 +1513,6 @@ describe('CrossChainMessenger', () => {
let l2Messenger: Contract
let l1Bridge: Contract
let l2Bridge: Contract
let provider: CrossChainProvider
let messenger: CrossChainMessenger
beforeEach(async () => {
l1Messenger = (await (
......@@ -235,9 +1528,9 @@ describe('CrossChainMessenger', () => {
await ethers.getContractFactory('MockBridge')
).deploy(l2Messenger.address)) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
messenger = new CrossChainMessenger({
l1SignerOrProvider: l1Signer,
l2SignerOrProvider: l2Signer,
l1ChainId: 31337,
contracts: {
l1: {
......@@ -257,12 +1550,6 @@ describe('CrossChainMessenger', () => {
},
},
})
messenger = new CrossChainMessenger({
provider,
l1Signer,
l2Signer,
})
})
it('should trigger the deposit ETH function with the given amount', async () => {
......@@ -282,7 +1569,6 @@ describe('CrossChainMessenger', () => {
let l2Messenger: Contract
let l1Bridge: Contract
let l2Bridge: Contract
let provider: CrossChainProvider
let messenger: CrossChainMessenger
beforeEach(async () => {
l1Messenger = (await (
......@@ -298,9 +1584,9 @@ describe('CrossChainMessenger', () => {
await ethers.getContractFactory('MockBridge')
).deploy(l2Messenger.address)) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
messenger = new CrossChainMessenger({
l1SignerOrProvider: l1Signer,
l2SignerOrProvider: l2Signer,
l1ChainId: 31337,
contracts: {
l1: {
......@@ -320,15 +1606,9 @@ describe('CrossChainMessenger', () => {
},
},
})
messenger = new CrossChainMessenger({
provider,
l1Signer,
l2Signer,
})
})
it('should trigger the deposit ETH function with the given amount', async () => {
it('should trigger the withdraw ETH function with the given amount', async () => {
await expect(messenger.withdrawETH(100000))
.to.emit(l2Bridge, 'WithdrawalInitiated')
.withArgs(
......
import { Provider } from '@ethersproject/abstract-provider'
import { expectApprox } from '@eth-optimism/core-utils'
import { Contract } from 'ethers'
import { ethers } from 'hardhat'
import { expect } from './setup'
import {
CrossChainProvider,
MessageDirection,
CONTRACT_ADDRESSES,
hashCrossChainMessage,
omit,
MessageStatus,
CrossChainMessage,
StandardBridgeAdapter,
} from '../src'
import { DUMMY_MESSAGE } from './helpers'
describe('CrossChainProvider', () => {
describe('construction', () => {
describe('when given an ethers provider for the L1 provider', () => {
it('should use the provider as the L1 provider', () => {
const provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 1,
})
expect(provider.l1Provider).to.equal(ethers.provider)
})
})
describe('when given an ethers provider for the L2 provider', () => {
it('should use the provider as the L2 provider', () => {
const provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 1,
})
expect(provider.l2Provider).to.equal(ethers.provider)
})
})
describe('when given a string as the L1 provider', () => {
it('should create a JSON-RPC provider for the L1 provider', () => {
const provider = new CrossChainProvider({
l1Provider: 'https://localhost:8545',
l2Provider: ethers.provider,
l1ChainId: 1,
})
expect(Provider.isProvider(provider.l1Provider)).to.be.true
})
})
describe('when given a string as the L2 provider', () => {
it('should create a JSON-RPC provider for the L2 provider', () => {
const provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: 'https://localhost:8545',
l1ChainId: 1,
})
expect(Provider.isProvider(provider.l2Provider)).to.be.true
})
})
describe('when no custom contract addresses are provided', () => {
describe('when given a known chain ID', () => {
it('should use the contract addresses for the known chain ID', () => {
const provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: 'https://localhost:8545',
l1ChainId: 1,
})
const addresses = CONTRACT_ADDRESSES[1]
for (const [contractName, contractAddress] of Object.entries(
addresses.l1
)) {
const contract = provider.contracts.l1[contractName]
expect(contract.address).to.equal(contractAddress)
}
for (const [contractName, contractAddress] of Object.entries(
addresses.l2
)) {
const contract = provider.contracts.l2[contractName]
expect(contract.address).to.equal(contractAddress)
}
})
})
describe('when given an unknown chain ID', () => {
it('should throw an error', () => {
expect(() => {
new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: 'https://localhost:8545',
l1ChainId: 1234,
})
}).to.throw()
})
})
})
describe('when custom contract addresses are provided', () => {
describe('when given a known chain ID', () => {
it('should use known addresses except where custom addresses are given', () => {
const overrides = {
l1: {
L1CrossDomainMessenger: '0x' + '11'.repeat(20),
},
l2: {
L2CrossDomainMessenger: '0x' + '22'.repeat(20),
},
}
const provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: 'https://localhost:8545',
l1ChainId: 1,
contracts: overrides,
})
const addresses = CONTRACT_ADDRESSES[1]
for (const [contractName, contractAddress] of Object.entries(
addresses.l1
)) {
if (overrides.l1[contractName]) {
const contract = provider.contracts.l1[contractName]
expect(contract.address).to.equal(overrides.l1[contractName])
} else {
const contract = provider.contracts.l1[contractName]
expect(contract.address).to.equal(contractAddress)
}
}
for (const [contractName, contractAddress] of Object.entries(
addresses.l2
)) {
if (overrides.l2[contractName]) {
const contract = provider.contracts.l2[contractName]
expect(contract.address).to.equal(overrides.l2[contractName])
} else {
const contract = provider.contracts.l2[contractName]
expect(contract.address).to.equal(contractAddress)
}
}
})
})
describe('when given an unknown chain ID', () => {
describe('when all L1 addresses are provided', () => {
it('should use custom addresses where provided', () => {
const overrides = {
l1: {
AddressManager: '0x' + '11'.repeat(20),
L1CrossDomainMessenger: '0x' + '12'.repeat(20),
L1StandardBridge: '0x' + '13'.repeat(20),
StateCommitmentChain: '0x' + '14'.repeat(20),
CanonicalTransactionChain: '0x' + '15'.repeat(20),
BondManager: '0x' + '16'.repeat(20),
},
l2: {
L2CrossDomainMessenger: '0x' + '22'.repeat(20),
},
}
const provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: 'https://localhost:8545',
l1ChainId: 1234,
contracts: overrides,
})
const addresses = CONTRACT_ADDRESSES[1]
for (const [contractName, contractAddress] of Object.entries(
addresses.l1
)) {
if (overrides.l1[contractName]) {
const contract = provider.contracts.l1[contractName]
expect(contract.address).to.equal(overrides.l1[contractName])
} else {
const contract = provider.contracts.l1[contractName]
expect(contract.address).to.equal(contractAddress)
}
}
for (const [contractName, contractAddress] of Object.entries(
addresses.l2
)) {
if (overrides.l2[contractName]) {
const contract = provider.contracts.l2[contractName]
expect(contract.address).to.equal(overrides.l2[contractName])
} else {
const contract = provider.contracts.l2[contractName]
expect(contract.address).to.equal(contractAddress)
}
}
})
})
describe('when not all L1 addresses are provided', () => {
it('should throw an error', () => {
expect(() => {
new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: 'https://localhost:8545',
l1ChainId: 1234,
contracts: {
l1: {
// Missing some required L1 addresses
AddressManager: '0x' + '11'.repeat(20),
L1CrossDomainMessenger: '0x' + '12'.repeat(20),
L1StandardBridge: '0x' + '13'.repeat(20),
},
l2: {
L2CrossDomainMessenger: '0x' + '22'.repeat(20),
},
},
})
}).to.throw()
})
})
})
})
})
describe('getMessagesByTransaction', () => {
let l1Messenger: Contract
let l2Messenger: Contract
let provider: CrossChainProvider
beforeEach(async () => {
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
},
},
})
})
describe('when a direction is specified', () => {
describe('when the transaction exists', () => {
describe('when the transaction has messages', () => {
for (const n of [1, 2, 4, 8]) {
it(`should find ${n} messages when the transaction emits ${n} messages`, async () => {
const messages = [...Array(n)].map(() => {
return DUMMY_MESSAGE
})
const tx = await l1Messenger.triggerSentMessageEvents(messages)
const found = await provider.getMessagesByTransaction(tx, {
direction: MessageDirection.L1_TO_L2,
})
expect(found).to.deep.equal(
messages.map((message, i) => {
return {
direction: MessageDirection.L1_TO_L2,
sender: message.sender,
target: message.target,
message: message.message,
messageNonce: ethers.BigNumber.from(message.messageNonce),
gasLimit: ethers.BigNumber.from(message.gasLimit),
logIndex: i,
blockNumber: tx.blockNumber,
transactionHash: tx.hash,
}
})
)
})
}
})
describe('when the transaction has no messages', () => {
it('should find nothing', async () => {
const tx = await l1Messenger.doNothing()
const found = await provider.getMessagesByTransaction(tx, {
direction: MessageDirection.L1_TO_L2,
})
expect(found).to.deep.equal([])
})
})
})
describe('when the transaction does not exist in the specified direction', () => {
it('should throw an error', async () => {
await expect(
provider.getMessagesByTransaction('0x' + '11'.repeat(32), {
direction: MessageDirection.L1_TO_L2,
})
).to.be.rejectedWith('unable to find transaction receipt')
})
})
})
describe('when a direction is not specified', () => {
describe('when the transaction exists only on L1', () => {
describe('when the transaction has messages', () => {
for (const n of [1, 2, 4, 8]) {
it(`should find ${n} messages when the transaction emits ${n} messages`, async () => {
const messages = [...Array(n)].map(() => {
return DUMMY_MESSAGE
})
const tx = await l1Messenger.triggerSentMessageEvents(messages)
const found = await provider.getMessagesByTransaction(tx)
expect(found).to.deep.equal(
messages.map((message, i) => {
return {
direction: MessageDirection.L1_TO_L2,
sender: message.sender,
target: message.target,
message: message.message,
messageNonce: ethers.BigNumber.from(message.messageNonce),
gasLimit: ethers.BigNumber.from(message.gasLimit),
logIndex: i,
blockNumber: tx.blockNumber,
transactionHash: tx.hash,
}
})
)
})
}
})
describe('when the transaction has no messages', () => {
it('should find nothing', async () => {
const tx = await l1Messenger.doNothing()
const found = await provider.getMessagesByTransaction(tx)
expect(found).to.deep.equal([])
})
})
})
describe('when the transaction exists only on L2', () => {
describe('when the transaction has messages', () => {
for (const n of [1, 2, 4, 8]) {
it(`should find ${n} messages when the transaction emits ${n} messages`, () => {
// TODO: Need support for simulating more than one network.
})
}
})
describe('when the transaction has no messages', () => {
it('should find nothing', () => {
// TODO: Need support for simulating more than one network.
})
})
})
describe('when the transaction does not exist', () => {
it('should throw an error', async () => {
await expect(
provider.getMessagesByTransaction('0x' + '11'.repeat(32))
).to.be.rejectedWith('unable to find transaction receipt')
})
})
describe('when the transaction exists on both L1 and L2', () => {
it('should throw an error', async () => {
// TODO: Need support for simulating more than one network.
})
})
})
})
describe('getMessagesByAddress', () => {
describe('when the address has sent messages', () => {
describe('when no direction is specified', () => {
it('should find all messages sent by the address')
})
describe('when a direction is specified', () => {
it('should find all messages only in the given direction')
})
describe('when a block range is specified', () => {
it('should find all messages within the block range')
})
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'
)
})
})
describe('when the address has not sent messages', () => {
it('should find nothing')
})
})
describe('getTokenBridgeMessagesByAddress', () => {
let l1Bridge: Contract
let l2Bridge: Contract
let l1Messenger: Contract
let l2Messenger: Contract
let provider: CrossChainProvider
beforeEach(async () => {
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l1Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l1Messenger.address)) as any
l2Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l2Messenger.address)) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
L1StandardBridge: l1Bridge.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
L2StandardBridge: l2Bridge.address,
},
},
bridges: {
Standard: {
Adapter: StandardBridgeAdapter,
l1Bridge: l1Bridge.address,
l2Bridge: l2Bridge.address,
},
},
})
})
describe('when the address has made deposits or withdrawals', () => {
describe('when a direction of L1 => L2 is specified', () => {
it('should find all deposits made by the address', async () => {
const from = '0x' + '99'.repeat(20)
const deposit = {
l1Token: '0x' + '11'.repeat(20),
l2Token: '0x' + '22'.repeat(20),
from,
to: '0x' + '44'.repeat(20),
amount: ethers.BigNumber.from(1234),
data: '0x1234',
}
const withdrawal = {
l1Token: '0x' + '12'.repeat(20),
l2Token: '0x' + '23'.repeat(20),
from,
to: '0x' + '45'.repeat(20),
amount: ethers.BigNumber.from(5678),
data: '0x5678',
}
await l1Bridge.emitERC20DepositInitiated(deposit)
await l2Bridge.emitWithdrawalInitiated(withdrawal)
const found = await provider.getTokenBridgeMessagesByAddress(from, {
direction: MessageDirection.L1_TO_L2,
})
expect(found.length).to.equal(1)
expect(found[0].amount).to.deep.equal(deposit.amount)
expect(found[0].data).to.deep.equal(deposit.data)
expect(found[0].direction).to.equal(MessageDirection.L1_TO_L2)
expect(found[0].l1Token).to.deep.equal(deposit.l1Token)
expect(found[0].l2Token).to.deep.equal(deposit.l2Token)
expect(found[0].from).to.deep.equal(deposit.from)
expect(found[0].to).to.deep.equal(deposit.to)
})
})
describe('when a direction of L2 => L1 is specified', () => {
it('should find all withdrawals made by the address', async () => {
const from = '0x' + '99'.repeat(20)
const deposit = {
l1Token: '0x' + '11'.repeat(20),
l2Token: '0x' + '22'.repeat(20),
from,
to: '0x' + '44'.repeat(20),
amount: ethers.BigNumber.from(1234),
data: '0x1234',
}
const withdrawal = {
l1Token: '0x' + '12'.repeat(20),
l2Token: '0x' + '23'.repeat(20),
from,
to: '0x' + '45'.repeat(20),
amount: ethers.BigNumber.from(5678),
data: '0x5678',
}
await l1Bridge.emitERC20DepositInitiated(deposit)
await l2Bridge.emitWithdrawalInitiated(withdrawal)
const found = await provider.getTokenBridgeMessagesByAddress(from, {
direction: MessageDirection.L2_TO_L1,
})
expect(found.length).to.equal(1)
expect(found[0].amount).to.deep.equal(withdrawal.amount)
expect(found[0].data).to.deep.equal(withdrawal.data)
expect(found[0].direction).to.equal(MessageDirection.L2_TO_L1)
expect(found[0].l1Token).to.deep.equal(withdrawal.l1Token)
expect(found[0].l2Token).to.deep.equal(withdrawal.l2Token)
expect(found[0].from).to.deep.equal(withdrawal.from)
expect(found[0].to).to.deep.equal(withdrawal.to)
})
})
describe('when no direction is specified', () => {
it('should find all deposits and withdrawals made by the address', async () => {
const from = '0x' + '99'.repeat(20)
const deposit = {
l1Token: '0x' + '11'.repeat(20),
l2Token: '0x' + '22'.repeat(20),
from,
to: '0x' + '44'.repeat(20),
amount: ethers.BigNumber.from(1234),
data: '0x1234',
}
const withdrawal = {
l1Token: '0x' + '12'.repeat(20),
l2Token: '0x' + '23'.repeat(20),
from,
to: '0x' + '45'.repeat(20),
amount: ethers.BigNumber.from(5678),
data: '0x5678',
}
await l1Bridge.emitERC20DepositInitiated(deposit)
await l2Bridge.emitWithdrawalInitiated(withdrawal)
const found = await provider.getTokenBridgeMessagesByAddress(from)
expect(found.length).to.equal(2)
// Check the deposit (deposits get searched first)
expect(found[0].amount).to.deep.equal(deposit.amount)
expect(found[0].data).to.deep.equal(deposit.data)
expect(found[0].direction).to.equal(MessageDirection.L1_TO_L2)
expect(found[0].l1Token).to.deep.equal(deposit.l1Token)
expect(found[0].l2Token).to.deep.equal(deposit.l2Token)
expect(found[0].from).to.deep.equal(deposit.from)
expect(found[0].to).to.deep.equal(deposit.to)
// Check the withdrawal
expect(found[1].amount).to.deep.equal(withdrawal.amount)
expect(found[1].data).to.deep.equal(withdrawal.data)
expect(found[1].direction).to.equal(MessageDirection.L2_TO_L1)
expect(found[1].l1Token).to.deep.equal(withdrawal.l1Token)
expect(found[1].l2Token).to.deep.equal(withdrawal.l2Token)
expect(found[1].from).to.deep.equal(withdrawal.from)
expect(found[1].to).to.deep.equal(withdrawal.to)
})
})
})
describe('when the address has not made any deposits or withdrawals', () => {
it('should find nothing', async () => {
const from = '0x' + '99'.repeat(20)
const found = await provider.getTokenBridgeMessagesByAddress(from)
expect(found).to.deep.equal([])
})
})
})
describe('toCrossChainMessage', () => {
let l1Bridge: Contract
let l2Bridge: Contract
let l1Messenger: Contract
let l2Messenger: Contract
let provider: CrossChainProvider
beforeEach(async () => {
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l1Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l1Messenger.address)) as any
l2Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l2Messenger.address)) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
L1StandardBridge: l1Bridge.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
L2StandardBridge: l2Bridge.address,
},
},
bridges: {
Standard: {
Adapter: StandardBridgeAdapter,
l1Bridge: l1Bridge.address,
l2Bridge: l2Bridge.address,
},
},
})
})
describe('when the input is a CrossChainMessage', () => {
it('should return the input', 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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
expect(await provider.toCrossChainMessage(message)).to.deep.equal(
message
)
})
})
describe('when the input is a TokenBridgeMessage', () => {
// TODO: There are some edge cases here with custom bridges that conform to the interface but
// not to the behavioral spec. Possibly worth testing those. For now this is probably
// sufficient.
it('should return the sent message event that came after the deposit or withdrawal', async () => {
const from = '0x' + '99'.repeat(20)
const deposit = {
l1Token: '0x' + '11'.repeat(20),
l2Token: '0x' + '22'.repeat(20),
from,
to: '0x' + '44'.repeat(20),
amount: ethers.BigNumber.from(1234),
data: '0x1234',
}
const tx = await l1Bridge.emitERC20DepositInitiated(deposit)
const foundCrossChainMessages = await provider.getMessagesByTransaction(
tx
)
const foundTokenBridgeMessages =
await provider.getTokenBridgeMessagesByAddress(from)
const resolved = await provider.toCrossChainMessage(
foundTokenBridgeMessages[0]
)
expect(resolved).to.deep.equal(foundCrossChainMessages[0])
})
})
describe('when the input is a TransactionLike', () => {
describe('when the transaction sent exactly one message', () => {
it('should return the CrossChainMessage sent in the transaction', async () => {
const tx = await l1Messenger.triggerSentMessageEvents([DUMMY_MESSAGE])
const foundCrossChainMessages =
await provider.getMessagesByTransaction(tx)
const resolved = await provider.toCrossChainMessage(tx)
expect(resolved).to.deep.equal(foundCrossChainMessages[0])
})
})
describe('when the transaction sent more than one message', () => {
it('should throw an error', async () => {
const messages = [...Array(2)].map(() => {
return DUMMY_MESSAGE
})
const tx = await l1Messenger.triggerSentMessageEvents(messages)
await expect(provider.toCrossChainMessage(tx)).to.be.rejectedWith(
'expected 1 message, got 2'
)
})
})
describe('when the transaction sent no messages', () => {
it('should throw an error', async () => {
const tx = await l1Messenger.triggerSentMessageEvents([])
await expect(provider.toCrossChainMessage(tx)).to.be.rejectedWith(
'expected 1 message, got 0'
)
})
})
})
})
describe('getMessageStatus', () => {
let scc: Contract
let l1Messenger: Contract
let l2Messenger: Contract
let provider: CrossChainProvider
beforeEach(async () => {
// TODO: Get rid of the nested awaits here. Could be a good first issue for someone.
scc = (await (await ethers.getContractFactory('MockSCC')).deploy()) as any
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
StateCommitmentChain: scc.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
},
},
})
})
const sendAndGetDummyMessage = async (direction: MessageDirection) => {
const messenger =
direction === MessageDirection.L1_TO_L2 ? l1Messenger : l2Messenger
const tx = await messenger.triggerSentMessageEvents([DUMMY_MESSAGE])
return (
await provider.getMessagesByTransaction(tx, {
direction,
})
)[0]
}
const submitStateRootBatchForMessage = async (
message: CrossChainMessage
) => {
await scc.setSBAParams({
batchIndex: 0,
batchRoot: ethers.constants.HashZero,
batchSize: 1,
prevTotalElements: message.blockNumber,
extraData: '0x',
})
await scc.appendStateBatch([ethers.constants.HashZero], 0)
}
describe('when the message is an L1 => L2 message', () => {
describe('when the message has not been executed on L2 yet', () => {
it('should return a status of UNCONFIRMED_L1_TO_L2_MESSAGE', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L1_TO_L2
)
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE
)
})
})
describe('when the message has been executed on L2', () => {
it('should return a status of RELAYED', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L1_TO_L2
)
await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.RELAYED
)
})
})
describe('when the message has been executed but failed', () => {
it('should return a status of FAILED_L1_TO_L2_MESSAGE', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L1_TO_L2
)
await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
])
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.FAILED_L1_TO_L2_MESSAGE
)
})
})
})
describe('when the message is an L2 => L1 message', () => {
describe('when the message state root has not been published', () => {
it('should return a status of STATE_ROOT_NOT_PUBLISHED', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.STATE_ROOT_NOT_PUBLISHED
)
})
})
describe('when the message state root is still in the challenge period', () => {
it('should return a status of IN_CHALLENGE_PERIOD', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
await submitStateRootBatchForMessage(message)
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.IN_CHALLENGE_PERIOD
)
})
})
describe('when the message is no longer in the challenge period', () => {
describe('when the message has been relayed successfully', () => {
it('should return a status of RELAYED', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
await submitStateRootBatchForMessage(message)
const challengePeriod = await provider.getChallengePeriodSeconds()
ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
ethers.provider.send('evm_mine', [])
await l1Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.RELAYED
)
})
})
describe('when the message has been relayed but the relay failed', () => {
it('should return a status of READY_FOR_RELAY', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
await submitStateRootBatchForMessage(message)
const challengePeriod = await provider.getChallengePeriodSeconds()
ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
ethers.provider.send('evm_mine', [])
await l1Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
])
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.READY_FOR_RELAY
)
})
})
describe('when the message has not been relayed', () => {
it('should return a status of READY_FOR_RELAY', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
await submitStateRootBatchForMessage(message)
const challengePeriod = await provider.getChallengePeriodSeconds()
ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
ethers.provider.send('evm_mine', [])
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.READY_FOR_RELAY
)
})
})
})
})
describe('when the message does not exist', () => {
// TODO: Figure out if this is the correct behavior. Mark suggests perhaps returning null.
it('should throw an error')
})
})
describe('getMessageReceipt', () => {
let l1Bridge: Contract
let l2Bridge: Contract
let l1Messenger: Contract
let l2Messenger: Contract
let provider: CrossChainProvider
beforeEach(async () => {
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l1Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l1Messenger.address)) as any
l2Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l2Messenger.address)) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
L1StandardBridge: l1Bridge.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
L2StandardBridge: l2Bridge.address,
},
},
})
})
describe('when the message has been relayed', () => {
describe('when the relay was successful', () => {
it('should return the receipt of the transaction that relayed the message', 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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
const tx = await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
const messageReceipt = await provider.getMessageReceipt(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 relay failed', () => {
it('should return the receipt of the transaction that attempted to relay the message', 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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
const tx = await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
])
const messageReceipt = await provider.getMessageReceipt(message)
expect(messageReceipt.receiptStatus).to.equal(0)
expect(
omit(messageReceipt.transactionReceipt, 'confirmations')
).to.deep.equal(
omit(
await ethers.provider.getTransactionReceipt(tx.hash),
'confirmations'
)
)
})
})
describe('when the relay failed more than once', () => {
it('should return the receipt of the last transaction that attempted to relay the message', 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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
])
const tx = await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
])
const messageReceipt = await provider.getMessageReceipt(message)
expect(messageReceipt.receiptStatus).to.equal(0)
expect(
omit(messageReceipt.transactionReceipt, 'confirmations')
).to.deep.equal(
omit(
await ethers.provider.getTransactionReceipt(tx.hash),
'confirmations'
)
)
})
})
})
describe('when the message has not been relayed', () => {
it('should return null', 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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
await l2Messenger.doNothing()
const messageReceipt = await provider.getMessageReceipt(message)
expect(messageReceipt).to.equal(null)
})
})
// TODO: Go over all of these tests and remove the empty functions so we can accurately keep
// track of
})
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', () => {
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,
gasLimit: 0,
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 no extra options are provided', () => {
it('should wait for the receipt to be published', 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,
gasLimit: 0,
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', () => {
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,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
await expect(
provider.waitForMessageReceipt(message, {
timeoutMs: 10000,
})
).to.be.rejectedWith('timed out waiting for message receipt')
})
})
})
})
describe('estimateL2MessageGasLimit', () => {
let provider: CrossChainProvider
beforeEach(async () => {
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 31337,
})
})
describe('when the message is an L1 to L2 message', () => {
it('should return an accurate gas estimate plus a ~20% buffer', 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 estimate = await ethers.provider.estimateGas({
to: message.target,
from: message.sender,
data: message.message,
})
// Approximately 20% greater than the estimate, +/- 1%.
expectApprox(
await provider.estimateL2MessageGasLimit(message),
estimate.mul(120).div(100),
{
percentUpperDeviation: 1,
percentLowerDeviation: 1,
}
)
})
it('should return an accurate gas estimate when a custom buffer is provided', 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 estimate = await ethers.provider.estimateGas({
to: message.target,
from: message.sender,
data: message.message,
})
// Approximately 30% greater than the estimate, +/- 1%.
expectApprox(
await provider.estimateL2MessageGasLimit(message, {
bufferPercent: 30,
}),
estimate.mul(130).div(100),
{
percentUpperDeviation: 1,
percentLowerDeviation: 1,
}
)
})
})
describe('when the message is an L2 to L1 message', () => {
it('should throw an error', async () => {
const message = {
direction: MessageDirection.L2_TO_L1,
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),
}
await expect(provider.estimateL2MessageGasLimit(message)).to.be.rejected
})
})
})
describe('estimateMessageWaitTimeBlocks', () => {
describe('when the message exists', () => {
describe('when the message is an L1 => L2 message', () => {
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'
)
})
describe('when the message has been executed on L2', () => {
it('should return 0')
})
})
describe('when the message is an L2 => L1 message', () => {
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'
)
})
describe('when the state root is within 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', () => {
it('should return 0')
})
})
})
describe('when the message does not exist', () => {
it('should throw an error')
})
})
describe('estimateMessageWaitTimeSeconds', () => {
it(
'should be the result of estimateMessageWaitTimeBlocks multiplied by the L1 block time'
)
})
})
......@@ -3,17 +3,17 @@ import { Contract } from 'ethers'
import { ethers } from 'hardhat'
import { expect } from '../setup'
import { toProvider, toTransactionHash } from '../../src'
import { toSignerOrProvider, toTransactionHash } from '../../src'
describe('type coercion utils', () => {
describe('toProvider', () => {
describe('toSignerOrProvider', () => {
it('should convert a string to a JsonRpcProvider', () => {
const provider = toProvider('http://localhost:8545')
const provider = toSignerOrProvider('http://localhost:8545')
expect(Provider.isProvider(provider)).to.be.true
})
it('should not do anything with a provider', () => {
const provider = toProvider(ethers.provider)
const provider = toSignerOrProvider(ethers.provider)
expect(provider).to.deep.equal(ethers.provider)
})
})
......
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