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.
this.#transport ??= TransportUSB
return false
}
- this.#status = 'UNSUPPORTED'
+ this.#setStatus('UNSUPPORTED')
return true
}
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 {
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
}
} 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)
}
}
}
export class Vault {
+ #eventTarget = new EventTarget()
#job?: Task
#isIdle: boolean
#isLocked: boolean
#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
})
}
+
get isLocked (): boolean { return this.#isLocked }
request<T extends Data> (data: NamedData): Promise<NamedData<T>> {
#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) {
return typeof id === 'string' ? wallets[0] : wallets\r
}\r
\r
+ #accounts: Map<number, Account> = new Map<number, Account>()\r
+ #eventTarget: EventTarget = new EventTarget()\r
+ #id: string = crypto.randomUUID()\r
+ #vault: Vault = new Vault()\r
+\r
+ #mnemonic?: ArrayBuffer\r
+ #seed?: ArrayBuffer\r
+ #type: WalletType\r
+\r
constructor (type: WalletType, id?: string)\r
constructor (type: unknown, id?: string) {\r
if (!(this.constructor as typeof Wallet).isInternal) {\r
if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Ledger') {\r
throw new TypeError('Invalid wallet type', { cause: type })\r
}\r
- this.#accounts = new Map<number, Account>()\r
- this.#id = id ?? crypto.randomUUID()\r
+ this.#id = id ?? this.#id\r
this.#type = type\r
- this.#vault = new Vault()\r
+ this.#vault.addEventListener('locked', () => this.dispatchEvent(new Event('locked')))\r
+ this.#vault.addEventListener('unlocked', () => this.dispatchEvent(new Event('unlocked')))\r
}\r
\r
+ // Compose event emission for status changes\r
+ addEventListener = this.#eventTarget.addEventListener.bind(this.#eventTarget)\r
+ dispatchEvent = this.#eventTarget.dispatchEvent.bind(this.#eventTarget)\r
+ removeEventListener = this.#eventTarget.removeEventListener.bind(this.#eventTarget)\r
+\r
/**\r
* @returns UUID of the wallet.\r
*/\r
async verify (secret: string): Promise<boolean> {\r
return await _verify(this.type, this.#vault, secret)\r
}\r
-\r
- #accounts: Map<number, Account>\r
- #id: string\r
- #mnemonic?: ArrayBuffer\r
- #seed?: ArrayBuffer\r
- #type: WalletType\r
- #vault: Vault\r
}\r
}
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))
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(
})
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...')
// 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)
// should still be locked
assert.equal(wallet.isLocked, true)
assert.equal(Ledger.status, 'LOCKED')
+ assert.equal(status, 'LOCKED')
resolve(null)
} catch (err) {
reject(err)
})
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...')
// should still be unlocked
assert.equal(wallet.isLocked, false)
assert.equal(Ledger.status, 'CONNECTED')
+ assert.equal(status, 'CONNECTED')
resolve(null)
} catch (err) {
reject(err)
// should now be locked
assert.equal(wallet.isLocked, true)
assert.equal(Ledger.status, 'LOCKED')
+ assert.equal(status, 'LOCKED')
resolve(null)
} catch (err) {
reject(err)
})
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 () => {
await Promise.all([\r
suite('Lock and unlock wallets', async () => {\r
\r
- await test('locking and unlocking a Bip44Wallet with a password', async () => {\r
+ await test('locking and unlocking a BIP-44 wallet with a password', async () => {\r
const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.ok('mnemonic' in wallet)\r
\r
await test('change the password on a BIP-44 wallet', async () => {\r
const wallet = await Wallet.load('BIP-44', '', NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
+ let isLocked = wallet.isLocked\r
+ wallet.addEventListener('locked', () => isLocked = true)\r
+ wallet.addEventListener('unlocked', () => isLocked = false)\r
await wallet.unlock('')\r
\r
assert.ok('mnemonic' in wallet)\r
assert.ok(wallet.seed === undefined)\r
assert.ok(await wallet.verify(NANO_TEST_VECTORS.MNEMONIC))\r
assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
+ assert.equal(isLocked, false)\r
\r
await wallet.update(NANO_TEST_VECTORS.PASSWORD)\r
wallet.lock()\r
+ await new Promise(r => setTimeout(r, 1000))\r
+ assert.equal(isLocked, true)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.ok('mnemonic' in wallet)\r