From: Chris Duncan Date: Wed, 24 Sep 2025 20:50:11 +0000 (-0700) Subject: Dispatch events when Ledger status changes and when wallet lock status changes. X-Git-Tag: v0.10.5~7 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=a2129b8562ab781847cdfd6203b51ccbf3a30ad1;p=libnemo.git Dispatch events when Ledger status changes and when wallet lock status changes. --- diff --git a/src/lib/ledger.ts b/src/lib/ledger.ts index a5bcd97..86ce95e 100644 --- a/src/lib/ledger.ts +++ b/src/lib/ledger.ts @@ -77,6 +77,12 @@ export class Ledger { 0x9000: 'OK' }) + // Compose event emission for status changes + static #eventTarget = new EventTarget() + static addEventListener = this.#eventTarget.addEventListener.bind(this.#eventTarget) + static dispatchEvent = this.#eventTarget.dispatchEvent.bind(this.#eventTarget) + static removeEventListener = this.#eventTarget.removeEventListener.bind(this.#eventTarget) + /** * Check which transport protocols are supported by the browser and return the * transport type according to the following priorities: HID, Bluetooth, USB. @@ -101,7 +107,7 @@ export class Ledger { this.#transport ??= TransportUSB return false } - this.#status = 'UNSUPPORTED' + this.#setStatus('UNSUPPORTED') return true } @@ -214,7 +220,7 @@ export class Ledger { const devices = [...hidDevices, ...usbDevices] .filter(device => device.vendorId === this.ledgerVendorId) await Promise.allSettled(devices.map(device => device.forget?.() ?? Promise.resolve())) - this.#status = 'DISCONNECTED' + this.#setStatus('DISCONNECTED') } catch (err) { console.warn('Ledger.disconnect()', err) } finally { @@ -426,26 +432,25 @@ export class Ledger { try { const version = await this.#version() if (version.status !== 'OK') { - this.#status = 'DISCONNECTED' + this.#setStatus('DISCONNECTED') } else { if (version.name === 'Nano') { const { status } = await this.account() if (status === 'OK') { - this.#status = 'CONNECTED' + this.#setStatus('CONNECTED') } else if (status === 'SECURITY_STATUS_NOT_SATISFIED') { - this.#status = 'LOCKED' + this.#setStatus('LOCKED') } else { - this.#status = 'DISCONNECTED' + this.#setStatus('DISCONNECTED') } } else { - this.#status = 'BUSY' + this.#setStatus('BUSY') } } } catch (err) { console.error('Ledger.#connect()', err) - this.#status = 'DISCONNECTED' + this.#setStatus('DISCONNECTED') } - console.log(this.#status) return this.#status } @@ -514,14 +519,24 @@ export class Ledger { } else if (this.#transport === TransportUSB && isUsbPaired) { await this.#connect() } else { - console.log('No Ledger devices paired on USB') - this.#status = 'DISCONNECTED' + this.#setStatus('DISCONNECTED') } + this.#isPolling ? setTimeout(() => this.#poll(), 500) : void 0 } catch { console.warn('Error polling Ledger device') - this.#status = 'DISCONNECTED' - } finally { - this.#isPolling ? setTimeout(() => this.#poll(), 500) : void 0 + this.#setStatus('DISCONNECTED') + } + } + + /** + * Sets the Ledger status and emits an event. + */ + static #setStatus (value: LedgerStatus) { + if (this.#status !== value) { + this.#status = value + const event = new CustomEvent('ledgerstatuschanged', { detail: value }) + this.dispatchEvent(event) + console.log(event) } } diff --git a/src/lib/vault/index.ts b/src/lib/vault/index.ts index b321c8f..e9459bb 100644 --- a/src/lib/vault/index.ts +++ b/src/lib/vault/index.ts @@ -17,6 +17,7 @@ type Task = { } export class Vault { + #eventTarget = new EventTarget() #job?: Task #isIdle: boolean #isLocked: boolean @@ -25,6 +26,11 @@ export class Vault { #url: string #worker: Worker | NodeWorker + // Compose event emission for status changes + addEventListener = this.#eventTarget.addEventListener.bind(this.#eventTarget) + dispatchEvent = this.#eventTarget.dispatchEvent.bind(this.#eventTarget) + removeEventListener = this.#eventTarget.removeEventListener.bind(this.#eventTarget) + constructor () { this.#isIdle = true this.#isLocked = true @@ -45,6 +51,7 @@ export class Vault { }) } + get isLocked (): boolean { return this.#isLocked } request (data: NamedData): Promise> { @@ -91,7 +98,11 @@ export class Vault { #report (results: any): void { if (results === 'locked' || results === 'unlocked') { - this.#isLocked = results === 'locked' + const isLocked = results === 'locked' + if (this.#isLocked !== isLocked) { + this.#isLocked = isLocked + this.dispatchEvent(new Event(results)) + } return } if (this.#job == null) { diff --git a/src/lib/wallet/index.ts b/src/lib/wallet/index.ts index 574f9c5..c042bda 100644 --- a/src/lib/wallet/index.ts +++ b/src/lib/wallet/index.ts @@ -132,6 +132,15 @@ export class Wallet { return typeof id === 'string' ? wallets[0] : wallets } + #accounts: Map = new Map() + #eventTarget: EventTarget = new EventTarget() + #id: string = crypto.randomUUID() + #vault: Vault = new Vault() + + #mnemonic?: ArrayBuffer + #seed?: ArrayBuffer + #type: WalletType + constructor (type: WalletType, id?: string) constructor (type: unknown, id?: string) { if (!(this.constructor as typeof Wallet).isInternal) { @@ -140,12 +149,17 @@ export class Wallet { if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Ledger') { throw new TypeError('Invalid wallet type', { cause: type }) } - this.#accounts = new Map() - this.#id = id ?? crypto.randomUUID() + this.#id = id ?? this.#id this.#type = type - this.#vault = new Vault() + this.#vault.addEventListener('locked', () => this.dispatchEvent(new Event('locked'))) + this.#vault.addEventListener('unlocked', () => this.dispatchEvent(new Event('unlocked'))) } + // Compose event emission for status changes + addEventListener = this.#eventTarget.addEventListener.bind(this.#eventTarget) + dispatchEvent = this.#eventTarget.dispatchEvent.bind(this.#eventTarget) + removeEventListener = this.#eventTarget.removeEventListener.bind(this.#eventTarget) + /** * @returns UUID of the wallet. */ @@ -403,11 +417,4 @@ export class Wallet { async verify (secret: string): Promise { return await _verify(this.type, this.#vault, secret) } - - #accounts: Map - #id: string - #mnemonic?: ArrayBuffer - #seed?: ArrayBuffer - #type: WalletType - #vault: Vault } diff --git a/test/test.ledger.mjs b/test/test.ledger.mjs index 0a64da4..7047ce0 100644 --- a/test/test.ledger.mjs +++ b/test/test.ledger.mjs @@ -39,6 +39,13 @@ await Promise.all([ } await test('request permissions', { skip: true }, async () => { + let status = Ledger.status + + Ledger.addEventListener('ledgerstatuschanged', (event) => { + //@ts-expect-error + status = event.detail ?? '' + }) + await click( 'Reset permissions, then click to continue', async () => new Promise(r => setTimeout(r, 5000)) @@ -46,6 +53,7 @@ await Promise.all([ await assert.rejects(wallet.unlock()) assert.equal(wallet.isLocked, true) assert.equal(Ledger.status, 'DISCONNECTED') + assert.equal(status, 'DISCONNECTED') await assert.rejects(async () => { await click( @@ -55,6 +63,7 @@ await Promise.all([ }) assert.equal(wallet.isLocked, true) assert.equal(Ledger.status, 'BUSY') + assert.equal(status, 'BUSY') await new Promise(async (resolve, reject) => { console.log('Waiting 6 seconds...') @@ -63,6 +72,7 @@ await Promise.all([ // should still be locked and busy assert.equal(wallet.isLocked, true) assert.equal(Ledger.status, 'BUSY') + assert.equal(status, 'BUSY') resolve(null) } catch (err) { reject(err) @@ -86,6 +96,7 @@ await Promise.all([ // should still be locked assert.equal(wallet.isLocked, true) assert.equal(Ledger.status, 'LOCKED') + assert.equal(status, 'LOCKED') resolve(null) } catch (err) { reject(err) @@ -101,6 +112,7 @@ await Promise.all([ }) assert.equal(wallet.isLocked, false) assert.equal(Ledger.status, 'CONNECTED') + assert.equal(status, 'CONNECTED') await new Promise(async (resolve, reject) => { console.log('Waiting 6 seconds...') @@ -109,6 +121,7 @@ await Promise.all([ // should still be unlocked assert.equal(wallet.isLocked, false) assert.equal(Ledger.status, 'CONNECTED') + assert.equal(status, 'CONNECTED') resolve(null) } catch (err) { reject(err) @@ -123,6 +136,7 @@ await Promise.all([ // should now be locked assert.equal(wallet.isLocked, true) assert.equal(Ledger.status, 'LOCKED') + assert.equal(status, 'LOCKED') resolve(null) } catch (err) { reject(err) @@ -138,6 +152,7 @@ await Promise.all([ }) assert.equal(wallet.isLocked, false) assert.equal(Ledger.status, 'CONNECTED') + assert.equal(status, 'CONNECTED') }) await test('switch between interfaces', { skip: false || isNode || navigator?.usb == null }, async () => { diff --git a/test/test.lock-unlock.mjs b/test/test.lock-unlock.mjs index b42edfa..ff694ce 100644 --- a/test/test.lock-unlock.mjs +++ b/test/test.lock-unlock.mjs @@ -10,7 +10,7 @@ import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './VECTORS.mjs' await Promise.all([ suite('Lock and unlock wallets', async () => { - await test('locking and unlocking a Bip44Wallet with a password', async () => { + await test('locking and unlocking a BIP-44 wallet with a password', async () => { const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) assert.ok('mnemonic' in wallet) @@ -28,6 +28,9 @@ await Promise.all([ await test('change the password on a BIP-44 wallet', async () => { const wallet = await Wallet.load('BIP-44', '', NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + let isLocked = wallet.isLocked + wallet.addEventListener('locked', () => isLocked = true) + wallet.addEventListener('unlocked', () => isLocked = false) await wallet.unlock('') assert.ok('mnemonic' in wallet) @@ -36,9 +39,12 @@ await Promise.all([ assert.ok(wallet.seed === undefined) assert.ok(await wallet.verify(NANO_TEST_VECTORS.MNEMONIC)) assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) + assert.equal(isLocked, false) await wallet.update(NANO_TEST_VECTORS.PASSWORD) wallet.lock() + await new Promise(r => setTimeout(r, 1000)) + assert.equal(isLocked, true) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) assert.ok('mnemonic' in wallet)