From: Chris Duncan Date: Fri, 15 May 2026 22:11:18 +0000 (-0700) Subject: Start extracting Ledger connect method to its own file. X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=e15d8f49bc46c1b420c27344cc190c78b15e96c1;p=libnemo.git Start extracting Ledger connect method to its own file. --- diff --git a/src/lib/ledger/connect.ts b/src/lib/ledger/connect.ts new file mode 100644 index 0000000..4faae98 --- /dev/null +++ b/src/lib/ledger/connect.ts @@ -0,0 +1,83 @@ +//! 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 +} + +/** + * Check if the Nano app is currently open and set device status accordingly. + * + * @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 + */ +export async function connect (): Promise { + try { + const v = await version() + if (v.status === 'LOCKED_DEVICE') { + this.#setStatus('LOCKED') + } else if (v.status !== 'OK') { + this.#setStatus('DISCONNECTED') + } else if (v.name === 'Nano') { + const { status } = await this.account() + if (status === 'OK') { + this.#setStatus('CONNECTED') + } else if (status === 'SECURITY_STATUS_NOT_SATISFIED') { + this.#setStatus('LOCKED') + } else { + this.#setStatus('DISCONNECTED') + } + } else { + this.#setStatus('BUSY') + } + } catch (err) { + console.error('Ledger.#connect()', err) + this.#setStatus('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 } + } + }) +} diff --git a/src/lib/ledger/index.ts b/src/lib/ledger/index.ts index 25ad785..09e3cd9 100644 --- a/src/lib/ledger/index.ts +++ b/src/lib/ledger/index.ts @@ -13,17 +13,12 @@ import { Rpc } from '../rpc' import { Wallet } from '../wallet' import { queue } from './queue' -type LedgerStatus = 'UNSUPPORTED' | 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED' +export type LedgerStatus = 'UNSUPPORTED' | 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED' -interface LedgerResponse { +export interface LedgerResponse { status: string } -interface LedgerVersionResponse extends LedgerResponse { - name: string | null, - version: string | null -} - interface LedgerAccountResponse extends LedgerResponse { publicKey: string | null, address: string | null @@ -34,6 +29,17 @@ interface LedgerSignResponse extends LedgerResponse { hash?: string } +export const APDU_CODES: Record = Object.freeze({ + class: 0xa1, + bip32DerivationLevel: 0x03, + version: 0x01, + account: 0x02, + cacheBlock: 0x03, + signBlock: 0x04, + signNonce: 0x05, + paramUnused: 0x00 +}) + /** * 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 @@ -48,18 +54,8 @@ export class Ledger { static #openTimeout: 3000 = 3000 static #status: LedgerStatus = 'DISCONNECTED' static #transport: typeof TransportHID | typeof TransportBLE | typeof TransportUSB - static #ADPU_CODES: Record = Object.freeze({ - class: 0xa1, - bip32DerivationLevel: 0x03, - version: 0x01, - account: 0x02, - cacheBlock: 0x03, - signBlock: 0x04, - signNonce: 0x05, - paramUnused: 0x00 - }) static #DERIVATION_PATH: Uint8Array = new Uint8Array([ - this.#ADPU_CODES.bip32DerivationLevel, + APDU_CODES.bip32DerivationLevel, ...dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4), ...dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4) ]) @@ -138,7 +134,7 @@ export class Ledger { const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout) const response = await transport - .send(this.#ADPU_CODES.class, this.#ADPU_CODES.account, show ? 1 : 0, this.#ADPU_CODES.paramUnused, data as Buffer) + .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 @@ -190,7 +186,7 @@ export class Ledger { } } try { - return this.#connect() + return connect() } catch (err) { throw new Error('Ledger.connect()', { cause: err }) } finally { @@ -394,11 +390,11 @@ export class Ledger { const representative = hex.toBytes(block.representative.publicKey, 32) const balance = hex.toBytes(block.balance.toString(16), 16) const signature = hex.toBytes(block.signature, 64) - const data = new Uint8Array([this.#ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance, ...signature]) + 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 response = await transport - .send(this.#ADPU_CODES.class, this.#ADPU_CODES.cacheBlock, this.#ADPU_CODES.paramUnused, this.#ADPU_CODES.paramUnused, data as Buffer) + .send(APDU_CODES.class, APDU_CODES.cacheBlock, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer) .then((res: Buffer) => bytes.toDec(res)) .catch((err: any) => err.statusCode) .finally(async () => await transport.close()) as number @@ -428,7 +424,7 @@ export class Ledger { return queue(async () => { const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout) const response = await transport - .send(0xb0, 0xa7, this.#ADPU_CODES.paramUnused, this.#ADPU_CODES.paramUnused) + .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 @@ -436,42 +432,6 @@ export class Ledger { }) } - /** - * Check if the Nano app is currently open and set device status accordingly. - * - * @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 (): Promise { - try { - const version = await this.#version() - if (version.status === 'LOCKED_DEVICE') { - this.#setStatus('LOCKED') - } else if (version.status !== 'OK') { - this.#setStatus('DISCONNECTED') - } else if (version.name === 'Nano') { - const { status } = await this.account() - if (status === 'OK') { - this.#setStatus('CONNECTED') - } else if (status === 'SECURITY_STATUS_NOT_SATISFIED') { - this.#setStatus('LOCKED') - } else { - this.#setStatus('DISCONNECTED') - } - } else { - this.#setStatus('BUSY') - } - } catch (err) { - console.error('Ledger.#connect()', err) - this.#setStatus('DISCONNECTED') - } - return this.#status - } - /** * Open the Nano app by launching a user flow. * @@ -490,7 +450,7 @@ export class Ledger { const name = new TextEncoder().encode('Nano') const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout) const response = await transport - .send(0xe0, 0xd8, this.#ADPU_CODES.paramUnused, this.#ADPU_CODES.paramUnused, name as Buffer) + .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 @@ -515,9 +475,9 @@ export class Ledger { const isUsbPaired = (await navigator.usb?.getDevices?.() ?? []) .some(device => device.vendorId === this.ledgerVendorId) if (this.#transport === TransportHID && isHidPaired) { - await this.#connect() + await connect() } else if (this.#transport === TransportUSB && isUsbPaired) { - await this.#connect() + await connect() } else { this.#setStatus('DISCONNECTED') } @@ -572,7 +532,7 @@ export class Ledger { const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout) const response = await transport - .send(this.#ADPU_CODES.class, this.#ADPU_CODES.signBlock, this.#ADPU_CODES.paramUnused, this.#ADPU_CODES.paramUnused, data as Buffer) + .send(APDU_CODES.class, APDU_CODES.signBlock, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer) .catch((err: any) => dec.toBytes(err.statusCode)) .finally(async () => await transport.close()) as Uint8Array @@ -617,7 +577,7 @@ export class Ledger { const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout) const response = await transport - .send(this.#ADPU_CODES.class, this.#ADPU_CODES.signNonce, this.#ADPU_CODES.paramUnused, this.#ADPU_CODES.paramUnused, data as Buffer) + .send(APDU_CODES.class, APDU_CODES.signNonce, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer) .catch((err: any) => dec.toBytes(err.statusCode)) .finally(async () => await transport.close()) as Uint8Array @@ -635,40 +595,4 @@ export class Ledger { } }) } - - /** - * 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 - */ - static async #version (): Promise { - return queue(async () => { - try { - const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout) - const response = await transport - .send(0xb0, this.#ADPU_CODES.version, this.#ADPU_CODES.paramUnused, this.#ADPU_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 } - } - }) - } }