import type { Account, TokenAccount } from "@ledgerhq/types-live"; import { BigNumber } from "bignumber.js"; import makeCliTools from "./cli-transaction"; import type { AlgorandAccount, Transaction } from "./types"; // Mock dependencies jest.mock("@ledgerhq/ledger-wallet-framework/account/index", () => ({ getAccountCurrency: jest.fn((account: TokenAccount) => account.token), })); describe("cli-transaction", () => { const cliTools = makeCliTools(); describe("makeCliTools", () => { it("should return object with options, inferAccounts, and inferTransactions", () => { expect(cliTools).toHaveProperty("options"); expect(cliTools).toHaveProperty("inferAccounts"); expect(cliTools).toHaveProperty("inferTransactions"); }); }); describe("options", () => { it("should include mode option", () => { const modeOption = cliTools.options.find(o => o.name === "mode"); expect(modeOption).not.toBeUndefined(); expect(modeOption?.type).toBe(String); expect(modeOption?.desc).toContain("send"); expect(modeOption?.desc).toContain("optIn"); expect(modeOption?.desc).toContain("claimReward"); }); it("should include fees option", () => { const feesOption = cliTools.options.find(o => o.name === "fees"); expect(feesOption).not.toBeUndefined(); expect(feesOption?.type).toBe(String); }); it("should include gasLimit option", () => { const gasLimitOption = cliTools.options.find(o => o.name === "gasLimit"); expect(gasLimitOption).not.toBeUndefined(); expect(gasLimitOption?.type).toBe(String); }); it("should include memo option", () => { const memoOption = cliTools.options.find(o => o.name === "memo"); expect(memoOption).not.toBeUndefined(); expect(memoOption?.type).toBe(String); }); it("should include token option with alias and multiple", () => { const tokenOption = cliTools.options.find(o => o.name === "token"); expect(tokenOption).not.toBeUndefined(); expect(tokenOption?.alias).toBe("t"); expect(tokenOption?.type).toBe(String); expect(tokenOption?.multiple).toBe(true); }); }); describe("inferAccounts", () => { const createMockAccount = (subAccounts: TokenAccount[] = []): Account => ({ id: "algorand-account-1", currency: { family: "algorand" }, subAccounts, }) as unknown as Account; const createMockTokenAccount = (ticker: string, id: string): TokenAccount => ({ type: "TokenAccount", id: `algorand-account-1+${id}`, token: { ticker, id: `algorand/asa/${id}` }, }) as unknown as TokenAccount; it("should return main account when no token option", () => { const account = createMockAccount(); const result = cliTools.inferAccounts(account, {}); expect(result).toHaveLength(1); expect(result[0]).toBe(account); }); it("should return main account for optIn mode even with token option", () => { const account = createMockAccount(); const result = cliTools.inferAccounts(account, { token: ["USDC"], mode: "optIn" }); expect(result).toHaveLength(1); expect(result[0]).toBe(account); }); it("should find token account by ticker (case insensitive)", () => { const tokenAccount = createMockTokenAccount("USDC", "12345"); const account = createMockAccount([tokenAccount]); const result = cliTools.inferAccounts(account, { token: ["usdc"] }); expect(result).toHaveLength(1); expect(result[0]).toBe(tokenAccount); }); it("should find token account by asset ID", () => { const tokenAccount = createMockTokenAccount("USDC", "12345"); const account = createMockAccount([tokenAccount]); const result = cliTools.inferAccounts(account, { token: ["12345"] }); expect(result).toHaveLength(1); expect(result[0]).toBe(tokenAccount); }); it("should throw error when token account not found", () => { const account = createMockAccount([]); expect(() => cliTools.inferAccounts(account, { token: ["UNKNOWN"] })).toThrow( "token account 'UNKNOWN' not found", ); }); it("should list available tokens in error message", () => { const tokenAccount1 = createMockTokenAccount("USDC", "12345"); const tokenAccount2 = createMockTokenAccount("USDT", "67890"); const account = createMockAccount([tokenAccount1, tokenAccount2]); expect(() => cliTools.inferAccounts(account, { token: ["UNKNOWN"] })).toThrow( /Available: USDC, USDT/, ); }); it("should handle multiple token options", () => { const tokenAccount1 = createMockTokenAccount("USDC", "12345"); const tokenAccount2 = createMockTokenAccount("USDT", "67890"); const account = createMockAccount([tokenAccount1, tokenAccount2]); const result = cliTools.inferAccounts(account, { token: ["USDC", "USDT"] }); expect(result).toHaveLength(2); expect(result[0]).toBe(tokenAccount1); expect(result[1]).toBe(tokenAccount2); }); it("should throw invariant error for non-algorand account", () => { const account = { currency: { family: "bitcoin" }, } as unknown as Account; expect(() => cliTools.inferAccounts(account, {})).toThrow(); }); }); describe("inferTransactions", () => { const createMockAlgorandAccount = (): AlgorandAccount => ({ type: "Account", id: "algorand-account-1", algorandResources: { rewards: new BigNumber("0"), nbAssets: 0, }, }) as unknown as AlgorandAccount; const createMockTokenAccount = (): TokenAccount => ({ type: "TokenAccount", id: "algorand-account-1+12345", token: { ticker: "USDC", id: "algorand/asa/12345", delisted: false }, }) as unknown as TokenAccount; const createMockTransaction = (): Transaction => ({ family: "algorand", amount: new BigNumber("1000000"), recipient: "RECIPIENT_ADDRESS", }) as Transaction; const mockInferAmount = jest.fn().mockReturnValue(new BigNumber("1000")); it("should set default mode to send", () => { const account = createMockAlgorandAccount(); const transaction = createMockTransaction(); const result = cliTools.inferTransactions( [{ account, transaction }], {}, { inferAmount: mockInferAmount }, ); expect(result[0].mode).toBe("send"); }); it("should use provided mode", () => { const account = createMockAlgorandAccount(); const transaction = createMockTransaction(); const result = cliTools.inferTransactions( [{ account, transaction }], { mode: "optIn" }, { inferAmount: mockInferAmount }, ); expect(result[0].mode).toBe("optIn"); }); it("should set memo from options", () => { const account = createMockAlgorandAccount(); const transaction = createMockTransaction(); const result = cliTools.inferTransactions( [{ account, transaction }], { memo: "test memo" }, { inferAmount: mockInferAmount }, ); expect(result[0].memo).toBe("test memo"); }); it("should infer fees when provided", () => { const account = createMockAlgorandAccount(); const transaction = createMockTransaction(); const result = cliTools.inferTransactions( [{ account, transaction }], { fees: "1000" }, { inferAmount: mockInferAmount }, ); expect(mockInferAmount).toHaveBeenCalledWith(account, "1000"); expect(result[0].fees).toEqual(new BigNumber("1000")); }); it("should set fees to null when not provided", () => { const account = createMockAlgorandAccount(); const transaction = createMockTransaction(); const result = cliTools.inferTransactions( [{ account, transaction }], {}, { inferAmount: mockInferAmount }, ); expect(result[0].fees).toBeNull(); }); it("should set subAccountId for token account", () => { const tokenAccount = createMockTokenAccount(); const transaction = createMockTransaction(); const result = cliTools.inferTransactions( [{ account: tokenAccount, transaction }], {}, { inferAmount: mockInferAmount }, ); expect(result[0].subAccountId).toBe("algorand-account-1+12345"); }); it("should set subAccountId to null for main account", () => { const account = createMockAlgorandAccount(); const transaction = createMockTransaction(); const result = cliTools.inferTransactions( [{ account, transaction }], {}, { inferAmount: mockInferAmount }, ); expect(result[0].subAccountId).toBeNull(); }); it("should set assetId from token option", () => { const account = createMockAlgorandAccount(); const transaction = createMockTransaction(); const result = cliTools.inferTransactions( [{ account, transaction }], { token: "12345" }, { inferAmount: mockInferAmount }, ); expect(result[0].assetId).toBe("algorand/asa/12345"); }); it("should set assetId to null when no token option", () => { const account = createMockAlgorandAccount(); const transaction = createMockTransaction(); const result = cliTools.inferTransactions( [{ account, transaction }], {}, { inferAmount: mockInferAmount }, ); expect(result[0].assetId).toBeNull(); }); it("should throw invariant error for non-algorand transaction", () => { const account = createMockAlgorandAccount(); const transaction = { family: "bitcoin" } as unknown as Transaction; expect(() => cliTools.inferTransactions( [{ account, transaction }], {}, { inferAmount: mockInferAmount }, ), ).toThrow(); }); it("should throw invariant error for unactivated account", () => { const account = { type: "Account", algorandResources: undefined, } as unknown as AlgorandAccount; const transaction = createMockTransaction(); expect(() => cliTools.inferTransactions( [{ account, transaction }], {}, { inferAmount: mockInferAmount }, ), ).toThrow("unactivated account"); }); it("should throw invariant error for delisted token", () => { const tokenAccount = { type: "TokenAccount", id: "token-1", token: { ticker: "DELISTED", delisted: true }, } as unknown as TokenAccount; const transaction = createMockTransaction(); expect(() => cliTools.inferTransactions( [{ account: tokenAccount, transaction }], {}, { inferAmount: mockInferAmount }, ), ).toThrow("token is delisted"); }); it("should handle multiple transactions", () => { const account1 = createMockAlgorandAccount(); const account2 = createMockAlgorandAccount(); const transaction1 = createMockTransaction(); const transaction2 = createMockTransaction(); const result = cliTools.inferTransactions( [ { account: account1, transaction: transaction1 }, { account: account2, transaction: transaction2 }, ], { mode: "send" }, { inferAmount: mockInferAmount }, ); expect(result).toHaveLength(2); expect(result[0].mode).toBe("send"); expect(result[1].mode).toBe("send"); }); it("should preserve original transaction properties", () => { const account = createMockAlgorandAccount(); const transaction = { ...createMockTransaction(), amount: new BigNumber("5000000"), recipient: "CUSTOM_RECIPIENT", }; const result = cliTools.inferTransactions( [{ account, transaction }], {}, { inferAmount: mockInferAmount }, ); expect(result[0].amount.toString()).toBe("5000000"); expect(result[0].recipient).toBe("CUSTOM_RECIPIENT"); }); }); });