]> git.codecow.com Git - libnemo.git/commitdiff
Include wallet type in export to assist in restoration after page reload. Accept...
authorChris Duncan <chris@zoso.dev>
Sun, 27 Jul 2025 09:14:59 +0000 (02:14 -0700)
committerChris Duncan <chris@zoso.dev>
Sun, 27 Jul 2025 09:14:59 +0000 (02:14 -0700)
src/lib/wallets/wallet.ts
test/test.lock-unlock.mjs

index d4981c3ab702989cdd4a635b5678c253ba2105da..63184a02d51204675eb0946456fc497ab3c3af5b 100644 (file)
@@ -8,7 +8,7 @@ import { ADDRESS_GAP } from '#src/lib/constants.js'
 import { bytes, hex, utf8 } from '#src/lib/convert.js'\r
 import { Entropy } from '#src/lib/entropy.js'\r
 import { Rpc } from '#src/lib/rpc.js'\r
-import { Key, KeyPair, WalletType } from '#types'\r
+import { KeyPair, NamedData, WalletType } from '#types'\r
 import { SafeWorker } from '#workers'\r
 \r
 /**\r
@@ -26,16 +26,26 @@ export abstract class Wallet {
        *\r
        * @returns Array of hexadecimal-formatted wallet IDs\r
        */\r
-       static async export (): Promise<string[]> {\r
+       static async export (): Promise<NamedData<string[]> | null> {\r
                try {\r
                        const response = await SafeWorker.request<ArrayBuffer>({\r
                                method: 'export',\r
                                store: 'Wallet'\r
                        })\r
-                       return Object.keys(response)\r
+                       const ids = Object.keys(response)\r
+                       const data: NamedData<string[]> = {}\r
+                       ids.map(i => {\r
+                               const [type, id] = i.split('_')\r
+                               if (data[type] == null) {\r
+                                       data[type] = [id]\r
+                               } else {\r
+                                       data[type].push(id)\r
+                               }\r
+                       })\r
+                       return data\r
                } catch (err) {\r
                        console.error(err)\r
-                       return []\r
+                       return null\r
                }\r
        }\r
 \r
@@ -190,28 +200,24 @@ export abstract class Wallet {
        * Locks the wallet and all currently derived accounts with a password that\r
        * will be needed to unlock it later.\r
        *\r
-       * @param {Key} password Used to lock the wallet\r
+       * @param {string} password Used to lock the wallet\r
        * @returns True if successfully locked\r
        */\r
-       async lock (password: Key): Promise<boolean> {\r
-               if (typeof password === 'string') {\r
-                       password = utf8.toBytes(password)\r
-               }\r
+       async lock (password: string): Promise<boolean> {\r
                try {\r
-                       if (password == null || !(password instanceof Uint8Array)) {\r
-                               throw new Error('password must be string or bytes')\r
+                       if (typeof password !== 'string') {\r
+                               throw new TypeError('Invalid password')\r
                        }\r
                        const serialized = JSON.stringify({\r
                                id: this.id,\r
                                mnemonic: this.#m?.phrase,\r
                                seed: this.#s == null ? this.#s : bytes.toHex(this.#s)\r
                        })\r
-                       const encoded = utf8.toBytes(serialized)\r
                        const success = await SafeWorker.request({\r
                                method: 'store',\r
                                store: 'Wallet',\r
-                               [this.id]: encoded.buffer,\r
-                               password: password.buffer\r
+                               [this.id]: utf8.toBuffer(serialized),\r
+                               password: utf8.toBuffer(password)\r
                        })\r
                        if (!success) {\r
                                throw null\r
@@ -224,8 +230,6 @@ export abstract class Wallet {
                        return this.#locked\r
                } catch (err) {\r
                        throw new Error('failed to lock wallet', { cause: err })\r
-               } finally {\r
-                       bytes.erase(password)\r
                }\r
        }\r
 \r
@@ -279,29 +283,24 @@ export abstract class Wallet {
        /**\r
        * Unlocks the wallet using the same password as used prior to lock it.\r
        *\r
-       * @param {Key} password Used previously to lock the wallet\r
+       * @param {string} password Used previously to lock the wallet\r
        * @returns True if successfully unlocked\r
        */\r
-       async unlock (password: Key): Promise<boolean> {\r
-               if (typeof password === 'string') {\r
-                       password = utf8.toBytes(password)\r
-               }\r
-               let decoded, deserialized, id, mnemonic, seed\r
+       async unlock (password: string): Promise<boolean> {\r
+               let response: NamedData<ArrayBuffer>\r
                try {\r
-                       if (password == null || !(password instanceof Uint8Array)) {\r
-                               throw new Error('password must be string or bytes')\r
+                       if (typeof password !== 'string') {\r
+                               throw new TypeError('Invalid password')\r
                        }\r
                        const response = await SafeWorker.request<ArrayBuffer>({\r
                                method: 'fetch',\r
                                names: this.id,\r
                                store: 'Wallet',\r
-                               password: password.buffer\r
+                               password: utf8.toBuffer(password)\r
                        })\r
-                       decoded = bytes.toUtf8(new Uint8Array(response[this.id]))\r
-                       deserialized = JSON.parse(decoded)\r
-                       id = deserialized.id\r
-                       mnemonic = deserialized.mnemonic\r
-                       seed = deserialized.seed\r
+                       const decoded = bytes.toUtf8(new Uint8Array(response[this.id]))\r
+                       const deserialized = JSON.parse(decoded)\r
+                       let { id, mnemonic, seed } = deserialized\r
                        if (id == null) {\r
                                throw new Error('ID is null')\r
                        }\r
@@ -321,9 +320,6 @@ export abstract class Wallet {
                        return true\r
                } catch (err) {\r
                        throw new Error('failed to unlock wallet', { cause: err })\r
-               } finally {\r
-                       bytes.erase(password)\r
-                       decoded = deserialized = id = mnemonic = seed = undefined\r
                }\r
        }\r
 \r
index 31f258d60862b06efd3fb8e84bf693a5d2d6abb5..a9ed5c586d0147405d3c86c122076836dae5417d 100644 (file)
@@ -46,29 +46,6 @@ await Promise.all([
                        await assert.resolves(wallet.destroy())\r
                })\r
 \r
-               await test('locking and unlocking a Bip44Wallet with random bytes', async () => {\r
-                       const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const key = globalThis.crypto.getRandomValues(new Uint8Array(64))\r
-\r
-                       const lockResult = await wallet.lock(new Uint8Array(key))\r
-                       assert.ok(lockResult)\r
-                       assert.ok('mnemonic' in wallet)\r
-                       assert.ok('seed' in wallet)\r
-                       assert.throws(() => wallet.mnemonic)\r
-                       assert.throws(() => wallet.seed)\r
-\r
-                       const unlockResult = await wallet.unlock(new Uint8Array(key))\r
-\r
-                       assert.equal(unlockResult, true)\r
-                       assert.ok('mnemonic' in wallet)\r
-                       assert.ok('seed' in wallet)\r
-                       assert.equal(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC)\r
-                       assert.equal(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
                await test('locking and unlocking a Bip44Wallet Account with a password', async () => {\r
                        const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
                        await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
@@ -101,23 +78,6 @@ await Promise.all([
                        await assert.resolves(wallet.destroy())\r
                })\r
 \r
-               await test('fail to unlock a Bip44Wallet with different random bytes', async () => {\r
-                       const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const rightKey = globalThis.crypto.getRandomValues(new Uint8Array(64))\r
-                       const wrongKey = globalThis.crypto.getRandomValues(new Uint8Array(64))\r
-                       const lockResult = await wallet.lock(new Uint8Array(rightKey))\r
-\r
-                       await assert.rejects(wallet.unlock(new Uint8Array(wrongKey)), { message: 'Failed to unlock wallet' })\r
-                       assert.equal(lockResult, true)\r
-                       assert.ok('mnemonic' in wallet)\r
-                       assert.ok('seed' in wallet)\r
-                       assert.throws(() => wallet.mnemonic)\r
-                       assert.throws(() => wallet.seed)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
                await test('fail to unlock a Bip44Wallet with different valid inputs', async () => {\r
                        const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
                        const key = globalThis.crypto.getRandomValues(new Uint8Array(64))\r
@@ -196,30 +156,6 @@ await Promise.all([
                        await assert.resolves(wallet.destroy())\r
                })\r
 \r
-               await test('locking and unlocking a Blake2bWallet with random bytes', async () => {\r
-                       const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const key = globalThis.crypto.getRandomValues(new Uint8Array(64))\r
-                       const lockResult = await wallet.lock(new Uint8Array(key))\r
-\r
-                       assert.equal(lockResult, true)\r
-                       assert.ok('mnemonic' in wallet)\r
-                       assert.ok('seed' in wallet)\r
-                       assert.throws(() => wallet.mnemonic)\r
-                       assert.throws(() => wallet.seed)\r
-\r
-                       const unlockResult = await wallet.unlock(new Uint8Array(key))\r
-\r
-                       assert.equal(lockResult, true)\r
-                       assert.equal(unlockResult, true)\r
-                       assert.ok('mnemonic' in wallet)\r
-                       assert.ok('seed' in wallet)\r
-                       assert.equal(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
-                       assert.equal(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
                await test('locking and unlocking a Blake2bWallet Account with a password', async () => {\r
                        const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0)\r
                        await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
@@ -249,23 +185,6 @@ await Promise.all([
                        await assert.resolves(wallet.destroy())\r
                })\r
 \r
-               await test('fail to unlock a Blake2bWallet with different keys', async () => {\r
-                       const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const rightKey = globalThis.crypto.getRandomValues(new Uint8Array(64))\r
-                       const wrongKey = globalThis.crypto.getRandomValues(new Uint8Array(64))\r
-                       const lockResult = await wallet.lock(new Uint8Array(rightKey))\r
-\r
-                       await assert.rejects(wallet.unlock(new Uint8Array(wrongKey)), { message: 'Failed to unlock wallet' })\r
-                       assert.equal(lockResult, true)\r
-                       assert.ok('mnemonic' in wallet)\r
-                       assert.ok('seed' in wallet)\r
-                       assert.throws(() => wallet.mnemonic)\r
-                       assert.throws(() => wallet.seed)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
                await test('fail to unlock a Blake2bWallet with different valid inputs', async () => {\r
                        const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1)\r
                        const key = globalThis.crypto.getRandomValues(new Uint8Array(64))\r