UNPKG

@ledgerhq/live-common

Version:
477 lines (416 loc) 15.2 kB
import { handlers } from "./server"; import { Account } from "@ledgerhq/types-live"; import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; import BigNumber from "bignumber.js"; import { AppPlatform, AppBranch, Visibility } from "../types"; import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state"; // Mock dependencies jest.mock("@ledgerhq/wallet-api-server", () => ({ RPCHandler: jest.fn(), customWrapper: jest.fn(handler => handler), })); jest.mock("@ledgerhq/cryptoassets", () => ({ getCryptoCurrencyById: jest.fn(), })); jest.mock("@ledgerhq/cryptoassets/state", () => ({ getCryptoAssetsStore: jest.fn(), })); jest.mock("../converters", () => ({ getAccountIdFromWalletAccountId: jest.fn(), getWalletAPITransactionSignFlowInfos: jest.fn(), })); jest.mock("../../bridge", () => ({ getAccountBridge: jest.fn(), })); jest.mock("@ledgerhq/live-env", () => ({ getEnv: jest.fn(), changes: { subscribe: jest.fn() }, })); // Mock types const mockTracking = { signMessageRequested: jest.fn(), signMessageSuccess: jest.fn(), signMessageFail: jest.fn(), signMessageNoParams: jest.fn(), signMessageUserRefused: jest.fn(), signTransactionRequested: jest.fn(), signTransactionSuccess: jest.fn(), signTransactionFail: jest.fn(), signTransactionNoParams: jest.fn(), signTransactionAndBroadcastNoParams: jest.fn(), broadcastSuccess: jest.fn(), broadcastFail: jest.fn(), broadcastOperationDetailsClick: jest.fn(), }; const mockManifest = { id: "test-manifest", name: "Test App", url: "https://test.app", homepageUrl: "https://test.app", icon: "data:image/png;base64,test", apiVersion: "1.0.0", permissions: [], domains: [], categories: [], platforms: ["desktop", "ios"] as AppPlatform[], manifestVersion: "1.0.0", branch: "stable" as AppBranch, currencies: [], visibility: "complete" as Visibility, content: { shortDescription: { en: "Test app" }, description: { en: "Test app description" }, }, }; const mockEthereumCurrency: CryptoCurrency = { type: "CryptoCurrency", id: "ethereum", coinType: 60, name: "Ethereum", managerAppName: "Ethereum", ticker: "ETH", scheme: "ethereum", color: "#0ebdcd", symbol: "Ξ", family: "evm", blockAvgTime: 15, units: [ { name: "ether", code: "ETH", magnitude: 18, }, ], keywords: ["eth", "ethereum"], explorerViews: [], explorerId: "eth", }; const mockTokenCurrency: TokenCurrency = { type: "TokenCurrency", id: "ethereum/erc20/acre_btc", contractAddress: "0x1234567890123456789012345678901234567890", parentCurrency: mockEthereumCurrency, tokenType: "erc20", name: "ACRE Bitcoin", ticker: "acreBTC", units: [ { name: "ACRE Bitcoin", code: "acreBTC", magnitude: 8, }, ], }; const mockAccount: Account = { type: "Account", id: "js:2:ethereum:0x1234567890123456789012345678901234567890:ethereum", seedIdentifier: "0x1234567890123456789012345678901234567890", derivationMode: "", index: 0, freshAddress: "0x1234567890123456789012345678901234567890", freshAddressPath: "44'/60'/0'/0/0", used: false, blockHeight: 0, creationDate: new Date(), balance: new BigNumber(0), spendableBalance: new BigNumber(0), operationsCount: 0, operations: [], pendingOperations: [], currency: mockEthereumCurrency, lastSyncDate: new Date(), swapHistory: [], balanceHistoryCache: { HOUR: { latestDate: null, balances: [] }, DAY: { latestDate: null, balances: [] }, WEEK: { latestDate: null, balances: [] }, }, syncHash: "0x00000000", subAccounts: [], nfts: [], }; jest.mock("@ledgerhq/ledger-wallet-framework/account/index", () => ({ getParentAccount: jest.fn(), getMainAccount: jest.fn(), makeEmptyTokenAccount: jest.fn(), isTokenAccount: jest.fn(), })); describe("ACRE Server Handlers", () => { let mockUiHooks: any; let serverHandlers: any; beforeEach(() => { jest.clearAllMocks(); (getCryptoAssetsStore as jest.Mock).mockReturnValue({ findTokenByAddressInCurrency: jest.fn().mockResolvedValue(mockTokenCurrency), findTokenById: jest.fn().mockResolvedValue(mockTokenCurrency), }); mockUiHooks = { "custom.acre.messageSign": jest.fn().mockImplementation(({ onSuccess }) => { onSuccess("0x1234567890abcdef"); }), "custom.acre.transactionSign": jest.fn().mockImplementation(({ onSuccess }) => { onSuccess({ operation: { hash: "0x1234567890abcdef" }, signature: "0xabcdef1234567890", }); }), "custom.acre.transactionBroadcast": jest.fn(), "custom.acre.registerAccount": jest.fn().mockImplementation(({ onSuccess }) => { onSuccess(); }), }; // Mock the account functions const { makeEmptyTokenAccount, getMainAccount, getParentAccount } = jest.requireMock( "@ledgerhq/ledger-wallet-framework/account/index", ); makeEmptyTokenAccount.mockReturnValue({ type: "TokenAccount", id: "mock-token-account-id", parentId: "mock-parent-id", token: mockTokenCurrency, balance: new BigNumber(0), spendableBalance: new BigNumber(0), creationDate: new Date(), operationsCount: 0, operations: [], pendingOperations: [], balanceHistoryCache: { HOUR: { latestDate: null, balances: [] }, DAY: { latestDate: null, balances: [] }, WEEK: { latestDate: null, balances: [] }, }, swapHistory: [], }); getMainAccount.mockReturnValue(mockAccount); getParentAccount.mockReturnValue(undefined); // Mock the converter functions const { getAccountIdFromWalletAccountId, getWalletAPITransactionSignFlowInfos } = jest.requireMock("../converters"); getAccountIdFromWalletAccountId.mockReturnValue( "js:2:ethereum:0x1234567890123456789012345678901234567890:ethereum", ); getWalletAPITransactionSignFlowInfos.mockReturnValue({ canEditFees: true, liveTx: { family: "evm" }, hasFeesProvided: true, }); // Mock the bridge const { getAccountBridge } = jest.requireMock("../../bridge"); getAccountBridge.mockReturnValue({ broadcast: jest.fn().mockResolvedValue({ hash: "0x1234567890abcdef" }), }); serverHandlers = handlers({ accounts: [mockAccount], tracking: mockTracking, manifest: mockManifest, uiHooks: mockUiHooks, }); }); describe("custom.acre.messageSign", () => { it("should handle message signing request successfully", async () => { const mockParams = { accountId: "test-account-id", message: { type: "SignIn", message: "test" }, derivationPath: "0/0", options: { hwAppId: "test-app" }, meta: { test: "data" }, }; const mockSignature = "0x1234567890abcdef"; mockUiHooks["custom.acre.messageSign"].mockImplementation(({ onSuccess }) => { onSuccess(mockSignature); }); const result = await serverHandlers["custom.acre.messageSign"](mockParams); expect(result).toEqual({ hexSignedMessage: "1234567890abcdef", }); expect(mockTracking.signMessageRequested).toHaveBeenCalledWith(mockManifest); expect(mockTracking.signMessageSuccess).toHaveBeenCalledWith(mockManifest); }); it("should handle missing parameters", async () => { const result = await serverHandlers["custom.acre.messageSign"](null); expect(result).toEqual({ hexSignedMessage: "", }); expect(mockTracking.signMessageNoParams).toHaveBeenCalledWith(mockManifest); }); it("should handle account not found", async () => { const mockParams = { accountId: "non-existent-account", message: { type: "SignIn", message: "test" }, derivationPath: "0/0", options: {}, meta: {}, }; // Mock getAccountIdFromWalletAccountId to return null for non-existent account const { getAccountIdFromWalletAccountId } = jest.requireMock("../converters"); getAccountIdFromWalletAccountId.mockReturnValueOnce(null); await expect(serverHandlers["custom.acre.messageSign"](mockParams)).rejects.toThrow( "accountId non-existent-account unknown", ); }); }); describe("custom.acre.transactionSign", () => { it("should handle transaction signing request successfully", async () => { const mockParams = { accountId: "test-account-id", rawTransaction: "0x1234567890abcdef", options: { hwAppId: "test-app" }, meta: { test: "data" }, tokenCurrency: "ethereum/erc20/test", }; const mockSignedOperation = { operation: { hash: "0x1234567890abcdef" }, signature: "0xabcdef1234567890", }; mockUiHooks["custom.acre.transactionSign"].mockImplementation(({ onSuccess }) => { onSuccess(mockSignedOperation); }); const result = await serverHandlers["custom.acre.transactionSign"](mockParams); expect(result).toEqual({ signedTransactionHex: "307861626364656631323334353637383930", }); expect(mockTracking.signTransactionRequested).toHaveBeenCalledWith(mockManifest); expect(mockTracking.signTransactionSuccess).toHaveBeenCalledWith(mockManifest); }); it("should handle missing parameters", async () => { const result = await serverHandlers["custom.acre.transactionSign"](null); expect(result).toEqual({ signedTransactionHex: "", }); expect(mockTracking.signTransactionNoParams).toHaveBeenCalledWith(mockManifest); }); }); describe("custom.acre.transactionSignAndBroadcast", () => { it("should handle transaction sign and broadcast successfully", async () => { const mockParams = { accountId: "test-account-id", rawTransaction: "0x1234567890abcdef", options: { hwAppId: "test-app" }, meta: { test: "data" }, tokenCurrency: "ethereum/erc20/test", }; const mockSignedOperation = { operation: { hash: "0x1234567890abcdef", recipients: ["0x1234567890123456789012345678901234567890"], }, signature: "0xabcdef1234567890", }; mockUiHooks["custom.acre.transactionSign"].mockImplementation(({ onSuccess }) => { onSuccess(mockSignedOperation); }); const result = await serverHandlers["custom.acre.transactionSignAndBroadcast"](mockParams); expect(result).toEqual({ transactionHash: "0x1234567890abcdef", }); }); it("should handle missing parameters", async () => { const result = await serverHandlers["custom.acre.transactionSignAndBroadcast"](null); expect(result).toEqual({ transactionHash: "", }); expect(mockTracking.signTransactionAndBroadcastNoParams).toHaveBeenCalledWith(mockManifest); }); }); describe("custom.acre.registerYieldBearingEthereumAddress", () => { const mockParams = { ethereumAddress: "0x9876543210987654321098765432109876543210", tokenContractAddress: "0x1234567890123456789012345678901234567890", meta: { test: "data" }, }; it("should register new yield-bearing Ethereum address successfully on mobile", async () => { const result = await serverHandlers["custom.acre.registerYieldBearingEthereumAddress"](mockParams); expect(result.success).toBe(true); expect(result.accountName).toBe("Yield-bearing BTC on ACRE"); expect(result.ethereumAddress).toBe(mockParams.ethereumAddress); expect(result.tokenContractAddress).toBe(mockParams.tokenContractAddress); // Verify UI hook was called with correct parameters expect(mockUiHooks["custom.acre.registerAccount"]).toHaveBeenCalledWith({ parentAccount: expect.objectContaining({ type: "Account", id: expect.stringContaining("0x9876543210987654321098765432109876543210"), }), accountName: "Yield-bearing BTC on ACRE", existingAccounts: [mockAccount], onSuccess: expect.any(Function), onError: expect.any(Function), }); }); it("should register new yield-bearing Ethereum address successfully on desktop", async () => { const desktopServerHandlers = handlers({ accounts: [mockAccount], tracking: mockTracking, manifest: mockManifest, uiHooks: mockUiHooks, }); const result = await ( desktopServerHandlers["custom.acre.registerYieldBearingEthereumAddress"] as any )(mockParams); expect(result.success).toBe(true); expect(mockUiHooks["custom.acre.registerAccount"]).toHaveBeenCalledWith({ parentAccount: expect.objectContaining({ type: "Account", id: expect.stringContaining("0x9876543210987654321098765432109876543210"), }), accountName: "Yield-bearing BTC on ACRE", existingAccounts: [mockAccount], onSuccess: expect.any(Function), onError: expect.any(Function), }); }); it("should handle existing account gracefully", async () => { const existingAccount = { ...mockAccount, freshAddress: mockParams.ethereumAddress, }; const serverHandlersWithExisting = handlers({ accounts: [existingAccount], tracking: mockTracking, manifest: mockManifest, uiHooks: mockUiHooks, }); const result = await ( serverHandlersWithExisting["custom.acre.registerYieldBearingEthereumAddress"] as any )(mockParams); expect(result.success).toBe(true); expect(result.parentAccountId).toBe(existingAccount.id); expect(result.tokenAccountId).toBe(existingAccount.id); expect(mockUiHooks["custom.acre.registerAccount"]).not.toHaveBeenCalled(); }); it("should handle missing UI hook", async () => { const serverHandlersWithoutHook = handlers({ accounts: [mockAccount], tracking: mockTracking, manifest: mockManifest, uiHooks: { ...mockUiHooks, "custom.acre.registerAccount": undefined, }, }); await expect( (serverHandlersWithoutHook["custom.acre.registerYieldBearingEthereumAddress"] as any)( mockParams, ), ).rejects.toThrow("No account registration UI hook available"); }); it("should validate Ethereum address format", async () => { const invalidParams = { ...mockParams, ethereumAddress: "invalid-address", }; await expect( serverHandlers["custom.acre.registerYieldBearingEthereumAddress"](invalidParams), ).rejects.toThrow("Invalid Ethereum address format"); }); it("should require either tokenContractAddress or tokenTicker", async () => { const invalidParams = { ethereumAddress: mockParams.ethereumAddress, meta: mockParams.meta, }; await expect( serverHandlers["custom.acre.registerYieldBearingEthereumAddress"](invalidParams), ).rejects.toThrow("Either tokenContractAddress or tokenTicker must be provided"); }); }); });