@ledgerhq/coin-filecoin
Version:
Ledger Filecoin Coin integration
175 lines (157 loc) • 5.83 kB
text/typescript
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/);
});
});