From 62dcebc53fbcb4b3edb4cca664114d64ee03ae6a Mon Sep 17 00:00:00 2001 From: BitcoinQnA Date: Fri, 11 Sep 2026 06:43:04 +0100 Subject: [PATCH 1/2] Seed Tool: QR scanning, Nostr keys, descriptors, passphrase lists, vectors New - QR scanner (camera, or pick an image), decoded in the page with jsQR and bc-ur via build_qrscan.js. Enter words reads a SeedQR, Compact SeedQR or seed-words QR into the word boxes for checking; nothing loads until Load Seed. The PSBT Inspector reads a PSBT QR or an animated UR code (ur:psbt, ur:crypto-psbt) with a progress bar. Read only. - Standard SeedQR next to Compact SeedQR, using a new numeric mode in the QR library (25x25 for 12 words, 29x29 for 24). - Nostr Keys tool: NIP-06 and the BIP-85 Nostr application. - Output descriptors in Derived Addresses for BIP-44/49/84/86 accounts: combined <0;1>, receive and change, with key origin and checksum. - Passphrase tester takes a list of passphrases, with progress, Stop and "Use this passphrase". Taproot paths now give Taproot addresses. Improved - Seed input tabs have text labels. - Word boxes are checked as typed and on leaving, with the nearest word suggested; Load Seed marks the bad box and explains a checksum failure. Seed word boxes have labels, and spellcheck and autocorrect are off. - Clear seed also wipes the new fields. Tests - test/keys.test.js pins the bundled libraries against published vectors: BIP-39 (24 English), BIP-32, BIP-85 (every application through the tool's own code), NIP-06, BIP-85 Nostr, BIP-380, BIP-84/86 addresses from descriptors, SeedQR. - test/qrscan.test.js and test/ui.test.js cover scanning and the new UI. Co-Authored-By: Claude Opus 5 --- README.md | 2 +- build_qrscan.js | 48 ++ dist/index.html | 1122 ++++++++++++++++++++++++++++-- package-lock.json | 114 +++ package.json | 4 +- src/www/css/styles.css | 69 +- src/www/dev.html | 179 +++-- src/www/js/dom.js | 205 +++++- src/www/js/keys.js | 272 ++++++++ src/www/js/lib/_qrscan_entry.js | 59 ++ src/www/js/lib/qrcode.js | 45 +- src/www/js/lib/qrscan.js | 82 +++ src/www/js/shell.js | 266 ++++++- test/fixtures/bip39-english.json | 146 ++++ test/keys.test.js | 292 ++++++++ test/qrscan.test.js | 190 +++++ test/ui.test.js | 165 +++++ 17 files changed, 3122 insertions(+), 138 deletions(-) create mode 100644 build_qrscan.js create mode 100644 src/www/js/keys.js create mode 100644 src/www/js/lib/_qrscan_entry.js create mode 100644 src/www/js/lib/qrscan.js create mode 100644 test/fixtures/bip39-english.json create mode 100644 test/keys.test.js create mode 100644 test/qrscan.test.js create mode 100644 test/ui.test.js diff --git a/README.md b/README.md index d16b2ac..6c7db10 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ node build.js sha256sum dist/index.html ``` -`build.js` needs Node.js only; no packages are installed. The bundled libraries in `src/www/js/lib` are committed build outputs: six are built with esbuild by the `build_*.js` scripts from the pinned versions in `package-lock.json`, the rest are older browserify builds from `libs/`. +`build.js` needs Node.js only; no packages are installed. The bundled libraries in `src/www/js/lib` are committed build outputs: seven are built with esbuild by the `build_*.js` scripts from the pinned versions in `package-lock.json`, the rest are older browserify builds from `libs/`. Before tagging a release, `npm run release:check` runs the tests, rebuilds `dist/index.html` and fails if the committed file differs from the source. diff --git a/build_qrscan.js b/build_qrscan.js new file mode 100644 index 0000000..d7bbace --- /dev/null +++ b/build_qrscan.js @@ -0,0 +1,48 @@ +// Bundle src/www/js/lib/_qrscan_entry.js β†’ src/www/js/lib/qrscan.js using +// esbuild + node-polyfill. Run via `node build_qrscan.js`. +// +// jsQR decodes QR codes; @ngraveio/bc-ur reassembles animated UR codes. Node's +// crypto module gets the same small shim as build_sp.js, and the build fails +// if the full crypto polyfill ends up bundled. + +const path = require('path'); +const esbuild = require('esbuild'); +const { polyfillNode } = require('esbuild-plugin-polyfill-node'); + +const cryptoShim = { + name: 'crypto-shim', + setup(build) { + build.onResolve({ filter: /^crypto$/ }, () => ({ + path: path.join(__dirname, 'src/www/js/lib/_crypto_shim.js'), + })); + }, +}; + +esbuild.build({ + entryPoints: ['src/www/js/lib/_qrscan_entry.js'], + bundle: true, + format: 'iife', + globalName: 'qrScan', + minify: true, + target: 'es2020', + outfile: 'src/www/js/lib/qrscan.js', + metafile: true, + plugins: [ + cryptoShim, + polyfillNode({ + globals: { buffer: true, process: true }, + polyfills: { crypto: false, buffer: true, process: true, util: true, stream: true, events: true, string_decoder: true, assert: true }, + }), + ], + define: { + 'global': 'globalThis', + }, +}).then((result) => { + const leaked = Object.keys(result.metafile.inputs) + .filter((input) => /crypto-browserify|node_modules\/elliptic\//.test(input)); + if (leaked.length) throw new Error(`qrscan.js must not bundle these: ${leaked.join(', ')}`); + console.log('qrscan.js bundled'); +}).catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/dist/index.html b/dist/index.html index f193e9a..1f6b327 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1349,8 +1349,10 @@ border: none; outline: none; cursor: pointer; - padding: 10px 16px; - font-size: 1.125rem; + padding: 10px 8px; + font-size: 0.875rem; + font-weight: 600; + line-height: 1.3; transition: background-color 120ms, color 120ms; border-radius: 7px; } @@ -3790,6 +3792,69 @@ .cards { grid-template-columns: 1fr; } } +/* ============================================================ + Load tabs, word boxes, button rows, QR scanner, Nostr keys + ============================================================ */ +.tab button span[aria-hidden="true"] { + font-size: 1.125rem; + margin-right: 4px; +} +.tab button:focus-visible { + outline: 2px solid var(--q-accent-hi); + outline-offset: 2px; +} +.inputMnemonic-word.word-invalid { + border-color: var(--q-danger); + box-shadow: 0 0 0 1px var(--q-danger); +} +.button-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + margin: 8px 0; +} +.qr-scan__video { + display: block; + width: 100%; + max-height: 50vh; + object-fit: cover; + border-radius: var(--radius-sm); + background: #000; + margin: 8px 0; +} +.qr-scan__progress { + height: 6px; + border-radius: 3px; + background: var(--q-border-soft); + overflow: hidden; + margin: 8px 0; +} +.qr-scan__bar { + height: 100%; + width: 0; + background: var(--q-accent-hi); + transition: width 150ms; +} +.qr-scan__file { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + overflow: hidden; +} +.qr-scan__file:focus-visible + label { + outline: 2px solid var(--q-accent-hi); + outline-offset: 2px; +} +.nostr-keys details { + margin: 8px 0 16px; +} +.nostr-keys summary { + cursor: pointer; + color: var(--q-muted); +} + @@ -3875,6 +3940,22 @@

Clear the loaded seed?

+ + +