feat: BIP-39 master seed for unified key derivation

Replace fragmented random key generation with a single 24-word BIP-39
mnemonic that deterministically derives all node keys: Ed25519 (DID),
secp256k1 (Nostr/Bitcoin), BIP-84 xprv (Bitcoin Core), and LND aezeed
entropy. New onboarding flow: seed generate → word verification → identity
naming. Restore path enabled via 24-word entry. Includes seed RPC handlers,
mock backend support, LND/Bitcoin Core wallet-from-seed integration, and
UI polish across settings and discover views.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-31 01:41:24 +01:00
co-authored by Claude Opus 4.6
parent 5da9e217e6
commit 19dcfd4f31
50 changed files with 2200 additions and 258 deletions
+37 -32
View File
@@ -84,9 +84,13 @@ function getNavBarItems(): HTMLElement[] {
function isNavBarItem(el: HTMLElement | null): boolean {
if (!el) return false
return isInZone(el, 'main') &&
!el.hasAttribute('data-controller-container') &&
!el.closest('[data-controller-container]')
if (!isInZone(el, 'main')) return false
if (el.hasAttribute('data-controller-container') || el.closest('[data-controller-container]')) return false
// On container-free pages (e.g. Settings), don't classify elements as nav bar items —
// let them fall through to the main zone handler which supports linear up/down/right nav.
const zone = document.querySelector('[data-controller-zone="main"]')
if (zone && !zone.querySelector('[data-controller-container]')) return false
return true
}
/** Inner focusables within a container (buttons, links — not the container itself) */
@@ -425,6 +429,13 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
if (dest) {
focusEl(dest)
} else {
// Check if this is a container-free page (e.g. Settings) — focus first button immediately
const zone = document.querySelector('[data-controller-zone="main"]') as HTMLElement | null
const hasAnyContainers = zone?.querySelector('[data-controller-container]')
if (!hasAnyContainers && zone) {
const focusable = getFocusableElements(zone)
if (focusable[0]) { focusEl(focusable[0]); return }
}
// Containers not rendered yet (route transition / animation in progress)
// Poll until they appear, up to 1s
let attempts = 0
@@ -436,9 +447,8 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
focusEl(retryContainers[0])
} else if (attempts >= 10) {
clearInterval(poll)
// No containers on this page (e.g. Settings) — focus first focusable element
const z = document.querySelector('[data-controller-zone="main"]') as HTMLElement | null
if (z) { const f = getFocusableElements(z); if (f[0]) focusEl(f[0]) }
// Last resort: focus first focusable element
if (zone) { const f = getFocusableElements(zone); if (f[0]) focusEl(f[0]) }
}
}, 100)
}
@@ -477,31 +487,20 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
return
}
if (dir === 'down') {
// Down from nav bar → jump to containers (remember tab for Up return)
rememberFocus('navBar', activeEl)
const containers = getContainers()
const nearest = findNearestInDirection(activeEl, containers, 'down')
if (nearest) { rememberFocus('main', nearest); focusEl(nearest); return }
// Fallback: just focus first container
if (containers[0]) { rememberFocus('main', containers[0]); focusEl(containers[0]); return }
// Containers not rendered yet — poll until they appear
let attempts = 0
const poll = setInterval(() => {
attempts++
const retryContainers = getContainers()
if (retryContainers[0]) {
clearInterval(poll)
rememberFocus('main', retryContainers[0])
focusEl(retryContainers[0])
} else if (attempts >= 10) {
clearInterval(poll)
}
}, 100)
return
if (dir === 'down' || dir === 'up') {
// Up/Down from standalone element → find nearest focusable (container or button) in direction.
// Searches containers + standalone elements together so mixed pages (Settings) don't jump.
if (dir === 'down') rememberFocus('navBar', activeEl)
const zone = document.querySelector('[data-controller-zone="main"]') as HTMLElement | null
if (zone) {
const allFocusable = getFocusableElements(zone).filter(el =>
el.hasAttribute('data-controller-container') ||
!el.closest('[data-controller-container]')
)
const target = findNearestInDirection(activeEl, allFocusable, dir)
if (target) { focusEl(target); return }
}
}
// Up from nav bar → nothing (use Escape to go to sidebar)
return
}
@@ -509,8 +508,14 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
if (isInZone(activeEl, 'main')) {
const containers = getContainers()
// Try spatial nav to another container
const next = findNearestInDirection(activeEl, containers, dir)
// Try spatial nav to containers + standalone focusables (not inner buttons).
// This handles mixed pages (e.g. Settings) where containers and buttons coexist.
const zone = document.querySelector('[data-controller-zone="main"]') as HTMLElement | null
const navTargets = zone ? getFocusableElements(zone).filter(el =>
el.hasAttribute('data-controller-container') ||
!el.closest('[data-controller-container]')
) : containers
const next = findNearestInDirection(activeEl, navTargets, dir)
if (next) {
rememberFocus('main', next)
focusEl(next)