mirror of
https://github.com/hoornet/vega.git
synced 2026-04-24 06:40:01 -07:00
Root cause: switchAccount fetched the nsec from the OS keychain on every switch. Any keychain failure (timeout, Windows Credential Manager quirk, no entry yet) silently fell through to loginWithPubkey → read-only mode. Fix: cache each NDKPrivateKeySigner in-memory (_signerCache map) the moment loginWithNsec succeeds. switchAccount checks the cache first; the OS keychain is now only consulted at startup (restoreSession). Signers are pure crypto objects with no session state — safe to reuse indefinitely. Verified with a 9-switch stress test across 3 accounts (A1→A2→A3→A1→ A2→A1→A3→A2→A1): hasSigner=true and correct pubkey on every switch. Also adds dev tooling: - src/lib/tauri-dev-mock.ts: localStorage-backed keychain + SQLite stubs so the frontend can run in a plain browser for Playwright testing - src/main.tsx: import mock first in DEV mode (no-op in production) - test/gen-accounts.mjs: generates 3 deterministic test keypairs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
80 lines
2.6 KiB
JavaScript
80 lines
2.6 KiB
JavaScript
/**
|
|
* Generates 3 test Nostr accounts for debugging.
|
|
* Run once: node test/gen-accounts.mjs
|
|
* Output saved to test/accounts.json (gitignored)
|
|
*/
|
|
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
import { sha256 } from "@noble/hashes/sha256";
|
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
|
import { writeFileSync } from "fs";
|
|
import { fileURLToPath } from "url";
|
|
import { dirname, join } from "path";
|
|
|
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
|
|
// bech32 encoder (minimal, for nsec/npub)
|
|
const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
function bech32Encode(hrp, data) {
|
|
const values = convertBits(data, 8, 5, true);
|
|
const checksum = createChecksum(hrp, values);
|
|
let result = hrp + "1";
|
|
for (const v of [...values, ...checksum]) result += CHARSET[v];
|
|
return result;
|
|
}
|
|
function polymod(values) {
|
|
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
let chk = 1;
|
|
for (const v of values) {
|
|
const b = chk >> 25;
|
|
chk = ((chk & 0x1ffffff) << 5) ^ v;
|
|
for (let i = 0; i < 5; i++) if ((b >> i) & 1) chk ^= GEN[i];
|
|
}
|
|
return chk;
|
|
}
|
|
function hrpExpand(hrp) {
|
|
const ret = [];
|
|
for (const c of hrp) ret.push(c.charCodeAt(0) >> 5);
|
|
ret.push(0);
|
|
for (const c of hrp) ret.push(c.charCodeAt(0) & 31);
|
|
return ret;
|
|
}
|
|
function createChecksum(hrp, data) {
|
|
const values = [...hrpExpand(hrp), ...data, 0, 0, 0, 0, 0, 0];
|
|
const mod = polymod(values) ^ 1;
|
|
return Array.from({ length: 6 }, (_, i) => (mod >> (5 * (5 - i))) & 31);
|
|
}
|
|
function convertBits(data, from, to, pad = false) {
|
|
let acc = 0, bits = 0;
|
|
const result = [];
|
|
const maxv = (1 << to) - 1;
|
|
for (const v of data) {
|
|
acc = (acc << from) | v;
|
|
bits += from;
|
|
while (bits >= to) { bits -= to; result.push((acc >> bits) & maxv); }
|
|
}
|
|
if (pad && bits > 0) result.push((acc << (to - bits)) & maxv);
|
|
return result;
|
|
}
|
|
|
|
const accounts = [];
|
|
for (let i = 0; i < 3; i++) {
|
|
const privBytes = secp256k1.utils.randomPrivateKey();
|
|
const pubBytes = secp256k1.getPublicKey(privBytes, true).slice(1); // x-only
|
|
const privHex = bytesToHex(privBytes);
|
|
const pubHex = bytesToHex(pubBytes);
|
|
const nsec = bech32Encode("nsec", Array.from(privBytes));
|
|
const npub = bech32Encode("npub", Array.from(pubBytes));
|
|
accounts.push({ label: `Test Account ${i + 1}`, privHex, pubHex, nsec, npub });
|
|
}
|
|
|
|
const outPath = join(__dir, "accounts.json");
|
|
writeFileSync(outPath, JSON.stringify(accounts, null, 2));
|
|
console.log("Generated accounts:");
|
|
for (const a of accounts) {
|
|
console.log(`\n${a.label}`);
|
|
console.log(` npub: ${a.npub}`);
|
|
console.log(` nsec: ${a.nsec}`);
|
|
console.log(` pub: ${a.pubHex}`);
|
|
}
|
|
console.log(`\nSaved to ${outPath}`);
|