UNPKG

@ledgerhq/coin-filecoin

Version:
175 lines (157 loc) 5.83 kB
import { fetchEstimatedFees } from "../../network/api"; import { craftTransaction } from "./craftTransaction"; import { combine } from "./combine"; jest.mock("../../network/api"); jest.mock("@ledgerhq/logs"); // Mock the iso-filecoin-backed validator so synthetic test addresses pass. jest.mock("../../network/addresses", () => ({ validateAddress: (input: string) => ({ isValid: input !== "INVALID", parsedAddress: { toString: () => input }, }), })); jest.mock("../../erc20/tokenAccounts", () => ({ abiEncodeTransferParams: (_recipient: string, _amount: string) => "0xencoded", encodeTxnParams: (_data: string) => "encoded-params", })); // Mock iso-filecoin/message Message so we don't hit real address checksum // validation inside serialize() — that is covered by integration tests, not units. jest.mock("iso-filecoin/message", () => ({ Message: class { version: number; to: string; from: string; nonce: number; value: string; gasLimit: number; gasFeeCap: string; gasPremium: string; method: number; params: string | undefined; constructor(msg: Record<string, unknown>) { this.version = (msg.version as number) ?? 0; this.to = msg.to as string; this.from = msg.from as string; this.nonce = msg.nonce as number; this.value = msg.value as string; this.gasLimit = msg.gasLimit as number; this.gasFeeCap = msg.gasFeeCap as string; this.gasPremium = msg.gasPremium as string; this.method = msg.method as number; this.params = msg.params as string | undefined; } serialize(): Uint8Array { return new Uint8Array([0xde, 0xad, 0xbe, 0xef]); } }, })); const mockedFetch = fetchEstimatedFees as jest.MockedFunction<typeof fetchEstimatedFees>; function mockFees() { mockedFetch.mockResolvedValue({ gas_limit: 1_000_000, gas_fee_cap: "150000", gas_premium: "125000", nonce: 5, }); } describe("craftTransaction", () => { beforeEach(() => { jest.clearAllMocks(); mockFees(); }); it("native intent produces a base64 string that combine can parse", async () => { const intent = { intentType: "transaction" as const, type: "send" as const, sender: "f1abjxfbp274xpdqcpuaykwkfb43omjotacm2p3za", recipient: "f1z4nykg7q6q5qnxs7h4zknhlqbqhq5jxcqm5qw4y", amount: 1_000_000_000_000_000_000n, asset: { type: "native" as const }, useAllAmount: false, }; const crafted = await craftTransaction(intent); expect(typeof crafted.transaction).toBe("string"); // Round-trip: combine should produce a parseable BroadcastTransactionRequest const combined = combine(crafted.transaction, ["c2ln"]); const parsed = JSON.parse(combined); expect(parsed.message.nonce).toBe(5); expect(parsed.message.value).toBe("1000000000000000000"); expect(parsed.message.method).toBe(0); }); it("throws on unknown asset type (no silent native fallback)", async () => { const intent = { intentType: "transaction" as const, type: "send" as const, sender: "f1abjxfbp274xpdqcpuaykwkfb43omjotacm2p3za", recipient: "f1z4nykg7q6q5qnxs7h4zknhlqbqhq5jxcqm5qw4y", amount: 100n, asset: { type: "spl-token" as unknown as "native" }, useAllAmount: false, }; await expect(craftTransaction(intent)).rejects.toThrow(/Unsupported asset type/); }); it("fetches nonce at craft time (via fetchEstimatedFees)", async () => { const intent = { intentType: "transaction" as const, type: "send" as const, sender: "f1abjxfbp274xpdqcpuaykwkfb43omjotacm2p3za", recipient: "f1z4nykg7q6q5qnxs7h4zknhlqbqhq5jxcqm5qw4y", amount: 1_000n, asset: { type: "native" as const }, useAllAmount: false, }; await craftTransaction(intent); // fetchEstimatedFees is called at least once: once for getNextSequence, once for gas expect(mockedFetch).toHaveBeenCalledTimes(2); }); it("uses custom fees when provided (skips fetchEstimatedFees for gas)", async () => { const intent = { intentType: "transaction" as const, type: "send" as const, sender: "f1sender", recipient: "f1recipient", amount: 1_000n, asset: { type: "native" as const }, useAllAmount: false, }; const customFees = { value: 100n, parameters: { gasFeeCap: "200", gasLimit: "500000", gasPremium: "100" }, }; const crafted = await craftTransaction(intent, customFees); const parsed = JSON.parse(crafted.transaction); expect(parsed.message.gasFeeCap).toBe("200"); expect(parsed.message.gasLimit).toBe(500000); expect(parsed.message.gasPremium).toBe("100"); // Only 1 call for nonce, none for gas expect(mockedFetch).toHaveBeenCalledTimes(1); }); it("crafts ERC-20 token transfer with InvokeEVM method", async () => { const intent = { intentType: "transaction" as const, type: "send" as const, sender: "f1sender", recipient: "0xrecipient", amount: 5_000n, asset: { type: "erc20" as const, assetReference: "0xcontract" }, useAllAmount: false, }; const crafted = await craftTransaction(intent); const parsed = JSON.parse(crafted.transaction); expect(parsed.message.method).toBe(3844450837); expect(parsed.message.value).toBe("0"); expect(parsed.message.params).toBe("encoded-params"); }); it("throws on invalid sender address", async () => { const intent = { intentType: "transaction" as const, type: "send" as const, sender: "INVALID", recipient: "f1recipient", amount: 1_000n, asset: { type: "native" as const }, useAllAmount: false, }; await expect(craftTransaction(intent)).rejects.toThrow(/Invalid sender address/); }); });