Files
archy/neode-ui/src/utils/seedqr.ts
T
archipelagoandClaude Fable 5 0880824fb3
Demo images / Build & push demo images (push) Successful in 2m52s
fix(ui): seed QRs use the SeedQR standard so hardware wallets can scan them
Plain-text seed QRs didn't scan into Passport Prime — wallets that
import seeds by QR (Passport, SeedSigner, Keystone, Nunchuk, Sparrow)
expect the SeedQR standard: each BIP39 word as its zero-padded 4-digit
wordlist index, concatenated into a numeric QR.

- new utils/seedqr.ts encodes BIP39 words per the SeedSigner spec
  (@scure/bip39 wordlist; vector-checked abandon=0000, zoo=2047)
- new shared SeedRevealPanel (Words/QR tabs, tap-to-reveal blur) now
  backs the LND reveal AND the Settings→Backup recovery-phrase reveal,
  so every current and future seed reveal behaves the same
- onboarding seed + Settings reveal: QR defaults to SeedQR with a
  plain-text toggle
- LND seed stays plain-text-only with an explicit note: aezeed is not
  BIP39 and only restores into LND-based wallets (Zeus/Blixt/another
  node) — SeedQR-encoding it would just mislead

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:09:43 -04:00

22 lines
1015 B
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SeedQR encoding (SeedSigner standard, supported by Passport/Passport Prime,
* SeedSigner, Keystone, Nunchuk, Sparrow, …): each BIP39 word becomes its
* zero-padded 4-digit wordlist index (00002047), concatenated into one
* digit stream and rendered as a numeric-mode QR. A 24-word seed is 96
* digits. Spec: github.com/SeedSigner/seedsigner/blob/main/docs/seed_qr
*
* Only valid for real BIP39 mnemonics — LND's aezeed shares the wordlist but
* is NOT BIP39, and hardware wallets cannot import it; never SeedQR-encode it.
*/
export async function toSeedQrDigits(words: string[]): Promise<string | null> {
if (words.length === 0) return null
const { wordlist } = await import('@scure/bip39/wordlists/english.js')
const digits: string[] = []
for (const raw of words) {
const idx = wordlist.indexOf(raw.trim().toLowerCase())
if (idx < 0) return null // not a BIP39 word — caller falls back to text
digits.push(idx.toString().padStart(4, '0'))
}
return digits.join('')
}