diff --git a/packages/sdk/src/adapters/dai-bridge.ts b/packages/sdk/src/adapters/dai-bridge.ts new file mode 100644 index 00000000000..2f7915bacf6 --- /dev/null +++ b/packages/sdk/src/adapters/dai-bridge.ts @@ -0,0 +1,63 @@ +/* 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 { + // 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 + } +} diff --git a/packages/sdk/src/adapters/eth-bridge.ts b/packages/sdk/src/adapters/eth-bridge.ts new file mode 100644 index 00000000000..ad1360da0c1 --- /dev/null +++ b/packages/sdk/src/adapters/eth-bridge.ts @@ -0,0 +1,142 @@ +/* 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 { + 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 { + 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 { + // 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 => { + 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 => { + 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 || {} + ) + }, + } +} diff --git a/packages/sdk/src/adapters/index.ts b/packages/sdk/src/adapters/index.ts new file mode 100644 index 00000000000..2d1168efb7a --- /dev/null +++ b/packages/sdk/src/adapters/index.ts @@ -0,0 +1,3 @@ +export * from './standard-bridge' +export * from './eth-bridge' +export * from './dai-bridge' diff --git a/packages/sdk/src/adapters/standard-bridge.ts b/packages/sdk/src/adapters/standard-bridge.ts new file mode 100644 index 00000000000..43b1a9bf784 --- /dev/null +++ b/packages/sdk/src/adapters/standard-bridge.ts @@ -0,0 +1,308 @@ +/* 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 => { + 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 => { + 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 => { + 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 => { + return this.provider.l2Provider.estimateGas( + await this.populateTransaction.withdraw(l1Token, l2Token, amount, opts) + ) + }, + } +} diff --git a/packages/sdk/src/cross-chain-messenger.ts b/packages/sdk/src/cross-chain-messenger.ts index 0745f81c2d0..460be7edab7 100644 --- a/packages/sdk/src/cross-chain-messenger.ts +++ b/packages/sdk/src/cross-chain-messenger.ts @@ -1,5 +1,5 @@ /* 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 { + 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 { + 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 => { - 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 => { - 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 => { + 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 => { + 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 => { - 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 => { - 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 => { - 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 => { + 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 => { + return this.provider.l2Provider.estimateGas( + await this.populateTransaction.withdrawERC20( + l1Token, + l2Token, + amount, + opts + ) + ) }, } } diff --git a/packages/sdk/src/cross-chain-provider.ts b/packages/sdk/src/cross-chain-provider.ts index 41241863754..e0f161bbbe5 100644 --- a/packages/sdk/src/cross-chain-provider.ts +++ b/packages/sdk/src/cross-chain-provider.ts @@ -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 - bridges?: Partial + 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 { - 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 { + 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 { + 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 { - 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 { - 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( diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 022bb13cefd..1f442e50c0d 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -2,3 +2,4 @@ export * from './interfaces' export * from './utils' export * from './cross-chain-provider' export * from './cross-chain-messenger' +export * from './adapters' diff --git a/packages/sdk/src/interfaces/bridge-adapter.ts b/packages/sdk/src/interfaces/bridge-adapter.ts new file mode 100644 index 00000000000..1297aafd3ae --- /dev/null +++ b/packages/sdk/src/interfaces/bridge-adapter.ts @@ -0,0 +1,239 @@ +import { Contract, Overrides, Signer, BigNumber } from 'ethers' +import { + TransactionRequest, + TransactionResponse, + BlockTag, +} from '@ethersproject/abstract-provider' + +import { + NumberLike, + AddressLike, + MessageDirection, + TokenBridgeMessage, +} from './types' +import { ICrossChainProvider } from './cross-chain-provider' + +/** + * 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 IBridgeAdapter { + /** + * Provider used to make queries related to cross-chain interactions. + */ + provider: ICrossChainProvider + + /** + * L1 bridge contract. + */ + l1Bridge: Contract + + /** + * L2 bridge contract. + */ + l2Bridge: Contract + + /** + * Finds all cross chain messages that correspond to token deposits or withdrawals sent by a + * particular address. Useful for finding deposits/withdrawals because the sender of the message + * will appear to be the StandardBridge contract and not the actual end user. + * + * @param address Address to search for messages from. + * @param opts Options object. + * @param opts.direction Direction to search for messages in. If not provided, will attempt to + * find all messages in both directions. + * @returns All token bridge messages sent by the given address. + */ + getTokenBridgeMessagesByAddress( + address: AddressLike, + opts?: { + direction?: MessageDirection + } + ): Promise + + /** + * 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. + */ + getDepositsByAddress( + address: AddressLike, + opts?: { + fromBlock?: BlockTag + toBlock?: BlockTag + } + ): Promise + + /** + * 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 + + /** + * 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 + + /** + * 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 + } + ): Promise + + /** + * 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 + } + ): Promise + + /** + * Object that holds the functions that generate transactions to be signed by the user. + * Follows the pattern used by ethers.js. + */ + populateTransaction: { + /** + * 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. + * @param opts.overrides Optional transaction overrides. + * @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 + + /** + * 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 + } + ): Promise + } + + /** + * Object that holds the functions that estimates the gas required for a given transaction. + * Follows the pattern used by ethers.js. + */ + estimateGas: { + /** + * 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 Gas estimate for the transaction. + */ + deposit( + l1Token: AddressLike, + l2Token: AddressLike, + amount: NumberLike, + opts?: { + l2GasLimit?: NumberLike + overrides?: Overrides + } + ): Promise + + /** + * 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 Gas estimate for the transaction. + */ + withdraw( + l1Token: AddressLike, + l2Token: AddressLike, + amount: NumberLike, + opts?: { + overrides?: Overrides + } + ): Promise + } +} diff --git a/packages/sdk/src/interfaces/cross-chain-erc20-pair.ts b/packages/sdk/src/interfaces/cross-chain-erc20-pair.ts deleted file mode 100644 index ef1412a3453..00000000000 --- a/packages/sdk/src/interfaces/cross-chain-erc20-pair.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Overrides, Contract } from 'ethers' -import { - TransactionRequest, - TransactionResponse, -} from '@ethersproject/abstract-provider' - -import { NumberLike } from './types' -import { ICrossChainMessenger } from './cross-chain-messenger' - -/** - * Represents an L1<>L2 ERC20 token pair. - */ -export interface ICrossChainERC20Pair { - /** - * Messenger that will be used to carry out cross-chain iteractions. - */ - messenger: ICrossChainMessenger - - /** - * Ethers Contract object connected to the L1 token. - */ - l1Token: Contract - - /** - * Ethers Contract object connected to the L2 token. - */ - l2Token: Contract - - /** - * Deposits some tokens into the L2 chain. - * - * @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 response for the deposit transaction. - */ - deposit( - amount: NumberLike, - opts?: { - l2GasLimit?: NumberLike - overrides?: Overrides - } - ): Promise - - /** - * Withdraws some tokens back to the L1 chain. - * - * @param amount Amount of the token to withdraw. - * @param opts Additional options. - * @param opts.overrides Optional transaction overrides. - * @returns Transaction response for the withdraw transaction. - */ - withdraw( - amount: NumberLike, - opts?: { - overrides?: Overrides - } - ): Promise - - /** - * Object that holds the functions that generate transactions to be signed by the user. - * Follows the pattern used by ethers.js. - */ - populateTransaction: { - /** - * Generates a transaction for depositing some tokens into the L2 chain. - * - * @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. - */ - deposit( - amount: NumberLike, - opts?: { - l2GasLimit?: NumberLike - overrides?: Overrides - } - ): Promise - - /** - * Generates a transaction for withdrawing some tokens back to the L1 chain. - * - * @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( - amount: NumberLike, - opts?: { - overrides?: Overrides - } - ): Promise - } - - /** - * Object that holds the functions that estimates the gas required for a given transaction. - * Follows the pattern used by ethers.js. - */ - estimateGas: { - /** - * Estimates gas required to deposit some tokens into the L2 chain. - * - * @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. - */ - deposit( - amount: NumberLike, - opts?: { - l2GasLimit?: NumberLike - overrides?: Overrides - } - ): Promise - - /** - * Estimates gas required to withdraw some tokens back to the L1 chain. - * - * @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( - amount: NumberLike, - opts?: { - overrides?: Overrides - } - ): Promise - } -} diff --git a/packages/sdk/src/interfaces/cross-chain-messenger.ts b/packages/sdk/src/interfaces/cross-chain-messenger.ts index 7865590c6a5..7b9de7ce067 100644 --- a/packages/sdk/src/interfaces/cross-chain-messenger.ts +++ b/packages/sdk/src/interfaces/cross-chain-messenger.ts @@ -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 + /** + * 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 + + /** + * 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 + /** * 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 + + /** + * 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 + + /** + * 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 } /** @@ -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 + + /** + * 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 + + /** + * 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 } } diff --git a/packages/sdk/src/interfaces/cross-chain-provider.ts b/packages/sdk/src/interfaces/cross-chain-provider.ts index 981a79c09f5..40316d44b0a 100644 --- a/packages/sdk/src/interfaces/cross-chain-provider.ts +++ b/packages/sdk/src/interfaces/cross-chain-provider.ts @@ -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 + /** + * 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 + /** * 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( diff --git a/packages/sdk/src/interfaces/index.ts b/packages/sdk/src/interfaces/index.ts index 9cecd167e35..b93c14e8c48 100644 --- a/packages/sdk/src/interfaces/index.ts +++ b/packages/sdk/src/interfaces/index.ts @@ -1,4 +1,4 @@ -export * from './cross-chain-erc20-pair' +export * from './bridge-adapter' export * from './cross-chain-messenger' export * from './cross-chain-provider' export * from './l2-provider' diff --git a/packages/sdk/src/interfaces/types.ts b/packages/sdk/src/interfaces/types.ts index 31dbcbd028c..38a5b7a4312 100644 --- a/packages/sdk/src/interfaces/types.ts +++ b/packages/sdk/src/interfaces/types.ts @@ -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 } /** diff --git a/packages/sdk/src/utils/contracts.ts b/packages/sdk/src/utils/contracts.ts index 9b009e739f2..d3da52f2904 100644 --- a/packages/sdk/src/utils/contracts.ts +++ b/packages/sdk/src/utils/contracts.ts @@ -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 - } = {} -): 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 } diff --git a/packages/sdk/test/cross-chain-erc20-pair.spec.ts b/packages/sdk/test/cross-chain-erc20-pair.spec.ts deleted file mode 100644 index 867148d7781..00000000000 --- a/packages/sdk/test/cross-chain-erc20-pair.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -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') - }) - }) -}) diff --git a/packages/sdk/test/cross-chain-messenger.spec.ts b/packages/sdk/test/cross-chain-messenger.spec.ts index 8d389eb8a6c..eda30059e3d 100644 --- a/packages/sdk/test/cross-chain-messenger.spec.ts +++ b/packages/sdk/test/cross-chain-messenger.spec.ts @@ -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({ diff --git a/packages/sdk/test/cross-chain-provider.spec.ts b/packages/sdk/test/cross-chain-provider.spec.ts index e005f2694af..0978896fe51 100644 --- a/packages/sdk/test/cross-chain-provider.spec.ts +++ b/packages/sdk/test/cross-chain-provider.spec.ts @@ -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, + }, + }, }) })