UNPKG

@ledgerhq/live-common

Version:
210 lines (167 loc) 7.1 kB
import { from } from "rxjs"; import calService, { convertCertificateToDeviceData } from "@ledgerhq/ledger-cal-service"; import { DmkSignerHyperliquid } from "@ledgerhq/live-signer-hyperliquid"; import { handlers, type PerpsSignParams, type PerpsSignResult } from "./server"; import { getMainAccount, getParentAccount } from "../../account"; import { withDevice } from "../../hw/deviceAccess"; import { isDmkTransport } from "../../hw/dmkUtils"; import { getAccountIdFromWalletAccountId } from "../converters"; import { createFixtureAccount } from "../../mock/fixtures/cryptoCurrencies"; jest.mock("@ledgerhq/wallet-api-server", () => ({ customWrapper: jest.fn(handler => handler), })); jest.mock("@ledgerhq/wallet-api-core", () => ({ ...jest.requireActual("@ledgerhq/wallet-api-core"), createAccountNotFound: jest.fn(id => ({ code: "AccountNotFound", id })), createUnknownError: jest.fn(opts => ({ code: "UnknownError", ...opts })), ServerError: class ServerError extends Error { constructor(public error: unknown) { super("ServerError"); } }, })); jest.mock("@ledgerhq/ledger-cal-service", () => ({ __esModule: true, default: { getCertificate: jest.fn() }, convertCertificateToDeviceData: jest.fn(), })); jest.mock("@ledgerhq/live-signer-hyperliquid", () => ({ DmkSignerHyperliquid: jest.fn(), })); jest.mock("../converters", () => ({ getAccountIdFromWalletAccountId: jest.fn(), })); jest.mock("../../account", () => ({ getMainAccount: jest.fn(), getParentAccount: jest.fn(), })); jest.mock("../../hw/deviceAccess", () => ({ withDevice: jest.fn(), })); jest.mock("../../hw/dmkUtils", () => ({ isDmkTransport: jest.fn(), })); // ─── Fixtures ──────────────────────────────────────────────────────────────── const mockAccount = createFixtureAccount("abcd"); const ACCOUNT_ID = mockAccount.id; const WALLET_ACCOUNT_ID = `wallet:${ACCOUNT_ID}`; const DERIVATION_PATH = mockAccount.freshAddressPath; const DEVICE_ID = "mock-device-id"; const mockCertificate = { descriptor: "aabbcc", signature: "ddeeff" }; const mockDeviceData = new Uint8Array([0xaa, 0xbb, 0xcc, 0x15, 0x03, 0xdd, 0xee, 0xff]); const mockDevice = { modelId: "stax", deviceId: DEVICE_ID, deviceName: undefined }; const baseParams = { accountId: WALLET_ACCOUNT_ID, metadataWithSignature: "0102030405", actions: [{ action: { type: "cancel" as const, cancels: [{ a: 1, o: 42 }] }, nonce: 1 }], }; // ─── Tests ─────────────────────────────────────────────────────────────────── describe("Perps handlers", () => { type MockedHandlers = { "custom.perps.signActions": (params?: PerpsSignParams) => Promise<PerpsSignResult>; }; let mockSignActions: jest.Mock; let mockUiDeviceSelect: jest.Mock; let serverHandlers: MockedHandlers; beforeEach(() => { jest.clearAllMocks(); // Converters jest.mocked(getAccountIdFromWalletAccountId).mockReturnValue(ACCOUNT_ID); // Coin-framework jest.mocked(getParentAccount).mockReturnValue(mockAccount); jest.mocked(getMainAccount).mockReturnValue(mockAccount as never); // CAL service jest.mocked(calService.getCertificate).mockResolvedValue(mockCertificate); jest.mocked(convertCertificateToDeviceData).mockReturnValue(mockDeviceData); // DMK signer mockSignActions = jest.fn(); jest .mocked(DmkSignerHyperliquid) .mockImplementation(() => ({ signActions: mockSignActions }) as never); // deviceAccess + dmkUtils jest.mocked(isDmkTransport).mockReturnValue(true); jest .mocked(withDevice) .mockReturnValue(job => from(job({ dmk: {}, sessionId: "session-1" } as never))); // UI hook mockUiDeviceSelect = jest .fn() .mockImplementation(({ onSuccess }) => onSuccess({ device: mockDevice })); serverHandlers = handlers({ accounts: [mockAccount], uiHooks: { "device.select": mockUiDeviceSelect }, }) as unknown as MockedHandlers; }); describe("custom.perps.signActions", () => { it("should return signatures on success", async () => { // GIVEN const expectedSignatures = [{ r: "0xr", s: "0xs", v: 27 }]; mockSignActions.mockResolvedValue(expectedSignatures); // WHEN const result = await serverHandlers["custom.perps.signActions"](baseParams); // THEN expect(result).toEqual({ signatures: expectedSignatures }); }); it("should call calService.getCertificate with the device modelId", async () => { // GIVEN mockSignActions.mockResolvedValue([]); // WHEN await serverHandlers["custom.perps.signActions"](baseParams); // THEN expect(calService.getCertificate).toHaveBeenCalledWith(mockDevice.modelId, "perps_data"); }); it("should call signActions with the correct parameters", async () => { // GIVEN mockSignActions.mockResolvedValue([]); // WHEN await serverHandlers["custom.perps.signActions"](baseParams); // THEN expect(mockSignActions).toHaveBeenCalledWith( DERIVATION_PATH, mockDeviceData, new Uint8Array(Buffer.from(baseParams.metadataWithSignature, "hex")), [{ type: "cancel", cancels: [{ a: 1, o: 42 }], nonce: 1 }], ); }); it("should throw a ServerError when params is undefined", async () => { // WHEN & THEN await expect(serverHandlers["custom.perps.signActions"](undefined)).rejects.toThrow( "ServerError", ); }); it("should throw a ServerError when account is not found", async () => { // GIVEN jest.mocked(getAccountIdFromWalletAccountId).mockReturnValue("unknown-id"); // WHEN & THEN await expect(serverHandlers["custom.perps.signActions"](baseParams)).rejects.toThrow( "ServerError", ); }); it("should throw a ServerError when the derivation path is missing", async () => { // GIVEN jest .mocked(getMainAccount) .mockReturnValue({ ...mockAccount, freshAddressPath: "" } as never); // WHEN & THEN await expect(serverHandlers["custom.perps.signActions"](baseParams)).rejects.toThrow( "ServerError", ); }); it("should reject when the user cancels the device selection", async () => { // GIVEN mockUiDeviceSelect.mockImplementation(({ onCancel }) => onCancel()); // WHEN & THEN await expect(serverHandlers["custom.perps.signActions"](baseParams)).rejects.toThrow( "User cancelled device selection", ); }); it("should throw when the transport is not a DMK transport", async () => { // GIVEN jest.mocked(isDmkTransport).mockReturnValue(false); // WHEN & THEN await expect(serverHandlers["custom.perps.signActions"](baseParams)).rejects.toThrow( "Not DMK transport", ); }); }); });