From: Chris Duncan Date: Sun, 10 Aug 2025 05:42:53 +0000 (-0700) Subject: Extract wallet create to separate module. Allow mnemonic salt to be excluded (undefin... X-Git-Tag: v0.10.5~41^2~133 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=06193bb61aa3986f2b3ec6023a8b19d74581cc31;p=libnemo.git Extract wallet create to separate module. Allow mnemonic salt to be excluded (undefined) but throw on any non-string instead of converting to empty string to avoid user confusion about resulting wallet functionality. --- diff --git a/src/lib/wallet/create.ts b/src/lib/wallet/create.ts new file mode 100644 index 0000000..59fa4b2 --- /dev/null +++ b/src/lib/wallet/create.ts @@ -0,0 +1,37 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { Database } from '../database' +import { _load } from './load' +import { Wallet } from './wallet' + +/** +* Creates a new HD wallet by using an entropy value generated using a +* cryptographically strong pseudorandom number generator. +* +* @param {string} password - Encrypts the wallet to lock and unlock it +* @param {string} [salt=''] - Used when generating the final seed +* @returns {Wallet} A newly instantiated Wallet +*/ +export async function _create (wallet: Wallet, password: ArrayBuffer, mnemonicSalt?: string) { + try { + const { iv, salt, encrypted, seed, mnemonic } = await wallet.safe.request({ + action: 'create', + type: wallet.type, + password, + mnemonicSalt: mnemonicSalt ?? '' + }) + const record = { + id: wallet.id, + type: wallet.type, + iv, + salt, + encrypted + } + await Database.add({ [wallet.id]: record }, Wallet.DB_NAME) + return { mnemonic, seed } + } catch (err) { + await wallet.destroy() + throw new Error('Error creating new Wallet', { cause: err }) + } +} diff --git a/src/lib/wallet/load.ts b/src/lib/wallet/load.ts index 245f28a..7a01d8c 100644 --- a/src/lib/wallet/load.ts +++ b/src/lib/wallet/load.ts @@ -15,7 +15,7 @@ import { Wallet } from './wallet' * @param {string} [salt=''] - Used when generating the final seed * @returns {Wallet} A newly instantiated Wallet */ -export async function _load (wallet: Wallet, password: ArrayBuffer, secret: string, mnemonicSalt?: string): Promise { +export async function _load (wallet: Wallet, password: ArrayBuffer, secret: string, mnemonicSalt?: string): Promise { try { const data: NamedData = { action: 'load', @@ -40,7 +40,6 @@ export async function _load (wallet: Wallet, password: ArrayBuffer, secret: stri encrypted } await Database.add({ [wallet.id]: record }, Wallet.DB_NAME) - return wallet } catch (err) { await wallet.destroy() throw new Error('Error creating new Wallet', { cause: err }) diff --git a/src/lib/wallet/wallet.ts b/src/lib/wallet/wallet.ts index e3446c3..61f0828 100644 --- a/src/lib/wallet/wallet.ts +++ b/src/lib/wallet/wallet.ts @@ -11,6 +11,7 @@ import { _load } from './load' import { Rpc } from '../rpc' import { default as SafeWorker } from './safe' import { WorkerQueue } from './worker-queue' +import { _create } from './create' /** * Represents a wallet containing numerous Nano accounts derived from a single @@ -41,31 +42,25 @@ export class Wallet { * @param {string} [salt=''] - Used when generating the final seed * @returns {Wallet} A newly instantiated Wallet */ - static async create (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise { + static async create (password: string, type: 'BIP-44' | 'BLAKE2b', mnemonicSalt?: string): Promise + static async create (password: unknown, type: unknown, mnemonicSalt?: unknown): Promise { + if (typeof password !== 'string') { + throw new TypeError('Password must be a string') + } + const passwordBuffer = utf8.toBuffer(password) + password = undefined + if (type !== 'BIP-44' && type !== 'BLAKE2b') { + throw new TypeError('Invalid wallet type') + } + if (mnemonicSalt !== undefined && typeof mnemonicSalt !== 'string') { + throw new TypeError('Mnemonic salt must be a string') + } Wallet.#isInternal = true const self = new this(type) - try { - const { iv, salt, encrypted, seed, mnemonic } = await self.#safe.request({ - action: 'create', - type, - password: utf8.toBuffer(password), - mnemonicSalt: mnemonicSalt ?? '' - }) - self.#mnemonic = mnemonic - self.#seed = seed - const record = { - id: self.id, - type, - iv, - salt, - encrypted - } - await Database.add({ [self.id]: record }, Wallet.DB_NAME) - return self - } catch (err) { - await self.destroy() - throw new Error('Error creating new Wallet', { cause: err }) - } + const { mnemonic, seed } = await _create(self, passwordBuffer, mnemonicSalt) + self.#mnemonic = mnemonic + self.#seed = seed + return self } /** @@ -110,7 +105,7 @@ export class Wallet { throw new TypeError('Password must be a string') } const passwordBuffer = utf8.toBuffer(password) - password = '' + password = undefined if (type !== 'BIP-44' && type !== 'BLAKE2b') { throw new TypeError('Invalid wallet type', { cause: type }) } @@ -122,7 +117,8 @@ export class Wallet { } Wallet.#isInternal = true const self = new this(type) - return _load(self, passwordBuffer, secret, mnemonicSalt) + await _load(self, passwordBuffer, secret, mnemonicSalt) + return self } /** diff --git a/test/perf.account.mjs b/test/perf.account.mjs index 90d8384..0e5919a 100644 --- a/test/perf.account.mjs +++ b/test/perf.account.mjs @@ -21,7 +21,7 @@ await Promise.all([ const COUNT = 0x200 await test(`Time to create ${COUNT} BIP-44 accounts`, async () => { - const wallet = await Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD) + const wallet = await Wallet.create(NANO_TEST_VECTORS.PASSWORD, 'BIP-44') await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const start = performance.now() @@ -35,7 +35,7 @@ await Promise.all([ }) await test(`Time to create ${COUNT} BLAKE2b accounts`, async () => { - const wallet = await Wallet.create('BLAKE2b', NANO_TEST_VECTORS.PASSWORD) + const wallet = await Wallet.create(NANO_TEST_VECTORS.PASSWORD, 'BLAKE2b') await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const start = performance.now() @@ -49,7 +49,7 @@ await Promise.all([ }) await test(`Time to create 1 BIP-44 account ${COUNT} times`, async () => { - const wallet = await Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD) + const wallet = await Wallet.create(NANO_TEST_VECTORS.PASSWORD, 'BIP-44') await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const times = [] @@ -65,7 +65,7 @@ await Promise.all([ }) await test(`Time to create 1 BLAKE2b account ${COUNT} times`, async () => { - const wallet = await Wallet.create('BLAKE2b', NANO_TEST_VECTORS.PASSWORD) + const wallet = await Wallet.create(NANO_TEST_VECTORS.PASSWORD, 'BLAKE2b') await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const times = [] diff --git a/test/perf.wallet.mjs b/test/perf.wallet.mjs index 6764e9e..30e1a0a 100644 --- a/test/perf.wallet.mjs +++ b/test/perf.wallet.mjs @@ -24,7 +24,7 @@ await Promise.all([ const times = [] for (let i = 0; i < COUNT; i++) { const start = performance.now() - const wallet = await Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD) + const wallet = await Wallet.create(NANO_TEST_VECTORS.PASSWORD, 'BIP-44') const end = performance.now() times.push(end - start) await wallet.destroy() @@ -36,7 +36,7 @@ await Promise.all([ const times = [] for (let i = 0; i < COUNT; i++) { const start = performance.now() - const wallet = await Wallet.create('BLAKE2b', NANO_TEST_VECTORS.PASSWORD) + const wallet = await Wallet.create(NANO_TEST_VECTORS.PASSWORD, 'BLAKE2b') const end = performance.now() times.push(end - start) await wallet.destroy() diff --git a/test/test.create-wallet.mjs b/test/test.create-wallet.mjs index cb3c6fe..a8eedf2 100644 --- a/test/test.create-wallet.mjs +++ b/test/test.create-wallet.mjs @@ -20,14 +20,14 @@ await Promise.all([ suite('Create wallets', async () => { await test('destroy BIP-44 wallet before unlocking', async () => { - const wallet = await Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD) + const wallet = await Wallet.create(NANO_TEST_VECTORS.PASSWORD, 'BIP-44') await assert.resolves(wallet.destroy()) await assert.rejects(wallet.unlock(NANO_TEST_VECTORS.PASSWORD)) }) await test('BIP-44 wallet with random entropy', async () => { - const wallet = await Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD) + const wallet = await Wallet.create(NANO_TEST_VECTORS.PASSWORD, 'BIP-44') await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) assert.ok('id' in wallet) @@ -48,7 +48,7 @@ await Promise.all([ }) await test('BLAKE2b wallet with random entropy', async () => { - const wallet = await Wallet.create('BLAKE2b', NANO_TEST_VECTORS.PASSWORD) + const wallet = await Wallet.create(NANO_TEST_VECTORS.PASSWORD, 'BLAKE2b') await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) assert.ok('id' in wallet) @@ -68,13 +68,11 @@ await Promise.all([ await assert.resolves(wallet.destroy()) }) - await test('BIP-44 replace invalid salt with empty string', async () => { + await test('BIP-44 throw on invalid salt', async () => { const invalidArgs = [null, true, false, 0, 1, 2, { foo: 'bar' }] for (const arg of invalidArgs) { //@ts-expect-error - const wallet = Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD, arg) - await assert.resolves(wallet) - await assert.resolves((await wallet).destroy()) + await assert.rejects(Wallet.create(NANO_TEST_VECTORS.PASSWORD, 'BIP-44', arg)) } })