@ledgerhq/coin-canton
Version:
835 lines (739 loc) • 27.6 kB
text/typescript
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([]);
});
});