]> git.codecow.com Git - libnemo.git/commitdiff
Merge base wallet with barrel module.
authorChris Duncan <chris@zoso.dev>
Tue, 15 Jul 2025 12:25:52 +0000 (05:25 -0700)
committerChris Duncan <chris@zoso.dev>
Tue, 15 Jul 2025 12:25:52 +0000 (05:25 -0700)
src/lib/wallets/bip44-wallet.ts
src/lib/wallets/blake2b-wallet.ts
src/lib/wallets/index.ts
src/lib/wallets/ledger-wallet.ts
src/lib/wallets/wallet.ts [deleted file]

index 3959cf9f80fd50d04fa8a19d9f1872fb0b697ef8..21c2538b89b614b5934d5e18dc1824ee7989c15a 100644 (file)
@@ -1,7 +1,7 @@
 // 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
index 5dfed7190332eeff00b6cf804b7472e395b60e00..9ace17a9e2479bb3a0c2996368d1a094a593b785 100644 (file)
@@ -1,7 +1,7 @@
 // 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
index 574fc703ec1128884fb8f64e9b22ce7b0d9bee6e..b7161e47842b19e54e123b127c4dd357cf663a71 100644 (file)
@@ -1,6 +1,303 @@
 // 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
index dbf630d09ff79be7c8c1c10e294a7676954b9c94..6601020777d2073edb43f18ed455d8db1d5b818c 100644 (file)
@@ -10,7 +10,7 @@ import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, LEDGER_ADPU_CODES, LED
 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
diff --git a/src/lib/wallets/wallet.ts b/src/lib/wallets/wallet.ts
deleted file mode 100644 (file)
index 795fc3b..0000000
+++ /dev/null
@@ -1,299 +0,0 @@
-// 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