Commit 1c6d89be authored by smartcontracts's avatar smartcontracts Committed by GitHub

Merge pull request #2076 from ethereum-optimism/sc/sdk-send-message

feat(sdk): implement sendMessage
parents 22234b1d bc1758ca
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Overrides, Signer, BigNumber } from 'ethers'
import {
TransactionRequest,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import {
CrossChainMessageRequest,
ICrossChainMessenger,
ICrossChainProvider,
L1ToL2Overrides,
MessageLike,
NumberLike,
MessageDirection,
} from './interfaces'
export class CrossChainMessenger implements ICrossChainMessenger {
provider: ICrossChainProvider
l1Signer: Signer
l2Signer: Signer
/**
* Creates a new CrossChainMessenger 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.
*/
constructor(opts: {
provider: ICrossChainProvider
l1Signer: Signer
l2Signer: Signer
}) {
this.provider = opts.provider
this.l1Signer = opts.l1Signer
this.l2Signer = opts.l2Signer
}
public async sendMessage(
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
): Promise<TransactionResponse> {
const tx = await this.populateTransaction.sendMessage(message, overrides)
if (message.direction === MessageDirection.L1_TO_L2) {
return this.l1Signer.sendTransaction(tx)
} else {
return this.l2Signer.sendTransaction(tx)
}
}
public async resendMessage(
message: MessageLike,
messageGasLimit: NumberLike,
overrides?: Overrides
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
public async finalizeMessage(
message: MessageLike,
overrides?: Overrides
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
public async depositETH(
amount: NumberLike,
overrides?: L1ToL2Overrides
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
public async withdrawETH(
amount: NumberLike,
overrides?: Overrides
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
populateTransaction = {
sendMessage: async (
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
): Promise<TransactionRequest> => {
if (message.direction === MessageDirection.L1_TO_L2) {
return this.provider.contracts.l1.L1CrossDomainMessenger.connect(
this.l1Signer
).populateTransaction.sendMessage(
message.target,
message.message,
overrides?.l2GasLimit ||
(await this.provider.estimateL2MessageGasLimit(message))
)
} else {
return this.provider.contracts.l2.L2CrossDomainMessenger.connect(
this.l2Signer
).populateTransaction.sendMessage(
message.target,
message.message,
0 // Gas limit goes unused when sending from L2 to L1
)
}
},
resendMessage: async (
message: MessageLike,
messageGasLimit: NumberLike,
overrides?: Overrides
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
finalizeMessage: async (
message: MessageLike,
overrides?: Overrides
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
depositETH: async (
amount: NumberLike,
overrides?: L1ToL2Overrides
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
withdrawETH: async (
amount: NumberLike,
overrides?: Overrides
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
}
estimateGas = {
sendMessage: async (
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
): Promise<BigNumber> => {
const tx = await this.populateTransaction.sendMessage(message, overrides)
if (message.direction === MessageDirection.L1_TO_L2) {
return this.provider.l1Provider.estimateGas(tx)
} else {
return this.provider.l2Provider.estimateGas(tx)
}
},
resendMessage: async (
message: MessageLike,
messageGasLimit: NumberLike,
overrides?: Overrides
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
finalizeMessage: async (
message: MessageLike,
overrides?: Overrides
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
depositETH: async (
amount: NumberLike,
overrides?: L1ToL2Overrides
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
withdrawETH: async (
amount: NumberLike,
overrides?: Overrides
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
}
}
......@@ -12,11 +12,13 @@ import {
OEContracts,
OEContractsLike,
MessageLike,
MessageRequestLike,
TransactionLike,
AddressLike,
NumberLike,
ProviderLike,
CrossChainMessage,
CrossChainMessageRequest,
MessageDirection,
MessageStatus,
TokenBridgeMessage,
......@@ -41,7 +43,6 @@ export class CrossChainProvider implements ICrossChainProvider {
public l1Provider: Provider
public l2Provider: Provider
public l1ChainId: number
public l1BlockTime: number
public contracts: OEContracts
public bridges: CustomBridges
......@@ -52,7 +53,6 @@ export class CrossChainProvider implements ICrossChainProvider {
* @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.l1BlockTime Optional L1 block time in seconds. Defaults to 15 seconds.
* @param opts.contracts Optional contract address overrides.
* @param opts.bridges Optional bridge address list.
*/
......@@ -60,16 +60,12 @@ export class CrossChainProvider implements ICrossChainProvider {
l1Provider: ProviderLike
l2Provider: ProviderLike
l1ChainId: NumberLike
l1BlockTime?: NumberLike
contracts?: DeepPartial<OEContractsLike>
bridges?: Partial<CustomBridgesLike>
}) {
this.l1Provider = toProvider(opts.l1Provider)
this.l2Provider = toProvider(opts.l2Provider)
this.l1ChainId = toBigNumber(opts.l1ChainId).toNumber()
this.l1BlockTime = opts.l1BlockTime
? toBigNumber(opts.l1ChainId).toNumber()
: 15
this.contracts = getAllOEContracts(this.l1ChainId, {
l1SignerOrProvider: this.l1Provider,
l2SignerOrProvider: this.l2Provider,
......@@ -364,9 +360,12 @@ export class CrossChainProvider implements ICrossChainProvider {
if (stateRoot === null) {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
} else {
const challengePeriod = await this.getChallengePeriodBlocks()
const latestBlock = await this.l1Provider.getBlockNumber()
if (stateRoot.blockNumber + challengePeriod > latestBlock) {
const challengePeriod = await this.getChallengePeriodSeconds()
const targetBlock = await this.l1Provider.getBlock(
stateRoot.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
......@@ -464,12 +463,21 @@ export class CrossChainProvider implements ICrossChainProvider {
}
public async estimateL2MessageGasLimit(
message: MessageLike,
message: MessageRequestLike,
opts?: {
bufferPercent?: number
from?: string
}
): Promise<BigNumber> {
const resolved = await this.toCrossChainMessage(message)
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) {
......@@ -477,7 +485,7 @@ export class CrossChainProvider implements ICrossChainProvider {
}
const estimate = await this.l2Provider.estimateGas({
from: resolved.sender,
from,
to: resolved.target,
data: resolved.message,
})
......@@ -493,24 +501,12 @@ export class CrossChainProvider implements ICrossChainProvider {
throw new Error('Not implemented')
}
public async estimateMessageWaitTimeBlocks(
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 getChallengePeriodBlocks(): Promise<number> {
return Math.ceil(
(await this.getChallengePeriodSeconds()) / this.l1BlockTime
)
}
public async getMessageStateRoot(
message: MessageLike
): Promise<StateRoot | null> {
......
export * from './interfaces'
export * from './utils'
export * from './cross-chain-provider'
export * from './cross-chain-messenger'
import { Overrides, Signer } from 'ethers'
import { Overrides, Signer, BigNumber } from 'ethers'
import {
TransactionRequest,
TransactionResponse,
......@@ -22,9 +22,14 @@ export interface ICrossChainMessenger {
provider: ICrossChainProvider
/**
* Signer that will carry out L1/L2 transactions.
* Signer that will carry out L1 transactions.
*/
signer: Signer
l1Signer: Signer
/**
* Signer that will carry out L2 transactions.
*/
l2Signer: Signer
/**
* Sends a given cross chain message. Where the message is sent depends on the direction attached
......@@ -107,7 +112,7 @@ export interface ICrossChainMessenger {
sendMessage: (
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
) => Promise<TransactionResponse>
) => Promise<TransactionRequest>
/**
* Generates a transaction that resends a given cross chain message. Only applies to L1 to L2
......@@ -178,7 +183,7 @@ export interface ICrossChainMessenger {
sendMessage: (
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
) => Promise<TransactionResponse>
) => Promise<BigNumber>
/**
* Estimates gas required to resend a cross chain message. Only applies to L1 to L2 messages.
......@@ -192,7 +197,7 @@ export interface ICrossChainMessenger {
message: MessageLike,
messageGasLimit: NumberLike,
overrides?: Overrides
): Promise<TransactionRequest>
): Promise<BigNumber>
/**
* Estimates gas required to finalize a cross chain message. Only applies to L2 to L1 messages.
......@@ -204,7 +209,7 @@ export interface ICrossChainMessenger {
finalizeMessage(
message: MessageLike,
overrides?: Overrides
): Promise<TransactionRequest>
): Promise<BigNumber>
/**
* Estimates gas required to deposit some ETH into the L2 chain.
......@@ -216,7 +221,7 @@ export interface ICrossChainMessenger {
depositETH(
amount: NumberLike,
overrides?: L1ToL2Overrides
): Promise<TransactionRequest>
): Promise<BigNumber>
/**
* Estimates gas required to withdraw some ETH back to the L1 chain.
......@@ -225,9 +230,6 @@ export interface ICrossChainMessenger {
* @param overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens.
*/
withdrawETH(
amount: NumberLike,
overrides?: Overrides
): Promise<TransactionRequest>
withdrawETH(amount: NumberLike, overrides?: Overrides): Promise<BigNumber>
}
}
......@@ -3,6 +3,7 @@ import { Provider, BlockTag } from '@ethersproject/abstract-provider'
import {
MessageLike,
MessageRequestLike,
TransactionLike,
AddressLike,
NumberLike,
......@@ -205,12 +206,14 @@ export interface ICrossChainProvider {
* @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: MessageLike,
message: MessageRequestLike,
opts?: {
bufferPercent?: number
from?: string
}
): Promise<BigNumber>
......@@ -225,17 +228,6 @@ export interface ICrossChainProvider {
*/
estimateMessageWaitTimeSeconds(message: MessageLike): Promise<number>
/**
* Returns the estimated amount of time before the message can be executed (in L1 blocks).
* 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 blocks) before the message can be executed.
*/
estimateMessageWaitTimeBlocks(message: MessageLike): Promise<number>
/**
* Queries the current challenge period in seconds from the StateCommitmentChain.
*
......@@ -243,14 +235,6 @@ export interface ICrossChainProvider {
*/
getChallengePeriodSeconds(): Promise<number>
/**
* Queries the current challenge period in blocks from the StateCommitmentChain. Estimation is
* based on the challenge period in seconds divided by the L1 block time.
*
* @returns Current challenge period in blocks.
*/
getChallengePeriodBlocks(): 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
......
......@@ -143,7 +143,6 @@ export interface CrossChainMessageRequest {
direction: MessageDirection
target: string
message: string
l2GasLimit: NumberLike
}
/**
......@@ -235,7 +234,7 @@ export interface StateRootBatch {
* limit field (gas used depends on the amount of gas provided).
*/
export type L1ToL2Overrides = Overrides & {
l2GasLimit: NumberLike
l2GasLimit?: NumberLike
}
/**
......@@ -244,13 +243,22 @@ export type L1ToL2Overrides = Overrides & {
export type TransactionLike = string | TransactionReceipt | TransactionResponse
/**
* Stuff that can be coerced into a message.
* Stuff that can be coerced into a CrossChainMessage.
*/
export type MessageLike =
| CrossChainMessage
| TransactionLike
| TokenBridgeMessage
/**
* Stuff that can be coerced into a CrossChainMessageRequest.
*/
export type MessageRequestLike =
| CrossChainMessageRequest
| CrossChainMessage
| TransactionLike
| TokenBridgeMessage
/**
* Stuff that can be coerced into a provider.
*/
......
......@@ -7,13 +7,22 @@ contract MockMessenger is ICrossDomainMessenger {
return address(0);
}
uint256 public nonce;
// Empty function to satisfy the interface.
function sendMessage(
address _target,
bytes calldata _message,
uint32 _gasLimit
) public {
return;
emit SentMessage(
_target,
msg.sender,
_message,
nonce,
_gasLimit
);
nonce++;
}
struct SentMessageEventParams {
......
import './setup'
import { Contract } from 'ethers'
import { ethers } from 'hardhat'
import { expect } from './setup'
import {
CrossChainProvider,
CrossChainMessenger,
MessageDirection,
} from '../src'
describe('CrossChainMessenger', () => {
let l1Signer: any
let l2Signer: any
before(async () => {
;[l1Signer, l2Signer] = await ethers.getSigners()
})
describe('sendMessage', () => {
let l1Messenger: Contract
let l2Messenger: Contract
let provider: CrossChainProvider
let messenger: CrossChainMessenger
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,
},
},
})
messenger = new CrossChainMessenger({
provider,
l1Signer,
l2Signer,
})
})
describe('when the message is an L1 to L2 message', () => {
describe('when no l2GasLimit is provided', () => {
it('should send a message with an estimated l2GasLimit')
it('should send a message with an estimated l2GasLimit', async () => {
const message = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
message: '0x' + '22'.repeat(32),
}
const estimate = await provider.estimateL2MessageGasLimit(message)
await expect(messenger.sendMessage(message))
.to.emit(l1Messenger, 'SentMessage')
.withArgs(
message.target,
await l1Signer.getAddress(),
message.message,
0,
estimate
)
})
})
describe('when an l2GasLimit is provided', () => {
it('should send a message with the provided l2GasLimit')
it('should send a message with the provided l2GasLimit', async () => {
const message = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
message: '0x' + '22'.repeat(32),
}
await expect(
messenger.sendMessage(message, {
l2GasLimit: 1234,
})
)
.to.emit(l1Messenger, 'SentMessage')
.withArgs(
message.target,
await l1Signer.getAddress(),
message.message,
0,
1234
)
})
})
})
describe('when the message is an L2 to L1 message', () => {
it('should send a message', async () => {
const message = {
direction: MessageDirection.L2_TO_L1,
target: '0x' + '11'.repeat(20),
message: '0x' + '22'.repeat(32),
}
await expect(messenger.sendMessage(message))
.to.emit(l2Messenger, 'SentMessage')
.withArgs(
message.target,
await l2Signer.getAddress(),
message.message,
0,
0
)
})
})
})
......
......@@ -898,10 +898,9 @@ describe('CrossChainProvider', () => {
await submitStateRootBatchForMessage(message)
const challengePeriod = await provider.getChallengePeriodBlocks()
for (let x = 0; x < challengePeriod + 1; x++) {
await ethers.provider.send('evm_mine', [])
}
const challengePeriod = await provider.getChallengePeriodSeconds()
ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
ethers.provider.send('evm_mine', [])
await l1Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
......@@ -921,10 +920,9 @@ describe('CrossChainProvider', () => {
await submitStateRootBatchForMessage(message)
const challengePeriod = await provider.getChallengePeriodBlocks()
for (let x = 0; x < challengePeriod + 1; x++) {
await ethers.provider.send('evm_mine', [])
}
const challengePeriod = await provider.getChallengePeriodSeconds()
ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
ethers.provider.send('evm_mine', [])
await l1Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
......@@ -944,10 +942,9 @@ describe('CrossChainProvider', () => {
await submitStateRootBatchForMessage(message)
const challengePeriod = await provider.getChallengePeriodBlocks()
for (let x = 0; x < challengePeriod + 1; x++) {
await ethers.provider.send('evm_mine', [])
}
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
......
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