aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-03 23:43:18 -0700
committerAdam Soltys <[email protected]>2026-08-03 23:43:18 -0700
commit9a9e2a7e64e02d080d121e1f3079573d00ab28cf (patch)
tree22fe70f90dd404ec49385f554d790a063d9c61dc /tools
parent3bc4bf2cb95c1bcf61a10d8b892ee9ccaeffcf2d (diff)
downloadcoinosv3-9a9e2a7e64e02d080d121e1f3079573d00ab28cf.tar.gz
coinosv3-9a9e2a7e64e02d080d121e1f3079573d00ab28cf.zip
Logout shouldn't ask for a password nobody set
Wallets save themselves to this device as soon as you open one, encrypted under an empty password unless you choose one. Logging out still sent you to 'Unlock saved wallets — enter your password', which asks for something that doesn't exist and makes logout look like it did something drastic. It didn't: logout only drops the keys from memory, and 'Clear all' is the one that deletes. With no password, logout now returns to the wallet list — honest, since nothing is protecting those wallets and a blank prompt protects nothing. With a password set it prompts exactly as before. Fixed underneath: mergeVaultList derived the comparison xpub with accountXpubFor's mainnet default, so on any other network it never matched the stored one and every unlock re-added the same wallet as a fresh entry — visible as the same wallet listed twice, once watch-only. The seed-mismatch checks in 'load recovery phrase' had the same default and would have rejected the correct seed on testnet. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'tools')
-rw-r--r--tools/logout-test.js107
1 files changed, 107 insertions, 0 deletions
diff --git a/tools/logout-test.js b/tools/logout-test.js
new file mode 100644
index 0000000..da2bb99
--- /dev/null
+++ b/tools/logout-test.js
@@ -0,0 +1,107 @@
+// Logging out should ask for a password only when there is one.
+//
+// Wallets save themselves to this device under an empty password unless the
+// user sets one, so the unlock screen was demanding a password nobody had
+// chosen — which made logout look like it had done something drastic. It
+// hasn't: logout keeps every saved wallet, and only "Clear all" deletes them.
+//
+// Run: bun tools/logout-test.js
+import puppeteer from 'puppeteer-core';
+import { generateMnemonic } from '@scure/bip39';
+import { wordlist } from '@scure/bip39/wordlists/english';
+import { buildHtml } from '../build.js';
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+let ok = true;
+const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + String(d).slice(0, 160) : ''}`); if (!c) ok = false; };
+
+const html = await buildHtml({ minify: true, pwa: false });
+const server = Bun.serve({ port: 5235, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
+const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const page = await browser.newPage();
+const body = () => page.evaluate(() => document.body.innerText);
+const click = (sel, t) => page.evaluate((s, x) => { const e = [...document.querySelectorAll(s)].find((n) => n.textContent.trim().toLowerCase().includes(x.toLowerCase())); if (e) { e.click(); return true; } return false; }, sel, t);
+const waitText = async (x, ms = 20000) => { for (let i = 0; i < ms / 250; i++) { if ((await body()).toLowerCase().includes(x.toLowerCase())) return true; await sleep(250); } return false; };
+const logout = async () => {
+ await page.evaluate(() => document.querySelector('.header-avatar')?.click());
+ await sleep(400);
+ await click('.icon-select-item', 'Logout');
+ await sleep(1500);
+};
+const vaultSize = () => page.evaluate(() => (localStorage.getItem('btc-wallet-vault') || '').length);
+
+try {
+ await page.goto('http://localhost:5235/', { waitUntil: 'domcontentloaded' });
+ await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await sleep(400);
+ await click('button', 'I already have a wallet');
+ await sleep(300);
+ await click('button', 'Import existing');
+ await sleep(300);
+ await page.waitForSelector('textarea');
+ await page.type('textarea', generateMnemonic(wordlist));
+ await click('button', 'Open wallet');
+ check('wallet open', await waitText('receive', 15000));
+ await sleep(2500);
+ const savedBytes = await vaultSize();
+ check('it saved itself to the device', savedBytes > 0, `${savedBytes} bytes of vault`);
+
+ console.log('\n[logging out with no password set]');
+ await logout();
+ let txt = await body();
+ check('no password is demanded', !/enter your password|unlock saved wallets/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | '));
+ check('the saved wallet is still there', (await vaultSize()) === savedBytes);
+ check('and it is offered back', /wallet 1|wallets/i.test(txt), txt.slice(0, 140).replace(/\n+/g, ' | '));
+
+ const listed = await page.evaluate(() => [...document.querySelectorAll('button, .item, .acct')].map((e) => e.textContent.trim()).filter((x) => /Wallet 1/.test(x)));
+ check('and only once — no phantom duplicate', listed.length === 1, JSON.stringify(listed));
+
+ console.log('\n[getting back in]');
+ await page.evaluate(() => { const e = [...document.querySelectorAll('*')].find((n) => n.children.length === 0 && /Wallet 1/.test(n.textContent) && n.closest('button, .item, .acct')); if (e) e.closest('button, .item, .acct').click(); });
+ await sleep(2000);
+ check('one tap returns to the wallet', await waitText('receive', 15000));
+
+ console.log('\n[now with a password]');
+ // "Change password" lives on the Wallets screen, behind the header selector
+ await page.evaluate(() => { const b = [...document.querySelectorAll('.row.between button')].find((e) => /Wallet/.test(e.textContent)); if (b) b.click(); });
+ await sleep(1200);
+ check('on the wallets screen', /wallets/i.test(await body()));
+ await click('button', 'Change password');
+ await sleep(1000);
+ const fields = await page.$$('input[type=password]');
+ check('the change-password form is up', fields.length >= 3, `${fields.length} field(s)`);
+ // current (empty) / new / confirm
+ await fields[fields.length - 2].type('hunter22');
+ await fields[fields.length - 1].type('hunter22');
+ await click('button', 'Save');
+ const pwSet = await waitText('password changed', 8000);
+ check('a password is actually set', pwSet, (await body()).slice(0, 120).replace(/\n+/g, ' | '));
+
+ // back into the wallet — the Wallets screen has no avatar menu to log out from
+ await click('button', 'Back');
+ await sleep(1500);
+ check('back in the wallet', await waitText('receive', 15000));
+ await logout();
+ txt = await body();
+ check('now it does ask', /unlock saved wallets|enter your password/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | '));
+ check('the wallet is still saved', (await vaultSize()) > 0);
+
+ console.log('\n[clear all is the destructive one]');
+ await click('button', 'Use another wallet');
+ await sleep(1500);
+ await page.evaluate(() => { const b = [...document.querySelectorAll('.row.between button')].find((e) => /Wallet/.test(e.textContent)); if (b) b.click(); });
+ await sleep(1200);
+ await click('button', 'Clear all');
+ await sleep(800);
+ const txt2 = await body();
+ check('it warns before wiping', /removes every wallet/i.test(txt2), txt2.slice(0, 140).replace(/\n+/g, ' | '));
+ check('and it has not wiped anything yet', (await vaultSize()) > 0);
+} catch (e) {
+ check('run completed', false, e.message);
+} finally {
+ await browser.close();
+ server.stop(true);
+}
+console.log(ok ? '\n✅ logout keeps your wallets' : '\n❌ failures above');
+process.exit(ok ? 0 : 1);