From: Chris Duncan Date: Sat, 16 May 2026 07:18:42 +0000 (-0700) Subject: Fix unfinished changes to move connect and also move account. X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=54a4b4975b485055f71468faee5e50bceb412aaa;p=libnemo.git Fix unfinished changes to move connect and also move account. --- diff --git a/src/lib/ledger/account.ts b/src/lib/ledger/account.ts new file mode 100644 index 0000000..f534011 --- /dev/null +++ b/src/lib/ledger/account.ts @@ -0,0 +1,41 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! 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 { + 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 } + } +} diff --git a/src/lib/ledger/connect.ts b/src/lib/ledger/connect.ts index a123c8a..3e799ac 100644 --- a/src/lib/ledger/connect.ts +++ b/src/lib/ledger/connect.ts @@ -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 { - let status = 'DISCONNECTED' +export async function _connect (transport: LedgerTransport): Promise { + 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 = 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> = 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 { - 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 { 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 { 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 { 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))