-
Notifications
You must be signed in to change notification settings - Fork 218
feat: indexer poc #2545
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
base: bump-react-19
Are you sure you want to change the base?
feat: indexer poc #2545
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { createClient } from '@ponder/client' | ||
|
|
||
| // Import the schema from the indexer package | ||
| // For now, we'll use a simple schema definition since we can't directly import | ||
| // from the indexer due to module resolution issues | ||
| const schema = { | ||
| DepositErc20: { | ||
| id: '', | ||
| fromAddress: '', | ||
| toAddress: '', | ||
| l1TokenAddress: '', | ||
| amount: BigInt(0), | ||
| statusOnChildChain: '', | ||
| parentChainTimestamp: BigInt(0) | ||
| }, | ||
| DepositEth: { | ||
| id: '', | ||
| parentChainSenderAddress: '', | ||
| childChainRecipientAddress: '', | ||
| ethAmountDepositedToChildChain: BigInt(0), | ||
| statusOnChildChain: '', | ||
| parentChainTimestamp: BigInt(0) | ||
| } | ||
| } | ||
|
|
||
| // Create client instance that connects to the ponder indexer API | ||
| export const client = createClient('http://localhost:42069/sql', { schema }) | ||
|
|
||
| // Export operators for building queries (these will be passed to the query functions) | ||
| export { eq, desc, and, gte, lte } from 'drizzle-orm' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| import { useMemo } from 'react' | ||
| import { Address } from 'viem' | ||
| import useSWRImmutable from 'swr/immutable' | ||
| import useSWR from 'swr' | ||
| import { getProviderForChainId } from '@/token-bridge-sdk/utils' | ||
| import { useArbitrumIndexer } from '@arbitrum/indexer-provider' | ||
| import { AssetType } from '../../hooks/arbTokenBridge.types' | ||
| import { DepositStatus, MergedTransaction } from '../../state/app/state' | ||
| import { BigNumber, utils } from 'ethers' | ||
| import { ether } from '../../constants' | ||
| import { | ||
| sortByTimestampDescending, | ||
| UseTransactionHistoryResult | ||
| } from '../../hooks/useTransactionHistory' | ||
| import { isExperimentalFeatureEnabled } from '../../util' | ||
| import { useTokensFromLists } from '../TransferPanel/TokenSearchUtils' | ||
| import { fetchErc20Data } from '../../util/TokenUtils' | ||
|
|
||
| type PartialTransfer = { | ||
| fromAddress: string | ||
| toAddress: string | ||
| timestamp: bigint | ||
| executionTimestamp: bigint | ||
| status: string | ||
| amount: bigint | ||
| txHash: string | ||
| childChainId: number | ||
| parentChainId: number | ||
| } | ||
|
|
||
| type EthIndexerTransfer = PartialTransfer & { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mirrored whatever indexer was returning, but maybe we can now get types from some package @douglance? If not then we have to keep them defined here. |
||
| type: 'ETH' | ||
| tokenAddress: undefined | ||
| } | ||
|
|
||
| type Erc20IndexerTransfer = PartialTransfer & { | ||
| type: 'ERC20' | ||
| tokenAddress: string | ||
| } | ||
|
|
||
| type IndexerTransfer = EthIndexerTransfer | Erc20IndexerTransfer | ||
|
|
||
| function getIndexerTransferStatus(tx: IndexerTransfer) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Took from indexer some time ago but these may have changed
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to map them to whatever we use in our UI |
||
| switch (tx.status) { | ||
| case 'PARENT_CHAIN_CONFIRMED': | ||
| case 'CHILD_CHAIN_REDEMPTION_SCHEDULED': | ||
| return 'pending' | ||
| case 'CHILD_CHAIN_EXECUTED': | ||
| return 'success' | ||
| default: | ||
| return 'failure' | ||
| } | ||
| } | ||
|
|
||
| function getIndexerDepositStatus(tx: IndexerTransfer): DepositStatus { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, may have changed |
||
| switch (tx.status) { | ||
| case 'PARENT_CHAIN_CONFIRMED': | ||
| return DepositStatus.L1_PENDING | ||
| case 'CHILD_CHAIN_REDEMPTION_SCHEDULED': | ||
| return DepositStatus.L2_PENDING | ||
| case 'CHILD_CHAIN_EXECUTED': | ||
| return DepositStatus.L2_SUCCESS | ||
| default: | ||
| return DepositStatus.L2_FAILURE | ||
| } | ||
| } | ||
|
|
||
| type TokenDetails = { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indexer only gives us token address because it would take a long time to fetch token data and store it. We need to fetch the token data ourselves. see L162 |
||
| name: string | ||
| symbol: string | ||
| decimals: number | ||
| } | ||
|
|
||
| function transformIndexerTransfer(params: { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some overrides for ETH or ERC20 |
||
| tx: EthIndexerTransfer | ||
| tokenDetails?: undefined | ||
| }): MergedTransaction | ||
|
|
||
| function transformIndexerTransfer(params: { | ||
| tx: Erc20IndexerTransfer | ||
| tokenDetails: TokenDetails | ||
| }): MergedTransaction | ||
|
|
||
| function transformIndexerTransfer(params: { | ||
| tx: IndexerTransfer | ||
| tokenDetails?: TokenDetails | undefined | ||
| }): MergedTransaction { | ||
| const { tx } = params | ||
| const tokenDetails = | ||
| 'tokenDetails' in params ? params.tokenDetails : undefined | ||
|
|
||
| return { | ||
| sender: tx.fromAddress, | ||
| destination: tx.toAddress, | ||
| direction: 'deposit', | ||
| depositStatus: getIndexerDepositStatus(tx), | ||
| status: getIndexerTransferStatus(tx), | ||
| createdAt: Number(tx.timestamp) * 1_000, | ||
| resolvedAt: Number(tx.executionTimestamp) * 1_000, | ||
| txId: tx.txHash, | ||
| asset: tokenDetails?.symbol ?? ether.symbol, | ||
| assetType: tx.type as AssetType, | ||
| value: utils.formatUnits( | ||
| tx.amount, | ||
| tokenDetails?.decimals ?? ether.decimals | ||
| ), | ||
| uniqueId: BigNumber.from(0), | ||
| isWithdrawal: false, | ||
| blockNum: 0, | ||
| tokenAddress: tx.tokenAddress || null, | ||
| childChainId: tx.childChainId, | ||
| parentChainId: tx.parentChainId, | ||
| sourceChainId: tx.parentChainId, | ||
| destinationChainId: tx.childChainId | ||
| } | ||
| } | ||
|
|
||
| export const useIndexerHistory = ( | ||
| address?: Address | ||
| ): Omit< | ||
| UseTransactionHistoryResult, | ||
| 'addPendingTransaction' | 'updatePendingTransaction' | ||
| > => { | ||
| const isIndexerEnabled = isExperimentalFeatureEnabled('indexer') | ||
| // todo: allow undefined in indexer and return empty | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const _address = address ?? '' | ||
|
|
||
| const tokensFromLists = useTokensFromLists() | ||
|
|
||
| const { pendingTransfers, completedTransfers, isLoading, error } = | ||
| useArbitrumIndexer(isIndexerEnabled ? _address : '') | ||
|
|
||
| const indexerTransactions = useMemo(() => { | ||
| return [ | ||
| ...pendingTransfers, | ||
| ...completedTransfers | ||
| // move types to indexer | ||
| ] as never as IndexerTransfer[] | ||
| }, [pendingTransfers, completedTransfers]) | ||
|
|
||
| // todo: cache | ||
| const { data: tokenDetailsMap } = useSWRImmutable( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ERC20 data because indexer doesn't have it. |
||
| [indexerTransactions, tokensFromLists, 'indexerTokenDetails'] as const, | ||
| async ([_indexerTransactions, _tokensFromLists]) => { | ||
| const result: { [key in string]: TokenDetails } = {} | ||
|
|
||
| for (let i = 0; i < _indexerTransactions.length; i++) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not great but I did it just for PoC to work. Basically as mentioned before, indexer is not giving us ERC20 data, so we fetch ourselves. We need to do it for each ERC20 transaction we get from indexer. We may be able to do something smarter here. |
||
| const tx = _indexerTransactions[i] | ||
| const tokenAddress = tx?.tokenAddress | ||
|
|
||
| if (!tokenAddress || result[tokenAddress]) continue | ||
|
|
||
| const tokenFromLists = _tokensFromLists[tokenAddress] | ||
| if (tokenFromLists) { | ||
| const { name, symbol, decimals } = tokenFromLists | ||
| result[tokenAddress] = { name, symbol, decimals } | ||
| continue | ||
| } | ||
|
|
||
| // todo: use https://www.npmjs.com/package/p-limit to fetch token in batches | ||
| // also don't refetch the same token | ||
| const { name, symbol, decimals } = await fetchErc20Data({ | ||
| address: tokenAddress, | ||
| provider: getProviderForChainId(tx.parentChainId) | ||
| }) | ||
|
|
||
| result[tokenAddress] = { name, symbol, decimals } | ||
| } | ||
|
|
||
| return result | ||
| } | ||
| ) | ||
|
|
||
| const { data: transactions = [] } = useSWR( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Final data which includes token data for ERC20 transactions |
||
| tokenDetailsMap | ||
| ? ([indexerTransactions, tokenDetailsMap, 'indexerTransactions'] as const) | ||
| : null, | ||
| ([_indexerTransactions, _tokenDetailsMap]) => { | ||
| return _indexerTransactions.map(tx => { | ||
| if (tx.type === 'ETH') { | ||
| return transformIndexerTransfer({ tx }) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With overrides we can use the same function for ETH and ERC20. The only difference is if it's ERC20 then |
||
| } | ||
|
|
||
| const tokenDetails = _tokenDetailsMap[tx.tokenAddress] | ||
|
|
||
| if (!tokenDetails) { | ||
| throw new Error( | ||
| 'Failed to fetch token data for ERC-20 indexer transfer.' | ||
| ) | ||
| } | ||
|
|
||
| return transformIndexerTransfer({ tx, tokenDetails }) | ||
| }) | ||
| }, | ||
| { | ||
| onSuccess: data => data.sort(sortByTimestampDescending) | ||
| } | ||
| ) | ||
|
|
||
| return { | ||
| transactions, | ||
| loading: isLoading, | ||
| // TODO: This will need to be based on pagination | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we want to do any pagination for v1. We can just fetch first 1000 results like we do for CCTP, and it should be sufficient for vast majority of users for now. |
||
| completed: !isLoading, | ||
| failedChainPairs: [], | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can't really do |
||
| error, | ||
| pause: () => {}, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| resume: () => {} | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,6 +75,8 @@ import { | |
| import { create } from 'zustand' | ||
| import { useLifiMergedTransactionCacheStore } from './useLifiMergedTransactionCacheStore' | ||
| import { useDisabledFeatures } from './useDisabledFeatures' | ||
| import { useIndexerHistory } from '../components/TransactionHistory/useIndexerHistory' | ||
| import { isExperimentalFeatureEnabled } from '../util' | ||
|
|
||
| const BATCH_FETCH_BLOCKS: { [key: number]: number } = { | ||
| 33139: 5_000_000, // ApeChain | ||
|
|
@@ -146,7 +148,7 @@ function getTransactionTimestamp(tx: Transfer) { | |
| return normalizeTimestamp(tx.timestamp?.toNumber() ?? 0) | ||
| } | ||
|
|
||
| function sortByTimestampDescending(a: Transfer, b: Transfer) { | ||
| export function sortByTimestampDescending(a: Transfer, b: Transfer) { | ||
| return getTransactionTimestamp(a) > getTransactionTimestamp(b) ? -1 : 1 | ||
| } | ||
|
|
||
|
|
@@ -512,7 +514,10 @@ const useTransactionHistoryWithoutStatuses = (address: Address | undefined) => { | |
| ] | ||
| ) | ||
|
|
||
| const shouldFetch = address && !isLoadingAccountType && isTxHistoryEnabled | ||
| const isIndexerEnabled = isExperimentalFeatureEnabled('indexer') | ||
|
|
||
| const shouldFetch = | ||
| address && !isLoadingAccountType && isTxHistoryEnabled && !isIndexerEnabled | ||
|
|
||
| const { | ||
| data: depositsData, | ||
|
|
@@ -579,6 +584,8 @@ export const useTransactionHistory = ( | |
| const { isFeatureDisabled } = useDisabledFeatures() | ||
| const isTxHistoryEnabled = !isFeatureDisabled(DisabledFeatures.TX_HISTORY) | ||
|
|
||
| const indexerResult = useIndexerHistory(address) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea is to only run indexer with the feature flag enabled. We return We also use the
|
||
|
|
||
| const lifiTransactions = useLifiMergedTransactionCacheStore( | ||
| state => state.transactions | ||
| ) | ||
|
|
@@ -955,6 +962,12 @@ export const useTransactionHistory = ( | |
| setPage(prevPage => prevPage + 1) | ||
| } | ||
|
|
||
| const isIndexerEnabled = isExperimentalFeatureEnabled('indexer') | ||
|
|
||
| if (isIndexerEnabled) { | ||
| return { ...indexerResult, addPendingTransaction, updatePendingTransaction } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| if (isLoadingTxsWithoutStatus || error) { | ||
| return { | ||
| transactions: newTransactionsData || [], | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to check with @douglance what we actually need here