Skip to content
Draft
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
Binary file removed packages/app/public/images/T-REX_Logo.png
Binary file not shown.
Binary file removed packages/app/public/images/T-Rex_Logo.png
Binary file not shown.
4 changes: 3 additions & 1 deletion packages/arb-token-bridge-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
"@heroicons/react": "^2.2.0",
"@lifi/sdk": "^3.7.0",
"@offchainlabs/cobalt": "^0.4.0",
"@ponder/client": "^0.11.19",
"@ponder/react": "^0.11.19",
"@rainbow-me/rainbowkit": "^2.2.4",
"@rehooks/local-storage": "^2.4.5",
"@sentry/react": "^9.13.0",
"@tanstack/react-query": "^5.63.0",
"@tanstack/react-query": "^5.85.9",
"@tippyjs/react": "^4.2.6",
"@uidotdev/usehooks": "^2.4.1",
"@uniswap/token-lists": "^1.0.0-beta.34",
Expand Down
39 changes: 26 additions & 13 deletions packages/arb-token-bridge-ui/src/components/App/AppProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode, useMemo } from 'react'
import { useMemo } from 'react'
import { Provider as OvermindProvider } from 'overmind-react'
import { WagmiProvider } from 'wagmi'
import { darkTheme, RainbowKitProvider, Theme } from '@rainbow-me/rainbowkit'
Expand All @@ -13,6 +13,10 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createConfig } from '@lifi/sdk'
import { INTEGRATOR_ID } from '@/bridge/app/api/crosschain-transfers/lifi'

import { ArbitrumIndexerProvider } from '@arbitrum/indexer-provider'
import { PonderProvider } from '@ponder/react'
import { client } from './ponder'

const rainbowkitTheme = merge(darkTheme(), {
colors: {
accentColor: 'var(--blue-link)'
Expand Down Expand Up @@ -45,7 +49,7 @@ Object.keys(localStorage).forEach(key => {
})

interface AppProvidersProps {
children: ReactNode
children: React.JSX.Element
}

const queryClient = new QueryClient()
Expand All @@ -55,16 +59,25 @@ export function AppProviders({ children }: AppProvidersProps) {
const overmind = useMemo(() => createOvermind(config), [])

return (
<OvermindProvider value={overmind}>
<ArbQueryParamProvider>
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider theme={rainbowkitTheme}>
<AppContextProvider>{children}</AppContextProvider>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
</ArbQueryParamProvider>
</OvermindProvider>
<PonderProvider client={client}>
<QueryClientProvider client={queryClient}>
<OvermindProvider value={overmind}>
<ArbQueryParamProvider>
<WagmiProvider config={wagmiConfig}>
<RainbowKitProvider theme={rainbowkitTheme}>
<AppContextProvider>
<ArbitrumIndexerProvider
ponderClient={client}
queryClient={queryClient}
>
{children}
</ArbitrumIndexerProvider>
</AppContextProvider>
</RainbowKitProvider>
</WagmiProvider>
</ArbQueryParamProvider>
</OvermindProvider>
</QueryClientProvider>
</PonderProvider>
)
}
30 changes: 30 additions & 0 deletions packages/arb-token-bridge-ui/src/components/App/ponder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createClient } from '@ponder/client'
Copy link
Contributor Author

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


// 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
Expand Up @@ -199,7 +199,7 @@ export const TransactionHistoryTable = (
return (
<EmptyTransactionHistory
loading={loading}
isError={typeof error !== 'undefined'}
isError={!!error}
paused={paused}
resume={resume}
tabType={isPendingTab ? 'pending' : 'settled'}
Expand Down
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 & {
Copy link
Contributor Author

@brtkx brtkx Sep 2, 2025

Choose a reason for hiding this comment

The 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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took from indexer some time ago but these may have changed

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useArbitrumIndexer doesn't allow undefined address so we need to pass empty string. Would be nice if indexer let us pass undefined, but it's just nice to have

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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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++) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 })
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 tokenDetails is required, otherwise it needs to be skipped for ETH.

}

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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: [],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can't really do failedChainPairs with indexer

error,
pause: () => {},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pause and resume N/A for indexer

resume: () => {}
}
}
17 changes: 15 additions & 2 deletions packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -579,6 +584,8 @@ export const useTransactionHistory = (
const { isFeatureDisabled } = useDisabledFeatures()
const isTxHistoryEnabled = !isFeatureDisabled(DisabledFeatures.TX_HISTORY)

const indexerResult = useIndexerHistory(address)
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 indexerResult if indexer flag is true, otherwise legacy results.

We also use the indexer flag when fetching indexer and legacy results so that:

  • we only fetch legacy if indexer === false || indexer === undefined
  • we only fetch indexer if indexer === true


const lifiTransactions = useLifiMergedTransactionCacheStore(
state => state.transactions
)
Expand Down Expand Up @@ -955,6 +962,12 @@ export const useTransactionHistory = (
setPage(prevPage => prevPage + 1)
}

const isIndexerEnabled = isExperimentalFeatureEnabled('indexer')

if (isIndexerEnabled) {
return { ...indexerResult, addPendingTransaction, updatePendingTransaction }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addPendingTransaction and updatePendingTransaction stays as is because it's a different useSWR key with it's own mutator

}

if (isLoadingTxsWithoutStatus || error) {
return {
transactions: newTransactionsData || [],
Expand Down
2 changes: 1 addition & 1 deletion packages/arb-token-bridge-ui/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const getAPIBaseUrl = () => {
}

// add feature flags to the array
const featureFlags = [] as const
const featureFlags = ['indexer'] as const

type FeatureFlag = (typeof featureFlags)[number]

Expand Down
Loading