@ledgerhq/coin-filecoin
Version:
Ledger Filecoin Coin integration
102 lines (85 loc) • 3.74 kB
text/typescript
import { fetchBalances, fetchERC20TokenBalance } from "../../network/api";
import { getBalance } from "./getBalance";
jest.mock("../../network/api");
jest.mock("@ledgerhq/logs");
// Mock convertAddressFilToEth so synthetic addresses can drive both code paths:
// when the address starts with "f410f" -> returns a fake ETH equivalent,
// otherwise -> throws (forcing the native-only fallback branch).
jest.mock("../../network/addresses", () => ({
convertAddressFilToEth: (addr: string) => {
if (addr.startsWith("f410f")) return "0x" + addr.slice(5);
throw new Error("not convertible");
},
}));
const mockedFetchBalances = fetchBalances as jest.MockedFunction<typeof fetchBalances>;
const mockedFetchERC20Balance = fetchERC20TokenBalance as jest.MockedFunction<
typeof fetchERC20TokenBalance
>;
describe("getBalance", () => {
beforeEach(() => jest.clearAllMocks());
it("returns native balance entry from API response", async () => {
mockedFetchBalances.mockResolvedValueOnce({
total_balance: "5000000000000000000",
spendable_balance: "4000000000000000000",
locked_balance: "1000000000000000000",
});
const result = await getBalance("f1abjxfbp274xpdqcpuaykwkfb43omjotacm2p3za");
expect(result).toHaveLength(1);
expect(result[0].asset).toEqual({ type: "native" });
expect(result[0].value).toBe(5_000_000_000_000_000_000n);
expect(result[0].locked).toBe(1_000_000_000_000_000_000n);
});
it("uses 0n as locked_balance fallback when field is absent", async () => {
mockedFetchBalances.mockResolvedValueOnce({
total_balance: "1000",
spendable_balance: "1000",
locked_balance: undefined as unknown as string,
});
const result = await getBalance("f1abjxfbp274xpdqcpuaykwkfb43omjotacm2p3za");
expect(result[0].locked).toBe(0n);
});
it("does not call fetchERC20TokenBalance when no tokens registered", async () => {
mockedFetchBalances.mockResolvedValueOnce({
total_balance: "0",
spendable_balance: "0",
locked_balance: "0",
});
await getBalance("f1abjxfbp274xpdqcpuaykwkfb43omjotacm2p3za");
expect(mockedFetchERC20Balance).not.toHaveBeenCalled();
});
it("returns token balance entries (lowercase assetReference) when tokenContracts provided", async () => {
mockedFetchBalances.mockResolvedValueOnce({
total_balance: "100",
spendable_balance: "100",
locked_balance: "0",
});
mockedFetchERC20Balance.mockResolvedValueOnce("0");
mockedFetchERC20Balance.mockResolvedValueOnce("42");
const result = await getBalance("f410fkkld55ioe7qg24wvt7fu6pbknb56ht7ptloy", [
"0xABCDEF",
"0xDEADBEEF",
]);
expect(result).toHaveLength(3);
const tokens = result.filter(b => b.asset.type === "erc20");
expect(tokens).toHaveLength(2);
// zero-balance entries must be included (specialized task constraint)
expect(tokens[0].value).toBe(0n);
expect(tokens[1].value).toBe(42n);
// assetReference normalized to lowercase
tokens.forEach(t => {
expect((t.asset as { assetReference: string }).assetReference).toMatch(/^0x[a-f0-9]+$/);
});
});
it("falls back to native-only when address cannot be converted to ETH form", async () => {
mockedFetchBalances.mockResolvedValueOnce({
total_balance: "100",
spendable_balance: "100",
locked_balance: "0",
});
// f1 SECP256K1 addresses are not convertAddressFilToEth-compatible — should fall back
const result = await getBalance("f1abjxfbp274xpdqcpuaykwkfb43omjotacm2p3za", ["0xabcdef"]);
expect(result).toHaveLength(1);
expect(result[0].asset.type).toBe("native");
expect(mockedFetchERC20Balance).not.toHaveBeenCalled();
});
});