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.
95 lines
4 KiB
TypeScript
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);
|
|
}
|