UNPKG

@ledgerhq/coin-hedera

Version:
213 lines (182 loc) 7.97 kB
import * as coinFrameworkAccount from "@ledgerhq/ledger-wallet-framework/account"; import type { AccountShapeInfo } from "@ledgerhq/ledger-wallet-framework/bridge/jsHelpers"; import type { Account } from "@ledgerhq/types-live"; import hederaCoinConfig from "../config"; import * as logic from "../logic"; import * as logicUtils from "../logic/utils"; import { apiClient } from "../network/api"; import * as networkUtils from "../network/utils"; import { getMockedAccount, getMockedTokenAccount } from "../test/fixtures/account.fixture"; import { getMockedConfig } from "../test/fixtures/config.fixture"; import { getMockedCurrency, getMockedHTSTokenCurrency } from "../test/fixtures/currency.fixture"; import { getMockedMirrorAccount } from "../test/fixtures/mirror.fixture"; import { getMockedOperation } from "../test/fixtures/operation.fixture"; import type { HederaAccount } from "../types"; import { getAccountShape, postSync } from "./synchronisation"; jest.mock("../config"); jest.mock("../network/api"); jest.mock("../network/utils"); jest.mock("../logic"); jest.mock("../logic/utils", () => ({ ...jest.requireActual("../logic/utils"), toEVMAddress: jest.fn(), })); jest.mock("@ledgerhq/ledger-wallet-framework/account", () => ({ ...jest.requireActual("@ledgerhq/ledger-wallet-framework/account"), getSyncHash: jest.fn(), encodeAccountId: jest.fn(), })); const mockEncodeAccountId = jest.mocked(coinFrameworkAccount.encodeAccountId); const mockGetSyncHash = jest.mocked(coinFrameworkAccount.getSyncHash); const mockHederaConfig = jest.mocked(hederaCoinConfig); const mockToEVMAddress = jest.mocked(logicUtils.toEVMAddress); const mockGetAccount = jest.mocked(apiClient.getAccount); const mockGetAccountTokens = jest.mocked(apiClient.getAccountTokens); const mockListOperationsV2 = jest.mocked(logic.listOperationsV2); const mockGetERC20BalancesForAccountV2 = jest.mocked(networkUtils.getERC20BalancesForAccountV2); const mockConfig = getMockedConfig(); const mockCurrency = getMockedCurrency(); const mockMirrorAccount = getMockedMirrorAccount(); const mockAddress = mockMirrorAccount.account; const mockEvmAddress = mockMirrorAccount.evm_address; const mockLiveAccountId = `js:2:hedera:${mockAddress}:`; const mockSyncHash = "synchash"; const mockInfo: AccountShapeInfo<HederaAccount> = { currency: mockCurrency, derivationMode: "" as const, address: mockAddress, initialAccount: undefined, index: 0, derivationPath: "44/3030", }; describe("getAccountShape", () => { beforeEach(() => { jest.clearAllMocks(); mockHederaConfig.getCoinConfig.mockReturnValue({ ...mockConfig, useHgraphForErc20: true }); mockEncodeAccountId.mockReturnValue(mockLiveAccountId); mockGetSyncHash.mockResolvedValue(mockSyncHash); mockToEVMAddress.mockResolvedValue(mockEvmAddress); mockGetAccount.mockResolvedValue(mockMirrorAccount); mockGetAccountTokens.mockResolvedValue([]); mockGetERC20BalancesForAccountV2.mockResolvedValue([]); mockListOperationsV2.mockResolvedValue({ coinOperations: [], tokenOperations: [], nextCursor: null, }); }); it("should call listOperationsV2 instead of listOperations", async () => { await getAccountShape(mockInfo, { paginationConfig: {} }); expect(logic.listOperations).not.toHaveBeenCalled(); expect(logic.listOperationsV2).toHaveBeenCalledTimes(1); expect(logic.listOperationsV2).toHaveBeenCalledWith( expect.objectContaining({ address: mockAddress, evmAddress: mockEvmAddress }), ); }); it("should call getERC20BalancesForAccountV2 instead of getERC20BalancesForAccount", async () => { await getAccountShape(mockInfo, { paginationConfig: {} }); expect(mockGetERC20BalancesForAccountV2).toHaveBeenCalledTimes(1); expect(mockGetERC20BalancesForAccountV2).toHaveBeenCalledWith(mockAddress); expect(networkUtils.getERC20BalancesForAccount).not.toHaveBeenCalled(); }); it("should return a valid account shape", async () => { const result = await getAccountShape(mockInfo, { paginationConfig: {} }); expect(result).toMatchObject({ id: mockLiveAccountId, freshAddress: mockAddress, operations: [], operationsCount: 0, }); }); it("should pass cursor when initial account has existing operations (incremental sync)", async () => { const existingOp = { date: new Date("2024-01-01T00:00:00Z") }; // @ts-expect-error - no other fields are needed for this test const initialAccount = { syncHash: mockSyncHash, operations: [existingOp], pendingOperations: [], } as Account; await getAccountShape({ ...mockInfo, initialAccount }, { paginationConfig: {} }); expect(logic.listOperationsV2).toHaveBeenCalledTimes(1); expect(logic.listOperationsV2).toHaveBeenCalledWith( expect.objectContaining({ cursor: expect.any(String) }), ); }); it("should NOT pass cursor on fresh sync", async () => { await getAccountShape(mockInfo, { paginationConfig: {} }); expect(logic.listOperationsV2).toHaveBeenCalledTimes(1); expect(logic.listOperationsV2).toHaveBeenCalledWith( expect.not.objectContaining({ cursor: expect.any(String) }), ); }); it("should NOT pass cursor when syncHash has changed", async () => { mockGetSyncHash.mockResolvedValue("new-synchash"); // @ts-expect-error - no other fields are needed for this test const initialAccount = { syncHash: "old-synchash", operations: [{ date: new Date() }], pendingOperations: [], } as Account; await getAccountShape({ ...mockInfo, initialAccount }, { paginationConfig: {} }); expect(logic.listOperationsV2).toHaveBeenCalledTimes(1); expect(logic.listOperationsV2).toHaveBeenCalledWith( expect.not.objectContaining({ cursor: expect.any(String) }), ); }); }); describe("postSync", () => { beforeEach(() => { jest.clearAllMocks(); mockHederaConfig.getCoinConfig.mockReturnValue(mockConfig); }); it("should remove pending operations that match confirmed ERC20 operations", () => { const confirmedERC20Ops = [ getMockedOperation({ hash: "hash1", standard: "erc20" }), getMockedOperation({ hash: "hash2", standard: "erc20" }), ]; const initialAccount = {} as Account; const syncedAccount = getMockedAccount({ operations: [...confirmedERC20Ops, getMockedOperation({ hash: "otherHash" })], pendingOperations: [ getMockedOperation({ hash: "hash1" }), getMockedOperation({ hash: "hash2" }), getMockedOperation({ hash: "hash3" }), ], }); const result = postSync(initialAccount, syncedAccount); expect(result.pendingOperations).toHaveLength(1); expect(result.pendingOperations).toMatchObject([{ hash: "hash3" }]); }); it("should filter pending operations from subaccounts", () => { const mockToken1 = getMockedHTSTokenCurrency(); const confirmedERC20Ops = [ getMockedOperation({ hash: "hash1", standard: "erc20" }), getMockedOperation({ hash: "hash2", standard: "erc20" }), ]; const subAccounts = [ getMockedTokenAccount(mockToken1, { pendingOperations: [ getMockedOperation({ hash: "hash1" }), getMockedOperation({ hash: "hash4" }), ], }), getMockedTokenAccount(mockToken1, { pendingOperations: [ getMockedOperation({ hash: "hash2" }), getMockedOperation({ hash: "hash5" }), ], }), ]; const initialAccount = {} as Account; const syncedAccount = getMockedAccount({ operations: [...confirmedERC20Ops, getMockedOperation({ hash: "otherHash" })], subAccounts, }); const result = postSync(initialAccount, syncedAccount); expect(result.subAccounts).toHaveLength(2); expect(result.subAccounts).toMatchObject([ { pendingOperations: [{ hash: "hash4" }] }, { pendingOperations: [{ hash: "hash5" }] }, ]); }); });