UNPKG

@ledgerhq/coin-canton

Version:
835 lines (739 loc) 27.6 kB
import { AccountShapeInfo } from "@ledgerhq/ledger-wallet-framework/bridge/jsHelpers"; import { TokenAccount } from "@ledgerhq/types-live"; import BigNumber from "bignumber.js"; import * as accountBalance from "../common-logic/account/getBalance"; import * as config from "../config"; import * as gateway from "../network/gateway"; import { OperationInfo } from "../network/gateway"; import resolver from "../signer"; import { createMockCantonCurrency } from "../test/fixtures"; import { CantonAccount } from "../types"; import * as onboard from "./onboard"; import { makeGetAccountShape, filterDisabledTokenAccounts } from "./sync"; jest.mock("../network/gateway", () => ({ ...jest.requireActual("../network/gateway"), getLedgerEnd: jest.fn(), getOperations: jest.fn(), getPartyById: jest.fn(), getPendingTransferProposals: jest.fn(), getCalTokensCached: jest.fn(), getEnabledInstrumentsCached: jest.fn(), })); jest.mock("../signer"); jest.mock("../config"); jest.mock("./onboard"); jest.mock("../common-logic/account/getBalance"); const mockFindTokenByAddressInCurrency = jest.fn().mockResolvedValue(undefined); const mockFindTokenById = jest.fn().mockResolvedValue(undefined); jest.mock("@ledgerhq/ledger-wallet-framework/cryptoAssetsStore", () => ({ getCryptoAssetsStore: jest.fn(() => ({ findTokenByAddressInCurrency: mockFindTokenByAddressInCurrency, findTokenById: mockFindTokenById, })), })); const mockedGetBalance = accountBalance.getBalance as jest.Mock; const mockedGetLedgerEnd = gateway.getLedgerEnd as jest.Mock; const mockedGetOperations = gateway.getOperations as jest.Mock; const mockedGetPendingTransferProposals = gateway.getPendingTransferProposals as jest.Mock; const mockedGetPartyById = gateway.getPartyById as jest.Mock; const mockedGetCalTokensCached = gateway.getCalTokensCached as unknown as jest.Mock; const mockedGetEnabledInstrumentsCached = gateway.getEnabledInstrumentsCached as unknown as jest.Mock; const mockedResolver = resolver as jest.Mock; const mockedIsOnboarded = onboard.isAccountOnboarded as jest.Mock; const mockedIsAuthorized = onboard.isCantonCoinPreapproved as jest.Mock; const mockedCoinConfig = config.default.getCoinConfig as jest.Mock; const sampleCurrency = createMockCantonCurrency(); type CantonBalance = { value: bigint; locked: bigint; asset: { type: "Amulet" } | { type: "token"; assetReference: string }; utxoCount: number; instrumentId: string; adminId: string; }; const createMockNativeBalance = (amount: string, locked = false): CantonBalance => ({ value: BigInt(amount), locked: locked ? BigInt(amount) : BigInt(0), asset: { type: "Amulet" }, utxoCount: 1, instrumentId: "Amulet", adminId: "native-admin", }); const createMockOperationView = ( overrides: { instrumentId?: string; instrumentAdmin?: string | null; txHash?: string; uid?: string; type?: string; value?: string; operationType?: string; } = {}, ): OperationInfo => ({ transaction_hash: overrides.txHash ?? "tx-test", uid: overrides.uid ?? "uid-test", type: overrides.type ?? "Send", status: "Success", fee: { value: "5", asset: { type: "native" as const, instrumentAdmin: "AmuletAdmin", instrumentId: "Amulet", }, details: { type: "fee", }, }, transfers: [ { address: "party123", type: "Send", value: overrides.value ?? "100", asset: overrides.instrumentId ?? "Amulet", details: { operationType: overrides.operationType ?? "transfer", metadata: { reason: "test transfer", }, }, }, ], transaction_timestamp: new Date().toISOString(), senders: ["party123"], recipients: ["party456"], block: { height: 1, time: new Date().toISOString(), hash: "blockhash1", }, asset: { type: "native", instrumentId: overrides.instrumentId ?? "Amulet", instrumentAdmin: overrides.instrumentAdmin ?? "AmuletAdmin", }, details: { operationType: overrides.operationType ?? "transfer", }, }) as OperationInfo; const createMockCantonAccountShapeInfo = ( overrides: Partial<AccountShapeInfo<CantonAccount>> = {}, ): AccountShapeInfo<CantonAccount> => { const currency = createMockCantonCurrency(); return { address: "addr1", currency, derivationMode: "", derivationPath: "44'/0'/0'/0/0", deviceId: "fakeDevice", index: 0, initialAccount: undefined, ...overrides, }; }; describe("makeGetAccountShape", () => { const fakeSignerContext = {} as any; const defaultInfo: AccountShapeInfo<CantonAccount> = { address: "addr1", currency: sampleCurrency, derivationMode: "", derivationPath: "44'/0'/0'/0/0", deviceId: "fakeDevice", index: 0, initialAccount: undefined, }; beforeEach(() => { jest.clearAllMocks(); mockedResolver.mockReturnValue(async () => ({ publicKey: "FAKE_PUBLIC_KEY", })); mockedIsOnboarded.mockResolvedValue({ isOnboarded: true, partyId: "party123", }); mockedCoinConfig.mockReturnValue({ nativeInstrumentId: "Amulet", minReserve: "0", useGateway: true, }); mockedIsAuthorized.mockResolvedValue(true); mockedGetLedgerEnd.mockResolvedValue(12345); mockedGetPendingTransferProposals.mockResolvedValue([]); mockedGetPartyById.mockResolvedValue({ party_id: "test-party-id", public_key: "" }); mockedGetCalTokensCached.mockResolvedValue(new Map()); mockFindTokenByAddressInCurrency.mockResolvedValue(undefined); mockFindTokenById.mockResolvedValue(undefined); }); it("should return a valid account shape with correct balances and operations", async () => { mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]); mockedGetOperations.mockResolvedValue({ operations: [ createMockOperationView({ txHash: "tx1", uid: "uid1", type: "Send", value: "100", }), ], }); const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(defaultInfo, { paginationConfig: {}, }); expect(shape).toHaveProperty("id"); expect(shape.balance).toEqual(BigNumber(1000)); expect(shape.operations?.length).toBe(1); expect((shape.operations as any)[0].type).toBe("OUT"); expect((shape.operations as any)[0].value).toEqual(BigNumber(105)); // 100 + 5 fee expect(shape.spendableBalance).toEqual(BigNumber(1000)); expect(shape.used).toBe(true); }); it("should handle locked balances correctly", async () => { mockedGetBalance.mockResolvedValue([ createMockNativeBalance("1000", true), createMockNativeBalance("10", false), ]); const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(defaultInfo, { paginationConfig: {}, }); expect(shape).toMatchObject({ balance: BigNumber(1010), spendableBalance: BigNumber(10), }); }); it("should handle empty balances correctly", async () => { mockedGetBalance.mockResolvedValue([]); const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(defaultInfo, { paginationConfig: {}, }); expect(shape).toMatchObject({ balance: BigNumber(0), spendableBalance: BigNumber(0), }); }); it("should default to FEES operation type when transferValue is 0", async () => { mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]); mockedGetOperations.mockResolvedValue({ operations: [ createMockOperationView({ txHash: "tx2", uid: "uid2", type: "Send", value: "0", }), ], }); const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(defaultInfo, { paginationConfig: {}, }); expect(shape).toMatchObject({ operations: [ expect.objectContaining({ type: "FEES", // In this case, value should equal the fee value: BigNumber(5), // fee is 5 in createMockOperationView }), ], }); }); it("should set operation type to TRANSFER_PROPOSAL when operationType is transfer-proposal", async () => { mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]); mockedGetOperations.mockResolvedValue({ operations: [ createMockOperationView({ txHash: "tx3", uid: "uid3", type: "Send", value: "200", operationType: "transfer-proposal", }), ], }); const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(defaultInfo, { paginationConfig: {}, }); expect(shape).toMatchObject({ operations: [ expect.objectContaining({ type: "TRANSFER_PROPOSAL", value: BigNumber(200), // transfer value only, fees not added for TRANSFER_PROPOSAL }), ], }); }); it("should set operation type to TRANSFER_REJECTED when operationType is transfer-rejected", async () => { mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]); mockedGetOperations.mockResolvedValue({ operations: [ createMockOperationView({ txHash: "tx4", uid: "uid4", type: "Send", value: "150", operationType: "transfer-rejected", }), ], }); const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape: any = await getAccountShape(defaultInfo, { paginationConfig: {}, }); expect(shape).toMatchObject({ operations: [ expect.objectContaining({ type: "TRANSFER_REJECTED", value: BigNumber(150), // transfer value only, fees not added for TRANSFER_REJECTED }), ], }); }); it("should set operation type to TRANSFER_WITHDRAWN when operationType is transfer-withdrawn", async () => { mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]); mockedGetOperations.mockResolvedValue({ operations: [ createMockOperationView({ txHash: "tx5", uid: "uid5", type: "Send", value: "50", operationType: "transfer-withdrawn", }), ], }); const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(defaultInfo, { paginationConfig: {}, }); expect(shape).toMatchObject({ operations: [ expect.objectContaining({ type: "TRANSFER_WITHDRAWN", value: BigNumber(50), // transfer value only, fees not added for TRANSFER_WITHDRAWN }), ], }); }); it("should filter out operations that match pending transfer proposals", async () => { mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]); mockedGetPendingTransferProposals.mockResolvedValue([ { contract_id: "pending-proposal-uid", sender: "sender-party", receiver: "test-party-id", amount: "100", instrument_id: "Amulet", instrument_admin: "native-admin", memo: "Test proposal", expires_at_micros: Date.now() + 100000, update_id: "tx-pending", }, ]); mockedGetOperations.mockResolvedValue({ operations: [ createMockOperationView({ txHash: "tx-pending", uid: "pending-proposal-uid", type: "Receive", value: "100", }), createMockOperationView({ txHash: "tx-completed", uid: "completed-uid", type: "Receive", value: "200", }), ], }); const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(defaultInfo, { paginationConfig: {}, }); expect(shape).toMatchObject({ operations: [expect.objectContaining({ hash: "tx-completed", value: BigNumber(200) })], cantonResources: expect.objectContaining({ pendingTransferProposals: [ expect.objectContaining({ contract_id: "pending-proposal-uid" }), ], }), }); }); it("should expose token transfer proposals on parent account so the offer UI is reachable from the main view", async () => { // GIVEN // a pending token offer addressed to the user const tokenAdminId = "token-admin-party::1220abc"; const tokenInstrumentId = "cbtc-instrument-id"; const cbtcToken = { type: "TokenCurrency" as const, id: "canton_network/cip56/cbtc", contractAddress: tokenAdminId, parentCurrencyId: "canton_network", tokenType: "cip56", name: "CBTC", ticker: "CBTC", delisted: false, disableCountervalue: false, units: [{ name: "CBTC", code: "CBTC", magnitude: 8 }], }; mockedGetCalTokensCached.mockResolvedValue( new Map([["canton_network/cip56/cbtc", tokenInstrumentId]]), ); mockFindTokenById.mockImplementation(async (id: string) => id === "canton_network/cip56/cbtc" ? cbtcToken : undefined, ); mockedGetEnabledInstrumentsCached.mockResolvedValue( new Set([`${tokenInstrumentId}____${tokenAdminId}`]), ); // No native balance, no token balance — the sub-account exists only because of the pending offer mockedGetBalance.mockResolvedValue([]); mockedGetOperations.mockResolvedValue({ operations: [] }); mockedGetPendingTransferProposals.mockResolvedValue([ { contract_id: "cbtc-offer-1", sender: "faucet-party", receiver: "test-party-id", amount: "1", instrument_id: tokenInstrumentId, instrument_admin: tokenAdminId, memo: "Faucet drop", expires_at_micros: (Date.now() + 100000) * 1000, update_id: "update-cbtc-1", }, ]); // WHEN // we sync the account const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(defaultInfo, { paginationConfig: {} }); // THEN // the proposal lands on both the parent account and the token sub-account expect(shape.cantonResources?.pendingTransferProposals).toEqual([ expect.objectContaining({ contract_id: "cbtc-offer-1", instrument_id: tokenInstrumentId }), ]); const subAccount = (shape.subAccounts as TokenAccount[] | undefined)?.[0] as | (TokenAccount & { cantonResources?: { pendingTransferProposals: unknown[] } }) | undefined; expect(subAccount?.token.ticker).toBe("CBTC"); expect(subAccount?.cantonResources?.pendingTransferProposals).toEqual([ expect.objectContaining({ contract_id: "cbtc-offer-1" }), ]); }); it("should sync without device when account has xpub but no publicKey", async () => { mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]); mockedGetOperations.mockResolvedValue({ operations: [createMockOperationView()], }); const infoWithXpub = createMockCantonAccountShapeInfo({ initialAccount: { xpub: "test-party-id", cantonResources: { instrumentUtxoCounts: {}, pendingTransferProposals: [], }, } as unknown as CantonAccount, }); delete infoWithXpub.deviceId; const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(infoWithXpub, { paginationConfig: {} }); expect(shape).toHaveProperty("id"); expect(shape.xpub).toBe("test-party-id"); // Should not call getAddress since we have xpub expect(mockedResolver).not.toHaveBeenCalled(); }); it("backfills publicKey from the gateway when account has xpub but no publicKey (LIVE-34585)", async () => { mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]); mockedGetOperations.mockResolvedValue({ operations: [createMockOperationView()] }); mockedGetPartyById.mockResolvedValue({ party_id: "test-party-id", public_key: "backfilled-public-key", }); const infoWithXpub = createMockCantonAccountShapeInfo({ initialAccount: { xpub: "test-party-id", cantonResources: { isOnboarded: true, instrumentUtxoCounts: {}, pendingTransferProposals: [], }, } as unknown as CantonAccount, }); delete infoWithXpub.deviceId; const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(infoWithXpub, { paginationConfig: {} }); // Deviceless backfill: publicKey now resolves, so validateTopology can run. expect(mockedResolver).not.toHaveBeenCalled(); expect(mockedGetPartyById).toHaveBeenCalledWith(sampleCurrency, "test-party-id"); expect(shape.cantonResources?.publicKey).toBe("backfilled-public-key"); }); it("should sync without device when account has publicKey but no xpub", async () => { mockedGetBalance.mockResolvedValue([]); // Empty balances since no xpub mockedGetOperations.mockResolvedValue({ operations: [], }); const infoWithPublicKey = createMockCantonAccountShapeInfo({ initialAccount: { xpub: "", // Missing xpub cantonResources: { publicKey: "test-public-key", instrumentUtxoCounts: {}, pendingTransferProposals: [], }, } as unknown as CantonAccount, }); delete infoWithPublicKey.deviceId; const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(infoWithPublicKey, { paginationConfig: {} }); expect(shape).toHaveProperty("id"); // Should not call getAddress since we have publicKey (even though xpub is missing) expect(mockedResolver).not.toHaveBeenCalled(); }); it("should sync without device when account has both xpub and publicKey", async () => { mockedGetBalance.mockResolvedValue([createMockNativeBalance("1000")]); mockedGetOperations.mockResolvedValue({ operations: [createMockOperationView()], }); const infoWithBoth = createMockCantonAccountShapeInfo({ initialAccount: { xpub: "test-party-id", cantonResources: { publicKey: "test-public-key", instrumentUtxoCounts: {}, pendingTransferProposals: [], }, } as unknown as CantonAccount, }); delete infoWithBoth.deviceId; const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(infoWithBoth, { paginationConfig: {} }); expect(shape).toHaveProperty("id"); expect(shape.xpub).toBe("test-party-id"); // Should not call getAddress since we have both values expect(mockedResolver).not.toHaveBeenCalled(); }); it("should correctly identify tokens with same adminId but different instrumentId", async () => { const sharedAdminId = "party-28dc4516-b5ca-44ff-86c7-2107e90a6807::1220b8301e18aa8a401d6e34e6c20f8b0243183c514373bca8f1b6b9270246341a9e"; const sbcInstrumentId = "f29bdd7a-1469-498a-ba2a-796bf5387b31"; const cusdInstrumentId = "481871d4-ca56-42a8-b2d3-4b7d28742946"; // Create token mocks const sbcToken = { type: "TokenCurrency" as const, id: "canton_network/cip56/sbc", contractAddress: sharedAdminId, parentCurrencyId: "canton_network", tokenType: "cip56", name: "SBC", ticker: "SBC", delisted: false, disableCountervalue: false, units: [{ name: "SBC", code: "SBC", magnitude: 38 }], }; const cusdToken = { type: "TokenCurrency" as const, id: "canton_network/cip56/cusd", contractAddress: sharedAdminId, parentCurrencyId: "canton_network", tokenType: "cip56", name: "CUSD", ticker: "CUSD", delisted: false, disableCountervalue: false, units: [{ name: "CUSD", code: "CUSD", magnitude: 38 }], }; // Mock CAL tokens map (token_identifier -> token id) mockedGetCalTokensCached.mockResolvedValue( new Map([ ["canton_network/cip56/sbc", sbcInstrumentId], ["canton_network/cip56/cusd", cusdInstrumentId], ]), ); // Mock findTokenById to return correct tokens mockFindTokenById.mockImplementation(async (id: string) => { if (id === "canton_network/cip56/sbc") return sbcToken; if (id === "canton_network/cip56/cusd") return cusdToken; return undefined; }); // Mock enabled instruments to include SBC (using SEPARATOR format) mockedGetEnabledInstrumentsCached.mockResolvedValue( new Set([`${sbcInstrumentId}____${sharedAdminId}`]), ); // Mock balances with SBC token (NOT CUSD) mockedGetBalance.mockResolvedValue([ createMockNativeBalance("1000"), { value: BigInt("990000000000000000000000000000000"), locked: BigInt(0), asset: { type: "token", assetReference: sbcInstrumentId }, utxoCount: 1, instrumentId: sbcInstrumentId, adminId: sharedAdminId, }, ]); mockedGetOperations.mockResolvedValue({ operations: [], }); const getAccountShape = makeGetAccountShape(fakeSignerContext); const shape = await getAccountShape(defaultInfo, { paginationConfig: {}, }); expect(shape).toMatchObject({ subAccounts: [ expect.objectContaining({ balance: BigNumber("990000000000000000000000000000000"), type: "TokenAccount", token: expect.objectContaining({ id: "canton_network/cip56/sbc", ticker: "SBC", // Should be SBC, not CUSD! }), }), ], }); }); }); describe("filterDisabledTokenAccounts", () => { const currency = createMockCantonCurrency(); const createMockTokenAccount = (contractAddress: string, tokenId?: string): TokenAccount => ({ type: "TokenAccount", id: `token-account-${contractAddress}`, parentId: "parent-account-id", token: { type: "TokenCurrency", id: tokenId ?? `token-id-${contractAddress}`, contractAddress, name: "Test Token", ticker: "TEST", decimals: 18, parentCurrencyId: "canton_network", } as any, balance: new BigNumber(100), spendableBalance: new BigNumber(100), operationsCount: 0, operations: [], pendingOperations: [], balanceHistoryCache: { HOUR: { latestDate: null, balances: [] }, DAY: { latestDate: null, balances: [] }, WEEK: { latestDate: null, balances: [] }, }, swapHistory: [], creationDate: new Date(), }); beforeEach(() => { jest.clearAllMocks(); }); it("should return empty array when subAccounts is undefined", async () => { const calTokens = new Map<string, string>(); const result = await filterDisabledTokenAccounts(currency, undefined, calTokens); expect(result).toEqual([]); expect(mockedGetEnabledInstrumentsCached).not.toHaveBeenCalled(); }); it("should return empty array when subAccounts is empty", async () => { const calTokens = new Map<string, string>(); const result = await filterDisabledTokenAccounts(currency, [], calTokens); expect(result).toEqual([]); expect(mockedGetEnabledInstrumentsCached).not.toHaveBeenCalled(); }); it("should filter out disabled token accounts", async () => { const enabledAdminId = "0xenabled"; const disabledAdminId = "0xdisabled"; const enabledTokenId = "token-id-enabled"; const disabledTokenId = "token-id-disabled"; const enabledInstrumentId = "instrument-enabled"; const disabledInstrumentId = "instrument-disabled"; const enabledTokenAccount = createMockTokenAccount(enabledAdminId, enabledTokenId); const disabledTokenAccount = createMockTokenAccount(disabledAdminId, disabledTokenId); // calTokens maps token.id -> instrumentId const calTokens = new Map<string, string>([ [enabledTokenId, enabledInstrumentId], [disabledTokenId, disabledInstrumentId], ]); // enabledInstruments is a Set of keys in format "instrumentId____adminId" mockedGetEnabledInstrumentsCached.mockResolvedValue( new Set([`${enabledInstrumentId}____${enabledAdminId}`]), ); const result = await filterDisabledTokenAccounts( currency, [enabledTokenAccount, disabledTokenAccount], calTokens, ); expect(result).toHaveLength(1); expect(result[0]).toBe(enabledTokenAccount); expect(mockedGetEnabledInstrumentsCached).toHaveBeenCalledWith(currency); }); it("should keep enabled token accounts", async () => { const adminId1 = "0xenabled1"; const adminId2 = "0xenabled2"; const tokenId1 = "token-id-1"; const tokenId2 = "token-id-2"; const instrumentId1 = "instrument-1"; const instrumentId2 = "instrument-2"; const enabledTokenAccount1 = createMockTokenAccount(adminId1, tokenId1); const enabledTokenAccount2 = createMockTokenAccount(adminId2, tokenId2); const calTokens = new Map<string, string>([ [tokenId1, instrumentId1], [tokenId2, instrumentId2], ]); mockedGetEnabledInstrumentsCached.mockResolvedValue( new Set([`${instrumentId1}____${adminId1}`, `${instrumentId2}____${adminId2}`]), ); const result = await filterDisabledTokenAccounts( currency, [enabledTokenAccount1, enabledTokenAccount2], calTokens, ); expect(result).toHaveLength(2); expect(result).toContain(enabledTokenAccount1); expect(result).toContain(enabledTokenAccount2); }); it("should not keep token accounts without contractAddress", async () => { const tokenAccountWithoutAddress = { ...createMockTokenAccount("0xtest", "token-id-noaddr"), token: { ...createMockTokenAccount("0xtest", "token-id-noaddr").token, contractAddress: "", }, }; const enabledAdminId = "0xenabled"; const enabledTokenId = "token-id-enabled"; const enabledInstrumentId = "instrument-enabled"; const enabledTokenAccount = createMockTokenAccount(enabledAdminId, enabledTokenId); const calTokens = new Map<string, string>([ ["token-id-noaddr", "instrument-noaddr"], [enabledTokenId, enabledInstrumentId], ]); mockedGetEnabledInstrumentsCached.mockResolvedValue( new Set([`${enabledInstrumentId}____${enabledAdminId}`, "instrument-noaddr____"]), ); const result = await filterDisabledTokenAccounts( currency, [tokenAccountWithoutAddress, enabledTokenAccount], calTokens, ); expect(result).toHaveLength(1); expect(result[0]).toBe(enabledTokenAccount); expect(result).not.toContain(tokenAccountWithoutAddress); }); it("should handle empty enabled instruments list", async () => { const adminId1 = "0xtoken1"; const adminId2 = "0xtoken2"; const tokenId1 = "token-id-1"; const tokenId2 = "token-id-2"; const tokenAccount1 = createMockTokenAccount(adminId1, tokenId1); const tokenAccount2 = createMockTokenAccount(adminId2, tokenId2); const calTokens = new Map<string, string>([ [tokenId1, "instrument-1"], [tokenId2, "instrument-2"], ]); mockedGetEnabledInstrumentsCached.mockResolvedValue(new Set()); const result = await filterDisabledTokenAccounts( currency, [tokenAccount1, tokenAccount2], calTokens, ); expect(result).toEqual([]); }); });