]> git.codecow.com Git - libnemo.git/commitdiff
Extract open and close functions. Adjust doc comments.
authorChris Duncan <chris@codecow.com>
Mon, 18 May 2026 13:30:20 +0000 (06:30 -0700)
committerChris Duncan <chris@codecow.com>
Mon, 18 May 2026 13:30:20 +0000 (06:30 -0700)
src/lib/ledger/close.ts [new file with mode: 0644]
src/lib/ledger/index.ts
src/lib/ledger/open.ts [new file with mode: 0644]

diff --git a/src/lib/ledger/close.ts b/src/lib/ledger/close.ts
new file mode 100644 (file)
index 0000000..18ffd7a
--- /dev/null
@@ -0,0 +1,15 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { LedgerResponse, LedgerTransport, openTimeout, listenTimeout, APDU_CODES, STATUS_CODES } from '.'
+import { bytes } from '../convert'
+
+export async function _close (transport: LedgerTransport): Promise<LedgerResponse> {
+       const t = await transport.create(openTimeout, listenTimeout)
+       const response = await t
+               .send(0xb0, 0xa7, APDU_CODES.paramUnused, APDU_CODES.paramUnused)
+               .then((res: Buffer) => bytes.toDec(res))
+               .catch((err: any) => err.statusCode)
+               .finally(async () => await t.close()) as number
+       return new Promise(r => setTimeout(r, 1000, { status: STATUS_CODES[response] }))
+}
index 73a0805607af0f73f2661d0aa013d8114bea675e..50ae83bcc265096a6842d86804b34c10547fb30f 100644 (file)
@@ -7,7 +7,7 @@ import { default as TransportHID } from '@ledgerhq/hw-transport-webhid'
 import { default as TransportUSB } from '@ledgerhq/hw-transport-webusb'
 import { Block } from '../block'
 import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET } from '../constants'
-import { bytes, dec, utf8 } from '../convert'
+import { dec, utf8 } from '../convert'
 import { Rpc } from '../rpc'
 import { Wallet } from '../wallet'
 import { _account } from './account'
@@ -15,6 +15,8 @@ import { _cache } from './cache'
 import { _connect } from './connect'
 import { queue } from './queue'
 import { signBlock, signNonce } from './sign'
+import { _open } from './open'
+import { _close } from './close'
 
 export type LedgerStatus = 'UNSUPPORTED' | 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED'
 export type LedgerTransport = typeof TransportHID | typeof TransportBLE | typeof TransportUSB
@@ -43,11 +45,13 @@ export const APDU_CODES: Record<string, number> = Object.freeze({
        signNonce: 0x05,
        paramUnused: 0x00
 })
+
 export const DERIVATION_PATH: Uint8Array = new Uint8Array([
        APDU_CODES.bip32DerivationLevel,
        ...dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4),
        ...dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)
 ])
+
 export const STATUS_CODES: Readonly<Record<number, string>> = Object.freeze({
        ...Object.fromEntries(Object.entries(StatusCodes).map(([k, v]) => [+v, k])),
        0x6807: 'APPLICATION_NOT_INSTALLED',
@@ -55,17 +59,19 @@ export const STATUS_CODES: Readonly<Record<number, string>> = Object.freeze({
        0x6a81: 'INVALID_SIGNATURE',
        0x6a82: 'CACHE_MISS'
 })
+
 export const listenTimeout: 30000 = 30000
+
 export const openTimeout: 3000 = 3000
 
 /**
-* Ledger hardware wallet created by communicating with a Ledger device via ADPU
-* 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.
-*
-* https://github.com/roosmaa/ledger-app-nano/blob/master/doc/nano.md
-*/
+ * Ledger hardware wallet created by communicating with a Ledger device via ADPU
+ * 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.
+ *
+ * https://github.com/roosmaa/ledger-app-nano/blob/master/doc/nano.md
+ */
 export class Ledger {
        static #isPolling: boolean = false
        static #status: LedgerStatus = 'DISCONNECTED'
@@ -78,9 +84,9 @@ export class Ledger {
        static removeEventListener = this.#eventTarget.removeEventListener.bind(this.#eventTarget)
 
        /**
-       * Check which transport protocols are supported by the browser and return the
-       * transport type according to the following priorities: HID, Bluetooth, USB.
-       */
+        * Check which transport protocols are supported by the browser and return the
+        * transport type according to the following priorities: HID, Bluetooth, USB.
+        */
        static get isUnsupported (): boolean {
                if (this.#status === 'UNSUPPORTED') {
                        return true
@@ -106,18 +112,18 @@ export class Ledger {
        }
 
        /**
-       * Vendor ID assigned to Ledger for HID and USB interfaces.
-       * https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/devices/src/index.ts#L164
-       */
+        * Vendor ID assigned to Ledger for HID and USB interfaces.
+        * https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/devices/src/index.ts#L164
+        */
        static get ledgerVendorId (): 0x2c97 {
                return 0x2c97
        }
 
        /**
-       * Status of the Ledger device connection.
-       *
-       * UNSUPPORTED | DISCONNECTED | BUSY | LOCKED | CONNECTED
-       */
+        * Status of the Ledger device connection.
+        *
+        * UNSUPPORTED | DISCONNECTED | BUSY | LOCKED | CONNECTED
+        */
        static get status (): LedgerStatus {
                return this.isUnsupported ? 'UNSUPPORTED' : this.#status
        }
@@ -168,9 +174,9 @@ export class Ledger {
        }
 
        /**
-       * Clears Ledger connections from HID and USB interfaces and stops polling for
-       * connection updates.
-       */
+        * Clears Ledger connections from HID and USB interfaces and stops polling for
+        * connection updates.
+        */
        static async disconnect (): Promise<void> {
                queue(async () => {
                        try {
@@ -191,33 +197,33 @@ export class Ledger {
        }
 
        /**
-       * Sign a nonce with the Ledger device. The nonce must be a string that encodes
-       * to 16 bytes using UTF-8. For this reason, although any Unicode characters
-       * can be used, it is recommended to only pass printable ASCII which encodes to
-       * one byte per character.
-       *
-       * The actual message signed is a string which can be expressed as the
-       * following template literal:
-       *
-       * `Nano Signed Nonce:\n${nonce_bytes_as_hex}`
-       *
-       * IMPORTANT: The current version of the Nano app for Ledger devices will NOT
-       * prompt users to confirm the signature. If valid, the nonce will immediately
-       * be signed and the signature returned without user interaction, similar to
-       * how receive blocks are automatically signed when auto-receive is configured.
-       * Plan for this eventuality if you implement this method to sign nonces.
-       *
-       * @param {number} index - Account number
-       * @param {string} nonce - 128-bit value to sign
-       */
+        * Sign a nonce with the Ledger device. The nonce must be a string that encodes
+        * to 16 bytes using UTF-8. For this reason, although any Unicode characters
+        * can be used, it is recommended to only pass printable ASCII which encodes to
+        * one byte per character.
+        *
+        * The actual message signed is a string which can be expressed as the
+        * following template literal:
+        *
+        * `Nano Signed Nonce:\n${nonce_bytes_as_hex}`
+        *
+        * IMPORTANT: The current version of the Nano app for Ledger devices will NOT
+        * prompt users to confirm the signature. If valid, the nonce will immediately
+        * be signed and the signature returned without user interaction, similar to
+        * how receive blocks are automatically signed when auto-receive is configured.
+        * Plan for this eventuality if you implement this method to sign nonces.
+        *
+        * @param {number} index - Account number
+        * @param {string} nonce - 128-bit value to sign
+        */
        static async sign (index: number, nonce: string): Promise<string>
        /**
-       * Sign a block with the Ledger device.
-       *
-       * @param {number} index - Account number
-       * @param {Block} block - Block data to sign
-       * @param {Block} [frontier] - Previous block data to cache in the device
-       */
+        * Sign a block with the Ledger device.
+        *
+        * @param {number} index - Account number
+        * @param {Block} block - Block data to sign
+        * @param {Block} [frontier] - Previous block data to cache in the device
+        */
        static async sign (index: number, block: Block, frontier?: Block): Promise<string>
        static async sign (index: number, data: string | Block, frontier?: Block): Promise<string> {
                try {
@@ -293,21 +299,21 @@ export class Ledger {
        }
 
        /**
-       * Checks whether a given seed matches the wallet seed. The wallet must be
-       * unlocked prior to verification.
-       *
-       * @param {string} seed - Hexadecimal seed to be matched against the wallet data
-       * @returns True if input matches wallet seed
-       */
+        * Checks whether a given seed matches the wallet seed. The wallet must be
+        * unlocked prior to verification.
+        *
+        * @param {string} seed - Hexadecimal seed to be matched against the wallet data
+        * @returns True if input matches wallet seed
+        */
        static async verify (seed: string): Promise<boolean>
        /**
-       * Checks whether a given mnemonic phrase matches the wallet mnemonic. If a
-       * personal salt was used when generating the mnemonic, it cannot be verified.
-       * The wallet must be unlocked prior to verification.
-       *
-       * @param {string} mnemonic - Phrase to be matched against the wallet data
-       * @returns True if input matches wallet mnemonic
-       */
+        * Checks whether a given mnemonic phrase matches the wallet mnemonic. If a
+        * personal salt was used when generating the mnemonic, it cannot be verified.
+        * The wallet must be unlocked prior to verification.
+        *
+        * @param {string} mnemonic - Phrase to be matched against the wallet data
+        * @returns True if input matches wallet mnemonic
+        */
        static async verify (mnemonic: string): Promise<boolean>
        static async verify (secret: string): Promise<boolean> {
                const testWallet = await Wallet.load('BIP-44', '', secret)
@@ -328,66 +334,49 @@ export class Ledger {
        }
 
        /**
-       * Close the currently running app and return to the device dashboard.
-       *
-       * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#quit-application
-       *
-       * This command resets the internal USB connection of the device which can
-       * cause subsequent commands to fail if called too quickly. A one-second delay
-       * is implemented in this method to mitigate the issue.
-       *
-       * https://github.com/LedgerHQ/ledger-live/issues/4964#issuecomment-1878361157
-       *
-       * @returns Status of command
-       */
+        * Close the currently running app and return to the device dashboard.
+        *
+        * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#quit-application
+        *
+        * This command resets the internal USB connection of the device which can
+        * cause subsequent commands to fail if called too quickly. A one-second delay
+        * is implemented in this method to mitigate the issue.
+        *
+        * https://github.com/LedgerHQ/ledger-live/issues/4964#issuecomment-1878361157
+        *
+        * @returns Status of command
+        */
        static async #close (): Promise<LedgerResponse> {
-               return queue(async () => {
-                       const transport = await this.#transport.create(openTimeout, listenTimeout)
-                       const response = await transport
-                               .send(0xb0, 0xa7, APDU_CODES.paramUnused, APDU_CODES.paramUnused)
-                               .then((res: Buffer) => bytes.toDec(res))
-                               .catch((err: any) => err.statusCode)
-                               .finally(async () => await transport.close()) as number
-                       return new Promise(r => setTimeout(r, 1000, { status: STATUS_CODES[response] }))
-               })
+               return queue(async () => _close(this.#transport))
        }
 
        /**
-       * Open the Nano app by launching a user flow.
-       *
-       * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#open-application
-       *
-       * This command resets the internal USB connection of the device which can
-       * cause subsequent commands to fail if called too quickly. A one-second delay
-       * is implemented in this method to mitigate the issue.
-       *
-       * https://github.com/LedgerHQ/ledger-live/issues/4964#issuecomment-1878361157
-       *
-       * @returns Status of command
-       */
+        * Open the Nano app by launching a user flow.
+        *
+        * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#open-application
+        *
+        * This command resets the internal USB connection of the device which can
+        * cause subsequent commands to fail if called too quickly. A one-second delay
+        * is implemented in this method to mitigate the issue.
+        *
+        * https://github.com/LedgerHQ/ledger-live/issues/4964#issuecomment-1878361157
+        *
+        * @returns Status of command
+        */
        static async #open (): Promise<LedgerResponse> {
-               return queue(async () => {
-                       const name = new TextEncoder().encode('Nano')
-                       const transport = await this.#transport.create(openTimeout, listenTimeout)
-                       const response = await transport
-                               .send(0xe0, 0xd8, APDU_CODES.paramUnused, APDU_CODES.paramUnused, name as Buffer)
-                               .then((res: Buffer) => bytes.toDec(res))
-                               .catch((err: any) => err.statusCode)
-                               .finally(async () => await transport.close()) as number
-                       return new Promise(r => setTimeout(r, 1000, { status: STATUS_CODES[response] }))
-               })
+               return queue(async () => _open(this.#transport))
        }
 
        /**
-       * Checks for connected HID and USB Ledger devices to which access has been
-       * previously granted in response to a `requestDevice()` call. It does not work
-       * for Bluetooth interfaces.
-       *
-       * If no devices have been granted access, or if none with access are currently
-       * connected, the poll will set the 'DISCONNECTED' status accordingly and
-       * return. Otherwise, it will attempt to determine the actual status of the
-       * device.
-       */
+        * Checks for connected HID and USB Ledger devices to which access has been
+        * previously granted in response to a `requestDevice()` call. It does not work
+        * for Bluetooth interfaces.
+        *
+        * If no devices have been granted access, or if none with access are currently
+        * connected, the poll will set the 'DISCONNECTED' status accordingly and
+        * return. Otherwise, it will attempt to determine the actual status of the
+        * device.
+        */
        static async #poll (): Promise<void> {
                try {
                        const isHidPaired = (await navigator.hid?.getDevices?.() ?? [])
@@ -408,8 +397,8 @@ export class Ledger {
        }
 
        /**
-       * Sets the Ledger status and emits an event.
-       */
+        * Sets the Ledger status and emits an event.
+        */
        static #setStatus (value: LedgerStatus) {
                if (this.#status !== value) {
                        this.#status = value
diff --git a/src/lib/ledger/open.ts b/src/lib/ledger/open.ts
new file mode 100644 (file)
index 0000000..5ffc2b6
--- /dev/null
@@ -0,0 +1,16 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { LedgerResponse, LedgerTransport, openTimeout, listenTimeout, APDU_CODES, STATUS_CODES } from '.'
+import { bytes } from '../convert'
+
+export async function _open (transport: LedgerTransport): Promise<LedgerResponse> {
+       const name = new TextEncoder().encode('Nano')
+       const t = await transport.create(openTimeout, listenTimeout)
+       const response = await t
+               .send(0xe0, 0xd8, APDU_CODES.paramUnused, APDU_CODES.paramUnused, name as Buffer)
+               .then((res: Buffer) => bytes.toDec(res))
+               .catch((err: any) => err.statusCode)
+               .finally(async () => await t.close()) as number
+       return new Promise(r => setTimeout(r, 1000, { status: STATUS_CODES[response] }))
+}