@marinade.finance/ledger-utils
Advanced tools
Comparing version 2.0.22 to 2.1.0
{ | ||
"name": "@marinade.finance/ledger-utils", | ||
"version": "2.0.22", | ||
"version": "2.1.0", | ||
"description": "Utility functions for interacting with the Ledger from CLI", | ||
@@ -30,3 +30,3 @@ "repository": { | ||
"@solana/web3.js": "^1.78.4", | ||
"@marinade.finance/ts-common": "^2.0.22" | ||
"@marinade.finance/ts-common": "^2.1.0" | ||
}, | ||
@@ -38,3 +38,3 @@ "peerDependencies": { | ||
"@solana/web3.js": "^1.78.4", | ||
"@marinade.finance/ts-common": "^2.0.22" | ||
"@marinade.finance/ts-common": "^2.1.0" | ||
}, | ||
@@ -41,0 +41,0 @@ "scripts": { |
@@ -24,3 +24,4 @@ "use strict"; | ||
if (e.statusCode === 0x6d02) { | ||
(0, ts_common_1.logError)(logger, 'Ledger device Solana application is not activated. ' + | ||
(0, ts_common_1.logError)(logger, `Ledger device [${pathOrUrl}] ` + | ||
': Solana application is not opened. ' + | ||
'Please, enter the Solana app on your ledger device first.'); | ||
@@ -32,17 +33,23 @@ } | ||
} | ||
else if (e.statusCode === 0x5515) { | ||
(0, ts_common_1.logError)(logger, `Ledger device [${pathOrUrl}] is locked. ` + | ||
'Please, unlock it first.'); | ||
} | ||
} | ||
else if (e instanceof errors_1.TransportError && | ||
e.message.includes('Invalid channel')) { | ||
(0, ts_common_1.logError)(logger, 'Ledger device seems not being acknowledged to open the ledger manager. ' + | ||
(0, ts_common_1.logError)(logger, `Ledger device [${pathOrUrl}] seems not being acknowledged to have opened the manager. ` + | ||
'Please, open ledger manager first on your device.'); | ||
} | ||
else if (e instanceof errors_1.LockedDeviceError) { | ||
(0, ts_common_1.logError)(logger, 'Ledger device is locked. ' + 'Please, unlock it first.'); | ||
(0, ts_common_1.logError)(logger, `Ledger device [${pathOrUrl}] is locked. ` + | ||
'Please, unlock it first.'); | ||
} | ||
else if (e instanceof Error && | ||
e.message.includes('read from a closed HID')) { | ||
(0, ts_common_1.logError)(logger, 'Ledger cannot be open, it seems to be closed. Ensure no other program uses it.'); | ||
(0, ts_common_1.logError)(logger, `Ledger device [${pathOrUrl}] ` + | ||
'cannot be open, it seems to be closed. Ensure no other program uses it.'); | ||
} | ||
else { | ||
(0, ts_common_1.logError)(logger, `Failed to connect to Ledger device of key ${pathOrUrl}`); | ||
(0, ts_common_1.logError)(logger, `Failed to connect to Ledger device [${pathOrUrl}]`); | ||
} | ||
@@ -49,0 +56,0 @@ throw e; |
import Solana from '@ledgerhq/hw-app-solana'; | ||
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid-noevents'; | ||
import { PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js'; | ||
@@ -25,4 +26,5 @@ import { LoggerPlaceholder } from '@marinade.finance/ts-common'; | ||
readonly publicKey: PublicKey; | ||
readonly logger: LoggerPlaceholder | undefined; | ||
/** | ||
* "Constructor" of SolanaLedger class. | ||
* "Constructor" of Solana Ledger to be opened and worked as a Wallet. | ||
* From ledger url in format of usb://ledger[/<pubkey>[?key=<number>] | ||
@@ -35,3 +37,2 @@ * creates wrapper class around Solana ledger device from '@ledgerhq/hw-app-solana' package. | ||
signAllTransactions<T extends Transaction | VersionedTransaction>(txs: T[]): Promise<T[]>; | ||
private static getPublicKey; | ||
/** | ||
@@ -48,3 +49,2 @@ * Based on the provided pubkey and derived path | ||
private static getSolanaApi; | ||
private static scheduleOnExitClose; | ||
/** | ||
@@ -63,2 +63,7 @@ * Signing versioned transaction message with ledger | ||
/** | ||
* From provided Solana API and derived path | ||
* it returns the public key of the derived path. | ||
*/ | ||
export declare function getPublicKey(solanaApi: Solana, derivedPath: string): Promise<PublicKey>; | ||
/** | ||
* Parsing string as ledger url that could be in format of url or derivation path. | ||
@@ -74,4 +79,20 @@ * Some of the examples (trying to be compatible with solana cli https://github.com/solana-labs/solana/blob/v1.14.19/clap-utils/src/keypair.rs#L613) | ||
export declare function parseLedgerUrl(ledgerUrl: string): { | ||
pubkey: PublicKey | undefined; | ||
parsedPubkey: PublicKey | undefined; | ||
parsedDerivedPath: string; | ||
}; | ||
export declare function searchDerivedPathFromPubkey(pubkey: PublicKey, logger?: LoggerPlaceholder | undefined, heuristicDepth?: number | undefined, heuristicWide?: number | undefined): Promise<{ | ||
derivedPath: string; | ||
solanaApi: Solana; | ||
transport: TransportNodeHid; | ||
} | null>; | ||
/** | ||
* | ||
* Parsing the derived path string to check heuristic depth and wide. | ||
* | ||
* When the derived path is e.g., 44'/501'/0/0/5 then | ||
* the wide will be 3, depth will be max of the provided numbers as it's 5. | ||
*/ | ||
export declare function getHeuristicDepthAndWide(derivedPath: string, defaultDepth?: number, defaultWide?: number): { | ||
depth: number; | ||
wide: number; | ||
}; |
@@ -29,3 +29,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.parseLedgerUrl = exports.LedgerWallet = exports.DEFAULT_DERIVATION_PATH = exports.SOLANA_LEDGER_BIP44_BASE_REGEXP = exports.SOLANA_LEDGER_BIP44_BASE_PATH = exports.CLI_LEDGER_URL_PREFIX = void 0; | ||
exports.getHeuristicDepthAndWide = exports.searchDerivedPathFromPubkey = exports.parseLedgerUrl = exports.getPublicKey = exports.LedgerWallet = exports.DEFAULT_DERIVATION_PATH = exports.SOLANA_LEDGER_BIP44_BASE_REGEXP = exports.SOLANA_LEDGER_BIP44_BASE_PATH = exports.CLI_LEDGER_URL_PREFIX = void 0; | ||
const hw_app_solana_1 = __importDefault(require("@ledgerhq/hw-app-solana")); | ||
@@ -36,3 +36,2 @@ const hw_transport_node_hid_noevents_1 = __importStar(require("@ledgerhq/hw-transport-node-hid-noevents")); | ||
const ts_common_1 = require("@marinade.finance/ts-common"); | ||
const process_1 = require("process"); | ||
exports.CLI_LEDGER_URL_PREFIX = 'usb://ledger'; | ||
@@ -42,5 +41,6 @@ exports.SOLANA_LEDGER_BIP44_BASE_PATH = "44'/501'"; | ||
exports.DEFAULT_DERIVATION_PATH = exports.SOLANA_LEDGER_BIP44_BASE_PATH; | ||
const IN_LIB_TRANSPORT_CACHE = new Map(); | ||
class LedgerWallet { | ||
/** | ||
* "Constructor" of SolanaLedger class. | ||
* "Constructor" of Solana Ledger to be opened and worked as a Wallet. | ||
* From ledger url in format of usb://ledger[/<pubkey>[?key=<number>] | ||
@@ -50,12 +50,12 @@ * creates wrapper class around Solana ledger device from '@ledgerhq/hw-app-solana' package. | ||
static async instance(ledgerUrl = '0', logger = undefined) { | ||
const { pubkey, derivedPath: parsedDerivedPath } = parseLedgerUrl(ledgerUrl); | ||
// getting | ||
const { api, derivedPath } = await LedgerWallet.getSolanaApi(pubkey, parsedDerivedPath, logger); | ||
const publicKey = await LedgerWallet.getPublicKey(api, derivedPath); | ||
return new LedgerWallet(api, derivedPath, publicKey); | ||
// parsedPubkey could be undefined when not provided in url string | ||
const { parsedPubkey, parsedDerivedPath } = parseLedgerUrl(ledgerUrl); | ||
const { api, pubkey, derivedPath } = await LedgerWallet.getSolanaApi(parsedPubkey, parsedDerivedPath, logger); | ||
return new LedgerWallet(api, derivedPath, pubkey, logger); | ||
} | ||
constructor(solanaApi, derivedPath, publicKey) { | ||
constructor(solanaApi, derivedPath, publicKey, logger = undefined) { | ||
this.solanaApi = solanaApi; | ||
this.derivedPath = derivedPath; | ||
this.publicKey = publicKey; | ||
this.logger = logger; | ||
} | ||
@@ -81,6 +81,2 @@ async signTransaction(tx) { | ||
} | ||
static async getPublicKey(solanaApi, derivedPath) { | ||
const { address: bufAddress } = await solanaApi.getAddress(derivedPath); | ||
return new web3_js_1.PublicKey(bufAddress); | ||
} | ||
/** | ||
@@ -103,16 +99,13 @@ * Based on the provided pubkey and derived path | ||
if (pubkey === undefined) { | ||
// taking first device | ||
transport = await hw_transport_node_hid_noevents_1.default.open(''); | ||
LedgerWallet.scheduleOnExitClose(transport); | ||
// we don't know where to search for the derived path and thus taking first device | ||
// we will search for the provided derived path at this first device when signing message | ||
// when pubkey is defined we search all available devices to find a match of pubkey and derived path | ||
transport = (await openTransports(ledgerDevices[0]))[0]; | ||
} | ||
else { | ||
const openedTransports = []; | ||
for (const device of ledgerDevices) { | ||
openedTransports.push(await hw_transport_node_hid_noevents_1.default.open(device.path)); | ||
} | ||
LedgerWallet.scheduleOnExitClose(...openedTransports); | ||
const openedTransports = await openTransports(...ledgerDevices); | ||
// if derived path is provided let's check if matches the pubkey | ||
for (const openedTransport of openedTransports) { | ||
const solanaApi = new hw_app_solana_1.default(openedTransport); | ||
const ledgerPubkey = await LedgerWallet.getPublicKey(solanaApi, derivedPath); | ||
const ledgerPubkey = await getPublicKey(solanaApi, derivedPath); | ||
if (ledgerPubkey.equals(pubkey)) { | ||
@@ -124,61 +117,20 @@ transport = openedTransport; | ||
if (transport === undefined) { | ||
(0, ts_common_1.logInfo)(logger, `Ledger device does not provide pubkey ${pubkey.toBase58()} ` + | ||
`at defined derivation path ${derivedPath}, searching...`); | ||
// parsing the derived path to check heuristic depth and wide | ||
// when the derived path is 44'/501'/0/0/5 | ||
// then the wide will be 3, depth will be max of numbers as it's 5 | ||
let splitDerivedPath = derivedPath.split('/'); | ||
if (splitDerivedPath.length > 2) { | ||
splitDerivedPath = splitDerivedPath.slice(2); | ||
heuristicWide = splitDerivedPath.length; | ||
heuristicDepth = Math.max(heuristicDepth, ...splitDerivedPath.map(v => parseFloat(v))); | ||
(0, ts_common_1.logInfo)(logger, `Public key ${pubkey.toBase58()} has not been found at the default or provided ` + | ||
`derivation path ${derivedPath}. Going to search, it will take a while...`); | ||
const { depth, wide } = getHeuristicDepthAndWide(derivedPath, heuristicDepth, heuristicWide); | ||
const searchedData = await searchDerivedPathFromPubkey(pubkey, logger, depth, wide); | ||
if (searchedData !== null) { | ||
transport = searchedData.transport; | ||
derivedPath = searchedData.derivedPath; | ||
(0, ts_common_1.logInfo)(logger, `For public key ${pubkey.toBase58()} has been found derived path ${derivedPath}`); | ||
} | ||
const heuristicsCombinations = (0, utils_1.generateAllCombinations)(heuristicDepth, heuristicWide); | ||
for (const openedTransport of openedTransports) { | ||
const solanaApi = new hw_app_solana_1.default(openedTransport); | ||
for (const combination of heuristicsCombinations) { | ||
const strCombination = combination.map(v => v.toString()); | ||
strCombination.unshift(exports.SOLANA_LEDGER_BIP44_BASE_PATH); | ||
const heuristicDerivedPath = strCombination.join('/'); | ||
(0, ts_common_1.logDebug)(logger, `search loop: ${heuristicDerivedPath}`); | ||
const ledgerPubkey = await LedgerWallet.getPublicKey(solanaApi, heuristicDerivedPath); | ||
if (ledgerPubkey.equals(pubkey)) { | ||
transport = openedTransport; | ||
derivedPath = heuristicDerivedPath; | ||
(0, ts_common_1.logInfo)(logger, `Using derived path ${derivedPath}, pubkey ${pubkey.toBase58()}`); | ||
break; // the last found transport is the one we need | ||
} | ||
} | ||
if (transport !== undefined) { | ||
break; // the last transport found as the last one is the one we need | ||
} | ||
} | ||
// let's close all the opened transports that are not the ones we need | ||
openedTransports.filter(t => t !== transport).forEach(t => t.close()); | ||
} | ||
if (transport === undefined) { | ||
throw new Error('Available ledger devices does not provide pubkey ' + | ||
pubkey.toBase58() + | ||
' for derivation path ' + | ||
derivedPath); | ||
} | ||
} | ||
return { api: new hw_app_solana_1.default(transport), derivedPath }; | ||
} | ||
// trying to close all provided transports in case of abrupt exit, or just exit | ||
static scheduleOnExitClose(...transports) { | ||
if (process) { | ||
const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'exit']; | ||
signals.forEach(signal => process.on(signal, () => { | ||
for (const openedTransport of transports) { | ||
try { | ||
openedTransport.close(); | ||
} | ||
catch (e) { | ||
// ignore error and go to next transport | ||
} | ||
(0, process_1.exit)(); | ||
} | ||
})); | ||
if (transport === undefined) { | ||
throw new Error('Available ledger devices does not provide pubkey ' + | ||
`'${pubkey === null || pubkey === void 0 ? void 0 : pubkey.toBase58()}' for derivation path '${derivedPath}'`); | ||
} | ||
const api = new hw_app_solana_1.default(transport); | ||
pubkey = await getPublicKey(api, derivedPath); | ||
return { api, derivedPath, pubkey }; | ||
} | ||
@@ -196,2 +148,5 @@ /** | ||
async signMessage(message) { | ||
(0, ts_common_1.logDebug)(this.logger, 'signing message with pubkey ' + | ||
(await getPublicKey(this.solanaApi, this.derivedPath)).toBase58() + | ||
` of derived path ${this.derivedPath}`); | ||
const { signature } = await this.solanaApi.signTransaction(this.derivedPath, Buffer.from(message.serialize())); | ||
@@ -203,2 +158,11 @@ return signature; | ||
/** | ||
* From provided Solana API and derived path | ||
* it returns the public key of the derived path. | ||
*/ | ||
async function getPublicKey(solanaApi, derivedPath) { | ||
const { address: bufAddress } = await solanaApi.getAddress(derivedPath); | ||
return new web3_js_1.PublicKey(bufAddress); | ||
} | ||
exports.getPublicKey = getPublicKey; | ||
/** | ||
* Parsing string as ledger url that could be in format of url or derivation path. | ||
@@ -218,4 +182,4 @@ * Some of the examples (trying to be compatible with solana cli https://github.com/solana-labs/solana/blob/v1.14.19/clap-utils/src/keypair.rs#L613) | ||
} | ||
let pubkey; | ||
let derivedPath; | ||
let parsedPubkey; | ||
let parsedDerivedPath; | ||
// removal of the prefix + optional slash | ||
@@ -243,16 +207,16 @@ const ledgerUrlRegexp = new RegExp(exports.CLI_LEDGER_URL_PREFIX + '/?'); | ||
//case: usb://ledger/<pubkey> | ||
pubkey = parsePubkey(parts[0]); | ||
derivedPath = exports.DEFAULT_DERIVATION_PATH; | ||
parsedPubkey = parsePubkey(parts[0]); | ||
parsedDerivedPath = exports.DEFAULT_DERIVATION_PATH; | ||
} | ||
else if (parts.length === 2) { | ||
//case: usb://ledger/<pubkey>?key=<number> | ||
pubkey = parsePubkey(parts[0]); | ||
parsedPubkey = parsePubkey(parts[0]); | ||
const key = parts[1]; | ||
if (key === '') { | ||
// case: usb://ledger/<pubkey>?key= | ||
derivedPath = exports.DEFAULT_DERIVATION_PATH; | ||
parsedDerivedPath = exports.DEFAULT_DERIVATION_PATH; | ||
} | ||
else if (exports.SOLANA_LEDGER_BIP44_BASE_REGEXP.test(key)) { | ||
// case: usb://ledger/<pubkey>?key=44'/501'/<number> | ||
derivedPath = key; | ||
parsedDerivedPath = key; | ||
} | ||
@@ -262,3 +226,3 @@ else { | ||
const keyTrimmed = key.replace(/^\//, ''); | ||
derivedPath = exports.SOLANA_LEDGER_BIP44_BASE_PATH + '/' + keyTrimmed; | ||
parsedDerivedPath = exports.SOLANA_LEDGER_BIP44_BASE_PATH + '/' + keyTrimmed; | ||
} | ||
@@ -270,5 +234,82 @@ } | ||
} | ||
return { pubkey, derivedPath }; | ||
return { parsedPubkey, parsedDerivedPath }; | ||
} | ||
exports.parseLedgerUrl = parseLedgerUrl; | ||
async function searchDerivedPathFromPubkey(pubkey, logger = undefined, heuristicDepth = 10, heuristicWide = 3) { | ||
const ledgerDevices = (0, hw_transport_node_hid_noevents_1.getDevices)(); | ||
if (ledgerDevices.length === 0) { | ||
throw new Error('No ledger device found'); | ||
} | ||
const openedTransports = await openTransports(...ledgerDevices); | ||
const heuristicsCombinations = (0, utils_1.generateAllCombinations)(heuristicDepth, heuristicWide); | ||
for (const transport of openedTransports) { | ||
const solanaApi = new hw_app_solana_1.default(transport); | ||
for (const combination of heuristicsCombinations) { | ||
const strCombination = combination.map(v => v.toString()); | ||
strCombination.unshift(exports.SOLANA_LEDGER_BIP44_BASE_PATH); | ||
const heuristicDerivedPath = strCombination.join('/'); | ||
(0, ts_common_1.logDebug)(logger, `search loop: ${heuristicDerivedPath}`); | ||
const ledgerPubkey = await getPublicKey(solanaApi, heuristicDerivedPath); | ||
if (ledgerPubkey.equals(pubkey)) { | ||
(0, ts_common_1.logDebug)(logger, `Found path ${heuristicDerivedPath}, pubkey ${pubkey.toBase58()}`); | ||
return { derivedPath: heuristicDerivedPath, solanaApi, transport }; | ||
} | ||
} | ||
} | ||
return null; | ||
} | ||
exports.searchDerivedPathFromPubkey = searchDerivedPathFromPubkey; | ||
/** | ||
* | ||
* Parsing the derived path string to check heuristic depth and wide. | ||
* | ||
* When the derived path is e.g., 44'/501'/0/0/5 then | ||
* the wide will be 3, depth will be max of the provided numbers as it's 5. | ||
*/ | ||
function getHeuristicDepthAndWide(derivedPath, defaultDepth = 10, defaultWide = 3) { | ||
let depth = defaultDepth; | ||
let wide = defaultWide; | ||
let splitDerivedPath = derivedPath.split('/'); | ||
// we expect derived path starts with solana derivation path 44'/501' | ||
// going to check parts after first 2 | ||
if (splitDerivedPath.length > 2) { | ||
splitDerivedPath = splitDerivedPath.slice(2); | ||
wide = Math.max(defaultWide, splitDerivedPath.length); | ||
depth = Math.max(defaultDepth, ...splitDerivedPath.map(v => parseFloat(v))); | ||
} | ||
return { depth, wide }; | ||
} | ||
exports.getHeuristicDepthAndWide = getHeuristicDepthAndWide; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
async function openTransports(...devices) { | ||
const transports = []; | ||
for (const device of devices) { | ||
let transport = IN_LIB_TRANSPORT_CACHE.get(device.path); | ||
if (transport === undefined) { | ||
transport = await hw_transport_node_hid_noevents_1.default.open(device.path); | ||
scheduleTransportCloseOnExit(transport); | ||
IN_LIB_TRANSPORT_CACHE.set(device.path, transport); | ||
} | ||
transports.push(transport); | ||
} | ||
return transports; | ||
} | ||
/** | ||
* Trying to close all provided transports in case of abrupt exit, or just exit | ||
* (ignoring errors when closing the transport). | ||
* | ||
* @param transports set of transport to be closed on exit | ||
*/ | ||
function scheduleTransportCloseOnExit(...transports) { | ||
(0, ts_common_1.scheduleOnExit)(() => { | ||
for (const openedTransport of transports) { | ||
try { | ||
openedTransport.close(); | ||
} | ||
catch (e) { | ||
// ignore error and go to next transport | ||
} | ||
} | ||
}); | ||
} | ||
//# sourceMappingURL=ledger.js.map |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
36145
518