]> git.codecow.com Git - libnemo.git/commitdiff
Merge separate ledger classes into single class.
authorChris Duncan <chris@zoso.dev>
Sat, 5 Jul 2025 09:21:58 +0000 (02:21 -0700)
committerChris Duncan <chris@zoso.dev>
Sat, 5 Jul 2025 09:21:58 +0000 (02:21 -0700)
src/lib/block.ts
src/lib/wallets/index.ts
src/lib/wallets/ledger-wallet.ts

index 2db2fe51e3c877f0880984353e176dc898d84482..d430bed5d167f7792d1582f3cd51afa4c12cc5c7 100644 (file)
@@ -119,8 +119,8 @@ abstract class Block {
        async sign (input?: number | string, block?: { [key: string]: string }): Promise<void> {
                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 {
index a6569d0715fb3a218c325d6453086cd38bb96db8..574fc703ec1128884fb8f64e9b22ce7b0d9bee6e 100644 (file)
@@ -3,4 +3,4 @@
 \r
 export { Bip44Wallet } from './bip44-wallet'\r
 export { Blake2bWallet } from './blake2b-wallet'\r
-export { Ledger, LedgerWallet } from './ledger-wallet'\r
+export { LedgerWallet } from './ledger-wallet'\r
index 4b4d28c45e2e12127d7cc24938153b940f5e4676..ed5e324f145044da6383ee574a4467ed71981bf8 100644 (file)
@@ -5,6 +5,7 @@ import Transport from '@ledgerhq/hw-transport'
 import { default as TransportBLE } from '@ledgerhq/hw-transport-web-ble'\r
 import { default as TransportUSB } from '@ledgerhq/hw-transport-webusb'\r
 import { default as TransportHID } from '@ledgerhq/hw-transport-webhid'\r
+import { Account } from '#src/lib/account.js'\r
 import { ChangeBlock, ReceiveBlock, SendBlock } from '#src/lib/block.js'\r
 import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, LEDGER_ADPU_CODES, LEDGER_STATUS_CODES } from '#src/lib/constants.js'\r
 import { bytes, dec, hex, utf8 } from '#src/lib/convert.js'\r
@@ -44,17 +45,13 @@ interface LedgerSignResponse extends LedgerResponse {
 */\r
 export class LedgerWallet extends Wallet {\r
        static #isInternal: boolean = false\r
-       #ledger: Ledger\r
 \r
-       get ledger () { return this.#ledger }\r
-\r
-       constructor (id: Entropy, ledger: Ledger) {\r
+       constructor (id: Entropy) {\r
                if (!LedgerWallet.#isInternal) {\r
                        throw new Error(`LedgerWallet cannot be instantiated directly. Use 'await LedgerWallet.create()' instead.`)\r
                }\r
                LedgerWallet.#isInternal = false\r
                super(id)\r
-               this.#ledger = ledger\r
        }\r
 \r
        /**\r
@@ -64,10 +61,11 @@ export class LedgerWallet extends Wallet {
        * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object\r
        */\r
        static async create (): Promise<LedgerWallet> {\r
-               const l = await Ledger.init()\r
                const id = await Entropy.create(16)\r
                LedgerWallet.#isInternal = true\r
-               return new this(id, l)\r
+               const wallet = new this(id)\r
+               await wallet.init()\r
+               return wallet\r
        }\r
 \r
        /**\r
@@ -80,9 +78,10 @@ export class LedgerWallet extends Wallet {
                if (typeof id !== 'string' || id === '') {\r
                        throw new TypeError('Wallet ID is required to restore')\r
                }\r
-               const l = await Ledger.init()\r
                LedgerWallet.#isInternal = true\r
-               return new this(await Entropy.import(id), l)\r
+               const wallet = new this(await Entropy.import(id))\r
+               await wallet.init()\r
+               return wallet\r
        }\r
 \r
        /**\r
@@ -94,7 +93,7 @@ export class LedgerWallet extends Wallet {
        async ckd (indexes: number[]): Promise<KeyPair[]> {\r
                const results: KeyPair[] = []\r
                for (const index of indexes) {\r
-                       const { status, publicKey } = await this.ledger.account(index)\r
+                       const { status, publicKey } = await this.#account(index)\r
                        if (status === 'OK' && publicKey != null) {\r
                                results.push({ publicKey, index })\r
                        } else {\r
@@ -113,10 +112,7 @@ export class LedgerWallet extends Wallet {
        * @returns True if successfully locked\r
        */\r
        async lock (): Promise<boolean> {\r
-               if (this.ledger == null) {\r
-                       return false\r
-               }\r
-               const result = await this.ledger.close()\r
+               const result = await this.close()\r
                return result.status === 'OK'\r
        }\r
 \r
@@ -129,16 +125,10 @@ export class LedgerWallet extends Wallet {
        * @returns True if successfully unlocked\r
        */\r
        async unlock (): Promise<boolean> {\r
-               if (this.ledger == null) {\r
-                       return false\r
-               }\r
-               const result = await this.ledger.connect()\r
+               const result = await this.connect()\r
                return result === 'OK'\r
        }\r
-}\r
 \r
-export class Ledger {\r
-       static #isInternal: boolean = false\r
        #status: 'DISCONNECTED' | 'LOCKED' | 'BUSY' | 'CONNECTED' = 'DISCONNECTED'\r
        get status () { return this.#status }\r
        openTimeout = 3000\r
@@ -146,20 +136,9 @@ export class Ledger {
        transport: Transport | null = null\r
        DynamicTransport: typeof TransportBLE | typeof TransportUSB | typeof TransportHID = TransportHID\r
 \r
-       constructor () {\r
-               if (!Ledger.#isInternal) {\r
-                       throw new Error('Ledger cannot be instantiated directly. Use Ledger.init()')\r
-               }\r
-               Ledger.#isInternal = false\r
-               Buffer\r
-       }\r
-\r
-       static async init (): Promise<Ledger> {\r
-               Ledger.#isInternal = true\r
-               const self = new this()\r
-               await self.checkBrowserSupport()\r
-               await self.listen()\r
-               return self\r
+       async init (): Promise<void> {\r
+               await this.checkBrowserSupport()\r
+               await this.listen()\r
        }\r
 \r
        /**\r
@@ -202,10 +181,10 @@ export class Ledger {
                const version = await this.version()\r
                if (version.status === 'OK') {\r
                        if (version.name === 'Nano') {\r
-                               const account = await this.account()\r
-                               if (account.status === 'OK') {\r
+                               const { status } = await this.#account()\r
+                               if (status === 'OK') {\r
                                        this.#status = 'CONNECTED'\r
-                               } else if (account.status === 'SECURITY_STATUS_NOT_SATISFIED') {\r
+                               } else if (status === 'SECURITY_STATUS_NOT_SATISFIED') {\r
                                        this.#status = 'LOCKED'\r
                                } else {\r
                                        this.#status = 'DISCONNECTED'\r
@@ -306,40 +285,17 @@ export class Ledger {
        }\r
 \r
        /**\r
-       * Get an account at a specific BIP-44 index.\r
+       * Request an account at a specific BIP-44 index.\r
        *\r
-       * @returns Response object containing command status, public key, and address\r
+       * @returns Account\r
        */\r
-       async account (index: number = 0, show: boolean = false): Promise<LedgerAccountResponse> {\r
-               if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {\r
-                       throw new TypeError('Invalid account index')\r
-               }\r
-               const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4)\r
-               const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)\r
-               const account = dec.toBytes(index + HARDENED_OFFSET, 4)\r
-               const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account])\r
-\r
-               const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
-               const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.account, show ? 0x01 : 0x00, LEDGER_ADPU_CODES.paramUnused, data as Buffer)\r
-                       .catch(err => dec.toBytes(err.statusCode)) as Uint8Array\r
-               await transport.close()\r
-\r
-               if (response.length === 2) {\r
-                       const statusCode = bytes.toDec(response) as number\r
-                       const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'\r
-                       return { status, publicKey: null, address: null }\r
-               }\r
-\r
-               try {\r
-                       const publicKey = bytes.toHex(response.slice(0, 32))\r
-                       const addressLength = response[32]\r
-                       const address = response.slice(33, 33 + addressLength).toString()\r
-                       const statusCode = bytes.toDec(response.slice(33 + addressLength)) as number\r
-                       const status = LEDGER_STATUS_CODES[statusCode]\r
-                       return { status, publicKey, address }\r
-               } catch (err) {\r
-                       return { status: 'ERROR_PARSING_ACCOUNT', publicKey: null, address: null }\r
+       async account (index: number = 0, show: boolean = false): Promise<Account> {\r
+               const { status, publicKey } = await this.#account(index, show)\r
+               if (publicKey == null) {\r
+                       throw new Error('Failed to get account from device', { cause: status })\r
                }\r
+               const account = await Account.fromPublicKey(publicKey)\r
+               return account\r
        }\r
 \r
        /**\r
@@ -480,4 +436,41 @@ export class Ledger {
                }\r
                return this.cacheBlock(index, input)\r
        }\r
+\r
+       /**\r
+       * Request an account at a specific BIP-44 index.\r
+       *\r
+       * @returns Response object containing command status, public key, and address\r
+       */\r
+       async #account (index: number = 0, show: boolean = false): Promise<LedgerAccountResponse> {\r
+               if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {\r
+                       throw new TypeError('Invalid account index')\r
+               }\r
+               const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4)\r
+               const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)\r
+               const account = dec.toBytes(index + HARDENED_OFFSET, 4)\r
+               const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account])\r
+\r
+               const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
+               const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.account, show ? 0x01 : 0x00, LEDGER_ADPU_CODES.paramUnused, data as Buffer)\r
+                       .catch(err => dec.toBytes(err.statusCode)) as Uint8Array\r
+               await transport.close()\r
+\r
+               if (response.length === 2) {\r
+                       const statusCode = bytes.toDec(response) as number\r
+                       const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'\r
+                       return { status, publicKey: null, address: null }\r
+               }\r
+\r
+               try {\r
+                       const publicKey = bytes.toHex(response.slice(0, 32))\r
+                       const addressLength = response[32]\r
+                       const address = response.slice(33, 33 + addressLength).toString()\r
+                       const statusCode = bytes.toDec(response.slice(33 + addressLength)) as number\r
+                       const status = LEDGER_STATUS_CODES[statusCode]\r
+                       return { status, publicKey, address }\r
+               } catch (err) {\r
+                       return { status: 'ERROR_PARSING_ACCOUNT', publicKey: null, address: null }\r
+               }\r
+       }\r
 }\r