From 156bd7844bfc628e8267f8808bd8adc81ae3a389 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Fri, 1 Aug 2025 14:15:00 -0700 Subject: [PATCH] Fix tests and types. Rename Ledger class. --- src/lib/block.ts | 4 ++-- src/lib/ledger.ts | 40 +++++++++++++++++------------------ src/lib/tools.ts | 6 +++--- src/main.ts | 2 ++ src/types.d.ts | 39 +++++++++++++++++++++++----------- test/test.blocks.mjs | 2 +- test/test.derive-accounts.mjs | 24 +++++++++------------ test/test.ledger.mjs | 14 ++++++------ 8 files changed, 72 insertions(+), 59 deletions(-) diff --git a/src/lib/block.ts b/src/lib/block.ts index 805d71f..f97215b 100644 --- a/src/lib/block.ts +++ b/src/lib/block.ts @@ -147,8 +147,8 @@ abstract class Block { async sign (input?: number | string, frontier?: ChangeBlock | ReceiveBlock | SendBlock): Promise { if (typeof input === 'number') { const index = input - const { LedgerWallet } = await import('./ledger') - const ledger = await LedgerWallet.create() + const { Ledger } = await import('./ledger') + const ledger = await Ledger.create() await ledger.connect() if (frontier) { try { diff --git a/src/lib/ledger.ts b/src/lib/ledger.ts index ef32031..002eacd 100644 --- a/src/lib/ledger.ts +++ b/src/lib/ledger.ts @@ -19,7 +19,7 @@ import { DeviceStatus, KeyPair, LedgerAccountResponse, LedgerResponse, LedgerSig * private keys are held in the secure chip of the device. As such, the user * is responsible for using Ledger technology to back up these pieces of data. */ -export class LedgerWallet extends Wallet { +export class Ledger extends Wallet { static #isInternal: boolean = false static #derivationPath: Uint8Array = new Uint8Array([ LEDGER_ADPU_CODES.bip32DerivationLevel, @@ -56,12 +56,12 @@ export class LedgerWallet extends Wallet { * Creates a new Ledger hardware wallet communication layer by dynamically * importing the ledger.js service. * - * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object + * @returns {Ledger} A wallet containing accounts and a Ledger device communication object */ - static async create (): Promise { + static async create (): Promise { try { if (this.isUnsupported) throw new Error('Browser is unsupported') - LedgerWallet.#isInternal = true + Ledger.#isInternal = true const self = new this() await Database.add({ [self.id]: { id: self.id, type: 'Ledger' } }, Wallet.DB_NAME) return self @@ -74,18 +74,18 @@ export class LedgerWallet extends Wallet { * Overrides `import()` from the base Wallet class since Ledger secrets cannot * be extracted from the device. */ - static import (): Promise { - return LedgerWallet.create() + static import (): Promise { + return Ledger.create() } #status: DeviceStatus = 'DISCONNECTED' get status (): DeviceStatus { return this.#status } private constructor () { - if (!LedgerWallet.#isInternal) { + if (!Ledger.#isInternal) { throw new Error(`LedgerWallet cannot be instantiated directly. Use 'await LedgerWallet.create()' instead.`) } - LedgerWallet.#isInternal = false + Ledger.#isInternal = false super('Ledger') } @@ -103,7 +103,7 @@ export class LedgerWallet extends Wallet { if (version.status !== 'OK') { this.#status = 'DISCONNECTED' } else if (version.name === 'Nano') { - const { status } = await LedgerWallet.#account() + const { status } = await Ledger.#account() if (status === 'OK') { this.#status = 'CONNECTED' } else if (status === 'SECURITY_STATUS_NOT_SATISFIED') { @@ -329,7 +329,7 @@ export class LedgerWallet extends Wallet { */ async #open (): Promise { const name = new TextEncoder().encode('Nano') - const transport = await LedgerWallet.DynamicTransport.create(LedgerWallet.openTimeout, LedgerWallet.listenTimeout) + const transport = await Ledger.DynamicTransport.create(Ledger.openTimeout, Ledger.listenTimeout) const response = await transport .send(0xe0, 0xd8, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, name as Buffer) .then(res => bytes.toDec(res)) @@ -351,7 +351,7 @@ export class LedgerWallet extends Wallet { * @returns Status of command */ async #close (): Promise { - const transport = await LedgerWallet.DynamicTransport.create(LedgerWallet.openTimeout, LedgerWallet.listenTimeout) + const transport = await Ledger.DynamicTransport.create(Ledger.openTimeout, Ledger.listenTimeout) const response = await transport .send(0xb0, 0xa7, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused) .then(res => bytes.toDec(res)) @@ -369,9 +369,9 @@ export class LedgerWallet extends Wallet { throw new TypeError('Invalid account index') } const account = dec.toBytes(index + HARDENED_OFFSET, 4) - const data = new Uint8Array([...LedgerWallet.#derivationPath, ...account]) + const data = new Uint8Array([...Ledger.#derivationPath, ...account]) - const transport = await LedgerWallet.DynamicTransport.create(LedgerWallet.openTimeout, LedgerWallet.listenTimeout) + const transport = await Ledger.DynamicTransport.create(Ledger.openTimeout, Ledger.listenTimeout) const response = await transport .send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.account, show ? 1 : 0, LEDGER_ADPU_CODES.paramUnused, data as Buffer) .catch(err => dec.toBytes(err.statusCode)) as Uint8Array @@ -422,7 +422,7 @@ export class LedgerWallet extends Wallet { const signature = hex.toBytes(block.signature, 64) const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance, ...signature]) - const transport = await LedgerWallet.DynamicTransport.create(LedgerWallet.openTimeout, LedgerWallet.listenTimeout) + const transport = await Ledger.DynamicTransport.create(Ledger.openTimeout, Ledger.listenTimeout) const response = await transport .send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.cacheBlock, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer) .then(res => bytes.toDec(res)) @@ -449,9 +449,9 @@ export class LedgerWallet extends Wallet { 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 data = new Uint8Array([...Ledger.#derivationPath, ...account, ...previous, ...link, ...representative, ...balance]) - const transport = await LedgerWallet.DynamicTransport.create(LedgerWallet.openTimeout, LedgerWallet.listenTimeout) + const transport = await Ledger.DynamicTransport.create(Ledger.openTimeout, Ledger.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() @@ -489,9 +489,9 @@ export class LedgerWallet extends Wallet { } const derivationAccount = dec.toBytes(index + HARDENED_OFFSET, 4) - const data = new Uint8Array([...LedgerWallet.#derivationPath, ...derivationAccount, ...nonce]) + const data = new Uint8Array([...Ledger.#derivationPath, ...derivationAccount, ...nonce]) - const transport = await LedgerWallet.DynamicTransport.create(LedgerWallet.openTimeout, LedgerWallet.listenTimeout) + const transport = await Ledger.DynamicTransport.create(Ledger.openTimeout, Ledger.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 @@ -520,7 +520,7 @@ export class LedgerWallet extends Wallet { * @returns Status, process name, and version */ async #version (): Promise { - const transport = await LedgerWallet.DynamicTransport.create(LedgerWallet.openTimeout, LedgerWallet.listenTimeout) + const transport = await Ledger.DynamicTransport.create(Ledger.openTimeout, Ledger.listenTimeout) const response = await transport .send(0xb0, LEDGER_ADPU_CODES.version, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused) .catch(err => dec.toBytes(err.statusCode)) as Uint8Array @@ -549,7 +549,7 @@ export class LedgerWallet extends Wallet { async ckd (indexes: number[]): Promise { const results: KeyPair[] = [] for (const index of indexes) { - const { status, publicKey } = await LedgerWallet.#account(index) + const { status, publicKey } = await Ledger.#account(index) if (status === 'OK' && publicKey != null) { results.push({ publicKey, index }) } else { diff --git a/src/lib/tools.ts b/src/lib/tools.ts index 8065aea..192aaa5 100644 --- a/src/lib/tools.ts +++ b/src/lib/tools.ts @@ -6,7 +6,7 @@ import { Blake2b } from './blake2b' import { SendBlock } from './block' import { UNITS } from './constants' import { bytes, hex } from './convert' -import { LedgerWallet } from './ledger' +import { Ledger } from './ledger' import { NanoNaCl } from './nano-nacl' import { Rpc } from './rpc' import { Wallet } from './wallet' @@ -107,7 +107,7 @@ export async function sign (key: Key, ...input: string[]): Promise { * them all to a single recipient address. Hardware wallets are unsupported. * * @param {Rpc|string|URL} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks -* @param {Blake2bWallet|Bip44Wallet|LedgerWallet} wallet - Wallet from which to sweep funds +* @param {Blake2bWallet|Bip44Wallet|Ledger} wallet - Wallet from which to sweep funds * @param {string} recipient - Destination address for all swept funds * @param {number} from - Starting account index to sweep * @param {number} to - Ending account index to sweep @@ -115,7 +115,7 @@ export async function sign (key: Key, ...input: string[]): Promise { */ export async function sweep ( rpc: Rpc | string | URL, - wallet: Wallet | LedgerWallet, + wallet: Wallet | Ledger, recipient: string, from: number = 0, to: number = from diff --git a/src/main.ts b/src/main.ts index 7d6536b..47918a7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { Account } from './lib/account' import { Blake2b } from './lib/blake2b' import { ChangeBlock, ReceiveBlock, SendBlock } from './lib/block' +import { Ledger } from './lib/ledger' import { Rolodex } from './lib/rolodex' import { Rpc } from './lib/rpc' import { Tools } from './lib/tools' @@ -13,6 +14,7 @@ export { Account, Blake2b, ChangeBlock, ReceiveBlock, SendBlock, + Ledger, Rolodex, Rpc, Tools, diff --git a/src/types.d.ts b/src/types.d.ts index 9be9b96..55fae05 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -559,13 +559,13 @@ export declare function sign (key: Key, ...input: string[]): Promise * them all to a single recipient address. Hardware wallets are unsupported. * * @param {Rpc|string|URL} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks -* @param {Wallet|LedgerWallet} wallet - Wallet from which to sweep funds +* @param {Wallet|Ledger} wallet - Wallet from which to sweep funds * @param {string} recipient - Destination address for all swept funds * @param {number} from - Starting account index to sweep * @param {number} to - Ending account index to sweep * @returns An array of results including both successes and failures */ -export declare function sweep (rpc: Rpc | string | URL, wallet: Wallet | LedgerWallet, recipient: string, from?: number, to?: number): Promise +export declare function sweep (rpc: Rpc | string | URL, wallet: Wallet | Ledger, recipient: string, from?: number, to?: number): Promise /** * Verifies the signature of arbitrary strings using a public key. * @@ -770,7 +770,7 @@ interface LedgerSignResponse extends LedgerResponse { * private keys are held in the secure chip of the device. As such, the user * is responsible for using Ledger technology to back up these pieces of data. */ -export declare class LedgerWallet extends Wallet { +export declare class Ledger extends Wallet { #private static DynamicTransport: typeof TransportBLE | typeof TransportUSB | typeof TransportHID static get listenTimeout (): 30000 @@ -784,9 +784,14 @@ export declare class LedgerWallet extends Wallet { * Creates a new Ledger hardware wallet communication layer by dynamically * importing the ledger.js service. * - * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object + * @returns {Ledger} A wallet containing accounts and a Ledger device communication object */ - static create (): Promise + static create (): Promise + /** + * Overrides `import()` from the base Wallet class since Ledger secrets cannot + * be extracted from the device. + */ + static import (): Promise get status (): DeviceStatus private constructor () /** @@ -820,13 +825,6 @@ export declare class LedgerWallet extends Wallet { onConnectUsb: (e: USBConnectionEvent) => Promise onDisconnectUsb: (e: USBConnectionEvent) => Promise /** - * Retrieves an existing Ledger wallet from storage using its ID. - * - * @param {string} id - Generated when the wallet was initially created - * @returns {LedgerWallet} Restored LedgerWallet - */ - static restore (id: string): Promise - /** * Sign a block with the Ledger device. * * @param {number} index - Account number @@ -860,6 +858,23 @@ export declare class LedgerWallet extends Wallet { */ updateCache (index: number, hash: string, rpc: Rpc): Promise /** + * Checks whether a given seed matches the wallet seed. The wallet must be + * unlocked prior to verification. + * + * @param {string} seed - Hexadecimal seed to be matched against the wallet data + * @returns True if input matches wallet seed + */ + verify (seed: string): Promise + /** + * Checks whether a given mnemonic phrase matches the wallet mnemonic. If a + * personal salt was used when generating the mnemonic, it cannot be verified. + * The wallet must be unlocked prior to verification. + * + * @param {string} mnemonic - Phrase to be matched against the wallet data + * @returns True if input matches wallet mnemonic + */ + verify (mnemonic: string): Promise + /** * Get the version of the current process. If a specific app is running, get * the app version. Otherwise, get the Ledger BOLOS version instead. * diff --git a/test/test.blocks.mjs b/test/test.blocks.mjs index 398c1e9..d6cf3bd 100644 --- a/test/test.blocks.mjs +++ b/test/test.blocks.mjs @@ -85,7 +85,7 @@ await Promise.all([ suite('Block signing using official test vectors', async () => { await test('sign open block with wallet', async () => { - const wallet = await Wallet.import('Test', 'BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) + const wallet = await Wallet.import('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await assert.resolves(wallet.unlock(NANO_TEST_VECTORS.PASSWORD)) const block = new ReceiveBlock( diff --git a/test/test.derive-accounts.mjs b/test/test.derive-accounts.mjs index 8c570c1..5a7745d 100644 --- a/test/test.derive-accounts.mjs +++ b/test/test.derive-accounts.mjs @@ -7,24 +7,20 @@ import { assert, isNode, suite, test } from './GLOBALS.mjs' import { NANO_TEST_VECTORS } from './VECTORS.mjs' /** -* @type {typeof import('../dist/types.d.ts').Bip44Wallet} +* @type {typeof import('../dist/types.d.ts').Wallet} */ -let Bip44Wallet -/** -* @type {typeof import('../dist/types.d.ts').Blake2bWallet} -*/ -let Blake2bWallet +let Wallet if (isNode) { - ({ Bip44Wallet, Blake2bWallet } = await import('../dist/nodejs.min.js')) + ({ Wallet } = await import('../dist/nodejs.min.js')) } else { - ({ Bip44Wallet, Blake2bWallet } = await import('../dist/browser.min.js')) + ({ Wallet } = await import('../dist/browser.min.js')) } await Promise.all([ suite('Derive accounts from BIP-44 wallet', async () => { await test('derive the first account from the given BIP-44 seed', async () => { - const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) + const wallet = await Wallet.import('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account() @@ -40,7 +36,7 @@ await Promise.all([ }) await test('derive low indexed accounts from the given BIP-44 seed', async () => { - const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) + const wallet = await Wallet.import('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const accounts = await wallet.accounts(1, 2) @@ -56,7 +52,7 @@ await Promise.all([ }) await test('derive high indexed accounts from the given seed', async () => { - const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) + const wallet = await Wallet.import('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const accounts = await wallet.accounts(0x70000000, 0x7000000f) @@ -76,7 +72,7 @@ await Promise.all([ suite('Derive accounts from BLAKE2b wallet', async () => { await test('derive the second account from the given BLAKE2b seed', async () => { - const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BLAKE2B_SEED) + const wallet = await Wallet.import('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BLAKE2B_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account(1) @@ -94,7 +90,7 @@ await Promise.all([ }) await test('derive low indexed accounts from the given BLAKE2B seed', async () => { - const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BLAKE2B_SEED) + const wallet = await Wallet.import('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BLAKE2B_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const accounts = await wallet.accounts(2, 3) @@ -110,7 +106,7 @@ await Promise.all([ }) await test('derive high indexed accounts from the given seed', async () => { - const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BLAKE2B_SEED) + const wallet = await Wallet.import('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BLAKE2B_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const accounts = await wallet.accounts(0x70000000, 0x7000000f) diff --git a/test/test.ledger.mjs b/test/test.ledger.mjs index b4c53ed..f405f64 100644 --- a/test/test.ledger.mjs +++ b/test/test.ledger.mjs @@ -11,9 +11,9 @@ import { NANO_TEST_VECTORS } from './VECTORS.mjs' */ let Account /** -* @type {typeof import('../dist/types.d.ts').LedgerWallet} +* @type {typeof import('../dist/types.d.ts').Ledger} */ -let LedgerWallet +let Ledger /** * @type {typeof import('../dist/types.d.ts').ReceiveBlock} */ @@ -28,9 +28,9 @@ let Rpc let SendBlock if (isNode) { - ({ Account, LedgerWallet, ReceiveBlock, Rpc, SendBlock } = await import('../dist/nodejs.min.js')) + ({ Account, Ledger, ReceiveBlock, Rpc, SendBlock } = await import('../dist/nodejs.min.js')) } else { - ({ Account, LedgerWallet, ReceiveBlock, Rpc, SendBlock } = await import('../dist/browser.min.js')) + ({ Account, Ledger, ReceiveBlock, Rpc, SendBlock } = await import('../dist/browser.min.js')) } const rpc = new Rpc(env.NODE_URL ?? '', env.API_KEY_NAME) @@ -41,12 +41,12 @@ const rpc = new Rpc(env.NODE_URL ?? '', env.API_KEY_NAME) */ await Promise.all([ /* node:coverage disable */ - suite('Ledger hardware wallet', { skip: false || isNode || LedgerWallet.isUnsupported }, async () => { + suite('Ledger hardware wallet', { skip: false || isNode || Ledger.isUnsupported }, async () => { let wallet, account, openBlock, sendBlock, receiveBlock await test('request permissions', async () => { - wallet = await LedgerWallet.create() + wallet = await Ledger.create() let status = wallet.status assert.equal(status, 'DISCONNECTED') assert.equal(status, wallet.status) @@ -214,7 +214,7 @@ await Promise.all([ await test('fail when using new', async () => { //@ts-expect-error - assert.throws(() => new LedgerWallet()) + assert.throws(() => new Ledger()) }) await test('fail to sign a block without caching frontier', async () => { -- 2.47.3