@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
177 lines (151 loc) • 6.2 kB
text/typescript
import BigNumber from "bignumber.js";
import { genericGetAccountShape } from "../getAccountShape";
jest.mock("@ledgerhq/coin-framework/account/index", () => ({
encodeAccountId: jest.fn(() => "accId"),
}));
const mergeOpsMock = jest.fn();
jest.mock("@ledgerhq/coin-framework/bridge/jsHelpers", () => ({
mergeOps: (...args: any[]) => mergeOpsMock(...args),
}));
const listOperationsMock = jest.fn();
const getBalanceMock = jest.fn();
const lastBlockMock = jest.fn();
const getTokenFromAssetMock = jest.fn();
const chainSpecificGetAccountShapeMock = jest.fn();
jest.mock("../alpaca", () => ({
getAlpacaApi: () => ({
lastBlock: (...a: any[]) => lastBlockMock(...a),
getBalance: (...a: any[]) => getBalanceMock(...a),
listOperations: (...a: any[]) => listOperationsMock(...a),
getTokenFromAsset: (...a: any[]) => getTokenFromAssetMock(...a),
getChainSpecificRules: () => ({
getAccountShape: (...a: any[]) => chainSpecificGetAccountShapeMock(...a),
}),
}),
}));
const adaptCoreOperationToLiveOperationMock = jest.fn();
const extractBalanceMock = jest.fn();
jest.mock("../utils", () => ({
adaptCoreOperationToLiveOperation: (...a: any[]) => adaptCoreOperationToLiveOperationMock(...a),
extractBalance: (...a: any[]) => extractBalanceMock(...a),
}));
const inferSubOperationsMock = jest.fn();
jest.mock("@ledgerhq/coin-framework/serialization", () => ({
inferSubOperations: (...a: any[]) => inferSubOperationsMock(...a),
}));
const buildSubAccountsMock = jest.fn();
jest.mock("../buildSubAccounts", () => ({
buildSubAccounts: (...a: any[]) => buildSubAccountsMock(...a),
}));
// Test matrix for Stellar & XRP
const chains = [
{ currency: { id: "stellar", name: "Stellar" }, network: "testnet" },
{ currency: { id: "ripple", name: "XRP" }, network: "mainnet" },
{ currency: { id: "tezos", name: "Tezos" }, network: "mainnet" },
];
describe("genericGetAccountShape (stellar & xrp)", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe.each(chains)("$currency.id", ({ currency, network }) => {
test("builds account shape with existing operations, pagination and sub accounts", async () => {
const oldOp = {
hash: "h1",
blockHeight: 10,
type: "OPT_IN",
extra: { pagingToken: "pt1", assetReference: "ar1", assetOwner: "ow1" },
};
const initialAccount = { operations: [oldOp], blockHeight: 10 };
extractBalanceMock.mockReturnValue({ value: "1000", locked: "300" });
getBalanceMock.mockResolvedValue([
{ asset: { type: "native" }, value: "1000", locked: "300" },
{ asset: { type: "token", symbol: "TOK1" }, value: "42" },
{ asset: { type: "token", symbol: "TOK_IGNORE" }, value: "5" },
]);
getTokenFromAssetMock.mockImplementation(asset =>
asset.symbol === "TOK1" ? { id: `${currency.id}_token1` } : null,
);
const coreOp = { hash: "h2", height: 12 };
listOperationsMock.mockResolvedValue([[coreOp]]);
adaptCoreOperationToLiveOperationMock.mockImplementation((_accId, op) => ({
hash: op.hash,
type: "IN",
blockHeight: 12,
extra: { assetReference: "ar2", assetOwner: "ow2" },
}));
mergeOpsMock.mockImplementation((oldOps, newOps) => [...newOps, ...oldOps]);
buildSubAccountsMock.mockReturnValue([
{ id: `${currency.id}_subAcc1`, type: "TokenAccount" },
]);
inferSubOperationsMock.mockImplementation(hash =>
hash === "h2" ? [{ id: `${currency.id}_subOp1` }] : [],
);
lastBlockMock.mockResolvedValue({ height: 123 });
const getShape = genericGetAccountShape(network, currency.id);
const result = await getShape(
{
address: `${currency.id}_addr1`,
initialAccount,
currency,
derivationMode: "",
} as any,
{ paginationConfig: {} as any },
);
expect(chainSpecificGetAccountShapeMock).toHaveBeenCalledWith(`${currency.id}_addr1`);
expect(listOperationsMock).toHaveBeenCalledWith(`${currency.id}_addr1`, {
minHeight: 11,
order: "asc",
lastPagingToken: "pt1",
});
const assetsBalancePassed = buildSubAccountsMock.mock.calls[0][0].allTokenAssetsBalances;
expect(assetsBalancePassed).toEqual([
{ asset: { symbol: "TOK1", type: "token" }, value: "42" },
{ asset: { symbol: "TOK_IGNORE", type: "token" }, value: "5" },
]);
const assetOpsPassed = buildSubAccountsMock.mock.calls[0][0].operations;
expect(assetOpsPassed).toHaveLength(1);
expect(assetOpsPassed[0].hash).toBe("h2");
expect(result).toMatchObject({
balance: new BigNumber(1000),
spendableBalance: new BigNumber(700),
blockHeight: 123,
operationsCount: 2,
subAccounts: [{ id: `${currency.id}_subAcc1`, type: "TokenAccount" }],
operations: [
{
hash: "h2",
type: "IN",
blockHeight: 12,
subOperations: [{ id: `${currency.id}_subOp1` }],
extra: { assetReference: "ar2", assetOwner: "ow2" },
},
oldOp,
],
});
});
test("handles empty operations (no old ops, no new ops) and blockHeight=0", async () => {
getBalanceMock.mockResolvedValue([{ asset: { type: "native" }, value: "0", locked: "0" }]);
extractBalanceMock.mockReturnValue({ value: "0", locked: "0" });
listOperationsMock.mockResolvedValue([[]]);
buildSubAccountsMock.mockReturnValue([]);
const getShape = genericGetAccountShape(network, currency.id);
const result = await getShape(
{
address: `${currency.id}_addr2`,
initialAccount: undefined,
currency,
derivationMode: "",
} as any,
{ paginationConfig: {} as any },
);
expect(result).toMatchObject({
operations: [], // Empty array check for `operations`
blockHeight: 0,
operationsCount: 0,
subAccounts: [], // Empty array check for `subAccounts`
balance: new BigNumber(0),
spendableBalance: new BigNumber(0),
});
});
});
});