@faremeter/wallet-ledger 0.10.1 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ import { type Chain } from "viem";
2
+ import type { LedgerEvmWallet, UserInterface } from "./types.js";
3
+ export declare function createLedgerEvmWallet(ui: UserInterface, chain: Chain, derivationPath: string): Promise<LedgerEvmWallet>;
4
+ //# sourceMappingURL=evm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evm.d.ts","sourceRoot":"","sources":["../../src/evm.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,KAAK,EAMX,MAAM,MAAM,CAAC;AAId,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE9D,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE,KAAK,EACZ,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,eAAe,CAAC,CA4J1B"}
@@ -0,0 +1,126 @@
1
+ import { createWalletClient, http, hashDomain, hashStruct, } from "viem";
2
+ import Eth from "@ledgerhq/hw-app-eth/lib-es/Eth.js";
3
+ import { type } from "arktype";
4
+ import { createTransport } from "./transport.js";
5
+ export async function createLedgerEvmWallet(ui, chain, derivationPath) {
6
+ const transport = await createTransport();
7
+ const eth = new Eth(transport);
8
+ const { address } = await eth.getAddress(derivationPath);
9
+ const formattedAddress = (address.startsWith("0x") ? address : `0x${address}`).toLowerCase();
10
+ const ledgerAccount = {
11
+ address: formattedAddress,
12
+ publicKey: formattedAddress,
13
+ type: "local",
14
+ source: "custom",
15
+ signMessage: async ({ message }) => {
16
+ let messageToSign;
17
+ if (typeof message === "string") {
18
+ messageToSign = Buffer.from(message).toString("hex");
19
+ }
20
+ else if (message && typeof message === "object" && "raw" in message) {
21
+ const raw = message.raw;
22
+ messageToSign =
23
+ typeof raw === "string"
24
+ ? raw.slice(2)
25
+ : Buffer.from(raw).toString("hex");
26
+ }
27
+ else {
28
+ messageToSign = Buffer.from(String(message)).toString("hex");
29
+ }
30
+ const result = await eth.signPersonalMessage(derivationPath, messageToSign);
31
+ const signature = `0x${result.r}${result.s}${result.v.toString(16).padStart(2, "0")}`;
32
+ return signature;
33
+ },
34
+ signTransaction: async (transaction) => {
35
+ const toHex = (v, defaultValue) => v ? `0x${v.toString(16)}` : defaultValue;
36
+ const tx = {
37
+ to: transaction.to,
38
+ value: toHex(transaction.value, "0x0"),
39
+ data: transaction.data ?? "0x", // 0x is valid encoding for empty data in EVM
40
+ nonce: toHex(transaction.nonce, "0x0"),
41
+ gasLimit: toHex(transaction.gas, "0x5208"),
42
+ gasPrice: toHex(transaction.gasPrice),
43
+ maxFeePerGas: toHex(transaction.maxFeePerGas),
44
+ maxPriorityFeePerGas: toHex(transaction.maxPriorityFeePerGas),
45
+ chainId: chain.id,
46
+ };
47
+ const result = await eth.signTransaction(derivationPath, JSON.stringify(tx));
48
+ const signature = `0x${result.r}${result.s}${result.v}`;
49
+ return signature;
50
+ },
51
+ signTypedData: async (parameters) => {
52
+ const typedDataParams = type({
53
+ "domain?": {
54
+ "name?": "string",
55
+ "version?": "string",
56
+ "chainId?": "number",
57
+ "verifyingContract?": "string",
58
+ "salt?": "string",
59
+ },
60
+ types: "object",
61
+ primaryType: "string",
62
+ message: "object",
63
+ });
64
+ const validated = typedDataParams.assert(parameters);
65
+ const { domain, types, primaryType, message } = validated;
66
+ try {
67
+ // Use EIP-712 hashed message approach. This calculates the domain
68
+ // separator and message hash separately and sends them to the Ledger
69
+ // for signing.
70
+ // Build types with EIP712Domain
71
+ const typesWithDomain = {
72
+ EIP712Domain: [
73
+ ...(domain?.name ? [{ name: "name", type: "string" }] : []),
74
+ ...(domain?.version ? [{ name: "version", type: "string" }] : []),
75
+ ...(domain?.chainId ? [{ name: "chainId", type: "uint256" }] : []),
76
+ ...(domain?.verifyingContract
77
+ ? [{ name: "verifyingContract", type: "address" }]
78
+ : []),
79
+ ],
80
+ ...types,
81
+ };
82
+ // Calculate hashes using viem
83
+ const domainSeparator = hashDomain({
84
+ domain: domain ?? {},
85
+ types: typesWithDomain,
86
+ });
87
+ // types without domain for message hash
88
+ const messageHash = hashStruct({
89
+ data: message,
90
+ primaryType,
91
+ types,
92
+ });
93
+ ui.message("\nEIP-712 hashes calculated:");
94
+ ui.message(` Domain separator: ${domainSeparator}`);
95
+ ui.message(` Message hash: ${messageHash}`);
96
+ ui.message(`\nPlease approve the transaction on your Ledger device...`);
97
+ const result = await eth.signEIP712HashedMessage(derivationPath, domainSeparator.slice(2), messageHash.slice(2));
98
+ const signature = `0x${result.r}${result.s}${result.v.toString(16).padStart(2, "0")}`;
99
+ return signature;
100
+ }
101
+ catch (error) {
102
+ ui.message(`EIP-712 signing failed: ${error}`);
103
+ throw error;
104
+ }
105
+ },
106
+ };
107
+ const client = createWalletClient({
108
+ account: ledgerAccount,
109
+ chain: chain,
110
+ transport: http(chain.rpcUrls.default.http[0]),
111
+ });
112
+ return {
113
+ chain,
114
+ address: formattedAddress,
115
+ client,
116
+ signTransaction: async (tx) => {
117
+ return await ledgerAccount.signTransaction(tx);
118
+ },
119
+ signTypedData: async (params) => {
120
+ return await ledgerAccount.signTypedData(params);
121
+ },
122
+ disconnect: async () => {
123
+ await transport.close();
124
+ },
125
+ };
126
+ }
@@ -0,0 +1,6 @@
1
+ export { createLedgerEvmWallet } from "./evm.js";
2
+ export { createLedgerSolanaWallet } from "./solana.js";
3
+ export { selectLedgerAccount } from "./utils.js";
4
+ export type { LedgerEvmWallet, LedgerSolanaWallet } from "./types.js";
5
+ export * from "./interface.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,OAAO,CAAC;AAC9C,OAAO,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AACnE,cAAc,aAAa,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { createLedgerEvmWallet } from "./evm.js";
2
+ export { createLedgerSolanaWallet } from "./solana.js";
3
+ export { selectLedgerAccount } from "./utils.js";
4
+ export * from "./interface.js";
@@ -0,0 +1,10 @@
1
+ export type createReadlineInterfaceArgs = {
2
+ stdin: NodeJS.ReadableStream;
3
+ stdout: NodeJS.WritableStream;
4
+ };
5
+ export declare function createReadlineInterface(args: createReadlineInterfaceArgs): Promise<{
6
+ message: (msg: string) => undefined;
7
+ question: (q: string) => Promise<string>;
8
+ close: () => Promise<void>;
9
+ }>;
10
+ //# sourceMappingURL=interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/interface.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,2BAA2B,GAAG;IACxC,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;CAC/B,CAAC;AAEF,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,2BAA2B;mBAShB,MAAM;kBACD,MAAM;;GAM7B"}
@@ -0,0 +1,14 @@
1
+ export async function createReadlineInterface(args) {
2
+ const readline = await import("readline");
3
+ const rl = readline.createInterface({
4
+ input: args.stdin,
5
+ output: args.stdout,
6
+ });
7
+ return {
8
+ message: (msg) => void args.stdout.write(msg + "\n"),
9
+ question: async (q) => new Promise((resolve) => {
10
+ rl.question(q, resolve);
11
+ }),
12
+ close: async () => rl.close(),
13
+ };
14
+ }
@@ -0,0 +1,2 @@
1
+ export declare const logger: import("@logtape/logtape").Logger;
2
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM,mCAA4C,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { getLogger } from "@logtape/logtape";
2
+ export const logger = getLogger(["faremeter", "wallet-ledger"]);
@@ -0,0 +1,3 @@
1
+ import type { LedgerSolanaWallet } from "./types.js";
2
+ export declare function createLedgerSolanaWallet(network: string, derivationPath: string): Promise<LedgerSolanaWallet>;
3
+ //# sourceMappingURL=solana.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solana.d.ts","sourceRoot":"","sources":["../../src/solana.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,kBAAkB,CAAC,CA0B7B"}
@@ -0,0 +1,22 @@
1
+ import { PublicKey, VersionedTransaction } from "@solana/web3.js";
2
+ import Solana from "@ledgerhq/hw-app-solana/lib-es/Solana.js";
3
+ import { createTransport } from "./transport.js";
4
+ export async function createLedgerSolanaWallet(network, derivationPath) {
5
+ const transport = await createTransport();
6
+ const solana = new Solana(transport);
7
+ const { address } = await solana.getAddress(derivationPath);
8
+ const publicKey = new PublicKey(address);
9
+ return {
10
+ network,
11
+ publicKey,
12
+ updateTransaction: async (tx) => {
13
+ const message = tx.message.serialize();
14
+ const signature = await solana.signTransaction(derivationPath, Buffer.from(message));
15
+ tx.addSignature(publicKey, signature.signature);
16
+ return tx;
17
+ },
18
+ disconnect: async () => {
19
+ await transport.close();
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,4 @@
1
+ import type Transport from "@ledgerhq/hw-transport";
2
+ export declare function translateLedgerError(error: unknown): Error;
3
+ export declare function createTransport(maxRetries?: number): Promise<Transport>;
4
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,SAAS,MAAM,wBAAwB,CAAC;AAcpD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAoB1D;AAED,wBAAsB,eAAe,CAAC,UAAU,SAAI,GAAG,OAAO,CAAC,SAAS,CAAC,CAyCxE"}
@@ -0,0 +1,56 @@
1
+ import { logger } from "./logger.js";
2
+ const LEDGER_ERRORS = {
3
+ "0x5515": "Ledger is locked. Please unlock your device.",
4
+ "0x6511": "Please unlock your Ledger and open the correct app.",
5
+ "0x6d00": "Wrong app open. Please open the correct app.",
6
+ "0x6d02": "No app open. Please open the correct app on your Ledger.",
7
+ "0x6e00": "Wrong app open. Please open the correct app on your Ledger.",
8
+ "0x6985": "Transaction rejected on Ledger device.",
9
+ "0x6a80": "Incorrect data. Please make sure the correct app is open on your Ledger.",
10
+ "0x6a83": "Wrong app open. Please open the correct app on your Ledger.",
11
+ };
12
+ export function translateLedgerError(error) {
13
+ const message = String(error instanceof Error ? error.message : error);
14
+ const hexMatch = /0x[0-9a-fA-F]{4}/.exec(message);
15
+ if (hexMatch && LEDGER_ERRORS[hexMatch[0]]) {
16
+ return new Error(LEDGER_ERRORS[hexMatch[0]]);
17
+ }
18
+ // Check for common connection errors
19
+ if (message.includes("NoDevice")) {
20
+ return new Error("No Ledger device found. Please connect your Ledger and unlock it.");
21
+ }
22
+ if (message.includes("Device busy")) {
23
+ return new Error("Ledger is in use by another app. Close Ledger Live.");
24
+ }
25
+ return error instanceof Error ? error : new Error(message);
26
+ }
27
+ export async function createTransport(maxRetries = 3) {
28
+ let lastError;
29
+ for (let i = 0; i < maxRetries; i++) {
30
+ try {
31
+ const isBrowser = typeof globalThis !== "undefined" && "window" in globalThis;
32
+ if (isBrowser) {
33
+ const { default: TransportWebUSB } = await import("@ledgerhq/hw-transport-webusb");
34
+ return await TransportWebUSB.create();
35
+ }
36
+ else {
37
+ const mod = await import("@ledgerhq/hw-transport-node-hid");
38
+ const TransportNodeHid = mod.default || mod;
39
+ return await TransportNodeHid.open("");
40
+ }
41
+ }
42
+ catch (error) {
43
+ const translatedError = translateLedgerError(error);
44
+ lastError = translatedError;
45
+ // Retry on generic USB errors
46
+ const errorMessage = translatedError.message;
47
+ if (i < maxRetries - 1 &&
48
+ (errorMessage.includes("USB") || errorMessage.includes("device"))) {
49
+ logger.warning(`USB connection attempt ${i + 1} failed, retrying...`);
50
+ await new Promise((resolve) => setTimeout(resolve, 1000));
51
+ continue;
52
+ }
53
+ }
54
+ }
55
+ throw lastError ?? new Error("Failed to connect to Ledger device");
56
+ }
@@ -0,0 +1,27 @@
1
+ import type { Hex, WalletClient, TransactionSerializable, TypedDataDefinition, Chain } from "viem";
2
+ import type { PublicKey, VersionedTransaction } from "@solana/web3.js";
3
+ import type Transport from "@ledgerhq/hw-transport";
4
+ export interface LedgerEvmWallet {
5
+ chain: Chain;
6
+ address: Hex;
7
+ client: WalletClient;
8
+ signTransaction: (tx: TransactionSerializable) => Promise<Hex>;
9
+ signTypedData: (params: TypedDataDefinition) => Promise<Hex>;
10
+ disconnect: () => Promise<void>;
11
+ }
12
+ export interface LedgerSolanaWallet {
13
+ network: string;
14
+ publicKey: PublicKey;
15
+ updateTransaction: (tx: VersionedTransaction) => Promise<VersionedTransaction>;
16
+ disconnect: () => Promise<void>;
17
+ }
18
+ export interface LedgerTransportWrapper {
19
+ transport: Transport;
20
+ close: () => Promise<void>;
21
+ }
22
+ export interface UserInterface {
23
+ message: (msg: string) => void;
24
+ question: (prompt: string) => Promise<string>;
25
+ close: () => Promise<void>;
26
+ }
27
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,GAAG,EACH,YAAY,EACZ,uBAAuB,EACvB,mBAAmB,EACnB,KAAK,EACN,MAAM,MAAM,CAAC;AACd,OAAO,KAAK,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvE,OAAO,KAAK,SAAS,MAAM,wBAAwB,CAAC;AAEpD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,EAAE,GAAG,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,eAAe,EAAE,CAAC,EAAE,EAAE,uBAAuB,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/D,aAAa,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7D,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC;IACrB,iBAAiB,EAAE,CACjB,EAAE,EAAE,oBAAoB,KACrB,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnC,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,SAAS,CAAC;IACrB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { UserInterface } from "./types.js";
2
+ export declare function selectLedgerAccount(ui: UserInterface, type: "evm" | "solana", numAccounts?: number): Promise<{
3
+ path: string;
4
+ address: string;
5
+ } | null>;
6
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAU7C,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,KAAK,GAAG,QAAQ,EACtB,WAAW,SAAI,GACd,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAyDnD"}
@@ -0,0 +1,64 @@
1
+ import Eth from "@ledgerhq/hw-app-eth/lib-es/Eth.js";
2
+ import Solana from "@ledgerhq/hw-app-solana/lib-es/Solana.js";
3
+ import { PublicKey } from "@solana/web3.js";
4
+ import { createTransport, translateLedgerError } from "./transport.js";
5
+ function evmDerivationPath(index) {
6
+ return `m/44'/60'/${index}'/0/0`;
7
+ }
8
+ function solanaDerivationPath(index) {
9
+ return `44'/501'/${index}'`;
10
+ }
11
+ export async function selectLedgerAccount(ui, type, numAccounts = 5) {
12
+ const isEvm = type === "evm";
13
+ ui.message(`\nScanning first ${numAccounts} ${isEvm ? "Ethereum" : "Solana"} accounts...`);
14
+ const accounts = [];
15
+ const transport = await createTransport();
16
+ try {
17
+ if (isEvm) {
18
+ const eth = new Eth(transport);
19
+ for (let i = 0; i < numAccounts; i++) {
20
+ const path = evmDerivationPath(i);
21
+ let result;
22
+ try {
23
+ result = await eth.getAddress(path, false);
24
+ }
25
+ catch (error) {
26
+ throw translateLedgerError(error);
27
+ }
28
+ const address = result.address;
29
+ const normalizedAddress = address.startsWith("0x")
30
+ ? address
31
+ : `0x${address}`;
32
+ accounts.push({ path, address: normalizedAddress });
33
+ ui.message(`${i + 1}. ${normalizedAddress}`);
34
+ }
35
+ }
36
+ else {
37
+ const solana = new Solana(transport);
38
+ for (let i = 0; i < numAccounts; i++) {
39
+ const path = solanaDerivationPath(i);
40
+ let result;
41
+ try {
42
+ result = await solana.getAddress(path, false);
43
+ }
44
+ catch (error) {
45
+ throw translateLedgerError(error);
46
+ }
47
+ const publicKey = new PublicKey(result.address);
48
+ const address = publicKey.toBase58();
49
+ accounts.push({ path, address });
50
+ ui.message(`${i + 1}. ${address}`);
51
+ }
52
+ }
53
+ }
54
+ finally {
55
+ await transport.close();
56
+ }
57
+ const selection = await ui.question(`\nSelect account (1-${numAccounts}): `);
58
+ const index = parseInt(selection) - 1;
59
+ if (index < 0 || index >= accounts.length) {
60
+ ui.message("Invalid selection");
61
+ return null;
62
+ }
63
+ return accounts[index] ?? null;
64
+ }