//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
//! 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.
* - LOCKED: Nano app is open but the device locked after a timeout
* - CONNECTED: Nano app is open and listening
*/
-export async function connect (): Promise<LedgerStatus> {
+export async function connect (transport: LedgerTransport): Promise<LedgerStatus> {
+ 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<LedgerVersionResponse> {
- 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
}
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
signNonce: 0x05,
paramUnused: 0x00
})
+export const STATUS_CODES: Readonly<Record<number, string>> = 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
*/
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([
...dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4),
...dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)
])
- static #STATUS_CODES: Readonly<Record<number, string>> = 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()
.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 }
}
.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 }
.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] }))
})
}
.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] }))
})
}
.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 }
.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 }
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! 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<LedgerVersionResponse> {
+ 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 }
+ }
+ })
+}