import { ACCOUNT_KEY_BYTE_LENGTH, ACCOUNT_KEY_HEX_LENGTH, ALPHABET, PREFIX, PREFIX_LEGACY } from './constants'\r
import { base32, bytes, hex, obj, utf8 } from './convert'\r
import { Rpc } from './rpc'\r
-import { NanoNaClWorker, Queue, SafeWorker } from '#workers'\r
+import { NanoNaClWorker, SafeWorker } from '#workers'\r
\r
/**\r
* Represents a single Nano address and the associated public key. To include the\r
*/\r
export class Account {\r
static #isInternal: boolean = false\r
- static #poolSafe: Queue = new Queue(SafeWorker)\r
- static #poolNanoNaCl: Queue = new Queue(NanoNaClWorker)\r
\r
#address: string\r
#locked: boolean\r
* allow garbage collection.\r
*/\r
async destroy (): Promise<void> {\r
- this.#prv.fill(0)\r
- await Account.#poolSafe.add({\r
+ bytes.erase(this.#prv)\r
+ await SafeWorker.add({\r
method: 'destroy',\r
name: this.#pub\r
})\r
const data = {\r
privateKey: privateKey.buffer\r
}\r
- const result = await this.#poolNanoNaCl.add(headers, data)\r
- publicKey = result.publicKey[0]\r
+ const result = await NanoNaClWorker.add(headers, data)\r
+ publicKey = bytes.toHex(new Uint8Array(result.publicKey))\r
} catch (err) {\r
throw new Error(`Failed to derive public key from private key`, { cause: err })\r
}\r
privateKey: this.#prv.buffer,\r
password: password.buffer\r
}\r
- const response = await Account.#poolSafe.add(headers, data)\r
+ const response = await SafeWorker.add(headers, data)\r
const success = response?.result[0]\r
if (!success) {\r
throw null\r
console.error(`Failed to lock account ${this.address}`, err)\r
return false\r
} finally {\r
- password.fill(0)\r
+ bytes.erase(password)\r
}\r
- this.#prv.fill(0)\r
+ bytes.erase(this.#prv)\r
this.#locked = true\r
return true\r
}\r
const data = {\r
password: password.buffer\r
}\r
- const response = await Account.#poolSafe.add(headers, data)\r
+ const response = await SafeWorker.add(headers, data)\r
const { id, privateKey } = response?.result[0]\r
if (id == null || id !== this.#pub) {\r
throw null\r
console.error(`Failed to unlock account ${this.address}`, err)\r
return false\r
} finally {\r
- password.fill(0)\r
+ bytes.erase(password)\r
}\r
this.#locked = false\r
return true\r
import { BURN_ADDRESS, PREAMBLE, DIFFICULTY_RECEIVE, DIFFICULTY_SEND } from './constants'
import { dec, hex } from './convert'
import { Rpc } from './rpc'
-import { NanoNaClWorker, Queue } from '#workers'
+import { NanoNaClWorker } from '#workers'
/**
* Represents a block as defined by the Nano cryptocurrency protocol. The Block
* of three derived classes: SendBlock, ReceiveBlock, ChangeBlock.
*/
abstract class Block {
- static #poolNanoNaCl: Queue = new Queue(NanoNaClWorker)
-
account: Account
type: string = 'state'
abstract subtype: 'send' | 'receive' | 'change'
const data = {
privateKey: hex.toBytes(account.privateKey).buffer
}
- const result = await Block.#poolNanoNaCl.add(headers, data)
- this.signature = result.signature[0]
+ const result = await NanoNaClWorker.add(headers, data)
+ this.signature = result[0].signature
} catch (err) {
throw new Error(`Failed to sign block`, { cause: err })
}
signature: this.signature ?? '',
publicKey: key
}
- const result = await Block.#poolNanoNaCl.add(headers)
+ const result = await NanoNaClWorker.add(headers)
return result.isVerified[0]
} catch (err) {
throw new Error(`Failed to derive public key from private key`, { cause: err })
}\r
\r
export class bytes {\r
+ /**\r
+ * Writes zeroes to memory to erase bytes and then transfers the buffer to\r
+ * render it inaccessible to any process.\r
+ *\r
+ * @param bytes - Buffer or bytes to erase\r
+ */\r
+ static erase (bytes: ArrayBuffer | Uint8Array<ArrayBuffer>): void {\r
+ if (bytes instanceof ArrayBuffer) {\r
+ if (bytes.detached) return\r
+ bytes = new Uint8Array(bytes)\r
+ }\r
+ if (bytes.buffer.detached) return\r
+ bytes.fill(0)\r
+ bytes.buffer.transfer()\r
+ }\r
+\r
/**\r
* Convert a Uint8Array to an array of decimal byte values.\r
*\r
* @returns {Promise<boolean>} True if the signature was used to sign the data, else false
*/
async verify (name: string, signature: string, ...data: string[]): Promise<boolean> {
- const { Tools } = await import('./tools.js')
+ const { verify } = await import('./tools.js')
const entries = this.#entries.filter(e => e.name === name)
for (const entry of entries) {
const key = entry.account.publicKey
- const verified = await Tools.verify(key, signature, ...data)
+ const verified = await verify(key, signature, ...data)
if (verified) {
return true
}
import { Blake2b } from './blake2b'
import { SendBlock } from './block'
import { UNITS } from './constants'
-import { hex } from './convert'
+import { bytes, hex } from './convert'
import { Rpc } from './rpc'
import { Bip44Wallet, Blake2bWallet, LedgerWallet } from './wallets'
-import { NanoNaCl } from '#workers'
+import { NanoNaClWorker } from '#workers'
type SweepResult = {
status: "success" | "error"
/**
* Signs arbitrary strings with a private key using the Ed25519 signature scheme.
*
-* @param {string} key - Hexadecimal-formatted private key to use for signing
+* @param {(string|Uint8Array)} key - Hexadecimal-formatted private key to use for signing
* @param {...string} input - Data to be signed
* @returns {Promise<string>} Hexadecimal-formatted signature
*/
-export async function sign (key: string, ...input: string[]): Promise<string> {
- const account = await Account.fromPrivateKey(key)
- const data = hash(input)
- const signature = NanoNaCl.detached(
- hex.toBytes(data),
- hex.toBytes(`${account.privateKey}`))
+export async function sign (key: string | Uint8Array<ArrayBuffer>, ...input: string[]): Promise<string> {
+ if (typeof key === 'string') key = hex.toBytes(key)
+ let signature: string
+ try {
+ const headers = {
+ method: 'detached',
+ msg: hash(input)
+ }
+ const data = {
+ privateKey: key.buffer
+ }
+ const result = await NanoNaClWorker.add(headers, data)
+ signature = result.publicKey[0]
+ } catch (err) {
+ throw new Error(`Failed to sign message with private key`, { cause: err })
+ } finally {
+ bytes.erase(key)
+ }
+
return signature
}
/**
* Verifies the signature of arbitrary strings using a public key.
*
-* @param {string} key - Hexadecimal-formatted public key to use for verification
+* @param {(string|Uint8Array)} key - Hexadecimal-formatted public key to use for verification
* @param {string} signature - Hexadcimal-formatted signature
* @param {...string} input - Data to be verified
* @returns {Promise<boolean>} True if the data was signed by the public key's matching private key
*/
-export async function verify (key: string, signature: string, ...input: string[]): Promise<boolean> {
- const data = hash(input)
+export async function verify (key: string | Uint8Array<ArrayBuffer>, signature: string, ...input: string[]): Promise<boolean> {
+ if (typeof key === 'string') key = hex.toBytes(key)
+ let isVerified: boolean
try {
- return await NanoNaCl.verify(
- hex.toBytes(data),
- hex.toBytes(signature),
- hex.toBytes(key))
+ const headers = {
+ method: 'verify',
+ msg: hash(input),
+ signature
+ }
+ const data = {
+ privateKey: key.buffer
+ }
+ isVerified = await NanoNaClWorker.add(headers, data)
} catch (err) {
- console.error(err)
- return false
+ console.log(err)
+ isVerified = false
+ } finally {
+ bytes.erase(key)
}
+ return isVerified
}
-
-export const Tools = { convert, sign, sweep, verify }
import { SEED_LENGTH_BIP44 } from '#src/lib/constants.js'\r
import { hex, utf8 } from '#src/lib/convert.js'\r
import { Entropy } from '#src/lib/entropy.js'\r
-import { Bip44CkdWorker, Queue } from '#workers'\r
+import { Bip44CkdWorker } from '#workers'\r
\r
/**\r
* Hierarchical deterministic (HD) wallet created by using a source of entropy to\r
*/\r
export class Bip44Wallet extends Wallet {\r
static #isInternal: boolean = false\r
- static #poolBip44Ckd: Queue\r
\r
constructor (id: Entropy, seed: string, mnemonic?: Bip39Mnemonic) {\r
if (!Bip44Wallet.#isInternal) {\r
}\r
Bip44Wallet.#isInternal = false\r
super(id, hex.toBytes(seed), mnemonic)\r
- Bip44Wallet.#poolBip44Ckd ??= new Queue(Bip44CkdWorker)\r
}\r
\r
/**\r
indexes\r
}\r
const data = {\r
- seed: this.seed.buffer\r
+ seed: hex.toBytes(this.seed).buffer\r
}\r
- const privateKeys: KeyPair[] = await Bip44Wallet.#poolBip44Ckd.add(headers, data)\r
+ const privateKeys: KeyPair[] = await Bip44CkdWorker.add(headers, data)\r
for (let i = 0; i < privateKeys.length; i++) {\r
if (privateKeys[i].privateKey == null) {\r
throw new Error('Failed to derive private keys')\r
const results = []\r
for (const index of indexes) {\r
const indexHex = index.toString(16).padStart(8, '0').toUpperCase()\r
- const inputHex = `${bytes.toHex(this.seed)}${indexHex}`.padStart(72, '0')\r
+ const inputHex = `${this.seed}${indexHex}`.padStart(72, '0')\r
const inputBytes = hex.toBytes(inputHex)\r
const privateKey: string = new Blake2b(32).update(inputBytes).digest('hex')\r
results.push({ privateKey, index })\r
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\r
// SPDX-License-Identifier: GPL-3.0-or-later\r
\r
-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 { 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 { Queue, SafeWorker } from '#workers'\r
-\r
+export type { KeyPair } from './wallet'\r
+export { Wallet } from './wallet'\r
export { Bip44Wallet } from './bip44-wallet'\r
export { Blake2bWallet } from './blake2b-wallet'\r
export { LedgerWallet } from './ledger-wallet'\r
-\r
-export type KeyPair = {\r
- publicKey?: string,\r
- privateKey?: string,\r
- index?: number\r
-}\r
-\r
-/**\r
-* Represents a wallet containing numerous Nano accounts derived from a single\r
-* source, the form of which can vary based on the type of wallet. The Wallet\r
-* class itself is abstract and cannot be directly instantiated. Currently, three\r
-* types of wallets are supported, each as a derived class: Bip44Wallet,\r
-* Blake2bWallet, LedgerWallet.\r
-*/\r
-export abstract class Wallet {\r
- abstract ckd (index: number[]): Promise<KeyPair[]>\r
-\r
- static #poolSafe: Queue = new Queue(SafeWorker)\r
-\r
- #accounts: AccountList\r
- #id: Entropy\r
- #locked: boolean = true\r
- #m: Bip39Mnemonic | null\r
- #s: Uint8Array<ArrayBuffer>\r
-\r
- get id () { return 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 seed () { return this.#s }\r
-\r
- constructor (id: Entropy, seed?: Uint8Array<ArrayBuffer>, mnemonic?: Bip39Mnemonic) {\r
- if (this.constructor === Wallet) {\r
- throw new Error('Wallet is an abstract class and cannot be instantiated directly.')\r
- }\r
- this.#accounts = new AccountList()\r
- this.#id = id\r
- this.#m = mnemonic ?? null\r
- this.#s = seed ?? new Uint8Array(32)\r
- }\r
-\r
- /**\r
- * Retrieves an account from a wallet using its child key derivation function.\r
- * Defaults to the first account at index 0.\r
- *\r
- * ```\r
- * console.log(await wallet.account(5))\r
- * // outputs sixth account of the wallet\r
- * // {\r
- * // privateKey: <...>,\r
- * // index: 5\r
- * // }\r
- * ```\r
- *\r
- * @param {number} index - Wallet index of secret key. Default: 0\r
- * @returns {Account} Account derived at the specified wallet index\r
- */\r
- async account (index: number = 0): Promise<Account> {\r
- return (await this.accounts(index))[index]\r
- }\r
-\r
- /**\r
- * Retrieves accounts from a wallet using its child key derivation function.\r
- * Defaults to the first account at index 0.\r
- *\r
- * The returned object will have keys corresponding with the requested range\r
- * of account indexes. The value of each key will be the Account derived for\r
- * that index in the wallet.\r
- *\r
- * ```\r
- * console.log(await wallet.accounts(5))\r
- * // outputs sixth account of the wallet\r
- * // {\r
- * // 5: {\r
- * // privateKey: <...>,\r
- * // index: 5\r
- * // }\r
- * // }\r
- * ```\r
- *\r
- * @param {number} from - Start index of secret keys. Default: 0\r
- * @param {number} to - End index of secret keys. Default: `from`\r
- * @returns {AccountList} Object with keys of account indexes and values of the corresponding Accounts\r
- */\r
- async accounts (from: number = 0, to: number = from): Promise<AccountList> {\r
- if (from > to) {\r
- const swap = from\r
- from = to\r
- to = swap\r
- }\r
- const output = new AccountList()\r
- const indexes: number[] = []\r
- for (let i = from; i <= to; i++) {\r
- if (this.#accounts[i] == null) {\r
- indexes.push(i)\r
- } else {\r
- output[i] = this.#accounts[i]\r
- }\r
- }\r
- if (indexes.length > 0) {\r
- const keypairs = await this.ckd(indexes)\r
- for (const keypair of keypairs) {\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(privateKey, index)\r
- } else if (publicKey != null) {\r
- output[index] = await Account.fromPublicKey(publicKey, index)\r
- } else {\r
- throw new RangeError('Account keys missing')\r
- }\r
- this.#accounts[index] = output[index]\r
- }\r
- }\r
- return output\r
- }\r
-\r
- /**\r
- * Removes encrypted secrets in storage and releases variable references to\r
- * allow garbage collection.\r
- */\r
- async destroy (): Promise<void> {\r
- let i = 0\r
- for (const a in this.#accounts) {\r
- await this.#accounts[a].destroy()\r
- delete this.#accounts[a]\r
- i++\r
- }\r
- this.#m = null\r
- this.#s.fill(0)\r
- await Wallet.#poolSafe.add({\r
- method: 'destroy',\r
- name: this.id\r
- })\r
- }\r
-\r
- /**\r
- * Locks the wallet and all currently derived accounts with a password that\r
- * will be needed to unlock it later.\r
- *\r
- * @param {(string|Uint8Array)} password Used to lock the wallet\r
- * @returns True if successfully locked\r
- */\r
- async lock (password: string | Uint8Array<ArrayBuffer>): Promise<boolean> {\r
- if (typeof password === 'string') {\r
- password = utf8.toBytes(password)\r
- }\r
- if (password == null || !(password instanceof Uint8Array)) {\r
- throw new Error('Failed to unlock wallet')\r
- }\r
- try {\r
- const headers = {\r
- method: 'set',\r
- name: this.id,\r
- id: this.id,\r
- }\r
- const data = {\r
- password: password.buffer,\r
- phrase: utf8.toBytes(this.#m?.phrase ?? '').buffer,\r
- seed: this.#s.buffer\r
- }\r
- const response = await Wallet.#poolSafe.add(headers, data)\r
- const success = response?.result[0]\r
- if (!success) {\r
- throw null\r
- }\r
- const promises = []\r
- for (const account of this.#accounts) {\r
- promises.push(account.lock(password))\r
- }\r
- await Promise.all(promises)\r
- } catch (err) {\r
- throw new Error('Failed to lock wallet')\r
- } finally {\r
- password.fill(0)\r
- }\r
- this.#m = null\r
- this.#s.fill(0)\r
- this.#locked = true\r
- return true\r
- }\r
-\r
- /**\r
- * Refreshes wallet account balances, frontiers, and representatives from the\r
- * current state on the network.\r
- *\r
- * A successful response will set these properties on each account.\r
- *\r
- * @param {Rpc|string|URL} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks\r
- * @returns {Promise<Account[]>} Accounts with updated balances, frontiers, and representatives\r
- */\r
- async refresh (rpc: Rpc | string | URL, from: number = 0, to: number = from): Promise<AccountList> {\r
- if (typeof rpc === 'string' || rpc instanceof URL) {\r
- rpc = new Rpc(rpc)\r
- }\r
- if (!(rpc instanceof Rpc)) {\r
- throw new TypeError('RPC must be a valid node')\r
- }\r
- const accounts = await this.accounts(from, to)\r
- for (const a in accounts) {\r
- try {\r
- await accounts[a].refresh(rpc)\r
- } catch (err) {\r
- delete accounts[a]\r
- }\r
- }\r
- return accounts\r
- }\r
-\r
- /**\r
- * Unlocks the wallet using the same password as used prior to lock it.\r
- *\r
- * @param {(string|Uint8Array)} password Used previously to lock the wallet\r
- * @returns True if successfully unlocked\r
- */\r
- async unlock (password: string | Uint8Array<ArrayBuffer>): Promise<boolean> {\r
- if (typeof password === 'string') {\r
- password = utf8.toBytes(password)\r
- }\r
- if (password == null || !(password instanceof Uint8Array)) {\r
- throw new Error('Failed to unlock wallet')\r
- }\r
- try {\r
- const headers = {\r
- method: 'get',\r
- name: this.id\r
- }\r
- const data = {\r
- password: password.buffer\r
- }\r
- const response = await Wallet.#poolSafe.add(headers, data)\r
- let { id, mnemonic, seed } = response?.result[0]\r
- if (id == null || id !== this.id) {\r
- throw null\r
- }\r
- if (mnemonic != null) {\r
- this.#m = await Bip39Mnemonic.fromPhrase(mnemonic)\r
- mnemonic = null\r
- }\r
- if (seed != null) {\r
- this.#s.set(hex.toBytes(seed))\r
- seed = null\r
- }\r
- const promises = []\r
- for (const account of this.#accounts) {\r
- promises.push(account.unlock(password))\r
- }\r
- await Promise.all(promises)\r
- } catch (err) {\r
- throw new Error('Failed to unlock wallet')\r
- } finally {\r
- password.fill(0)\r
- }\r
- this.#locked = false\r
- return true\r
- }\r
-\r
- /**\r
- * Fetches the lowest-indexed unopened account from a wallet in sequential\r
- * order. An account is unopened if it has no frontier block.\r
- *\r
- * @param {Rpc|string|URL} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks\r
- * @param {number} batchSize - Number of accounts to fetch and check per RPC callout\r
- * @param {number} from - Account index from which to start the search\r
- * @returns {Promise<Account>} The lowest-indexed unopened account belonging to the wallet\r
- */\r
- async unopened (rpc: Rpc, batchSize: number = ADDRESS_GAP, from: number = 0): Promise<Account> {\r
- if (!Number.isSafeInteger(batchSize) || batchSize < 1) {\r
- throw new RangeError(`Invalid batch size ${batchSize}`)\r
- }\r
- const accounts = await this.accounts(from, from + batchSize - 1)\r
- const addresses = []\r
- for (const a in accounts) {\r
- addresses.push(accounts[a].address)\r
- }\r
- const data = {\r
- "accounts": addresses\r
- }\r
- const { errors } = await rpc.call('accounts_frontiers', data)\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
- }\r
- }\r
- return await this.unopened(rpc, batchSize, from + batchSize)\r
- }\r
-}\r
--- /dev/null
+// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\r
+// SPDX-License-Identifier: GPL-3.0-or-later\r
+\r
+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 { Entropy } from '#src/lib/entropy.js'\r
+import { Rpc } from '#src/lib/rpc.js'\r
+import { SafeWorker } from '#workers'\r
+\r
+export type KeyPair = {\r
+ publicKey?: string,\r
+ privateKey?: string,\r
+ index?: number\r
+}\r
+\r
+/**\r
+* Represents a wallet containing numerous Nano accounts derived from a single\r
+* source, the form of which can vary based on the type of wallet. The Wallet\r
+* class itself is abstract and cannot be directly instantiated. Currently, three\r
+* types of wallets are supported, each as a derived class: Bip44Wallet,\r
+* Blake2bWallet, LedgerWallet.\r
+*/\r
+export abstract class Wallet {\r
+ abstract ckd (index: number[]): Promise<KeyPair[]>\r
+\r
+ #accounts: AccountList\r
+ #id: Entropy\r
+ #locked: boolean = true\r
+ #m: Bip39Mnemonic | null\r
+ #s: Uint8Array<ArrayBuffer>\r
+\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 seed () { return bytes.toHex(this.#s) }\r
+\r
+ constructor (id: Entropy, seed?: Uint8Array<ArrayBuffer>, mnemonic?: Bip39Mnemonic) {\r
+ if (this.constructor === Wallet) {\r
+ throw new Error('Wallet is an abstract class and cannot be instantiated directly.')\r
+ }\r
+ this.#accounts = new AccountList()\r
+ this.#id = id\r
+ this.#m = mnemonic ?? null\r
+ this.#s = seed ?? new Uint8Array(32)\r
+ }\r
+\r
+ /**\r
+ * Retrieves an account from a wallet using its child key derivation function.\r
+ * Defaults to the first account at index 0.\r
+ *\r
+ * ```\r
+ * console.log(await wallet.account(5))\r
+ * // outputs sixth account of the wallet\r
+ * // {\r
+ * // privateKey: <...>,\r
+ * // index: 5\r
+ * // }\r
+ * ```\r
+ *\r
+ * @param {number} index - Wallet index of secret key. Default: 0\r
+ * @returns {Account} Account derived at the specified wallet index\r
+ */\r
+ async account (index: number = 0): Promise<Account> {\r
+ return (await this.accounts(index))[index]\r
+ }\r
+\r
+ /**\r
+ * Retrieves accounts from a wallet using its child key derivation function.\r
+ * Defaults to the first account at index 0.\r
+ *\r
+ * The returned object will have keys corresponding with the requested range\r
+ * of account indexes. The value of each key will be the Account derived for\r
+ * that index in the wallet.\r
+ *\r
+ * ```\r
+ * console.log(await wallet.accounts(5))\r
+ * // outputs sixth account of the wallet\r
+ * // {\r
+ * // 5: {\r
+ * // privateKey: <...>,\r
+ * // index: 5\r
+ * // }\r
+ * // }\r
+ * ```\r
+ *\r
+ * @param {number} from - Start index of secret keys. Default: 0\r
+ * @param {number} to - End index of secret keys. Default: `from`\r
+ * @returns {AccountList} Object with keys of account indexes and values of the corresponding Accounts\r
+ */\r
+ async accounts (from: number = 0, to: number = from): Promise<AccountList> {\r
+ if (from > to) {\r
+ const swap = from\r
+ from = to\r
+ to = swap\r
+ }\r
+ const output = new AccountList()\r
+ const indexes: number[] = []\r
+ for (let i = from; i <= to; i++) {\r
+ if (this.#accounts[i] == null) {\r
+ indexes.push(i)\r
+ } else {\r
+ output[i] = this.#accounts[i]\r
+ }\r
+ }\r
+ if (indexes.length > 0) {\r
+ const keypairs = await this.ckd(indexes)\r
+ for (const keypair of keypairs) {\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(privateKey, index)\r
+ } else if (publicKey != null) {\r
+ output[index] = await Account.fromPublicKey(publicKey, index)\r
+ } else {\r
+ throw new RangeError('Account keys missing')\r
+ }\r
+ this.#accounts[index] = output[index]\r
+ }\r
+ }\r
+ return output\r
+ }\r
+\r
+ /**\r
+ * Removes encrypted secrets in storage and releases variable references to\r
+ * allow garbage collection.\r
+ */\r
+ async destroy (): Promise<void> {\r
+ let i = 0\r
+ for (const a in this.#accounts) {\r
+ await this.#accounts[a].destroy()\r
+ delete this.#accounts[a]\r
+ i++\r
+ }\r
+ this.#m = null\r
+ bytes.erase(this.#s)\r
+ await SafeWorker.add({\r
+ method: 'destroy',\r
+ name: this.id\r
+ })\r
+ }\r
+\r
+ /**\r
+ * Locks the wallet and all currently derived accounts with a password that\r
+ * will be needed to unlock it later.\r
+ *\r
+ * @param {(string|Uint8Array)} password Used to lock the wallet\r
+ * @returns True if successfully locked\r
+ */\r
+ async lock (password: string | Uint8Array<ArrayBuffer>): Promise<boolean> {\r
+ if (typeof password === 'string') {\r
+ password = utf8.toBytes(password)\r
+ }\r
+ if (password == null || !(password instanceof Uint8Array)) {\r
+ throw new Error('Failed to lock wallet')\r
+ }\r
+ try {\r
+ const headers = {\r
+ method: 'set',\r
+ name: this.id\r
+ }\r
+ const data = {\r
+ password: new Uint8Array(password).buffer,\r
+ id: new Uint8Array(this.#id.bytes).buffer,\r
+ mnemonic: utf8.toBytes(this.#m?.phrase ?? '').buffer,\r
+ seed: this.#s.buffer\r
+ }\r
+ const response = await SafeWorker.add(headers, data)\r
+ const success = response[0].result\r
+ if (!success) {\r
+ throw null\r
+ }\r
+ const promises = []\r
+ for (const account of this.#accounts) {\r
+ promises.push(account.lock(new Uint8Array(password)))\r
+ }\r
+ await Promise.all(promises)\r
+ } catch (err) {\r
+ throw new Error('Failed to lock wallet')\r
+ } finally {\r
+ bytes.erase(password)\r
+ }\r
+ this.#m = null\r
+ this.#locked = true\r
+ return true\r
+ }\r
+\r
+ /**\r
+ * Refreshes wallet account balances, frontiers, and representatives from the\r
+ * current state on the network.\r
+ *\r
+ * A successful response will set these properties on each account.\r
+ *\r
+ * @param {Rpc|string|URL} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks\r
+ * @returns {Promise<Account[]>} Accounts with updated balances, frontiers, and representatives\r
+ */\r
+ async refresh (rpc: Rpc | string | URL, from: number = 0, to: number = from): Promise<AccountList> {\r
+ if (typeof rpc === 'string' || rpc instanceof URL) {\r
+ rpc = new Rpc(rpc)\r
+ }\r
+ if (!(rpc instanceof Rpc)) {\r
+ throw new TypeError('RPC must be a valid node')\r
+ }\r
+ const accounts = await this.accounts(from, to)\r
+ for (const a in accounts) {\r
+ try {\r
+ await accounts[a].refresh(rpc)\r
+ } catch (err) {\r
+ delete accounts[a]\r
+ }\r
+ }\r
+ return accounts\r
+ }\r
+\r
+ /**\r
+ * Unlocks the wallet using the same password as used prior to lock it.\r
+ *\r
+ * @param {(string|Uint8Array)} password Used previously to lock the wallet\r
+ * @returns True if successfully unlocked\r
+ */\r
+ async unlock (password: string | Uint8Array<ArrayBuffer>): Promise<boolean> {\r
+ if (typeof password === 'string') {\r
+ password = utf8.toBytes(password)\r
+ }\r
+ if (password == null || !(password instanceof Uint8Array)) {\r
+ throw new Error('Failed to unlock wallet')\r
+ }\r
+ try {\r
+ const headers = {\r
+ method: 'get',\r
+ name: this.id\r
+ }\r
+ const data = {\r
+ password: new Uint8Array(password).buffer\r
+ }\r
+ const response = await SafeWorker.add(headers, data)\r
+ let { id, mnemonic, seed } = response[0].result\r
+ if (id == null) {\r
+ throw null\r
+ }\r
+ id = await Entropy.import(id as ArrayBuffer)\r
+ if (id.hex !== this.#id.hex) {\r
+ throw null\r
+ }\r
+ if (mnemonic != null) {\r
+ this.#m = await Bip39Mnemonic.fromPhrase(bytes.toUtf8(mnemonic))\r
+ mnemonic = null\r
+ }\r
+ if (seed != null) {\r
+ this.#s = new Uint8Array(seed as ArrayBuffer)\r
+ seed = null\r
+ }\r
+ const promises = []\r
+ for (const account of this.#accounts) {\r
+ promises.push(account.unlock(new Uint8Array(password)))\r
+ }\r
+ await Promise.all(promises)\r
+ } catch (err) {\r
+ throw new Error('Failed to unlock wallet')\r
+ } finally {\r
+ bytes.erase(password)\r
+ }\r
+ this.#locked = false\r
+ return true\r
+ }\r
+\r
+ /**\r
+ * Fetches the lowest-indexed unopened account from a wallet in sequential\r
+ * order. An account is unopened if it has no frontier block.\r
+ *\r
+ * @param {Rpc|string|URL} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks\r
+ * @param {number} batchSize - Number of accounts to fetch and check per RPC callout\r
+ * @param {number} from - Account index from which to start the search\r
+ * @returns {Promise<Account>} The lowest-indexed unopened account belonging to the wallet\r
+ */\r
+ async unopened (rpc: Rpc, batchSize: number = ADDRESS_GAP, from: number = 0): Promise<Account> {\r
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1) {\r
+ throw new RangeError(`Invalid batch size ${batchSize}`)\r
+ }\r
+ const accounts = await this.accounts(from, from + batchSize - 1)\r
+ const addresses = []\r
+ for (const a in accounts) {\r
+ addresses.push(accounts[a].address)\r
+ }\r
+ const data = {\r
+ "accounts": addresses\r
+ }\r
+ const { errors } = await rpc.call('accounts_frontiers', data)\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
+ }\r
+ }\r
+ return await this.unopened(rpc, batchSize, from + batchSize)\r
+ }\r
+}\r
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
// SPDX-License-Identifier: GPL-3.0-or-later
-import { Data, Headers, WorkerInterface } from '.'
+import { Data, Headers } from '.'
+import { WorkerInterface } from './worker-interface'
type ExtendedKey = {
privateKey: DataView<ArrayBuffer>
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
// SPDX-License-Identifier: GPL-3.0-or-later
-import { default as Bip44CkdWorker, Bip44Ckd } from './bip44-ckd'
-import { default as NanoNaClWorker, NanoNaCl } from './nano-nacl'
-import { default as SafeWorker, Safe } from './safe'
-
-export {
- Bip44Ckd,
- Bip44CkdWorker,
- NanoNaCl,
- NanoNaClWorker,
- Safe,
- SafeWorker
-}
-
-export type Headers = {
- [key: string]: any
-}
-
export type Data = {
[key: string]: ArrayBuffer
}
-type Task = {
- id: number
- headers: Headers | null
- data?: Data
- reject: (value: any) => void
- resolve: (value: any) => void
-}
-
-/**
-* Processes a queue of tasks using Web Workers.
-*/
-export class Queue {
- static #decoder: TextDecoder = new TextDecoder()
- static #encoder: TextEncoder = new TextEncoder()
- static #instances: Queue[] = []
- static get instances (): Queue[] { return this.#instances }
-
- #job?: Task
- #isIdle: boolean
- #queue: Task[] = []
- #url: string
- #worker: Worker
-
- /**
- * Creates a Web Worker from a stringified script.
- *
- * @param {string} worker - Stringified worker class body
- * @param {number} [count=1] - Integer between 1 and CPU thread count shared among all Pools
- */
- constructor (worker: string) {
- this.#isIdle = true
- this.#queue = []
- 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 = JSON.parse(Queue.#decoder.decode(message.data) || '[]')
- if (!Array.isArray(result)) result = [result]
- debugger
- this.#report(result)
- })
- Queue.#instances.push(this)
- }
-
- async add (headers: Headers | null, data?: Data): Promise<any> {
- return await this.#assign(task => this.#queue.push(task), headers, data)
- }
-
- async prioritize (headers: Headers | null, data?: Data): Promise<any> {
- return await this.#assign(task => this.#queue.unshift(task), headers, data)
- }
-
- terminate (): void {
- this.#job = undefined
- this.#worker.terminate()
- }
-
- async #assign (enqueue: (task: Task) => number, headers: Headers | null, data?: Data) {
- return new Promise(async (resolve, reject): Promise<void> => {
- const task: Task = {
- id: performance.now(),
- headers,
- data,
- resolve,
- reject
- }
- await enqueue(task)
- debugger
- if (this.#isIdle) this.#process()
- })
- }
-
- #process = (): void => {
- debugger
- this.#job = this.#queue.shift()
- if (this.#job == null) {
- throw new Error('Failed to get job from empty task queue.')
- }
- const { id, headers, data, reject } = this.#job
- this.#isIdle = !id
- try {
- const buffers: ArrayBuffer[] = []
- if (data != null) {
- for (let d of Object.keys(data)) {
- buffers.push(data[d])
- }
- }
- this.#worker.postMessage({ headers, data }, buffers)
- } catch (err) {
- reject(err)
- }
- }
-
- #report (results: any[]): void {
- if (this.#job == null) {
- throw new Error('Worker returned results but had nowhere to report it.')
- }
- const { resolve, reject } = this.#job
- debugger
- try {
- resolve(results)
- } catch (err) {
- reject(err)
- } finally {
- this.#process()
- }
- }
+export type Headers = {
+ [key: string]: any
}
-/**
-* Provides basic worker event messaging to extending classes.
-*
-* In order to be properly bundled in a format that can be used to create an
-* inline Web Worker, the extending classes must export WorkerInterface and
-* themselves as a string:
-*```
-* export default `
-* const WorkerInterface = ${WorkerInterface}
-* const Pow = ${Pow}
-* `
-* ```
-* They must also initialize the event listener by calling their inherited
-* `listen()` function. Finally, they must override the implementation of the
-* `work()` function. See the documentation of those functions for details.
-*/
-export abstract class WorkerInterface {
- /**
- * Processes data through a worker.
- *
- * Extending classes must override this template by implementing the same
- * function signature and providing their own processing call in the try-catch
- * block.
- *
- * @param {Header} headers - Flat object of header data
- * @param {any[]} data - Transferred buffer of data to process
- * @returns Promise for processed data
- */
- static async work (headers: Headers | null, data?: Data): Promise<any> {
- return new Promise(async (resolve, reject): Promise<void> => {
- try {
- let x, y
- if (headers != null) {
- const { sample } = headers
- x = sample
- }
- if (data != null) {
- const { buf } = data
- y = buf
- }
- resolve({ x, y })
- } catch (err) {
- reject(err)
- }
- })
- }
-
- /**
- * Encodes worker results as an ArrayBuffer so it can be transferred back to
- * the main thread.
- *
- * @param {any[]} results - Array of processed data
- */
- static report (results: any[]): void {
- const buffer = new TextEncoder().encode(JSON.stringify(results)).buffer
- //@ts-expect-error
- postMessage(buffer, [buffer])
- }
-
- /**
- * Listens for messages from the main thread.
- *
- * Extending classes must call this in a static initialization block:
- * ```
- * static {
- * Extension.listen()
- * }
- * ```
- */
- static listen (): void {
- addEventListener('message', (message: any): void => {
- const { name, headers, data } = message
- if (name === 'STOP') {
- close()
- const buffer = new ArrayBuffer(0)
- //@ts-expect-error
- postMessage(buffer, [buffer])
- } else {
- this.work(headers, data).then(this.report).catch(this.report)
- }
- })
- }
-}
+export { Bip44CkdWorker, NanoNaClWorker, SafeWorker } from './queue'
\r
'use strict'\r
\r
-import { Data, Headers, WorkerInterface } from '.'\r
+import { Data, Headers } from '.'\r
+import { WorkerInterface } from './worker-interface'\r
import { Blake2b } from '#src/lib/blake2b.js'\r
+import { default as Convert, bytes, hex } from '#src/lib/convert.js'\r
\r
/**\r
* Ported in 2014 by Dmitry Chestnykh and Devi Mandiri.\r
NanoNaCl.listen()\r
}\r
\r
- static async work (headers: Headers, data: Data): Promise<any> {\r
+ static async work (headers: Headers, data: Data): Promise<boolean | string> {\r
const { method, msg, signature, publicKey } = headers\r
const privateKey = new Uint8Array(data.privateKey)\r
switch (method) {\r
case 'convert': {\r
- return await this.convert(privateKey)\r
+ return bytes.toHex(await this.convert(privateKey))\r
}\r
case 'detached': {\r
- return await this.detached(msg, privateKey)\r
+ return bytes.toHex(await this.detached(msg, privateKey))\r
}\r
case 'verify': {\r
return await this.verify(msg, signature, publicKey)\r
static crypto_sign_SEEDBYTES: 32 = 32\r
\r
/* High-level API */\r
- static checkArrayTypes (...args: Uint8Array[]): void {\r
+ static checkArrayTypes (...args: Uint8Array<ArrayBuffer>[]): void {\r
for (let i = 0; i < args.length; i++) {\r
if (!(args[i] instanceof Uint8Array)) {\r
throw new TypeError(`expected Uint8Array; actual ${args[i].constructor?.name ?? typeof args[i]}`)\r
}\r
}\r
\r
- static parseHex (hex: string): Uint8Array {\r
- if (hex.length % 2 === 1) hex = `0${hex}`\r
- const arr = hex.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16))\r
- return Uint8Array.from(arr ?? [])\r
- }\r
-\r
- static hexify (buf: Uint8Array): string {\r
- let str = ''\r
- for (let i = 0; i < buf.length; i++) {\r
- if (typeof buf[i] !== 'number') {\r
- throw new TypeError(`expected number to convert to hex; received ${typeof buf[i]}`)\r
- }\r
- if (buf[i] < 0 || buf[i] > 255) {\r
- throw new RangeError(`expected byte value 0-255; received ${buf[i]}`)\r
- }\r
- str += buf[i].toString(16).padStart(2, '0')\r
- }\r
- return str\r
- }\r
-\r
- static sign (msg: Uint8Array, privateKey: Uint8Array): Uint8Array {\r
+ static sign (msg: Uint8Array<ArrayBuffer>, privateKey: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer> {\r
this.checkArrayTypes(msg, privateKey)\r
if (privateKey.byteLength !== this.crypto_sign_PRIVATEKEYBYTES) {\r
throw new Error(`expected key size ${this.crypto_sign_PRIVATEKEYBYTES} bytes; actual key size ${privateKey.byteLength} bytes`)\r
}\r
const signedMsg = new Uint8Array(this.crypto_sign_BYTES + msg.length)\r
- const publicKey = this.parseHex(this.convert(privateKey))\r
+ const publicKey = this.convert(privateKey)\r
this.crypto_sign(signedMsg, msg, msg.length, privateKey, publicKey)\r
return signedMsg\r
}\r
\r
- static open (signedMsg: Uint8Array, publicKey: Uint8Array): Uint8Array {\r
+ static open (signedMsg: Uint8Array<ArrayBuffer>, publicKey: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer> {\r
this.checkArrayTypes(signedMsg, publicKey)\r
if (publicKey.length !== this.crypto_sign_PUBLICKEYBYTES) {\r
throw new Error('bad public key size')\r
return m\r
}\r
\r
- static detached (msg: Uint8Array, privateKey: Uint8Array): string {\r
+ static detached (msg: Uint8Array<ArrayBuffer>, privateKey: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer> {\r
const signedMsg = this.sign(msg, privateKey)\r
const sig = new Uint8Array(this.crypto_sign_BYTES)\r
for (let i = 0; i < sig.length; i++) {\r
sig[i] = signedMsg[i]\r
}\r
- return this.hexify(sig).toUpperCase()\r
+ return sig\r
}\r
\r
- static verify (msg: Uint8Array, sig: Uint8Array, publicKey: Uint8Array): boolean {\r
+ static verify (msg: Uint8Array<ArrayBuffer>, sig: Uint8Array<ArrayBuffer>, publicKey: Uint8Array<ArrayBuffer>): boolean {\r
this.checkArrayTypes(msg, sig, publicKey)\r
if (sig.length !== this.crypto_sign_BYTES) {\r
throw new Error('bad signature size')\r
return (this.crypto_sign_open(m, sm, sm.length, publicKey) >= 0)\r
}\r
\r
- static convert (seed: string | Uint8Array): string {\r
- if (typeof seed === 'string') seed = this.parseHex(seed)\r
+ /**\r
+ * Derives a public key from a private key.\r
+ *\r
+ * @param {(string|Uint8Array)} seed - 32-byte private key\r
+ * @returns 32-byte public key byte array\r
+ */\r
+ static convert (seed: string | Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer> {\r
+ if (typeof seed === 'string') seed = hex.toBytes(seed)\r
this.checkArrayTypes(seed)\r
if (seed.length !== this.crypto_sign_SEEDBYTES) {\r
throw new Error('bad seed size')\r
this.scalarbase(p, hash)\r
this.pack(pk, p)\r
\r
- return this.hexify(pk).toUpperCase()\r
+ return pk\r
}\r
}\r
\r
export default `\r
+ ${Convert}\r
const Blake2b = ${Blake2b}\r
const WorkerInterface = ${WorkerInterface}\r
const NanoNaCl = ${NanoNaCl}\r
--- /dev/null
+// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { Data, Headers } from '.'
+import { default as bip44 } from './bip44-ckd'
+import { default as nacl } from './nano-nacl'
+import { default as safe } from './safe'
+
+type Task = {
+ id: number
+ headers: Headers | null
+ data?: Data
+ reject: (value: any) => void
+ resolve: (value: any) => void
+}
+
+/**
+* Processes a queue of tasks using Web Workers.
+*/
+export class Queue {
+ static #instances: Queue[] = []
+ static get instances (): Queue[] { return this.#instances }
+
+ #job?: Task
+ #isIdle: boolean
+ #queue: Task[] = []
+ #url: string
+ #worker: Worker
+
+ /**
+ * Creates a Web Worker from a stringified script.
+ *
+ * @param {string} worker - Stringified worker class body
+ * @param {number} [count=1] - Integer between 1 and CPU thread count shared among all Pools
+ */
+ constructor (worker: string) {
+ this.#isIdle = true
+ this.#queue = []
+ 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
+ if (!Array.isArray(result)) result = [result]
+ this.#report(result)
+ })
+ Queue.#instances.push(this)
+ }
+
+ async add (headers: Headers | null, data?: Data): Promise<any> {
+ return await this.#assign(task => this.#queue.push(task), headers, data)
+ }
+
+ async prioritize (headers: Headers | null, data?: Data): Promise<any> {
+ return await this.#assign(task => this.#queue.unshift(task), headers, data)
+ }
+
+ terminate (): void {
+ this.#job = undefined
+ this.#worker.terminate()
+ }
+
+ async #assign (enqueue: (task: Task) => number, headers: Headers | null, data?: Data): Promise<Data> {
+ return new Promise(async (resolve, reject): Promise<void> => {
+ const task: Task = {
+ id: performance.now(),
+ headers,
+ data,
+ resolve,
+ reject
+ }
+ await enqueue(task)
+ if (this.#isIdle) this.#process()
+ })
+ }
+
+ #process = (): void => {
+ this.#job = this.#queue.shift()
+ this.#isIdle = this.#job == null
+ if (this.#job != null) {
+ const { id, headers, data, reject } = this.#job
+ try {
+ const buffers: ArrayBuffer[] = []
+ if (data != null) {
+ for (let d of Object.keys(data)) {
+ buffers.push(data[d])
+ }
+ }
+ this.#worker.postMessage({ headers, data }, buffers)
+ } catch (err) {
+ reject(err)
+ }
+ }
+ }
+
+ #report (results: any): void {
+ if (this.#job == null) {
+ throw new Error('Worker returned results but had nowhere to report it.')
+ }
+ const { resolve, reject } = this.#job
+ try {
+ resolve(results)
+ } catch (err) {
+ reject(err)
+ } finally {
+ this.#process()
+ }
+ }
+}
+
+export const Bip44CkdWorker = new Queue(bip44)
+export const NanoNaClWorker = new Queue(nacl)
+export const SafeWorker = new Queue(safe)
'use strict'
-import { Data, Headers, WorkerInterface } from '.'
-import { bytes, hex, utf8, default as Convert } from '#src/lib/convert.js'
+import { Data, Headers } from '.'
+import { WorkerInterface } from './worker-interface'
+import { default as Convert, bytes } from '#src/lib/convert.js'
import { Entropy } from '#src/lib/entropy.js'
type SafeRecord = {
- encrypted: string,
iv: string
+ data: Data
}
/**
static async work (headers: Headers, data: Data): Promise<any> {
this.#storage = await this.#open(this.DB_NAME)
- const { method, name, id } = headers
- const { password, phrase, seed } = data
+ const { method, name } = headers
const results = []
let result
try {
switch (method) {
case 'set': {
- result = await this.set(name, password, { id, phrase, seed })
+ result = await this.set(name, data)
break
}
case 'get': {
- result = await this.get(name, password)
+ result = await this.get(name, data)
break
}
case 'destroy': {
result = `unknown Safe method ${method}`
}
}
- results.push({ name, method, result })
+ results.push({ method, name, result })
} catch (err) {
result = false
}
/**
* Encrypts data with a password byte array and stores it in the Safe.
*/
- static async set (name: string, password: ArrayBuffer, data: any): Promise<boolean> {
+ static async set (name: string, data: Data): Promise<boolean> {
+ const { password } = data
+ delete data.password
let passkey: CryptoKey
try {
if (await this.#exists(name)) throw new Error('Record is already locked')
} catch {
throw new Error(this.ERR_MSG)
} finally {
- new Uint8Array(password).fill(0)
+ bytes.erase(password)
}
if (this.#isInvalid(name, passkey, data)) {
throw new Error(this.ERR_MSG)
try {
const iv = await Entropy.create()
- data = JSON.stringify(data, (k, v) => typeof v === 'bigint' ? v.toString() : v)
const derivationAlgorithm: Pbkdf2Params = {
name: 'PBKDF2',
hash: 'SHA-512',
length: 256
}
passkey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, passkey, derivedKeyType, false, ['encrypt'])
- const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, utf8.toBytes(data))
- const record = {
- encrypted: bytes.toHex(new Uint8Array(encrypted)),
- iv: iv.hex
+ for (const d of Object.keys(data)) {
+ data[d] = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, data[d])
+ }
+ const record: SafeRecord = {
+ iv: iv.hex,
+ data
}
return await this.#add(record, name)
} catch (err) {
/**
* Retrieves data from the Safe and decrypts it with a password byte array.
*/
- static async get (name: string, password: ArrayBuffer): Promise<any> {
+ static async get (name: string, data: Data): Promise<Data | null> {
+ const { password } = data
+ delete data.password
let passkey: CryptoKey
try {
passkey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
} catch {
return null
} finally {
- new Uint8Array(password).fill(0)
+ bytes.erase(password)
}
if (this.#isInvalid(name, passkey)) {
return null
}
try {
- const encrypted = hex.toBytes(record.encrypted)
const iv = await Entropy.import(record.iv)
+ const { data } = record
const derivationAlgorithm: Pbkdf2Params = {
name: 'PBKDF2',
hash: 'SHA-512',
length: 256
}
passkey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, passkey, derivedKeyType, false, ['decrypt'])
- const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, encrypted)
- const decoded = bytes.toUtf8(new Uint8Array(decrypted))
- const data = JSON.parse(decoded)
+ for (const d of Object.keys(data)) {
+ const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, data[d])
+ data[d] = decrypted
+ }
await this.destroy(name)
return data
} catch (err) {
static async #delete (name: string): Promise<boolean> {
try {
const result = await this.#transact<undefined>('readwrite', db => db.delete(name))
- return !(await this.#exists(name))
+ return !(result || await this.#exists(name))
} catch {
throw new Error(this.ERR_MSG)
}
--- /dev/null
+// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { Data, Headers } from '.'
+/**
+* Provides basic worker event messaging to extending classes.
+*
+* In order to be properly bundled in a format that can be used to create an
+* inline Web Worker, the extending classes must export WorkerInterface and
+* themselves as a string:
+*```
+* export default `
+* const WorkerInterface = ${WorkerInterface}
+* const WorkerImplementation = ${WorkerImplementation}
+* `
+* ```
+* They must also initialize the event listener by calling their inherited
+* `listen()` function. Finally, they must override the implementation of the
+* `work()` function. See the documentation of those functions for details.
+*/
+export abstract class WorkerInterface {
+ /**
+ * Processes data through a worker.
+ *
+ * Extending classes must override this template by implementing the same
+ * function signature and providing their own processing call in the try-catch
+ * block.
+ *
+ * @param {Header} headers - Flat object of header data
+ * @param {Data} data - String keys for ArrayBuffer values to transfer and process
+ * @returns Promise for processed data
+ */
+ static async work (headers: Headers | null, data?: Data): Promise<any> {
+ return new Promise(async (resolve, reject): Promise<void> => {
+ try {
+ let x, y = new ArrayBuffer(0)
+ if (headers != null) {
+ const { sample } = headers
+ x = sample
+ }
+ if (data != null) {
+ const { buf } = data
+ y = buf
+ }
+ resolve({ x, y })
+ } catch (err) {
+ reject(err)
+ }
+ })
+ }
+
+ /**
+ * Transfers buffers of worker results back to the main thread.
+ *
+ * @param {Headers} results - Key-value pairs of processed data
+ */
+ static report (results: Headers): void {
+ const buffers = []
+ for (const d of Object.keys(results)) {
+ if (results[d] instanceof ArrayBuffer) {
+ buffers.push(results[d])
+ }
+ }
+ //@ts-expect-error
+ postMessage(results, buffers)
+ }
+
+ /**
+ * Listens for messages from the main thread.
+ *
+ * Extending classes must call this in a static initialization block:
+ * ```
+ * static {
+ * Extension.listen()
+ * }
+ * ```
+ */
+ static listen (): void {
+ addEventListener('message', (message: MessageEvent<any>): void => {
+ const { name, headers, data } = message.data
+ if (name === 'STOP') {
+ close()
+ this.report({})
+ } else {
+ this.work(headers, data).then(this.report).catch(this.report)
+ }
+ })
+ }
+}
import { SendBlock, ReceiveBlock, ChangeBlock } from './lib/block'
import { Rolodex } from './lib/rolodex'
import { Rpc } from './lib/rpc'
-import { Tools } from './lib/tools'
import { Bip44Wallet, Blake2bWallet, LedgerWallet } from './lib/wallets'
+import * as Tools from './lib/tools'
-export { Account, Blake2b, SendBlock, ReceiveBlock, ChangeBlock, Rpc, Rolodex, Tools, Bip44Wallet, Blake2bWallet, LedgerWallet }
+export { Account, Blake2b, SendBlock, ReceiveBlock, ChangeBlock, Rpc, Rolodex, Bip44Wallet, Blake2bWallet, LedgerWallet, Tools }
const failures = []
const passes = []
-function fail (...args) {
- failures.push(args)
- console.error(`%cFAIL `, 'color:red', ...args)
+function fail (err) {
+ failures.push(err.message)
+ console.error(`%cFAIL `, 'color:red', err.message, err.cause)
}
-function pass (...args) {
- passes.push(args)
- console.log(`%cPASS `, 'color:green', ...args)
+function pass (name) {
+ passes.push(name)
+ console.log(`%cPASS `, 'color:green', name)
}
await suite('TEST RUNNER CHECK', async () => {
await test('promise should pass', new Promise(resolve => { resolve('') }))
console.assert(failures.some(call => /.*promise should pass.*/.test(call[0])) === false, `good promise errored`)
- console.assert(passes.some(call => /.*promise should pass.*/.test(call[0])) === true, `good promise not logged`)
+ console.assert(passes.some(call => /.*promise should pass.*/.test(call)) === true, `good promise not logged`)
await test('promise should fail', new Promise((resolve, reject) => { reject('FAILURE EXPECTED HERE') }))
- console.assert(failures.some(call => /.*promise should fail.*/.test(call[0])) === true, `bad promise not errored`)
- console.assert(passes.some(call => /.*promise should fail.*/.test(call[0])) === false, 'bad promise logged')
+ console.assert(failures.some(call => /.*promise should fail.*/.test(call)) === true, `bad promise not errored`)
+ console.assert(passes.some(call => /.*promise should fail.*/.test(call)) === false, 'bad promise logged')
await test('async should pass', async () => {})
- console.assert(failures.some(call => /.*async should pass.*/.test(call[0])) === false, 'good async errored')
- console.assert(passes.some(call => /.*async should pass.*/.test(call[0])) === true, 'good async not logged')
+ console.assert(failures.some(call => /.*async should pass.*/.test(call)) === false, 'good async errored')
+ console.assert(passes.some(call => /.*async should pass.*/.test(call)) === true, 'good async not logged')
await test('async should fail', async () => { throw new Error('FAILURE EXPECTED HERE') })
- console.assert(failures.some(call => /.*async should fail.*/.test(call[0])) === true, 'bad async not errored')
- console.assert(passes.some(call => /.*async should fail.*/.test(call[0])) === false, 'bad async logged')
+ console.assert(failures.some(call => /.*async should fail.*/.test(call)) === true, 'bad async not errored')
+ console.assert(passes.some(call => /.*async should fail.*/.test(call)) === false, 'bad async logged')
await test('function should pass', () => {})
- console.assert(failures.some(call => /.*function should pass.*/.test(call[0])) === false, 'good function errored')
- console.assert(passes.some(call => /.*function should pass.*/.test(call[0])) === true, 'good function not logged')
+ console.assert(failures.some(call => /.*function should pass.*/.test(call)) === false, 'good function errored')
+ console.assert(passes.some(call => /.*function should pass.*/.test(call)) === true, 'good function not logged')
await test('function should fail', 'FAILURE EXPECTED HERE')
- console.assert(failures.some(call => /.*function should fail.*/.test(call[0])) === true, 'bad function not errored')
- console.assert(passes.some(call => /.*function should fail.*/.test(call[0])) === false, 'bad function logged')
+ console.assert(failures.some(call => /.*function should fail.*/.test(call)) === true, 'bad function not errored')
+ console.assert(passes.some(call => /.*function should fail.*/.test(call)) === false, 'bad function logged')
console.log(`%cTEST RUNNER CHECK DONE`, 'font-weight:bold')
})
})
}
-export function test (name, opts, fn) {
+export async function test (name, opts, fn) {
if (opts?.skip) return console.log(`%cSKIP `, 'color:CornflowerBlue', name)
if (fn === undefined) fn = opts
if (fn instanceof Promise) {
try {
- return fn
- .then(() => pass(name))
- .catch((err) => fail(`${name}: ${err}`))
- } catch (err) {
- fail(`${name}: ${err.message}`)
- fail(err)
- }
- } else if (fn?.constructor?.name === 'AsyncFunction') {
- try {
- return fn()
- .then(() => pass(name))
- .catch((err) => fail(`${name}: ${err}`))
+ await fn
+ pass(name)
} catch (err) {
- fail(`${name}: ${err.message}`)
- fail(err)
+ fail(new Error(name, { cause: err }))
}
} else if (typeof fn === 'function') {
try {
- fn()
+ await fn()
pass(name)
} catch (err) {
- fail(`${name}: ${err}`)
- fail(err)
+ fail(new Error(name, { cause: err }))
}
} else {
- fail(`${name}: test cannot execute on ${typeof fn} ${fn}`)
+ fail(new Error(name, { cause: `test cannot execute on ${typeof fn} ${fn}` }))
}
}
throw new Error('Invalid assertion')
}
if (!bool) {
- throw new Error(`test result falsy`)
+ throw new Error(`test result falsy`, { cause: bool })
}
return true
},
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.ok('id' in wallet)\r
- assert.ok(/[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
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.ok('id' in wallet)\r
- assert.ok(/[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