From 495feb21ce28ced9575a6ee0a995e729095d7fc5 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Fri, 15 May 2026 16:02:11 -0700 Subject: [PATCH] Back up WIP --- src/lib/ledger/connect.ts | 71 +++++++++------------------------------ src/lib/ledger/index.ts | 31 ++++++++--------- src/lib/ledger/version.ts | 46 +++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 71 deletions(-) create mode 100644 src/lib/ledger/version.ts diff --git a/src/lib/ledger/connect.ts b/src/lib/ledger/connect.ts index 4faae98..a123c8a 100644 --- a/src/lib/ledger/connect.ts +++ b/src/lib/ledger/connect.ts @@ -1,14 +1,8 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -import { APDU_CODES, LedgerResponse, LedgerStatus } from '.' -import { bytes, dec } from '../convert' -import { queue } from './queue' - -interface LedgerVersionResponse extends LedgerResponse { - name: string | null, - version: string | null -} +import { LedgerStatus, LedgerTransport } from '.' +import { version } from './version' /** * Check if the Nano app is currently open and set device status accordingly. @@ -20,64 +14,29 @@ interface LedgerVersionResponse extends LedgerResponse { * - LOCKED: Nano app is open but the device locked after a timeout * - CONNECTED: Nano app is open and listening */ -export async function connect (): Promise { +export async function connect (transport: LedgerTransport): Promise { + let status = 'DISCONNECTED' try { - const v = await version() + const v = await version(transport) if (v.status === 'LOCKED_DEVICE') { - this.#setStatus('LOCKED') + status = 'LOCKED' } else if (v.status !== 'OK') { - this.#setStatus('DISCONNECTED') + status = 'DISCONNECTED' } else if (v.name === 'Nano') { - const { status } = await this.account() - if (status === 'OK') { - this.#setStatus('CONNECTED') + const a = await account() + if (a.status === 'OK') { + status = 'CONNECTED' } else if (status === 'SECURITY_STATUS_NOT_SATISFIED') { - this.#setStatus('LOCKED') + status = 'LOCKED' } else { - this.#setStatus('DISCONNECTED') + status = 'DISCONNECTED' } } else { - this.#setStatus('BUSY') + status = 'BUSY' } } catch (err) { console.error('Ledger.#connect()', err) - this.#setStatus('DISCONNECTED') + status = 'DISCONNECTED' } - return this.#status -} - -/** - * Get the version of the current process. If a specific app is running, get - * the app version. Otherwise, get the Ledger BOLOS version instead. - * - * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#get-information - * - * @returns Status, process name, and version - */ -async function version (): Promise { - return queue(async () => { - try { - const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout) - const response = await transport - .send(0xb0, APDU_CODES.version, APDU_CODES.paramUnused, APDU_CODES.paramUnused) - .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 = this.#STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' - if (status !== 'OK') { - return { status, name: null, version: null } - } - - const nameLength = response[1] - const name = response.slice(2, 2 + nameLength).toString() - const versionLength = response[2 + nameLength] - const version = response.slice(2 + nameLength + 1, 2 + nameLength + 1 + versionLength).toString() - - return { status, name, version } - } catch (err: any) { - console.error('Ledger.#version()', err) - return { status: err.message, name: null, version: null } - } - }) + return status } diff --git a/src/lib/ledger/index.ts b/src/lib/ledger/index.ts index 09e3cd9..66d6873 100644 --- a/src/lib/ledger/index.ts +++ b/src/lib/ledger/index.ts @@ -14,6 +14,7 @@ import { Wallet } from '../wallet' import { queue } from './queue' export type LedgerStatus = 'UNSUPPORTED' | 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED' +export type LedgerTransport = typeof TransportHID | typeof TransportBLE | typeof TransportUSB export interface LedgerResponse { status: string @@ -39,6 +40,15 @@ export const APDU_CODES: Record = Object.freeze({ signNonce: 0x05, paramUnused: 0x00 }) +export const STATUS_CODES: Readonly> = Object.freeze({ + ...Object.fromEntries(Object.entries(StatusCodes).map(([k, v]) => [+v, k])), + 0x6807: 'APPLICATION_NOT_INSTALLED', + 0x6d00: 'APPLICATION_ALREADY_LAUNCHED', + 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 @@ -50,8 +60,6 @@ export const APDU_CODES: Record = Object.freeze({ */ export class Ledger { static #isPolling: boolean = false - static #listenTimeout: 30000 = 30000 - static #openTimeout: 3000 = 3000 static #status: LedgerStatus = 'DISCONNECTED' static #transport: typeof TransportHID | typeof TransportBLE | typeof TransportUSB static #DERIVATION_PATH: Uint8Array = new Uint8Array([ @@ -59,13 +67,6 @@ export class Ledger { ...dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4), ...dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4) ]) - static #STATUS_CODES: Readonly> = Object.freeze({ - ...Object.fromEntries(Object.entries(StatusCodes).map(([k, v]) => [+v, k])), - 0x6807: 'APPLICATION_NOT_INSTALLED', - 0x6d00: 'APPLICATION_ALREADY_LAUNCHED', - 0x6a81: 'INVALID_SIGNATURE', - 0x6a82: 'CACHE_MISS' - }) // Compose event emission for status changes static #eventTarget = new EventTarget() @@ -139,7 +140,7 @@ export class Ledger { .finally(async () => await transport.close()) as Uint8Array const statusCode = bytes.toDec(response.slice(-2)) as number - const status = this.#STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' + const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' if (status !== 'OK') { return { status, publicKey: null, address: null } } @@ -399,7 +400,7 @@ export class Ledger { .catch((err: any) => err.statusCode) .finally(async () => await transport.close()) as number - return { status: this.#STATUS_CODES[response] } + return { status: STATUS_CODES[response] } } catch (err: any) { console.error('Ledger.#cacheBlock()', err) return { status: err.message } @@ -428,7 +429,7 @@ export class Ledger { .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: this.#STATUS_CODES[response] })) + return new Promise(r => setTimeout(r, 1000, { status: STATUS_CODES[response] })) }) } @@ -454,7 +455,7 @@ export class Ledger { .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: this.#STATUS_CODES[response] })) + return new Promise(r => setTimeout(r, 1000, { status: STATUS_CODES[response] })) }) } @@ -537,7 +538,7 @@ export class Ledger { .finally(async () => await transport.close()) as Uint8Array const statusCode = bytes.toDec(response.slice(-2)) as number - const status = this.#STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' + const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' if (response.byteLength === 2) { return { status, signature: null } @@ -582,7 +583,7 @@ export class Ledger { .finally(async () => await transport.close()) as Uint8Array const statusCode = bytes.toDec(response.slice(-2)) as number - const status = this.#STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' + const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' if (response.byteLength === 2) { return { status, signature: null } diff --git a/src/lib/ledger/version.ts b/src/lib/ledger/version.ts new file mode 100644 index 0000000..b643156 --- /dev/null +++ b/src/lib/ledger/version.ts @@ -0,0 +1,46 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { APDU_CODES, LedgerResponse, LedgerTransport, STATUS_CODES, listenTimeout, openTimeout } from '.' +import { bytes, dec } from '../convert' +import { queue } from './queue' + +interface LedgerVersionResponse extends LedgerResponse { + name: string | null, + version: string | null +} +/** + * Get the version of the current process. If a specific app is running, get + * the app version. Otherwise, get the Ledger BOLOS version instead. + * + * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#get-information + * + * @returns Status, process name, and version + */ +export async function version (transport: LedgerTransport): Promise { + return queue(async () => { + try { + const t = await transport.create(openTimeout, listenTimeout) + const response = await t + .send(0xb0, APDU_CODES.version, APDU_CODES.paramUnused, APDU_CODES.paramUnused) + .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, name: null, version: null } + } + + const nameLength = response[1] + const name = response.slice(2, 2 + nameLength).toString() + const versionLength = response[2 + nameLength] + const version = response.slice(2 + nameLength + 1, 2 + nameLength + 1 + versionLength).toString() + + return { status, name, version } + } catch (err: any) { + console.error('Ledger.#version()', err) + return { status: err.message, name: null, version: null } + } + }) +} -- 2.47.3