diff --git a/.changeset/shaggy-lamps-leave.md b/.changeset/shaggy-lamps-leave.md new file mode 100644 index 000000000..dbfe0913c --- /dev/null +++ b/.changeset/shaggy-lamps-leave.md @@ -0,0 +1,5 @@ +--- +"@rabbitholegg/questdk-plugin-connext": minor +--- + +combine filters for xcall and multisend diff --git a/packages/connext/src/Connext.test.ts b/packages/connext/src/Connext.test.ts index d93e1d09b..eaefde368 100644 --- a/packages/connext/src/Connext.test.ts +++ b/packages/connext/src/Connext.test.ts @@ -1,135 +1,89 @@ -import { getDeployedMultisendContract } from '@connext/nxtp-txservice' -import { MultisendAbi } from '@connext/nxtp-utils' -import { GreaterThanOrEqual, apply } from '@rabbitholegg/questdk/filter' -import type { TransactionEIP1559 } from 'viem' +import { apply } from '@rabbitholegg/questdk/filter' import { describe, expect, test } from 'vitest' -import { bridge } from './Connext.js' +import { + bridge, + getSupportedChainIds, + getSupportedTokenAddresses, +} from './Connext.js' +import { MultisendAbi } from '@connext/nxtp-utils' import { XCALL_ABI_FRAGMENTS } from './abi.js' +import { passingTestCases, failingTestCases } from './test-transactions.js' -describe('Connext', () => { - const ETH = '0x0000000000000000000000000000000000000000' - const OP_WETH = '0x4200000000000000000000000000000000000006' - - describe('Bridge', () => { - const USDC = '0x7F5c764cBc14f9669B88837ca1490cCa17c31607' - - test('should return a valid bridge action filter', async () => { - const filter = await bridge({ - sourceChainId: 10, - destinationChainId: 137, - tokenAddress: USDC, - amount: GreaterThanOrEqual(100000n), - }) - - expect(filter).to.deep.equal({ - chainId: 10, - to: '0x8f7492DE823025b4CfaAB1D34c58963F2af5DEDA', - input: { - $abi: XCALL_ABI_FRAGMENTS, - _destination: 1886350457, - _asset: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', - _amount: { - $gte: '100000', +describe('Given the Connext plugin', () => { + describe('When handling the bridge action', () => { + describe('should return a valid action filter', () => { + test('when doing a valid bridge action', async () => { + const { params } = passingTestCases[0] + const filter = await bridge(params) + expect(filter).to.deep.equal({ + chainId: 10, + to: { + $or: [ + '0x8f7492de823025b4cfaab1d34c58963f2af5deda', + '0xb0eef3e1de973d045c3858e072c540299585252d', + ], + }, + from: '0xd59a74e615c9d55422ed8c5ce64cb50fda0bb58d', + input: { + $or: [ + { + $abi: MultisendAbi, + transactions: { + $regex: 'd59a74e615c9d55422ed8c5ce64cb50fda0bb58d', + }, + }, + { + $abi: XCALL_ABI_FRAGMENTS, + _destination: 6778479, + _asset: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', + _amount: { + $gte: '2000000000000000000', + }, + _delegate: '0xd59a74e615c9d55422ed8c5ce64cb50fda0bb58d', + }, + ], }, - }, + }) }) }) - }) - test('should use the WETH wrapper multisend contract when bridging ETH', async () => { - const filter = await bridge({ - sourceChainId: 10, - destinationChainId: 137, - tokenAddress: ETH, - amount: GreaterThanOrEqual(100000n), - }) - - const multiSendContract = getDeployedMultisendContract(10) - - expect(filter).to.deep.equal({ - chainId: 10, - to: multiSendContract?.address, - value: { - $gte: '100000', - }, - input: { - $abi: MultisendAbi, - transactions: { - $regex: OP_WETH.slice(2), - }, - }, + describe('should pass filter with valid transactions', () => { + passingTestCases.forEach((testCase) => { + const { transaction, description, params } = testCase + test(description, async () => { + const filter = await bridge(params) + expect(apply(transaction, filter)).to.be.true + }) + }) }) - }) - describe('Apply filter', () => { - test('transaction should pass filter', async () => { - const transaction = { - blockHash: - '0x80ca121924779dc65f575409be05e7c2cbaf718858e6e20d66d130cf1acec4f3', - blockNumber: '0x6606396', - from: '0xd59a74e615c9d55422ed8c5ce64cb50fda0bb58d', - gas: '0x73766', - gasPrice: '0xc9', - maxFeePerGas: '0x17a', - maxPriorityFeePerGas: '0x71', - hash: '0x22d3715ca5ae0bd0d87f9341fafc7a330fd6962e13bf318a6a541c93e4e6bc04', - input: - '0x93f18ac50000000000000000000000000000000000000000000000000000000000676e6f000000000000000000000000642c27a96dffb6f21443a89b789a3194ff8399fa000000000000000000000000da10009cbd5d07dd0cecc66161fc93d7c9000da1000000000000000000000000d59a74e615c9d55422ed8c5ce64cb50fda0bb58d000000000000000000000000000000000000000000000000204d764a78eac238000000000000000000000000000000000000000000000000000000000000012c000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000050b91c62117dc0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000d59a74e615c9d55422ed8c5ce64cb50fda0bb58d', - nonce: '0x3', - to: '0x8f7492de823025b4cfaab1d34c58963f2af5deda', - transactionIndex: '0x3', - value: '0x0', - type: '0x2', - accessList: [], - chainId: 10, - v: '0x0', - r: '0x4fcc9247c0b29a50b9db146191e93fbd503be520998f62c9445aa2eeb944d613', - s: '0x78e1ace158ae1166687db900743fc7db36cc80e42a43a800577895c5cb2b95b3', - } - - const filter = await bridge({ - sourceChainId: 10, - destinationChainId: 100, // xDAI Chain - tokenAddress: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', - amount: GreaterThanOrEqual(2000000000000000000n), + describe('should not pass filter with invalid transactions', () => { + failingTestCases.forEach((testCase) => { + const { transaction, description, params } = testCase + test(description, async () => { + const filter = await bridge(params) + expect(apply(transaction, filter)).to.be.false + }) }) - - expect(apply(transaction, filter)).to.be.true }) - test('ETH bridge should pass filter', async () => { - const transaction: TransactionEIP1559 = { - blockHash: - '0xfdb722e4a99e3422490bc12d15fafab54ebb7e2e83ff08e9fe20d70045e94889', - blockNumber: BigInt('0x67812f1'), - from: '0xa4c8bb4658bc44bac430699c8b7b13dab28e0f4e', - gas: BigInt('0x8820a'), - maxFeePerGas: BigInt('0x11e2'), - maxPriorityFeePerGas: BigInt('0x112d'), - hash: '0xb8e2c0baf137b64553c91f286bde62cc37275d0b9f9d3e6c0041c6be79de45af', - input: - '0x8d80ff0a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000026b004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000008af8a1fa5fcc180000000000000000000000000000000000000000000000000000000000000004d0e30db000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000008f7492de823025b4cfaab1d34c58963f2af5deda000000000000000000000000000000000000000000000000008af8a1fa5fcc18008f7492de823025b4cfaab1d34c58963f2af5deda0000000000000000000000000000000000000000000000000026aa1a3465338400000000000000000000000000000000000000000000000000000000000001248aac16ba0000000000000000000000000000000000000000000000000000000000657468000000000000000000000000268682b7d9992ae7e2ca4a8bcc9d9655fb06056f0000000000000000000000004200000000000000000000000000000000000006000000000000000000000000a4c8bb4658bc44bac430699c8b7b13dab28e0f4e000000000000000000000000000000000000000000000000008af8a1fa5fcc18000000000000000000000000000000000000000000000000000000000000012c00000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a4c8bb4658bc44bac430699c8b7b13dab28e0f4e000000000000000000000000000000000000000000', - nonce: 45, - to: '0xb0eef3e1de973d045c3858e072c540299585252d', - transactionIndex: 6, - value: BigInt('0xb1a2bc2ec4ff9c'), - type: 'eip1559', - accessList: [], - typeHex: '0x2', - chainId: 10, - v: BigInt('0x1'), - r: '0x11d6fd962cf4090c1464404492a36ef7323c6173908883c87fbc695219e6d026', - s: '0x739b7240f8dd466881a66971b476f07c2fcce9a6f519d3c06f95133ad983092e', - } - - const filter = await bridge({ - sourceChainId: 10, - destinationChainId: 137, - tokenAddress: ETH, - amount: GreaterThanOrEqual(1000000000000000n), + describe('should return a valid list of tokens for each supported chain', async () => { + const chainIdArray = await getSupportedChainIds() + chainIdArray.forEach((chainId) => { + test(`for chainId: ${chainId}`, async () => { + const tokens = await getSupportedTokenAddresses(chainId) + const addressRegex = /^0x[a-fA-F0-9]{40}$/ + expect(tokens).to.be.an('array') + expect(tokens).to.have.length.greaterThan(0) + expect(tokens).to.have.length.lessThan(100) + tokens.forEach((token) => { + expect(token).to.match( + addressRegex, + `Token address ${token} is not a valid Ethereum address`, + ) + }) + }) }) - - expect(apply(transaction, filter)).to.be.true }) }) }) diff --git a/packages/connext/src/Connext.ts b/packages/connext/src/Connext.ts index b29289b2a..a23945e14 100644 --- a/packages/connext/src/Connext.ts +++ b/packages/connext/src/Connext.ts @@ -11,13 +11,13 @@ import { type TransactionFilter, compressJson, } from '@rabbitholegg/questdk' -import { type Address } from 'viem' +import { type Address, zeroAddress } from 'viem' import { XCALL_ABI_FRAGMENTS } from './abi.js' import { ConnextContract } from './contract-addresses.js' let _chainDataCache: Map | null = null -const ETH_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000' +const ETH_TOKEN_ADDRESS = zeroAddress const _getChainData = async () => { if (!_chainDataCache) { @@ -28,82 +28,56 @@ const _getChainData = async () => { return _chainDataCache } -export const getWETHAddress = async (chainId: number) => { - const chains = await _getChainData() - const domainId = chainIdToDomain(chainId) - const chainData = chains?.get(String(domainId)) - const assets = Object.keys(chainData?.assetId || {}) - - let wethAddress - for (const address of assets) { - if (chainData?.assetId[address].symbol === 'WETH') { - wethAddress = address - break - } - } - return wethAddress -} - export const bridge = async ( bridge: BridgeActionParams, ): Promise => { - const { - sourceChainId, - destinationChainId, - contractAddress, - tokenAddress, - amount, - recipient, - } = bridge - - const defaultContractAddress = ConnextContract[sourceChainId] - const destinationDomain = chainIdToDomain(destinationChainId) - const requiresWrapperMultisend = tokenAddress === ETH_TOKEN_ADDRESS - - /* - Connext uses a MultiSend to wrap when briding ETH. - https://github.com/connext/monorepo/issues/2905 - https://github.com/connext/monorepo/issues/4218 - Contract addresses: https://github.com/search?q=repo%3Aconnext%2Fmonorepo%20MultiSend.json&type=code - */ - - if (requiresWrapperMultisend) { - const multiSendContract = getDeployedMultisendContract(sourceChainId) - - if (!multiSendContract) { - throw new Error( - `No multisend contract deployed on chain ${sourceChainId}`, - ) - } - - const wethAddress = await getWETHAddress(sourceChainId) - - if (!wethAddress) { - throw new Error(`No WETH address found on chain ${sourceChainId}`) - } + const { sourceChainId, destinationChainId, tokenAddress, amount, recipient } = + bridge + + const xcallContractAddress = ConnextContract[sourceChainId] + const destinationDomain = destinationChainId + ? chainIdToDomain(destinationChainId) + : undefined + const multiSendContractAddress = + getDeployedMultisendContract(sourceChainId)?.address + const ethUsedIn = tokenAddress === ETH_TOKEN_ADDRESS + + if (!xcallContractAddress) { + throw new Error(`No xcall contract deployed on chain ${sourceChainId}`) + } - return compressJson({ - chainId: sourceChainId, - to: multiSendContract.address, - value: amount, - input: { - $abi: MultisendAbi, - transactions: { - $regex: wethAddress.slice(2), - }, - }, - }) + if (!multiSendContractAddress) { + throw new Error(`No multisend contract deployed on chain ${sourceChainId}`) } return compressJson({ chainId: sourceChainId, - to: contractAddress || defaultContractAddress, + to: { + $or: [ + xcallContractAddress.toLowerCase(), + multiSendContractAddress.toLowerCase(), + ], + }, + from: recipient, + value: ethUsedIn ? amount : undefined, input: { - $abi: XCALL_ABI_FRAGMENTS, - _destination: Number(destinationDomain), - _asset: tokenAddress, - _amount: amount, - _to: recipient, + $or: [ + { + $abi: MultisendAbi, + transactions: { + $regex: recipient?.toLowerCase().slice(2), + }, + }, + { + $abi: XCALL_ABI_FRAGMENTS, + _destination: destinationDomain + ? Number(destinationDomain) + : undefined, + _asset: tokenAddress, + _amount: amount, + _delegate: recipient, + }, + ], }, }) } diff --git a/packages/connext/src/README.md b/packages/connext/src/README.md new file mode 100644 index 000000000..98664a5f8 --- /dev/null +++ b/packages/connext/src/README.md @@ -0,0 +1,12 @@ +# Connext Plugin + +## Overview +Connext is a cross chain bridge protocol on Ethereum which services many popular L2 chains. The bridge transaction will use one of two method depending on wether ETH or tokens were bridged. For ETH, it will use `multiSend` and for tokens it will use `xCall` + +## Limitations +- There should be no limitations with the current bridge plugin. Everything should work as expected. + +## Sample Transactions +- [multiSend](https://etherscan.io/tx/0xb8e2c0baf137b64553c91f286bde62cc37275d0b9f9d3e6c0041c6be79de45af) +- [xCall](https://optimistic.etherscan.io/tx/0x22d3715ca5ae0bd0d87f9341fafc7a330fd6962e13bf318a6a541c93e4e6bc04) + diff --git a/packages/connext/src/test-transactions.ts b/packages/connext/src/test-transactions.ts new file mode 100644 index 000000000..bf88e00ff --- /dev/null +++ b/packages/connext/src/test-transactions.ts @@ -0,0 +1,104 @@ +import type { BridgeActionParams } from '@rabbitholegg/questdk' +import { GreaterThanOrEqual } from '@rabbitholegg/questdk' +import { createTestCase, type TestParams } from './utils' +import { parseEther, parseUnits, zeroAddress } from 'viem' + +export const DAI_OP_GNOSIS: TestParams = { + transaction: { + chainId: 10, + to: '0x8f7492de823025b4cfaab1d34c58963f2af5deda', + from: '0xd59a74e615c9d55422ed8c5ce64cb50fda0bb58d', + hash: '0x22d3715ca5ae0bd0d87f9341fafc7a330fd6962e13bf318a6a541c93e4e6bc04', + input: + '0x93f18ac50000000000000000000000000000000000000000000000000000000000676e6f000000000000000000000000642c27a96dffb6f21443a89b789a3194ff8399fa000000000000000000000000da10009cbd5d07dd0cecc66161fc93d7c9000da1000000000000000000000000d59a74e615c9d55422ed8c5ce64cb50fda0bb58d000000000000000000000000000000000000000000000000204d764a78eac238000000000000000000000000000000000000000000000000000000000000012c000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000050b91c62117dc0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000d59a74e615c9d55422ed8c5ce64cb50fda0bb58d', + value: '0', + }, + params: { + sourceChainId: 10, + destinationChainId: 100, + tokenAddress: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', + amount: GreaterThanOrEqual(parseEther('2')), + recipient: '0xd59a74e615c9d55422ed8c5ce64cb50fda0bb58d', + }, +} + +export const ETH_OP_POLYGON: TestParams = { + transaction: { + chainId: 10, + to: '0xb0eef3e1de973d045c3858e072c540299585252d', + from: '0xa4c8bb4658bc44bac430699c8b7b13dab28e0f4e', + hash: '0xb8e2c0baf137b64553c91f286bde62cc37275d0b9f9d3e6c0041c6be79de45af', + input: + '0x8d80ff0a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000026b004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000008af8a1fa5fcc180000000000000000000000000000000000000000000000000000000000000004d0e30db000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000008f7492de823025b4cfaab1d34c58963f2af5deda000000000000000000000000000000000000000000000000008af8a1fa5fcc18008f7492de823025b4cfaab1d34c58963f2af5deda0000000000000000000000000000000000000000000000000026aa1a3465338400000000000000000000000000000000000000000000000000000000000001248aac16ba0000000000000000000000000000000000000000000000000000000000657468000000000000000000000000268682b7d9992ae7e2ca4a8bcc9d9655fb06056f0000000000000000000000004200000000000000000000000000000000000006000000000000000000000000a4c8bb4658bc44bac430699c8b7b13dab28e0f4e000000000000000000000000000000000000000000000000008af8a1fa5fcc18000000000000000000000000000000000000000000000000000000000000012c00000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a4c8bb4658bc44bac430699c8b7b13dab28e0f4e000000000000000000000000000000000000000000', + value: '49999999999999900', + }, + params: { + sourceChainId: 10, + destinationChainId: 137, + tokenAddress: zeroAddress, + amount: GreaterThanOrEqual(parseEther('0.001')), + recipient: '0xa4c8bb4658bc44bac430699c8b7b13dab28e0f4e', + }, +} + +export const ETH_OP_ARBITRUM: TestParams = { + transaction: { + chainId: 10, + to: '0xb0eef3e1de973d045c3858e072c540299585252d', + from: '0x9333f2d9861e367dbf6072bcd55325cc133c6d86', + hash: '0x00b92f23522b23648f0bb05529a728dcca55b1cc08347d68bae50c24977621ed', + input: + '0x8d80ff0a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000026b00420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000003258eb8854cd0000000000000000000000000000000000000000000000000000000000000004d0e30db000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000008f7492de823025b4cfaab1d34c58963f2af5deda00000000000000000000000000000000000000000000000000003258eb8854cd008f7492de823025b4cfaab1d34c58963f2af5deda0000000000000000000000000000000000000000000000000000ba1f0c1beadc00000000000000000000000000000000000000000000000000000000000001248aac16ba000000000000000000000000000000000000000000000000000000006172626f000000000000000000000000429b9eb01362b2799131efcc44319689b662999d00000000000000000000000042000000000000000000000000000000000000060000000000000000000000009333f2d9861e367dbf6072bcd55325cc133c6d8600000000000000000000000000000000000000000000000000003258eb8854cd000000000000000000000000000000000000000000000000000000000000012c00000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000200000000000000000000000009333f2d9861e367dbf6072bcd55325cc133c6d86000000000000000000000000000000000000000000', + value: '49999999999999900', + }, + params: { + sourceChainId: 10, + destinationChainId: 42161, + tokenAddress: zeroAddress, + amount: GreaterThanOrEqual(parseEther('0.00005')), + recipient: '0x9333f2d9861e367dbf6072bcd55325cc133c6d86', + }, +} + +export const passingTestCases = [ + createTestCase(DAI_OP_GNOSIS, 'when bridging tokens'), + createTestCase(ETH_OP_POLYGON, 'when bridging ETH'), + createTestCase(ETH_OP_ARBITRUM, 'when bridging ETH to arbitrum'), + createTestCase(DAI_OP_GNOSIS, 'when bridging tokens', { + tokenAddress: undefined, + amount: undefined, + }), + createTestCase(ETH_OP_POLYGON, 'when bridging ETH', { + tokenAddress: undefined, + amount: undefined, + }), + createTestCase(ETH_OP_ARBITRUM, 'when bridging ETH to arbitrum', { + tokenAddress: undefined, + amount: undefined, + }), +] + +export const failingTestCases = [ + createTestCase(DAI_OP_GNOSIS, 'when sourceChainId is not correct', { + sourceChainId: 42161, + }), + createTestCase(DAI_OP_GNOSIS, 'when destinationChainId is not correct', { + destinationChainId: 42161, + }), + createTestCase(DAI_OP_GNOSIS, 'when tokenAddress is not correct', { + tokenAddress: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', + }), + createTestCase( + ETH_OP_POLYGON, + 'when bridging ETH and amount is insufficient', + { amount: GreaterThanOrEqual(parseEther('1000')) }, + ), + createTestCase( + DAI_OP_GNOSIS, + 'when bridging tokens and amount is insufficient', + { amount: GreaterThanOrEqual(parseUnits('10000', 18)) }, + ), + createTestCase(ETH_OP_ARBITRUM, 'when recipient is not correct', { + recipient: '0xa4c8bb4658bc44bac430699c8b7b13dab28e0f4e', + }), +] diff --git a/packages/connext/src/utils.ts b/packages/connext/src/utils.ts new file mode 100644 index 000000000..3b1cdc769 --- /dev/null +++ b/packages/connext/src/utils.ts @@ -0,0 +1,63 @@ +import type { ActionParams } from '@rabbitholegg/questdk' +import { type Address, type Hash } from 'viem' + +export enum Chains { + ETHEREUM = 1, + OPTIMISM = 10, + BINANCE_SMART_CHAIN = 56, + GNOSIS = 100, + POLYGON_POS = 137, + ZK_SYNC_ERA = 324, + POLYGON_ZK = 1101, + MANTLE = 5000, + BASE = 8453, + ARBITRUM_ONE = 42161, + AVALANCHE = 43114, + LINEA = 59144, + SCROLL = 534352, +} + +interface Transaction { + chainId: number + from: Address + hash?: Hash + input: string + to: Address + value: string +} + +export interface TestCase { + transaction: Transaction + params: T + description: string +} + +export type TestParams = { + transaction: Transaction + params: T +} + +/** + * Creates a test case object for a given action and transaction. + * + * This function takes a `TestParams` object that includes both a `Transaction` and + * `ActionParams`, a description of the test case, and an optional set of overrides + * for the action parameters. It returns a `TestCase` object that contains the transaction, + * the combined action parameters with any overrides applied, and the description. + * + * @param {TestParams} testParams - An object containing the transaction and action parameters. + * @param {string} description - A brief description of the test case. + * @param {Partial} [overrides] - Optional overrides for the action parameters. + * @returns {TestCase} A test case object with the transaction, params, and description. + */ +export function createTestCase( + testParams: TestParams, + description: string, + overrides: Partial = {}, +): TestCase { + return { + transaction: testParams.transaction, + params: { ...testParams.params, ...overrides }, + description, + } +}