UNPKG

@ledgerhq/coin-tron

Version:
482 lines (422 loc) 14.8 kB
import BigNumber from "bignumber.js"; import { fetchTronAccountTxsPage, getBlock } from "../network"; import { fromTrongridTxInfoToOperation } from "../network/trongrid/trongrid-adapters"; import { TrongridTxInfo } from "../types"; import { listOperations, ListOperationsOptions } from "./listOperations"; import { TronEmptyPage } from "../types/errors"; jest.mock("../network", () => ({ fetchTronAccountTxsPage: jest.fn(), getBlock: jest.fn(), })); jest.mock("../network/trongrid/trongrid-adapters", () => ({ fromTrongridTxInfoToOperation: jest.fn(), })); describe("listOperations", () => { const mockAddress = "tronExampleAddress"; const defaultOptions: ListOperationsOptions = { limit: 200, minTimestamp: 0, order: "asc", }; beforeEach(() => { jest.clearAllMocks(); (getBlock as jest.Mock).mockResolvedValue({ height: 0, hash: "hash0", time: new Date("2023-01-01T00:00:00Z"), }); }); it("should fetch transactions and return operations with pagination", async () => { const mockTxs: Partial<TrongridTxInfo>[] = [ { txID: "tx1", value: new BigNumber(0), date: new Date("2023-01-01T00:00:00Z"), blockHeight: 100, }, { txID: "tx2", value: new BigNumber(42), date: new Date("2023-01-01T01:00:00Z"), blockHeight: 200, }, ]; (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: mockTxs, hasNextPage: false }, trc20Txs: { txs: [], hasNextPage: false }, }); (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({ tx: { hash: tx.txID }, value: BigInt(tx.value.toString()), })); const result = await listOperations(mockAddress, defaultOptions); expect(fetchTronAccountTxsPage).toHaveBeenCalledWith(mockAddress, { limit: 200, minTimestamp: 0, order: "asc", }); expect(result.items).toHaveLength(2); expect(result.next).toBeUndefined(); }); it("should handle empty transactions on first page (no cursor)", async () => { (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: [], hasNextPage: false }, trc20Txs: { txs: [], hasNextPage: false }, }); const result = await listOperations(mockAddress, defaultOptions); expect(result.items).toHaveLength(0); expect(result.next).toBeUndefined(); }); it("should throw TronEmptyPage when cursor is provided but TronGrid returns empty page", async () => { // TronGrid occasionally returns 0 results for a valid cursor (transient failure). // A cursor is only issued when hasNextPage=true, so this is never a legitimate end-of-stream. (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: [], hasNextPage: false }, trc20Txs: { txs: [], hasNextPage: false }, }); const cursor = `${new Date("2023-01-01T01:00:00Z").getTime()}:tx1`; await expect(listOperations(mockAddress, { ...defaultOptions, cursor })).rejects.toThrow( TronEmptyPage, ); }); it("should return cursor when hasNextPage is true", async () => { const mockTxs: Partial<TrongridTxInfo>[] = [ { txID: "tx1", value: new BigNumber(0), date: new Date("2023-01-01T00:00:00Z"), blockHeight: 100, }, { txID: "tx2", value: new BigNumber(42), date: new Date("2023-01-01T01:00:00Z"), blockHeight: 200, }, { txID: "tx3", value: new BigNumber(50), date: new Date("2023-01-01T02:00:00Z"), blockHeight: 300, }, ]; (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: mockTxs, hasNextPage: true }, trc20Txs: { txs: [], hasNextPage: false }, }); (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({ tx: { hash: tx.txID }, value: BigInt(tx.value.toString()), })); const result = await listOperations(mockAddress, defaultOptions); expect(result.items).toHaveLength(3); expect(result.next).toContain("tx3"); }); it("should merge and dedupe native and trc20 transactions", async () => { const nativeTxs: Partial<TrongridTxInfo>[] = [ { txID: "tx1", value: new BigNumber(0), date: new Date("2023-01-01T00:00:00Z"), blockHeight: 100, }, { txID: "tx3", value: new BigNumber(30), date: new Date("2023-01-01T02:00:00Z"), blockHeight: 300, }, ]; const trc20Txs: Partial<TrongridTxInfo>[] = [ { txID: "tx2", value: new BigNumber(20), date: new Date("2023-01-01T01:00:00Z"), blockHeight: 200, }, { txID: "tx1", value: new BigNumber(0), date: new Date("2023-01-01T00:00:00Z"), blockHeight: 100, }, ]; (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: nativeTxs, hasNextPage: false }, trc20Txs: { txs: trc20Txs, hasNextPage: false }, }); (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({ tx: { hash: tx.txID }, value: BigInt(tx.value.toString()), })); const result = await listOperations(mockAddress, defaultOptions); expect(result.items).toHaveLength(3); const hashes = result.items.map(op => op.tx.hash); expect(hashes).toEqual(["tx1", "tx2", "tx3"]); }); it("should sort transactions by timestamp in ascending order", async () => { const mockTxs: Partial<TrongridTxInfo>[] = [ { txID: "tx3", value: new BigNumber(30), date: new Date("2023-01-01T03:00:00Z"), blockHeight: 300, }, { txID: "tx1", value: new BigNumber(10), date: new Date("2023-01-01T01:00:00Z"), blockHeight: 100, }, { txID: "tx2", value: new BigNumber(20), date: new Date("2023-01-01T02:00:00Z"), blockHeight: 200, }, ]; (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: mockTxs, hasNextPage: false }, trc20Txs: { txs: [], hasNextPage: false }, }); (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({ tx: { hash: tx.txID, date: tx.date }, value: BigInt(tx.value.toString()), })); const result = await listOperations(mockAddress, { ...defaultOptions, order: "asc" }); const hashes = result.items.map(op => op.tx.hash); expect(hashes).toEqual(["tx1", "tx2", "tx3"]); }); it("should sort transactions by timestamp in descending order", async () => { const mockTxs: Partial<TrongridTxInfo>[] = [ { txID: "tx1", value: new BigNumber(10), date: new Date("2023-01-01T01:00:00Z"), blockHeight: 100, }, { txID: "tx3", value: new BigNumber(30), date: new Date("2023-01-01T03:00:00Z"), blockHeight: 300, }, { txID: "tx2", value: new BigNumber(20), date: new Date("2023-01-01T02:00:00Z"), blockHeight: 200, }, ]; (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: mockTxs, hasNextPage: false }, trc20Txs: { txs: [], hasNextPage: false }, }); (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({ tx: { hash: tx.txID, date: tx.date }, value: BigInt(tx.value.toString()), })); const result = await listOperations(mockAddress, { ...defaultOptions, order: "desc" }); const hashes = result.items.map(op => op.tx.hash); expect(hashes).toEqual(["tx3", "tx2", "tx1"]); }); it("should filter out transactions before cursor", async () => { const mockTxs: Partial<TrongridTxInfo>[] = [ { txID: "tx1", value: new BigNumber(10), date: new Date("2023-01-01T01:00:00Z"), blockHeight: 100, }, { txID: "tx2", value: new BigNumber(20), date: new Date("2023-01-01T02:00:00Z"), blockHeight: 200, }, { txID: "tx3", value: new BigNumber(30), date: new Date("2023-01-01T03:00:00Z"), blockHeight: 300, }, ]; (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: mockTxs, hasNextPage: false }, trc20Txs: { txs: [], hasNextPage: false }, }); (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({ tx: { hash: tx.txID }, value: BigInt(tx.value.toString()), })); const cursorTimestamp = new Date("2023-01-01T01:00:00Z").getTime(); const cursor = `${cursorTimestamp}:tx1`; const result = await listOperations(mockAddress, { ...defaultOptions, cursor }); const hashes = result.items.map(op => op.tx.hash); expect(hashes).toEqual(["tx2", "tx3"]); }); it("should filter by cursor position when multiple txs share same timestamp", async () => { const sameTimestamp = new Date("2023-01-01T01:00:00Z"); const mockTxs: Partial<TrongridTxInfo>[] = [ { txID: "txA", value: new BigNumber(10), date: sameTimestamp, blockHeight: 100 }, { txID: "txB", value: new BigNumber(20), date: sameTimestamp, blockHeight: 100 }, { txID: "txC", value: new BigNumber(30), date: sameTimestamp, blockHeight: 100 }, { txID: "txD", value: new BigNumber(40), date: new Date("2023-01-01T02:00:00Z"), blockHeight: 200, }, ]; (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: mockTxs, hasNextPage: false }, trc20Txs: { txs: [], hasNextPage: false }, }); (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({ tx: { hash: tx.txID }, value: BigInt(tx.value.toString()), })); const cursor = `${sameTimestamp.getTime()}:txB`; const result = await listOperations(mockAddress, { ...defaultOptions, cursor }); const hashes = result.items.map(op => op.tx.hash); expect(hashes).toEqual(["txC", "txD"]); }); it("should handle errors from fetchTronAccountTxsPage", async () => { const exampleError = new Error("Network error!"); (fetchTronAccountTxsPage as jest.Mock).mockRejectedValue(exampleError); await expect(listOperations(mockAddress, defaultOptions)).rejects.toThrow("Network error!"); }); it("should throw on invalid cursor format", async () => { await expect( listOperations(mockAddress, { ...defaultOptions, cursor: "invalid" }), ).rejects.toThrow("Invalid cursor format"); }); it("should use boundary operation for next cursor when endpoints have different last ops", async () => { const nativeTxs: Partial<TrongridTxInfo>[] = [ { txID: "native1", value: new BigNumber(10), date: new Date("2023-01-01T01:00:00Z"), blockHeight: 100, }, { txID: "native2", value: new BigNumber(20), date: new Date("2023-01-01T03:00:00Z"), blockHeight: 300, }, ]; const trc20Txs: Partial<TrongridTxInfo>[] = [ { txID: "trc20-1", value: new BigNumber(15), date: new Date("2023-01-01T02:00:00Z"), blockHeight: 200, }, ]; (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: nativeTxs, hasNextPage: true }, trc20Txs: { txs: trc20Txs, hasNextPage: true }, }); (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({ tx: { hash: tx.txID }, value: BigInt(tx.value.toString()), })); const result = await listOperations(mockAddress, defaultOptions); expect(result.next).toContain("trc20-1"); const hashes = result.items.map(op => op.tx.hash); expect(hashes).toEqual(["native1", "trc20-1"]); }); it("should pass maxTimestamp from cursor for desc order pagination", async () => { const mockTxs: Partial<TrongridTxInfo>[] = [ { txID: "tx3", value: new BigNumber(30), date: new Date("2023-01-01T03:00:00Z"), blockHeight: 300, }, { txID: "tx2", value: new BigNumber(20), date: new Date("2023-01-01T02:00:00Z"), blockHeight: 200, }, ]; (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: mockTxs, hasNextPage: false }, trc20Txs: { txs: [], hasNextPage: false }, }); (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({ tx: { hash: tx.txID }, value: BigInt(tx.value.toString()), })); const cursorTimestamp = new Date("2023-01-01T04:00:00Z").getTime(); const cursor = `${cursorTimestamp}:tx4`; const minTimestamp = 1000; await listOperations(mockAddress, { ...defaultOptions, order: "desc", cursor, minTimestamp, }); expect(fetchTronAccountTxsPage).toHaveBeenCalledWith(mockAddress, { limit: 200, minTimestamp, maxTimestamp: cursorTimestamp, order: "desc", }); }); it("should not pass maxTimestamp for desc order without cursor", async () => { (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: [], hasNextPage: false }, trc20Txs: { txs: [], hasNextPage: false }, }); const minTimestamp = 1000; await listOperations(mockAddress, { ...defaultOptions, order: "desc", minTimestamp, }); expect(fetchTronAccountTxsPage).toHaveBeenCalledWith(mockAddress, { limit: 200, minTimestamp, maxTimestamp: undefined, order: "desc", }); }); it("should pass cursor timestamp as minTimestamp for asc order pagination", async () => { const cursorTimestamp = new Date("2023-01-01T02:00:00Z").getTime(); const cursor = `${cursorTimestamp}:tx2`; (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({ nativeTxs: { txs: [ { txID: "tx3", value: new BigNumber(30), date: new Date("2023-01-01T03:00:00Z"), blockHeight: 300, }, ], hasNextPage: false, }, trc20Txs: { txs: [], hasNextPage: false }, }); (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({ tx: { hash: tx.txID }, value: BigInt(tx.value.toString()), })); const minTimestamp = 1000; await listOperations(mockAddress, { ...defaultOptions, order: "asc", cursor, minTimestamp, }); expect(fetchTronAccountTxsPage).toHaveBeenCalledWith(mockAddress, { limit: 200, minTimestamp: cursorTimestamp, maxTimestamp: undefined, order: "asc", }); }); });