UNPKG

0xtrails

Version:

SDK for Trails

514 lines (459 loc) 12.2 kB
import { createPublicClient, http } from "viem" import { useQuery } from "@tanstack/react-query" import { getChainInfo } from "./chains.js" import { getSequenceProjectAccessKey, getSequenceEnv, getSequenceApiUrl, } from "./config.js" import type { GuestModuleEvent, TrailsTokenSweeperEvent } from "./decoders.js" import { arbitrum, base, baseSepolia, optimism, polygon, mainnet, apeChain, arbitrumNova, avalanche, b3, blast, gnosis, soneium, xai, bsc, etherlink, katana, } from "viem/chains" import { logger } from "./logger.js" import { bigintReplacer } from "./utils.js" import { getExplorerUrl } from "./explorer.js" export type TransactionStateStatus = | "pending" | "failed" | "confirmed" | "aborted" export type TransactionState = { transactionHash: string explorerUrl: string blockNumber?: number chainId: number state: TransactionStateStatus label: string decodedTrailsTokenSweeperEvents?: TrailsTokenSweeperEvent[] decodedGuestModuleEvents?: GuestModuleEvent[] refunded?: boolean } export type TransferType = "SEND" | "RECEIVE" export type ContractType = "ERC20" | "ERC721" | "ERC1155" | "NATIVE" export type ContractInfo = { chainId: number address: string source: string name: string type: ContractType symbol: string decimals: number logoURI: string deployed: boolean bytecodeHash: string extensions: { link: string description: string ogImage: string ogName: string originChainId: number originAddress: string verified: boolean verifiedBy: string } updatedAt: string queuedAt: string status: string } export type Transfer = { transferType: TransferType contractAddress: string contractType: ContractType from: string to: string tokenIds: string[] amounts: string[] logIndex: number contractInfo: ContractInfo } export type TransactionHistoryItemFromAPI = { txnHash: string blockNumber: number blockHash: string chainId: number metaTxnID: string | null transfers: Transfer[] timestamp: string } export type TransactionHistoryItem = TransactionHistoryItemFromAPI & { explorerUrl?: string chainName?: string } export type TransactionHistoryResponse = { page: { column: string pageSize: number more: boolean } transactions: TransactionHistoryItem[] } export type GetAccountTransactionHistoryParams = { chainId: number accountAddress: string pageSize?: number includeMetadata?: boolean page?: number } // Standalone function to calculate time difference between two transactions export async function getTxTimeDiff( firstTx?: TransactionState, lastTx?: TransactionState, ): Promise<number> { if (!firstTx?.chainId || !lastTx?.chainId || !firstTx || !lastTx) { return 0 } if ( !(firstTx.blockNumber || firstTx.transactionHash) || !(lastTx.blockNumber || lastTx.transactionHash) ) { return 0 } if ( firstTx.blockNumber === lastTx.blockNumber && firstTx.transactionHash === lastTx.transactionHash ) { return 0 } const firstChainInfo = getChainInfo(firstTx.chainId) const lastChainInfo = getChainInfo(lastTx.chainId) if (!firstChainInfo || !lastChainInfo) return 0 const firstClient = createPublicClient({ chain: firstChainInfo, transport: http(), }) const lastClient = createPublicClient({ chain: lastChainInfo, transport: http(), }) async function getBlockNumber( client: ReturnType<typeof createPublicClient>, tx: TransactionState, ) { if (tx.blockNumber) return BigInt(tx.blockNumber) const receipt = await client.getTransactionReceipt({ hash: tx.transactionHash as `0x${string}`, }) return receipt.blockNumber } async function getTimestamp( client: ReturnType<typeof createPublicClient>, blockNumber: bigint, ) { const block = await client.getBlock({ blockNumber }) return typeof block.timestamp === "bigint" ? Number(block.timestamp) : block.timestamp } try { const [firstBlockNumber, lastBlockNumber] = await Promise.all([ getBlockNumber(firstClient, firstTx), getBlockNumber(lastClient, lastTx), ]) const [firstTs, lastTs] = await Promise.all([ getTimestamp(firstClient, firstBlockNumber), getTimestamp(lastClient, lastBlockNumber), ]) const diff = lastTs - firstTs if (diff < 1) { return 1 // round up to 1 second } return diff } catch (error) { logger.console.error( "[trails-sdk] Error calculating transaction time difference:", error, ) return 0 } } export function getIndexerUrlFromChainSlug(chainSlug: string) { const env = getSequenceEnv() let envPrefix = "" if (env === "dev") { envPrefix = "dev-" } return `https://${envPrefix}${chainSlug}-indexer.sequence.app` } const chainIdToIndexerUrl = { [arbitrum.id]: getIndexerUrlFromChainSlug("arbitrum"), [base.id]: getIndexerUrlFromChainSlug("base"), [baseSepolia.id]: getIndexerUrlFromChainSlug("base-sepolia"), [optimism.id]: getIndexerUrlFromChainSlug("optimism"), [polygon.id]: getIndexerUrlFromChainSlug("polygon"), [mainnet.id]: getIndexerUrlFromChainSlug("mainnet"), [apeChain.id]: getIndexerUrlFromChainSlug("apechain"), [arbitrumNova.id]: getIndexerUrlFromChainSlug("arbitrum-nova"), [avalanche.id]: getIndexerUrlFromChainSlug("avalanche"), [b3.id]: getIndexerUrlFromChainSlug("b3"), [blast.id]: getIndexerUrlFromChainSlug("blast"), [gnosis.id]: getIndexerUrlFromChainSlug("gnosis"), [soneium.id]: getIndexerUrlFromChainSlug("soneium"), [xai.id]: getIndexerUrlFromChainSlug("xai"), [bsc.id]: getIndexerUrlFromChainSlug("bsc"), 421613: getIndexerUrlFromChainSlug("arbitrum-nova-sepolia"), [etherlink.id]: getIndexerUrlFromChainSlug("etherlink"), [katana.id]: getIndexerUrlFromChainSlug("katana"), } export async function getAccountTransactionHistory({ chainId, accountAddress, pageSize = 10, page = 1, includeMetadata = true, }: GetAccountTransactionHistoryParams): Promise<TransactionHistoryResponse> { const accessKey = getSequenceProjectAccessKey() if (!accessKey) { throw new Error("Sequence project access key is required") } // Get the chain-specific indexer URL const chainIndexerUrl = chainIdToIndexerUrl[chainId as keyof typeof chainIdToIndexerUrl] if (!chainIndexerUrl) { throw new Error(`Unsupported chain ID: ${chainId}`) } const endpoint = `${chainIndexerUrl}/rpc/Indexer/GetTransactionHistory` const requestBody = { filter: { accountAddress: accountAddress.toLowerCase(), }, includeMetadata, page: { page, pageSize, }, } try { const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", "X-Access-Key": accessKey, }, body: JSON.stringify(requestBody, bigintReplacer, 2), }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const data: TransactionHistoryResponse = await response.json() const transactions = data.transactions.map((tx) => ({ ...tx, explorerUrl: getExplorerUrl({ txHash: tx.txnHash, chainId: tx.chainId, }), chainName: getChainInfo(tx.chainId)?.name, })) return { ...data, transactions, } } catch (error) { logger.console.error( "[trails-sdk] Error fetching transaction history:", error, ) throw error } } export type IntentTransaction = { originIntentAddress: string destinationIntentAddress: string mainSigner: string metaTxnId?: string txnHash?: string executionStatus?: string originChainId?: number destinationChainId?: number originTokenAddress?: string originTokenAmount?: string destinationTokenAddress?: string destinationTokenAmount?: string destinationToAddress?: string createdAt?: string // Enriched token information originToken?: { symbol: string name: string decimals: number imageUrl: string chainId: number } destinationToken?: { symbol: string name: string decimals: number imageUrl: string chainId: number } } export type IntentTransactionHistoryResponse = { page?: { page?: number pageSize?: number more?: boolean } transactions: IntentTransaction[] } export type GetIntentTransactionHistoryParams = { accountAddress: string pageSize?: number page?: number } export async function getIntentTransactionHistory({ accountAddress, pageSize = 10, page = 1, }: GetIntentTransactionHistoryParams): Promise<IntentTransactionHistoryResponse> { const accessKey = getSequenceProjectAccessKey() if (!accessKey) { throw new Error("Sequence project access key is required") } const apiUrl = getSequenceApiUrl() const endpoint = `${apiUrl}/rpc/API/GetIntentTransactionHistory` const requestBody = { accountAddress: accountAddress.toLowerCase(), page: { page, pageSize, sort: [ { column: "created_at", order: "DESC", }, ], }, } try { const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", "X-Access-Key": accessKey, }, body: JSON.stringify(requestBody, bigintReplacer, 2), }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const data: IntentTransactionHistoryResponse = await response.json() return data } catch (error) { logger.console.error( "[trails-sdk] Error fetching intent transaction history:", error, ) throw error } } export type UseAccountTransactionHistoryParams = { chainId?: number | null accountAddress?: string | null pageSize?: number page?: number includeMetadata?: boolean } export type UseAccountTransactionHistoryReturn = { data: TransactionHistoryResponse | undefined isLoading: boolean error: Error | null } export function useAccountTransactionHistory({ chainId, accountAddress, pageSize = 10, page = 1, includeMetadata = true, }: UseAccountTransactionHistoryParams): UseAccountTransactionHistoryReturn { const { data, isLoading, error } = useQuery({ queryKey: [ "accountTransactionHistory", chainId, accountAddress, pageSize, page, includeMetadata, ], queryFn: async () => { if (!chainId || !accountAddress) { return undefined } return await getAccountTransactionHistory({ chainId, accountAddress, pageSize, page, includeMetadata, }) }, enabled: Boolean(chainId && accountAddress), staleTime: 30 * 1000, // Consider data fresh for 30 seconds refetchOnWindowFocus: false, refetchOnMount: false, refetchInterval: false, retry: 2, refetchOnReconnect: true, }) return { data, isLoading, error: error as Error | null, } } export type UseIntentTransactionHistoryParams = { accountAddress?: string | null pageSize?: number page?: number } export type UseIntentTransactionHistoryReturn = { data: IntentTransactionHistoryResponse | undefined isLoading: boolean error: Error | null } export function useIntentTransactionHistory({ accountAddress, pageSize = 10, page = 1, }: UseIntentTransactionHistoryParams): UseIntentTransactionHistoryReturn { const { data, isLoading, error } = useQuery({ queryKey: ["intentTransactionHistory", accountAddress, pageSize, page], queryFn: async () => { if (!accountAddress) { return undefined } return await getIntentTransactionHistory({ accountAddress, pageSize, page, }) }, enabled: Boolean(accountAddress), staleTime: 30 * 1000, // Consider data fresh for 30 seconds refetchOnWindowFocus: false, refetchOnMount: false, refetchInterval: false, retry: 2, refetchOnReconnect: true, }) return { data, isLoading, error: error as Error | null, } }