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

Merge pull request #1943 from ethereum-optimism/sc/sdk-token-bridge-query-tests

feat(sdk): add tests for getTokenBridgeMessagesByAddress
parents afa24d4a 21ee8659
......@@ -4,7 +4,7 @@ import {
BlockTag,
TransactionReceipt,
} from '@ethersproject/abstract-provider'
import { BigNumber } from 'ethers'
import { ethers, BigNumber, Event } from 'ethers'
import {
ICrossChainProvider,
OEContracts,
......@@ -19,6 +19,8 @@ import {
MessageStatus,
TokenBridgeMessage,
MessageReceipt,
CustomBridges,
CustomBridgesLike,
} from './interfaces'
import {
toProvider,
......@@ -26,6 +28,7 @@ import {
toTransactionHash,
DeepPartial,
getAllOEContracts,
getCustomBridges,
} from './utils'
export class CrossChainProvider implements ICrossChainProvider {
......@@ -33,6 +36,7 @@ export class CrossChainProvider implements ICrossChainProvider {
public l2Provider: Provider
public l1ChainId: number
public contracts: OEContracts
public bridges: CustomBridges
/**
* Creates a new CrossChainProvider instance.
......@@ -48,6 +52,7 @@ export class CrossChainProvider implements ICrossChainProvider {
l2Provider: ProviderLike
l1ChainId: NumberLike
contracts?: DeepPartial<OEContractsLike>
bridges?: Partial<CustomBridgesLike>
}) {
this.l1Provider = toProvider(opts.l1Provider)
this.l2Provider = toProvider(opts.l2Provider)
......@@ -57,6 +62,11 @@ export class CrossChainProvider implements ICrossChainProvider {
l2SignerOrProvider: this.l2Provider,
overrides: opts.contracts,
})
this.bridges = getCustomBridges(this.l1ChainId, {
l1SignerOrProvider: this.l1Provider,
l2SignerOrProvider: this.l2Provider,
overrides: opts.bridges,
})
}
public async getMessagesByTransaction(
......@@ -115,6 +125,9 @@ export class CrossChainProvider implements ICrossChainProvider {
sender: parsed.args.sender,
message: parsed.args.message,
messageNonce: parsed.args.messageNonce,
logIndex: log.logIndex,
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
}
})
}
......@@ -132,13 +145,137 @@ export class CrossChainProvider implements ICrossChainProvider {
public async getTokenBridgeMessagesByAddress(
address: AddressLike,
opts?: {
opts: {
direction?: MessageDirection
fromBlock?: BlockTag
toBlock?: BlockTag
} = {}
): Promise<TokenBridgeMessage[]> {
const parseTokenEvent = (
event: Event,
dir: MessageDirection
): TokenBridgeMessage => {
return {
direction: dir,
from: event.args._from,
to: event.args._to,
l1Token: event.args._l1Token || ethers.constants.AddressZero,
l2Token: event.args._l2Token || this.contracts.l2.OVM_ETH.address,
amount: event.args._amount,
data: event.args._data,
logIndex: event.logIndex,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
}
}
// Make sure you provide a direction if you specify a block range. Block ranges don't make
// sense to use on both chains at the same time.
if (opts.fromBlock !== undefined || opts.toBlock !== undefined) {
if (opts.direction === undefined) {
throw new Error('direction must be specified when using a block range')
}
}
// Keep track of all of the messages triggered by the address in question.
// We'll add messages to this list as we find them, based on the direction that the user has
// requested we find messages in. If the user hasn't requested a direction, we find messages in
// both directions.
const messages: TokenBridgeMessage[] = []
// First find all messages in the L1 to L2 direction.
if (
opts.direction === undefined ||
opts.direction === MessageDirection.L1_TO_L2
) {
// Find all ETH deposit events and push them into the messages array.
const ethDepositEvents =
await this.contracts.l1.L1StandardBridge.queryFilter(
this.contracts.l1.L1StandardBridge.filters.ETHDepositInitiated(
address
),
opts.fromBlock,
opts.toBlock
)
for (const event of ethDepositEvents) {
messages.push(parseTokenEvent(event, MessageDirection.L1_TO_L2))
}
// Send an event query for every L1 bridge, this will return an array of arrays.
const erc20DepositEventSets = await Promise.all(
[
this.contracts.l1.L1StandardBridge,
...Object.values(this.bridges.l1),
].map(async (bridge) => {
return bridge.queryFilter(
bridge.filters.ERC20DepositInitiated(undefined, undefined, address),
opts.fromBlock,
opts.toBlock
)
})
)
for (const erc20DepositEvents of erc20DepositEventSets) {
for (const event of erc20DepositEvents) {
messages.push(parseTokenEvent(event, MessageDirection.L1_TO_L2))
}
}
}
// Next find all messages in the L2 to L1 direction.
if (
opts.direction === undefined ||
opts.direction === MessageDirection.L2_TO_L1
) {
// ETH withdrawals and ERC20 withdrawals are the same event on L2.
// Send an event query for every L2 bridge, this will return an array of arrays.
const withdrawalEventSets = await Promise.all(
[
this.contracts.l2.L2StandardBridge,
...Object.values(this.bridges.l2),
].map(async (bridge) => {
return bridge.queryFilter(
bridge.filters.WithdrawalInitiated(undefined, undefined, address),
opts.fromBlock,
opts.toBlock
)
})
)
for (const withdrawalEvents of withdrawalEventSets) {
for (const event of withdrawalEvents) {
messages.push(parseTokenEvent(event, MessageDirection.L2_TO_L1))
}
}
}
return messages
}
public async getDepositsByAddress(
address: AddressLike,
opts: {
fromBlock?: BlockTag
toBlock?: BlockTag
} = {}
): Promise<TokenBridgeMessage[]> {
throw new Error('Not implemented')
return this.getTokenBridgeMessagesByAddress(address, {
...opts,
direction: MessageDirection.L1_TO_L2,
})
}
public async getWithdrawalsByAddress(
address: AddressLike,
opts: {
fromBlock?: BlockTag
toBlock?: BlockTag
} = {}
): Promise<TokenBridgeMessage[]> {
return this.getTokenBridgeMessagesByAddress(address, {
...opts,
direction: MessageDirection.L2_TO_L1,
})
}
public async getMessageStatus(message: MessageLike): Promise<MessageStatus> {
......
......@@ -11,6 +11,7 @@ import {
TokenBridgeMessage,
OEContracts,
MessageReceipt,
CustomBridges,
} from './types'
/**
......@@ -38,6 +39,11 @@ export interface ICrossChainProvider {
*/
contracts: OEContracts
/**
* List of custom bridges for the given network.
*/
bridges: CustomBridges
/**
* Retrieves all cross chain messages sent within a given transaction.
*
......@@ -80,7 +86,7 @@ export interface ICrossChainProvider {
/**
* 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. Returns
* 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.
......@@ -101,6 +107,44 @@ export interface ICrossChainProvider {
}
): 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[]>
/**
* Retrieves the status of a particular message as an enum.
*
......
......@@ -67,6 +67,30 @@ export interface OEContractsLike {
l2: OEL2ContractsLike
}
/**
* Represents list of custom bridges.
*/
export interface CustomBridges {
l1: {
[name: string]: Contract
}
l2: {
[name: string]: Contract
}
}
/**
* Something that looks like the list of custom bridges.
*/
export interface CustomBridgesLike {
l1: {
[K in keyof CustomBridges['l1']]: AddressLike
}
l2: {
[K in keyof CustomBridges['l2']]: AddressLike
}
}
/**
* Enum describing the status of a message.
*/
......@@ -123,11 +147,9 @@ export interface CrossChainMessageRequest {
}
/**
* Describes a message that is sent between L1 and L2. Direction determines where the message was
* sent from and where it's being sent to.
* Core components of a cross chain message.
*/
export interface CrossChainMessage {
direction: MessageDirection
export interface CoreCrossChainMessage {
sender: string
target: string
message: string
......@@ -135,12 +157,15 @@ export interface CrossChainMessage {
}
/**
* Convenience type for when you don't care which direction the message is going in.
* Describes a message that is sent between L1 and L2. Direction determines where the message was
* sent from and where it's being sent to.
*/
export type DirectionlessCrossChainMessage = Omit<
CrossChainMessage,
'direction'
>
export interface CrossChainMessage extends CoreCrossChainMessage {
direction: MessageDirection
logIndex: number
blockNumber: number
transactionHash: string
}
/**
* Describes a token withdrawal or deposit, along with the underlying raw cross chain message
......@@ -153,7 +178,10 @@ export interface TokenBridgeMessage {
l1Token: string
l2Token: string
amount: BigNumber
raw: CrossChainMessage
data: string
logIndex: number
blockNumber: number
transactionHash: string
}
/**
......
......@@ -7,6 +7,8 @@ import {
OEContractsLike,
OEL2ContractsLike,
AddressLike,
CustomBridges,
CustomBridgesLike,
} from '../interfaces'
import { toAddress } from './coercion'
import { DeepPartial } from './type-utils'
......@@ -36,6 +38,42 @@ const NAME_REMAPPING = {
WETH: 'WETH9',
}
/**
* Mapping of L1 chain IDs to the list of custom bridge addresses for each chain.
*/
export const CUSTOM_BRIDGE_ADDRESSES: {
[l1ChainId: number]: CustomBridgesLike
} = {
// TODO: Maybe we can pull these automatically from the token list?
// Alternatively, check against the token list in CI.
1: {
l1: {
SNX: '0xCd9D4988C0AE61887B075bA77f08cbFAd2b65068',
DAI: '0x10E6593CDda8c58a1d0f14C5164B376352a55f2F',
BitBTC: '0xaBA2c5F108F7E820C049D5Af70B16ac266c8f128',
},
l2: {
SNX: '0x3f87Ff1de58128eF8FCb4c807eFD776E1aC72E51',
DAI: '0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65',
BitBTC: '0x158F513096923fF2d3aab2BcF4478536de6725e2',
},
},
42: {
l1: {
SNX: '0xD134Db47DDF5A6feB245452af17cCAf92ee53D3c',
DAI: '0xb415e822C4983ecD6B1c1596e8a5f976cf6CD9e3',
BitBTC: '0x0b651A42F32069d62d5ECf4f2a7e5Bd3E9438746',
USX: '0x40E862341b2416345F02c41Ac70df08525150dC7',
},
l2: {
SNX: '0x5C3f51CEd0C2F6157e2be67c029264D6C44bfe42',
DAI: '0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65',
BitBTC: '0x0CFb46528a7002a7D8877a5F7a69b9AaF1A9058e',
USX: '0xB4d37826b14Cd3CB7257A2A5094507d701fe715f',
},
},
}
/**
* Mapping of L1 chain IDs to the appropriate contract addresses for the OE deployments to the
* given network. Simplifies the process of getting the correct contract addresses for a given
......@@ -190,3 +228,59 @@ export const getAllOEContracts = (
l2: l2Contracts,
}
}
/**
* Gets a series of custom bridges for the given L1 chain ID.
*
* @param l1ChainId L1 chain ID for the L1 network where the custom bridges are deployed.
* @param opts Additional options for connecting to the custom bridges.
* @param opts.l1SignerOrProvider Signer or provider to connect to the L1 contracts.
* @param opts.l2SignerOrProvider Signer or provider to connect to the L2 contracts.
* @param opts.overrides Custom contract address overrides for L1 or L2 contracts.
* @returns An object containing ethers.Contract objects connected to the appropriate addresses on
* both L1 and L2.
*/
export const getCustomBridges = (
l1ChainId: number,
opts: {
l1SignerOrProvider?: ethers.Signer | ethers.providers.Provider
l2SignerOrProvider?: ethers.Signer | ethers.providers.Provider
overrides?: Partial<CustomBridgesLike>
} = {}
): CustomBridges => {
const addresses = CUSTOM_BRIDGE_ADDRESSES[l1ChainId] || {
l1: {},
l2: {},
}
for (const [contractName, contractAddress] of Object.entries(
opts.overrides?.l1 || {}
)) {
addresses.l1[contractName] = contractAddress
}
for (const [contractName, contractAddress] of Object.entries(
opts.overrides?.l2 || {}
)) {
addresses.l2[contractName] = contractAddress
}
const bridges = {
l1: {},
l2: {},
}
for (const [contractName, contractAddress] of Object.entries(addresses.l1)) {
bridges.l1[contractName] = new Contract(
toAddress(contractAddress),
getContractInterface('IL1ERC20Bridge'),
opts.l1SignerOrProvider
)
}
for (const [contractName, contractAddress] of Object.entries(addresses.l2)) {
bridges.l2[contractName] = new Contract(
toAddress(contractAddress),
getContractInterface('IL2ERC20Bridge'),
opts.l2SignerOrProvider
)
}
return bridges
}
import { getContractInterface } from '@eth-optimism/contracts'
import { ethers } from 'ethers'
import { DirectionlessCrossChainMessage } from '../interfaces'
import { CoreCrossChainMessage } from '../interfaces'
/**
* Returns the canonical encoding of a cross chain message. This encoding is used in various
......@@ -10,7 +10,7 @@ import { DirectionlessCrossChainMessage } from '../interfaces'
* @returns Canonical encoding of the message.
*/
export const encodeCrossChainMessage = (
message: DirectionlessCrossChainMessage
message: CoreCrossChainMessage
): string => {
return getContractInterface('L2CrossDomainMessenger').encodeFunctionData(
'relayMessage',
......@@ -27,7 +27,7 @@ export const encodeCrossChainMessage = (
* @returns Canonical hash of the message.
*/
export const hashCrossChainMessage = (
message: DirectionlessCrossChainMessage
message: CoreCrossChainMessage
): string => {
return ethers.utils.solidityKeccak256(
['bytes'],
......
pragma solidity ^0.8.9;
import { MockMessenger } from "./MockMessenger.sol";
contract MockBridge {
event ERC20DepositInitiated(
address indexed _l1Token,
address indexed _l2Token,
address indexed _from,
address _to,
uint256 _amount,
bytes _data
);
event ERC20WithdrawalFinalized(
address indexed _l1Token,
address indexed _l2Token,
address indexed _from,
address _to,
uint256 _amount,
bytes _data
);
event WithdrawalInitiated(
address indexed _l1Token,
address indexed _l2Token,
address indexed _from,
address _to,
uint256 _amount,
bytes _data
);
event DepositFinalized(
address indexed _l1Token,
address indexed _l2Token,
address indexed _from,
address _to,
uint256 _amount,
bytes _data
);
event DepositFailed(
address indexed _l1Token,
address indexed _l2Token,
address indexed _from,
address _to,
uint256 _amount,
bytes _data
);
struct TokenEventStruct {
address l1Token;
address l2Token;
address from;
address to;
uint256 amount;
bytes data;
}
MockMessenger public messenger;
constructor(MockMessenger _messenger) {
messenger = _messenger;
}
function emitERC20DepositInitiated(
TokenEventStruct memory _params
) public {
emit ERC20DepositInitiated(_params.l1Token, _params.l2Token, _params.from, _params.to, _params.amount, _params.data);
messenger.triggerSentMessageEvent(
MockMessenger.SentMessageEventParams(
address(0),
address(0),
hex"1234",
1234,
12345678
)
);
}
function emitERC20WithdrawalFinalized(
TokenEventStruct memory _params
) public {
emit ERC20WithdrawalFinalized(_params.l1Token, _params.l2Token, _params.from, _params.to, _params.amount, _params.data);
}
function emitWithdrawalInitiated(
TokenEventStruct memory _params
) public {
emit WithdrawalInitiated(_params.l1Token, _params.l2Token, _params.from, _params.to, _params.amount, _params.data);
messenger.triggerSentMessageEvent(
MockMessenger.SentMessageEventParams(
address(0),
address(0),
hex"1234",
1234,
12345678
)
);
}
function emitDepositFinalized(
TokenEventStruct memory _params
) public {
emit DepositFinalized(_params.l1Token, _params.l2Token, _params.from, _params.to, _params.amount, _params.data);
}
function emitDepositFailed(
TokenEventStruct memory _params
) public {
emit DepositFailed(_params.l1Token, _params.l2Token, _params.from, _params.to, _params.amount, _params.data);
}
}
......@@ -28,17 +28,23 @@ contract MockMessenger is ICrossDomainMessenger {
return;
}
function triggerSentMessageEvent(
SentMessageEventParams memory _params
) public {
emit SentMessage(
_params.target,
_params.sender,
_params.message,
_params.messageNonce,
_params.gasLimit
);
}
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
);
triggerSentMessageEvent(_params[i]);
}
}
......
......@@ -3,8 +3,7 @@ import { Contract, Signer } from 'ethers'
import { ethers } from 'hardhat'
import { getContractFactory } from '@eth-optimism/contracts'
import {
CrossChainMessage,
MessageDirection,
CoreCrossChainMessage,
encodeCrossChainMessage,
hashCrossChainMessage,
} from '../../src'
......@@ -25,8 +24,7 @@ describe('message encoding utils', () => {
})
it('should properly encode a message', async () => {
const message: CrossChainMessage = {
direction: MessageDirection.L1_TO_L2,
const message: CoreCrossChainMessage = {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '1234'.repeat(32),
......@@ -53,8 +51,7 @@ describe('message encoding utils', () => {
})
it('should properly hash a message', async () => {
const message: CrossChainMessage = {
direction: MessageDirection.L1_TO_L2,
const message: CoreCrossChainMessage = {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '1234'.repeat(32),
......
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