UNPKG

@ledgerhq/coin-tron

Version:
110 lines (97 loc) 4.39 kB
import { Operation, Page } from "@ledgerhq/coin-module-framework/api/index"; import { promiseAllBatched } from "@ledgerhq/live-promise"; import uniqBy from "lodash/uniqBy"; import { fetchTronAccountTxsPage, getBlock } from "../network"; import { fromTrongridTxInfoToOperation } from "../network/trongrid/trongrid-adapters"; import { Block } from "../network/types"; import { compareTxsByTimestamp, dropTxsAfterNextCursor, dropTxsBeforeCursor, parseCursor, } from "./cursor"; import { TronEmptyPage } from "../types/errors"; // Pagination uses a SUI-style cursor approach to handle two separate endpoints // (native transactions and TRC20 transactions) while maintaining chronological order. // // The cursor format is "{timestamp}:{txHash}" which identifies the last transaction // returned. On the next page, we fetch from both endpoints starting at that timestamp, // then filter out transactions at or before the cursor position. // // To ensure no transactions are skipped when endpoints have different page boundaries, // we find the "boundary transaction" - the earliest (for asc) or latest (for desc) of // the last transactions from each endpoint - and only return transactions up to that // boundary. The next cursor points to this boundary transaction. export type ListOperationsOptions = { limit: number; minTimestamp: number; order: "asc" | "desc"; cursor?: string; }; export async function listOperations( address: string, options: ListOperationsOptions, ): Promise<Page<Operation>> { const { limit, order, cursor } = options; const parsedCursor = parseCursor(cursor); // For asc: cursor timestamp is the new lower bound (we're moving forward in time) // For desc: cursor timestamp is the new upper bound (we're moving backward in time) // minTimestamp remains the lower bound (the stopping point) let fetchMinTimestamp: number; let fetchMaxTimestamp: number | undefined; if (order === "asc") { fetchMinTimestamp = parsedCursor ? parsedCursor.timestamp : options.minTimestamp; fetchMaxTimestamp = undefined; } else { fetchMinTimestamp = options.minTimestamp; fetchMaxTimestamp = parsedCursor?.timestamp; } // Fetch native and TRC20 transactions in parallel from TronGrid. // Both endpoints are queried with the same timestamp bounds to ensure // we can properly merge and sort them chronologically. const { nativeTxs, trc20Txs } = await fetchTronAccountTxsPage(address, { limit, minTimestamp: fetchMinTimestamp, maxTimestamp: fetchMaxTimestamp, order, }); // TronGrid occasionally returns an empty page for a valid cursor (transient API failure). // A cursor is only issued when TronGrid indicated hasNextPage=true, so 0 results here // is never a legitimate end-of-stream — throw so the client can retry with the same cursor. if (parsedCursor && nativeTxs.txs.length === 0 && trc20Txs.txs.length === 0) { throw new TronEmptyPage( `TronGrid returned empty page for cursor ${cursor} — transient failure, retry required`, ); } // Merge and dedupe: some transactions appear in both native and TRC20 results const mergedTxs = uniqBy([...nativeTxs.txs, ...trc20Txs.txs], tx => tx.txID); const sortedTxs = [...mergedTxs].sort(compareTxsByTimestamp(order)); const afterCursorTxs = dropTxsBeforeCursor({ txs: sortedTxs, order, cursor: parsedCursor }); const { txs: pageTxs, nextCursor } = dropTxsAfterNextCursor({ order, cursor, pageTxs: afterCursorTxs, nativeResult: nativeTxs, trc20Result: trc20Txs, }); const blocksByHeight = new Map<number, Block>(); const uniqueHeights = Array.from( new Set(pageTxs.map(tx => tx.blockHeight).filter((h): h is number => typeof h === "number")), ); await promiseAllBatched(5, uniqueHeights, async height => { const fetchedBlock = await getBlock(height); blocksByHeight.set(height, fetchedBlock); }); const operations = pageTxs.map(tx => { const height = tx.blockHeight; if (typeof height !== "number") { throw new Error(`Transaction ${tx.txID} has no block height`); } const txBlock = blocksByHeight.get(height); if (!txBlock) { throw new Error(`Block ${height} not found for transaction ${tx.txID}`); } return fromTrongridTxInfoToOperation(tx, txBlock, address); }); return { items: operations, next: nextCursor }; }