]> git.codecow.com Git - libnemo.git/commitdiff
Extract wallets into their own files.
authorChris Duncan <chris@zoso.dev>
Thu, 3 Jul 2025 04:08:28 +0000 (21:08 -0700)
committerChris Duncan <chris@zoso.dev>
Thu, 3 Jul 2025 04:08:28 +0000 (21:08 -0700)
src/lib/wallets/bip44-wallet.ts [new file with mode: 0644]
src/lib/wallets/blake2b-wallet.ts [new file with mode: 0644]
src/lib/wallets/index.ts
src/lib/wallets/ledger-wallet.ts [new file with mode: 0644]

diff --git a/src/lib/wallets/bip44-wallet.ts b/src/lib/wallets/bip44-wallet.ts
new file mode 100644 (file)
index 0000000..a34b617
--- /dev/null
@@ -0,0 +1,209 @@
+// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>\r
+// SPDX-License-Identifier: GPL-3.0-or-later\r
+\r
+import { KeyPair, Wallet } from '.'\r
+import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
+import { SEED_LENGTH_BIP44 } from '#src/lib/constants.js'\r
+import { Entropy } from '#src/lib/entropy.js'\r
+import { Pool } from '#src/lib/pool.js'\r
+import { Bip44CkdWorker } from '#workers'\r
+\r
+/**\r
+* Hierarchical deterministic (HD) wallet created by using a source of entropy to\r
+* derive a mnemonic phrase. The mnemonic phrase, in combination with an optional\r
+* salt, is used to generate a seed. A value can be provided as a parameter for\r
+* entropy, mnemonic + salt, or seed; if no argument is passed, a new entropy\r
+* value will be generated using a cryptographically strong pseudorandom number\r
+* generator.\r
+*\r
+* Importantly, the salt is not stored in the instantiated Wallet object. If a\r
+* salt is used, then losing it means losing the ability to regenerate the seed\r
+* from the mnemonic.\r
+*\r
+* Accounts are derived from the seed. Private keys are derived using a BIP-44\r
+* derivation path. The public key is derived from the private key using the\r
+* Ed25519 key algorithm. Account addresses are derived as described in the nano\r
+* documentation (https://docs.nano.org)\r
+*\r
+* A password must be provided when creating or importing the wallet and is used\r
+* to lock and unlock the wallet. The wallet will be initialized as locked. When\r
+* the wallet is unlocked, a new password can be specified using the lock()\r
+* method.\r
+*/\r
+export class Bip44Wallet extends Wallet {\r
+       static #isInternal: boolean = false\r
+       #poolBip44Ckd: Pool\r
+\r
+       constructor (id: Entropy, seed: string, mnemonic?: Bip39Mnemonic) {\r
+               if (!Bip44Wallet.#isInternal) {\r
+                       throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`)\r
+               }\r
+               Bip44Wallet.#isInternal = false\r
+               super(id, seed, mnemonic)\r
+               this.#poolBip44Ckd = new Pool(Bip44CkdWorker)\r
+       }\r
+\r
+       /**\r
+       * Removes encrypted secrets in storage and releases variable references to\r
+       * allow garbage collection.\r
+       */\r
+       destroy () {\r
+               super.destroy()\r
+               this.#poolBip44Ckd.terminate()\r
+       }\r
+\r
+       /**\r
+       * Creates a new HD wallet by using an entropy value generated using a\r
+       * cryptographically strong pseudorandom number generator.\r
+       *\r
+       * @param {string} password - Encrypts the wallet to lock and unlock it\r
+       * @param {string} [salt=''] - Used when generating the final seed\r
+       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
+       */\r
+       static async create (password: string, salt?: string): Promise<Bip44Wallet>\r
+       /**\r
+       * Creates a new HD wallet by using an entropy value generated using a\r
+       * cryptographically strong pseudorandom number generator.\r
+       *\r
+       * @param {CryptoKey} key - Encrypts the wallet to lock and unlock it\r
+       * @param {string} [salt=''] - Used when generating the final seed\r
+       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
+       */\r
+       static async create (key: CryptoKey, salt?: string): Promise<Bip44Wallet>\r
+       static async create (passkey: string | CryptoKey, salt: string = ''): Promise<Bip44Wallet> {\r
+               try {\r
+                       const e = await Entropy.create()\r
+                       return await Bip44Wallet.fromEntropy(passkey as string, e.hex, salt)\r
+               } catch (err) {\r
+                       throw new Error(`Error creating new Bip44Wallet: ${err}`)\r
+               }\r
+       }\r
+\r
+       /**\r
+       * Creates a new HD wallet by using a pregenerated entropy value. The user\r
+       * must ensure that it is cryptographically strongly random.\r
+       *\r
+       * @param {string} password - Used to lock and unlock the wallet\r
+       * @param {string} entropy - Used when generating the initial mnemonic phrase\r
+       * @param {string} [salt=''] - Used when generating the final seed\r
+       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
+       */\r
+       static async fromEntropy (password: string, entropy: string, salt?: string): Promise<Bip44Wallet>\r
+       /**\r
+       * Creates a new HD wallet by using a pregenerated entropy value. The user\r
+       * must ensure that it is cryptographically strongly random.\r
+       *\r
+       * @param {CryptoKey} key - Used to lock and unlock the wallet\r
+       * @param {string} entropy - Used when generating the initial mnemonic phrase\r
+       * @param {string} [salt=''] - Used when generating the final seed\r
+       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
+       */\r
+       static async fromEntropy (key: CryptoKey, entropy: string, salt?: string): Promise<Bip44Wallet>\r
+       static async fromEntropy (passkey: string | CryptoKey, entropy: string, salt: string = ''): Promise<Bip44Wallet> {\r
+               try {\r
+                       const id = await Entropy.create(16)\r
+                       const e = await Entropy.import(entropy)\r
+                       const m = await Bip39Mnemonic.fromEntropy(e.hex)\r
+                       const s = await m.toBip39Seed(salt)\r
+                       Bip44Wallet.#isInternal = true\r
+                       const wallet = new this(id, s, m)\r
+                       await wallet.lock(passkey as string)\r
+                       return wallet\r
+               } catch (err) {\r
+                       throw new Error(`Error importing Bip44Wallet from entropy: ${err}`)\r
+               }\r
+       }\r
+\r
+       /**\r
+       * Creates a new HD wallet by using a pregenerated mnemonic phrase.\r
+       *\r
+       * @param {string} password - Used to lock and unlock the wallet\r
+       * @param {string} mnemonic - Used when generating the final seed\r
+       * @param {string} [salt=''] - Used when generating the final seed\r
+       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
+       */\r
+       static async fromMnemonic (password: string, mnemonic: string, salt?: string): Promise<Bip44Wallet>\r
+       /**\r
+       * Creates a new HD wallet by using a pregenerated mnemonic phrase.\r
+       *\r
+       * @param {CryptoKey} key - Used to lock and unlock the wallet\r
+       * @param {string} mnemonic - Used when generating the final seed\r
+       * @param {string} [salt=''] - Used when generating the final seed\r
+       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
+       */\r
+       static async fromMnemonic (key: CryptoKey, mnemonic: string, salt?: string): Promise<Bip44Wallet>\r
+       static async fromMnemonic (passkey: string | CryptoKey, mnemonic: string, salt: string = ''): Promise<Bip44Wallet> {\r
+               try {\r
+                       const id = await Entropy.create(16)\r
+                       const m = await Bip39Mnemonic.fromPhrase(mnemonic)\r
+                       const s = await m.toBip39Seed(salt)\r
+                       Bip44Wallet.#isInternal = true\r
+                       const wallet = new this(id, s, m)\r
+                       await wallet.lock(passkey as string)\r
+                       return wallet\r
+               } catch (err) {\r
+                       throw new Error(`Error importing Bip44Wallet from mnemonic: ${err}`)\r
+               }\r
+       }\r
+\r
+       /**\r
+       * Creates a new HD wallet by using a pregenerated seed value. This seed cannot\r
+       * be used to regenerate any higher level randomness which includes entropy,\r
+       * mnemonic phrase, and salt.\r
+       *\r
+       * @param {string} password - Used to lock and unlock the wallet\r
+       * @param {string} seed - Hexadecimal 128-character string used to derive private-public key pairs\r
+       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
+       */\r
+       static async fromSeed (password: string, seed: string): Promise<Bip44Wallet>\r
+       /**\r
+       * Creates a new HD wallet by using a pregenerated seed value. This seed cannot\r
+       * be used to regenerate any higher level randomness which includes entropy,\r
+       * mnemonic phrase, and salt.\r
+       *\r
+       * @param {CryptoKey} key - Used to lock and unlock the wallet\r
+       * @param {string} seed - Hexadecimal 128-character string used to derive private-public key pairs\r
+       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
+       */\r
+       static async fromSeed (key: CryptoKey, seed: string): Promise<Bip44Wallet>\r
+       static async fromSeed (passkey: string | CryptoKey, seed: string): Promise<Bip44Wallet> {\r
+               if (seed.length !== SEED_LENGTH_BIP44) {\r
+                       throw new Error(`Expected a ${SEED_LENGTH_BIP44}-character seed, but received ${seed.length}-character string.`)\r
+               }\r
+               if (!/^[0-9a-fA-F]+$/i.test(seed)) {\r
+                       throw new Error('Seed contains invalid hexadecimal characters.')\r
+               }\r
+               const id = await Entropy.create(16)\r
+               Bip44Wallet.#isInternal = true\r
+               const wallet = new this(id, seed)\r
+               await wallet.lock(passkey as string)\r
+               return wallet\r
+       }\r
+\r
+       /**\r
+       * Retrieves an existing HD wallet from session storage using its ID.\r
+       *\r
+       * @param {string} id - Generated when the wallet was initially created\r
+       * @returns {Bip44Wallet} Restored locked Bip44Wallet\r
+       */\r
+       static async restore (id: string): Promise<Bip44Wallet> {\r
+               if (typeof id !== 'string' || id === '') {\r
+                       throw new TypeError('Wallet ID is required to restore')\r
+               }\r
+               Bip44Wallet.#isInternal = true\r
+               return new this(await Entropy.import(id), '')\r
+       }\r
+\r
+       /**\r
+       * Derives BIP-44 Nano account private keys.\r
+       *\r
+       * @param {number[]} indexes - Indexes of the accounts\r
+       * @returns {Promise<Account>}\r
+       */\r
+       async ckd (indexes: number[]): Promise<KeyPair[]> {\r
+               const data: any = []\r
+               indexes.forEach(i => data.push({ seed: this.seed, index: i }))\r
+               const privateKeys: KeyPair[] = await this.#poolBip44Ckd.assign(data)\r
+               return privateKeys\r
+       }\r
+}\r
diff --git a/src/lib/wallets/blake2b-wallet.ts b/src/lib/wallets/blake2b-wallet.ts
new file mode 100644 (file)
index 0000000..8bbb231
--- /dev/null
@@ -0,0 +1,157 @@
+// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>\r
+// SPDX-License-Identifier: GPL-3.0-or-later\r
+\r
+import { KeyPair, Wallet } from '.'\r
+import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
+import { Blake2b } from '#src/lib/blake2b.js'\r
+import { SEED_LENGTH_BLAKE2B } from '#src/lib/constants.js'\r
+import { hex } from '#src/lib/convert.js'\r
+import { Entropy } from '#src/lib/entropy.js'\r
+\r
+/**\r
+* BLAKE2b wallet created by deriving a mnemonic phrase from a seed or vice\r
+* versa. If no value is provided for either, a new BIP-39 seed and mnemonic will\r
+* be generated using a cryptographically strong pseudorandom number generator.\r
+*\r
+* Account private keys are derived on an ad hoc basis using the Blake2b hashing\r
+* function. Account public key are derived from the private key using the\r
+* Ed25519 key algorithm. Account addresses are derived from the public key as\r
+* described in the Nano documentation.\r
+* https://docs.nano.org/integration-guides/the-basics/\r
+*\r
+* A password must be provided when creating or importing the wallet and is used\r
+* to lock and unlock the wallet. The wallet will be initialized as locked. When\r
+* the wallet is unlocked, a new password can be specified using the lock()\r
+* method.\r
+*/\r
+export class Blake2bWallet extends Wallet {\r
+       static #isInternal: boolean = false\r
+\r
+       constructor (id: Entropy, seed: string, mnemonic?: Bip39Mnemonic) {\r
+               if (!Blake2bWallet.#isInternal) {\r
+                       throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`)\r
+               }\r
+               Blake2bWallet.#isInternal = false\r
+               super(id, seed, mnemonic)\r
+       }\r
+\r
+       /**\r
+       * Creates a new BLAKE2b wallet by using a seed generated using a\r
+       * cryptographically strong pseudorandom number generator.\r
+       *\r
+       * @param {string} password - Encrypts the wallet to lock and unlock it\r
+       * @returns {Blake2bWallet} A newly instantiated Blake2bWallet\r
+       */\r
+       static async create (password: string): Promise<Blake2bWallet>\r
+       /**\r
+       * Creates a new BLAKE2b wallet by using a seed generated using a\r
+       * cryptographically strong pseudorandom number generator.\r
+       *\r
+       * @param {CryptoKey} key - Encrypts the wallet to lock and unlock it\r
+       * @returns {Blake2bWallet} A newly instantiated Blake2bWallet\r
+       */\r
+       static async create (key: CryptoKey): Promise<Blake2bWallet>\r
+       static async create (passkey: string | CryptoKey): Promise<Blake2bWallet> {\r
+               try {\r
+                       const seed = await Entropy.create()\r
+                       return await Blake2bWallet.fromSeed(passkey as string, seed.hex)\r
+               } catch (err) {\r
+                       throw new Error(`Error creating new Blake2bWallet: ${err}`)\r
+               }\r
+       }\r
+\r
+       /**\r
+       * Creates a new BLAKE2b wallet by using a pregenerated seed. The user must\r
+       * ensure that it is cryptographically strongly random.\r
+       *\r
+       * @param {string} password - Used to lock and unlock the wallet\r
+       * @param {string} seed - Hexadecimal 64-character string used to derive private-public key pairs\r
+       * @returns {Blake2bWallet} A newly instantiated Blake2bWallet\r
+       */\r
+       static async fromSeed (password: string, seed: string): Promise<Blake2bWallet>\r
+       /**\r
+       * Creates a new BLAKE2b wallet by using a pregenerated seed. The user must\r
+       * ensure that it is cryptographically strongly random.\r
+       *\r
+       * @param {CryptoKey} key - Used to lock and unlock the wallet\r
+       * @param {string} seed - Hexadecimal 64-character string used to derive private-public key pairs\r
+       * @returns {Blake2bWallet} A newly instantiated Blake2bWallet\r
+       */\r
+       static async fromSeed (key: CryptoKey, seed: string): Promise<Blake2bWallet>\r
+       static async fromSeed (passkey: string | CryptoKey, seed: string): Promise<Blake2bWallet> {\r
+               if (seed.length !== SEED_LENGTH_BLAKE2B) {\r
+                       throw new Error(`Expected a ${SEED_LENGTH_BLAKE2B}-character seed, but received ${seed.length}-character string.`)\r
+               }\r
+               if (!/^[0-9a-fA-F]+$/i.test(seed)) {\r
+                       throw new Error('Seed contains invalid hexadecimal characters.')\r
+               }\r
+               const id = await Entropy.create(16)\r
+               const s = seed\r
+               const m = await Bip39Mnemonic.fromEntropy(seed)\r
+               Blake2bWallet.#isInternal = true\r
+               const wallet = new this(id, s, m)\r
+               await wallet.lock(passkey as string)\r
+               return wallet\r
+       }\r
+\r
+       /**\r
+       * Creates a new BLAKE2b wallet by using a pregenerated mnemonic phrase.\r
+       *\r
+       * @param {string} password - Used to lock and unlock the wallet\r
+       * @param {string} mnemonic - Used when generating the final seed\r
+       * @returns {Blake2bWallet} A newly instantiated Blake2bWallet\r
+       */\r
+       static async fromMnemonic (password: string, mnemonic: string): Promise<Blake2bWallet>\r
+       /**\r
+       * Creates a new BLAKE2b wallet by using a pregenerated mnemonic phrase.\r
+       *\r
+       * @param {CryptoKey} key - Used to lock and unlock the wallet\r
+       * @param {string} mnemonic - Used when generating the final seed\r
+       * @returns {Blake2bWallet} A newly instantiated Blake2bWallet\r
+       */\r
+       static async fromMnemonic (key: CryptoKey, mnemonic: string): Promise<Blake2bWallet>\r
+       static async fromMnemonic (passkey: string | CryptoKey, mnemonic: string): Promise<Blake2bWallet> {\r
+               try {\r
+                       const id = await Entropy.create(16)\r
+                       const m = await Bip39Mnemonic.fromPhrase(mnemonic)\r
+                       const s = await m.toBlake2bSeed()\r
+                       Blake2bWallet.#isInternal = true\r
+                       const wallet = new this(id, s, m)\r
+                       await wallet.lock(passkey as string)\r
+                       return wallet\r
+               } catch (err) {\r
+                       throw new Error(`Error importing Blake2bWallet from mnemonic: ${err}`)\r
+               }\r
+       }\r
+\r
+       /**\r
+       * Retrieves an existing BLAKE2b wallet from session storage using its ID.\r
+       *\r
+       * @param {string} id - Generated when the wallet was initially created\r
+       * @returns {Blake2bWallet} Restored locked Blake2bWallet\r
+       */\r
+       static async restore (id: string): Promise<Blake2bWallet> {\r
+               if (typeof id !== 'string' || id === '') {\r
+                       throw new TypeError('Wallet ID is required to restore')\r
+               }\r
+               Blake2bWallet.#isInternal = true\r
+               return new this(await Entropy.import(id), '')\r
+       }\r
+\r
+       /**\r
+       * Derives BLAKE2b account private keys.\r
+       *\r
+       * @param {number[]} indexes - Indexes of the accounts\r
+       * @returns {Promise<Account>}\r
+       */\r
+       async ckd (indexes: number[]): Promise<KeyPair[]> {\r
+               const results = indexes.map(index => {\r
+                       const indexHex = index.toString(16).padStart(8, '0').toUpperCase()\r
+                       const inputHex = `${this.seed}${indexHex}`.padStart(72, '0')\r
+                       const inputBytes = hex.toBytes(inputHex)\r
+                       const privateKey: string = new Blake2b(32).update(inputBytes).digest('hex')\r
+                       return { privateKey, index }\r
+               })\r
+               return results\r
+       }\r
+}\r
index 79dfc572fab69c56ae5c266ee4ebf7ac38e0454a..5d167d6fc130b7f1f13cd5f68f8c3975db4620cf 100644 (file)
@@ -3,17 +3,17 @@
 \r
 import { Account, AccountList } from '#src/lib/account.js'\r
 import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
-import { Blake2b } from '#src/lib/blake2b.js'\r
-import { ADDRESS_GAP, SEED_LENGTH_BIP44, SEED_LENGTH_BLAKE2B } from '#src/lib/constants.js'\r
-import { hex } from '#src/lib/convert.js'\r
+import { ADDRESS_GAP } from '#src/lib/constants.js'\r
 import { Entropy } from '#src/lib/entropy.js'\r
-import { Ledger } from '#src/lib/ledger.js'\r
 import { Pool } from '#src/lib/pool.js'\r
 import { Rpc } from '#src/lib/rpc.js'\r
 import { Safe } from '#src/lib/safe.js'\r
-import { Bip44CkdWorker, NanoNaClWorker } from '#workers'\r
+import { NanoNaClWorker } from '#workers'\r
 \r
-type KeyPair = {\r
+export { Bip44Wallet } from './bip44-wallet'\r
+export { Blake2bWallet } from './blake2b-wallet'\r
+export { LedgerWallet } from './ledger-wallet'\r
+export type KeyPair = {\r
        publicKey?: string,\r
        privateKey?: string,\r
        index?: number\r
@@ -26,7 +26,7 @@ type KeyPair = {
 * types of wallets are supported, each as a derived class: Bip44Wallet,\r
 * Blake2bWallet, LedgerWallet.\r
 */\r
-abstract class Wallet {\r
+export abstract class Wallet {\r
        #accounts: AccountList\r
        #id: Entropy\r
        #locked: boolean = true\r
@@ -302,459 +302,3 @@ abstract class Wallet {
                return true\r
        }\r
 }\r
-\r
-/**\r
-* Hierarchical deterministic (HD) wallet created by using a source of entropy to\r
-* derive a mnemonic phrase. The mnemonic phrase, in combination with an optional\r
-* salt, is used to generate a seed. A value can be provided as a parameter for\r
-* entropy, mnemonic + salt, or seed; if no argument is passed, a new entropy\r
-* value will be generated using a cryptographically strong pseudorandom number\r
-* generator.\r
-*\r
-* Importantly, the salt is not stored in the instantiated Wallet object. If a\r
-* salt is used, then losing it means losing the ability to regenerate the seed\r
-* from the mnemonic.\r
-*\r
-* Accounts are derived from the seed. Private keys are derived using a BIP-44\r
-* derivation path. The public key is derived from the private key using the\r
-* Ed25519 key algorithm. Account addresses are derived as described in the nano\r
-* documentation (https://docs.nano.org)\r
-*\r
-* A password must be provided when creating or importing the wallet and is used\r
-* to lock and unlock the wallet. The wallet will be initialized as locked. When\r
-* the wallet is unlocked, a new password can be specified using the lock()\r
-* method.\r
-*/\r
-export class Bip44Wallet extends Wallet {\r
-       static #isInternal: boolean = false\r
-       #poolBip44Ckd: Pool\r
-\r
-       constructor (id: Entropy, seed: string, mnemonic?: Bip39Mnemonic) {\r
-               if (!Bip44Wallet.#isInternal) {\r
-                       throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`)\r
-               }\r
-               Bip44Wallet.#isInternal = false\r
-               super(id, seed, mnemonic)\r
-               this.#poolBip44Ckd = new Pool(Bip44CkdWorker)\r
-       }\r
-\r
-       /**\r
-       * Removes encrypted secrets in storage and releases variable references to\r
-       * allow garbage collection.\r
-       */\r
-       destroy () {\r
-               super.destroy()\r
-               this.#poolBip44Ckd.terminate()\r
-       }\r
-\r
-       /**\r
-       * Creates a new HD wallet by using an entropy value generated using a\r
-       * cryptographically strong pseudorandom number generator.\r
-       *\r
-       * @param {string} password - Encrypts the wallet to lock and unlock it\r
-       * @param {string} [salt=''] - Used when generating the final seed\r
-       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
-       */\r
-       static async create (password: string, salt?: string): Promise<Bip44Wallet>\r
-       /**\r
-       * Creates a new HD wallet by using an entropy value generated using a\r
-       * cryptographically strong pseudorandom number generator.\r
-       *\r
-       * @param {CryptoKey} key - Encrypts the wallet to lock and unlock it\r
-       * @param {string} [salt=''] - Used when generating the final seed\r
-       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
-       */\r
-       static async create (key: CryptoKey, salt?: string): Promise<Bip44Wallet>\r
-       static async create (passkey: string | CryptoKey, salt: string = ''): Promise<Bip44Wallet> {\r
-               try {\r
-                       const e = await Entropy.create()\r
-                       return await Bip44Wallet.fromEntropy(passkey as string, e.hex, salt)\r
-               } catch (err) {\r
-                       throw new Error(`Error creating new Bip44Wallet: ${err}`)\r
-               }\r
-       }\r
-\r
-       /**\r
-       * Creates a new HD wallet by using a pregenerated entropy value. The user\r
-       * must ensure that it is cryptographically strongly random.\r
-       *\r
-       * @param {string} password - Used to lock and unlock the wallet\r
-       * @param {string} entropy - Used when generating the initial mnemonic phrase\r
-       * @param {string} [salt=''] - Used when generating the final seed\r
-       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
-       */\r
-       static async fromEntropy (password: string, entropy: string, salt?: string): Promise<Bip44Wallet>\r
-       /**\r
-       * Creates a new HD wallet by using a pregenerated entropy value. The user\r
-       * must ensure that it is cryptographically strongly random.\r
-       *\r
-       * @param {CryptoKey} key - Used to lock and unlock the wallet\r
-       * @param {string} entropy - Used when generating the initial mnemonic phrase\r
-       * @param {string} [salt=''] - Used when generating the final seed\r
-       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
-       */\r
-       static async fromEntropy (key: CryptoKey, entropy: string, salt?: string): Promise<Bip44Wallet>\r
-       static async fromEntropy (passkey: string | CryptoKey, entropy: string, salt: string = ''): Promise<Bip44Wallet> {\r
-               try {\r
-                       const id = await Entropy.create(16)\r
-                       const e = await Entropy.import(entropy)\r
-                       const m = await Bip39Mnemonic.fromEntropy(e.hex)\r
-                       const s = await m.toBip39Seed(salt)\r
-                       Bip44Wallet.#isInternal = true\r
-                       const wallet = new this(id, s, m)\r
-                       await wallet.lock(passkey as string)\r
-                       return wallet\r
-               } catch (err) {\r
-                       throw new Error(`Error importing Bip44Wallet from entropy: ${err}`)\r
-               }\r
-       }\r
-\r
-       /**\r
-       * Creates a new HD wallet by using a pregenerated mnemonic phrase.\r
-       *\r
-       * @param {string} password - Used to lock and unlock the wallet\r
-       * @param {string} mnemonic - Used when generating the final seed\r
-       * @param {string} [salt=''] - Used when generating the final seed\r
-       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
-       */\r
-       static async fromMnemonic (password: string, mnemonic: string, salt?: string): Promise<Bip44Wallet>\r
-       /**\r
-       * Creates a new HD wallet by using a pregenerated mnemonic phrase.\r
-       *\r
-       * @param {CryptoKey} key - Used to lock and unlock the wallet\r
-       * @param {string} mnemonic - Used when generating the final seed\r
-       * @param {string} [salt=''] - Used when generating the final seed\r
-       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
-       */\r
-       static async fromMnemonic (key: CryptoKey, mnemonic: string, salt?: string): Promise<Bip44Wallet>\r
-       static async fromMnemonic (passkey: string | CryptoKey, mnemonic: string, salt: string = ''): Promise<Bip44Wallet> {\r
-               try {\r
-                       const id = await Entropy.create(16)\r
-                       const m = await Bip39Mnemonic.fromPhrase(mnemonic)\r
-                       const s = await m.toBip39Seed(salt)\r
-                       Bip44Wallet.#isInternal = true\r
-                       const wallet = new this(id, s, m)\r
-                       await wallet.lock(passkey as string)\r
-                       return wallet\r
-               } catch (err) {\r
-                       throw new Error(`Error importing Bip44Wallet from mnemonic: ${err}`)\r
-               }\r
-       }\r
-\r
-       /**\r
-       * Creates a new HD wallet by using a pregenerated seed value. This seed cannot\r
-       * be used to regenerate any higher level randomness which includes entropy,\r
-       * mnemonic phrase, and salt.\r
-       *\r
-       * @param {string} password - Used to lock and unlock the wallet\r
-       * @param {string} seed - Hexadecimal 128-character string used to derive private-public key pairs\r
-       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
-       */\r
-       static async fromSeed (password: string, seed: string): Promise<Bip44Wallet>\r
-       /**\r
-       * Creates a new HD wallet by using a pregenerated seed value. This seed cannot\r
-       * be used to regenerate any higher level randomness which includes entropy,\r
-       * mnemonic phrase, and salt.\r
-       *\r
-       * @param {CryptoKey} key - Used to lock and unlock the wallet\r
-       * @param {string} seed - Hexadecimal 128-character string used to derive private-public key pairs\r
-       * @returns {Bip44Wallet} A newly instantiated Bip44Wallet\r
-       */\r
-       static async fromSeed (key: CryptoKey, seed: string): Promise<Bip44Wallet>\r
-       static async fromSeed (passkey: string | CryptoKey, seed: string): Promise<Bip44Wallet> {\r
-               if (seed.length !== SEED_LENGTH_BIP44) {\r
-                       throw new Error(`Expected a ${SEED_LENGTH_BIP44}-character seed, but received ${seed.length}-character string.`)\r
-               }\r
-               if (!/^[0-9a-fA-F]+$/i.test(seed)) {\r
-                       throw new Error('Seed contains invalid hexadecimal characters.')\r
-               }\r
-               const id = await Entropy.create(16)\r
-               Bip44Wallet.#isInternal = true\r
-               const wallet = new this(id, seed)\r
-               await wallet.lock(passkey as string)\r
-               return wallet\r
-       }\r
-\r
-       /**\r
-       * Retrieves an existing HD wallet from session storage using its ID.\r
-       *\r
-       * @param {string} id - Generated when the wallet was initially created\r
-       * @returns {Bip44Wallet} Restored locked Bip44Wallet\r
-       */\r
-       static async restore (id: string): Promise<Bip44Wallet> {\r
-               if (typeof id !== 'string' || id === '') {\r
-                       throw new TypeError('Wallet ID is required to restore')\r
-               }\r
-               Bip44Wallet.#isInternal = true\r
-               return new this(await Entropy.import(id), '')\r
-       }\r
-\r
-       /**\r
-       * Derives BIP-44 Nano account private keys.\r
-       *\r
-       * @param {number[]} indexes - Indexes of the accounts\r
-       * @returns {Promise<Account>}\r
-       */\r
-       async ckd (indexes: number[]): Promise<KeyPair[]> {\r
-               const data: any = []\r
-               indexes.forEach(i => data.push({ seed: this.seed, index: i }))\r
-               const privateKeys: KeyPair[] = await this.#poolBip44Ckd.assign(data)\r
-               return privateKeys\r
-       }\r
-}\r
-\r
-/**\r
-* BLAKE2b wallet created by deriving a mnemonic phrase from a seed or vice\r
-* versa. If no value is provided for either, a new BIP-39 seed and mnemonic will\r
-* be generated using a cryptographically strong pseudorandom number generator.\r
-*\r
-* Account private keys are derived on an ad hoc basis using the Blake2b hashing\r
-* function. Account public key are derived from the private key using the\r
-* Ed25519 key algorithm. Account addresses are derived from the public key as\r
-* described in the Nano documentation.\r
-* https://docs.nano.org/integration-guides/the-basics/\r
-*\r
-* A password must be provided when creating or importing the wallet and is used\r
-* to lock and unlock the wallet. The wallet will be initialized as locked. When\r
-* the wallet is unlocked, a new password can be specified using the lock()\r
-* method.\r
-*/\r
-export class Blake2bWallet extends Wallet {\r
-       static #isInternal: boolean = false\r
-\r
-       constructor (id: Entropy, seed: string, mnemonic?: Bip39Mnemonic) {\r
-               if (!Blake2bWallet.#isInternal) {\r
-                       throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`)\r
-               }\r
-               Blake2bWallet.#isInternal = false\r
-               super(id, seed, mnemonic)\r
-       }\r
-\r
-       /**\r
-       * Creates a new BLAKE2b wallet by using a seed generated using a\r
-       * cryptographically strong pseudorandom number generator.\r
-       *\r
-       * @param {string} password - Encrypts the wallet to lock and unlock it\r
-       * @returns {Blake2bWallet} A newly instantiated Blake2bWallet\r
-       */\r
-       static async create (password: string): Promise<Blake2bWallet>\r
-       /**\r
-       * Creates a new BLAKE2b wallet by using a seed generated using a\r
-       * cryptographically strong pseudorandom number generator.\r
-       *\r
-       * @param {CryptoKey} key - Encrypts the wallet to lock and unlock it\r
-       * @returns {Blake2bWallet} A newly instantiated Blake2bWallet\r
-       */\r
-       static async create (key: CryptoKey): Promise<Blake2bWallet>\r
-       static async create (passkey: string | CryptoKey): Promise<Blake2bWallet> {\r
-               try {\r
-                       const seed = await Entropy.create()\r
-                       return await Blake2bWallet.fromSeed(passkey as string, seed.hex)\r
-               } catch (err) {\r
-                       throw new Error(`Error creating new Blake2bWallet: ${err}`)\r
-               }\r
-       }\r
-\r
-       /**\r
-       * Creates a new BLAKE2b wallet by using a pregenerated seed. The user must\r
-       * ensure that it is cryptographically strongly random.\r
-       *\r
-       * @param {string} password - Used to lock and unlock the wallet\r
-       * @param {string} seed - Hexadecimal 64-character string used to derive private-public key pairs\r
-       * @returns {Blake2bWallet} A newly instantiated Blake2bWallet\r
-       */\r
-       static async fromSeed (password: string, seed: string): Promise<Blake2bWallet>\r
-       /**\r
-       * Creates a new BLAKE2b wallet by using a pregenerated seed. The user must\r
-       * ensure that it is cryptographically strongly random.\r
-       *\r
-       * @param {CryptoKey} key - Used to lock and unlock the wallet\r
-       * @param {string} seed - Hexadecimal 64-character string used to derive private-public key pairs\r
-       * @returns {Blake2bWallet} A newly instantiated Blake2bWallet\r
-       */\r
-       static async fromSeed (key: CryptoKey, seed: string): Promise<Blake2bWallet>\r
-       static async fromSeed (passkey: string | CryptoKey, seed: string): Promise<Blake2bWallet> {\r
-               if (seed.length !== SEED_LENGTH_BLAKE2B) {\r
-                       throw new Error(`Expected a ${SEED_LENGTH_BLAKE2B}-character seed, but received ${seed.length}-character string.`)\r
-               }\r
-               if (!/^[0-9a-fA-F]+$/i.test(seed)) {\r
-                       throw new Error('Seed contains invalid hexadecimal characters.')\r
-               }\r
-               const id = await Entropy.create(16)\r
-               const s = seed\r
-               const m = await Bip39Mnemonic.fromEntropy(seed)\r
-               Blake2bWallet.#isInternal = true\r
-               const wallet = new this(id, s, m)\r
-               await wallet.lock(passkey as string)\r
-               return wallet\r
-       }\r
-\r
-       /**\r
-       * Creates a new BLAKE2b wallet by using a pregenerated mnemonic phrase.\r
-       *\r
-       * @param {string} password - Used to lock and unlock the wallet\r
-       * @param {string} mnemonic - Used when generating the final seed\r
-       * @returns {Blake2bWallet} A newly instantiated Blake2bWallet\r
-       */\r
-       static async fromMnemonic (password: string, mnemonic: string): Promise<Blake2bWallet>\r
-       /**\r
-       * Creates a new BLAKE2b wallet by using a pregenerated mnemonic phrase.\r
-       *\r
-       * @param {CryptoKey} key - Used to lock and unlock the wallet\r
-       * @param {string} mnemonic - Used when generating the final seed\r
-       * @returns {Blake2bWallet} A newly instantiated Blake2bWallet\r
-       */\r
-       static async fromMnemonic (key: CryptoKey, mnemonic: string): Promise<Blake2bWallet>\r
-       static async fromMnemonic (passkey: string | CryptoKey, mnemonic: string): Promise<Blake2bWallet> {\r
-               try {\r
-                       const id = await Entropy.create(16)\r
-                       const m = await Bip39Mnemonic.fromPhrase(mnemonic)\r
-                       const s = await m.toBlake2bSeed()\r
-                       Blake2bWallet.#isInternal = true\r
-                       const wallet = new this(id, s, m)\r
-                       await wallet.lock(passkey as string)\r
-                       return wallet\r
-               } catch (err) {\r
-                       throw new Error(`Error importing Blake2bWallet from mnemonic: ${err}`)\r
-               }\r
-       }\r
-\r
-       /**\r
-       * Retrieves an existing BLAKE2b wallet from session storage using its ID.\r
-       *\r
-       * @param {string} id - Generated when the wallet was initially created\r
-       * @returns {Blake2bWallet} Restored locked Blake2bWallet\r
-       */\r
-       static async restore (id: string): Promise<Blake2bWallet> {\r
-               if (typeof id !== 'string' || id === '') {\r
-                       throw new TypeError('Wallet ID is required to restore')\r
-               }\r
-               Blake2bWallet.#isInternal = true\r
-               return new this(await Entropy.import(id), '')\r
-       }\r
-\r
-       /**\r
-       * Derives BLAKE2b account private keys.\r
-       *\r
-       * @param {number[]} indexes - Indexes of the accounts\r
-       * @returns {Promise<Account>}\r
-       */\r
-       async ckd (indexes: number[]): Promise<KeyPair[]> {\r
-               const results = indexes.map(index => {\r
-                       const indexHex = index.toString(16).padStart(8, '0').toUpperCase()\r
-                       const inputHex = `${this.seed}${indexHex}`.padStart(72, '0')\r
-                       const inputBytes = hex.toBytes(inputHex)\r
-                       const privateKey: string = new Blake2b(32).update(inputBytes).digest('hex')\r
-                       return { privateKey, index }\r
-               })\r
-               return results\r
-       }\r
-}\r
-\r
-/**\r
-* Ledger hardware wallet created by communicating with a Ledger device via ADPU\r
-* calls. This wallet does not feature any seed nor mnemonic phrase as all\r
-* private keys are held in the secure chip of the device. As such, the user\r
-* is responsible for using Ledger technology to back up these pieces of data.\r
-*\r
-* Usage of this wallet is generally controlled by calling functions of the\r
-* `ledger` object. For example, the wallet interface should have a button to\r
-* initiate a device connection by calling `wallet.ledger.connect()`. For more\r
-* information, refer to the ledger.js service file.\r
-*/\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
-               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
-       * Creates a new Ledger hardware wallet communication layer by dynamically\r
-       * importing the ledger.js service.\r
-       *\r
-       * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object\r
-       */\r
-       static async create (): Promise<LedgerWallet> {\r
-               const { Ledger } = await import('../ledger')\r
-               const l = await Ledger.init()\r
-               const id = await Entropy.create(16)\r
-               LedgerWallet.#isInternal = true\r
-               return new this(id, l)\r
-       }\r
-\r
-       /**\r
-       * Retrieves an existing Ledger wallet from session storage using its ID.\r
-       *\r
-       * @param {string} id - Generated when the wallet was initially created\r
-       * @returns {LedgerWallet} Restored LedgerWallet\r
-       */\r
-       static async restore (id: string): Promise<LedgerWallet> {\r
-               if (typeof id !== 'string' || id === '') {\r
-                       throw new TypeError('Wallet ID is required to restore')\r
-               }\r
-               const { Ledger } = await import('../ledger')\r
-               const l = await Ledger.init()\r
-               LedgerWallet.#isInternal = true\r
-               return new this(await Entropy.import(id), l)\r
-       }\r
-\r
-       /**\r
-       * Gets the public key for an account from the Ledger device.\r
-       *\r
-       * @param {number[]} indexes - Indexes of the accounts\r
-       * @returns {Promise<Account>}\r
-       */\r
-       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
-                       if (status === 'OK' && publicKey != null) {\r
-                               results.push({ publicKey, index })\r
-                       } else {\r
-                               throw new Error(`Error getting Ledger account: ${status}`)\r
-                       }\r
-               }\r
-               return results\r
-       }\r
-\r
-       /**\r
-       * Attempts to close the current process on the Ledger device.\r
-       *\r
-       * Overrides the default wallet `lock()` method since as a hardware wallet it\r
-       * does not need to be encrypted by software.\r
-       *\r
-       * @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
-               return result.status === 'OK'\r
-       }\r
-\r
-       /**\r
-       * Attempts to connect to the Ledger device.\r
-       *\r
-       * Overrides the default wallet `unlock()` method since as a hardware wallet it\r
-       * does not need to be encrypted by software.\r
-       *\r
-       * @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
-               return result === 'OK'\r
-       }\r
-}\r
diff --git a/src/lib/wallets/ledger-wallet.ts b/src/lib/wallets/ledger-wallet.ts
new file mode 100644 (file)
index 0000000..f25f83f
--- /dev/null
@@ -0,0 +1,114 @@
+// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>\r
+// SPDX-License-Identifier: GPL-3.0-or-later\r
+\r
+import { KeyPair, Wallet } from '.'\r
+import { Entropy } from '#src/lib/entropy.js'\r
+import { Ledger } from '#src/lib/ledger.js'\r
+\r
+/**\r
+* Ledger hardware wallet created by communicating with a Ledger device via ADPU\r
+* calls. This wallet does not feature any seed nor mnemonic phrase as all\r
+* private keys are held in the secure chip of the device. As such, the user\r
+* is responsible for using Ledger technology to back up these pieces of data.\r
+*\r
+* Usage of this wallet is generally controlled by calling functions of the\r
+* `ledger` object. For example, the wallet interface should have a button to\r
+* initiate a device connection by calling `wallet.ledger.connect()`. For more\r
+* information, refer to the ledger.js service file.\r
+*/\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
+               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
+       * Creates a new Ledger hardware wallet communication layer by dynamically\r
+       * importing the ledger.js service.\r
+       *\r
+       * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object\r
+       */\r
+       static async create (): Promise<LedgerWallet> {\r
+               const { Ledger } = await import('../ledger')\r
+               const l = await Ledger.init()\r
+               const id = await Entropy.create(16)\r
+               LedgerWallet.#isInternal = true\r
+               return new this(id, l)\r
+       }\r
+\r
+       /**\r
+       * Retrieves an existing Ledger wallet from session storage using its ID.\r
+       *\r
+       * @param {string} id - Generated when the wallet was initially created\r
+       * @returns {LedgerWallet} Restored LedgerWallet\r
+       */\r
+       static async restore (id: string): Promise<LedgerWallet> {\r
+               if (typeof id !== 'string' || id === '') {\r
+                       throw new TypeError('Wallet ID is required to restore')\r
+               }\r
+               const { Ledger } = await import('../ledger')\r
+               const l = await Ledger.init()\r
+               LedgerWallet.#isInternal = true\r
+               return new this(await Entropy.import(id), l)\r
+       }\r
+\r
+       /**\r
+       * Gets the public key for an account from the Ledger device.\r
+       *\r
+       * @param {number[]} indexes - Indexes of the accounts\r
+       * @returns {Promise<Account>}\r
+       */\r
+       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
+                       if (status === 'OK' && publicKey != null) {\r
+                               results.push({ publicKey, index })\r
+                       } else {\r
+                               throw new Error(`Error getting Ledger account: ${status}`)\r
+                       }\r
+               }\r
+               return results\r
+       }\r
+\r
+       /**\r
+       * Attempts to close the current process on the Ledger device.\r
+       *\r
+       * Overrides the default wallet `lock()` method since as a hardware wallet it\r
+       * does not need to be encrypted by software.\r
+       *\r
+       * @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
+               return result.status === 'OK'\r
+       }\r
+\r
+       /**\r
+       * Attempts to connect to the Ledger device.\r
+       *\r
+       * Overrides the default wallet `unlock()` method since as a hardware wallet it\r
+       * does not need to be encrypted by software.\r
+       *\r
+       * @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
+               return result === 'OK'\r
+       }\r
+}\r