From: Chris Duncan Date: Sat, 5 Jul 2025 09:21:58 +0000 (-0700) Subject: Merge separate ledger classes into single class. X-Git-Tag: v0.10.5~116^2 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=2d586af55562edee6cb4e579ecfd637fa5cde828;p=libnemo.git Merge separate ledger classes into single class. --- diff --git a/src/lib/block.ts b/src/lib/block.ts index 2db2fe5..d430bed 100644 --- a/src/lib/block.ts +++ b/src/lib/block.ts @@ -119,8 +119,8 @@ abstract class Block { async sign (input?: number | string, block?: { [key: string]: string }): Promise { if (typeof input === 'number') { const index = input - const { Ledger } = await import('./wallets') - const ledger = await Ledger.init() + const { LedgerWallet } = await import('./wallets') + const ledger = await LedgerWallet.create() await ledger.open() if (block) { try { diff --git a/src/lib/wallets/index.ts b/src/lib/wallets/index.ts index a6569d0..574fc70 100644 --- a/src/lib/wallets/index.ts +++ b/src/lib/wallets/index.ts @@ -3,4 +3,4 @@ export { Bip44Wallet } from './bip44-wallet' export { Blake2bWallet } from './blake2b-wallet' -export { Ledger, LedgerWallet } from './ledger-wallet' +export { LedgerWallet } from './ledger-wallet' diff --git a/src/lib/wallets/ledger-wallet.ts b/src/lib/wallets/ledger-wallet.ts index 4b4d28c..ed5e324 100644 --- a/src/lib/wallets/ledger-wallet.ts +++ b/src/lib/wallets/ledger-wallet.ts @@ -5,6 +5,7 @@ import Transport from '@ledgerhq/hw-transport' 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' @@ -44,17 +45,13 @@ interface LedgerSignResponse extends LedgerResponse { */ export class LedgerWallet extends Wallet { static #isInternal: boolean = false - #ledger: Ledger - get ledger () { return this.#ledger } - - constructor (id: Entropy, ledger: Ledger) { + constructor (id: Entropy) { if (!LedgerWallet.#isInternal) { throw new Error(`LedgerWallet cannot be instantiated directly. Use 'await LedgerWallet.create()' instead.`) } LedgerWallet.#isInternal = false super(id) - this.#ledger = ledger } /** @@ -64,10 +61,11 @@ export class LedgerWallet extends Wallet { * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object */ static async create (): Promise { - const l = await Ledger.init() const id = await Entropy.create(16) LedgerWallet.#isInternal = true - return new this(id, l) + const wallet = new this(id) + await wallet.init() + return wallet } /** @@ -80,9 +78,10 @@ export class LedgerWallet extends Wallet { if (typeof id !== 'string' || id === '') { throw new TypeError('Wallet ID is required to restore') } - const l = await Ledger.init() LedgerWallet.#isInternal = true - return new this(await Entropy.import(id), l) + const wallet = new this(await Entropy.import(id)) + await wallet.init() + return wallet } /** @@ -94,7 +93,7 @@ export class LedgerWallet extends Wallet { async ckd (indexes: number[]): Promise { const results: KeyPair[] = [] for (const index of indexes) { - const { status, publicKey } = await this.ledger.account(index) + const { status, publicKey } = await this.#account(index) if (status === 'OK' && publicKey != null) { results.push({ publicKey, index }) } else { @@ -113,10 +112,7 @@ export class LedgerWallet extends Wallet { * @returns True if successfully locked */ async lock (): Promise { - if (this.ledger == null) { - return false - } - const result = await this.ledger.close() + const result = await this.close() return result.status === 'OK' } @@ -129,16 +125,10 @@ export class LedgerWallet extends Wallet { * @returns True if successfully unlocked */ async unlock (): Promise { - if (this.ledger == null) { - return false - } - const result = await this.ledger.connect() + const result = await this.connect() return result === 'OK' } -} -export class Ledger { - static #isInternal: boolean = false #status: 'DISCONNECTED' | 'LOCKED' | 'BUSY' | 'CONNECTED' = 'DISCONNECTED' get status () { return this.#status } openTimeout = 3000 @@ -146,20 +136,9 @@ export class Ledger { transport: Transport | null = null DynamicTransport: typeof TransportBLE | typeof TransportUSB | typeof TransportHID = TransportHID - constructor () { - if (!Ledger.#isInternal) { - throw new Error('Ledger cannot be instantiated directly. Use Ledger.init()') - } - Ledger.#isInternal = false - Buffer - } - - static async init (): Promise { - Ledger.#isInternal = true - const self = new this() - await self.checkBrowserSupport() - await self.listen() - return self + async init (): Promise { + await this.checkBrowserSupport() + await this.listen() } /** @@ -202,10 +181,10 @@ export class Ledger { const version = await this.version() if (version.status === 'OK') { if (version.name === 'Nano') { - const account = await this.account() - if (account.status === 'OK') { + const { status } = await this.#account() + if (status === 'OK') { this.#status = 'CONNECTED' - } else if (account.status === 'SECURITY_STATUS_NOT_SATISFIED') { + } else if (status === 'SECURITY_STATUS_NOT_SATISFIED') { this.#status = 'LOCKED' } else { this.#status = 'DISCONNECTED' @@ -306,40 +285,17 @@ export class Ledger { } /** - * Get an account at a specific BIP-44 index. + * Request an account at a specific BIP-44 index. * - * @returns Response object containing command status, public key, and address + * @returns Account */ - async account (index: number = 0, show: boolean = false): 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) - const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account]) - - const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) - const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.account, show ? 0x01 : 0x00, LEDGER_ADPU_CODES.paramUnused, data as Buffer) - .catch(err => dec.toBytes(err.statusCode)) as Uint8Array - await transport.close() - - if (response.length === 2) { - const statusCode = bytes.toDec(response) as number - const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' - return { status, publicKey: null, address: null } - } - - try { - const publicKey = bytes.toHex(response.slice(0, 32)) - const addressLength = response[32] - const address = response.slice(33, 33 + addressLength).toString() - const statusCode = bytes.toDec(response.slice(33 + addressLength)) as number - const status = LEDGER_STATUS_CODES[statusCode] - return { status, publicKey, address } - } catch (err) { - return { status: 'ERROR_PARSING_ACCOUNT', publicKey: null, address: null } + 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 } /** @@ -480,4 +436,41 @@ export class Ledger { } return this.cacheBlock(index, input) } + + /** + * Request an account at a specific BIP-44 index. + * + * @returns Response object containing command status, public key, and address + */ + async #account (index: number = 0, show: boolean = false): 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) + const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account]) + + const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) + const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.account, show ? 0x01 : 0x00, LEDGER_ADPU_CODES.paramUnused, data as Buffer) + .catch(err => dec.toBytes(err.statusCode)) as Uint8Array + await transport.close() + + if (response.length === 2) { + const statusCode = bytes.toDec(response) as number + const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' + return { status, publicKey: null, address: null } + } + + try { + const publicKey = bytes.toHex(response.slice(0, 32)) + const addressLength = response[32] + const address = response.slice(33, 33 + addressLength).toString() + const statusCode = bytes.toDec(response.slice(33 + addressLength)) as number + const status = LEDGER_STATUS_CODES[statusCode] + return { status, publicKey, address } + } catch (err) { + return { status: 'ERROR_PARSING_ACCOUNT', publicKey: null, address: null } + } + } }