UNPKG

@ledgerhq/live-common

Version:
314 lines (270 loc) • 9.61 kB
/** * @jest-environment jsdom */ /* eslint-disable @typescript-eslint/consistent-type-assertions */ import { renderHook } from "@testing-library/react"; import { BigNumber } from "bignumber.js"; import { createFixtureAccount } from "../../mock/fixtures/cryptoCurrencies"; import type { Transaction, TransactionStatus } from "../../generated/types"; import { useBitcoinUtxoDisplayData, type UseBitcoinUtxoDisplayDataParams } from "./react"; import type { BitcoinAccount, BitcoinOutput } from "./types"; import { bitcoinPickingStrategy } from "./types"; jest.mock("./logic", () => ({ getUTXOStatus: jest.fn(), })); jest.mock("./logic", () => ({ getUTXOStatus: jest.fn(), })); jest.mock("./logic", () => ({ getUTXOStatus: jest.fn(), })); jest.mock("@ledgerhq/coin-framework/currencies/formatCurrencyUnit", () => ({ formatCurrencyUnit: jest.fn((_unit: unknown, value: BigNumber) => `${value.toString()} BTC`), })); const getUTXOStatus = jest.requireMock("./logic").getUTXOStatus; function createBitcoinAccount(overrides?: Partial<BitcoinAccount>): BitcoinAccount { return { ...createFixtureAccount("bitcoin"), ...overrides, } as BitcoinAccount; } function createUtxo(overrides?: Partial<BitcoinOutput>): BitcoinOutput { return { hash: "abc123", outputIndex: 0, blockHeight: 799000, address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", value: new BigNumber(10000), rbf: false, isChange: false, ...overrides, }; } function createBitcoinTransaction(overrides?: Partial<Transaction>): Transaction { return { family: "bitcoin", amount: new BigNumber(0), recipient: "", useAllAmount: false, utxoStrategy: { strategy: bitcoinPickingStrategy.MERGE_OUTPUTS, excludeUTXOs: [], }, ...overrides, } as Transaction; } function createBitcoinStatus(overrides?: Partial<TransactionStatus>): TransactionStatus { return { errors: {}, warnings: {}, estimatedFees: new BigNumber(0), amount: new BigNumber(0), totalSpent: new BigNumber(5000), txInputs: [], ...overrides, } as TransactionStatus; } describe("useBitcoinUtxoDisplayData", () => { beforeEach(() => { jest.clearAllMocks(); (getUTXOStatus as jest.Mock).mockReturnValue({ excluded: false }); }); it("should return null when account is not Bitcoin-based", () => { const account = createFixtureAccount("ethereum"); const params: UseBitcoinUtxoDisplayDataParams = { account, transaction: createBitcoinTransaction(), status: createBitcoinStatus(), locale: "en", }; const { result } = renderHook(() => useBitcoinUtxoDisplayData(params)); expect(result.current).toBeNull(); expect(getUTXOStatus).not.toHaveBeenCalled(); }); it("should return null when account has no bitcoinResources", () => { const account = createFixtureAccount("bitcoin"); const params: UseBitcoinUtxoDisplayDataParams = { account, transaction: createBitcoinTransaction(), status: createBitcoinStatus(), locale: "en", }; const { result } = renderHook(() => useBitcoinUtxoDisplayData(params)); expect(result.current).toBeNull(); }); it("should return null when transaction has no utxoStrategy", () => { const account = createBitcoinAccount({ bitcoinResources: { utxos: [createUtxo()] }, }); const transactionWithoutStrategy = { ...createBitcoinTransaction(), utxoStrategy: undefined, }; const params: UseBitcoinUtxoDisplayDataParams = { account, transaction: transactionWithoutStrategy as Transaction, status: createBitcoinStatus(), locale: "en", }; const { result } = renderHook(() => useBitcoinUtxoDisplayData(params)); expect(result.current).toBeNull(); }); it("should return null when status has no txInputs (not Bitcoin transaction status)", () => { const account = createBitcoinAccount({ bitcoinResources: { utxos: [createUtxo()] }, }); const statusWithoutTxInputs = createBitcoinStatus(); delete (statusWithoutTxInputs as Record<string, unknown>).txInputs; const params: UseBitcoinUtxoDisplayDataParams = { account, transaction: createBitcoinTransaction(), status: statusWithoutTxInputs, locale: "en", }; const { result } = renderHook(() => useBitcoinUtxoDisplayData(params)); expect(result.current).toBeNull(); }); it("should return null when bitcoinResources has no utxos", () => { const account = createBitcoinAccount({ bitcoinResources: { utxos: [] }, }); const params: UseBitcoinUtxoDisplayDataParams = { account, transaction: createBitcoinTransaction(), status: createBitcoinStatus(), locale: "en", }; const { result } = renderHook(() => useBitcoinUtxoDisplayData(params)); expect(result.current).toBeNull(); }); it("should return display data with correct shape when all conditions are met", () => { const utxo = createUtxo(); const account = createBitcoinAccount({ bitcoinResources: { utxos: [utxo] }, blockHeight: 800000, }); const transaction = createBitcoinTransaction(); const status = createBitcoinStatus({ totalSpent: new BigNumber(3000) }); const params: UseBitcoinUtxoDisplayDataParams = { account, transaction, status, locale: "en", }; const { result } = renderHook(() => useBitcoinUtxoDisplayData(params)); expect(result.current).not.toBeNull(); expect(result.current).toMatchObject({ pickingStrategyValue: bitcoinPickingStrategy.MERGE_OUTPUTS, totalExcludedUTXOS: 0, totalSpent: new BigNumber(3000), }); expect(result.current?.pickingStrategyOptions).toHaveLength( Object.keys(bitcoinPickingStrategy).length, ); expect(result.current?.utxoRows).toHaveLength(1); expect(getUTXOStatus).toHaveBeenCalledWith( utxo, (transaction as { utxoStrategy: unknown }).utxoStrategy, ); }); it("should compute totalExcludedUTXOS from getUTXOStatus results", () => { const utxo1 = createUtxo({ outputIndex: 0 }); const utxo2 = createUtxo({ hash: "def456", outputIndex: 1 }); const account = createBitcoinAccount({ bitcoinResources: { utxos: [utxo1, utxo2] }, }); (getUTXOStatus as jest.Mock).mockImplementation((u: BitcoinOutput) => ({ excluded: u.outputIndex === 1, reason: u.outputIndex === 1 ? ("userExclusion" as const) : undefined, })); const { result } = renderHook(() => useBitcoinUtxoDisplayData({ account, transaction: createBitcoinTransaction(), status: createBitcoinStatus(), locale: "en", }), ); expect(result.current).not.toBeNull(); expect(result.current?.totalExcludedUTXOS).toBe(1); expect(result.current?.utxoRows[0].excluded).toBe(false); expect(result.current?.utxoRows[1].excluded).toBe(true); expect(result.current?.utxoRows[1].exclusionReason).toBe("userExclusion"); }); it("should set isUsedInTx when utxo matches txInputs", () => { const utxo = createUtxo({ hash: "txHash1", outputIndex: 2 }); const account = createBitcoinAccount({ bitcoinResources: { utxos: [utxo] }, }); const status = createBitcoinStatus({ txInputs: [ { address: null, value: null, previousTxHash: "txHash1", previousOutputIndex: 2, }, ], }); const { result } = renderHook(() => useBitcoinUtxoDisplayData({ account, transaction: createBitcoinTransaction(), status, locale: "en", }), ); expect(result.current?.utxoRows[0].isUsedInTx).toBe(true); }); it("should set unconfirmed and disabled when exclusion reason is pickPendingUtxo", () => { const utxo = createUtxo({ blockHeight: undefined, isChange: false }); const account = createBitcoinAccount({ bitcoinResources: { utxos: [utxo] }, }); (getUTXOStatus as jest.Mock).mockReturnValue({ excluded: true, reason: "pickPendingUtxo", }); const { result } = renderHook(() => useBitcoinUtxoDisplayData({ account, transaction: createBitcoinTransaction(), status: createBitcoinStatus(), locale: "en", }), ); expect(result.current?.utxoRows[0].unconfirmed).toBe(true); expect(result.current?.utxoRows[0].disabled).toBe(true); expect(result.current?.utxoRows[0].exclusionReason).toBe("pickPendingUtxo"); }); it("should format utxo value with locale", () => { const utxo = createUtxo({ value: new BigNumber(250000) }); const account = createBitcoinAccount({ bitcoinResources: { utxos: [utxo] }, }); const { result } = renderHook(() => useBitcoinUtxoDisplayData({ account, transaction: createBitcoinTransaction(), status: createBitcoinStatus(), locale: "fr", }), ); expect(result.current?.utxoRows[0].formattedValue).toBe("250000 BTC"); }); it("should use totalSpent from status or default to zero", () => { const account = createBitcoinAccount({ bitcoinResources: { utxos: [createUtxo()] }, }); const statusWithNoSpent = createBitcoinStatus({ totalSpent: undefined }); const { result } = renderHook(() => useBitcoinUtxoDisplayData({ account, transaction: createBitcoinTransaction(), status: statusWithNoSpent, locale: "en", }), ); expect(result.current?.totalSpent).toEqual(new BigNumber(0)); }); });