Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge transactions from indexer and explorer for redundancy #279

Merged
merged 1 commit into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions apps/bridge/src/data/useDeposits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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<BlockExplorerApiResponse<BlockExplorerTransaction[]>>(
publicRuntimeConfig.l1ExplorerApiUrl,
{
Expand Down Expand Up @@ -79,19 +80,20 @@ export function useDeposits(address: string): {
},
);

const { data: cctpDeposits, isFetched: isCCTPDepositsFetched } = useQuery<BridgeTransaction[]>(
['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,
};
}
20 changes: 13 additions & 7 deletions apps/bridge/src/data/useWithdrawals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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<BlockExplorerApiResponse<BlockExplorerTransaction[]>>(
// TODO: filter to transactions to the withdraw contract
publicRuntimeConfig.l2ExplorerApiURL,
Expand All @@ -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)),
);
}

Expand All @@ -82,18 +83,23 @@ 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
notifyOnChangeProps: ['data', 'isFetched'],
refetchInterval: 1000 * 30, // Automatically refetch every 30 seconds
});

const withdrawals = dedupeTransactions([
...(opWithdrawals ?? []),
...(explorerWithdrawals ?? []),
]);

return {
withdrawals: [...(opWithdrawals ?? []), ...(cctpWithdrawals ?? [])],
isFetched: isOPWithdrawalsFetched && isCCTPWithdrawalsFetched,
withdrawals,
isFetched: isOPWithdrawalsFetched && isExplorerWithdrawalsFetched,
};
}
13 changes: 13 additions & 0 deletions apps/bridge/src/utils/array/dedupeTransactions.ts
Original file line number Diff line number Diff line change
@@ -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;
}
56 changes: 56 additions & 0 deletions apps/bridge/src/utils/transactions/isETHOrERC20Deposit.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}
51 changes: 51 additions & 0 deletions apps/bridge/src/utils/transactions/isETHOrERCWithdrawal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
Loading