Commit 0bc53656 authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Merge branch 'develop' into feat/bring-back-unsupported-rpcs

parents 81d90563 40ffd24d
---
'@eth-optimism/integration-tests': patch
---
Increase withdrawal test timeout
---
'@eth-optimism/data-transport-layer': patch
---
Updates DTL to correctly parse L1 to L2 tx timestamps after the first bss hardfork
---
'@eth-optimism/integration-tests': patch
---
Add an integration test showing the infeasability of withdrawing a fake token in exchange for a legitimate token.
---
'@eth-optimism/integration-tests': patch
---
Updates integration tests to include a test for syncing a Verifier from L1
---
'@eth-optimism/l2geth': patch
---
Fixes incorrect timestamp handling for L1 syncing verifiers
---
'@eth-optimism/batch-submitter': patch
---
Updates batch submitter to also include separate timestamps for deposit transactions"
---
'@eth-optimism/integration-tests': patch
---
Add verifier integration tests
.github
node_modules
.env
**/.env
test
**/*_test.go
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract FakeL2StandardERC20 {
address public immutable l1Token;
constructor(address _l1Token) {
l1Token = _l1Token;
}
// Burn will be called by the L2 Bridge to burn the tokens we are bridging to L1
function burn(address, uint256) external {}
}
......@@ -127,4 +127,58 @@ describe('Bridged tokens', () => {
)
}
)
// This test demonstrates that an apparent withdrawal bug is in fact non-existent.
// Specifically, the L2 bridge does not check that the L2 token being burned corresponds
// with the L1 token which is specified for the withdrawal.
withdrawalTest(
'should not allow an arbitrary L2 token to be withdrawn in exchange for a legitimate L1 token',
async () => {
before(async () => {
// First deposit some of the L1 token to L2, so that there is something which could be stolen.
const depositTx = await env.l1Bridge
.connect(env.l1Wallet)
.depositERC20(
L1__ERC20.address,
L2__ERC20.address,
1000,
2000000,
'0x'
)
await env.waitForXDomainTransaction(depositTx, Direction.L1ToL2)
expect(await L2__ERC20.balanceOf(env.l2Wallet.address)).to.deep.equal(
BigNumber.from(1000)
)
})
// Deploy a Fake L2 token, which:
// - returns the address of a legitimate L1 token from its l1Token() getter.
// - allows the L2 bridge to call its burn() function.
const fakeToken = await (
await ethers.getContractFactory('FakeL2StandardERC20', env.l2Wallet)
).deploy(L1__ERC20.address)
await fakeToken.deployed()
const balBefore = await L1__ERC20.balanceOf(otherWalletL1.address)
// Withdraw some of the Fake L2 token, hoping to receive the same amount of the legitimate
// token on L1.
const withdrawalTx = await env.l2Bridge
.connect(otherWalletL2)
.withdrawTo(
fakeToken.address,
otherWalletL1.address,
500,
1_000_000,
'0x'
)
await env.relayXDomainMessages(withdrawalTx)
await env.waitForXDomainTransaction(withdrawalTx, Direction.L2ToL1)
// Ensure that the L1 recipient address has not received any additional L1 token balance.
expect(await L1__ERC20.balanceOf(otherWalletL1.address)).to.deep.equal(
balBefore
)
}
)
})
......@@ -11,6 +11,7 @@ import {
l1Provider,
l2Provider,
replicaProvider,
verifierProvider,
l1Wallet,
l2Wallet,
gasPriceOracleWallet,
......@@ -57,6 +58,7 @@ export class OptimismEnv {
l1Provider: providers.JsonRpcProvider
l2Provider: providers.JsonRpcProvider
replicaProvider: providers.JsonRpcProvider
verifierProvider: providers.JsonRpcProvider
constructor(args: any) {
this.addressManager = args.addressManager
......@@ -74,6 +76,7 @@ export class OptimismEnv {
this.l1Provider = args.l1Provider
this.l2Provider = args.l2Provider
this.replicaProvider = args.replicaProvider
this.verifierProvider = args.verifierProvider
this.ctc = args.ctc
this.scc = args.scc
}
......@@ -140,6 +143,7 @@ export class OptimismEnv {
l2Wallet,
l1Provider,
l2Provider,
verifierProvider,
replicaProvider,
})
}
......
......@@ -62,6 +62,8 @@ const procEnv = cleanEnv(process.env, {
REPLICA_URL: str({ default: 'http://localhost:8549' }),
REPLICA_POLLING_INTERVAL: num({ default: 10 }),
VERIFIER_URL: str({ default: 'http://localhost:8547' }),
PRIVATE_KEY: str({
default:
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
......@@ -96,6 +98,9 @@ const procEnv = cleanEnv(process.env, {
RUN_NIGHTLY_TESTS: bool({
default: false,
}),
RUN_VERIFIER_TESTS: bool({
default: true,
}),
MOCHA_TIMEOUT: num({
default: 120_000,
......@@ -121,6 +126,11 @@ export const replicaProvider = injectL2Context(
)
replicaProvider.pollingInterval = procEnv.REPLICA_POLLING_INTERVAL
export const verifierProvider = injectL2Context(
new providers.JsonRpcProvider(procEnv.VERIFIER_URL)
)
verifierProvider.pollingInterval = procEnv.L2_POLLING_INTERVAL
// The sequencer private key which is funded on L1
export const l1Wallet = new Wallet(procEnv.PRIVATE_KEY, l1Provider)
......@@ -210,7 +220,7 @@ export const conditionalTest = (
}
await fn()
}).timeout(timeout || envConfig.MOCHA_TIMEOUT)
}).timeout(timeout || envConfig.MOCHA_TIMEOUT * 2)
}
export const withdrawalTest = (name, fn, timeout?: number) =>
......
import { TransactionReceipt } from '@ethersproject/abstract-provider'
import { expect } from './shared/setup'
import { OptimismEnv } from './shared/env'
import {
defaultTransactionFactory,
gasPriceForL2,
sleep,
envConfig,
} from './shared/utils'
describe('Verifier Tests', () => {
let env: OptimismEnv
before(async function () {
if (!envConfig.RUN_VERIFIER_TESTS) {
this.skip()
return
}
env = await OptimismEnv.new()
})
describe('Matching blocks', () => {
it('should sync a transaction', async () => {
const tx = defaultTransactionFactory()
tx.gasPrice = await gasPriceForL2()
const result = await env.l2Wallet.sendTransaction(tx)
let receipt: TransactionReceipt
while (!receipt) {
receipt = await env.verifierProvider.getTransactionReceipt(result.hash)
await sleep(200)
}
const sequencerBlock = (await env.l2Provider.getBlock(
result.blockNumber
)) as any
const verifierBlock = (await env.verifierProvider.getBlock(
result.blockNumber
)) as any
expect(sequencerBlock.stateRoot).to.deep.eq(verifierBlock.stateRoot)
expect(sequencerBlock.hash).to.deep.eq(verifierBlock.hash)
})
it('sync an unprotected tx (eip155)', async () => {
const tx = {
...defaultTransactionFactory(),
nonce: await env.l2Wallet.getTransactionCount(),
gasPrice: await gasPriceForL2(),
chainId: null, // Disables EIP155 transaction signing.
}
const signed = await env.l2Wallet.signTransaction(tx)
const result = await env.l2Provider.sendTransaction(signed)
let receipt: TransactionReceipt
while (!receipt) {
receipt = await env.verifierProvider.getTransactionReceipt(result.hash)
await sleep(200)
}
const sequencerBlock = (await env.l2Provider.getBlock(
result.blockNumber
)) as any
const verifierBlock = (await env.verifierProvider.getBlock(
result.blockNumber
)) as any
expect(sequencerBlock.stateRoot).to.deep.eq(verifierBlock.stateRoot)
expect(sequencerBlock.hash).to.deep.eq(verifierBlock.hash)
})
it('should forward tx to sequencer', async () => {
const tx = {
...defaultTransactionFactory(),
nonce: await env.l2Wallet.getTransactionCount(),
gasPrice: await gasPriceForL2(),
}
const signed = await env.l2Wallet.signTransaction(tx)
const result = await env.verifierProvider.sendTransaction(signed)
let receipt: TransactionReceipt
while (!receipt) {
receipt = await env.verifierProvider.getTransactionReceipt(result.hash)
await sleep(200)
}
const sequencerBlock = (await env.l2Provider.getBlock(
result.blockNumber
)) as any
const verifierBlock = (await env.verifierProvider.getBlock(
result.blockNumber
)) as any
expect(sequencerBlock.stateRoot).to.deep.eq(verifierBlock.stateRoot)
expect(sequencerBlock.hash).to.deep.eq(verifierBlock.hash)
})
})
})
......@@ -931,18 +931,7 @@ func (w *worker) commitNewTx(tx *types.Transaction) error {
// Preserve liveliness as best as possible. Must panic on L1 to L2
// transactions as the timestamp cannot be malleated
if parent.Time() > tx.L1Timestamp() {
log.Error("Monotonicity violation", "index", num)
if tx.QueueOrigin() == types.QueueOriginSequencer {
tx.SetL1Timestamp(parent.Time())
prev := parent.Transactions()
if len(prev) == 1 {
tx.SetL1BlockNumber(prev[0].L1BlockNumber().Uint64())
} else {
log.Error("Cannot recover L1 Blocknumber")
}
} else {
log.Error("Cannot recover from monotonicity violation")
}
log.Error("Monotonicity violation", "index", num, "parent", parent.Time(), "tx", tx.L1Timestamp())
}
// Fill in the index field in the tx meta if it is `nil`.
......
......@@ -825,13 +825,14 @@ func (s *SyncService) applyTransactionToTip(tx *types.Transaction) error {
if now.Sub(current) > s.timestampRefreshThreshold {
current = now
}
log.Info("Updating latest timestamp", "timestamp", current, "unix", current.Unix())
tx.SetL1Timestamp(uint64(current.Unix()))
} else if tx.L1Timestamp() == 0 && s.verifier {
// This should never happen
log.Error("No tx timestamp found when running as verifier", "hash", tx.Hash().Hex())
} else if tx.L1Timestamp() < s.GetLatestL1Timestamp() {
} else if tx.L1Timestamp() < ts {
// This should never happen, but sometimes does
log.Error("Timestamp monotonicity violation", "hash", tx.Hash().Hex())
log.Error("Timestamp monotonicity violation", "hash", tx.Hash().Hex(), "latest", ts, "tx", tx.L1Timestamp())
}
l1BlockNumber := tx.L1BlockNumber()
......
......@@ -136,8 +136,9 @@ services:
- l1_chain
- deployer
- dtl
- l2geth
deploy:
replicas: 0
replicas: 1
build:
context: ..
dockerfile: ./ops/docker/Dockerfile.geth
......@@ -146,6 +147,7 @@ services:
- ./envs/geth.env
environment:
ETH1_HTTP: http://l1_chain:8545
SEQUENCER_CLIENT_HTTP: http://l2geth:8545
ROLLUP_STATE_DUMP_PATH: http://deployer:8081/state-dump.latest.json
ROLLUP_CLIENT_HTTP: http://dtl:7878
ROLLUP_BACKEND: 'l1'
......
FROM golang:1.17.6-alpine3.15 as builder
FROM golang:1.17.3-alpine3.13 as builder
RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash
......@@ -9,7 +9,7 @@ RUN go mod graph | grep -v l2geth | awk '{if ($1 !~ "@") print $2}' | xargs -n 1
COPY ./go/batch-submitter/ ./
RUN make
FROM alpine:3.15
FROM alpine:3.13
RUN apk add --no-cache ca-certificates jq curl
COPY --from=builder /go/batch-submitter/batch-submitter /usr/local/bin/
......
......@@ -9,6 +9,7 @@ DATA_TRANSPORT_LAYER__LOGS_PER_POLLING_INTERVAL=2000
DATA_TRANSPORT_LAYER__DANGEROUSLY_CATCH_ALL_ERRORS=true
DATA_TRANSPORT_LAYER__SERVER_HOSTNAME=0.0.0.0
DATA_TRANSPORT_LAYER__L1_START_HEIGHT=1
DATA_TRANSPORT_LAYER__BSS_HARDFORK_1_INDEX=0
DATA_TRANSPORT_LAYER__ADDRESS_MANAGER=
DATA_TRANSPORT_LAYER__L1_RPC_ENDPOINT=
......
......@@ -31,11 +31,17 @@ interface Indexed {
index: number
}
interface ExtraTransportDBOptions {
bssHardfork1Index?: number
}
export class TransportDB {
public db: SimpleDB
public opts: ExtraTransportDBOptions
constructor(leveldb: LevelUp) {
constructor(leveldb: LevelUp, opts?: ExtraTransportDBOptions) {
this.db = new SimpleDB(leveldb)
this.opts = opts || {}
}
public async putEnqueueEntries(entries: EnqueueEntry[]): Promise<void> {
......@@ -254,26 +260,7 @@ export class TransportDB {
return null
}
if (transaction.queueOrigin === 'l1') {
const enqueue = await this.getEnqueueByIndex(transaction.queueIndex)
if (enqueue === null) {
return null
}
return {
...transaction,
...{
blockNumber: enqueue.blockNumber,
timestamp: enqueue.timestamp,
gasLimit: enqueue.gasLimit,
target: enqueue.target,
origin: enqueue.origin,
data: enqueue.data,
},
}
} else {
return transaction
}
return this._makeFullTransaction(transaction)
}
public async getLatestFullTransaction(): Promise<TransactionEntry> {
......@@ -293,31 +280,46 @@ export class TransportDB {
const fullTransactions = []
for (const transaction of transactions) {
if (transaction.queueOrigin === 'l1') {
const enqueue = await this.getEnqueueByIndex(transaction.queueIndex)
if (enqueue === null) {
return null
}
fullTransactions.push({
...transaction,
...{
blockNumber: enqueue.blockNumber,
timestamp: enqueue.timestamp,
gasLimit: enqueue.gasLimit,
target: enqueue.target,
origin: enqueue.origin,
data: enqueue.data,
},
})
} else {
fullTransactions.push(transaction)
}
fullTransactions.push(await this._makeFullTransaction(transaction))
}
return fullTransactions
}
private async _makeFullTransaction(
transaction: TransactionEntry
): Promise<TransactionEntry> {
// We only need to do extra work for L1 to L2 transactions.
if (transaction.queueOrigin !== 'l1') {
return transaction
}
const enqueue = await this.getEnqueueByIndex(transaction.queueIndex)
if (enqueue === null) {
return null
}
let timestamp = enqueue.timestamp
if (
typeof this.opts.bssHardfork1Index === 'number' &&
transaction.index >= this.opts.bssHardfork1Index
) {
timestamp = transaction.timestamp
}
return {
...transaction,
...{
blockNumber: enqueue.blockNumber,
timestamp,
gasLimit: enqueue.gasLimit,
target: enqueue.target,
origin: enqueue.origin,
data: enqueue.data,
},
}
}
private async _getLatestEntryIndex(key: string): Promise<number> {
return this.db.get<number>(`${key}:latest`, 0) || 0
}
......
......@@ -143,7 +143,7 @@ export const handleEventsSequencerBatchAppended: EventHandlerSet<
.toNumber(),
batchIndex: extraData.batchIndex.toNumber(),
blockNumber: BigNumber.from(0).toNumber(),
timestamp: BigNumber.from(0).toNumber(),
timestamp: context.timestamp,
gasLimit: BigNumber.from(0).toString(),
target: constants.AddressZero,
origin: constants.AddressZero,
......
......@@ -104,7 +104,9 @@ export class L1IngestionService extends BaseService<L1IngestionServiceOptions> {
} = {} as any
protected async _init(): Promise<void> {
this.state.db = new TransportDB(this.options.db)
this.state.db = new TransportDB(this.options.db, {
bssHardfork1Index: this.options.bssHardfork1Index,
})
this.l1IngestionMetrics = registerMetrics(this.metrics)
......
......@@ -84,7 +84,9 @@ export class L2IngestionService extends BaseService<L2IngestionServiceOptions> {
this.l2IngestionMetrics = registerMetrics(this.metrics)
this.state.db = new TransportDB(this.options.db)
this.state.db = new TransportDB(this.options.db, {
bssHardfork1Index: this.options.bssHardfork1Index,
})
this.state.l2RpcProvider =
typeof this.options.l2RpcProvider === 'string'
......
......@@ -36,6 +36,7 @@ export interface L1DataTransportServiceOptions {
defaultBackend: string
l1GasPriceBackend: string
l1StartHeight?: number
bssHardfork1Index?: number
}
const optionSettings = {
......
......@@ -51,6 +51,7 @@ type ethNetwork = 'mainnet' | 'kovan' | 'goerli'
useSentry: config.bool('use-sentry', false),
sentryDsn: config.str('sentry-dsn'),
sentryTraceRate: config.ufloat('sentry-trace-rate', 0.05),
bssHardfork1Index: config.uint('bss-hardfork-1-index', null),
})
const stop = async (signal) => {
......
......@@ -87,7 +87,10 @@ export class L1TransportServer extends BaseService<L1TransportServerOptions> {
await this.options.db.open()
}
this.state.db = new TransportDB(this.options.db)
this.state.db = new TransportDB(this.options.db, {
bssHardfork1Index: this.options.bssHardfork1Index,
})
this.state.l1RpcProvider =
typeof this.options.l1RpcProvider === 'string'
? new JsonRpcProvider(this.options.l1RpcProvider)
......
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Contract } from 'ethers'
import { hexStringEquals } from '@eth-optimism/core-utils'
import { AddressLike } from '../interfaces'
import { toAddress } from '../utils'
import { StandardBridgeAdapter } from './standard-bridge'
/**
* Bridge adapter for DAI.
*/
export class DAIBridgeAdapter extends StandardBridgeAdapter {
public async supportsTokenPair(
l1Token: AddressLike,
l2Token: AddressLike
): Promise<boolean> {
// Just need access to this ABI for this one function.
const l1Bridge = new Contract(
this.l1Bridge.address,
[
{
inputs: [],
name: 'l1Token',
outputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'l2Token',
outputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
stateMutability: 'view',
type: 'function',
},
],
this.provider.l1Provider
)
const allowedL1Token = await l1Bridge.l1Token()
if (!hexStringEquals(allowedL1Token, toAddress(l1Token))) {
return false
}
const allowedL2Token = await l1Bridge.l2Token()
if (!hexStringEquals(allowedL2Token, toAddress(l2Token))) {
return false
}
return true
}
}
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ethers, Overrides } from 'ethers'
import { TransactionRequest, BlockTag } from '@ethersproject/abstract-provider'
import { predeploys } from '@eth-optimism/contracts'
import { hexStringEquals } from '@eth-optimism/core-utils'
import {
NumberLike,
AddressLike,
TokenBridgeMessage,
MessageDirection,
} from '../interfaces'
import { toAddress, omit } from '../utils'
import { StandardBridgeAdapter } from './standard-bridge'
/**
* Bridge adapter for the ETH bridge.
*/
export class ETHBridgeAdapter extends StandardBridgeAdapter {
public async getDepositsByAddress(
address: AddressLike,
opts?: {
fromBlock?: BlockTag
toBlock?: BlockTag
}
): Promise<TokenBridgeMessage[]> {
const events = await this.l1Bridge.queryFilter(
this.l1Bridge.filters.ETHDepositInitiated(address),
opts?.fromBlock,
opts?.toBlock
)
return events.map((event) => {
return {
direction: MessageDirection.L1_TO_L2,
from: event.args._from,
to: event.args._to,
l1Token: ethers.constants.AddressZero,
l2Token: predeploys.OVM_ETH,
amount: event.args._amount,
data: event.args._data,
logIndex: event.logIndex,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
}
})
}
public async getWithdrawalsByAddress(
address: AddressLike,
opts?: {
fromBlock?: BlockTag
toBlock?: BlockTag
}
): Promise<TokenBridgeMessage[]> {
const events = await this.l2Bridge.queryFilter(
this.l2Bridge.filters.WithdrawalInitiated(undefined, undefined, address),
opts?.fromBlock,
opts?.toBlock
)
return events
.filter((event) => {
// Only find ETH withdrawals.
return (
hexStringEquals(event.args._l1Token, ethers.constants.AddressZero) &&
hexStringEquals(event.args._l2Token, predeploys.OVM_ETH)
)
})
.map((event) => {
return {
direction: MessageDirection.L2_TO_L1,
from: event.args._from,
to: event.args._to,
l1Token: event.args._l1Token,
l2Token: event.args._l2Token,
amount: event.args._amount,
data: event.args._data,
logIndex: event.logIndex,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
}
})
}
public async supportsTokenPair(
l1Token: AddressLike,
l2Token: AddressLike
): Promise<boolean> {
// Only support ETH deposits and withdrawals.
return (
hexStringEquals(toAddress(l1Token), ethers.constants.AddressZero) &&
hexStringEquals(toAddress(l2Token), predeploys.OVM_ETH)
)
}
populateTransaction = {
deposit: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionRequest> => {
if (!(await this.supportsTokenPair(l1Token, l2Token))) {
throw new Error(`token pair not supported by bridge`)
}
return this.l1Bridge.populateTransaction.depositETH(
opts?.l2GasLimit || 200_000, // Default to 200k gas limit.
'0x', // No data.
{
...omit(opts?.overrides || {}, 'value'),
value: amount,
}
)
},
withdraw: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
if (!(await this.supportsTokenPair(l1Token, l2Token))) {
throw new Error(`token pair not supported by bridge`)
}
return this.l2Bridge.populateTransaction.withdraw(
toAddress(l2Token),
amount,
0, // L1 gas not required.
'0x', // No data.
opts?.overrides || {}
)
},
}
}
export * from './standard-bridge'
export * from './eth-bridge'
export * from './dai-bridge'
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ethers, Contract, Overrides, Signer, BigNumber } from 'ethers'
import {
TransactionRequest,
TransactionResponse,
BlockTag,
} from '@ethersproject/abstract-provider'
import { getContractInterface, predeploys } from '@eth-optimism/contracts'
import { hexStringEquals } from '@eth-optimism/core-utils'
import {
IBridgeAdapter,
ICrossChainProvider,
NumberLike,
AddressLike,
TokenBridgeMessage,
MessageDirection,
} from '../interfaces'
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 l1Bridge: Contract
public l2Bridge: Contract
/**
* 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.l1Bridge L1 bridge contract.
* @param opts.l2Bridge L2 bridge contract.
*/
constructor(opts: {
provider: ICrossChainProvider
l1Bridge: AddressLike
l2Bridge: AddressLike
}) {
this.provider = opts.provider
this.l1Bridge = new Contract(
toAddress(opts.l1Bridge),
getContractInterface('L1StandardBridge'),
this.provider.l1Provider
)
this.l2Bridge = new Contract(
toAddress(opts.l2Bridge),
getContractInterface('IL2ERC20Bridge'),
this.provider.l2Provider
)
}
public async getTokenBridgeMessagesByAddress(
address: AddressLike,
opts?: {
direction?: MessageDirection
}
): Promise<TokenBridgeMessage[]> {
let messages: TokenBridgeMessage[] = []
if (
opts?.direction === undefined ||
opts?.direction === MessageDirection.L1_TO_L2
) {
messages = messages.concat(await this.getDepositsByAddress(address))
}
if (
opts?.direction === undefined ||
opts?.direction === MessageDirection.L2_TO_L1
) {
messages = messages.concat(await this.getWithdrawalsByAddress(address))
}
return messages
}
public async getDepositsByAddress(
address: AddressLike,
opts?: {
fromBlock?: BlockTag
toBlock?: BlockTag
}
): Promise<TokenBridgeMessage[]> {
const events = await this.l1Bridge.queryFilter(
this.l1Bridge.filters.ERC20DepositInitiated(
undefined,
undefined,
address
),
opts?.fromBlock,
opts?.toBlock
)
return events
.filter((event) => {
// Specifically filter out ETH. ETH deposits and withdrawals are handled by the ETH bridge
// adapter. Bridges that are not the ETH bridge should not be able to handle or even
// present ETH deposits or withdrawals.
return (
!hexStringEquals(event.args._l1Token, ethers.constants.AddressZero) &&
!hexStringEquals(event.args._l2Token, predeploys.OVM_ETH)
)
})
.map((event) => {
return {
direction: MessageDirection.L1_TO_L2,
from: event.args._from,
to: event.args._to,
l1Token: event.args._l1Token,
l2Token: event.args._l2Token,
amount: event.args._amount,
data: event.args._data,
logIndex: event.logIndex,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
}
})
}
public async getWithdrawalsByAddress(
address: AddressLike,
opts?: {
fromBlock?: BlockTag
toBlock?: BlockTag
}
): Promise<TokenBridgeMessage[]> {
const events = await this.l2Bridge.queryFilter(
this.l2Bridge.filters.WithdrawalInitiated(undefined, undefined, address),
opts?.fromBlock,
opts?.toBlock
)
return events
.filter((event) => {
// Specifically filter out ETH. ETH deposits and withdrawals are handled by the ETH bridge
// adapter. Bridges that are not the ETH bridge should not be able to handle or even
// present ETH deposits or withdrawals.
return (
!hexStringEquals(event.args._l1Token, ethers.constants.AddressZero) &&
!hexStringEquals(event.args._l2Token, predeploys.OVM_ETH)
)
})
.map((event) => {
return {
direction: MessageDirection.L2_TO_L1,
from: event.args._from,
to: event.args._to,
l1Token: event.args._l1Token,
l2Token: event.args._l2Token,
amount: event.args._amount,
data: event.args._data,
logIndex: event.logIndex,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
}
})
}
public async supportsTokenPair(
l1Token: AddressLike,
l2Token: AddressLike
): Promise<boolean> {
try {
const contract = new Contract(
toAddress(l2Token),
getContractInterface('L2StandardERC20'),
this.provider.l2Provider
)
// Don't support ETH deposits or withdrawals via this bridge.
if (
hexStringEquals(toAddress(l1Token), ethers.constants.AddressZero) ||
hexStringEquals(toAddress(l2Token), predeploys.OVM_ETH)
) {
return false
}
// Make sure the L1 token matches.
const remoteL1Token = await contract.l1Token()
if (!hexStringEquals(remoteL1Token, toAddress(l1Token))) {
return false
}
// Make sure the L2 bridge matches.
const remoteL2Bridge = await contract.l2Bridge()
if (!hexStringEquals(remoteL2Bridge, this.l2Bridge.address)) {
return false
}
return true
} catch (err) {
// If the L2 token is not an L2StandardERC20, it may throw an error. If there's a call
// exception then we assume that the token is not supported. Other errors are thrown.
if (err.message.toString().includes('CALL_EXCEPTION')) {
return false
} else {
throw err
}
}
}
public async deposit(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
signer: Signer,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> {
return signer.sendTransaction(
await this.populateTransaction.deposit(l1Token, l2Token, amount, opts)
)
}
public async withdraw(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
signer: Signer,
opts?: {
overrides?: Overrides
}
): Promise<TransactionResponse> {
return signer.sendTransaction(
await this.populateTransaction.withdraw(l1Token, l2Token, amount, opts)
)
}
populateTransaction = {
deposit: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionRequest> => {
if (!(await this.supportsTokenPair(l1Token, l2Token))) {
throw new Error(`token pair not supported by bridge`)
}
return this.l1Bridge.depositERC20(
toAddress(l1Token),
toAddress(l2Token),
amount,
opts?.l2GasLimit || 200_000, // Default to 200k gas limit.
'0x', // No data.
opts?.overrides || {}
)
},
withdraw: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
if (!(await this.supportsTokenPair(l1Token, l2Token))) {
throw new Error(`token pair not supported by bridge`)
}
return this.l2Bridge.populateTransaction.withdraw(
toAddress(l2Token),
amount,
0, // L1 gas not required.
'0x', // No data.
opts?.overrides || {}
)
},
}
estimateGas = {
deposit: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l1Provider.estimateGas(
await this.populateTransaction.deposit(l1Token, l2Token, amount, opts)
)
},
withdraw: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l2Provider.estimateGas(
await this.populateTransaction.withdraw(l1Token, l2Token, amount, opts)
)
},
}
}
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Overrides, Signer, BigNumber } from 'ethers'
import { ethers, Overrides, Signer, BigNumber } from 'ethers'
import {
TransactionRequest,
TransactionResponse,
......@@ -10,11 +10,12 @@ import {
CrossChainMessageRequest,
ICrossChainMessenger,
ICrossChainProvider,
IBridgeAdapter,
MessageLike,
NumberLike,
AddressLike,
MessageDirection,
} from './interfaces'
import { omit } from './utils'
export class CrossChainMessenger implements ICrossChainMessenger {
provider: ICrossChainProvider
......@@ -102,6 +103,43 @@ export class CrossChainMessenger implements ICrossChainMessenger {
)
}
public async depositERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> {
return this.l1Signer.sendTransaction(
await this.populateTransaction.depositERC20(
l1Token,
l2Token,
amount,
opts
)
)
}
public async withdrawERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionResponse> {
return this.l2Signer.sendTransaction(
await this.populateTransaction.withdrawERC20(
l1Token,
l2Token,
amount,
opts
)
)
}
populateTransaction = {
sendMessage: async (
message: CrossChainMessageRequest,
......@@ -118,7 +156,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
message.message,
opts?.l2GasLimit ||
(await this.provider.estimateL2MessageGasLimit(message)),
omit(opts?.overrides || {}, 'l2GasLimit')
opts?.overrides || {}
)
} else {
return this.provider.contracts.l2.L2CrossDomainMessenger.connect(
......@@ -127,7 +165,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
message.target,
message.message,
0, // Gas limit goes unused when sending from L2 to L1
omit(opts?.overrides || {}, 'l2GasLimit')
opts?.overrides || {}
)
}
},
......@@ -173,13 +211,11 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
return this.provider.contracts.l1.L1StandardBridge.populateTransaction.depositETH(
opts?.l2GasLimit || 200000, // 200k gas is fine as a default
'0x', // No data
{
...omit(opts?.overrides || {}, 'l2GasLimit', 'value'),
value: amount,
}
return this.provider.bridges.ETH.populateTransaction.deposit(
ethers.constants.AddressZero,
predeploys.OVM_ETH,
amount,
opts
)
},
......@@ -189,14 +225,38 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
return this.provider.contracts.l2.L2StandardBridge.populateTransaction.withdraw(
return this.provider.bridges.ETH.populateTransaction.withdraw(
ethers.constants.AddressZero,
predeploys.OVM_ETH,
amount,
0, // No need to supply gas here
'0x', // No data,
opts?.overrides || {}
opts
)
},
depositERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionRequest> => {
const bridge = await this.provider.getBridgeForTokenPair(l1Token, l2Token)
return bridge.populateTransaction.deposit(l1Token, l2Token, amount, opts)
},
withdrawERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
const bridge = await this.provider.getBridgeForTokenPair(l1Token, l2Token)
return bridge.populateTransaction.withdraw(l1Token, l2Token, amount, opts)
},
}
estimateGas = {
......@@ -222,12 +282,13 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<BigNumber> => {
const tx = await this.populateTransaction.resendMessage(
message,
messageGasLimit,
opts
return this.provider.l1Provider.estimateGas(
await this.populateTransaction.resendMessage(
message,
messageGasLimit,
opts
)
)
return this.provider.l1Provider.estimateGas(tx)
},
finalizeMessage: async (
......@@ -246,8 +307,9 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<BigNumber> => {
const tx = await this.populateTransaction.depositETH(amount, opts)
return this.provider.l1Provider.estimateGas(tx)
return this.provider.l1Provider.estimateGas(
await this.populateTransaction.depositETH(amount, opts)
)
},
withdrawETH: async (
......@@ -256,8 +318,46 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<BigNumber> => {
const tx = await this.populateTransaction.withdrawETH(amount, opts)
return this.provider.l2Provider.estimateGas(tx)
return this.provider.l2Provider.estimateGas(
await this.populateTransaction.withdrawETH(amount, opts)
)
},
depositERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l2Provider.estimateGas(
await this.populateTransaction.depositERC20(
l1Token,
l2Token,
amount,
opts
)
)
},
withdrawERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l2Provider.estimateGas(
await this.populateTransaction.withdrawERC20(
l1Token,
l2Token,
amount,
opts
)
)
},
}
}
......@@ -4,7 +4,7 @@ import {
BlockTag,
TransactionReceipt,
} from '@ethersproject/abstract-provider'
import { ethers, BigNumber, Event } from 'ethers'
import { ethers, BigNumber } from 'ethers'
import { sleep } from '@eth-optimism/core-utils'
import {
......@@ -24,10 +24,11 @@ import {
TokenBridgeMessage,
MessageReceipt,
MessageReceiptStatus,
CustomBridges,
CustomBridgesLike,
BridgeAdapterData,
BridgeAdapters,
StateRoot,
StateRootBatch,
IBridgeAdapter,
} from './interfaces'
import {
toProvider,
......@@ -35,7 +36,7 @@ import {
toTransactionHash,
DeepPartial,
getAllOEContracts,
getCustomBridges,
getBridgeAdapters,
hashCrossChainMessage,
} from './utils'
......@@ -44,7 +45,7 @@ export class CrossChainProvider implements ICrossChainProvider {
public l2Provider: Provider
public l1ChainId: number
public contracts: OEContracts
public bridges: CustomBridges
public bridges: BridgeAdapters
/**
* Creates a new CrossChainProvider instance.
......@@ -61,7 +62,7 @@ export class CrossChainProvider implements ICrossChainProvider {
l2Provider: ProviderLike
l1ChainId: NumberLike
contracts?: DeepPartial<OEContractsLike>
bridges?: Partial<CustomBridgesLike>
bridges?: BridgeAdapterData
}) {
this.l1Provider = toProvider(opts.l1Provider)
this.l2Provider = toProvider(opts.l2Provider)
......@@ -71,9 +72,7 @@ export class CrossChainProvider implements ICrossChainProvider {
l2SignerOrProvider: this.l2Provider,
overrides: opts.contracts,
})
this.bridges = getCustomBridges(this.l1ChainId, {
l1SignerOrProvider: this.l1Provider,
l2SignerOrProvider: this.l2Provider,
this.bridges = getBridgeAdapters(this.l1ChainId, this, {
overrides: opts.bridges,
})
}
......@@ -153,113 +152,43 @@ export class CrossChainProvider implements ICrossChainProvider {
throw new Error('Not implemented')
}
public async getTokenBridgeMessagesByAddress(
address: AddressLike,
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,
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)
}
}
// 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')
}
if (bridges.length === 0) {
throw new Error(`no supported bridge for token pair`)
}
// 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))
}
}
if (bridges.length > 1) {
throw new Error(`found more than one bridge for token pair`)
}
// 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
)
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)
})
)
for (const withdrawalEvents of withdrawalEventSets) {
for (const event of withdrawalEvents) {
messages.push(parseTokenEvent(event, MessageDirection.L2_TO_L1))
}
}
}
return messages
).reduce((acc, val) => {
return acc.concat(val)
}, [])
}
public async getDepositsByAddress(
......@@ -269,10 +198,15 @@ export class CrossChainProvider implements ICrossChainProvider {
toBlock?: BlockTag
} = {}
): Promise<TokenBridgeMessage[]> {
return this.getTokenBridgeMessagesByAddress(address, {
...opts,
direction: MessageDirection.L1_TO_L2,
})
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(
......@@ -282,10 +216,15 @@ export class CrossChainProvider implements ICrossChainProvider {
toBlock?: BlockTag
} = {}
): Promise<TokenBridgeMessage[]> {
return this.getTokenBridgeMessagesByAddress(address, {
...opts,
direction: MessageDirection.L2_TO_L1,
})
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(
......
......@@ -2,3 +2,4 @@ export * from './interfaces'
export * from './utils'
export * from './cross-chain-provider'
export * from './cross-chain-messenger'
export * from './adapters'
import { Overrides, Contract } from 'ethers'
import { Contract, Overrides, Signer, BigNumber } from 'ethers'
import {
TransactionRequest,
TransactionResponse,
BlockTag,
} from '@ethersproject/abstract-provider'
import { NumberLike } from './types'
import { ICrossChainMessenger } from './cross-chain-messenger'
import {
NumberLike,
AddressLike,
MessageDirection,
TokenBridgeMessage,
} from './types'
import { ICrossChainProvider } from './cross-chain-provider'
/**
* Represents an L1<>L2 ERC20 token pair.
* Represents an adapter for an L1<>L2 token bridge. Each custom bridge currently needs its own
* adapter because the bridge interface is not standardized. This may change in the future.
*/
export interface ICrossChainERC20Pair {
export interface IBridgeAdapter {
/**
* Provider used to make queries related to cross-chain interactions.
*/
provider: ICrossChainProvider
/**
* L1 bridge contract.
*/
l1Bridge: Contract
/**
* Messenger that will be used to carry out cross-chain iteractions.
* L2 bridge contract.
*/
messenger: ICrossChainMessenger
l2Bridge: Contract
/**
* Ethers Contract object connected to the L1 token.
* 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.
*/
l1Token: Contract
getTokenBridgeMessagesByAddress(
address: AddressLike,
opts?: {
direction?: MessageDirection
}
): Promise<TokenBridgeMessage[]>
/**
* Ethers Contract object connected to the L2 token.
* Gets all deposits for a given address.
*
* @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.
*/
l2Token: Contract
getDepositsByAddress(
address: AddressLike,
opts?: {
fromBlock?: BlockTag
toBlock?: BlockTag
}
): Promise<TokenBridgeMessage[]>
/**
* Gets all withdrawals for a given address.
*
* @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[]>
/**
* Checks whether the given token pair is supported by the bridge.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @returns Whether the given token pair is supported by the bridge.
*/
supportsTokenPair(
l1Token: AddressLike,
l2Token: AddressLike
): Promise<boolean>
/**
* Deposits some tokens into the L2 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to deposit.
* @param signer Signer used to sign and send the transaction.
* @param opts Additional options.
* @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.
*/
deposit(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
signer: Signer,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
......@@ -46,13 +127,19 @@ export interface ICrossChainERC20Pair {
/**
* Withdraws some tokens back to the L1 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to withdraw.
* @param signer Signer used to sign and send the transaction.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the withdraw transaction.
*/
withdraw(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
signer: Signer,
opts?: {
overrides?: Overrides
}
......@@ -66,6 +153,8 @@ export interface ICrossChainERC20Pair {
/**
* Generates a transaction for depositing some tokens into the L2 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to deposit.
* @param opts Additional options.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
......@@ -73,22 +162,28 @@ export interface ICrossChainERC20Pair {
* @returns Transaction that can be signed and executed to deposit the tokens.
*/
deposit(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse>
): Promise<TransactionRequest>
/**
* Generates a transaction for withdrawing some tokens back to the L1 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to withdraw.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens.
*/
withdraw(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
......@@ -104,33 +199,41 @@ export interface ICrossChainERC20Pair {
/**
* Estimates gas required to deposit some tokens into the L2 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to deposit.
* @param opts Additional options.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to deposit the tokens.
* @returns Gas estimate for the transaction.
*/
deposit(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse>
): Promise<BigNumber>
/**
* Estimates gas required to withdraw some tokens back to the L1 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to withdraw.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens.
* @returns Gas estimate for the transaction.
*/
withdraw(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest>
): Promise<BigNumber>
}
}
......@@ -4,7 +4,12 @@ import {
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { MessageLike, NumberLike, CrossChainMessageRequest } from './types'
import {
MessageLike,
NumberLike,
CrossChainMessageRequest,
AddressLike,
} from './types'
import { ICrossChainProvider } from './cross-chain-provider'
/**
......@@ -110,6 +115,46 @@ export interface ICrossChainMessenger {
}
): Promise<TransactionResponse>
/**
* Deposits some ERC20 tokens into the L2 chain.
*
* @param l1Token Address of the L1 token.
* @param l2Token Address of the L2 token.
* @param amount Amount to deposit.
* @param opts Additional options.
* @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.
*/
depositERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse>
/**
* Withdraws some ERC20 tokens back to the L1 chain.
*
* @param l1Token Address of the L1 token.
* @param l2Token Address of the L2 token.
* @param amount Amount to withdraw.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the withdraw transaction.
*/
withdrawERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionResponse>
/**
* Object that holds the functions that generate transactions to be signed by the user.
* Follows the pattern used by ethers.js.
......@@ -191,7 +236,7 @@ export interface ICrossChainMessenger {
* @param amount Amount of ETH to withdraw.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens.
* @returns Transaction that can be signed and executed to withdraw the ETH.
*/
withdrawETH(
amount: NumberLike,
......@@ -199,6 +244,46 @@ export interface ICrossChainMessenger {
overrides?: Overrides
}
): Promise<TransactionRequest>
/**
* Generates a transaction for depositing some ERC20 tokens into the L2 chain.
*
* @param l1Token Address of the L1 token.
* @param l2Token Address of the L2 token.
* @param amount Amount to deposit.
* @param opts Additional options.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to deposit the tokens.
*/
depositERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionRequest>
/**
* Generates a transaction for withdrawing some ERC20 tokens back to the L1 chain.
*
* @param l1Token Address of the L1 token.
* @param l2Token Address of the L2 token.
* @param amount Amount to withdraw.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens.
*/
withdrawERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest>
}
/**
......@@ -213,7 +298,7 @@ export interface ICrossChainMessenger {
* @param opts Additional options.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to send the message.
* @returns Gas estimate for the transaction.
*/
sendMessage: (
message: CrossChainMessageRequest,
......@@ -230,7 +315,7 @@ export interface ICrossChainMessenger {
* @param messageGasLimit New gas limit to use for the message.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to resend the message.
* @returns Gas estimate for the transaction.
*/
resendMessage(
message: MessageLike,
......@@ -246,7 +331,7 @@ export interface ICrossChainMessenger {
* @param message Message to generate the finalization transaction for.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to finalize the message.
* @returns Gas estimate for the transaction.
*/
finalizeMessage(
message: MessageLike,
......@@ -262,7 +347,7 @@ export interface ICrossChainMessenger {
* @param opts Additional options.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to deposit the ETH.
* @returns Gas estimate for the transaction.
*/
depositETH(
amount: NumberLike,
......@@ -278,7 +363,7 @@ export interface ICrossChainMessenger {
* @param amount Amount of ETH to withdraw.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens.
* @returns Gas estimate for the transaction.
*/
withdrawETH(
amount: NumberLike,
......@@ -286,5 +371,45 @@ export interface ICrossChainMessenger {
overrides?: Overrides
}
): Promise<BigNumber>
/**
* Estimates gas required to deposit some ERC20 tokens into the L2 chain.
*
* @param l1Token Address of the L1 token.
* @param l2Token Address of the L2 token.
* @param amount Amount to deposit.
* @param opts Additional options.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Gas estimate for the transaction.
*/
depositERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<BigNumber>
/**
* Estimates gas required to withdraw some ERC20 tokens back to the L1 chain.
*
* @param l1Token Address of the L1 token.
* @param l2Token Address of the L2 token.
* @param amount Amount to withdraw.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Gas estimate for the transaction.
*/
withdrawERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<BigNumber>
}
}
......@@ -13,10 +13,11 @@ import {
TokenBridgeMessage,
OEContracts,
MessageReceipt,
CustomBridges,
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
......@@ -46,7 +47,7 @@ export interface ICrossChainProvider {
/**
* List of custom bridges for the given network.
*/
bridges: CustomBridges
bridges: BridgeAdapters
/**
* Retrieves all cross chain messages sent within a given transaction.
......@@ -87,6 +88,19 @@ export interface ICrossChainProvider {
}
): 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
......@@ -96,10 +110,6 @@ export interface ICrossChainProvider {
* @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 token bridge messages sent by the given address.
*/
getTokenBridgeMessagesByAddress(
......
export * from './cross-chain-erc20-pair'
export * from './bridge-adapter'
export * from './cross-chain-messenger'
export * from './cross-chain-provider'
export * from './l2-provider'
......
......@@ -6,6 +6,9 @@ import {
import { Signer } from '@ethersproject/abstract-signer'
import { Contract, BigNumber } from 'ethers'
import { ICrossChainProvider } from './cross-chain-provider'
import { IBridgeAdapter } from './bridge-adapter'
/**
* L1 contract references.
*/
......@@ -68,27 +71,25 @@ export interface OEContractsLike {
}
/**
* Represents list of custom bridges.
* Something that looks like the list of custom bridges.
*/
export interface CustomBridges {
l1: {
[name: string]: Contract
}
l2: {
[name: string]: Contract
export interface BridgeAdapterData {
[name: string]: {
Adapter: new (opts: {
provider: ICrossChainProvider
l1Bridge: AddressLike
l2Bridge: AddressLike
}) => IBridgeAdapter
l1Bridge: AddressLike
l2Bridge: AddressLike
}
}
/**
* 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
}
export interface BridgeAdapters {
[name: string]: IBridgeAdapter
}
/**
......
......@@ -10,9 +10,15 @@ import {
OEContractsLike,
OEL2ContractsLike,
AddressLike,
CustomBridges,
CustomBridgesLike,
BridgeAdapters,
BridgeAdapterData,
ICrossChainProvider,
} from '../interfaces'
import {
StandardBridgeAdapter,
ETHBridgeAdapter,
DAIBridgeAdapter,
} from '../adapters'
/**
* Full list of default L2 contract addresses.
......@@ -39,42 +45,6 @@ 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
......@@ -135,6 +105,93 @@ export const CONTRACT_ADDRESSES: {
},
}
/**
* Mapping of L1 chain IDs to the list of custom bridge addresses for each chain.
*/
export const BRIDGE_ADAPTER_DATA: {
[l1ChainId: number]: BridgeAdapterData
} = {
// TODO: Maybe we can pull these automatically from the token list?
// Alternatively, check against the token list in CI.
1: {
Standard: {
Adapter: StandardBridgeAdapter,
l1Bridge: CONTRACT_ADDRESSES[1].l1.L1StandardBridge,
l2Bridge: predeploys.L2StandardBridge,
},
ETH: {
Adapter: ETHBridgeAdapter,
l1Bridge: CONTRACT_ADDRESSES[1].l1.L1StandardBridge,
l2Bridge: predeploys.L2StandardBridge,
},
BitBTC: {
Adapter: StandardBridgeAdapter,
l1Bridge: '0xaBA2c5F108F7E820C049D5Af70B16ac266c8f128',
l2Bridge: '0x158F513096923fF2d3aab2BcF4478536de6725e2',
},
DAI: {
Adapter: DAIBridgeAdapter,
l1Bridge: '0x10E6593CDda8c58a1d0f14C5164B376352a55f2F',
l2Bridge: '0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65',
},
},
42: {
Standard: {
Adapter: StandardBridgeAdapter,
l1Bridge: CONTRACT_ADDRESSES[42].l1.L1StandardBridge,
l2Bridge: predeploys.L2StandardBridge,
},
ETH: {
Adapter: ETHBridgeAdapter,
l1Bridge: CONTRACT_ADDRESSES[42].l1.L1StandardBridge,
l2Bridge: predeploys.L2StandardBridge,
},
BitBTC: {
Adapter: StandardBridgeAdapter,
l1Bridge: '0x0b651A42F32069d62d5ECf4f2a7e5Bd3E9438746',
l2Bridge: '0x0CFb46528a7002a7D8877a5F7a69b9AaF1A9058e',
},
USX: {
Adapter: StandardBridgeAdapter,
l1Bridge: '0x40E862341b2416345F02c41Ac70df08525150dC7',
l2Bridge: '0xB4d37826b14Cd3CB7257A2A5094507d701fe715f',
},
DAI: {
Adapter: DAIBridgeAdapter,
l1Bridge: '0xb415e822C4983ecD6B1c1596e8a5f976cf6CD9e3',
l2Bridge: '0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65',
},
},
5: {
Standard: {
Adapter: StandardBridgeAdapter,
l1Bridge: CONTRACT_ADDRESSES[5].l1.L1StandardBridge,
l2Bridge: predeploys.L2StandardBridge,
},
ETH: {
Adapter: ETHBridgeAdapter,
l1Bridge: CONTRACT_ADDRESSES[5].l1.L1StandardBridge,
l2Bridge: predeploys.L2StandardBridge,
},
},
}
// TODO: PR is big enough as-is, will add support for SNX in another PR
// MAINNET
// l1: {
// SNX: '0xCd9D4988C0AE61887B075bA77f08cbFAd2b65068',
// },
// l2: {
// SNX: '0x3f87Ff1de58128eF8FCb4c807eFD776E1aC72E51',
// },
// KOVAN
// l1: {
// SNX: '0xD134Db47DDF5A6feB245452af17cCAf92ee53D3c',
// },
// l2: {
// SNX: '0x5C3f51CEd0C2F6157e2be67c029264D6C44bfe42',
// },
/**
* Returns an ethers.Contract object for the given name, connected to the appropriate address for
* the given L1 chain ID. Users can also provide a custom address to connect the contract to
......@@ -231,57 +288,32 @@ export const getAllOEContracts = (
}
/**
* Gets a series of custom bridges for the given L1 chain ID.
* 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 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.
* @param opts.overrides Custom bridge adapters.
* @returns An object containing all bridge adapters
*/
export const getCustomBridges = (
export const getBridgeAdapters = (
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
provider: ICrossChainProvider,
opts?: {
overrides?: BridgeAdapterData
}
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
)
): BridgeAdapters => {
const adapters: BridgeAdapters = {}
for (const [bridgeName, bridgeData] of Object.entries({
...(BRIDGE_ADAPTER_DATA[l1ChainId] || {}),
...(opts?.overrides || {}),
})) {
adapters[bridgeName] = new bridgeData.Adapter({
provider,
l1Bridge: bridgeData.l1Bridge,
l2Bridge: bridgeData.l2Bridge,
})
}
return bridges
return adapters
}
import './setup'
describe('CrossChainERC20Pair', () => {
describe('construction', () => {
it('should have a messenger')
describe('when the token is a standard bridge token', () => {
it('should resolve the correct bridge')
})
describe('when the token is SNX', () => {
it('should resolve the correct bridge')
})
describe('when the token is DAI', () => {
it('should resolve the correct bridge')
})
describe('when a custom adapter is provided', () => {
it('should use the custom adapter')
})
})
describe('deposit', () => {
describe('when the user has enough balance and allowance', () => {
describe('when the token is a standard bridge token', () => {
it('should trigger a token deposit')
})
describe('when the token is ETH', () => {
it('should trigger a token deposit')
})
describe('when the token is SNX', () => {
it('should trigger a token deposit')
})
describe('when the token is DAI', () => {
it('should trigger a token deposit')
})
})
describe('when the user does not have enough balance', () => {
it('should throw an error')
})
describe('when the user has not given enough allowance to the bridge', () => {
it('should throw an error')
})
})
describe('withdraw', () => {
describe('when the user has enough balance', () => {
describe('when the token is a standard bridge token', () => {
it('should trigger a token withdrawal')
})
describe('when the token is ETH', () => {
it('should trigger a token withdrawal')
})
describe('when the token is SNX', () => {
it('should trigger a token withdrawal')
})
describe('when the token is DAI', () => {
it('should trigger a token withdrawal')
})
})
describe('when the user does not have enough balance', () => {
it('should throw an error')
})
})
describe('populateTransaction', () => {
describe('deposit', () => {
it('should populate the transaction with the correct values')
})
describe('withdraw', () => {
it('should populate the transaction with the correct values')
})
})
describe('estimateGas', () => {
describe('deposit', () => {
it('should estimate gas required for the transaction')
})
describe('withdraw', () => {
it('should estimate gas required for the transaction')
})
})
})
......@@ -7,6 +7,7 @@ import {
CrossChainProvider,
CrossChainMessenger,
MessageDirection,
ETHBridgeAdapter,
} from '../src'
describe('CrossChainMessenger', () => {
......@@ -215,7 +216,9 @@ describe('CrossChainMessenger', () => {
describe('depositETH', () => {
let l1Messenger: Contract
let l2Messenger: Contract
let l1Bridge: Contract
let l2Bridge: Contract
let provider: CrossChainProvider
let messenger: CrossChainMessenger
beforeEach(async () => {
......@@ -225,6 +228,12 @@ describe('CrossChainMessenger', () => {
l1Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l1Messenger.address)) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l2Messenger.address)) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
......@@ -235,6 +244,17 @@ describe('CrossChainMessenger', () => {
L1CrossDomainMessenger: l1Messenger.address,
L1StandardBridge: l1Bridge.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
L2StandardBridge: l2Bridge.address,
},
},
bridges: {
ETH: {
Adapter: ETHBridgeAdapter,
l1Bridge: l1Bridge.address,
l2Bridge: l2Bridge.address,
},
},
})
......@@ -258,11 +278,19 @@ describe('CrossChainMessenger', () => {
})
describe('withdrawETH', () => {
let l1Messenger: Contract
let l2Messenger: Contract
let l1Bridge: Contract
let l2Bridge: Contract
let provider: CrossChainProvider
let messenger: CrossChainMessenger
beforeEach(async () => {
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l1Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l1Messenger.address)) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
......@@ -275,11 +303,22 @@ describe('CrossChainMessenger', () => {
l2Provider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
L1StandardBridge: l1Bridge.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
L2StandardBridge: l2Bridge.address,
},
},
bridges: {
ETH: {
Adapter: ETHBridgeAdapter,
l1Bridge: l1Bridge.address,
l2Bridge: l2Bridge.address,
},
},
})
messenger = new CrossChainMessenger({
......
......@@ -12,6 +12,7 @@ import {
omit,
MessageStatus,
CrossChainMessage,
StandardBridgeAdapter,
} from '../src'
import { DUMMY_MESSAGE } from './helpers'
......@@ -434,6 +435,13 @@ describe('CrossChainProvider', () => {
L2StandardBridge: l2Bridge.address,
},
},
bridges: {
Standard: {
Adapter: StandardBridgeAdapter,
l1Bridge: l1Bridge.address,
l2Bridge: l2Bridge.address,
},
},
})
})
......@@ -566,71 +574,6 @@ describe('CrossChainProvider', () => {
expect(found[1].to).to.deep.equal(withdrawal.to)
})
})
describe('when a block range is specified', () => {
describe('when a direction is specified', () => {
it('should find all deposits or withdrawals only in the given direction and within the block range', async () => {
const from = '0x' + '99'.repeat(20)
const deposit1 = {
l1Token: '0x' + '11'.repeat(20),
l2Token: '0x' + '22'.repeat(20),
from,
to: '0x' + '44'.repeat(20),
amount: ethers.BigNumber.from(1234),
data: '0x1234',
}
const deposit2 = {
l1Token: '0x' + '33'.repeat(20),
l2Token: '0x' + '44'.repeat(20),
from,
to: '0x' + '55'.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(deposit1)
const tx = await l1Bridge.emitERC20DepositInitiated(deposit2)
await l2Bridge.emitWithdrawalInitiated(withdrawal)
const found = await provider.getTokenBridgeMessagesByAddress(from, {
direction: MessageDirection.L1_TO_L2,
fromBlock: tx.blockNumber,
})
expect(found.length).to.equal(1)
expect(found[0].amount).to.deep.equal(deposit2.amount)
expect(found[0].data).to.deep.equal(deposit2.data)
expect(found[0].direction).to.equal(MessageDirection.L1_TO_L2)
expect(found[0].l1Token).to.deep.equal(deposit2.l1Token)
expect(found[0].l2Token).to.deep.equal(deposit2.l2Token)
expect(found[0].from).to.deep.equal(deposit2.from)
expect(found[0].to).to.deep.equal(deposit2.to)
})
})
describe('when a direction is not specified', () => {
it('should throw an error', async () => {
const from = '0x' + '99'.repeat(20)
await expect(
provider.getTokenBridgeMessagesByAddress(from, {
fromBlock: 0,
toBlock: 100,
})
).to.be.rejectedWith('direction must be specified')
})
})
})
})
describe('when the address has not made any deposits or withdrawals', () => {
......@@ -676,6 +619,13 @@ describe('CrossChainProvider', () => {
L2StandardBridge: l2Bridge.address,
},
},
bridges: {
Standard: {
Adapter: StandardBridgeAdapter,
l1Bridge: l1Bridge.address,
l2Bridge: l2Bridge.address,
},
},
})
})
......
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