From ed88dfac53c194be0b878aeea7601c4a410c0119 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Fri, 25 Jul 2025 05:10:41 -0700 Subject: [PATCH] Refactor Ledger browser support check to static sync method and deprecate redundant init method. Skip Ledger tests if browser unsupported. Update Ledger types. --- src/lib/wallets/ledger-wallet.ts | 72 +++++++++++++++----------------- src/types.d.ts | 23 ++++------ test/test.ledger.mjs | 8 +++- 3 files changed, 50 insertions(+), 53 deletions(-) diff --git a/src/lib/wallets/ledger-wallet.ts b/src/lib/wallets/ledger-wallet.ts index a813183..24f8822 100644 --- a/src/lib/wallets/ledger-wallet.ts +++ b/src/lib/wallets/ledger-wallet.ts @@ -18,11 +18,6 @@ import { DeviceStatus, KeyPair, LedgerAccountResponse, LedgerResponse, LedgerSig * calls. This wallet does not feature any seed nor mnemonic phrase as all * private keys are held in the secure chip of the device. As such, the user * is responsible for using Ledger technology to back up these pieces of data. -* -* Usage of this wallet is generally controlled by calling functions of the -* `ledger` object. For example, the wallet interface should have a button to -* initiate a device connection by calling `wallet.ledger.connect()`. For more -* information, refer to the ledger.js service file. */ export class LedgerWallet extends Wallet { static #isInternal: boolean = false @@ -49,25 +44,44 @@ export class LedgerWallet extends Wallet { } /** - * Check which transport protocols are supported by the browser and set the + * Check which transport protocols are supported by the browser and return the * transport type according to the following priorities: Bluetooth, USB, HID. */ - async checkBrowserSupport (): Promise { + static checkBrowserSupport (): typeof TransportBLE | typeof TransportUSB | typeof TransportHID { console.log('Checking browser Ledger support...') try { - if (await TransportBLE.isSupported()) { + if (typeof globalThis.navigator?.bluetooth?.getDevices === 'function') { return TransportBLE } - if (await TransportUSB.isSupported()) { + if (typeof globalThis.navigator?.usb?.getDevices === 'function') { return TransportUSB } - if (await TransportHID.isSupported()) { + if (typeof globalThis.navigator?.hid?.getDevices === 'function') { return TransportHID } } catch { } throw new Error('Unsupported browser') } + /** + * Creates a new Ledger hardware wallet communication layer by dynamically + * importing the ledger.js service. + * + * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object + */ + static async create (): Promise { + try { + const transport = LedgerWallet.checkBrowserSupport() + const id = await Entropy.create(16) + LedgerWallet.#isInternal = true + const wallet = new this(id) + wallet.DynamicTransport = transport + return wallet + } catch (err) { + throw new Error('failed to initialize Ledger wallet', { cause: err }) + } + } + /** * Check if the Nano app is currently open and set device status accordingly. * @@ -96,20 +110,6 @@ export class LedgerWallet extends Wallet { return this.status } - /** - * Creates a new Ledger hardware wallet communication layer by dynamically - * importing the ledger.js service. - * - * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object - */ - static async create (): Promise { - const id = await Entropy.create(16) - LedgerWallet.#isInternal = true - const wallet = new this(id) - await wallet.init() - return wallet - } - /** * Removes encrypted secrets in storage and releases variable references to * allow garbage collection. @@ -121,15 +121,6 @@ export class LedgerWallet extends Wallet { } } - async init (): Promise { - try { - this.DynamicTransport = await this.checkBrowserSupport() - // await this.connect() - } catch (err) { - throw new Error('Failed to initialize Ledger wallet', { cause: err }) - } - } - /** * Revokes permission granted by the user to access the Ledger device. * @@ -188,11 +179,16 @@ export class LedgerWallet extends Wallet { if (typeof id !== 'string' || id === '') { throw new TypeError('Wallet ID is required to restore') } - LedgerWallet.#isInternal = true - id = id.replace('libnemo_', '') - const wallet = new this(await Entropy.import(id)) - await wallet.init() - return wallet + try { + const transport = LedgerWallet.checkBrowserSupport() + LedgerWallet.#isInternal = true + const wallet = new this(await Entropy.import(id)) + wallet.DynamicTransport = transport + return wallet + } catch (err) { + console.error(err) + throw new Error('failed to restore wallet', { cause: err }) + } } /** diff --git a/src/types.d.ts b/src/types.d.ts index 6180d2f..e3c0e25 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -920,11 +920,6 @@ interface LedgerSignResponse extends LedgerResponse { * calls. This wallet does not feature any seed nor mnemonic phrase as all * private keys are held in the secure chip of the device. As such, the user * is responsible for using Ledger technology to back up these pieces of data. -* -* Usage of this wallet is generally controlled by calling functions of the -* `ledger` object. For example, the wallet interface should have a button to -* initiate a device connection by calling `wallet.ledger.connect()`. For more -* information, refer to the ledger.js service file. */ export declare class LedgerWallet extends Wallet { #private @@ -934,10 +929,17 @@ export declare class LedgerWallet extends Wallet { DynamicTransport: typeof TransportBLE | typeof TransportUSB | typeof TransportHID private constructor () /** - * Check which transport protocols are supported by the browser and set the + * Check which transport protocols are supported by the browser and return the * transport type according to the following priorities: Bluetooth, USB, HID. */ - checkBrowserSupport (): Promise + static checkBrowserSupport (): typeof TransportBLE | typeof TransportUSB | typeof TransportHID + /** + * Creates a new Ledger hardware wallet communication layer by dynamically + * importing the ledger.js service. + * + * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object + */ + static create (): Promise /** * Check if the Nano app is currently open and set device status accordingly. * @@ -949,13 +951,6 @@ export declare class LedgerWallet extends Wallet { */ connect (): Promise /** - * Creates a new Ledger hardware wallet communication layer by dynamically - * importing the ledger.js service. - * - * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object - */ - static create (): Promise - /** * Removes encrypted secrets in storage and releases variable references to * allow garbage collection. */ diff --git a/test/test.ledger.mjs b/test/test.ledger.mjs index 384c3d4..c334655 100644 --- a/test/test.ledger.mjs +++ b/test/test.ledger.mjs @@ -35,13 +35,19 @@ if (isNode) { const rpc = new Rpc(env.NODE_URL ?? '', env.API_KEY_NAME) +let isUnsupported = true +try { + LedgerWallet.checkBrowserSupport() + isUnsupported = false +} catch {} + /** * HID interactions require user gestures, so to reduce clicks, the variables * shared among tests like wallet and account are declared at the top-level. */ await Promise.all([ /* node:coverage disable */ - suite('Ledger hardware wallet', { skip: false || isNode }, async () => { + suite('Ledger hardware wallet', { skip: false || isNode || isUnsupported }, async () => { let wallet, account, openBlock, sendBlock, receiveBlock -- 2.47.3