// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\r
// SPDX-License-Identifier: GPL-3.0-or-later\r
\r
-import { KeyPair, Wallet } from './wallet'\r
+import { KeyPair, Wallet } from '.'\r
import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
import { SEED_LENGTH_BIP44 } from '#src/lib/constants.js'\r
import { hex, utf8 } from '#src/lib/convert.js'\r
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\r
// SPDX-License-Identifier: GPL-3.0-or-later\r
\r
-import { KeyPair, Wallet } from './wallet'\r
+import { KeyPair, Wallet } from '.'\r
import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
import { Blake2b } from '#src/lib/blake2b.js'\r
import { SEED_LENGTH_BLAKE2B } from '#src/lib/constants.js'\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 { Queue } from '#src/lib/pool.js'\r
+import { Rpc } from '#src/lib/rpc.js'\r
+import { SafeWorker } from '#workers'\r
+\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
import { bytes, dec, hex } from '#src/lib/convert.js'\r
import { Entropy } from '#src/lib/entropy.js'\r
import { Rpc } from '#src/lib/rpc.js'\r
-import { KeyPair, Wallet } from './wallet'\r
+import { KeyPair, Wallet } from '.'\r
\r
type DeviceStatus = 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED'\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, hex, utf8 } from '#src/lib/convert.js'\r
-import { Entropy } from '#src/lib/entropy.js'\r
-import { Queue } from '#src/lib/pool.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
- 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