// SPDX-License-Identifier: GPL-3.0-or-later\r
\r
import { Blake2b } from './blake2b'\r
+import { ChangeBlock, ReceiveBlock, SendBlock } from './block'\r
import { ACCOUNT_KEY_BYTE_LENGTH, ACCOUNT_KEY_HEX_LENGTH, ALPHABET, PREFIX, PREFIX_LEGACY } from './constants'\r
import { base32, bytes, hex, utf8 } from './convert'\r
import { Rpc } from './rpc'\r
+import { Data } from '#types'\r
import { NanoNaClWorker, SafeWorker } from '#workers'\r
-import { ChangeBlock, ReceiveBlock, SendBlock } from './block'\r
\r
/**\r
* Represents a single Nano address and the associated public key. To include the\r
\r
#address: string\r
#publicKey: Uint8Array<ArrayBuffer>\r
- #privateKey: Uint8Array<ArrayBuffer>\r
\r
#balance?: bigint\r
#frontier?: string\r
- #index?: number\r
#receivable?: bigint\r
#representative?: Account\r
#weight?: bigint\r
\r
get address () { return `${PREFIX}${this.#address}` }\r
- get isLocked () { return this.#privateKey.buffer.detached }\r
- get isUnlocked () { return !this.isLocked }\r
get publicKey () { return bytes.toHex(this.#publicKey) }\r
- get privateKey () { return bytes.toHex(this.#privateKey) }\r
\r
get balance () { return this.#balance }\r
get frontier () { return this.#frontier }\r
- get index () { return this.#index }\r
get receivable () { return this.#receivable }\r
get representative () { return this.#representative }\r
get weight () { return this.#weight }\r
if (v instanceof Account) {\r
this.#representative = v\r
} else if (typeof v === 'string') {\r
- this.#representative = Account.fromAddress(v)\r
+ this.#representative = Account.import(v)\r
} else {\r
throw new TypeError(`Invalid argument for account representative: ${v}`)\r
}\r
}\r
set weight (v) { this.#weight = v ? BigInt(v) : undefined }\r
\r
- constructor (address: string, publicKey: Uint8Array<ArrayBuffer>, index?: number) {\r
+ constructor (address: string, publicKey: Uint8Array<ArrayBuffer>) {\r
if (!Account.#isInternal) {\r
throw new Error(`Account cannot be instantiated directly. Use factory methods instead.`)\r
}\r
- if (index !== undefined && typeof index !== 'number') {\r
- throw new TypeError(`Invalid index ${index} when creating Account ${address}`)\r
- }\r
this.#address = address\r
.replace(PREFIX, '')\r
.replace(PREFIX_LEGACY, '')\r
- this.#index = index\r
this.#publicKey = publicKey\r
- this.#privateKey = new Uint8Array(0)\r
- bytes.erase(this.#privateKey)\r
}\r
\r
/**\r
* allow garbage collection.\r
*/\r
async destroy (): Promise<void> {\r
- bytes.erase(this.#privateKey)\r
await SafeWorker.add({\r
+ store: 'Account',\r
method: 'destroy',\r
name: this.#publicKey\r
})\r
- this.#index = undefined\r
this.#frontier = undefined\r
this.#balance = undefined\r
this.#receivable = undefined\r
this.#weight = undefined\r
}\r
\r
+\r
+ /**\r
+ * Instantiates an Account object from its Nano address.\r
+ *\r
+ * @param {string} addresses - Address of the account\r
+ * @returns {Account} The instantiated Account object\r
+ */\r
+ static import (address: string): Account\r
+ /**\r
+ * Instantiates Account objects from their Nano addresses.\r
+ *\r
+ * @param {string[]} addresses - Addresses of the accounts\r
+ * @returns {Account[]} The instantiated Account objects\r
+ */\r
+ static import (addresses: string[]): Account[]\r
+ /**\r
+ * Instantiates an Account object from its public key. It is unable to sign\r
+ * blocks or messages since it has no private key.\r
+ *\r
+ * @param {string} publicKey - Public key of the account\r
+ * @returns {Account} The instantiated Account object\r
+ */\r
+ static import (publicKey: string): Account\r
+ /**\r
+ * Instantiates an Account object from its public key. It is unable to sign\r
+ * blocks or messages since it has no private key.\r
+ *\r
+ * @param {Uint8Array} publicKey - Public key of the account\r
+ * @returns {Account} The instantiated Account object\r
+ */\r
+ static import (publicKey: Uint8Array<ArrayBuffer>): Account\r
+ /**\r
+ * Instantiates Account objects from their public keys. They are unable to sign\r
+ * blocks or messages since they have no private key.\r
+ *\r
+ * @param {string[]} publicKeys - Public keys of the accounts\r
+ * @returns {Account[]} The instantiated Account objects\r
+ */\r
+ static import (publicKeys: string[]): Account[]\r
+ /**\r
+ * Instantiates Account objects from their public keys. They are unable to sign\r
+ * blocks or messages since they have no private key.\r
+ *\r
+ * @param {Uint8Array[]} publicKeys - Public keys of the accounts\r
+ * @returns {Account[]} The instantiated Account objects\r
+ */\r
+ static import (publicKeys: Uint8Array<ArrayBuffer>[]): Account[]\r
+ /**\r
+ * Instantiates an Account object from its private key which is then encrypted\r
+ * and stored in IndexedDB. The corresponding public key will automatically be\r
+ * derived and saved.\r
+ *\r
+ * @param {string} privateKey - Private key of the account\r
+ * @param {(string|Uint8Array)} password - Used to encrypt the private key\r
+ * @returns {Account} A new Account object\r
+ */\r
+ static async import (privateKey: string, password: string | Uint8Array<ArrayBuffer>): Promise<Account>\r
+ /**\r
+ * Instantiates an Account object from its private key which is then encrypted\r
+ * and stored in IndexedDB. The corresponding public key will automatically be\r
+ * derived and saved.\r
+ *\r
+ * @param {Uint8Array} privateKey - Private key of the account\r
+ * @param {(string|Uint8Array)} password - Used to encrypt the private key\r
+ * @returns {Account} A new Account object\r
+ */\r
+ static async import (privateKey: Uint8Array<ArrayBuffer>, password: string | Uint8Array<ArrayBuffer>): Promise<Account>\r
+ /**\r
+ * Instantiates Account objects from their private keys which are then\r
+ * encrypted and stored in IndexedDB. The corresponding public keys will\r
+ * automatically be derived and saved.\r
+ *\r
+ * @param {string[]} privateKeys - Private keys of the account\r
+ * @param {(string|Uint8Array)} password - Used to encrypt the private keys\r
+ * @returns {Account[]} The instantiated Account objects\r
+ */\r
+ static async import (privateKeys: string[], password: string | Uint8Array<ArrayBuffer>): Promise<Account[]>\r
+ /**\r
+ * Instantiates Account objects from their private keys which are then\r
+ * encrypted and stored in IndexedDB. The corresponding public keys will\r
+ * automatically be derived and saved.\r
+ *\r
+ * @param {Uint8Array[]} privateKeys - Private keys of the account\r
+ * @param {(string|Uint8Array)} password - Used to encrypt the private keys\r
+ * @returns {Account[]} The instantiated Account objects\r
+ */\r
+ static async import (privateKeys: Uint8Array<ArrayBuffer>[], password: string | Uint8Array<ArrayBuffer>): Promise<Account[]>\r
+ static import (input: string | string[] | Uint8Array<ArrayBuffer> | Uint8Array<ArrayBuffer>[], password?: string | Uint8Array<ArrayBuffer>): Account | Account[] | Promise<Account | Account[]> {\r
+ if (Array.isArray(input)) {\r
+ if (password != null) {\r
+ return new Promise((resolve, reject): void => {\r
+ this.#fromPrivateKeys(input, password)\r
+ .then(r => resolve(r))\r
+ .catch(e => reject(e))\r
+ })\r
+ }\r
+ if (input[0] instanceof Uint8Array || /^[A-Fa-f0-9]{64}$/.test(input[0])) {\r
+ return this.#fromPublicKeys(input)\r
+ }\r
+ return this.#fromAddresses(input as string[])\r
+ } else {\r
+ if (password != null) {\r
+ return new Promise((resolve, reject): void => {\r
+ this.#fromPrivateKeys([input] as string[], password)\r
+ .then(r => resolve(r[0]))\r
+ .catch(e => reject(e))\r
+ })\r
+ }\r
+ if (input instanceof Uint8Array || /^[A-Fa-f0-9]{64}$/.test(input)) {\r
+ return this.#fromPublicKeys([input] as string[])[0]\r
+ }\r
+ return this.#fromAddresses([input] as string[])[0]\r
+ }\r
+ }\r
+\r
/**\r
* Instantiates an Account object from its Nano address.\r
*\r
* @param {string} address - Address of the account\r
- * @param {number} [index] - Account number used when deriving the address\r
* @returns {Account} The instantiated Account object\r
*/\r
- static fromAddress (address: string, index?: number): Account {\r
- this.#isInternal = true\r
- this.validate(address)\r
- const publicKey = this.#addressToKey(address)\r
- return new this(address, publicKey, index)\r
+ static #fromAddresses (addresses: string[]): Account[] {\r
+ const accounts: Account[] = []\r
+ for (let address of addresses) {\r
+ this.#isInternal = true\r
+ this.validate(address)\r
+ const publicKey = this.#addressToKey(address)\r
+ accounts.push(new this(address, publicKey))\r
+ }\r
+ return accounts\r
}\r
\r
/**\r
* blocks or messages since it has no private key.\r
*\r
* @param {(string|Uint8Array)} publicKey - Public key of the account\r
- * @param {number} [index] - Account number used when deriving the key\r
* @returns {Account} The instantiated Account object\r
*/\r
- static fromPublicKey (publicKey: string | Uint8Array<ArrayBuffer>, index?: number): Account {\r
- this.#validateKey(publicKey)\r
- if (typeof publicKey === 'string') publicKey = hex.toBytes(publicKey)\r
- const address = this.#keyToAddress(publicKey)\r
- this.#isInternal = true\r
- return new this(address, publicKey, index)\r
+ static #fromPublicKeys (publicKeys: string[] | Uint8Array<ArrayBuffer>[]): Account[] {\r
+ const accounts: Account[] = []\r
+ for (let publicKey of publicKeys) {\r
+ this.#validateKey(publicKey)\r
+ if (typeof publicKey === 'string') publicKey = hex.toBytes(publicKey)\r
+ const address = this.#keyToAddress(publicKey)\r
+ this.#isInternal = true\r
+ accounts.push(new this(address, publicKey))\r
+ }\r
+ return accounts\r
}\r
\r
/**\r
* and stored in IndexedDB. The corresponding public key will automatically be\r
* derived and saved.\r
*\r
- * @param {(string|Uint8Array)} privateKey - Private key of the account\r
+ * @param {(string|Uint8Array)} privateKeys - Private key of the account\r
* @param {number} [index] - Account number used when deriving the key\r
* @returns {Account} A new Account object\r
*/\r
- static async fromPrivateKey (password: string | Uint8Array<ArrayBuffer>, privateKey: string | Uint8Array<ArrayBuffer>, index?: number): Promise<Account> {\r
+ static async #fromPrivateKeys (privateKeys: string[] | Uint8Array<ArrayBuffer>[], password: string | Uint8Array<ArrayBuffer>): Promise<Account[]> {\r
if (typeof password === 'string') password = utf8.toBytes(password)\r
if (password == null || !(password instanceof Uint8Array)) {\r
throw new Error('Invalid password when importing Account')\r
}\r
- this.#validateKey(privateKey)\r
- if (typeof privateKey === 'string') privateKey = hex.toBytes(privateKey)\r
- let publicKey: string\r
- try {\r
- const headers = {\r
- method: 'convert'\r
- }\r
- const data = {\r
- privateKey: new Uint8Array(privateKey).buffer\r
+\r
+ const keypairs: Data = {}\r
+ for (let privateKey of privateKeys) {\r
+ this.#validateKey(privateKey)\r
+ if (typeof privateKey === 'string') privateKey = hex.toBytes(privateKey)\r
+ let publicKey: string\r
+ try {\r
+ const headers = {\r
+ method: 'convert'\r
+ }\r
+ const data = {\r
+ privateKey: new Uint8Array(privateKey).buffer\r
+ }\r
+ publicKey = await NanoNaClWorker.add(headers, data)\r
+ keypairs[publicKey] = privateKey.buffer\r
+ } catch (err) {\r
+ throw new Error(`Failed to derive public key from private key`, { cause: err })\r
}\r
- publicKey = await NanoNaClWorker.add(headers, data)\r
- } catch (err) {\r
- throw new Error(`Failed to derive public key from private key`, { cause: err })\r
}\r
\r
- const self = await this.fromPublicKey(publicKey, index)\r
+ const accounts = await this.#fromPublicKeys(Object.keys(keypairs))\r
try {\r
const headers = {\r
+ store: 'Account',\r
method: 'set',\r
- name: publicKey\r
- }\r
- const data = {\r
- password: password.buffer,\r
- id: hex.toBytes(publicKey).buffer,\r
- privateKey: privateKey.buffer\r
+ password: password.buffer\r
}\r
- const isLocked = await SafeWorker.add(headers, data)\r
+ const isLocked = await SafeWorker.add(headers, keypairs)\r
if (!isLocked) {\r
throw null\r
}\r
- return self\r
+ return accounts\r
} catch (err) {\r
- throw new Error(`Failed to lock Account ${self.address}`, { cause: err })\r
+ throw new Error(`Failed to lock Accounts`, { cause: err })\r
} finally {\r
bytes.erase(password)\r
}\r
this.#balance = BigInt(balance)\r
this.#frontier = frontier\r
this.#receivable = BigInt(receivable)\r
- this.#representative = Account.fromAddress(representative)\r
+ this.#representative = await Account.import(representative)\r
this.#weight = BigInt(weight)\r
}\r
\r
* @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - The block data to be hashed and signed\r
* @returns {Promise<string>} Hexadecimal-formatted 64-byte signature\r
*/\r
- async sign (password: string | Uint8Array<ArrayBuffer>, block: ChangeBlock | ReceiveBlock | SendBlock): Promise<string> {\r
+ async sign (block: ChangeBlock | ReceiveBlock | SendBlock, password: string | Uint8Array<ArrayBuffer>): Promise<string> {\r
const privateKey = await this.exportPrivateKey(password)\r
try {\r
const headers = {\r
}\r
try {\r
const headers = {\r
+ store: 'Account',\r
method: 'get',\r
- name: this.publicKey\r
- }\r
- const data = {\r
+ name: this.publicKey,\r
password: password.buffer\r
}\r
- const response = await SafeWorker.add(headers, data)\r
- let { id, privateKey } = response\r
- if (id == null) {\r
- throw null\r
- }\r
- id = bytes.toHex(new Uint8Array(id))\r
- if (id !== this.publicKey) {\r
- throw null\r
- }\r
- const sk = new Uint8Array(privateKey as ArrayBuffer)\r
+ const response = await SafeWorker.add(headers)\r
+ const privateKey = new Uint8Array(response[this.publicKey] as ArrayBuffer)\r
return format === 'hex'\r
- ? bytes.toHex(sk)\r
- : sk\r
+ ? bytes.toHex(privateKey)\r
+ : privateKey\r
} catch (err) {\r
throw new Error(`Failed to export private key for Account ${this.address}`, { cause: err })\r
} finally {\r
if (account instanceof Account) {
this.account = account
} else if (typeof account === 'string') {
- this.account = Account.fromAddress(account)
+ this.account = Account.import(account)
} else {
throw new TypeError('Invalid account')
}
} else {
try {
const account = (typeof input === 'string')
- ? await Account.fromPrivateKey('', input)
+ ? await Account.import(input, '')
: this.account
- this.signature = await account.sign('', this)
+ this.signature = await account.sign(this, '')
} catch (err) {
throw new Error(`Failed to sign block`, { cause: err })
}
* @returns {boolean} True if block was signed by the matching private key
*/
async verify (key?: string): Promise<boolean> {
+ debugger
key ??= this.account.publicKey
if (!key) {
throw new Error('Provide a key for block signature verification.')
frontier: string,
work?: string
) {
- if (typeof sender === 'string') sender = Account.fromAddress(sender)
- if (typeof representative === 'string') representative = Account.fromAddress(representative)
+ if (typeof sender === 'string') sender = Account.import(sender)
+ if (typeof representative === 'string') representative = Account.import(representative)
super(sender)
this.previous = frontier
this.representative = representative
- this.link = Account.fromAddress(recipient).publicKey
+ this.link = Account.import(recipient).publicKey
this.work = work ?? ''
const bigBalance = BigInt(balance)
frontier?: string,
work?: string
) {
- if (typeof recipient === 'string') recipient = Account.fromAddress(recipient)
- if (typeof representative === 'string') representative = Account.fromAddress(representative)
+ if (typeof recipient === 'string') recipient = Account.import(recipient)
+ if (typeof representative === 'string') representative = Account.import(representative)
super(recipient)
this.previous = frontier ?? recipient.publicKey
this.representative = representative
previous: string
representative: Account
balance: bigint
- link: string = Account.fromAddress(BURN_ADDRESS).publicKey
+ link: string = Account.import(BURN_ADDRESS).publicKey
signature?: string
work?: string
frontier: string,
work?: string
) {
- if (typeof account === 'string') account = Account.fromAddress(account)
- if (typeof representative === 'string') representative = Account.fromAddress(representative)
+ if (typeof account === 'string') account = Account.import(account)
+ if (typeof representative === 'string') representative = Account.import(representative)
super(account)
this.previous = frontier
this.representative = representative
.replaceAll('<', '\\u003c')
.replaceAll('>', '\\u003d')
.replaceAll('\\', '\\u005c')
- const account = Account.fromAddress(address)
+ const account = Account.import(address)
const nameResult = this.#entries.find(e => e.name === name)
const accountResult = this.#entries.find(e => e.account.address === address)
if (!accountResult) {
const blockQueue: Promise<void>[] = []
const results: { status: 'success' | 'error', address: string, message: string }[] = []
- const recipientAccount = Account.fromAddress(recipient)
+ const recipientAccount = Account.import(recipient)
const accounts = await wallet.refresh(rpc, from, to)
for (const account of accounts) {
if (account.representative?.address && account.frontier) {
import { Account, AccountList } from '#src/lib/account.js'\r
import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
import { ADDRESS_GAP } from '#src/lib/constants.js'\r
-import { bytes, utf8 } from '#src/lib/convert.js'\r
+import { bytes, hex, utf8 } from '#src/lib/convert.js'\r
import { Entropy } from '#src/lib/entropy.js'\r
import { Rpc } from '#src/lib/rpc.js'\r
import { Data, KeyPair } from '#types'\r
get id () { return `libnemo_${this.#id.hex}` }\r
get isLocked () { return this.#locked }\r
get isUnlocked () { return !this.#locked }\r
- get mnemonic () { return this.#m instanceof Bip39Mnemonic ? this.#m.phrase : '' }\r
+ get mnemonic () { return this.#m instanceof Bip39Mnemonic ? this.#m.phrase : null }\r
get seed () { return bytes.toHex(this.#s) }\r
\r
constructor (id: Entropy, seed?: Uint8Array<ArrayBuffer>, mnemonic?: Bip39Mnemonic) {\r
const { privateKey, publicKey, index } = keypair\r
if (index == null) throw new RangeError('Account keys derived but index missing')\r
if (privateKey != null) {\r
- output[index] = await Account.fromPrivateKey(this.seed, privateKey, index)\r
+ output[index] = await Account.import(privateKey, this.seed)\r
} else if (publicKey != null) {\r
- output[index] = await Account.fromPublicKey(publicKey, index)\r
+ output[index] = await Account.import(publicKey)\r
} else {\r
throw new RangeError('Account keys missing')\r
}\r
this.#m = null\r
bytes.erase(this.#s)\r
await SafeWorker.add({\r
+ store: 'Wallet',\r
method: 'destroy',\r
name: this.id\r
})\r
throw new Error('Failed to lock wallet')\r
}\r
try {\r
- // const promises = []\r
- // for (const account of this.#accounts) {\r
- // promises.push(account.lock(this.seed))\r
- // }\r
- // await Promise.all(promises)\r
+ const serialized = JSON.stringify({\r
+ id: this.id,\r
+ mnemonic: this.mnemonic,\r
+ seed: this.seed\r
+ })\r
+ const encoded = utf8.toBytes(serialized)\r
const headers = {\r
+ store: 'Wallet',\r
method: 'set',\r
- name: this.id\r
+ password: new Uint8Array(password).buffer\r
}\r
const data: Data = {\r
- password: new Uint8Array(password).buffer,\r
- id: new Uint8Array(this.#id.bytes).buffer,\r
- seed: this.#s.buffer\r
- }\r
- if (this.#m != null) {\r
- data.mnemonic = utf8.toBytes(this.#m?.phrase ?? '').buffer\r
+ [this.id]: encoded.buffer\r
}\r
const success = await SafeWorker.add(headers, data)\r
if (!success) {\r
} finally {\r
bytes.erase(password)\r
}\r
+ bytes.erase(this.#s)\r
this.#m = null\r
this.#locked = true\r
return true\r
}\r
try {\r
const headers = {\r
+ store: 'Wallet',\r
method: 'get',\r
- name: this.id\r
- }\r
- const data = {\r
+ name: this.id,\r
password: new Uint8Array(password).buffer\r
}\r
- const response = await SafeWorker.add(headers, data)\r
- let { id, mnemonic, seed } = response\r
+ const response = await SafeWorker.add(headers)\r
+ const decoded = bytes.toUtf8(response[this.id])\r
+ const deserialized = JSON.parse(decoded)\r
+ let { id, mnemonic, seed } = deserialized\r
if (id == null) {\r
- throw null\r
+ throw new Error('ID is null')\r
}\r
- id = await Entropy.import(id as ArrayBuffer)\r
+ id = await Entropy.import(id.replace('libnemo_', ''))\r
if (id.hex !== this.#id.hex) {\r
- throw null\r
+ throw new Error('ID does not match')\r
}\r
if (mnemonic != null) {\r
- this.#m = await Bip39Mnemonic.fromPhrase(bytes.toUtf8(mnemonic))\r
+ this.#m = await Bip39Mnemonic.fromPhrase(mnemonic)\r
mnemonic = null\r
}\r
if (seed != null) {\r
- this.#s = new Uint8Array(seed as ArrayBuffer)\r
+ this.#s = hex.toBytes(seed)\r
seed = null\r
}\r
- // const promises = []\r
- // for (const account of this.#accounts) {\r
- // promises.push(account.exportPrivateKey(this.seed))\r
- // }\r
- // await Promise.all(promises)\r
} catch (err) {\r
throw new Error('Failed to unlock wallet')\r
} finally {\r
for (const key of Object.keys(errors ?? {})) {\r
const value = errors[key]\r
if (value === 'Account not found') {\r
- return Account.fromAddress(key)\r
+ return Account.import(key)\r
}\r
}\r
return await this.unopened(rpc, batchSize, from + batchSize)\r
const pk = await this.ckd(seed, coin, i)
privateKeys[i] = pk
} catch (err) {
- console.log(err)
+ console.log('BIP-44 error')
}
}
return privateKeys
this.#url = URL.createObjectURL(new Blob([worker], { type: 'text/javascript' }))
this.#worker = new Worker(this.#url, { type: 'module' })
this.#worker.addEventListener('message', message => {
- let result = message.data
- this.#report(result)
+ this.#report(message.data)
})
Queue.#instances.push(this)
}
const { id, headers, data, reject } = this.#job
try {
const buffers: ArrayBuffer[] = []
+ if (headers != null) {
+ for (let h of Object.keys(headers)) {
+ if (headers[h] instanceof ArrayBuffer) {
+ buffers.push(headers[h])
+ }
+ }
+ }
if (data != null) {
for (let d of Object.keys(data)) {
buffers.push(data[d])
*/
export class Safe extends WorkerInterface {
static DB_NAME = 'libnemo'
- static STORE_NAME = 'Safe'
+ static DB_STORES = ['Wallet', 'Account']
static ERR_MSG = 'Failed to store item in Safe'
- static #decoder: TextDecoder = new TextDecoder()
- static #encoder: TextEncoder = new TextEncoder()
static #storage: IDBDatabase
static {
}
static async work (headers: Headers, data: Data): Promise<any> {
+ const { method, name, password, store } = headers
this.#storage = await this.#open(this.DB_NAME)
- const { method, name } = headers
let result
try {
switch (method) {
case 'set': {
- result = await this.set(name, data)
+ result = await this.set(store, password, data)
break
}
case 'get': {
- result = await this.get(name, data)
+ result = await this.get(store, password, name)
break
}
case 'destroy': {
- result = await this.destroy(name)
+ result = await this.destroy(store, name)
break
}
default: {
result = `unknown Safe method ${method}`
}
}
- } catch {
+ } catch (err) {
result = false
}
return result
/**
* Removes data from the Safe without decrypting.
*/
- static async destroy (name: string): Promise<boolean> {
+ static async destroy (store: string, name: string): Promise<boolean> {
try {
- return await this.#delete(name)
+ return await this.#delete(store, name)
} catch {
throw new Error(this.ERR_MSG)
}
/**
* Encrypts data with a password byte array and stores it in the Safe.
*/
- static async set (name: string, data: Data): Promise<boolean> {
- const { password } = data
- delete data.password
-
+ static async set (store: string, password: ArrayBuffer, data: Data): Promise<boolean> {
try {
const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
- if (this.#isInvalid(name, derivationKey, data)) {
+ if (this.#isInvalid(store, derivationKey, data)) {
throw new Error('Failed to import key')
}
- const base32: { [key: string]: string } = {}
- for (const d of Object.keys(data)) {
- base32[d] = bytes.toBase32(new Uint8Array(data[d]))
- }
- const serialized = JSON.stringify(base32)
- const encoded = this.#encoder.encode(serialized)
-
- const salt = await Entropy.create()
- const derivationAlgorithm: Pbkdf2Params = {
- name: 'PBKDF2',
- hash: 'SHA-512',
- salt: salt.bytes,
- iterations: 210000
- }
- const derivedKeyType: AesKeyGenParams = {
- name: 'AES-GCM',
- length: 256
+ const records: SafeRecord[] = []
+ for (const label of Object.keys(data)) {
+ const salt = await Entropy.create()
+ const derivationAlgorithm: Pbkdf2Params = {
+ name: 'PBKDF2',
+ hash: 'SHA-512',
+ salt: salt.bytes,
+ iterations: 210000
+ }
+ const derivedKeyType: AesKeyGenParams = {
+ name: 'AES-GCM',
+ length: 256
+ }
+ const encryptionKey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, ['encrypt'])
+
+ const iv = await Entropy.create()
+ const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, encryptionKey, data[label])
+ const record: SafeRecord = {
+ iv: iv.hex,
+ salt: salt.hex,
+ label,
+ encrypted
+ }
+ records.push(record)
}
- const encryptionKey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, ['encrypt'])
- const iv = await Entropy.create()
- const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, encryptionKey, encoded)
- const record: SafeRecord = {
- iv: iv.hex,
- salt: salt.hex,
- encrypted
- }
- return await this.#put(record, name)
+ return await this.#put(records, store)
} catch (err) {
throw new Error(this.ERR_MSG)
} finally {
/**
* Retrieves data from the Safe and decrypts it with a password byte array.
*/
- static async get (name: string, data: Data): Promise<Data | null> {
- const { password } = data
- delete data.password
-
+ static async get (store: string, password: ArrayBuffer, name: string): Promise<Data | null> {
try {
const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
- if (this.#isInvalid(name, derivationKey)) {
+ if (this.#isInvalid(store, derivationKey)) {
throw new Error('Failed to import key')
}
- const record: SafeRecord = await this.#get(name)
+ const record: SafeRecord = await this.#get(name, store)
if (record == null) {
throw new Error('Failed to find record')
}
- const { encrypted } = record
+ const { label, encrypted } = record
const salt = await Entropy.import(record.salt)
const derivationAlgorithm: Pbkdf2Params = {
const iv = await Entropy.import(record.iv)
const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, decryptionKey, encrypted)
- const decoded = this.#decoder.decode(decrypted)
- const deserialized: { [key: string]: string } = JSON.parse(decoded)
- const bytes: Data = {}
- for (const d of Object.keys(deserialized)) {
- bytes[d] = new Uint8Array(base32.toBytes(deserialized[d])).buffer
- }
- await this.destroy(name)
- return bytes
+ await this.destroy(store, name)
+ return { [label]: decrypted }
} catch (err) {
return null
} finally {
return false
}
- static async #exists (name: string): Promise<boolean> {
- return await this.#get(name) !== undefined
- }
-
- static async #delete (name: string): Promise<boolean> {
- try {
- const result = await this.#transact<undefined>('readwrite', db => db.delete(name))
- return !(result || await this.#exists(name))
- } catch {
- throw new Error(this.ERR_MSG)
- }
+ static async #delete (store: string, name: string): Promise<boolean> {
+ const transaction = this.#storage.transaction(store, 'readwrite')
+ const db = transaction.objectStore(store)
+ return new Promise((resolve, reject) => {
+ const request = db.delete(name)
+ request.onsuccess = (event) => {
+ resolve((event.target as IDBRequest).result)
+ }
+ request.onerror = (event) => {
+ console.error('Database error')
+ reject((event.target as IDBRequest).error)
+ }
+ })
}
- static async #get (name: string): Promise<SafeRecord> {
- try {
- return await this.#transact<SafeRecord>('readonly', db => db.get(name))
- } catch {
- throw new Error('Failed to get record')
- }
+ static async #get (name: string, store: string): Promise<SafeRecord> {
+ const transaction = this.#storage.transaction(store, 'readonly')
+ const db = transaction.objectStore(store)
+ return new Promise((resolve, reject) => {
+ const request = db.get(name)
+ request.onsuccess = (event) => {
+ resolve((event.target as IDBRequest).result)
+ }
+ request.onerror = (event) => {
+ console.error('Database error')
+ reject((event.target as IDBRequest).error)
+ }
+ })
}
static async #open (database: string): Promise<IDBDatabase> {
const request = indexedDB.open(database, 1)
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result
- if (!db.objectStoreNames.contains(this.STORE_NAME)) {
- db.createObjectStore(this.STORE_NAME)
+ for (const DB_STORE of this.DB_STORES) {
+ if (!db.objectStoreNames.contains(DB_STORE)) {
+ db.createObjectStore(DB_STORE)
+ }
}
}
request.onsuccess = (event) => {
})
}
- static async #put (record: SafeRecord, name: string): Promise<boolean> {
- const result = await this.#transact<typeof name>('readwrite', db => db.put(record, name))
- return await this.#exists(result)
- }
-
- static async #transact<T> (mode: IDBTransactionMode, method: (db: IDBObjectStore) => IDBRequest): Promise<T> {
- const db = this.#storage.transaction(this.STORE_NAME, mode).objectStore(this.STORE_NAME)
+ static async #put (records: SafeRecord[], store: string): Promise<boolean> {
+ const transaction = this.#storage.transaction(store, 'readwrite')
+ const db = transaction.objectStore(store)
return new Promise((resolve, reject) => {
- const request = method(db)
- request.onsuccess = (event) => {
- resolve((event.target as IDBRequest).result)
+ records.map(record => db.put(record, record.label))
+ transaction.oncomplete = (event) => {
+ resolve((event.target as IDBRequest).error == null)
}
- request.onerror = (event) => {
+ transaction.onerror = (event) => {
console.error('Database error')
reject((event.target as IDBRequest).error)
}
}
export type KeyPair = {
- publicKey?: string,
- privateKey?: string,
+ publicKey?: string
+ privateKey?: string
index?: number
}
export type SafeRecord = {
iv: string
salt: string
+ label: string
encrypted: ArrayBuffer
}
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.ok('id' in wallet)\r
- assert.ok(/libnemo_[A-Fa-f0-9]{32,64}/.test(wallet.id))\r
+ assert.ok(/^libnemo_[A-Fa-f0-9]{32,64}$/.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]{32,64}/.test(wallet.seed))\r
+ assert.ok(/^[A-Fa-f0-9]{128}$/.test(wallet.seed))\r
\r
await wallet.destroy()\r
})\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.ok('id' in wallet)\r
- assert.ok(/libnemo_[A-Fa-f0-9]{32,64}/.test(wallet.id))\r
+ assert.ok(/^libnemo_[A-Fa-f0-9]{32,64}$/.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]{32,64}/.test(wallet.seed))\r
+ assert.ok(/^[A-Fa-f0-9]{64}$/.test(wallet.seed))\r
\r
await wallet.destroy()\r
})\r
assert.equals(privateKey, NANO_TEST_VECTORS.PRIVATE_0)\r
assert.equals(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0)\r
assert.equals(account.address, NANO_TEST_VECTORS.ADDRESS_0)\r
- assert.equals(account.index, 0)\r
\r
const accounts = await wallet.accounts()\r
assert.equals(account, accounts[0])\r
await test('should derive high indexed accounts from the given seed', async () => {\r
const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
- const accounts = await wallet.accounts(0x70000000, 0x700000ff)\r
+ const accounts = await wallet.accounts(0x70000000, 0x7000000f)\r
\r
- assert.equals(accounts.length, 0x100)\r
- for (let i = 0x70000000; i < 0x700000ff; i++) {\r
+ assert.equals(accounts.length, 0x10)\r
+ for (let i = 0x70000000; i < 0x7000000f; i++) {\r
const a = accounts[i]\r
assert.exists(a)\r
assert.exists(a.address)\r
assert.exists(a.publicKey)\r
- assert.exists(a.index)\r
- assert.equals(a.index, i)\r
const privateKey = await a.exportPrivateKey(wallet.seed, 'hex')\r
assert.exists(privateKey)\r
}\r
assert.exists(a)\r
assert.exists(a.address)\r
assert.exists(a.publicKey)\r
- assert.exists(a.index)\r
const privateKey = await a.exportPrivateKey(wallet.seed, 'hex')\r
assert.exists(privateKey)\r
}\r
\r
- const highAccounts = await wallet.accounts(0x70000000, 0x700000ff)\r
+ const highAccounts = await wallet.accounts(0x70000000, 0x7000000f)\r
\r
- assert.equals(highAccounts.length, 0x100)\r
+ assert.equals(highAccounts.length, 0x10)\r
for (const a of highAccounts) {\r
assert.exists(a)\r
assert.exists(a.address)\r
assert.exists(a.publicKey)\r
- assert.exists(a.index)\r
const privateKey = await a.exportPrivateKey(wallet.seed, 'hex')\r
assert.exists(privateKey)\r
}\r
await test('nano.org BIP-44 test vector mnemonic', async () => {\r
const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
- const accounts = await wallet.accounts()\r
+ const account = await wallet.account()\r
\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.ok(accounts[0] instanceof Account)\r
+ assert.ok(account instanceof Account)\r
assert.equals(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC)\r
assert.equals(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED)\r
- assert.equals(accounts[0].privateKey, NANO_TEST_VECTORS.PRIVATE_0)\r
- assert.equals(accounts[0].publicKey, NANO_TEST_VECTORS.PUBLIC_0)\r
- assert.equals(accounts[0].address, NANO_TEST_VECTORS.ADDRESS_0)\r
+ assert.equals(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0)\r
+ assert.equals(account.address, NANO_TEST_VECTORS.ADDRESS_0)\r
+\r
+ const privateKey = await account.exportPrivateKey(wallet.seed, 'hex')\r
+ assert.equals(privateKey, NANO_TEST_VECTORS.PRIVATE_0)\r
\r
await wallet.destroy()\r
})\r
await test('nano.org BIP-44 test vector seed with no mnemonic', async () => {\r
const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
- const accounts = await wallet.accounts()\r
+ const account = await wallet.account()\r
\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.ok(accounts[0] instanceof Account)\r
- assert.equals(wallet.mnemonic, '')\r
+ assert.ok(account instanceof Account)\r
+ assert.nullish(wallet.mnemonic)\r
assert.equals(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED)\r
- assert.equals(accounts[0].privateKey, NANO_TEST_VECTORS.PRIVATE_0)\r
- assert.equals(accounts[0].publicKey, NANO_TEST_VECTORS.PUBLIC_0)\r
- assert.equals(accounts[0].address, NANO_TEST_VECTORS.ADDRESS_0)\r
+ assert.equals(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0)\r
+ assert.equals(account.address, NANO_TEST_VECTORS.ADDRESS_0)\r
+\r
+ const privateKey = await account.exportPrivateKey(wallet.seed, 'hex')\r
+ assert.equals(privateKey, NANO_TEST_VECTORS.PRIVATE_0)\r
\r
await wallet.destroy()\r
})\r
await test('Trezor-derived BIP-44 entropy for 12-word mnemonic', async () => {\r
const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.ENTROPY_0)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
- const accounts = await wallet.accounts()\r
- const account = accounts[0]\r
+ const account = await wallet.account()\r
\r
assert.equals(wallet.mnemonic, CUSTOM_TEST_VECTORS.MNEMONIC_0)\r
assert.equals(wallet.seed, CUSTOM_TEST_VECTORS.SEED_0)\r
- assert.equals(account.privateKey, CUSTOM_TEST_VECTORS.PRIVATE_0)\r
assert.equals(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_0)\r
assert.equals(account.address, CUSTOM_TEST_VECTORS.ADDRESS_0)\r
\r
+ const privateKey = await account.exportPrivateKey(wallet.seed, 'hex')\r
+ assert.equals(privateKey, CUSTOM_TEST_VECTORS.PRIVATE_0)\r
+\r
await wallet.destroy()\r
})\r
\r
await test('Trezor-derived BIP-44 entropy for 15-word mnemonic', async () => {\r
const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.ENTROPY_1)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
- const accounts = await wallet.accounts()\r
- const account = accounts[0]\r
+ const account = await wallet.account()\r
\r
assert.equals(wallet.mnemonic, CUSTOM_TEST_VECTORS.MNEMONIC_1)\r
assert.equals(wallet.seed, CUSTOM_TEST_VECTORS.SEED_1)\r
- assert.equals(account.privateKey, CUSTOM_TEST_VECTORS.PRIVATE_1)\r
assert.equals(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_1)\r
assert.equals(account.address, CUSTOM_TEST_VECTORS.ADDRESS_1)\r
\r
+ const privateKey = await account.exportPrivateKey(wallet.seed, 'hex')\r
+ assert.equals(privateKey, CUSTOM_TEST_VECTORS.PRIVATE_1)\r
+\r
await wallet.destroy()\r
})\r
\r
await test('Trezor-derived BIP-44 entropy for 18-word mnemonic', async () => {\r
const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.ENTROPY_2)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
- const accounts = await wallet.accounts()\r
- const account = accounts[0]\r
+ const account = await wallet.account()\r
\r
assert.equals(wallet.mnemonic, CUSTOM_TEST_VECTORS.MNEMONIC_2)\r
assert.equals(wallet.seed, CUSTOM_TEST_VECTORS.SEED_2)\r
- assert.equals(account.privateKey, CUSTOM_TEST_VECTORS.PRIVATE_2)\r
assert.equals(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_2)\r
assert.equals(account.address, CUSTOM_TEST_VECTORS.ADDRESS_2)\r
\r
+ const privateKey = await account.exportPrivateKey(wallet.seed, 'hex')\r
+ assert.equals(privateKey, CUSTOM_TEST_VECTORS.PRIVATE_2)\r
+\r
await wallet.destroy()\r
})\r
\r
await test('Trezor-derived BIP-44 entropy for 21-word mnemonic', async () => {\r
const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.ENTROPY_3)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
- const accounts = await wallet.accounts()\r
- const account = accounts[0]\r
+ const account = await wallet.account()\r
\r
assert.equals(wallet.mnemonic, CUSTOM_TEST_VECTORS.MNEMONIC_3)\r
assert.equals(wallet.seed, CUSTOM_TEST_VECTORS.SEED_3)\r
- assert.equals(account.privateKey, CUSTOM_TEST_VECTORS.PRIVATE_3)\r
assert.equals(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_3)\r
assert.equals(account.address, CUSTOM_TEST_VECTORS.ADDRESS_3)\r
\r
+ const privateKey = await account.exportPrivateKey(wallet.seed, 'hex')\r
+ assert.equals(privateKey, CUSTOM_TEST_VECTORS.PRIVATE_3)\r
+\r
await wallet.destroy()\r
})\r
\r
assert.equals(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_0)\r
assert.equals(wallet.seed, TREZOR_TEST_VECTORS.SEED_0.toUpperCase())\r
assert.equals(accounts.length, 4)\r
+\r
for (let i = 0; i < accounts.length; i++) {\r
assert.exists(accounts[i])\r
assert.exists(accounts[i].address)\r
assert.exists(accounts[i].publicKey)\r
- assert.exists(accounts[i].privateKey)\r
- assert.equals(accounts[i].index, i)\r
+ const privateKey = await accounts[i].exportPrivateKey(wallet.seed, 'hex')\r
+ assert.exists(privateKey)\r
}\r
\r
await wallet.destroy()\r
assert.exists(accounts[i])\r
assert.exists(accounts[i].address)\r
assert.exists(accounts[i].publicKey)\r
- assert.exists(accounts[i].privateKey)\r
- assert.equals(accounts[i].index, i)\r
+ const privateKey = await accounts[i].exportPrivateKey(wallet.seed, 'hex')\r
+ assert.exists(privateKey)\r
}\r
\r
await wallet.destroy()\r
assert.ok('seed' in wallet)\r
assert.equals(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
assert.equals(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1)\r
+\r
assert.ok(accounts[0] instanceof Account)\r
- assert.equals(accounts[0].index, 0)\r
- assert.equals(accounts[0].privateKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PRIVATE_0)\r
assert.equals(accounts[0].publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_0)\r
assert.equals(accounts[0].address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_0)\r
+ const privateKey0 = await accounts[0].exportPrivateKey(wallet.seed, 'hex')\r
+ assert.equals(privateKey0, TREZOR_TEST_VECTORS.BLAKE2B_1_PRIVATE_0)\r
+\r
assert.ok(accounts[1] instanceof Account)\r
- assert.equals(accounts[1].index, 1)\r
- assert.equals(accounts[1].privateKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PRIVATE_1)\r
assert.equals(accounts[1].publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_1)\r
assert.equals(accounts[1].address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_1)\r
+ const privateKey1 = await accounts[1].exportPrivateKey(wallet.seed, 'hex')\r
+ assert.equals(privateKey1, TREZOR_TEST_VECTORS.BLAKE2B_1_PRIVATE_1)\r
\r
await wallet.destroy()\r
})\r
await test('BLAKE2b seed creates identical wallet as its derived mnemonic', async () => {\r
const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
- const walletAccounts = await wallet.accounts()\r
- const walletAccount = walletAccounts[0]\r
+ const walletAccount = await wallet.account()\r
+ const walletAccountPrivateKey = await walletAccount.exportPrivateKey(wallet.seed, 'hex')\r
\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
\r
const imported = await Blake2bWallet.fromMnemonic(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_2)\r
await imported.unlock(TREZOR_TEST_VECTORS.PASSWORD)\r
- const importedAccounts = await imported.accounts()\r
- const importedAccount = importedAccounts[0]\r
+ const importedAccount = await imported.account()\r
+ const importedAccountPrivateKey = await importedAccount.exportPrivateKey(imported.seed, 'hex')\r
\r
assert.equals(imported.mnemonic, wallet.mnemonic)\r
assert.equals(imported.seed, wallet.seed)\r
- assert.equals(importedAccount.privateKey, walletAccount.privateKey)\r
assert.equals(importedAccount.publicKey, walletAccount.publicKey)\r
+ assert.equals(importedAccountPrivateKey, walletAccountPrivateKey)\r
\r
await wallet.destroy()\r
})\r
await test('BLAKE2b mnemonic for maximum seed value', async () => {\r
const wallet = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
- const accounts = await wallet.accounts()\r
+ const account = await wallet.account()\r
\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.ok(accounts[0] instanceof Account)\r
+ assert.ok(account instanceof Account)\r
assert.equals(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_3)\r
assert.equals(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_3)\r
- assert.equals(accounts[0].index, 0)\r
- assert.equals(accounts[0].privateKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PRIVATE_0)\r
- assert.equals(accounts[0].publicKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PUBLIC_0)\r
- assert.equals(accounts[0].address, TREZOR_TEST_VECTORS.BLAKE2B_3_ADDRESS_0)\r
+ assert.equals(account.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PUBLIC_0)\r
+ assert.equals(account.address, TREZOR_TEST_VECTORS.BLAKE2B_3_ADDRESS_0)\r
+\r
+ const privateKey = await account.exportPrivateKey(wallet.seed, 'hex')\r
+ assert.equals(privateKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PRIVATE_0)\r
\r
await wallet.destroy()\r
})\r
\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.equals(wallet.mnemonic, '')\r
+ assert.nullish(wallet.mnemonic)\r
assert.equals(wallet.seed, '')\r
\r
const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.equals(wallet.mnemonic, '')\r
+ assert.nullish(wallet.mnemonic)\r
assert.equals(wallet.seed, '')\r
\r
const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
'0'
)
- assert.ok(/[A-Fa-f0-9]{64}/.test(openBlock.hash))
+ assert.ok(/^[A-Fa-f0-9]{64}$/.test(openBlock.hash))
assert.nullish(openBlock.signature)
assert.equals(openBlock.account.publicKey, account.publicKey)
const { status, hash, signature } = await wallet.sign(0, openBlock)
assert.equals(status, 'OK')
- assert.ok(/[A-Fa-f0-9]{64}/.test(hash))
- assert.ok(/[A-Fa-f0-9]{128}/.test(signature))
+ assert.ok(/^[A-Fa-f0-9]{64}$/.test(hash))
+ assert.ok(/^[A-Fa-f0-9]{128}$/.test(signature))
await openBlock.sign(0)
- assert.ok(/[A-Fa-f0-9]{128}/.test(openBlock.signature))
+ assert.ok(/^[A-Fa-f0-9]{128}$/.test(openBlock.signature))
assert.equals(signature, openBlock.signature)
})
openBlock.hash
)
- assert.ok(/[A-Fa-f0-9]{64}/.test(sendBlock.hash))
+ assert.ok(/^[A-Fa-f0-9]{64}$/.test(sendBlock.hash))
assert.nullish(sendBlock.signature)
assert.equals(sendBlock.account.publicKey, account.publicKey)
const { status, hash, signature } = await wallet.sign(0, sendBlock)
assert.equals(status, 'OK')
- assert.ok(/[A-Fa-f0-9]{64}/.test(hash))
- assert.ok(/[A-Fa-f0-9]{128}/.test(signature))
+ assert.ok(/^[A-Fa-f0-9]{64}$/.test(hash))
+ assert.ok(/^[A-Fa-f0-9]{128}$/.test(signature))
sendBlock.signature = signature
})
sendBlock.hash
)
- assert.ok(/[A-Fa-f0-9]{64}/.test(sendBlock.hash))
+ assert.ok(/^[A-Fa-f0-9]{64}$/.test(sendBlock.hash))
assert.nullish(receiveBlock.signature)
assert.equals(receiveBlock.account.publicKey, account.publicKey)
await receiveBlock.sign(0, sendBlock)
- assert.ok(/[A-Fa-f0-9]{128}/.test(receiveBlock.signature))
+ assert.ok(/^[A-Fa-f0-9]{128}$/.test(receiveBlock.signature))
})
// nonce signing is currently broken: https://github.com/LedgerHQ/app-nano/pull/14
const { status, signature } = await click('Click to sign nonce', wallet.sign(0, nonce))
assert.equals(status, 'OK')
- assert.OK(/[A-Fa-f0-9]{128}/.test(signature))
+ assert.OK(/^[A-Fa-f0-9]{128}$/.test(signature))
})
await test('destroy wallet', async () => {
\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.equals(wallet.mnemonic, '')\r
+ assert.nullish(wallet.mnemonic)\r
assert.equals(wallet.seed, '')\r
\r
const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
assert.ok(lockResult)\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.equals(wallet.mnemonic, '')\r
+ assert.nullish(wallet.mnemonic)\r
assert.equals(wallet.seed, '')\r
\r
const unlockResult = await wallet.unlock(new Uint8Array(key))\r
const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
const account = await wallet.account()\r
- const lockResult = await account.lock(NANO_TEST_VECTORS.PASSWORD)\r
+ const lockResult = await wallet.lock(NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.equals(lockResult, true)\r
- assert.ok(account.isLocked)\r
- assert.ok('privateKey' in account)\r
- assert.equals(account.privateKey, '')\r
\r
- const unlockResult = await account.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+ const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+ const privateKey = await account.exportPrivateKey(wallet.seed, 'hex')\r
\r
assert.equals(unlockResult, true)\r
- assert.ok(account.isUnlocked)\r
- assert.ok('privateKey' in account)\r
- assert.equals(account.privateKey, NANO_TEST_VECTORS.PRIVATE_0)\r
+ assert.equals(privateKey, NANO_TEST_VECTORS.PRIVATE_0)\r
\r
await wallet.destroy()\r
})\r
assert.equals(lockResult, true)\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC)\r
+ assert.nullish(wallet.mnemonic)\r
assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED)\r
\r
await wallet.destroy()\r
assert.equals(lockResult, true)\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC)\r
+ assert.nullish(wallet.mnemonic)\r
assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED)\r
\r
await wallet.destroy()\r
await assert.rejects(wallet.unlock(new Uint8Array(key)), { message: 'Failed to unlock wallet' })\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC)\r
+ assert.nullish(wallet.mnemonic)\r
assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED)\r
\r
await wallet.destroy()\r
await assert.rejects(wallet.unlock(), { message: 'Failed to unlock wallet' })\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC)\r
+ assert.nullish(wallet.mnemonic)\r
assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED)\r
\r
await wallet.destroy()\r
await assert.rejects(wallet.unlock(1), { message: 'Failed to unlock wallet' })\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC)\r
+ assert.nullish(wallet.mnemonic)\r
assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED)\r
\r
await wallet.destroy()\r
\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.equals(wallet.mnemonic, '')\r
+ assert.nullish(wallet.mnemonic)\r
assert.equals(wallet.seed, '')\r
\r
const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
assert.equals(lockResult, true)\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.equals(wallet.mnemonic, '')\r
+ assert.nullish(wallet.mnemonic)\r
assert.equals(wallet.seed, '')\r
\r
const unlockResult = await wallet.unlock(new Uint8Array(key))\r
const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
const account = await wallet.account()\r
- const lockResult = await account.lock(NANO_TEST_VECTORS.PASSWORD)\r
+ const lockResult = await wallet.lock(NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.equals(lockResult, true)\r
- assert.ok(account.isLocked)\r
- assert.ok('privateKey' in account)\r
- assert.equals(account.privateKey, '')\r
\r
- const unlockResult = await account.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+ const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+ const privateKey = await account.exportPrivateKey(wallet.seed, 'hex')\r
\r
assert.equals(unlockResult, true)\r
- assert.ok(account.isUnlocked)\r
- assert.ok('privateKey' in account)\r
- assert.equals(account.privateKey, TREZOR_TEST_VECTORS.BLAKE2B_PRIVATE_0)\r
+ assert.equals(privateKey, TREZOR_TEST_VECTORS.BLAKE2B_PRIVATE_0)\r
\r
await wallet.destroy()\r
})\r
await assert.rejects(wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD), { message: 'Failed to unlock wallet' })\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
+ assert.nullish(wallet.mnemonic)\r
assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1)\r
\r
await wallet.destroy()\r
assert.equals(lockResult, true)\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
+ assert.nullish(wallet.mnemonic)\r
assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1)\r
\r
await wallet.destroy()\r
await assert.rejects(wallet.unlock(new Uint8Array(key)), { message: 'Failed to unlock wallet' })\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
+ assert.nullish(wallet.mnemonic)\r
assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1)\r
\r
await wallet.destroy()\r
await assert.rejects(wallet.unlock(), { message: 'Failed to unlock wallet' })\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
+ assert.nullish(wallet.mnemonic)\r
assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1)\r
\r
await wallet.destroy()\r
await assert.rejects(wallet.unlock(1), { message: 'Failed to unlock wallet' })\r
assert.ok('mnemonic' in wallet)\r
assert.ok('seed' in wallet)\r
- assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
+ assert.nullish(wallet.mnemonic)\r
assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1)\r
\r
await wallet.destroy()\r
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
// SPDX-License-Identifier: GPL-3.0-or-later
-import './test.blake2b.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.sign-blocks.mjs'
+// import './test.blake2b.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.sign-blocks.mjs'
import './test.tools.mjs'
console.log('%cTESTING COMPLETE', 'color:orange;font-weight:bold')
const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
const account = await wallet.account()\r
+ const privateKey = await account.exportPrivateKey(wallet.seed)\r
const sendBlock = new SendBlock(\r
account.address,\r
'5618869000000000000000000000000',\r
'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou',\r
'92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D',\r
)\r
- await sendBlock.sign(account.privateKey ?? '')\r
+ await sendBlock.sign(privateKey)\r
const valid = await sendBlock.verify(account.publicKey)\r
assert.equals(valid, true)\r
\r
const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
const account = await wallet.account()\r
+ const privateKey = await account.exportPrivateKey(wallet.seed)\r
const sendBlock = new SendBlock(\r
account.address,\r
'5618869000000000000000000000000',\r
'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou',\r
'92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D',\r
)\r
- await sendBlock.sign(account.privateKey ?? '')\r
+ await sendBlock.sign(privateKey)\r
\r
- sendBlock.account = Account.fromAddress('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p')\r
+ sendBlock.account = Account.import('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p')\r
const valid = await sendBlock.verify(account.publicKey)\r
assert.equals(valid, false)\r
\r