import { BigNumber } from "bignumber.js"; import * as network from "../network"; import { AlgoTransactionType } from "../network"; import { listOperations } from "./listOperations"; jest.mock("../network"); const mockGetAccountTransactions = network.getAccountTransactions as jest.MockedFunction< typeof network.getAccountTransactions >; describe("listOperations", () => { const address = "ALGO_ADDRESS_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; beforeEach(() => { jest.clearAllMocks(); }); it("should return empty array when no transactions", async () => { mockGetAccountTransactions.mockResolvedValue({ transactions: [], nextToken: undefined }); const { items, next } = await listOperations(address, { order: "desc" }); expect(items).toEqual([]); expect(next).toBe(""); }); it("should convert payment transaction to OUT operation for sender", async () => { const tx = { id: "TX_123", type: AlgoTransactionType.PAYMENT, senderAddress: address, fee: new BigNumber("1000"), round: 1000000, timestamp: "1700000000", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { amount: new BigNumber("1000000"), recipientAddress: "RECIPIENT_ADDRESS", closeToAddress: undefined, closeAmount: undefined, }, }; mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined }); const { items } = await listOperations(address, { order: "desc" }); expect(items).toHaveLength(1); expect(items[0].type).toBe("OUT"); expect(items[0].value).toBe(1000000n); expect(items[0].senders).toEqual([address]); expect(items[0].recipients).toEqual(["RECIPIENT_ADDRESS"]); expect(items[0].asset).toEqual({ type: "native" }); }); it("should convert payment transaction to IN operation for recipient", async () => { const tx = { id: "TX_456", type: AlgoTransactionType.PAYMENT, senderAddress: "SENDER_ADDRESS", fee: new BigNumber("1000"), round: 1000001, timestamp: "1700000100", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { amount: new BigNumber("500000"), recipientAddress: address, closeToAddress: undefined, closeAmount: undefined, }, }; mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined }); const { items } = await listOperations(address, { order: "desc" }); expect(items[0].type).toBe("IN"); expect(items[0].value).toBe(500000n); expect(items[0].senders).toEqual(["SENDER_ADDRESS"]); expect(items[0].recipients).toEqual([address]); expect(items[0].asset).toEqual({ type: "native" }); }); it("should convert asset transfer to operation with asa type", async () => { const tx = { id: "TX_789", type: AlgoTransactionType.ASSET_TRANSFER, senderAddress: address, fee: new BigNumber("1000"), round: 1000002, timestamp: "1700000200", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { assetAmount: new BigNumber("100"), assetRecipientAddress: "RECIPIENT_ADDRESS", assetId: "12345", assetCloseToAddress: undefined, assetCloseAmount: undefined, }, }; mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined }); const { items } = await listOperations(address, { order: "desc" }); expect(items[0].type).toBe("OUT"); expect(items[0].value).toBe(100n); expect(items[0].asset).toEqual({ type: "asa", assetReference: "12345" }); }); it("should detect OPT_IN operation", async () => { const tx = { id: "TX_OPTIN", type: AlgoTransactionType.ASSET_TRANSFER, senderAddress: address, fee: new BigNumber("1000"), round: 1000003, timestamp: "1700000300", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { assetAmount: new BigNumber("0"), assetRecipientAddress: address, // Same as sender assetId: "67890", assetCloseToAddress: undefined, assetCloseAmount: undefined, }, }; mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined }); const { items } = await listOperations(address, { order: "desc" }); expect(items[0].type).toBe("OPT_IN"); }); it("should detect OPT_OUT operation", async () => { const tx = { id: "TX_OPTOUT", type: AlgoTransactionType.ASSET_TRANSFER, senderAddress: address, fee: new BigNumber("1000"), round: 1000004, timestamp: "1700000400", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { assetAmount: new BigNumber("50"), assetRecipientAddress: "RECIPIENT_ADDRESS", assetId: "11111", assetCloseToAddress: "CLOSE_ADDRESS", assetCloseAmount: new BigNumber("100"), }, }; mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined }); const { items } = await listOperations(address, { order: "desc" }); expect(items[0].type).toBe("OPT_OUT"); }); it("should include memo when note is present", async () => { const tx = { id: "TX_MEMO", type: AlgoTransactionType.PAYMENT, senderAddress: address, fee: new BigNumber("1000"), round: 1000005, timestamp: "1700000500", note: "Test memo", senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { amount: new BigNumber("1000000"), recipientAddress: "RECIPIENT_ADDRESS", closeToAddress: undefined, closeAmount: undefined, }, }; mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined }); const { items } = await listOperations(address, { order: "desc" }); expect(items[0].details).toEqual({ memo: { type: "string", kind: "note", value: "Test memo", }, }); }); it("should include rewards in details when present", async () => { const tx = { id: "TX_REWARDS", type: AlgoTransactionType.PAYMENT, senderAddress: address, fee: new BigNumber("1000"), round: 1000006, timestamp: "1700000600", note: undefined, senderRewards: new BigNumber("500"), recipientRewards: new BigNumber("300"), details: { amount: new BigNumber("1000000"), recipientAddress: "RECIPIENT_ADDRESS", closeToAddress: undefined, closeAmount: undefined, }, }; mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined }); const { items } = await listOperations(address, { order: "desc" }); expect(items[0].details).toEqual({ rewards: 800n }); }); it("should sort operations in descending order", async () => { const txs = [ { id: "TX_1", type: AlgoTransactionType.PAYMENT, senderAddress: address, fee: new BigNumber("1000"), round: 1000, timestamp: "1700000000", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { amount: new BigNumber("100"), recipientAddress: "R1", closeToAddress: undefined, closeAmount: undefined, }, }, { id: "TX_2", type: AlgoTransactionType.PAYMENT, senderAddress: address, fee: new BigNumber("1000"), round: 3000, timestamp: "1700002000", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { amount: new BigNumber("200"), recipientAddress: "R2", closeToAddress: undefined, closeAmount: undefined, }, }, { id: "TX_3", type: AlgoTransactionType.PAYMENT, senderAddress: address, fee: new BigNumber("1000"), round: 2000, timestamp: "1700001000", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { amount: new BigNumber("300"), recipientAddress: "R3", closeToAddress: undefined, closeAmount: undefined, }, }, ]; mockGetAccountTransactions.mockResolvedValue({ transactions: txs, nextToken: "NEXT_TOKEN" }); const { items, next } = await listOperations(address, { order: "desc" }); expect(items[0].tx.block.height).toBe(3000); expect(items[1].tx.block.height).toBe(2000); expect(items[2].tx.block.height).toBe(1000); expect(next).toBe("NEXT_TOKEN"); }); it("should sort operations in ascending order", async () => { const txs = [ { id: "TX_1", type: AlgoTransactionType.PAYMENT, senderAddress: address, fee: new BigNumber("1000"), round: 3000, timestamp: "1700002000", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { amount: new BigNumber("100"), recipientAddress: "R1", closeToAddress: undefined, closeAmount: undefined, }, }, { id: "TX_2", type: AlgoTransactionType.PAYMENT, senderAddress: address, fee: new BigNumber("1000"), round: 1000, timestamp: "1700000000", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { amount: new BigNumber("200"), recipientAddress: "R2", closeToAddress: undefined, closeAmount: undefined, }, }, ]; mockGetAccountTransactions.mockResolvedValue({ transactions: txs, nextToken: undefined }); const { items } = await listOperations(address, { order: "asc" }); expect(items[0].tx.block.height).toBe(1000); expect(items[1].tx.block.height).toBe(3000); }); it("should filter out non-payment/transfer transactions", async () => { const txs = [ { id: "TX_PAY", type: AlgoTransactionType.PAYMENT, senderAddress: address, fee: new BigNumber("1000"), round: 1000, timestamp: "1700000000", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { amount: new BigNumber("100"), recipientAddress: "R1", closeToAddress: undefined, closeAmount: undefined, }, }, { id: "TX_OTHER", type: "keyreg" as AlgoTransactionType, // Key registration senderAddress: address, fee: new BigNumber("1000"), round: 2000, timestamp: "1700001000", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: {}, }, ]; mockGetAccountTransactions.mockResolvedValue({ transactions: txs, nextToken: undefined }); const { items } = await listOperations(address, { order: "desc" }); expect(items).toHaveLength(1); expect(items[0].id).toBe("TX_PAY"); }); it("should forward pagination options to getAccountTransactions", async () => { mockGetAccountTransactions.mockResolvedValue({ transactions: [], nextToken: undefined }); await listOperations(address, { order: "asc", minHeight: 5000, limit: 50, cursor: "PAGE_TOKEN", }); expect(mockGetAccountTransactions).toHaveBeenCalledWith(address, { minRound: 5000, limit: 50, nextToken: "PAGE_TOKEN", }); }); it("should return next token from paginated response", async () => { const tx = { id: "TX_PAGE", type: AlgoTransactionType.PAYMENT, senderAddress: address, fee: new BigNumber("1000"), round: 1000, timestamp: "1700000000", note: undefined, senderRewards: new BigNumber("0"), recipientRewards: new BigNumber("0"), details: { amount: new BigNumber("100"), recipientAddress: "R1", closeToAddress: undefined, closeAmount: undefined, }, }; mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: "CURSOR_ABC", }); const { items, next } = await listOperations(address, { order: "asc" }); expect(items).toHaveLength(1); expect(next).toBe("CURSOR_ABC"); }); it("should propagate network errors", async () => { mockGetAccountTransactions.mockRejectedValue(new Error("Network error")); await expect(listOperations(address, { order: "desc" })).rejects.toThrow("Network error"); }); });