From c74910cea2e03035840bbfa90a663a1cb22e08d1 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Thu, 10 Jul 2025 06:13:09 -0700 Subject: [PATCH] Reorder and refactor Ledger device wallet functions. Update ledger device tests. --- src/lib/block.ts | 2 +- src/lib/wallets/ledger-wallet.ts | 495 ++++++++++++++++++------------- test/test.ledger.mjs | 59 ++-- 3 files changed, 305 insertions(+), 251 deletions(-) diff --git a/src/lib/block.ts b/src/lib/block.ts index 3ab8c99..cf99ed3 100644 --- a/src/lib/block.ts +++ b/src/lib/block.ts @@ -128,7 +128,7 @@ abstract class Block { const index = input const { LedgerWallet } = await import('./wallets') const ledger = await LedgerWallet.create() - await ledger.open() + await ledger.connect() if (block) { try { await ledger.updateCache(index, block) diff --git a/src/lib/wallets/ledger-wallet.ts b/src/lib/wallets/ledger-wallet.ts index 6cce198..d62402c 100644 --- a/src/lib/wallets/ledger-wallet.ts +++ b/src/lib/wallets/ledger-wallet.ts @@ -1,19 +1,19 @@ // SPDX-FileCopyrightText: 2025 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later -import Transport from '@ledgerhq/hw-transport' +import { ledgerUSBVendorId } from '@ledgerhq/devices' import { default as TransportBLE } from '@ledgerhq/hw-transport-web-ble' import { default as TransportUSB } from '@ledgerhq/hw-transport-webusb' import { default as TransportHID } from '@ledgerhq/hw-transport-webhid' import { Account } from '#src/lib/account.js' import { ChangeBlock, ReceiveBlock, SendBlock } from '#src/lib/block.js' import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, LEDGER_ADPU_CODES, LEDGER_STATUS_CODES } from '#src/lib/constants.js' -import { bytes, dec, hex, utf8 } from '#src/lib/convert.js' +import { bytes, dec, hex } from '#src/lib/convert.js' import { Entropy } from '#src/lib/entropy.js' import { Rpc } from '#src/lib/rpc.js' import { KeyPair, Wallet } from './wallet' -type DeviceStatus = 'DISCONNECTED' | 'PAIRED' | 'LOCKED' | 'BUSY' | 'CONNECTED' +type DeviceStatus = 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED' interface LedgerResponse { status: string @@ -47,6 +47,11 @@ interface LedgerSignResponse extends LedgerResponse { */ export class LedgerWallet extends Wallet { static #isInternal: boolean = false + static #derivationPath: Uint8Array = new Uint8Array([ + LEDGER_ADPU_CODES.bip32DerivationLevel, + ...dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4), + ...dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4) + ]) #status: DeviceStatus = 'DISCONNECTED' @@ -64,6 +69,81 @@ export class LedgerWallet extends Wallet { super(id) } + /** + * Since the Ledger device can only return one account per request, this + * overrides the default behavior of calling `accounts()` to instead + * directly derive and return a single account at the specified index. + * + * @returns Account + */ + async account (index: number = 0, show: boolean = false): Promise { + const { status, publicKey } = await this.#account(index, show) + if (publicKey == null) { + throw new Error('Failed to get account from device', { cause: status }) + } + const account = await Account.fromPublicKey(publicKey) + return account + } + + /** + * Since the Ledger device can only return one account per request, this + * overrides the default behavior of returning multiple accounts to instead + * return a single account at the specified index. + * + * @returns Account + */ + async accounts (): Promise { + throw new Error(`Ledger device only supports 'account()' calls`) + } + + /** + * Check which transport protocols are supported by the browser and set the + * transport type according to the following priorities: Bluetooth, USB, HID. + */ + async checkBrowserSupport (): Promise { + console.log('Checking browser Ledger support...') + try { + if (await TransportBLE.isSupported()) { + return TransportBLE + } + if (await TransportUSB.isSupported()) { + return TransportUSB + } + if (await TransportHID.isSupported()) { + return TransportHID + } + } catch { } + throw new Error('Unsupported browser') + } + + /** + * Check if the Nano app is currently open and set device status accordingly. + * + * @returns Device status as follows: + * - 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 + */ + async connect (): Promise { + 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' + } + return this.status + } + /** * Creates a new Ledger hardware wallet communication layer by dynamically * importing the ledger.js service. @@ -84,9 +164,81 @@ export class LedgerWallet extends Wallet { */ async destroy (): Promise { await super.destroy() - const { status } = await this.close() + const { status } = await this.#close() if (status !== 'OK') { - throw new Error('Failed to close wallet', { cause: status }) + throw new Error('Failed to lock Ledger wallet', { cause: status }) + } + } + + async init (): Promise { + try { + this.DynamicTransport = await this.checkBrowserSupport() + // await this.connect() + } catch (err) { + throw new Error('Failed to initialize Ledger wallet', { cause: err }) + } + } + + /** + * Attempts to close the current process on the Ledger device. + * + * Overrides the default wallet `lock()` method since as a hardware wallet it + * does not need to be encrypted by software. + * + * @returns True if successfully locked + */ + async lock (): Promise { + const { status } = await this.#close() + return status === 'OK' + } + + onConnectUsb = async (e: USBConnectionEvent): Promise => { + console.log(e) + if (e.device?.vendorId === ledgerUSBVendorId) { + console.log('Ledger connected') + const { usb } = globalThis.navigator + usb.addEventListener('disconnect', this.onDisconnectUsb) + usb.removeEventListener('connect', this.onConnectUsb) + } + } + + onDisconnectUsb = async (e: USBConnectionEvent): Promise => { + console.log(e) + if (e.device?.vendorId === ledgerUSBVendorId) { + console.log('Ledger disconnected') + const { usb } = globalThis.navigator + usb.addEventListener('connect', this.onConnectUsb) + usb.removeEventListener('disconnect', this.onDisconnectUsb) + this.#status = 'DISCONNECTED' + } + } + + /** + * Sign a block with the Ledger device. + * + * @param {number} index - Account number + * @param {object} block - Block data to sign + * @returns {Promise} Status, signature, and block hash + */ + async sign (index: number, block: SendBlock | ReceiveBlock | ChangeBlock): Promise + /** + * Sign a nonce with the Ledger device. + * + * @param {number} index - Account number + * @param {Uint8Array} nonce - 128-bit value to sign + * @returns {Promise} Status and signature + */ + async sign (index: number, nonce: Uint8Array): Promise + async sign (index: number = 0, input: Uint8Array | SendBlock | ReceiveBlock | ChangeBlock): Promise { + if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) { + throw new TypeError('Invalid account index') + } + if (input instanceof Uint8Array) { + // input is a nonce + return await this.#signNonce(index, input) + } else { + // input is a block + return await this.#signBlock(index, input) } } @@ -106,19 +258,6 @@ export class LedgerWallet extends Wallet { return wallet } - /** - * Attempts to close the current process on the Ledger device. - * - * Overrides the default wallet `lock()` method since as a hardware wallet it - * does not need to be encrypted by software. - * - * @returns True if successfully locked - */ - async lock (): Promise { - const { status } = await this.close() - return status === 'OK' - } - /** * Attempts to connect to the Ledger device. * @@ -131,72 +270,51 @@ export class LedgerWallet extends Wallet { return await this.connect() === 'CONNECTED' } - async init (): Promise { - await this.checkBrowserSupport() - const { usb } = globalThis.navigator - if (usb) { - usb.addEventListener('connect', console.log.bind(console)) - usb.addEventListener('disconnect', console.log.bind(console)) - } - } - /** - * Check which transport protocols are supported by the browser and set the - * transport type according to the following priorities: Bluetooth, USB, HID. + * Update cache from raw block data. Suitable for offline use. + * + * @param {number} index - Account number + * @param {object} block - JSON-formatted block data */ - async checkBrowserSupport (): Promise { - console.log('Checking browser Ledger support...') - try { - if (await TransportBLE.isSupported()) { - return TransportBLE - } - if (await TransportUSB.isSupported()) { - return TransportUSB + async updateCache (index: number, block: { [key: string]: string }): Promise + /** + * Update cache from a block hash by calling out to a node. Suitable for online + * use only. + * + * @param {number} index - Account number + * @param {string} hash - Hexadecimal block hash + * @param {Rpc} rpc - Rpc class object with a node URL + */ + async updateCache (index: number, hash: string, rpc: Rpc): Promise + async updateCache (index: number, input: any, node?: Rpc): Promise { + if (typeof input === 'string' && node instanceof Rpc) { + const data = { + 'json_block': 'true', + 'hash': input } - if (await TransportHID.isSupported()) { - return TransportHID + const res = await node.call('block_info', data) + if (!res || res.ok === false) { + throw new Error(`Unable to fetch block info`, res) } - } catch { } - throw new Error('Unsupported browser') - } - - async connect (): Promise { - const { usb } = globalThis.navigator - if (usb) { - usb.removeEventListener('disconnect', this.onDisconnectUsb.bind(this)) - usb.addEventListener('disconnect', this.onDisconnectUsb.bind(this)) + input = res.contents } - const version = await this.version() - if (version.status === 'OK') { - 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 if (version.name === 'BOLOS') { - const open = await this.open() - this.#status = (open.status === 'OK') - ? 'CONNECTED' - : 'DISCONNECTED' - } else { - this.#status = 'BUSY' - } - } else { - this.#status = 'DISCONNECTED' + const { status } = await this.#cacheBlock(index, input) + if (status !== 'OK') { + throw new Error(status) } - return { status: this.status } + return { status } } - async onDisconnectUsb (e: USBConnectionEvent): Promise { - if (e.device?.manufacturerName === 'Ledger') { - const { usb } = globalThis.navigator - usb.removeEventListener('disconnect', this.onDisconnectUsb) - this.#status = 'DISCONNECTED' - } + /** + * 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 version (): Promise { + return await this.#version() } /** @@ -212,13 +330,14 @@ export class LedgerWallet extends Wallet { * * @returns Status of command */ - async open (): Promise { + async #open (): Promise { const name = new TextEncoder().encode('Nano') const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) - const response = await transport.send(0xe0, 0xd8, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, name as Buffer) + const response = await transport + .send(0xe0, 0xd8, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, name as Buffer) .then(res => bytes.toDec(res)) .catch(err => err.statusCode) as number - return new Promise(resolve => setTimeout(resolve, 1000, { status: LEDGER_STATUS_CODES[response] })) + return new Promise(r => setTimeout(r, 1000, { status: LEDGER_STATUS_CODES[response] })) } /** @@ -234,12 +353,13 @@ export class LedgerWallet extends Wallet { * * @returns Status of command */ - async close (): Promise { + async #close (): Promise { const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) - const response = await transport.send(0xb0, 0xa7, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused) + const response = await transport + .send(0xb0, 0xa7, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused) .then(res => bytes.toDec(res)) .catch(err => err.statusCode) as number - return new Promise(resolve => setTimeout(resolve, 1000, { status: LEDGER_STATUS_CODES[response] })) + return new Promise(r => setTimeout(r, 1000, { status: LEDGER_STATUS_CODES[response] })) } /** @@ -250,7 +370,7 @@ export class LedgerWallet extends Wallet { * * @returns Status, process name, and version */ - async version (): Promise { + async #version (): Promise { const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) const response = await transport .send(0xb0, LEDGER_ADPU_CODES.version, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused) @@ -271,136 +391,6 @@ export class LedgerWallet extends Wallet { return { status, name, version } } - /** - * Since the Ledger device can only return one account per request, this - * overrides the default behavior of calling `accounts()` to instead - * directly derive and return a single account at the specified index. - * - * @returns Account - */ - async account (index: number = 0, show: boolean = false): Promise { - const { status, publicKey } = await this.#account(index, show) - if (publicKey == null) { - throw new Error('Failed to get account from device', { cause: status }) - } - const account = await Account.fromPublicKey(publicKey) - return account - } - - /** - * Since the Ledger device can only return one account per request, this - * overrides the default behavior of returning multiple accounts to instead - * return a single account at the specified index. - * - * @returns Account - */ - async accounts (): Promise { - throw new Error(`Ledger device only supports 'account()' calls`) - } - - /** - * Sign a block with the Ledger device. - * - * @param {number} index - Account number - * @param {object} block - Block data to sign - * @returns {Promise} Status, signature, and block hash - */ - async sign (index: number, block: SendBlock | ReceiveBlock | ChangeBlock): Promise - /** - * Sign a nonce with the Ledger device. - * - * @param {number} index - Account number - * @param {Uint8Array} nonce - 128-bit value to sign - * @returns {Promise} Status and signature - */ - async sign (index: number, nonce: Uint8Array): Promise - async sign (index: number = 0, input: Uint8Array | SendBlock | ReceiveBlock | ChangeBlock): Promise { - if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) { - throw new TypeError('Invalid account index') - } - const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4) - const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4) - const account = dec.toBytes(index + HARDENED_OFFSET, 4) - - let instruction: number - let data: Uint8Array - - if (input instanceof Uint8Array) { - // nonce signing is currently broken: https://github.com/LedgerHQ/app-nano/pull/14 - // input is a nonce - instruction = LEDGER_ADPU_CODES.signNonce - if (input.byteLength !== 16) { - throw new RangeError('Nonce must be 16-byte string') - } - data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...input]) - } else { - // input is a block - instruction = LEDGER_ADPU_CODES.signBlock - const previous = hex.toBytes(input.previous, 32) - const link = hex.toBytes(input.link, 32) - const representative = hex.toBytes(input.representative.publicKey, 32) - const balance = hex.toBytes(BigInt(input.balance).toString(16), 16) - data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance]) - } - const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) - const response = await transport.send(LEDGER_ADPU_CODES.class, instruction, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer) - .catch(err => dec.toBytes(err.statusCode)) as Uint8Array - await transport.close() - - const statusCode = bytes.toDec(response.slice(-2)) as number - const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' - - if (response.byteLength === 2) { - return { status, signature: null } - } - if (response.byteLength === 66) { - const signature = bytes.toHex(response.slice(0, 64)) - return { status, signature } - } - if (response.byteLength === 98) { - const hash = bytes.toHex(response.slice(0, 32)) - const signature = bytes.toHex(response.slice(32, 96)) - return { status, signature, hash } - } - - throw new Error('Unexpected byte length from device signature', { cause: response }) - } - - /** - * Update cache from raw block data. Suitable for offline use. - * - * @param {number} index - Account number - * @param {object} block - JSON-formatted block data - */ - async updateCache (index: number, block: { [key: string]: string }): Promise - /** - * Update cache from a block hash by calling out to a node. Suitable for online - * use only. - * - * @param {number} index - Account number - * @param {string} hash - Hexadecimal block hash - * @param {Rpc} rpc - Rpc class object with a node URL - */ - async updateCache (index: number, hash: string, rpc: Rpc): Promise - async updateCache (index: number, input: any, node?: Rpc): Promise { - if (typeof input === 'string' && node instanceof Rpc) { - const data = { - 'json_block': 'true', - 'hash': input - } - const res = await node.call('block_info', data) - if (!res || res.ok === false) { - throw new Error(`Unable to fetch block info`, res) - } - input = res.contents - } - const { status } = await this.#cacheBlock(index, input) - if (status !== 'OK') { - throw new Error(status) - } - return { status } - } - /** * Request an account at a specific BIP-44 index. * @@ -476,6 +466,85 @@ export class LedgerWallet extends Wallet { return { status: LEDGER_STATUS_CODES[response] } } + /** + * Sign a block with the Ledger device. + * + * @param {number} index - Account number + * @param {object} block - Block data to sign + * @returns {Promise} Status, signature, and block hash + */ + async #signBlock (index: number, block: SendBlock | ReceiveBlock | ChangeBlock): Promise { + if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) { + throw new TypeError('Invalid account index') + } + + const account = dec.toBytes(index + HARDENED_OFFSET, 4) + const previous = hex.toBytes(block.previous, 32) + const link = hex.toBytes(block.link, 32) + const representative = hex.toBytes(block.representative.publicKey, 32) + const balance = hex.toBytes(BigInt(block.balance).toString(16), 16) + const data = new Uint8Array([...LedgerWallet.#derivationPath, ...account, ...previous, ...link, ...representative, ...balance]) + + const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) + const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.signBlock, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer) + .catch(err => dec.toBytes(err.statusCode)) as Uint8Array + await transport.close() + + const statusCode = bytes.toDec(response.slice(-2)) as number + const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' + + if (response.byteLength === 2) { + return { status, signature: null } + } + if (response.byteLength === 98) { + const hash = bytes.toHex(response.slice(0, 32)) + const signature = bytes.toHex(response.slice(32, 96)) + return { status, signature, hash } + } + + throw new Error('Unexpected byte length from device signature', { cause: response }) + } + + /** + * Sign a nonce with the Ledger device. + * + * nonce signing is currently broken: https://github.com/LedgerHQ/app-nano/pull/14 + * + * @param {number} index - Account number + * @param {Uint8Array} nonce - 128-bit value to sign + * @returns {Promise} Status and signature + */ + async #signNonce (index: number, nonce: Uint8Array): Promise { + if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) { + throw new TypeError('Invalid account index') + } + if (nonce.byteLength !== 16) { + throw new RangeError('Nonce must be 16-byte string') + } + + const derivationAccount = dec.toBytes(index + HARDENED_OFFSET, 4) + const data = new Uint8Array([...LedgerWallet.#derivationPath, ...derivationAccount, ...nonce]) + + const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) + const response = await transport + .send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.signNonce, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer) + .catch(err => dec.toBytes(err.statusCode)) as Uint8Array + await transport.close() + + const statusCode = bytes.toDec(response.slice(-2)) as number + const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' + + if (response.byteLength === 2) { + return { status, signature: null } + } + if (response.byteLength === 66) { + const signature = bytes.toHex(response.slice(0, 64)) + return { status, signature } + } + + throw new Error('Unexpected byte length from device signature', { cause: response }) + } + /** * Gets the public key for an account from the Ledger device. * diff --git a/test/test.ledger.mjs b/test/test.ledger.mjs index 67e4519..62eb225 100644 --- a/test/test.ledger.mjs +++ b/test/test.ledger.mjs @@ -13,22 +13,27 @@ import { Account, LedgerWallet, ReceiveBlock, SendBlock } from '../dist/main.min */ await suite('Ledger hardware wallet', { skip: false || isNode }, async () => { - let wallet, account, openBlock, sendBlock, receiveBlock + let wallet = await LedgerWallet.create(), account, openBlock, sendBlock, receiveBlock - await test('connect to the device', async () => { - wallet = await LedgerWallet.create() - const { status } = await click( - 'Unlock, then click to connect', + await test('request permissions', async () => { + let status = wallet.status + status = await click( + 'Reset permissions, unlock device, quit Nano app, then click to continue', + async () => wallet.connect() + ) + assert.equals(status, 'BUSY') + assert.equals(status, wallet.status) + + status = await click( + 'Open Nano app on device, then click to continue', async () => wallet.connect() ) assert.equals(status, 'CONNECTED') + assert.equals(status, wallet.status) }) await test('get version', async () => { - const { status, name, version } = await click( - 'Click to get version', - async () => wallet.version() - ) + const { status, name, version } = await wallet.version() assert.equals(status, 'OK') assert.equals(name, 'Nano') @@ -36,10 +41,7 @@ await suite('Ledger hardware wallet', { skip: false || isNode }, async () => { }) await test('get first account', async () => { - account = await click( - 'Click to get account', - async () => wallet.account() - ) + account = await wallet.account() assert.exists(account) assert.ok(account instanceof Account) @@ -61,29 +63,20 @@ await suite('Ledger hardware wallet', { skip: false || isNode }, async () => { assert.nullish(openBlock.signature) assert.equals(openBlock.account.publicKey, account.publicKey) - const { status, hash, signature } = await click( - 'Click to sign opening ReceiveBlock from wallet', - async () => wallet.sign(0, openBlock) - ) + const { status, hash, signature } = await wallet.sign(0, openBlock) assert.equals(status, 'OK') assert.ok(/[A-Fa-f0-9]{64}/.test(hash)) assert.ok(/[A-Fa-f0-9]{128}/.test(signature)) - await click( - 'Click to sign open ReceiveBlock from block and compare signatures', - async () => openBlock.sign(0) - ) + await openBlock.sign(0) assert.ok(/[A-Fa-f0-9]{128}/.test(openBlock.signature)) assert.equals(signature, openBlock.signature) }) await test('cache open block', async () => { - const { status } = await click( - 'Click to cache open block', - async () => wallet.updateCache(0, openBlock) - ) + const { status } = await wallet.updateCache(0, openBlock) assert.equals(status, 'OK') }) @@ -102,10 +95,7 @@ await suite('Ledger hardware wallet', { skip: false || isNode }, async () => { assert.nullish(sendBlock.signature) assert.equals(sendBlock.account.publicKey, account.publicKey) - const { status, hash, signature } = await click( - 'Click to sign SendBlock from wallet', - async () => wallet.sign(0, sendBlock) - ) + const { status, hash, signature } = await wallet.sign(0, sendBlock) assert.equals(status, 'OK') assert.ok(/[A-Fa-f0-9]{64}/.test(hash)) @@ -127,19 +117,14 @@ await suite('Ledger hardware wallet', { skip: false || isNode }, async () => { assert.nullish(receiveBlock.signature) assert.equals(receiveBlock.account.publicKey, account.publicKey) - await click( - 'Click to sign SendBlock from block', - async () => receiveBlock.sign(0, sendBlock) - ) + await receiveBlock.sign(0, sendBlock) assert.ok(/[A-Fa-f0-9]{128}/.test(receiveBlock.signature)) }) await test('destroy wallet', async () => { - const { status } = await click( - 'Click to close', - async () => wallet.close() - ) + const { status } = await wallet.close() + assert.equals(status, 'OK') await wallet.destroy() }) -- 2.47.3