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'
*
* @param {string} [key] - Hexadecimal-formatted private key to use for signing
*/
- async sign (key?: string): Promise<void>
+ async sign (key?: string): Promise<string>
/**
* Signs the block using a Ledger hardware wallet. If that fails, an error is
* thrown with the status code from the device.
* @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<void>
- async sign (input?: number | string, frontier?: ChangeBlock | ReceiveBlock | SendBlock): Promise<void> {
+ async sign (index?: number, frontier?: ChangeBlock | ReceiveBlock | SendBlock): Promise<string>
+ async sign (input?: number | string, frontier?: ChangeBlock | ReceiveBlock | SendBlock): Promise<string> {
if (typeof input === 'number') {
const index = input
const { LedgerWallet } = await import('./ledger')
}
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 })
*/\r
static toHex (bytes: Uint8Array): string {\r
if (bytes.buffer instanceof ArrayBuffer && bytes.buffer.detached) return ''\r
- const byteArray = [...bytes].map(byte => byte.toString(16).padStart(2, '0'))\r
- return byteArray.join('').toUpperCase()\r
+ return [...bytes]\r
+ .map(byte => byte.toString(16).padStart(2, '0'))\r
+ .join('')\r
+ .toUpperCase()\r
}\r
\r
/**\r
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<T extends Data> (data: NamedData<T>, 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<boolean>} True if data was successfully removed, else false
*/
- static async delete (name: string, store: string): Promise<boolean>
+ static async delete (id: string, store: string): Promise<boolean>
/**
* 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<boolean>} True if data was successfully removed, else false
*/
- static async delete (names: string[], store: string): Promise<boolean>
- static async delete (names: string | string[], store: string): Promise<boolean> {
- if (!Array.isArray(names)) names = [names]
+ static async delete (ids: string[], store: string): Promise<boolean>
+ static async delete (ids: string | string[], store: string): Promise<boolean> {
+ 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) {
* @param {string} store - Datastore from which to get the record
* @returns {Promise<NamedData>} Object of key-value pairs
*/
- static async get<T extends Data> (name: string, store: string): Promise<NamedData<T>>
+ static async get<T extends Data> (id: string, store: string): Promise<NamedData<T>>
/**
* Gets specific records from a datastore.
*
* @param {string} store - Datastore from which to get records
* @returns {Promise<NamedData>} Object of key-value pairs
*/
- static async get<T extends Data> (names: string[], store: string): Promise<NamedData<T>>
- static async get<T extends Data> (names: string | string[], store: string): Promise<NamedData<T>> {
- if (!Array.isArray(names)) names = [names]
+ static async get<T extends Data> (ids: string[], store: string): Promise<NamedData<T>>
+ static async get<T extends Data> (ids: string | string[], store: string): Promise<NamedData<T>> {
+ 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<T> = {}
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)
}
}
for (const DB_STORE of this.DB_STORES) {
if (!db.objectStoreNames.contains(DB_STORE)) {
- db.createObjectStore(DB_STORE)
+ db.createObjectStore(DB_STORE, { keyPath: 'id' })
}
}
}
import { ChangeBlock, ReceiveBlock, SendBlock } from './block'\r
import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, LEDGER_ADPU_CODES, LEDGER_STATUS_CODES } from './constants'\r
import { bytes, dec, hex } from './convert'\r
+import { Database } from './database'\r
import { Rpc } from './rpc'\r
import { Wallet } from './wallet'\r
import { DeviceStatus, KeyPair, LedgerAccountResponse, LedgerResponse, LedgerSignResponse, LedgerVersionResponse } from '#types'\r
static async create (): Promise<LedgerWallet> {\r
try {\r
if (this.isUnsupported) throw new Error('Browser is unsupported')\r
- const id = 'Ledger'\r
LedgerWallet.#isInternal = true\r
- const wallet = new this(id)\r
- return wallet\r
+ const self = new this()\r
+ await Database.add({ [self.id]: { id: self.id, type: 'Ledger' } }, Wallet.DB_NAME)\r
+ return self\r
} catch (err) {\r
- throw new Error('failed to initialize Ledger wallet', { cause: err })\r
+ throw new Error('Failed to initialize Ledger wallet', { cause: err })\r
}\r
}\r
\r
+ /**\r
+ * Overrides `import()` from the base Wallet class since Ledger secrets cannot\r
+ * be extracted from the device.\r
+ */\r
+ static import (): Promise<LedgerWallet> {\r
+ return LedgerWallet.create()\r
+ }\r
+\r
#status: DeviceStatus = 'DISCONNECTED'\r
get status (): DeviceStatus { return this.#status }\r
\r
- private constructor (id: string) {\r
+ private constructor () {\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, 'Ledger')\r
+ super('Ledger')\r
}\r
\r
/**\r
}\r
}\r
\r
- /**\r
- * Retrieves an existing Ledger wallet from 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
- try {\r
- id = id.replace('Ledger_', '')\r
- LedgerWallet.#isInternal = true\r
- return new this(id)\r
- } catch (err) {\r
- console.error(err)\r
- throw new Error('failed to restore wallet', { cause: err })\r
- }\r
- }\r
-\r
/**\r
* Sign a block with the Ledger device.\r
*\r
return { status }\r
}\r
\r
+ /**\r
+ * Checks whether a given seed matches the wallet seed. The wallet must be\r
+ * unlocked prior to verification.\r
+ *\r
+ * @param {string} seed - Hexadecimal seed to be matched against the wallet data\r
+ * @returns True if input matches wallet seed\r
+ */\r
+ async verify (seed: string): Promise<boolean>\r
+ /**\r
+ * Checks whether a given mnemonic phrase matches the wallet mnemonic. If a\r
+ * personal salt was used when generating the mnemonic, it cannot be verified.\r
+ * The wallet must be unlocked prior to verification.\r
+ *\r
+ * @param {string} mnemonic - Phrase to be matched against the wallet data\r
+ * @returns True if input matches wallet mnemonic\r
+ */\r
+ async verify (mnemonic: string): Promise<boolean>\r
+ async verify (secret: string): Promise<boolean> {\r
+ const testWallet = await Wallet.import('BIP-44', '', secret)\r
+ await testWallet.unlock('')\r
+ const testAccount = await testWallet.account(0)\r
+ const testOpenBlock = new ReceiveBlock(\r
+ testAccount.address,\r
+ '0',\r
+ testAccount.address,\r
+ '0',\r
+ testAccount.address,\r
+ '0'\r
+ )\r
+ await testWallet.sign(0, testOpenBlock)\r
+ const testSendBlock = new SendBlock(\r
+ testAccount.address,\r
+ '0',\r
+ testAccount.address,\r
+ '0',\r
+ testAccount.address,\r
+ testOpenBlock.hash\r
+ )\r
+ const testSignature = await testWallet.sign(0, testOpenBlock)\r
+ try {\r
+ const signature = await this.sign(0, testSendBlock, testOpenBlock)\r
+ return signature === testSignature\r
+ } catch (err) {\r
+ throw new Error('Failed to verify wallet', { cause: err })\r
+ }\r
+ }\r
+\r
/**\r
* Get the version of the current process. If a specific app is running, get\r
* the app version. Otherwise, get the Ledger BOLOS version instead.\r
transfer.push(result[k])
}
}
- debugger
//@ts-expect-error
BROWSER: postMessage(result, transfer)
//@ts-expect-error
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 })
}
}
}
static async #createAesKey (purpose: 'encrypt' | 'decrypt', password: ArrayBuffer, keySalt: ArrayBuffer): Promise<CryptoKey> {
- 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 = {
}
static async #decryptWallet (key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<NamedData<string | ArrayBuffer>> {
- 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)
* three types of wallets are supported: BIP-44, BLAKE2b, and Ledger.\r
*/\r
export class Wallet {\r
- static #DB_NAME = 'Wallet'\r
static #isInternal: boolean = false\r
+ static DB_NAME = 'Wallet'\r
\r
/**\r
* Retrieves a wallet from the database.\r
*/\r
- static async #get (name: string) {\r
+ static async #get (id: string) {\r
try {\r
- const record = await Database.get<NamedData>(name, this.#DB_NAME)\r
- return record[name]\r
+ const record = await Database.get<NamedData>(id, this.DB_NAME)\r
+ return record[id]\r
} catch (err) {\r
throw new Error('Failed to get wallet from database', { cause: err })\r
}\r
* @param {string} [salt=''] - Used when generating the final seed\r
* @returns {Wallet} A newly instantiated Wallet\r
*/\r
- static async create (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise<Wallet> {\r
+ static async create (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise<Wallet> {\r
Wallet.#isInternal = true\r
- const self = new this(name, type)\r
+ const self = new this(type)\r
try {\r
- debugger\r
- const { iv, salt, encrypted } = await self.#safe.request<ArrayBuffer>({\r
+ const { iv, salt, encrypted, seed, mnemonic } = await self.#safe.request<ArrayBuffer>({\r
action: 'create',\r
type,\r
password: utf8.toBuffer(password),\r
mnemonicSalt: mnemonicSalt ?? ''\r
})\r
- const data = {\r
- name,\r
+ self.#mnemonic = mnemonic\r
+ self.#seed = seed\r
+ const record = {\r
+ id: self.id,\r
type,\r
iv,\r
salt,\r
encrypted\r
}\r
- await Database.put({ [name]: data }, Wallet.#DB_NAME)\r
+ await Database.add({ [self.id]: record }, Wallet.DB_NAME)\r
return self\r
} catch (err) {\r
throw new Error('Error creating new Wallet', { cause: err })\r
\r
/**\r
* Imports an existing HD wallet by using an entropy value generated using a\r
- * cryptographically strong pseudorandom number generator.\r
+ * cryptographically strong pseudorandom number generator.NamedD\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 {Wallet} A newly instantiated Wallet\r
*/\r
- static async import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise<Wallet>\r
+ static async import (type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise<Wallet>\r
/**\r
* Imports an existing HD wallet by using an entropy value generated using a\r
* cryptographically strong pseudorandom number generator.\r
* @param {string} [salt=''] - Used when generating the final seed\r
* @returns {Wallet} A newly instantiated Wallet\r
*/\r
- static async import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicPhrase: string, mnemonicSalt?: string): Promise<Wallet>\r
- static async import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, secret: string, mnemonicSalt?: string): Promise<Wallet> {\r
+ static async import (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicPhrase: string, mnemonicSalt?: string): Promise<Wallet>\r
+ static async import (type: 'BIP-44' | 'BLAKE2b', password: string, secret: string, mnemonicSalt?: string): Promise<Wallet> {\r
Wallet.#isInternal = true\r
- const self = new this(name, type)\r
+ const self = new this(type)\r
try {\r
- const data: any = {\r
+ const data: NamedData = {\r
action: 'import',\r
type,\r
- password: utf8.toBuffer(password),\r
- mnemonicSalt\r
+ password: utf8.toBuffer(password)\r
}\r
- if (/^(?:[A-Fa-f0-9]{64}){1,2}$/.test(secret)) {\r
+ if (/^[A-Fa-f0-9]+$/.test(secret)) {\r
data.seed = hex.toBuffer(secret)\r
} else {\r
data.mnemonicPhrase = secret\r
+ if (mnemonicSalt != null) data.mnemonicSalt = mnemonicSalt\r
}\r
const result = self.#safe.request<ArrayBuffer>(data)\r
const { iv, salt, encrypted } = await result\r
const record = {\r
- name,\r
+ id: self.id,\r
type,\r
iv,\r
salt,\r
encrypted\r
}\r
- console.log(record)\r
- await Database.put({ [name]: record }, Wallet.#DB_NAME)\r
+ await Database.add({ [self.id]: record }, Wallet.DB_NAME)\r
return self\r
} catch (err) {\r
throw new Error('Error creating new Wallet', { cause: err })\r
}\r
\r
/**\r
- * Retrieves an existing wallet from the database using its name.\r
+ * Retrieves an existing wallet from the database using its UUID.\r
*\r
- * @param {string} name - Entered by user when the wallet was initially created\r
+ * @param {string} id - Entered by user when the wallet was initially created\r
* @returns {Wallet} Restored locked Wallet\r
*/\r
- static async restore (name: string): Promise<Wallet> {\r
+ static async restore (id: string): Promise<Wallet> {\r
try {\r
- if (typeof name !== 'string' || name === '') {\r
- throw new TypeError('Wallet name is required to restore')\r
+ if (typeof id !== 'string' || id === '') {\r
+ throw new TypeError('Wallet ID is required to restore')\r
}\r
- const { type } = await this.#get(name)\r
- if (type !== 'BIP-44' && type !== 'BLAKE2b') {\r
+ const { type } = await this.#get(id)\r
+ if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Ledger') {\r
throw new Error('Invalid wallet type from database')\r
}\r
Wallet.#isInternal = true\r
- return new this(name, type)\r
+ return new this(type)\r
} catch (err) {\r
throw new Error('Failed to restore wallet', { cause: err })\r
}\r
\r
#accounts: AccountList\r
#lockTimer?: any\r
- #name: string\r
+ #id: string\r
+ #mnemonic?: ArrayBuffer\r
#safe: WorkerQueue\r
+ #seed?: ArrayBuffer\r
#type: WalletType\r
\r
- get name () { return `${this.type}_${this.#name}` }\r
+ get id () { return this.#id }\r
get type () { return this.#type }\r
\r
- constructor (name: string, type: WalletType) {\r
+ /** Set when calling `create()` and self-destructs after the first read. */\r
+ get mnemonic () {\r
+ if (this.#mnemonic == null) return undefined\r
+ try {\r
+ const b = new Uint8Array(this.#mnemonic)\r
+ this.#mnemonic = undefined\r
+ const m = bytes.toUtf8(b)\r
+ b.fill(0).buffer.transfer()\r
+ return m\r
+ } catch {\r
+ this.#mnemonic = undefined\r
+ return undefined\r
+ }\r
+ }\r
+ /** Set when calling `create()` and self-destructs after the first read. */\r
+ get seed () {\r
+ if (this.#seed == null) return undefined\r
+ try {\r
+ const b = new Uint8Array(this.#seed)\r
+ this.#seed = undefined\r
+ const s = bytes.toHex(b)\r
+ b.fill(0).buffer.transfer()\r
+ return s\r
+ } catch {\r
+ this.#seed = undefined\r
+ return undefined\r
+ }\r
+ }\r
+\r
+ constructor (type: WalletType) {\r
if (!Wallet.#isInternal) {\r
throw new Error(`Wallet cannot be instantiated directly. Use 'await Wallet.create()' instead.`)\r
}\r
Wallet.#isInternal = false\r
this.#accounts = new AccountList()\r
- this.#name = name\r
- this.#safe = new WorkerQueue(SafeWorker)\r
+ this.#id = crypto.randomUUID()\r
+ this.#safe = new WorkerQueue(type === 'Ledger' ? '' : SafeWorker)\r
this.#type = type\r
}\r
\r
*/\r
async destroy (): Promise<void> {\r
try {\r
- await Database.delete(this.name, Wallet.#DB_NAME)\r
+ const isDeleted = await Database.delete(this.#id, Wallet.DB_NAME)\r
+ if (!isDeleted) {\r
+ throw new Error('Failed to delete wallet from database')\r
+ }\r
+ this.#safe.terminate()\r
} catch (err) {\r
console.error(err)\r
- throw new Error('failed to destroy wallet', { cause: err })\r
+ throw new Error('Failed to destroy wallet', { cause: err })\r
}\r
}\r
\r
* specified. The signature is appended to the signature field of the block\r
* before being returned. The wallet must be unlocked prior to signing.\r
*\r
- * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - Block data to be hashed and signed\r
* @param {number} index - Account to use for signing\r
+ * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - Block data to be hashed and signed\r
* @returns {Promise<string>} Hexadecimal-formatted 64-byte signature\r
*/\r
async sign (index: number, block: ChangeBlock | ReceiveBlock | SendBlock): Promise<string> {\r
*/\r
async unlock (password: string): Promise<boolean> {\r
try {\r
- debugger\r
- const { iv, salt, encrypted } = await Wallet.#get(this.#name)\r
- console.log(iv, salt, encrypted)\r
- const result = await this.#safe.request<boolean>({\r
+ const { iv, salt, encrypted } = await Wallet.#get(this.#id)\r
+ const { isUnlocked } = await this.#safe.request<boolean>({\r
action: 'unlock',\r
password: utf8.toBuffer(password),\r
iv,\r
keySalt: salt,\r
encrypted\r
})\r
- const { isUnlocked } = result\r
if (!isUnlocked) {\r
throw new Error('Unlock request to Safe failed')\r
}\r
clearTimeout(this.#lockTimer)\r
- this.#lockTimer = setTimeout(() => this.lock(), 120)\r
+ this.#lockTimer = setTimeout(() => this.lock(), 120000)\r
return isUnlocked\r
} catch (err) {\r
throw new Error('Failed to unlock wallet', { cause: err })\r
}\r
return await this.unopened(rpc, batchSize, from + batchSize)\r
}\r
+\r
+ /**\r
+ * Checks whether a given seed matches the wallet seed. The wallet must be\r
+ * unlocked prior to verification.\r
+ *\r
+ * @param {string} seed - Hexadecimal seed to be matched against the wallet data\r
+ * @returns True if input matches wallet seed\r
+ */\r
+ async verify (seed: string): Promise<boolean>\r
+ /**\r
+ * Checks whether a given mnemonic phrase matches the wallet mnemonic. If a\r
+ * personal salt was used when generating the mnemonic, it cannot be verified.\r
+ * The wallet must be unlocked prior to verification.\r
+ *\r
+ * @param {string} mnemonic - Phrase to be matched against the wallet data\r
+ * @returns True if input matches wallet mnemonic\r
+ */\r
+ async verify (mnemonic: string): Promise<boolean>\r
+ async verify (secret: string): Promise<boolean> {\r
+ try {\r
+ const data: NamedData<string> = {\r
+ action: 'verify'\r
+ }\r
+ if (/^[A-Fa-f0-9]+$/.test(secret)) {\r
+ data.seed = secret\r
+ } else {\r
+ data.mnemonicPhrase = secret\r
+ }\r
+ const result = await this.#safe.request<boolean>(data)\r
+ const { isUnlocked } = result\r
+ if (!isUnlocked) {\r
+ throw new Error('Unlock request to Safe failed')\r
+ }\r
+ clearTimeout(this.#lockTimer)\r
+ this.#lockTimer = setTimeout(() => this.lock(), 120)\r
+ return isUnlocked\r
+ } catch (err) {\r
+ throw new Error('Failed to unlock wallet', { cause: err })\r
+ }\r
+ }\r
}\r
*/
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.
* @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<Wallet>
+ static create (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise<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 import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise<Wallet>
+ static import (type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise<Wallet>
/**
* Imports an existing HD wallet by using an entropy value generated using a
* cryptographically strong pseudorandom number generator.
* @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<Wallet>
+ static import (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicPhrase: string, mnemonicSalt?: string): Promise<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 restore (name: string): Promise<Wallet>
- get name (): string
+ static restore (id: string): Promise<Wallet>
+ 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.
* 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<string>} Hexadecimal-formatted 64-byte signature
*/
sign (index: number, block: ChangeBlock | ReceiveBlock | SendBlock): Promise<string>
* @returns {Promise<Account>} The lowest-indexed unopened account belonging to the wallet
*/
unopened (rpc: Rpc, batchSize?: number, from?: number): Promise<Account>
+ /**
+ * 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<boolean>
+ /**
+ * 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<boolean>
}
type DeviceStatus = 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED'
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)
assert.equal(block.signature, signature)\r
})\r
\r
- await test('sign open block with account', async () => {\r
+ await test('fail to sign open block with wallet when locked', async () => {\r
+ const wallet = await Wallet.import('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
const block = new ReceiveBlock(\r
NANO_TEST_VECTORS.OPEN_BLOCK.account,\r
'0',\r
NANO_TEST_VECTORS.OPEN_BLOCK.previous,\r
NANO_TEST_VECTORS.OPEN_BLOCK.work\r
)\r
- await block.sign(NANO_TEST_VECTORS.OPEN_BLOCK.key)\r
+ await wallet.sign(0, block)\r
assert.equal(block.hash, NANO_TEST_VECTORS.OPEN_BLOCK.hash)\r
assert.equal(block.signature, NANO_TEST_VECTORS.OPEN_BLOCK.signature)\r
})\r
import { assert, isNode, suite, test } from './GLOBALS.mjs'\r
import { NANO_TEST_VECTORS } from './VECTORS.mjs'\r
\r
-/**\r
-* @type {typeof import('../dist/types.d.ts').Bip44Wallet}\r
-*/\r
-let Bip44Wallet\r
-/**\r
-* @type {typeof import('../dist/types.d.ts').Blake2bWallet}\r
-*/\r
-let Blake2bWallet\r
/**\r
* @type {typeof import('../dist/types.d.ts').Wallet}\r
*/\r
let Wallet\r
if (isNode) {\r
- ({ Bip44Wallet, Blake2bWallet, Wallet } = await import('../dist/nodejs.min.js'))\r
+ ({ Wallet } = await import('../dist/nodejs.min.js'))\r
} else {\r
- ({ Bip44Wallet, Blake2bWallet, Wallet } = await import('../dist/browser.min.js'))\r
+ ({ Wallet } = await import('../dist/browser.min.js'))\r
}\r
\r
await Promise.all([\r
\r
await test('destroy BIP-44 wallet before unlocking', async () => {\r
const wallet = await Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD)\r
- await assert.resolves(wallet.destroy())\r
-\r
- assert.ok('mnemonic' in wallet)\r
- assert.ok('seed' in wallet)\r
- assert.throws(() => wallet.mnemonic)\r
- assert.throws(() => wallet.seed)\r
\r
+ await assert.resolves(wallet.destroy())\r
await assert.rejects(wallet.unlock(NANO_TEST_VECTORS.PASSWORD))\r
})\r
\r
await test('BIP-44 wallet with random entropy', async () => {\r
- const wallet = await Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD)\r
+ const wallet = await Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.ok('id' in wallet)\r
- assert.ok(/^BIP-44_[A-Fa-f0-9]{32,64}$/.test(wallet.id))\r
+ 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))\r
+ assert.ok('mnemonic' in wallet)\r
+ assert.ok(/^(?:[a-z]{3,} ){11,23}[a-z]{3,}$/.test(wallet.mnemonic ?? ''))\r
+ assert.ok('seed' in wallet)\r
+ assert.ok(/^[A-Fa-f0-9]{128}$/.test(wallet.seed ?? ''))\r
+\r
+ assert.ok('id' in wallet)\r
+ 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))\r
assert.ok('mnemonic' in wallet)\r
- assert.ok(/^(?:[a-z]{3,} ){11,23}[a-z]{3,}$/.test(wallet.mnemonic))\r
+ assert.ok(wallet.mnemonic === undefined)\r
assert.ok('seed' in wallet)\r
- assert.ok(/^[A-Fa-f0-9]{128}$/.test(wallet.seed))\r
+ assert.ok(wallet.seed === undefined)\r
\r
await assert.resolves(wallet.destroy())\r
})\r
\r
await test('BLAKE2b wallet with random entropy', async () => {\r
- const wallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD)\r
+ const wallet = await Wallet.create('BLAKE2b', NANO_TEST_VECTORS.PASSWORD)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.ok('id' in wallet)\r
- assert.ok(/^BLAKE2b_[A-Fa-f0-9]{32,64}$/.test(wallet.id))\r
+ 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))\r
+ assert.ok('mnemonic' in wallet)\r
+ assert.ok(/^(?:[a-z]{3,} ){11,23}[a-z]{3,}$/.test(wallet.mnemonic ?? ''))\r
+ assert.ok('seed' in wallet)\r
+ assert.ok(/^[A-Fa-f0-9]{64}$/.test(wallet.seed ?? ''))\r
+\r
+ assert.ok('id' in wallet)\r
+ 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))\r
assert.ok('mnemonic' in wallet)\r
- assert.ok(/^(?:[a-z]{3,} ){11,23}[a-z]{3,}$/.test(wallet.mnemonic))\r
+ assert.ok(wallet.mnemonic === undefined)\r
assert.ok('seed' in wallet)\r
- assert.ok(/^[A-Fa-f0-9]{64}$/.test(wallet.seed))\r
+ assert.ok(wallet.seed === undefined)\r
\r
await assert.resolves(wallet.destroy())\r
})\r
const invalidArgs = [null, true, false, 0, 1, 2, { foo: 'bar' }]\r
for (const arg of invalidArgs) {\r
//@ts-expect-error\r
- const wallet = Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD, arg)\r
+ const wallet = Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD, arg)\r
await assert.resolves(wallet)\r
await assert.resolves((await wallet).destroy())\r
}\r
\r
await test('fail when using new', async () => {\r
//@ts-expect-error\r
- assert.throws(() => new Bip44Wallet())\r
+ assert.throws(() => new Wallet())\r
//@ts-expect-error\r
- assert.throws(() => new Blake2bWallet())\r
+ assert.throws(() => new Wallet())\r
})\r
\r
await test('fail without a password', async () => {\r
//@ts-expect-error\r
- await assert.rejects(Bip44Wallet.create())\r
+ await assert.rejects(Wallet.create())\r
//@ts-expect-error\r
- await assert.rejects(Blake2bWallet.create())\r
+ await assert.rejects(Wallet.create())\r
})\r
})\r
])\r