]> git.codecow.com Git - libnemo.git/commitdiff
Fix unfinished changes to move connect and also move account.
authorChris Duncan <chris@zoso.dev>
Sat, 16 May 2026 07:18:42 +0000 (00:18 -0700)
committerChris Duncan <chris@zoso.dev>
Sat, 16 May 2026 07:18:42 +0000 (00:18 -0700)
src/lib/ledger/account.ts [new file with mode: 0644]
src/lib/ledger/connect.ts
src/lib/ledger/index.ts

diff --git a/src/lib/ledger/account.ts b/src/lib/ledger/account.ts
new file mode 100644 (file)
index 0000000..f534011
--- /dev/null
@@ -0,0 +1,41 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { APDU_CODES, DERIVATION_PATH, LedgerAccountResponse, LedgerTransport, STATUS_CODES, listenTimeout, openTimeout } from '.'
+import { HARDENED_OFFSET } from '../constants'
+import { bytes, dec } from '../convert'
+
+export async function _account (transport: LedgerTransport, index: number = 0, show: boolean = false): Promise<LedgerAccountResponse> {
+       try {
+               if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
+                       throw new TypeError('Invalid account index')
+               }
+               const account = dec.toBytes(index + HARDENED_OFFSET, 4)
+               const data = new Uint8Array([...DERIVATION_PATH, ...account])
+
+               const t = await transport.create(openTimeout, listenTimeout)
+               const response = await t
+                       .send(APDU_CODES.class, APDU_CODES.account, show ? 1 : 0, APDU_CODES.paramUnused, data as Buffer)
+                       .catch((err: any) => dec.toBytes(err.statusCode))
+                       .finally(async () => await t.close()) as Uint8Array
+
+               const statusCode = bytes.toDec(response.slice(-2)) as number
+               const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'
+               if (status !== 'OK') {
+                       return { status, publicKey: null, address: null }
+               }
+
+               try {
+                       const publicKey = bytes.toHex(response.slice(0, 32))
+                       const addressLength = response[32]
+                       const address = response.slice(33, 33 + addressLength).toString()
+
+                       return { status, publicKey, address }
+               } catch (err) {
+                       return { status: 'ERROR_PARSING_ACCOUNT', publicKey: null, address: null }
+               }
+       } catch (err: any) {
+               console.error('Ledger.account()', err)
+               return { status: err.message, publicKey: null, address: null }
+       }
+}
index a123c8ab4c0bd0f067cc4fb1254c76717e25b5ed..3e799ac834a95aa605a53aa7b93501c97f2c7acc 100644 (file)
@@ -2,6 +2,7 @@
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
 import { LedgerStatus, LedgerTransport } from '.'
+import { _account } from './account'
 import { version } from './version'
 
 /**
@@ -14,8 +15,8 @@ import { version } from './version'
  * - LOCKED: Nano app is open but the device locked after a timeout
  * - CONNECTED: Nano app is open and listening
  */
-export async function connect (transport: LedgerTransport): Promise<LedgerStatus> {
-       let status = 'DISCONNECTED'
+export async function _connect (transport: LedgerTransport): Promise<LedgerStatus> {
+       let status: LedgerStatus = 'DISCONNECTED'
        try {
                const v = await version(transport)
                if (v.status === 'LOCKED_DEVICE') {
@@ -23,10 +24,10 @@ export async function connect (transport: LedgerTransport): Promise<LedgerStatus
                } else if (v.status !== 'OK') {
                        status = 'DISCONNECTED'
                } else if (v.name === 'Nano') {
-                       const a = await account()
+                       const a = await _account(transport)
                        if (a.status === 'OK') {
                                status = 'CONNECTED'
-                       } else if (status === 'SECURITY_STATUS_NOT_SATISFIED') {
+                       } else if (a.status === 'SECURITY_STATUS_NOT_SATISFIED') {
                                status = 'LOCKED'
                        } else {
                                status = 'DISCONNECTED'
index 66d687307573a38327fb19188608d966ed1c6fe9..ecf70f40b42cec3890734c0639797c5fac0b0378 100644 (file)
@@ -11,6 +11,8 @@ import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET } from '../constants'
 import { bytes, dec, hex, utf8 } from '../convert'
 import { Rpc } from '../rpc'
 import { Wallet } from '../wallet'
+import { _account } from './account'
+import { _connect } from './connect'
 import { queue } from './queue'
 
 export type LedgerStatus = 'UNSUPPORTED' | 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED'
@@ -20,7 +22,7 @@ export interface LedgerResponse {
        status: string
 }
 
-interface LedgerAccountResponse extends LedgerResponse {
+export interface LedgerAccountResponse extends LedgerResponse {
        publicKey: string | null,
        address: string | null
 }
@@ -40,6 +42,11 @@ 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',
@@ -62,11 +69,6 @@ export class Ledger {
        static #isPolling: boolean = false
        static #status: LedgerStatus = 'DISCONNECTED'
        static #transport: typeof TransportHID | typeof TransportBLE | typeof TransportUSB
-       static #DERIVATION_PATH: Uint8Array = new Uint8Array([
-               APDU_CODES.bip32DerivationLevel,
-               ...dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4),
-               ...dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)
-       ])
 
        // Compose event emission for status changes
        static #eventTarget = new EventTarget()
@@ -120,58 +122,27 @@ export class Ledger {
        }
 
        /**
-       * Request an account at a specific BIP-44 index.
-       *
-       * @returns Response object containing command status, public key, and address
-       */
+        * Request an account at a specific BIP-44 index.
+        *
+        * @param [index=0] BIP-44 account level
+        * @param [show=false] Display the account's Nano address on the Ledger device
+        * @returns Response object containing command status, public key, and address
+        */
        static async account (index: number = 0, show: boolean = false): Promise<LedgerAccountResponse> {
-               return queue(async () => {
-                       try {
-                               if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
-                                       throw new TypeError('Invalid account index')
-                               }
-                               const account = dec.toBytes(index + HARDENED_OFFSET, 4)
-                               const data = new Uint8Array([...this.#DERIVATION_PATH, ...account])
-
-                               const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout)
-                               const response = await transport
-                                       .send(APDU_CODES.class, APDU_CODES.account, show ? 1 : 0, APDU_CODES.paramUnused, data as Buffer)
-                                       .catch((err: any) => dec.toBytes(err.statusCode))
-                                       .finally(async () => await transport.close()) as Uint8Array
-
-                               const statusCode = bytes.toDec(response.slice(-2)) as number
-                               const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'
-                               if (status !== 'OK') {
-                                       return { status, publicKey: null, address: null }
-                               }
-
-                               try {
-                                       const publicKey = bytes.toHex(response.slice(0, 32))
-                                       const addressLength = response[32]
-                                       const address = response.slice(33, 33 + addressLength).toString()
-
-                                       return { status, publicKey, address }
-                               } catch (err) {
-                                       return { status: 'ERROR_PARSING_ACCOUNT', publicKey: null, address: null }
-                               }
-                       } catch (err: any) {
-                               console.error('Ledger.account()', err)
-                               return { status: err.message, publicKey: null, address: null }
-                       }
-               })
+               return queue(async () => _account(this.#transport, index, show))
        }
 
        /**
-       * Check if the Nano app is currently open and set device status accordingly.
-       *
-       * @param {string} [api] Transport interface to use
-       * @returns Device status as follows:
-       * - UNSUPPORTED: Platform does not support any Ledger transport protocols
-       * - DISCONNECTED: Failed to communicate properly with the app
-       * - BUSY: Nano app is not currently open
-       * - LOCKED: Nano app is open but the device locked after a timeout
-       * - CONNECTED: Nano app is open and listening
-       */
+        * Check if the Nano app is currently open and set device status accordingly.
+        *
+        * @param {string} [api] Transport interface to use
+        * @returns Device status as follows:
+        * - UNSUPPORTED: Platform does not support any Ledger transport protocols
+        * - DISCONNECTED: Failed to communicate properly with the app
+        * - BUSY: Nano app is not currently open
+        * - LOCKED: Nano app is open but the device locked after a timeout
+        * - CONNECTED: Nano app is open and listening
+        */
        static async connect (api?: 'hid' | 'ble' | 'usb'): Promise<LedgerStatus> {
                if (api !== undefined || this.#status !== 'UNSUPPORTED') {
                        if (api === 'hid' && this.#transport !== TransportHID) {
@@ -186,16 +157,13 @@ export class Ledger {
                                        : TransportUSB
                        }
                }
-               try {
-                       return connect()
-               } catch (err) {
-                       throw new Error('Ledger.connect()', { cause: err })
-               } finally {
-                       if (!this.isUnsupported && !this.#isPolling) {
-                               this.#isPolling = true
-                               this.#poll().catch(() => { })
-                       }
+               const status = await _connect(this.#transport)
+               this.#setStatus(status)
+               if (!this.isUnsupported && !this.#isPolling) {
+                       this.#isPolling = true
+                       this.#poll().then(() => void 0, () => void 0)
                }
+               return this.#status
        }
 
        /**
@@ -393,7 +361,7 @@ export class Ledger {
                                const signature = hex.toBytes(block.signature, 64)
                                const data = new Uint8Array([APDU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance, ...signature])
 
-                               const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout)
+                               const transport = await this.#transport.create(openTimeout, listenTimeout)
                                const response = await transport
                                        .send(APDU_CODES.class, APDU_CODES.cacheBlock, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer)
                                        .then((res: Buffer) => bytes.toDec(res))
@@ -423,7 +391,7 @@ export class Ledger {
        */
        static async #close (): Promise<LedgerResponse> {
                return queue(async () => {
-                       const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout)
+                       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))
@@ -449,7 +417,7 @@ export class Ledger {
        static async #open (): Promise<LedgerResponse> {
                return queue(async () => {
                        const name = new TextEncoder().encode('Nano')
-                       const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout)
+                       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))
@@ -476,9 +444,9 @@ export class Ledger {
                        const isUsbPaired = (await navigator.usb?.getDevices?.() ?? [])
                                .some(device => device.vendorId === this.ledgerVendorId)
                        if (this.#transport === TransportHID && isHidPaired) {
-                               await connect()
+                               await _connect(this.#transport)
                        } else if (this.#transport === TransportUSB && isUsbPaired) {
-                               await connect()
+                               await _connect(this.#transport)
                        } else {
                                this.#setStatus('DISCONNECTED')
                        }
@@ -529,9 +497,9 @@ export class Ledger {
                                const link = block.link
                                const representative = hex.toBytes(block.representative.publicKey, 32)
                                const balance = hex.toBytes(BigInt(block.balance).toString(16), 16)
-                               const data = new Uint8Array([...this.#DERIVATION_PATH, ...account, ...previous, ...link, ...representative, ...balance])
+                               const data = new Uint8Array([...DERIVATION_PATH, ...account, ...previous, ...link, ...representative, ...balance])
 
-                               const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout)
+                               const transport = await this.#transport.create(openTimeout, listenTimeout)
                                const response = await transport
                                        .send(APDU_CODES.class, APDU_CODES.signBlock, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer)
                                        .catch((err: any) => dec.toBytes(err.statusCode))
@@ -574,9 +542,9 @@ export class Ledger {
                        }
 
                        const derivationAccount = dec.toBytes(index + HARDENED_OFFSET, 4)
-                       const data = new Uint8Array([...this.#DERIVATION_PATH, ...derivationAccount, ...nonce])
+                       const data = new Uint8Array([...DERIVATION_PATH, ...derivationAccount, ...nonce])
 
-                       const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout)
+                       const transport = await this.#transport.create(openTimeout, listenTimeout)
                        const response = await transport
                                .send(APDU_CODES.class, APDU_CODES.signNonce, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer)
                                .catch((err: any) => dec.toBytes(err.statusCode))