From 7f6df42dc57efdb3dee0f362fe47819756d709a0 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Mon, 22 Sep 2025 14:02:58 -0700 Subject: [PATCH] Separate public connect from private implementation. Start rewiring connection polling to allow it to end on disconnect. --- src/lib/ledger.ts | 80 +++++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/src/lib/ledger.ts b/src/lib/ledger.ts index b8fd1cb..a0fc5f0 100644 --- a/src/lib/ledger.ts +++ b/src/lib/ledger.ts @@ -42,6 +42,7 @@ interface LedgerSignResponse extends LedgerResponse { */ export class Ledger { static #isIdle: boolean = true + static #isPolling: boolean = false static #listenTimeout: 30000 = 30000 static #openTimeout: 3000 = 3000 static #queue: { task: Function, resolve: Function, reject: Function }[] = [] @@ -188,36 +189,20 @@ export class Ledger { : TransportUSB } } - try { - const version = await this.#version() - if (version.status !== 'OK') { - this.#status = 'DISCONNECTED' - } else { - if (version.name === 'Nano') { - const { status } = await this.account() - if (status === 'OK') { - this.#status = 'CONNECTED' - } else if (status === 'SECURITY_STATUS_NOT_SATISFIED') { - this.#status = 'LOCKED' - } else { - this.#status = 'DISCONNECTED' - } - } else { - this.#status = 'BUSY' - } - } - } catch (err) { - console.error('Ledger.connect()', err) - this.#status = 'DISCONNECTED' + const status = await this.#connect() + if (!this.isUnsupported && !this.#isPolling) { + this.#isPolling = true + this.#poll().catch(() => { }) } - console.log(this.#status) - return this.#status + return status } /** - * Clears Ledger connections from HID and USB interfaces. + * Clears Ledger connections from HID and USB interfaces and stops polling for + * connection updates. */ static disconnect (): void { + this.#isPolling = false setTimeout(async () => { const hidDevices = (await navigator?.hid?.getDevices?.() ?? []) .filter(device => device.vendorId === this.ledgerVendorId) @@ -415,6 +400,43 @@ 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 !== 'OK') { + this.#status = 'DISCONNECTED' + } else { + if (version.name === 'Nano') { + const { status } = await this.account() + if (status === 'OK') { + this.#status = 'CONNECTED' + } else if (status === 'SECURITY_STATUS_NOT_SATISFIED') { + this.#status = 'LOCKED' + } else { + this.#status = 'DISCONNECTED' + } + } else { + this.#status = 'BUSY' + } + } + } catch (err) { + console.error('Ledger.#connect()', err) + this.#status = 'DISCONNECTED' + } + console.log(this.#status) + return this.#status + } + /** * Serially executes asynchronous functions. */ @@ -476,9 +498,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 this.#connect() } else if (this.#transport === TransportUSB && isUsbPaired) { - await this.connect() + await this.#connect() } else { console.log('No Ledger devices paired on current interface') this.#status = 'DISCONNECTED' @@ -487,7 +509,7 @@ export class Ledger { console.warn('Error polling Ledger device') this.#status = 'DISCONNECTED' } finally { - setTimeout(() => this.#poll(), 500) + this.#isPolling ? setTimeout(() => this.#poll(), 500) : void 0 } } @@ -621,8 +643,4 @@ export class Ledger { } }) } - - static { - this.isUnsupported ? void 0 : this.#poll().catch(() => { }) - } } -- 2.47.3