Commit c9991823 authored by Kelvin Fichter's avatar Kelvin Fichter

feat: tests for CrossChainProvider constructor and getMessagesByTransaction

parent 654a4907
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
Provider,
BlockTag,
TransactionReceipt,
} from '@ethersproject/abstract-provider'
import { BigNumber } from 'ethers'
import {
ICrossChainProvider,
OEContracts,
OEContractsLike,
MessageLike,
TransactionLike,
AddressLike,
NumberLike,
ProviderLike,
CrossChainMessage,
MessageDirection,
MessageStatus,
TokenBridgeMessage,
MessageReceipt,
} from './interfaces'
import {
toProvider,
toBigNumber,
toTransactionHash,
DeepPartial,
getAllOEContracts,
} from './utils'
export class CrossChainProvider implements ICrossChainProvider {
public l1Provider: Provider
public l2Provider: Provider
public l1ChainId: number
public contracts: OEContracts
/**
* 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.
*/
constructor(opts: {
l1Provider: ProviderLike
l2Provider: ProviderLike
l1ChainId: NumberLike
contracts?: DeepPartial<OEContractsLike>
}) {
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,
})
}
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,
}
})
}
public async getMessagesByAddress(
address: AddressLike,
opts?: {
direction?: MessageDirection
fromBlock?: NumberLike
toBlock?: NumberLike
}
): Promise<CrossChainMessage[]> {
throw new Error('Not implemented')
}
public async getTokenBridgeMessagesByAddress(
address: AddressLike,
opts?: {
direction?: MessageDirection
fromBlock?: BlockTag
toBlock?: BlockTag
}
): Promise<TokenBridgeMessage[]> {
throw new Error('Not implemented')
}
public async getMessageStatus(message: MessageLike): Promise<MessageStatus> {
throw new Error('Not implemented')
}
public async getMessageReceipt(
message: MessageLike
): Promise<MessageReceipt> {
throw new Error('Not implemented')
}
public async waitForMessageReciept(
message: MessageLike,
opts?: {
confirmations?: number
pollIntervalMs?: number
timeoutMs?: number
}
): Promise<MessageReceipt> {
throw new Error('Not implemented')
}
public async estimateL2MessageGasLimit(
message: MessageLike
): Promise<BigNumber> {
throw new Error('Not implemented')
}
public async estimateMessageWaitTimeSeconds(
message: MessageLike
): Promise<number> {
throw new Error('Not implemented')
}
public async estimateMessageWaitTimeBlocks(
message: MessageLike
): Promise<number> {
throw new Error('Not implemented')
}
}
export * from './interfaces'
export * from './utils'
export * from './cross-chain-provider'
......@@ -80,6 +80,20 @@ export const CONTRACT_ADDRESSES: {
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
},
// Hardhat local
// TODO: Get the actual addresses for this, temporary addresses here are fine for now until we
// start using this package in the integration tests.
31337: {
l1: {
AddressManager: '0x2F7E3cAC91b5148d336BbffB224B4dC79F09f01D',
L1CrossDomainMessenger: '0xEcC89b9EDD804850C4F343A278Be902be11AaF42',
L1StandardBridge: '0x73298186A143a54c20ae98EEE5a025bD5979De02',
StateCommitmentChain: '0x1afcA918eff169eE20fF8AB6Be75f3E872eE1C1A',
CanonicalTransactionChain: '0x2ebA8c4EfDB39A8Cd8f9eD65c50ec079f7CEBD81',
BondManager: '0xE5AE60bD6F8DEe4D0c2BC9268e23B92F1cacC58F',
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
},
}
/**
......
pragma solidity ^0.8.9;
// Right now this is copy/pasted from the contracts package. We need to do this because we don't
// currently copy the contracts into the root of the contracts package in the correct way until
// we bundle the contracts package for publication. As a result, we can't properly use the
// package the way we want to from inside the monorepo (yet). Needs to be fixed as part of a
// separate pull request.
interface ICrossDomainMessenger {
/**********
* Events *
**********/
event SentMessage(
address indexed target,
address sender,
bytes message,
uint256 messageNonce,
uint256 gasLimit
);
event RelayedMessage(bytes32 indexed msgHash);
event FailedRelayedMessage(bytes32 indexed msgHash);
/*************
* Variables *
*************/
function xDomainMessageSender() external view returns (address);
/********************
* Public Functions *
********************/
/**
* Sends a cross domain message to the target messenger.
* @param _target Target contract address.
* @param _message Message to send to the target.
* @param _gasLimit Gas limit for the provided message.
*/
function sendMessage(
address _target,
bytes calldata _message,
uint32 _gasLimit
) external;
}
pragma solidity ^0.8.9;
import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";
contract MockMessenger is ICrossDomainMessenger {
function xDomainMessageSender() public view returns (address) {
return address(0);
}
// Empty function to satisfy the interface.
function sendMessage(
address _target,
bytes calldata _message,
uint32 _gasLimit
) public {
return;
}
struct SentMessageEventParams {
address target;
address sender;
bytes message;
uint256 messageNonce;
uint256 gasLimit;
}
function doNothing() public {
return;
}
function triggerSentMessageEvents(
SentMessageEventParams[] memory _params
) public {
for (uint256 i = 0; i < _params.length; i++) {
emit SentMessage(
_params[i].target,
_params[i].sender,
_params[i].message,
_params[i].messageNonce,
_params[i].gasLimit
);
}
}
function triggerRelayedMessageEvents(
bytes32[] memory _params
) public {
for (uint256 i = 0; i < _params.length; i++) {
emit RelayedMessage(_params[i]);
}
}
function triggerFailedRelayedMessageEvents(
bytes32[] memory _params
) public {
for (uint256 i = 0; i < _params.length; i++) {
emit FailedRelayedMessage(_params[i]);
}
}
}
/* eslint-disable @typescript-eslint/no-empty-function */
import './setup'
import { expect } from './setup'
import { Provider } from '@ethersproject/abstract-provider'
import { Contract } from 'ethers'
import { ethers } from 'hardhat'
import {
CrossChainProvider,
MessageDirection,
CONTRACT_ADDRESSES,
} from '../src'
describe('CrossChainProvider', () => {
describe('construction', () => {
describe('when given an ethers provider for the L1 provider', () => {
it('should use the provider as 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', () => {})
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', () => {})
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', () => {})
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', () => {})
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', () => {})
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', () => {})
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', () => {
describe('when not all L2 addresses are provided', () => {
it('should use the custom L1 addresses and the default L2 addresses', () => {})
})
describe('when all L2 addresses are provided', () => {
it('should use the custom addresses everywhere', () => {})
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', () => {})
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`, () => {})
it(`should find ${n} messages when the transaction emits ${n} messages`, async () => {
const messages = [...Array(n)].map(() => {
return {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 100000,
}
})
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) => {
return {
direction: MessageDirection.L1_TO_L2,
sender: message.sender,
target: message.target,
message: message.message,
messageNonce: ethers.BigNumber.from(message.messageNonce),
}
})
)
})
}
})
describe('when the transaction has no messages', () => {
it('should find nothing', () => {})
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', () => {
it('should throw an error', () => {})
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')
})
})
})
......@@ -75,33 +303,71 @@ describe('CrossChainProvider', () => {
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`, () => {})
it(`should find ${n} messages when the transaction emits ${n} messages`, async () => {
const messages = [...Array(n)].map(() => {
return {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 100000,
}
})
const tx = await l1Messenger.triggerSentMessageEvents(messages)
const found = await provider.getMessagesByTransaction(tx)
expect(found).to.deep.equal(
messages.map((message) => {
return {
direction: MessageDirection.L1_TO_L2,
sender: message.sender,
target: message.target,
message: message.message,
messageNonce: ethers.BigNumber.from(message.messageNonce),
}
})
)
})
}
})
describe('when the transaction has no messages', () => {
it('should find nothing', () => {})
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`, () => {})
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', () => {})
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', () => {})
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', () => {})
it('should throw an error', async () => {
// TODO: Need support for simulating more than one network.
})
})
})
})
......
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