From: Chris Duncan Date: Fri, 1 Aug 2025 21:07:07 +0000 (-0700) Subject: Create wallet tests now passing. X-Git-Tag: v0.10.5~47^2~30 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=6a534e43c0cfd4866ab0c4cd50263ca35ff2c455;p=libnemo.git Create wallet tests now passing. --- diff --git a/src/lib/block.ts b/src/lib/block.ts index 35d73a1..805d71f 100644 --- a/src/lib/block.ts +++ b/src/lib/block.ts @@ -5,7 +5,7 @@ import { NanoPow } from 'nano-pow' import { Account } from './account' import { Blake2b } from './blake2b' import { BURN_ADDRESS, PREAMBLE, DIFFICULTY_RECEIVE, DIFFICULTY_SEND } from './constants' -import { dec, hex } from './convert' +import { bytes, dec, hex } from './convert' import { NanoNaCl } from './nano-nacl' import { Rpc } from './rpc' @@ -132,7 +132,7 @@ abstract class Block { * * @param {string} [key] - Hexadecimal-formatted private key to use for signing */ - async sign (key?: string): Promise + async sign (key?: string): Promise /** * Signs the block using a Ledger hardware wallet. If that fails, an error is * thrown with the status code from the device. @@ -143,8 +143,8 @@ abstract class Block { * @param {number} index - Account index between 0x0 and 0x7fffffff * @param {object} [frontier] - JSON of frontier block for offline signing */ - async sign (index?: number, frontier?: ChangeBlock | ReceiveBlock | SendBlock): Promise - async sign (input?: number | string, frontier?: ChangeBlock | ReceiveBlock | SendBlock): Promise { + async sign (index?: number, frontier?: ChangeBlock | ReceiveBlock | SendBlock): Promise + async sign (input?: number | string, frontier?: ChangeBlock | ReceiveBlock | SendBlock): Promise { if (typeof input === 'number') { const index = input const { LedgerWallet } = await import('./ledger') @@ -159,16 +159,17 @@ abstract class Block { } try { this.signature = await ledger.sign(index, this as SendBlock | ReceiveBlock | ChangeBlock) + return this.signature } catch (err) { - throw new Error('failed to sign with ledger', { cause: err }) + throw new Error('Failed to sign block with Ledger', { cause: err }) } } else if (typeof input === 'string') { try { - const account = await Account.import({ index: 0, privateKey: input }, '') - // this.signature = await account.sign(this, '') - await account.destroy() + const sig = await NanoNaCl.detached(hex.toBytes(this.hash), hex.toBytes(input)) + this.signature = bytes.toHex(sig) + return this.signature } catch (err) { - throw new Error(`Failed to sign block`, { cause: err }) + throw new Error(`Failed to sign block with private key`, { cause: err }) } } else { throw new TypeError('invalid key for block signature', { cause: typeof input }) diff --git a/src/lib/convert.ts b/src/lib/convert.ts index aa6440d..fdcf291 100644 --- a/src/lib/convert.ts +++ b/src/lib/convert.ts @@ -142,8 +142,10 @@ export class bytes { */ static toHex (bytes: Uint8Array): string { if (bytes.buffer instanceof ArrayBuffer && bytes.buffer.detached) return '' - const byteArray = [...bytes].map(byte => byte.toString(16).padStart(2, '0')) - return byteArray.join('').toUpperCase() + return [...bytes] + .map(byte => byte.toString(16).padStart(2, '0')) + .join('') + .toUpperCase() } /** diff --git a/src/lib/database.ts b/src/lib/database.ts index 529c77a..fb833f2 100644 --- a/src/lib/database.ts +++ b/src/lib/database.ts @@ -13,29 +13,55 @@ export class Database { static DB_STORES = ['Wallet', 'Account', 'Rolodex'] static #storage: IDBDatabase + /** + * Inserts records in a datastore, throwing if they already exist. + * + * @param {NamedData} data - Object of key-value pairs + * @param {string} store - Datastore in which to put records + * @returns {Promise<(IDBValidKey | DOMException)[]>} Index keys of the records inserted + */ + static async add (data: NamedData, store: string): Promise<(IDBValidKey | DOMException)[]> { + this.#storage ??= await this.#open(this.DB_NAME) + const transaction = this.#storage.transaction(store, 'readwrite') + const db = transaction.objectStore(store) + return new Promise((resolve, reject) => { + const requests = Object.keys(data).map(key => db.add(data[key], key)) + transaction.oncomplete = (event) => { + const results = [] + for (const request of requests) { + results.push(request.error ?? request.result) + } + resolve(results) + } + transaction.onerror = (event) => { + reject((event.target as IDBRequest).error) + } + }) + } + /** * Deletes a record from a datastore. * - * @param {string} name - Index key of the record to delete + * @param {string} id - Index key of the record to delete * @param {string} store - Datastore from which to delete the record * @returns {Promise} True if data was successfully removed, else false */ - static async delete (name: string, store: string): Promise + static async delete (id: string, store: string): Promise /** * Deletes records from a datastore. * - * @param {string[]} names - Index keys of the records to delete + * @param {string[]} ids - Index keys of the records to delete * @param {string} store - Datastore from which to delete records * @returns {Promise} True if data was successfully removed, else false */ - static async delete (names: string[], store: string): Promise - static async delete (names: string | string[], store: string): Promise { - if (!Array.isArray(names)) names = [names] + static async delete (ids: string[], store: string): Promise + static async delete (ids: string | string[], store: string): Promise { + if (!Array.isArray(ids)) ids = [ids] this.#storage ??= await this.#open(this.DB_NAME) const transaction = this.#storage.transaction(store, 'readwrite') const db = transaction.objectStore(store) return new Promise((resolve, reject) => { - const requests = names.map(name => db.delete(name)) + const requests = ids.map(id => db.delete(id)) transaction.oncomplete = (event) => { for (const request of requests) { if (request?.error != null) { @@ -60,7 +86,7 @@ export class Database { * @param {string} store - Datastore from which to get the record * @returns {Promise} Object of key-value pairs */ - static async get (name: string, store: string): Promise> + static async get (id: string, store: string): Promise> /** * Gets specific records from a datastore. * @@ -68,18 +94,20 @@ export class Database { * @param {string} store - Datastore from which to get records * @returns {Promise} Object of key-value pairs */ - static async get (names: string[], store: string): Promise> - static async get (names: string | string[], store: string): Promise> { - if (!Array.isArray(names)) names = [names] + static async get (ids: string[], store: string): Promise> + static async get (ids: string | string[], store: string): Promise> { + if (!Array.isArray(ids)) ids = [ids] this.#storage ??= await this.#open(this.DB_NAME) const transaction = this.#storage.transaction(store, 'readonly') const db = transaction.objectStore(store) return new Promise((resolve, reject) => { - const requests = names.map(name => db.get(name)) + const requests = ids.map(id => db.get(id)) transaction.oncomplete = (event) => { const results: NamedData = {} for (const request of requests) { - results[request.result.name] = request.error ?? request.result + if (request?.result?.id != null) { + results[request.result.id] = request.error ?? request.result + } } resolve(results) } @@ -156,7 +184,7 @@ export class Database { } for (const DB_STORE of this.DB_STORES) { if (!db.objectStoreNames.contains(DB_STORE)) { - db.createObjectStore(DB_STORE) + db.createObjectStore(DB_STORE, { keyPath: 'id' }) } } } diff --git a/src/lib/ledger.ts b/src/lib/ledger.ts index 689d5e2..ef32031 100644 --- a/src/lib/ledger.ts +++ b/src/lib/ledger.ts @@ -8,6 +8,7 @@ import { default as TransportHID } from '@ledgerhq/hw-transport-webhid' import { ChangeBlock, ReceiveBlock, SendBlock } from './block' import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, LEDGER_ADPU_CODES, LEDGER_STATUS_CODES } from './constants' import { bytes, dec, hex } from './convert' +import { Database } from './database' import { Rpc } from './rpc' import { Wallet } from './wallet' import { DeviceStatus, KeyPair, LedgerAccountResponse, LedgerResponse, LedgerSignResponse, LedgerVersionResponse } from '#types' @@ -60,24 +61,32 @@ export class LedgerWallet extends Wallet { static async create (): Promise { try { if (this.isUnsupported) throw new Error('Browser is unsupported') - const id = 'Ledger' LedgerWallet.#isInternal = true - const wallet = new this(id) - return wallet + const self = new this() + await Database.add({ [self.id]: { id: self.id, type: 'Ledger' } }, Wallet.DB_NAME) + return self } catch (err) { - throw new Error('failed to initialize Ledger wallet', { cause: err }) + throw new Error('Failed to initialize Ledger wallet', { cause: err }) } } + /** + * Overrides `import()` from the base Wallet class since Ledger secrets cannot + * be extracted from the device. + */ + static import (): Promise { + return LedgerWallet.create() + } + #status: DeviceStatus = 'DISCONNECTED' get status (): DeviceStatus { return this.#status } - private constructor (id: string) { + private constructor () { if (!LedgerWallet.#isInternal) { throw new Error(`LedgerWallet cannot be instantiated directly. Use 'await LedgerWallet.create()' instead.`) } LedgerWallet.#isInternal = false - super(id, 'Ledger') + super('Ledger') } /** @@ -167,26 +176,6 @@ export class LedgerWallet extends Wallet { } } - /** - * 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 async restore (id: string): Promise { - if (typeof id !== 'string' || id === '') { - throw new TypeError('Wallet ID is required to restore') - } - try { - id = id.replace('Ledger_', '') - LedgerWallet.#isInternal = true - return new this(id) - } catch (err) { - console.error(err) - throw new Error('failed to restore wallet', { cause: err }) - } - } - /** * Sign a block with the Ledger device. * @@ -266,6 +255,53 @@ export class LedgerWallet extends Wallet { return { status } } + /** + * 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 + */ + async 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 + */ + async verify (mnemonic: string): Promise + async verify (secret: string): Promise { + const testWallet = await Wallet.import('BIP-44', '', secret) + await testWallet.unlock('') + const testAccount = await testWallet.account(0) + const testOpenBlock = new ReceiveBlock( + testAccount.address, + '0', + testAccount.address, + '0', + testAccount.address, + '0' + ) + await testWallet.sign(0, testOpenBlock) + const testSendBlock = new SendBlock( + testAccount.address, + '0', + testAccount.address, + '0', + testAccount.address, + testOpenBlock.hash + ) + const testSignature = await testWallet.sign(0, testOpenBlock) + try { + const signature = await this.sign(0, testSendBlock, testOpenBlock) + return signature === testSignature + } catch (err) { + throw new Error('Failed to verify wallet', { cause: err }) + } + } + /** * 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/src/lib/safe.ts b/src/lib/safe.ts index 41fdadc..1679554 100644 --- a/src/lib/safe.ts +++ b/src/lib/safe.ts @@ -88,7 +88,6 @@ export class Safe { transfer.push(result[k]) } } - debugger //@ts-expect-error BROWSER: postMessage(result, transfer) //@ts-expect-error @@ -111,9 +110,13 @@ export class Safe { try { const entropy = crypto.getRandomValues(new Uint8Array(32)) const mnemonicPhrase = (await Bip39Mnemonic.fromEntropy(entropy)).phrase - return await this.import(type, key, keySalt, mnemonicPhrase, mnemonicSalt) + const record = await this.import(type, key, keySalt, mnemonicPhrase, mnemonicSalt) + if (this.#seed == null || this.#mnemonic?.phrase == null) { + throw new Error('Failed to generate seed and mnemonic') + } + return { ...record, seed: this.#seed.slice(), mnemonic: utf8.toBuffer(this.#mnemonic.phrase) } } catch (err) { - throw new Error('Failed to unlock wallet', { cause: err }) + throw new Error('Failed to create wallet', { cause: err }) } } @@ -306,8 +309,6 @@ export class Safe { } static async #createAesKey (purpose: 'encrypt' | 'decrypt', password: ArrayBuffer, keySalt: ArrayBuffer): Promise { - console.log(keySalt) - debugger const derivationKey = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey']) new Uint8Array(password).fill(0).buffer.transfer() const derivationAlgorithm: Pbkdf2Params = { @@ -324,8 +325,6 @@ export class Safe { } static async #decryptWallet (key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise> { - console.log(iv, encrypted) - debugger const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, encrypted) const decoded = JSON.parse(bytes.toUtf8(new Uint8Array(decrypted))) const seed = hex.toBuffer(decoded.seed) diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts index 02810b5..4b3b1c7 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -17,16 +17,16 @@ import { KeyPair, NamedData, WalletType } from '#types' * three types of wallets are supported: BIP-44, BLAKE2b, and Ledger. */ export class Wallet { - static #DB_NAME = 'Wallet' static #isInternal: boolean = false + static DB_NAME = 'Wallet' /** * Retrieves a wallet from the database. */ - static async #get (name: string) { + static async #get (id: string) { try { - const record = await Database.get(name, this.#DB_NAME) - return record[name] + const record = await Database.get(id, this.DB_NAME) + return record[id] } catch (err) { throw new Error('Failed to get wallet from database', { cause: err }) } @@ -40,25 +40,26 @@ export class Wallet { * @param {string} [salt=''] - Used when generating the final seed * @returns {Wallet} A newly instantiated Wallet */ - static async create (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise { + static async create (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise { Wallet.#isInternal = true - const self = new this(name, type) + const self = new this(type) try { - debugger - const { iv, salt, encrypted } = await self.#safe.request({ + const { iv, salt, encrypted, seed, mnemonic } = await self.#safe.request({ action: 'create', type, password: utf8.toBuffer(password), mnemonicSalt: mnemonicSalt ?? '' }) - const data = { - name, + self.#mnemonic = mnemonic + self.#seed = seed + const record = { + id: self.id, type, iv, salt, encrypted } - await Database.put({ [name]: data }, Wallet.#DB_NAME) + await Database.add({ [self.id]: record }, Wallet.DB_NAME) return self } catch (err) { throw new Error('Error creating new Wallet', { cause: err }) @@ -67,13 +68,13 @@ export class Wallet { /** * Imports an existing HD wallet by using an entropy value generated using a - * cryptographically strong pseudorandom number generator. + * cryptographically strong pseudorandom number generator.NamedD * * @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 */ - static async import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise + static async import (type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise /** * Imports an existing HD wallet by using an entropy value generated using a * cryptographically strong pseudorandom number generator. @@ -82,33 +83,32 @@ export class Wallet { * @param {string} [salt=''] - Used when generating the final seed * @returns {Wallet} A newly instantiated Wallet */ - static async import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicPhrase: string, mnemonicSalt?: string): Promise - static async import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, secret: string, mnemonicSalt?: string): Promise { + static async import (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicPhrase: string, mnemonicSalt?: string): Promise + static async import (type: 'BIP-44' | 'BLAKE2b', password: string, secret: string, mnemonicSalt?: string): Promise { Wallet.#isInternal = true - const self = new this(name, type) + const self = new this(type) try { - const data: any = { + const data: NamedData = { action: 'import', type, - password: utf8.toBuffer(password), - mnemonicSalt + password: utf8.toBuffer(password) } - if (/^(?:[A-Fa-f0-9]{64}){1,2}$/.test(secret)) { + if (/^[A-Fa-f0-9]+$/.test(secret)) { data.seed = hex.toBuffer(secret) } else { data.mnemonicPhrase = secret + if (mnemonicSalt != null) data.mnemonicSalt = mnemonicSalt } const result = self.#safe.request(data) const { iv, salt, encrypted } = await result const record = { - name, + id: self.id, type, iv, salt, encrypted } - console.log(record) - await Database.put({ [name]: record }, Wallet.#DB_NAME) + await Database.add({ [self.id]: record }, Wallet.DB_NAME) return self } catch (err) { throw new Error('Error creating new Wallet', { cause: err }) @@ -116,22 +116,22 @@ export class Wallet { } /** - * Retrieves an existing wallet from the database using its name. + * Retrieves an existing wallet from the database using its UUID. * - * @param {string} name - Entered by user when the wallet was initially created + * @param {string} id - Entered by user when the wallet was initially created * @returns {Wallet} Restored locked Wallet */ - static async restore (name: string): Promise { + static async restore (id: string): Promise { try { - if (typeof name !== 'string' || name === '') { - throw new TypeError('Wallet name is required to restore') + if (typeof id !== 'string' || id === '') { + throw new TypeError('Wallet ID is required to restore') } - const { type } = await this.#get(name) - if (type !== 'BIP-44' && type !== 'BLAKE2b') { + const { type } = await this.#get(id) + if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Ledger') { throw new Error('Invalid wallet type from database') } Wallet.#isInternal = true - return new this(name, type) + return new this(type) } catch (err) { throw new Error('Failed to restore wallet', { cause: err }) } @@ -139,21 +139,52 @@ export class Wallet { #accounts: AccountList #lockTimer?: any - #name: string + #id: string + #mnemonic?: ArrayBuffer #safe: WorkerQueue + #seed?: ArrayBuffer #type: WalletType - get name () { return `${this.type}_${this.#name}` } + get id () { return this.#id } get type () { return this.#type } - constructor (name: string, type: WalletType) { + /** Set when calling `create()` and self-destructs after the first read. */ + get mnemonic () { + if (this.#mnemonic == null) return undefined + try { + const b = new Uint8Array(this.#mnemonic) + this.#mnemonic = undefined + const m = bytes.toUtf8(b) + b.fill(0).buffer.transfer() + return m + } catch { + this.#mnemonic = undefined + return undefined + } + } + /** Set when calling `create()` and self-destructs after the first read. */ + get seed () { + if (this.#seed == null) return undefined + try { + const b = new Uint8Array(this.#seed) + this.#seed = undefined + const s = bytes.toHex(b) + b.fill(0).buffer.transfer() + return s + } catch { + this.#seed = undefined + return undefined + } + } + + constructor (type: WalletType) { if (!Wallet.#isInternal) { throw new Error(`Wallet cannot be instantiated directly. Use 'await Wallet.create()' instead.`) } Wallet.#isInternal = false this.#accounts = new AccountList() - this.#name = name - this.#safe = new WorkerQueue(SafeWorker) + this.#id = crypto.randomUUID() + this.#safe = new WorkerQueue(type === 'Ledger' ? '' : SafeWorker) this.#type = type } @@ -242,10 +273,14 @@ export class Wallet { */ async destroy (): Promise { try { - await Database.delete(this.name, Wallet.#DB_NAME) + const isDeleted = await Database.delete(this.#id, Wallet.DB_NAME) + if (!isDeleted) { + throw new Error('Failed to delete wallet from database') + } + this.#safe.terminate() } catch (err) { console.error(err) - throw new Error('failed to destroy wallet', { cause: err }) + throw new Error('Failed to destroy wallet', { cause: err }) } } @@ -302,8 +337,8 @@ export class Wallet { * specified. The signature is appended to the signature field of the block * before being returned. The wallet must be unlocked prior to signing. * - * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - Block data to be hashed and signed * @param {number} index - Account to use for signing + * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - Block data to be hashed and signed * @returns {Promise} Hexadecimal-formatted 64-byte signature */ async sign (index: number, block: ChangeBlock | ReceiveBlock | SendBlock): Promise { @@ -331,22 +366,19 @@ export class Wallet { */ async unlock (password: string): Promise { try { - debugger - const { iv, salt, encrypted } = await Wallet.#get(this.#name) - console.log(iv, salt, encrypted) - const result = await this.#safe.request({ + const { iv, salt, encrypted } = await Wallet.#get(this.#id) + const { isUnlocked } = await this.#safe.request({ action: 'unlock', password: utf8.toBuffer(password), iv, keySalt: salt, encrypted }) - const { isUnlocked } = result if (!isUnlocked) { throw new Error('Unlock request to Safe failed') } clearTimeout(this.#lockTimer) - this.#lockTimer = setTimeout(() => this.lock(), 120) + this.#lockTimer = setTimeout(() => this.lock(), 120000) return isUnlocked } catch (err) { throw new Error('Failed to unlock wallet', { cause: err }) @@ -383,4 +415,44 @@ export class Wallet { } return await this.unopened(rpc, batchSize, from + batchSize) } + + /** + * 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 + */ + async 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 + */ + async verify (mnemonic: string): Promise + async verify (secret: string): Promise { + try { + const data: NamedData = { + action: 'verify' + } + if (/^[A-Fa-f0-9]+$/.test(secret)) { + data.seed = secret + } else { + data.mnemonicPhrase = secret + } + const result = await this.#safe.request(data) + const { isUnlocked } = result + if (!isUnlocked) { + throw new Error('Unlock request to Safe failed') + } + clearTimeout(this.#lockTimer) + this.#lockTimer = setTimeout(() => this.lock(), 120) + return isUnlocked + } catch (err) { + throw new Error('Failed to unlock wallet', { cause: err }) + } + } } diff --git a/src/types.d.ts b/src/types.d.ts index 4261cb0..9be9b96 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -592,6 +592,7 @@ export type WalletType = 'BIP-44' | 'BLAKE2b' | 'Ledger' */ export declare class Wallet { #private + static DB_NAME: string /** * Creates a new HD wallet by using an entropy value generated using a * cryptographically strong pseudorandom number generator. @@ -600,16 +601,16 @@ export declare class Wallet { * @param {string} [salt=''] - Used when generating the final seed * @returns {Wallet} A newly instantiated Wallet */ - static create (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise + static create (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise /** * Imports an existing HD wallet by using an entropy value generated using a - * cryptographically strong pseudorandom number generator. + * cryptographically strong pseudorandom number generator.NamedD * * @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 */ - static import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise + static import (type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise /** * Imports an existing HD wallet by using an entropy value generated using a * cryptographically strong pseudorandom number generator. @@ -618,17 +619,21 @@ export declare class Wallet { * @param {string} [salt=''] - Used when generating the final seed * @returns {Wallet} A newly instantiated Wallet */ - static import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicPhrase: string, mnemonicSalt?: string): Promise + static import (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicPhrase: string, mnemonicSalt?: string): Promise /** - * Retrieves an existing wallet from the database using its name. + * Retrieves an existing wallet from the database using its UUID. * - * @param {string} name - Entered by user when the wallet was initially created + * @param {string} id - Entered by user when the wallet was initially created * @returns {Wallet} Restored locked Wallet */ - static restore (name: string): Promise - get name (): string + static restore (id: string): Promise + get id (): string get type (): WalletType - constructor (name: string, type: WalletType) + /** Set when calling `create()` and self-destructs after the first read. */ + get mnemonic (): string | undefined + /** Set when calling `create()` and self-destructs after the first read. */ + get seed (): string | undefined + constructor (type: WalletType) /** * Retrieves an account from a wallet using its child key derivation function. * Defaults to the first account at index 0. @@ -697,8 +702,8 @@ export declare class Wallet { * specified. The signature is appended to the signature field of the block * before being returned. The wallet must be unlocked prior to signing. * - * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - Block data to be hashed and signed * @param {number} index - Account to use for signing + * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - Block data to be hashed and signed * @returns {Promise} Hexadecimal-formatted 64-byte signature */ sign (index: number, block: ChangeBlock | ReceiveBlock | SendBlock): Promise @@ -719,6 +724,23 @@ export declare class Wallet { * @returns {Promise} The lowest-indexed unopened account belonging to the wallet */ unopened (rpc: Rpc, batchSize?: number, from?: number): 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 } type DeviceStatus = 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED' diff --git a/test/main.test.mjs b/test/main.test.mjs index 3ab48d6..ada0c28 100644 --- a/test/main.test.mjs +++ b/test/main.test.mjs @@ -4,17 +4,17 @@ import { failures, passes } from './GLOBALS.mjs' import './test.runner-check.mjs' -// import './test.blake2b.mjs' +import './test.blake2b.mjs' import './test.blocks.mjs' -// import './test.calculate-pow.mjs' -// import './test.create-wallet.mjs' -// import './test.derive-accounts.mjs' -// import './test.import-wallet.mjs' -// import './test.ledger.mjs' -// import './test.lock-unlock.mjs' -// import './test.manage-rolodex.mjs' -// import './test.refresh-accounts.mjs' -// import './test.tools.mjs' +import './test.calculate-pow.mjs' +import './test.create-wallet.mjs' +import './test.derive-accounts.mjs' +import './test.import-wallet.mjs' +import './test.ledger.mjs' +import './test.lock-unlock.mjs' +import './test.manage-rolodex.mjs' +import './test.refresh-accounts.mjs' +import './test.tools.mjs' console.log('%cTESTING COMPLETE', 'color:orange;font-weight:bold') console.log('%cPASS: ', 'color:green;font-weight:bold', passes.length) diff --git a/test/test.blocks.mjs b/test/test.blocks.mjs index 6347a64..398c1e9 100644 --- a/test/test.blocks.mjs +++ b/test/test.blocks.mjs @@ -101,7 +101,8 @@ await Promise.all([ assert.equal(block.signature, signature) }) - await test('sign open block with account', async () => { + await test('fail to sign open block with wallet when locked', async () => { + const wallet = await Wallet.import('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) const block = new ReceiveBlock( NANO_TEST_VECTORS.OPEN_BLOCK.account, '0', @@ -111,7 +112,7 @@ await Promise.all([ NANO_TEST_VECTORS.OPEN_BLOCK.previous, NANO_TEST_VECTORS.OPEN_BLOCK.work ) - await block.sign(NANO_TEST_VECTORS.OPEN_BLOCK.key) + await wallet.sign(0, block) assert.equal(block.hash, NANO_TEST_VECTORS.OPEN_BLOCK.hash) assert.equal(block.signature, NANO_TEST_VECTORS.OPEN_BLOCK.signature) }) diff --git a/test/test.create-wallet.mjs b/test/test.create-wallet.mjs index f3365a6..af4bfcf 100644 --- a/test/test.create-wallet.mjs +++ b/test/test.create-wallet.mjs @@ -6,22 +6,14 @@ import { assert, isNode, suite, test } from './GLOBALS.mjs' import { NANO_TEST_VECTORS } from './VECTORS.mjs' -/** -* @type {typeof import('../dist/types.d.ts').Bip44Wallet} -*/ -let Bip44Wallet -/** -* @type {typeof import('../dist/types.d.ts').Blake2bWallet} -*/ -let Blake2bWallet /** * @type {typeof import('../dist/types.d.ts').Wallet} */ let Wallet if (isNode) { - ({ Bip44Wallet, Blake2bWallet, Wallet } = await import('../dist/nodejs.min.js')) + ({ Wallet } = await import('../dist/nodejs.min.js')) } else { - ({ Bip44Wallet, Blake2bWallet, Wallet } = await import('../dist/browser.min.js')) + ({ Wallet } = await import('../dist/browser.min.js')) } await Promise.all([ @@ -29,40 +21,49 @@ await Promise.all([ await test('destroy BIP-44 wallet before unlocking', async () => { const wallet = await Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD) - await assert.resolves(wallet.destroy()) - - assert.ok('mnemonic' in wallet) - assert.ok('seed' in wallet) - assert.throws(() => wallet.mnemonic) - assert.throws(() => wallet.seed) + 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 Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD) + const wallet = await Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) assert.ok('id' in wallet) - assert.ok(/^BIP-44_[A-Fa-f0-9]{32,64}$/.test(wallet.id)) + assert.ok(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(wallet.id)) + assert.ok('mnemonic' in wallet) + assert.ok(/^(?:[a-z]{3,} ){11,23}[a-z]{3,}$/.test(wallet.mnemonic ?? '')) + assert.ok('seed' in wallet) + assert.ok(/^[A-Fa-f0-9]{128}$/.test(wallet.seed ?? '')) + + assert.ok('id' in wallet) + assert.ok(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(wallet.id)) assert.ok('mnemonic' in wallet) - assert.ok(/^(?:[a-z]{3,} ){11,23}[a-z]{3,}$/.test(wallet.mnemonic)) + assert.ok(wallet.mnemonic === undefined) assert.ok('seed' in wallet) - assert.ok(/^[A-Fa-f0-9]{128}$/.test(wallet.seed)) + assert.ok(wallet.seed === undefined) await assert.resolves(wallet.destroy()) }) await test('BLAKE2b wallet with random entropy', async () => { - const wallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD) + const wallet = await Wallet.create('BLAKE2b', NANO_TEST_VECTORS.PASSWORD) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) assert.ok('id' in wallet) - assert.ok(/^BLAKE2b_[A-Fa-f0-9]{32,64}$/.test(wallet.id)) + assert.ok(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(wallet.id)) + assert.ok('mnemonic' in wallet) + assert.ok(/^(?:[a-z]{3,} ){11,23}[a-z]{3,}$/.test(wallet.mnemonic ?? '')) + assert.ok('seed' in wallet) + assert.ok(/^[A-Fa-f0-9]{64}$/.test(wallet.seed ?? '')) + + assert.ok('id' in wallet) + assert.ok(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(wallet.id)) assert.ok('mnemonic' in wallet) - assert.ok(/^(?:[a-z]{3,} ){11,23}[a-z]{3,}$/.test(wallet.mnemonic)) + assert.ok(wallet.mnemonic === undefined) assert.ok('seed' in wallet) - assert.ok(/^[A-Fa-f0-9]{64}$/.test(wallet.seed)) + assert.ok(wallet.seed === undefined) await assert.resolves(wallet.destroy()) }) @@ -71,7 +72,7 @@ await Promise.all([ const invalidArgs = [null, true, false, 0, 1, 2, { foo: 'bar' }] for (const arg of invalidArgs) { //@ts-expect-error - const wallet = Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD, arg) + const wallet = Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD, arg) await assert.resolves(wallet) await assert.resolves((await wallet).destroy()) } @@ -79,16 +80,16 @@ await Promise.all([ await test('fail when using new', async () => { //@ts-expect-error - assert.throws(() => new Bip44Wallet()) + assert.throws(() => new Wallet()) //@ts-expect-error - assert.throws(() => new Blake2bWallet()) + assert.throws(() => new Wallet()) }) await test('fail without a password', async () => { //@ts-expect-error - await assert.rejects(Bip44Wallet.create()) + await assert.rejects(Wallet.create()) //@ts-expect-error - await assert.rejects(Blake2bWallet.create()) + await assert.rejects(Wallet.create()) }) }) ])