Skip to content

Commit

Permalink
feat: subgraphs for ETH transfers to custom destination (#1943)
Browse files Browse the repository at this point in the history
  • Loading branch information
brtkx authored Oct 2, 2024
1 parent 5d7349d commit a638afe
Show file tree
Hide file tree
Showing 3 changed files with 311 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { gql } from '@apollo/client'

import {
getL1SubgraphClient,
getSourceFromSubgraphClient
} from '../../api-utils/ServerSubgraphUtils'
import { FetchEthDepositsToCustomDestinationFromSubgraphResult } from '../../util/deposits/fetchEthDepositsToCustomDestinationFromSubgraph'

type NextApiRequestWithDepositParams = NextApiRequest & {
query: {
sender?: string
receiver?: string
l2ChainId: string
search?: string
page?: string
pageSize?: string
fromBlock?: string
toBlock?: string
}
}

type RetryableFromSubgraph = {
destAddr: string
sender: string
timestamp: string
transactionHash: string
id: string
l2Callvalue: string
blockCreatedAt: string
}

type EthDepositsToCustomDestinationResponse = {
meta?: { source: string | null }
data: FetchEthDepositsToCustomDestinationFromSubgraphResult[]
message?: string
}

export default async function handler(
req: NextApiRequestWithDepositParams,
res: NextApiResponse<EthDepositsToCustomDestinationResponse>
) {
try {
const {
sender,
receiver,
search = '',
l2ChainId,
page = '0',
pageSize = '10',
fromBlock,
toBlock
} = req.query

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

const errorMessage = []
if (!l2ChainId) errorMessage.push('<l2ChainId> is required')
if (!sender && !receiver)
errorMessage.push('<sender> or <receiver> 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
}

const additionalFilters = `${
typeof fromBlock !== 'undefined'
? `blockCreatedAt_gte: ${Number(fromBlock)},`
: ''
}
${
typeof toBlock !== 'undefined'
? `blockCreatedAt_lte: ${Number(toBlock)},`
: ''
}
${search ? `transactionHash_contains: "${search}",` : ''}
l2Callvalue_gt: 0
l2Calldata: "0x"
`

const subgraphClient = getL1SubgraphClient(Number(l2ChainId))

const subgraphResult = await subgraphClient.query({
query: gql(`{
retryables(
where: {
or: [
${sender ? `{ sender: "${sender}", ${additionalFilters} },` : ''}
${
receiver
? `{ destAddr: "${receiver}", ${additionalFilters} },`
: ''
}
]
}
orderBy: blockCreatedAt
orderDirection: desc
first: ${Number(pageSize)},
skip: ${Number(page) * Number(pageSize)}
) {
destAddr
sender
timestamp
transactionHash
id
l2Callvalue
blockCreatedAt
}
}`)
})

const retryablesFromSubgraph: RetryableFromSubgraph[] =
subgraphResult.data.retryables

const transactions: FetchEthDepositsToCustomDestinationFromSubgraphResult[] =
retryablesFromSubgraph.map(retryable => {
return {
receiver: retryable.destAddr,
sender: retryable.sender,
timestamp: retryable.timestamp,
transactionHash: retryable.transactionHash,
type: 'EthDeposit',
isClassic: false,
id: retryable.id,
ethValue: retryable.l2Callvalue,
blockCreatedAt: retryable.blockCreatedAt
}
})

res.status(200).json({
meta: { source: getSourceFromSubgraphClient(subgraphClient) },
data: transactions
})
} catch (error: any) {
res.status(500).json({
message: error?.message ?? 'Something went wrong',
data: []
})
}
}
72 changes: 61 additions & 11 deletions packages/arb-token-bridge-ui/src/util/deposits/fetchDeposits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { AssetType } from '../../hooks/arbTokenBridge.types'
import { Transaction } from '../../hooks/useTransactions'
import { defaultErc20Decimals } from '../../defaults'
import { fetchNativeCurrency } from '../../hooks/useNativeCurrency'
import {
fetchEthDepositsToCustomDestinationFromSubgraph,
FetchEthDepositsToCustomDestinationFromSubgraphResult
} from './fetchEthDepositsToCustomDestinationFromSubgraph'

export type FetchDepositParams = {
sender?: string
Expand Down Expand Up @@ -50,21 +54,36 @@ export const fetchDeposits = async ({
}

let depositsFromSubgraph: FetchDepositsFromSubgraphResult[] = []
let ethDepositsToCustomDestinationFromSubgraph: FetchEthDepositsToCustomDestinationFromSubgraphResult[] =
[]

const subgraphParams = {
sender,
receiver,
fromBlock,
toBlock,
l2ChainId,
pageSize,
pageNumber,
searchString
}

try {
depositsFromSubgraph = await fetchDepositsFromSubgraph({
sender,
receiver,
fromBlock,
toBlock,
l2ChainId,
pageSize,
pageNumber,
searchString
})
depositsFromSubgraph = await fetchDepositsFromSubgraph(subgraphParams)
} catch (error: any) {
console.log('Error fetching deposits from subgraph', error)
}

try {
ethDepositsToCustomDestinationFromSubgraph =
await fetchEthDepositsToCustomDestinationFromSubgraph(subgraphParams)
} catch (error: any) {
console.log(
'Error fetching native token deposits to custom destination from subgraph',
error
)
}

const mappedDepositsFromSubgraph: Transaction[] = depositsFromSubgraph.map(
(tx: FetchDepositsFromSubgraphResult) => {
const isEthDeposit = tx.type === 'EthDeposit'
Expand Down Expand Up @@ -115,5 +134,36 @@ export const fetchDeposits = async ({
}
)

return mappedDepositsFromSubgraph
const mappedEthDepositsToCustomDestinationFromSubgraph: Transaction[] =
ethDepositsToCustomDestinationFromSubgraph.map(
(tx: FetchEthDepositsToCustomDestinationFromSubgraphResult) => {
return {
type: 'deposit-l1',
status: 'pending',
direction: 'deposit',
source: 'subgraph',
value: utils.formatUnits(tx.ethValue, nativeCurrency.decimals),
txID: tx.transactionHash,
sender: tx.sender,
destination: tx.receiver,

assetName: nativeCurrency.symbol,
assetType: AssetType.ETH,

l1NetworkID: String(l1ChainId),
l2NetworkID: String(l2ChainId),
blockNumber: Number(tx.blockCreatedAt),
timestampCreated: tx.timestamp,
isClassic: false,

childChainId: l2ChainId,
parentChainId: l1ChainId
}
}
)

return [
...mappedDepositsFromSubgraph,
...mappedEthDepositsToCustomDestinationFromSubgraph
].sort((a, b) => Number(b.timestampCreated) - Number(a.timestampCreated))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { hasL1Subgraph } from '../SubgraphUtils'
import {
getAPIBaseUrl,
isExperimentalFeatureEnabled,
sanitizeQueryParams
} from '../index'

export type FetchEthDepositsToCustomDestinationFromSubgraphResult = {
receiver: string
sender: string
timestamp: string
transactionHash: string
type: 'EthDeposit'
isClassic: false
id: string
ethValue: string
blockCreatedAt: string
}

/**
* Fetches initiated retryable deposits (ETH transfers to custom destination) from subgraph in range of [fromBlock, toBlock] and pageParams.
*
* @param query Query params
* @param query.sender Address that initiated the deposit
* @param query.receiver Address that received the funds
* @param query.fromBlock Start at this block number (including)
* @param query.toBlock Stop at this block number (including)
* @param query.l2ChainId Chain id for the L2 network
* @param query.pageSize Fetch these many records from subgraph
* @param query.pageNumber Fetch records starting [pageNumber * pageSize] records
* @param query.searchString Searches records through the l1TxHash
*/

export const fetchEthDepositsToCustomDestinationFromSubgraph = async ({
sender,
receiver,
fromBlock,
toBlock,
l2ChainId,
pageSize = 10,
pageNumber = 0,
searchString = ''
}: {
sender?: string
receiver?: string
fromBlock: number
toBlock?: number
l2ChainId: number
pageSize?: number
pageNumber?: number
searchString?: string
}): Promise<FetchEthDepositsToCustomDestinationFromSubgraphResult[]> => {
if (!isExperimentalFeatureEnabled('eth-custom-dest')) {
return []
}

if (toBlock && fromBlock >= toBlock) {
// if fromBlock > toBlock or both are equal / 0
return []
}

const urlParams = new URLSearchParams(
sanitizeQueryParams({
sender,
receiver,
fromBlock,
toBlock,
l2ChainId,
pageSize,
page: pageNumber,
search: searchString
})
)

if (!hasL1Subgraph(Number(l2ChainId))) {
throw new Error(`L1 subgraph not available for network: ${l2ChainId}`)
}

if (pageSize === 0) return [] // don't query subgraph if nothing requested

const response = await fetch(
`${getAPIBaseUrl()}/api/eth-deposits-custom-destination?${urlParams}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' }
}
)

const transactions: FetchEthDepositsToCustomDestinationFromSubgraphResult[] =
(await response.json()).data

return transactions
}

0 comments on commit a638afe

Please sign in to comment.