diff --git a/apps/bridge/src/data/useDeposits.ts b/apps/bridge/src/data/useDeposits.ts index a49161104f..30eddec670 100644 --- a/apps/bridge/src/data/useDeposits.ts +++ b/apps/bridge/src/data/useDeposits.ts @@ -11,6 +11,7 @@ import getConfig from 'next/config'; import { DepositItem } from '@eth-optimism/indexer-api'; import { indexerTxToBridgeDeposit } from 'apps/bridge/src/utils/transactions/indexerTxToBridgeDeposit'; import { useChainEnv } from 'apps/bridge/src/utils/hooks/useChainEnv'; +import { dedupeTransactions } from 'apps/bridge/src/utils/array/dedupeTransactions'; const { publicRuntimeConfig } = getConfig(); @@ -43,7 +44,7 @@ async function fetchOPDeposits(address: string) { ); } -async function fetchCCTPDeposits(address: string, isMainnet: boolean) { +async function fetchExplorerDeposits(address: string, isMainnet: boolean) { const response = await getJSON>( publicRuntimeConfig.l1ExplorerApiUrl, { @@ -79,19 +80,20 @@ export function useDeposits(address: string): { }, ); - const { data: cctpDeposits, isFetched: isCCTPDepositsFetched } = useQuery( - ['cctpDeposits', address], - async () => fetchCCTPDeposits(address, isMainnet), - { - enabled: !!address, - suspense: false, // Does suspense work w/ SSR? We'll just not use it. - staleTime: 5000, // Stale after 5 seconds - notifyOnChangeProps: ['data', 'isFetched'], - refetchInterval: 1000 * 30, // Automatically refetch every 30 seconds - }, - ); + const { data: explorerDeposits, isFetched: isExplorerDepositsFetched } = useQuery< + BridgeTransaction[] + >(['explorerDeposits', address], async () => fetchExplorerDeposits(address, isMainnet), { + enabled: !!address, + suspense: false, // Does suspense work w/ SSR? We'll just not use it. + staleTime: 5000, // Stale after 5 seconds + notifyOnChangeProps: ['data', 'isFetched'], + refetchInterval: 1000 * 30, // Automatically refetch every 30 seconds + }); + + const deposits = dedupeTransactions([...(opDeposits ?? []), ...(explorerDeposits ?? [])]); + return { - deposits: [...(opDeposits ?? []), ...(cctpDeposits ?? [])], - isFetched: isOPDepositsFetched && isCCTPDepositsFetched, + deposits, + isFetched: isOPDepositsFetched && isExplorerDepositsFetched, }; } diff --git a/apps/bridge/src/data/useWithdrawals.ts b/apps/bridge/src/data/useWithdrawals.ts index 4331499dab..6c4a36458b 100644 --- a/apps/bridge/src/data/useWithdrawals.ts +++ b/apps/bridge/src/data/useWithdrawals.ts @@ -5,12 +5,13 @@ import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction'; import { useChainEnv } from 'apps/bridge/src/utils/hooks/useChainEnv'; import { explorerTxToBridgeWithdrawal } from 'apps/bridge/src/utils/transactions/explorerTxToBridgeWithdrawal'; import { - isExplorerTxETHOrERC20Withdrawal, isIndexerTxETHOrERC20Withdrawal, + isETHOrERC20OrCCTPWithdrawal, } from 'apps/bridge/src/utils/transactions/isETHOrERCWithdrawal'; import getConfig from 'next/config'; import { WithdrawalItem } from '@eth-optimism/indexer-api'; import { indexerTxToBridgeWithdrawal } from 'apps/bridge/src/utils/transactions/indexerTxToBridgeWithdrawal'; +import { dedupeTransactions } from 'apps/bridge/src/utils/array/dedupeTransactions'; const { publicRuntimeConfig } = getConfig(); @@ -45,7 +46,7 @@ async function fetchOPWithdrawals(address: string) { ); } -async function fetchCCTPWithdrawals(address: string, isMainnet: boolean) { +async function fetchExplorerWithdrawals(address: string, isMainnet: boolean) { const response = await getJSON>( // TODO: filter to transactions to the withdraw contract publicRuntimeConfig.l2ExplorerApiURL, @@ -59,7 +60,7 @@ async function fetchCCTPWithdrawals(address: string, isMainnet: boolean) { ); return explorerTxToBridgeWithdrawals( - response.result.filter((tx) => tx.isError !== '1' && isExplorerTxETHOrERC20Withdrawal(tx)), + response.result.filter((tx) => tx.isError !== '1' && isETHOrERC20OrCCTPWithdrawal(tx)), ); } @@ -82,9 +83,9 @@ export function useWithdrawals(address: string): { }, ); - const { data: cctpWithdrawals, isFetched: isCCTPWithdrawalsFetched } = useQuery< + const { data: explorerWithdrawals, isFetched: isExplorerWithdrawalsFetched } = useQuery< BridgeTransaction[] - >(['cctpWithdrawals', address], async () => fetchCCTPWithdrawals(address, isMainnet), { + >(['explorerWithdrawals', address], async () => fetchExplorerWithdrawals(address, isMainnet), { enabled: !!address, suspense: false, // Does suspense work w/ SSR? We'll just not use it. staleTime: 5000, // Stale after 5 seconds @@ -92,8 +93,13 @@ export function useWithdrawals(address: string): { refetchInterval: 1000 * 30, // Automatically refetch every 30 seconds }); + const withdrawals = dedupeTransactions([ + ...(opWithdrawals ?? []), + ...(explorerWithdrawals ?? []), + ]); + return { - withdrawals: [...(opWithdrawals ?? []), ...(cctpWithdrawals ?? [])], - isFetched: isOPWithdrawalsFetched && isCCTPWithdrawalsFetched, + withdrawals, + isFetched: isOPWithdrawalsFetched && isExplorerWithdrawalsFetched, }; } diff --git a/apps/bridge/src/utils/array/dedupeTransactions.ts b/apps/bridge/src/utils/array/dedupeTransactions.ts new file mode 100644 index 0000000000..0c710f5bae --- /dev/null +++ b/apps/bridge/src/utils/array/dedupeTransactions.ts @@ -0,0 +1,13 @@ +import { BridgeTransaction } from 'apps/bridge/src/types/BridgeTransaction'; + +export function dedupeTransactions(transactions: BridgeTransaction[]) { + const deduped = []; + const hashes = new Set(); + for (const transaction of transactions) { + if (!hashes.has(transaction.hash)) { + deduped.push(transaction); + hashes.add(transaction.hash); + } + } + return deduped; +} diff --git a/apps/bridge/src/utils/transactions/isETHOrERC20Deposit.ts b/apps/bridge/src/utils/transactions/isETHOrERC20Deposit.ts index 0bfeef0dfd..4e915aad35 100644 --- a/apps/bridge/src/utils/transactions/isETHOrERC20Deposit.ts +++ b/apps/bridge/src/utils/transactions/isETHOrERC20Deposit.ts @@ -1,3 +1,4 @@ +import { l1StandardBridgeABI } from '@eth-optimism/contracts-ts'; import { decodeFunctionData } from 'viem'; import { BlockExplorerTransaction } from 'apps/bridge/src/types/API'; import getConfig from 'next/config'; @@ -45,3 +46,58 @@ export function isIndexerTxETHOrERC20Deposit(tx: DepositItem) { return Boolean(token); } + +const ETH_DEPOSIT_ADDRESS = ( + publicRuntimeConfig?.l1OptimismPortalProxyAddress ?? '0xe93c8cD0D409341205A592f8c4Ac1A5fe5585cfA' +).toLowerCase(); + +const ERC20_DEPOSIT_ADDRESS = ( + publicRuntimeConfig?.l1BridgeProxyAddress ?? '0xfA6D8Ee5BE770F84FC001D098C4bD604Fe01284a' +).toLowerCase(); + +export function isETHOrERC20OrCCTPDeposit(tx: BlockExplorerTransaction) { + // Immediately filter out if tx is not to an address we don't care about + if ( + tx.to !== ETH_DEPOSIT_ADDRESS && + tx.to !== ERC20_DEPOSIT_ADDRESS && + tx.to !== CCTP_DEPOSIT_ADDRESS + ) { + return false; + } + + // ETH deposit + if (tx.to === ETH_DEPOSIT_ADDRESS && tx.value !== '0') { + return true; + } + + // ERC-20 desposit + if (tx.to === ERC20_DEPOSIT_ADDRESS) { + const { functionName, args } = decodeFunctionData({ + abi: l1StandardBridgeABI, + data: tx.input, + }); + if (functionName === 'depositERC20' || functionName === 'depositERC20To') { + const token = assetList.find( + (asset) => + asset.L1chainId === parseInt(publicRuntimeConfig.l1ChainID) && + asset.L1contract?.toLowerCase() === (args?.[0] as string).toLowerCase() && + asset.protocol === 'OP', + ); + // Return true if this is a depositERC20 call to the L1StandardBridge and is a token the UI supports + return Boolean(token); + } + } + + // CCTP deposit + if (tx.to === CCTP_DEPOSIT_ADDRESS && publicRuntimeConfig.cctpEnabled === 'true') { + const { functionName } = decodeFunctionData({ + abi: TokenMessenger, + data: tx.input, + }); + if (functionName === 'depositForBurn') { + return true; + } + } + + return false; +} diff --git a/apps/bridge/src/utils/transactions/isETHOrERCWithdrawal.ts b/apps/bridge/src/utils/transactions/isETHOrERCWithdrawal.ts index 9db16edce2..ec4540f4ad 100644 --- a/apps/bridge/src/utils/transactions/isETHOrERCWithdrawal.ts +++ b/apps/bridge/src/utils/transactions/isETHOrERCWithdrawal.ts @@ -5,6 +5,8 @@ import { getAssetListForChainEnv } from 'apps/bridge/src/utils/assets/getAssetLi import getConfig from 'next/config'; import { decodeFunctionData } from 'viem'; +import { l2StandardBridgeABI } from '@eth-optimism/contracts-ts'; + const { publicRuntimeConfig } = getConfig(); const assetList = getAssetListForChainEnv(); @@ -43,3 +45,52 @@ export function isIndexerTxETHOrERC20Withdrawal(tx: WithdrawalItem) { return Boolean(token); } + +const ETH_WITHDRAWAL_ADDRESS = ( + publicRuntimeConfig?.l2L1MessagePasserAddress ?? '0x4200000000000000000000000000000000000016' +).toLowerCase(); + +const ERC20_WITHDRAWAL_ADDRESS = ( + publicRuntimeConfig?.L2StandardBridge ?? '0x4200000000000000000000000000000000000010' +).toLowerCase(); + +export function isETHOrERC20OrCCTPWithdrawal(tx: BlockExplorerTransaction) { + // Immediately filter out if tx is not to an address we don't care about + if ( + tx.to !== ETH_WITHDRAWAL_ADDRESS && + tx.to !== ERC20_WITHDRAWAL_ADDRESS && + tx.to !== CCTP_WITHDRAWAL_ADDRESS + ) { + return false; + } + + // ETH withdrawal + if (tx.to === ETH_WITHDRAWAL_ADDRESS && tx.value !== '0') { + return true; + } + + // ERC-20 Withdrawal + if (tx.to === ERC20_WITHDRAWAL_ADDRESS) { + const { functionName, args } = decodeFunctionData({ abi: l2StandardBridgeABI, data: tx.input }); + if (functionName === 'withdraw' || functionName === 'withdrawTo') { + const token = assetList.find( + (asset) => + asset.L2chainId === parseInt(publicRuntimeConfig.l2ChainID) && + asset.L2contract?.toLowerCase() === ((args?.[0] as string) ?? '').toLowerCase() && + asset.protocol === 'OP', + ); + // Return true if this is a withdraw call to the L2StandardBridge and is a token the UI supports + return Boolean(token); + } + } + + // CCTP Withdrawal + if (tx.to === CCTP_WITHDRAWAL_ADDRESS && publicRuntimeConfig.cctpEnabled === 'true') { + const { functionName } = decodeFunctionData({ abi: TokenMessenger, data: tx.input }); + if (functionName === 'depositForBurn') { + return true; + } + } + + return false; +}