dgb-wallet/packages/web-ui/src/wallet-extra-paths.ts
Digibyte.X 989a2696c9 recovery: official 2018-19 DigiByte mobile wallets derived the master key with HMAC "DigiByte seed"
The BreadWallet-based DigiByte Android/iOS apps used HMAC-SHA512 key
"DigiByte seed" (not BIP32's "Bitcoin seed") when turning the BIP39 seed
into the master node, so the same 12 words give a different key tree and
a standard m/0' scan finds nothing. rootFromSeed() takes an optional HMAC
key; balance scan, history, send and the derivation-path search now
cover m/0' under both keys, and user-added extra paths carry the key.
2026-10-02 17:44:16 +02:00

95 lines
4 KiB
TypeScript

// 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<string> {
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);
}