From a638afe924e7727eadc993e80313ad1904120197 Mon Sep 17 00:00:00 2001 From: Bartek Date: Wed, 2 Oct 2024 15:03:41 +0200 Subject: [PATCH] feat: subgraphs for ETH transfers to custom destination (#1943) --- .../api/eth-deposits-custom-destination.ts | 157 ++++++++++++++++++ .../src/util/deposits/fetchDeposits.ts | 72 ++++++-- ...DepositsToCustomDestinationFromSubgraph.ts | 93 +++++++++++ 3 files changed, 311 insertions(+), 11 deletions(-) create mode 100644 packages/arb-token-bridge-ui/src/pages/api/eth-deposits-custom-destination.ts create mode 100644 packages/arb-token-bridge-ui/src/util/deposits/fetchEthDepositsToCustomDestinationFromSubgraph.ts diff --git a/packages/arb-token-bridge-ui/src/pages/api/eth-deposits-custom-destination.ts b/packages/arb-token-bridge-ui/src/pages/api/eth-deposits-custom-destination.ts new file mode 100644 index 0000000000..b0e5b084a9 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/pages/api/eth-deposits-custom-destination.ts @@ -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 +) { + 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(' is required') + if (!sender && !receiver) + errorMessage.push(' or 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: [] + }) + } +} diff --git a/packages/arb-token-bridge-ui/src/util/deposits/fetchDeposits.ts b/packages/arb-token-bridge-ui/src/util/deposits/fetchDeposits.ts index 43e3a36735..b2ec571855 100644 --- a/packages/arb-token-bridge-ui/src/util/deposits/fetchDeposits.ts +++ b/packages/arb-token-bridge-ui/src/util/deposits/fetchDeposits.ts @@ -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 @@ -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' @@ -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)) } diff --git a/packages/arb-token-bridge-ui/src/util/deposits/fetchEthDepositsToCustomDestinationFromSubgraph.ts b/packages/arb-token-bridge-ui/src/util/deposits/fetchEthDepositsToCustomDestinationFromSubgraph.ts new file mode 100644 index 0000000000..6c1aa896fb --- /dev/null +++ b/packages/arb-token-bridge-ui/src/util/deposits/fetchEthDepositsToCustomDestinationFromSubgraph.ts @@ -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 => { + 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 +}