/** Blockscout ERC-20 transfer parsing (shared by aggregator + indexer). */ export interface TokenTransferSummary { token: string; tokenSymbol: string; tokenDecimals: number; from: string; to: string; value: bigint; logIndex: number; } interface TokenTransferItem { from?: { hash?: string }; to?: { hash?: string }; token?: { address?: string; symbol?: string; decimals?: string }; total?: { value?: string; decimals?: string }; value?: string | null; type?: string; log_index?: number; } function parseItem(item: TokenTransferItem): TokenTransferSummary | null { const token = item.token?.address; const raw = item.total?.value ?? (item.value != null && item.value !== '' ? item.value : undefined); if (!token || raw === undefined) return null; const value = BigInt(raw); if (value === 0n) return null; const decimals = parseInt( item.token?.decimals || item.total?.decimals || '18', 10 ); return { token, tokenSymbol: item.token?.symbol || '', tokenDecimals: Number.isFinite(decimals) ? decimals : 18, from: item.from?.hash || '', to: item.to?.hash || '', value, logIndex: item.log_index ?? 0, }; } /** Largest ERC-20 transfer in a tx (legacy primary payment). */ export function pickPrimaryTokenTransfer(items: TokenTransferItem[]): TokenTransferSummary | null { let best: TokenTransferSummary | null = null; for (const item of items) { const summary = parseItem(item); if (!summary) continue; if (!best || summary.value > best.value) best = summary; } return best; } /** All non-zero ERC-20 transfers in a tx. */ export function parseAllTokenTransfers(items: TokenTransferItem[]): TokenTransferSummary[] { const out: TokenTransferSummary[] = []; for (const item of items) { const summary = parseItem(item); if (summary) out.push(summary); } return out.sort((a, b) => (a.logIndex < b.logIndex ? -1 : a.logIndex > b.logIndex ? 1 : 0)); } export async function fetchTokenTransferItemsForTx( apiBase: string, txHash: string ): Promise { const base = apiBase.replace(/\/$/, ''); const res = await fetch(`${base}/transactions/${txHash}/token-transfers`); if (!res.ok) return []; const body = (await res.json()) as { items?: TokenTransferItem[] }; return body.items ?? []; } export function pickPrimaryFromSummaries( items: TokenTransferSummary[] ): TokenTransferSummary | null { let best: TokenTransferSummary | null = null; for (const s of items) { if (!best || s.value > best.value) best = s; } return best; } export function applyPrimaryTransferToLeaf( leaf: Record, primary: TokenTransferSummary | null ): void { if (!primary) return; leaf.token = primary.token; leaf.tokenSymbol = primary.tokenSymbol; leaf.tokenDecimals = primary.tokenDecimals; leaf.tokenValue = primary.value.toString(); leaf.tokenLogIndex = primary.logIndex; if (leaf.nativeValueWei == null) leaf.nativeValueWei = String(leaf.value ?? '0'); } export async function fetchAllTokenTransfersForTx( apiBase: string, txHash: string ): Promise { const items = await fetchTokenTransferItemsForTx(apiBase, txHash); return parseAllTokenTransfers(items); } export async function fetchPrimaryTokenTransferForTx( apiBase: string, txHash: string ): Promise { const items = await fetchTokenTransferItemsForTx(apiBase, txHash); return pickPrimaryTokenTransfer(items); }