Skip to content

Commit

Permalink
feat: fetch teleport transactions from subgraphs (#1677)
Browse files Browse the repository at this point in the history
  • Loading branch information
dewanshparashar authored Jun 12, 2024
1 parent 4734c15 commit 05e2b2e
Show file tree
Hide file tree
Showing 14 changed files with 939 additions and 17 deletions.
28 changes: 28 additions & 0 deletions packages/arb-token-bridge-ui/src/api-utils/ServerSubgraphUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ const subgraphs = {
'l2-arbitrum-sepolia': {
theGraphNetworkSubgraphId: 'AaUuKWWuQbCXbvRkXpVDEpw9B7oVicYrovNyMLPZtLPw',
theGraphHostedServiceSubgraphName: 'fionnachan/layer2-token-gateway-sepolia'
},
// Teleport Sepolia
'teleporter-sepolia': {
theGraphNetworkSubgraphId: '6AwhH4JF8Ss5ZFf12azD13D1nNhuNzLnjH56irYqA7fD',
theGraphHostedServiceSubgraphName: '' // we don't have a hosted service subgraph for teleports
},
'teleporter-ethereum': {
theGraphNetworkSubgraphId: 'GEVHWg3FLKvWivMhqkeVrQVt4WCN6cWnsvdf6MpNrHpg',
theGraphHostedServiceSubgraphName: '' // we don't have a hosted service subgraph for teleports
}
} as const

Expand Down Expand Up @@ -148,6 +157,25 @@ export function getCctpSubgraphClient(chainId: number) {
}
}

export function getTeleporterSubgraphClient(chainId: number) {
switch (chainId) {
case ChainId.Ethereum:
return createTheGraphNetworkClient(
subgraphs['teleporter-ethereum'].theGraphNetworkSubgraphId
)

case ChainId.Sepolia:
return createTheGraphNetworkClient(
subgraphs['teleporter-sepolia'].theGraphNetworkSubgraphId
)

default:
throw new Error(
`[getTeleporterSubgraphClient] unsupported chain: ${chainId}`
)
}
}

export function getL1SubgraphClient(l2ChainId: number) {
switch (l2ChainId) {
case ChainId.ArbitrumOne:
Expand Down
71 changes: 58 additions & 13 deletions packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ import {
} from '../util/SubgraphUtils'
import { isTeleport } from '@/token-bridge-sdk/teleport'
import { Address } from '../util/AddressUtils'
import {
TeleportFromSubgraph,
fetchTeleports
} from '../util/teleports/fetchTeleports'
import {
isTransferTeleportFromSubgraph,
transformTeleportFromSubgraph
} from '../util/teleports/helpers'

export type UseTransactionHistoryResult = {
transactions: MergedTransaction[]
Expand All @@ -77,13 +85,20 @@ export type Withdrawal =
| EthWithdrawal

type DepositOrWithdrawal = Deposit | Withdrawal
export type Transfer = DepositOrWithdrawal | MergedTransaction
export type Transfer =
| DepositOrWithdrawal
| MergedTransaction
| TeleportFromSubgraph

function getStandardizedTimestampByTx(tx: Transfer) {
if (isCctpTransfer(tx)) {
return (tx.createdAt ?? 0) / 1_000
}

if (isTransferTeleportFromSubgraph(tx)) {
return tx.timestamp
}

if (isDeposit(tx)) {
return tx.timestampCreated ?? 0
}
Expand Down Expand Up @@ -133,6 +148,11 @@ function isDeposit(tx: DepositOrWithdrawal): tx is Deposit {
}

async function transformTransaction(tx: Transfer): Promise<MergedTransaction> {
// teleport-from-subgraph doesn't have a child-chain-id, we detect it later, hence, an early return
if (isTransferTeleportFromSubgraph(tx)) {
return await transformTeleportFromSubgraph(tx)
}

const parentChainProvider = getProvider(tx.parentChainId)
const childChainProvider = getProvider(tx.childChainId)

Expand Down Expand Up @@ -185,6 +205,10 @@ async function transformTransaction(tx: Transfer): Promise<MergedTransaction> {
}

function getTxIdFromTransaction(tx: Transfer) {
if (isTransferTeleportFromSubgraph(tx)) {
return tx.transactionHash
}

if (isCctpTransfer(tx)) {
return tx.txId
}
Expand All @@ -200,6 +224,23 @@ function getTxIdFromTransaction(tx: Transfer) {
return tx.l2TxHash ?? tx.transactionHash
}

function getCacheKeyFromTransaction(
tx: Transaction | MergedTransaction | TeleportFromSubgraph | Withdrawal
) {
const txId = getTxIdFromTransaction(tx)
if (!txId) {
return undefined
}
return `${tx.parentChainId}-${txId.toLowerCase()}`
}

// remove the duplicates from the transactions passed
function dedupeTransactions(txs: Transfer[]) {
return Array.from(
new Map(txs.map(tx => [getCacheKeyFromTransaction(tx), tx])).values()
)
}

/**
* Fetches transaction history only for deposits and withdrawals, without their statuses.
*/
Expand Down Expand Up @@ -334,16 +375,27 @@ const useTransactionHistoryWithoutStatuses = (address: Address | undefined) => {
isConnectedToParentChain
})
try {
// early check for fetching teleport
if (
isTeleport({
sourceChainId: chainPair.parentChainId,
destinationChainId: chainPair.childChainId
})
) {
// Teleport fetcher will go here.
return []
// teleporter does not support withdrawals
if (type === 'withdrawals') return []

return await fetchTeleports({
sender: includeSentTxs ? address : undefined,
receiver: includeReceivedTxs ? address : undefined,
parentChainProvider: getProvider(chainPair.parentChainId),
childChainProvider: getProvider(chainPair.childChainId),
pageNumber: 0,
pageSize: 1000
})
}

// else, fetch deposits or withdrawals
return await fetcherFn({
sender: includeSentTxs ? address : undefined,
receiver: includeReceivedTxs ? address : undefined,
Expand Down Expand Up @@ -515,16 +567,9 @@ export const useTransactionHistory = (

// duplicates may occur when txs are taken from the local storage
// we don't use Set because it wouldn't dedupe objects with different reference (we fetch them from different sources)
const dedupedTransactions = Array.from(
new Map(
dataWithCache.map(tx => [
`${tx.parentChainId}-${tx.childChainId}-${getTxIdFromTransaction(
tx
)?.toLowerCase()}}`,
tx
])
).values()
).sort(sortByTimestampDescending)
const dedupedTransactions = dedupeTransactions(dataWithCache).sort(
sortByTimestampDescending
)

const startIndex = _page * MAX_BATCH_SIZE
const endIndex = startIndex + MAX_BATCH_SIZE
Expand Down
112 changes: 112 additions & 0 deletions packages/arb-token-bridge-ui/src/pages/api/teleports/erc20.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { gql } from '@apollo/client'

import {
getSourceFromSubgraphClient,
getTeleporterSubgraphClient
} from '../../../api-utils/ServerSubgraphUtils'
import { FetchErc20TeleportsFromSubgraphResult } from '../../../util/teleports/fetchErc20TeleportsFromSubgraph'

// Extending the standard NextJs request with Deposit-params
type NextApiRequestWithErc20TeleportParams = NextApiRequest & {
query: {
sender?: string
l1ChainId: string
page?: string
pageSize?: string
}
}

type Erc20TeleportResponse = {
meta?: { source: string | null }
data: FetchErc20TeleportsFromSubgraphResult[]
message?: string // in case of any error
}

export default async function handler(
req: NextApiRequestWithErc20TeleportParams,
res: NextApiResponse<Erc20TeleportResponse>
) {
try {
const { sender, l1ChainId, page = '0', pageSize = '10' } = req.query

// validate method
if (req.method !== 'GET') {
res
.status(400)
.send({ message: `invalid_method: ${req.method}`, data: [] })
return
}

// validate the request parameters
const errorMessage = []
if (!l1ChainId) errorMessage.push('<l1ChainId> is required')
if (!sender) errorMessage.push('<sender> is required')

if (errorMessage.length) {
res.status(400).json({
message: `incomplete request: ${errorMessage.join(', ')}`,
data: []
})
return
}

// if invalid pageSize, send empty data instead of error
if (isNaN(Number(pageSize)) || Number(pageSize) === 0) {
res.status(200).json({
data: []
})
return
}

let subgraphClient
try {
subgraphClient = getTeleporterSubgraphClient(Number(l1ChainId))
} catch (error: any) {
// catch attempt to query unsupported networks and throw a 400
res.status(400).json({
message: error?.message ?? 'Something went wrong',
data: []
})
return
}

const subgraphResult = await subgraphClient.query({
query: gql(`{
teleporteds(
where: {
sender: "${sender}"
}
first: ${Number(pageSize)}
skip: ${Number(page) * Number(pageSize)}
orderBy: timestamp
orderDirection: desc
) {
id
sender
l1Token
l3FeeTokenL1Addr
l1l2Router
l2l3RouterOrInbox
to
amount
transactionHash
timestamp
}
}`)
})

const transactions: FetchErc20TeleportsFromSubgraphResult[] =
subgraphResult.data.teleporteds

res.status(200).json({
meta: { source: getSourceFromSubgraphClient(subgraphClient) },
data: transactions
})
} catch (error: any) {
res.status(500).json({
message: error?.message ?? 'Something went wrong',
data: []
})
}
}
Loading

0 comments on commit 05e2b2e

Please sign in to comment.