// Per-wallet extra derivation paths — additional account-level BIP32 // paths that this wallet's balance / history scans should look at // beyond the 6 built-in defaults (BIP44/49/84/86 at DGB coin type, plus // the 2018-19 breadwallet-mobile m/0' and the Bitcoin-coin-type-under- // BIP44 m/44'/0'/0' reused-coin-type variant). // // Populated from PathSearchPanel — when the recovery sweep finds // activity on a layout the default scan doesn't cover, the user // promotes that path here in one click. Stored per wallet by the same // walletIdHash the SWR scan cache uses, so a phrase moved to a new // device keeps its identity but a different wallet on the same device // has its own list. // // Cleartext localStorage: the entries are public-derivation metadata // (an xpath and a script kind), not secrets. Someone reading local // storage sees which extra paths this wallet asked us to scan, which // leaks the same information as an on-chain address the user handed // out. No key material. import { walletIdHash } from './scan-cache.js'; export type ExtraPathScript = 'p2pkh' | 'p2sh-p2wpkh' | 'p2wpkh'; export interface ExtraPath { /** BIP32 path of the account node (e.g. "m/44'/20'/1'"). */ path: string; /** Output script kind for addresses derived under this account. */ script: ExtraPathScript; /** Optional display label; UI synthesizes one if absent. */ label?: string; /** BIP32 master-key HMAC string. Absent = standard "Bitcoin seed". The * 2018-19 DigiByte mobile wallets used "DigiByte seed". */ hmac?: string; } /** Identity of an extra path: path + script + hmac. */ export function extraKey(e: { path: string; script: string; hmac?: string }): string { return `${e.path}::${e.script}::${e.hmac ?? ''}`; } const KEY_PREFIX = 'dgb-wallet:extra-paths:'; const CHANGE_EVENT = 'dgb-wallet:extra-paths-changed'; /** Compute the storage key for a wallet's extras list. Async because * walletIdHash is Web Crypto SHA-256 based. Passphrase, when present, * gives a passphrased wallet its own extras list distinct from the * bare-phrase twin. */ export async function extrasKey(phrase: string, kind: string, passphrase?: string): Promise { const id = await walletIdHash(phrase, kind, passphrase); return `${KEY_PREFIX}${id}`; } export function readExtras(storageKey: string): ExtraPath[] { try { const raw = localStorage.getItem(storageKey); if (!raw) return []; const parsed = JSON.parse(raw) as unknown; if (!Array.isArray(parsed)) return []; // Filter defensively so a corrupt entry can't crash the caller. return parsed.filter((e): e is ExtraPath => !!e && typeof (e as ExtraPath).path === 'string' && ['p2pkh', 'p2sh-p2wpkh', 'p2wpkh'].includes((e as ExtraPath).script) ); } catch { return []; } } export function writeExtras(storageKey: string, paths: ExtraPath[]): void { try { if (paths.length === 0) localStorage.removeItem(storageKey); else localStorage.setItem(storageKey, JSON.stringify(paths)); window.dispatchEvent(new CustomEvent(CHANGE_EVENT, { detail: { storageKey } })); } catch { /* private mode / quota */ } } export function addExtra(storageKey: string, entry: ExtraPath): boolean { const existing = readExtras(storageKey); if (existing.some((e) => extraKey(e) === extraKey(entry))) return false; writeExtras(storageKey, [...existing, entry]); return true; } export function removeExtra(storageKey: string, path: string, script: ExtraPathScript, hmac?: string): void { const k = extraKey({ path, script, hmac }); writeExtras(storageKey, readExtras(storageKey).filter((e) => extraKey(e) !== k)); } /** Subscribe to any extras-list change (any wallet). Returns unsubscribe. */ export function subscribeExtras(cb: (storageKey: string | null) => void): () => void { const handler = (e: Event): void => { const key = (e as CustomEvent<{ storageKey?: string }>).detail?.storageKey ?? null; cb(key); }; window.addEventListener(CHANGE_EVENT, handler as EventListener); return () => window.removeEventListener(CHANGE_EVENT, handler as EventListener); }