@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
393 lines • 16.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const server_1 = require("./server");
const bignumber_js_1 = __importDefault(require("bignumber.js"));
// Mock dependencies
jest.mock("@ledgerhq/wallet-api-server", () => ({
RPCHandler: jest.fn(),
customWrapper: jest.fn(handler => handler),
}));
jest.mock("@ledgerhq/cryptoassets", () => ({
findTokenById: jest.fn(),
findTokenByAddressInCurrency: jest.fn(),
getCryptoCurrencyById: 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(),
}));
// 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"],
manifestVersion: "1.0.0",
branch: "stable",
currencies: [],
visibility: "complete",
content: {
shortDescription: { en: "Test app" },
description: { en: "Test app description" },
},
};
const mockEthereumCurrency = {
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 = {
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 = {
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_js_1.default(0),
spendableBalance: new bignumber_js_1.default(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/coin-framework/account/index", () => ({
getParentAccount: jest.fn(),
getMainAccount: jest.fn(),
makeEmptyTokenAccount: jest.fn(),
isTokenAccount: jest.fn(),
}));
describe("ACRE Server Handlers", () => {
let mockUiHooks;
let serverHandlers;
beforeEach(() => {
jest.clearAllMocks();
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 cryptoassets functions globally
const { findTokenByAddressInCurrency, getCryptoCurrencyById } = jest.requireMock("@ledgerhq/cryptoassets");
findTokenByAddressInCurrency.mockReturnValue(mockTokenCurrency);
getCryptoCurrencyById.mockReturnValue(mockEthereumCurrency);
// Mock the account functions
const { makeEmptyTokenAccount, getMainAccount, getParentAccount } = jest.requireMock("@ledgerhq/coin-framework/account/index");
makeEmptyTokenAccount.mockReturnValue({
type: "TokenAccount",
id: "mock-token-account-id",
parentId: "mock-parent-id",
token: mockTokenCurrency,
balance: new bignumber_js_1.default(0),
spendableBalance: new bignumber_js_1.default(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 = (0, server_1.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" },
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 = (0, server_1.handlers)({
accounts: [mockAccount],
tracking: mockTracking,
manifest: mockManifest,
uiHooks: mockUiHooks,
});
const result = await desktopServerHandlers["custom.acre.registerYieldBearingEthereumAddress"](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 = (0, server_1.handlers)({
accounts: [existingAccount],
tracking: mockTracking,
manifest: mockManifest,
uiHooks: mockUiHooks,
});
const result = await serverHandlersWithExisting["custom.acre.registerYieldBearingEthereumAddress"](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 = (0, server_1.handlers)({
accounts: [mockAccount],
tracking: mockTracking,
manifest: mockManifest,
uiHooks: {
...mockUiHooks,
"custom.acre.registerAccount": undefined,
},
});
await expect(serverHandlersWithoutHook["custom.acre.registerYieldBearingEthereumAddress"](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");
});
});
});
//# sourceMappingURL=server.test.js.map