import { ApiResponse } from './client' import { fetchBlockscoutJson, normalizeTransaction, type BlockscoutInternalTransaction } from './blockscout' import { resolveExplorerApiBase } from '../../../libs/frontend-api-client/api-base' export interface Transaction { chain_id: number hash: string block_number: number block_hash: string transaction_index: number from_address: string to_address?: string value: string gas_price?: number max_fee_per_gas?: number max_priority_fee_per_gas?: number gas_limit: number gas_used?: number status?: number input_data?: string contract_address?: string created_at: string fee?: string method?: string revert_reason?: string transaction_tag?: string decoded_input?: { method_call?: string method_id?: string parameters: Array<{ name?: string type?: string value?: unknown }> } token_transfers?: TransactionTokenTransfer[] } export interface TransactionTokenTransfer { block_number?: number from_address: string from_label?: string to_address: string to_label?: string token_address: string token_name?: string token_symbol?: string token_decimals: number amount: string type?: string timestamp?: string } export interface TransactionInternalCall { from_address: string from_label?: string to_address?: string to_label?: string contract_address?: string contract_label?: string type?: string value: string success?: boolean error?: string result?: string timestamp?: string } export interface TransactionLookupDiagnostic { checked_hash: string chain_id: number explorer_indexed: boolean rpc_transaction_found: boolean rpc_receipt_found: boolean latest_block_number?: number rpc_url?: string } const CHAIN_138_PUBLIC_RPC_URL = 'https://rpc-http-pub.d-bis.org' function resolvePublicRpcUrl(chainId: number): string | null { if (chainId !== 138) { return null } const envValue = (process.env.NEXT_PUBLIC_CHAIN_138_RPC_URL || '').trim() return envValue || CHAIN_138_PUBLIC_RPC_URL } async function fetchJsonWithStatus(input: RequestInfo | URL, init?: RequestInit): Promise<{ ok: boolean; status: number; data: T | null }> { const response = await fetch(input, init) let data: T | null = null try { data = (await response.json()) as T } catch { data = null } return { ok: response.ok, status: response.status, data } } async function fetchRpcResult(rpcUrl: string, method: string, params: unknown[]): Promise { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 6000) try { const response = await fetch(rpcUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params, }), signal: controller.signal, }) if (!response.ok) { return null } const payload = (await response.json()) as { result?: T | null } return payload.result ?? null } catch { return null } finally { clearTimeout(timeout) } } async function diagnoseMissingTransaction(chainId: number, hash: string): Promise { const diagnostic: TransactionLookupDiagnostic = { checked_hash: hash, chain_id: chainId, explorer_indexed: false, rpc_transaction_found: false, rpc_receipt_found: false, } const explorerLookup = await fetchJsonWithStatus(`${resolveExplorerApiBase()}/api/v2/transactions/${hash}`) diagnostic.explorer_indexed = explorerLookup.ok const rpcUrl = resolvePublicRpcUrl(chainId) if (!rpcUrl) { return diagnostic } diagnostic.rpc_url = rpcUrl const [transactionResult, receiptResult, latestBlockHex] = await Promise.all([ fetchRpcResult>(rpcUrl, 'eth_getTransactionByHash', [hash]), fetchRpcResult>(rpcUrl, 'eth_getTransactionReceipt', [hash]), fetchRpcResult(rpcUrl, 'eth_blockNumber', []), ]) diagnostic.rpc_transaction_found = transactionResult != null diagnostic.rpc_receipt_found = receiptResult != null if (typeof latestBlockHex === 'string' && latestBlockHex.startsWith('0x')) { diagnostic.latest_block_number = parseInt(latestBlockHex, 16) } return diagnostic } function normalizeInternalTransactions(items: BlockscoutInternalTransaction[] | null | undefined): TransactionInternalCall[] { if (!Array.isArray(items)) { return [] } return items.map((item) => ({ from_address: item.from?.hash || '', from_label: item.from?.name || item.from?.label || undefined, to_address: item.to?.hash || undefined, to_label: item.to?.name || item.to?.label || undefined, contract_address: item.created_contract?.hash || undefined, contract_label: item.created_contract?.name || item.created_contract?.label || undefined, type: item.type || undefined, value: item.value || '0', success: item.success ?? undefined, error: item.error || undefined, result: item.result || undefined, timestamp: item.timestamp || undefined, })) } export const transactionsApi = { get: async (chainId: number, hash: string): Promise> => { const raw = await fetchBlockscoutJson(`/api/v2/transactions/${hash}`) return { data: normalizeTransaction(raw as never, chainId) } }, /** Use when you need to check response.ok before setting state (avoids treating 4xx/5xx body as data). */ getSafe: async (chainId: number, hash: string): Promise<{ ok: boolean; data: Transaction | null }> => { try { const { data } = await transactionsApi.get(chainId, hash) return { ok: true, data } } catch { return { ok: false, data: null } } }, diagnoseMissing: async (chainId: number, hash: string): Promise => { return diagnoseMissingTransaction(chainId, hash) }, getInternalTransactionsSafe: async (hash: string): Promise<{ ok: boolean; data: TransactionInternalCall[] }> => { try { const raw = await fetchBlockscoutJson<{ items?: BlockscoutInternalTransaction[] } | BlockscoutInternalTransaction[]>( `/api/v2/transactions/${hash}/internal-transactions` ) const items = Array.isArray(raw) ? raw : raw.items return { ok: true, data: normalizeInternalTransactions(items) } } catch { return { ok: false, data: [] } } }, list: async (chainId: number, page: number, pageSize: number): Promise> => { const params = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), }) const raw = await fetchBlockscoutJson<{ items?: unknown[] }>(`/api/v2/transactions?${params.toString()}`) const data = Array.isArray(raw?.items) ? raw.items.map((item) => normalizeTransaction(item as never, chainId)) : [] return { data } }, /** Use when you need to check ok before setting state (avoids treating error body as list). */ listSafe: async (chainId: number, page: number, pageSize: number): Promise<{ ok: boolean; data: Transaction[] }> => { try { const { data } = await transactionsApi.list(chainId, page, pageSize) return { ok: true, data } } catch { return { ok: false, data: [] } } }, }