feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets

Ported from Digibyte.X/dgb-wallet @ 989a2696.

BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's
official 2018-19 Android/iOS wallets were BreadWallet forks and used
"DigiByte seed" as the HMAC key instead, so the same twelve words produce a
completely different key tree — every address differs, and a standard scan
finds nothing at all. Anyone importing one of those seeds into Aegis got a
valid, empty address and a zero balance with no way to tell why. Same shape
as the Bitcoin.com coin-type-0 problem.

- rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC
  key, matching upstream, and exports HMAC_BITCOIN_SEED /
  HMAC_DIGIBYTE_SEED.
- lib/import-derive.js grows the same option, since that is what actually
  derives the address on an import, and importWallet records the variant on
  the spec so the entry says which tree its stored address came from.
- The path scanner added in 0.24.0 now covers DigiByte: all four purposes
  under the standard key, plus m/0' and BIP44 under the legacy one. So the
  answer to "which tree holds my coins" is a scan rather than a guess, and
  the Use button carries the key along with the path.

Verified against an independent HMAC-SHA512 computation, and end to end: the
same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under
the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn.

Also fixes a coupling this exposed: lib/import-derive.js derives taproot
addresses but never called initEccLib, relying on chain-btc.js (and formerly
lib/dgb/core/address.js, before it went lazy here) doing it at load time. It
now installs the schnorr backend itself, once, so its taproot output no
longer depends on an unrelated module's import order.

The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is
upstream-only for now — Aegis's imported DGB adapter watches a single stored
address rather than scanning a gap.
This commit is contained in:
Local Dev 2026-10-03 14:13:25 +02:00
parent c1988b9674
commit 469ff482ea
5 changed files with 145 additions and 34 deletions

View file

@ -35,8 +35,11 @@ let ctx = null;
async function loadDeps(api) { async function loadDeps(api) {
const { secp256k1 } = await api.import("@noble/curves/secp256k1.js"); const { secp256k1 } = await api.import("@noble/curves/secp256k1.js");
const { ed25519 } = await api.import("@noble/curves/ed25519.js"); const { ed25519 } = await api.import("@noble/curves/ed25519.js");
const { sha256 } = await api.import("@noble/hashes/sha2.js"); const { sha256, sha512 } = await api.import("@noble/hashes/sha2.js");
const { hkdf } = await api.import("@noble/hashes/hkdf.js"); const { hkdf } = await api.import("@noble/hashes/hkdf.js");
// For DigiByte's legacy master-key derivation — see HMAC_DIGIBYTE_SEED in
// lib/dgb/core/hd.js.
const { hmac } = await api.import("@noble/hashes/hmac.js");
const { ripemd160 } = await api.import("@noble/hashes/legacy.js"); const { ripemd160 } = await api.import("@noble/hashes/legacy.js");
const { keccak_256 } = await api.import("@noble/hashes/sha3.js"); const { keccak_256 } = await api.import("@noble/hashes/sha3.js");
const { blake2b } = await api.import("@noble/hashes/blake2.js"); const { blake2b } = await api.import("@noble/hashes/blake2.js");
@ -89,6 +92,7 @@ async function loadDeps(api) {
bip32Factory: BIP32Factory, bip32Factory: BIP32Factory,
ecpairFactory: ECPairFactory, ecpairFactory: ECPairFactory,
bip39: api.require("bip39"), bip39: api.require("bip39"),
hmac, sha512,
}); });
dgbCore = await import(pathToFileURL(path.join(api.folder, "lib/dgb/core/index.js")).href); dgbCore = await import(pathToFileURL(path.join(api.folder, "lib/dgb/core/index.js")).href);
dgbPsbt = await import(pathToFileURL(path.join(api.folder, "lib/dgb/psbt/index.js")).href); dgbPsbt = await import(pathToFileURL(path.join(api.folder, "lib/dgb/psbt/index.js")).href);
@ -125,7 +129,7 @@ async function loadDeps(api) {
// chain-native private key). Used by the importWallet handler to compute // chain-native private key). Used by the importWallet handler to compute
// the address client-side before wallet-imports.enc stores the material. // the address client-side before wallet-imports.enc stores the material.
const derive = require("./lib/import-derive.js")({ const derive = require("./lib/import-derive.js")({
HDKey, secp256k1, ed25519, sha256, ripemd160, keccak_256, blake2b, HDKey, secp256k1, ed25519, sha256, sha512, hmac, ripemd160, keccak_256, blake2b,
cashaddr, base58check, bitcoinjs, bip32Factory: BIP32Factory, cashaddr, base58check, bitcoinjs, bip32Factory: BIP32Factory,
ecpairFactory: ECPairFactory, ecc, bip39, dgbCore, ecpairFactory: ECPairFactory, ecc, bip39, dgbCore,
}); });
@ -1220,7 +1224,19 @@ function registerPanelMessages(api) {
} else if (p && p.mnemonic) { } else if (p && p.mnemonic) {
spec.kind = "seed"; spec.seed = der.mnemonicToSeedHex(String(p.mnemonic).trim()); spec.kind = "seed"; spec.seed = der.mnemonicToSeedHex(String(p.mnemonic).trim());
spec.path = String(p.path || defaults[chain].path); spec.path = String(p.path || defaults[chain].path);
address = chain === "btc" ? der.btc.fromSeed(spec.seed, spec.path, net) : der.dgb.fromSeed(spec.seed, spec.path); if (chain === "btc") address = der.btc.fromSeed(spec.seed, spec.path, net);
else {
// A DigiByte seed can belong to the 2018-19 official mobile
// wallets, which built the master node with HMAC key
// "DigiByte seed". Carry the choice onto the spec so the entry
// records which tree the stored address came from.
const hk = String((p && p.hmacKey) || der.dgb.HMAC_BITCOIN_SEED);
if (hk !== der.dgb.HMAC_BITCOIN_SEED && hk !== der.dgb.HMAC_DIGIBYTE_SEED) {
throw new Error("unknown DigiByte master-key variant");
}
if (hk !== der.dgb.HMAC_BITCOIN_SEED) spec.hmacKey = hk;
address = der.dgb.fromSeed(spec.seed, spec.path, hk);
}
} else { throw new Error("supply mnemonic or wif"); } } else { throw new Error("supply mnemonic or wif"); }
// Theseus's api.vault.imports.add validates a `cashaddr` field (from // Theseus's api.vault.imports.add validates a `cashaddr` field (from
// when only BCH imports existed). Reuse the same field name for // when only BCH imports existed). Reuse the same field name for
@ -2132,6 +2148,19 @@ function registerPanelMessages(api) {
{ path: "m/44'/145'/0'", note: "account node — some QR exports" }, { path: "m/44'/145'/0'", note: "account node — some QR exports" },
{ path: "m/0'/0/0", note: "pre-BIP44" }, { path: "m/0'/0/0", note: "pre-BIP44" },
], ],
// DigiByte. Both master-key variants are covered, because the 2018-19
// official mobile wallets (BreadWallet forks) used HMAC "DigiByte seed"
// instead of BIP32's "Bitcoin seed" — the same words then produce a
// completely different key tree and a standard scan finds nothing.
// Finding from Digibyte.X/dgb-wallet @ 989a2696.
"dgb/mainnet": [
{ path: "m/84'/20'/0'/0/0", note: "BIP84 native SegWit — dgb1q…, the modern default" },
{ path: "m/44'/20'/0'/0/0", note: "BIP44 legacy — D…" },
{ path: "m/49'/20'/0'/0/0", note: "BIP49 wrapped SegWit — S…" },
{ path: "m/86'/20'/0'/0/0", note: "BIP86 Taproot — dgb1p…" },
{ path: "m/0'/0/0", note: "official 2018-19 mobile wallet", hmacKey: "DigiByte seed" },
{ path: "m/44'/20'/0'/0/0", note: "BIP44 with the 2018-19 mobile master key", hmacKey: "DigiByte seed" },
],
"bch/chipnet": [ "bch/chipnet": [
{ path: "m/44'/145'/0'/0/0", note: "BCH coin type on chipnet — most chipnet tooling" }, { path: "m/44'/145'/0'/0/0", note: "BCH coin type on chipnet — most chipnet tooling" },
{ path: "m/44'/1'/0'/0/0", note: "BIP44 testnet coin type" }, { path: "m/44'/1'/0'/0/0", note: "BIP44 testnet coin type" },
@ -2141,7 +2170,7 @@ function registerPanelMessages(api) {
api.onMessage("seedPathCandidates", (p, m) => { api.onMessage("seedPathCandidates", (p, m) => {
fromPanel(m); fromPanel(m);
const key = `${String(p && p.chain || "")}/${String(p && p.network || "")}`; const key = `${String(p && p.chain || "")}/${String(p && p.network || "")}`;
return { key, candidates: SEED_PATH_CANDIDATES[key] || [] }; return { key, candidates: (SEED_PATH_CANDIDATES[key] || []).map((c) => ({ ...c })) };
}); });
api.onMessage("scanSeedPaths", async (p, m) => { api.onMessage("scanSeedPaths", async (p, m) => {
@ -2158,20 +2187,39 @@ function registerPanelMessages(api) {
try { seedHex = ctx.d.derive.mnemonicToSeedHex(mnemonic); } try { seedHex = ctx.d.derive.mnemonicToSeedHex(mnemonic); }
catch (e) { throw new Error("That does not look like a BIP39 seed phrase: " + (e?.message || e)); } catch (e) { throw new Error("That does not look like a BIP39 seed phrase: " + (e?.message || e)); }
const prefix = network === "mainnet" ? "bitcoincash" : "bchtest"; // Per-chain: where to ask, how to turn a path into an address, and how to
const servers = network === "mainnet" // key a script for Electrum. Everything below is chain-agnostic.
? bchServerList(api) let servers, addressAt, scripthashOf;
: ["wss://chipnet.imaginary.cash:50004", "wss://chipnet.bch.ninja:50004"]; if (chain === "bch") {
const prefix = network === "mainnet" ? "bitcoincash" : "bchtest";
servers = network === "mainnet"
? bchServerList(api)
: ["wss://chipnet.imaginary.cash:50004", "wss://chipnet.bch.ninja:50004"];
addressAt = (pth) => deriveCashaddrFromSeed(seedHex, pth, prefix);
scripthashOf = (addr) => {
const d = ctx.d.cashaddr.decode(addr);
const h = Buffer.from(d.hash);
const spk = d.type === 1
? Buffer.concat([Buffer.from([0xa9, 0x14]), h, Buffer.from([0x87])])
: Buffer.concat([Buffer.from([0x76, 0xa9, 0x14]), h, Buffer.from([0x88, 0xac])]);
return Buffer.from(ctx.d.sha256(new Uint8Array(spk))).reverse().toString("hex");
};
} else if (chain === "dgb") {
if (!ctx.d.dgbCore) throw new Error("the DigiByte modules did not load, so paths cannot be scanned");
servers = ["wss://electrum1.cipig.net:20063", "wss://electrum2.cipig.net:20063"];
// hmacKey rides on the candidate, so both master-key variants are
// scanned in the same pass.
addressAt = (pth, hmacKey) => ctx.d.derive.dgb.fromSeed(seedHex, pth, hmacKey);
scripthashOf = (addr) => {
// bitcoinjs knows every DGB output type (D…, S…, dgb1q…, dgb1p…),
// so let it build the scriptPubKey rather than hand-rolling four.
const spk = ctx.d.bitcoinjs.address.toOutputScript(addr, ctx.d.dgbCore.digibyte);
return Buffer.from(ctx.d.sha256(new Uint8Array(spk))).reverse().toString("hex");
};
} else {
throw new Error(`Path scanning is not available for ${chain} yet.`);
}
const client = new ctx.d.electrum.Client(servers); const client = new ctx.d.electrum.Client(servers);
// Electrum keys a script by sha256(scriptPubKey), little-endian.
const scripthashOf = (addr) => {
const d = ctx.d.cashaddr.decode(addr);
const h = Buffer.from(d.hash);
const spk = d.type === 1
? Buffer.concat([Buffer.from([0xa9, 0x14]), h, Buffer.from([0x87])])
: Buffer.concat([Buffer.from([0x76, 0xa9, 0x14]), h, Buffer.from([0x88, 0xac])]);
return Buffer.from(ctx.d.sha256(new Uint8Array(spk))).reverse().toString("hex");
};
// Look a few addresses deep per candidate: a wallet whose first receive // Look a few addresses deep per candidate: a wallet whose first receive
// address is spent clean still has history, and funds often sit further // address is spent clean still has history, and funds often sit further
// along the branch. // along the branch.
@ -2188,7 +2236,7 @@ function registerPanelMessages(api) {
let balance = 0, txCount = 0, firstAddress = null, fundedAddress = null; let balance = 0, txCount = 0, firstAddress = null, fundedAddress = null;
for (const [idx, pth] of probes.entries()) { for (const [idx, pth] of probes.entries()) {
let addr; let addr;
try { addr = deriveCashaddrFromSeed(seedHex, pth, prefix); } try { addr = addressAt(pth, c.hmacKey); }
catch { continue; } catch { continue; }
if (idx === 0 || firstAddress === null) firstAddress = firstAddress || addr; if (idx === 0 || firstAddress === null) firstAddress = firstAddress || addr;
const sh = scripthashOf(addr); const sh = scripthashOf(addr);
@ -2202,7 +2250,7 @@ function registerPanelMessages(api) {
} catch (e) { api.log("scanSeedPaths:", pth, e?.message || e); } } catch (e) { api.log("scanSeedPaths:", pth, e?.message || e); }
} }
out.push({ out.push({
path: c.path, note: c.note, accountPath: acct, path: c.path, note: c.note, accountPath: acct, hmacKey: c.hmacKey || null,
firstAddress, fundedAddress, balance, txCount, scanned: probes.length, firstAddress, fundedAddress, balance, txCount, scanned: probes.length,
}); });
} }

View file

@ -1,13 +1,23 @@
import { bip32 } from '../deps.js'; import { bip32, hmac, sha512 } from '../deps.js';
import { digibyte, DGB_COIN_TYPE } from './network.js'; import { digibyte, DGB_COIN_TYPE } from './network.js';
// BIP32 derives the master key as HMAC-SHA512(key, seed) with the key
// "Bitcoin seed". DigiByte's 2018-19 official mobile wallets (BreadWallet
// forks) used "DigiByte seed" instead, so the SAME 12 words give a
// completely different key tree — every address differs. A recovery that
// only tries the standard key never finds those coins.
// Ported from Digibyte.X/dgb-wallet @ 989a2696.
export const HMAC_BITCOIN_SEED = 'Bitcoin seed';
export const HMAC_DIGIBYTE_SEED = 'DigiByte seed';
export const PURPOSE_LABEL = { export const PURPOSE_LABEL = {
44: 'BIP44 legacy P2PKH', 44: 'BIP44 legacy P2PKH',
49: 'BIP49 P2SH-wrapped SegWit', 49: 'BIP49 P2SH-wrapped SegWit',
84: 'BIP84 native SegWit v0', 84: 'BIP84 native SegWit v0',
86: 'BIP86 Taproot (SegWit v1)', 86: 'BIP86 Taproot (SegWit v1)',
}; };
export function rootFromSeed(seed, network = digibyte) { export function rootFromSeed(seed, network = digibyte, hmacKey = HMAC_BITCOIN_SEED) {
return bip32().fromSeed(seed, network); if (hmacKey === HMAC_BITCOIN_SEED) return bip32().fromSeed(seed, network);
const I = hmac()(sha512(), new TextEncoder().encode(hmacKey), seed);
return bip32().fromPrivateKey(Buffer.from(I.slice(0, 32)), Buffer.from(I.slice(32)), network);
} }
// Standard account-level derivation: m/purpose'/coin'/account'. // Standard account-level derivation: m/purpose'/coin'/account'.
// account defaults to 0 (the first account). // account defaults to 0 (the first account).

View file

@ -19,7 +19,7 @@
let deps = null; let deps = null;
export function setDgbDeps(d) { export function setDgbDeps(d) {
const need = ["bitcoinjs", "ecc", "bip32Factory", "ecpairFactory", "bip39"]; const need = ["bitcoinjs", "ecc", "bip32Factory", "ecpairFactory", "bip39", "hmac", "sha512"];
for (const k of need) { for (const k of need) {
if (!d || !d[k]) throw new Error(`setDgbDeps: missing ${k}`); if (!d || !d[k]) throw new Error(`setDgbDeps: missing ${k}`);
} }
@ -36,6 +36,8 @@ export const payments = () => need().bitcoinjs.payments;
export const Psbt = () => need().bitcoinjs.Psbt; export const Psbt = () => need().bitcoinjs.Psbt;
export const ecc = () => need().ecc; export const ecc = () => need().ecc;
export const bip39 = () => need().bip39; export const bip39 = () => need().bip39;
export const hmac = () => need().hmac;
export const sha512 = () => need().sha512;
// initEccLib must run before any Taproot derivation, and exactly once. // initEccLib must run before any Taproot derivation, and exactly once.
let eccInstalled = false; let eccInstalled = false;

View file

@ -7,7 +7,7 @@
// npm packages — same "hand it in" pattern the other adapters use. // npm packages — same "hand it in" pattern the other adapters use.
module.exports = function makeImportDerive({ module.exports = function makeImportDerive({
HDKey, secp256k1, ed25519, sha256, ripemd160, keccak_256, blake2b, HDKey, secp256k1, ed25519, sha256, sha512, hmac, ripemd160, keccak_256, blake2b,
cashaddr, base58check, bitcoinjs, bip32Factory, ecpairFactory, ecc, bip39, cashaddr, base58check, bitcoinjs, bip32Factory, ecpairFactory, ecc, bip39,
dgbCore, dgbCore,
}) { }) {
@ -15,6 +15,17 @@ module.exports = function makeImportDerive({
// imported SC wallets end up with byte-identical addresses to what // imported SC wallets end up with byte-identical addresses to what
// Sia Central Lite or walletd would show for the same seed. // Sia Central Lite or walletd would show for the same seed.
const sia = blake2b ? require("./sia/sia.js")({ ed25519, blake2b }) : null; const sia = blake2b ? require("./sia/sia.js")({ ed25519, blake2b }) : null;
// Taproot needs bitcoinjs-lib's schnorr backend installed via initEccLib,
// which is process-global and must happen once. This module used to rely on
// chain-btc.js (and, before it went lazy, lib/dgb/core/address.js) doing it
// at load time — a hidden dependency on an unrelated module's import order,
// which broke the moment either stopped being eagerly loaded. Own it here.
let eccInstalled = false;
function ensureEcc() {
if (eccInstalled) return;
try { if (bitcoinjs && bitcoinjs.initEccLib && ecc) bitcoinjs.initEccLib(ecc); } catch { /* p2tr will report it */ }
eccInstalled = true;
}
const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join(""); const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
const fromHex = (h) => { const fromHex = (h) => {
const s = String(h || "").replace(/^0x/i, ""); const s = String(h || "").replace(/^0x/i, "");
@ -49,6 +60,7 @@ module.exports = function makeImportDerive({
if (purpose === 86) { if (purpose === 86) {
// Taproot — bitcoinjs.p2tr wants the 32-byte x-only pubkey. // Taproot — bitcoinjs.p2tr wants the 32-byte x-only pubkey.
const xonly = pk.slice(1, 33); const xonly = pk.slice(1, 33);
ensureEcc();
return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address; return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address;
} }
if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address; if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address;
@ -69,6 +81,7 @@ module.exports = function makeImportDerive({
const net = BTC_NET[network]; const net = BTC_NET[network];
if (purpose === 86) { if (purpose === 86) {
const xonly = pk.slice(1, 33); const xonly = pk.slice(1, 33);
ensureEcc();
return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address; return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address;
} }
if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address; if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address;
@ -81,15 +94,29 @@ module.exports = function makeImportDerive({
if (!dgbCore) throw new Error("DGB adapter not available"); if (!dgbCore) throw new Error("DGB adapter not available");
return dgbCore.digibyte; return dgbCore.digibyte;
} }
function deriveDgbFromSeed(seedHex, path) { // DigiByte's 2018-19 official mobile wallets (BreadWallet forks) built the
// master node with HMAC-SHA512 key "DigiByte seed" rather than BIP32's
// "Bitcoin seed", so the same words yield a different key tree and a
// standard scan finds nothing. Pass hmacKey to recover those.
// Ported from Digibyte.X/dgb-wallet @ 989a2696.
const HMAC_BITCOIN_SEED = "Bitcoin seed";
const HMAC_DIGIBYTE_SEED = "DigiByte seed";
function dgbRootFromSeed(seedBytes, net, hmacKey) {
const bip32 = bip32Factory(ecc); const bip32 = bip32Factory(ecc);
if (!hmacKey || hmacKey === HMAC_BITCOIN_SEED) return bip32.fromSeed(Buffer.from(seedBytes), net);
if (!hmac || !sha512) throw new Error("this build cannot derive the legacy DigiByte master key");
const I = hmac(sha512, new TextEncoder().encode(hmacKey), Buffer.from(seedBytes));
return bip32.fromPrivateKey(Buffer.from(I.slice(0, 32)), Buffer.from(I.slice(32)), net);
}
function deriveDgbFromSeed(seedHex, path, hmacKey) {
const net = digibyteNetwork(); const net = digibyteNetwork();
const node = bip32.fromSeed(Buffer.from(fromHex(seedHex)), net).derivePath(path); const node = dgbRootFromSeed(fromHex(seedHex), net, hmacKey).derivePath(path);
const pk = Buffer.from(node.publicKey); const pk = Buffer.from(node.publicKey);
const m = /^m\/(\d+)'/.exec(String(path || "")); const m = /^m\/(\d+)'/.exec(String(path || ""));
const purpose = m ? Number(m[1]) : 84; const purpose = m ? Number(m[1]) : 84;
if (purpose === 86) { if (purpose === 86) {
const xonly = pk.slice(1, 33); const xonly = pk.slice(1, 33);
ensureEcc();
return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address; return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address;
} }
if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address; if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address;
@ -102,6 +129,7 @@ module.exports = function makeImportDerive({
const kp = ECPair.fromWIF(wif, net); const kp = ECPair.fromWIF(wif, net);
const purpose = hint || 84; const purpose = hint || 84;
const pk = kp.publicKey; const pk = kp.publicKey;
ensureEcc();
if (purpose === 86) return bitcoinjs.payments.p2tr({ internalPubkey: pk.slice(1, 33), network: net }).address; if (purpose === 86) return bitcoinjs.payments.p2tr({ internalPubkey: pk.slice(1, 33), network: net }).address;
if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address; if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address;
if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address; if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address;
@ -250,7 +278,10 @@ module.exports = function makeImportDerive({
return { return {
mnemonicToSeedHex, mnemonicToSeedHex,
btc: { fromSeed: deriveBtcFromSeed, fromWif: deriveBtcFromWif }, btc: { fromSeed: deriveBtcFromSeed, fromWif: deriveBtcFromWif },
dgb: { fromSeed: deriveDgbFromSeed, fromWif: deriveDgbFromWif }, dgb: {
fromSeed: deriveDgbFromSeed, fromWif: deriveDgbFromWif,
HMAC_BITCOIN_SEED, HMAC_DIGIBYTE_SEED,
},
eth: { fromSeed: deriveEthFromSeed, fromPrivHex: deriveEthFromPrivHex }, eth: { fromSeed: deriveEthFromSeed, fromPrivHex: deriveEthFromPrivHex },
trx: { fromSeed: deriveTrxFromSeed, fromPrivHex: deriveTrxFromPrivHex }, trx: { fromSeed: deriveTrxFromSeed, fromPrivHex: deriveTrxFromPrivHex },
sol: { fromSeed: deriveSolFromSeed, fromPrivHex: deriveSolFromPrivHex, fromBase58: deriveSolFromBase58 }, sol: { fromSeed: deriveSolFromSeed, fromPrivHex: deriveSolFromPrivHex, fromBase58: deriveSolFromBase58 },

View file

@ -1887,6 +1887,10 @@ function openImportModal(initialChain) {
// Derivation-path presets + the "which path has my funds" scan. Both are // Derivation-path presets + the "which path has my funds" scan. Both are
// mnemonic-only: a WIF carries its own single key and has no path. // mnemonic-only: a WIF carries its own single key and has no path.
const CUSTOM = "__custom__"; const CUSTOM = "__custom__";
// Which master-key variant the chosen path belongs to (DigiByte only;
// null means BIP32's standard "Bitcoin seed").
let importHmacKey = null;
let lastCandidates = null;
async function paintPathPresets() { async function paintPathPresets() {
const presetSel = overlay.querySelector("#imPathPreset"); const presetSel = overlay.querySelector("#imPathPreset");
const scanRow = overlay.querySelector("#imScanRow"); const scanRow = overlay.querySelector("#imScanRow");
@ -1900,22 +1904,33 @@ function openImportModal(initialChain) {
try { cands = (await S.invoke("seedPathCandidates", { chain: curChain, network: netId }))?.candidates || []; } try { cands = (await S.invoke("seedPathCandidates", { chain: curChain, network: netId }))?.candidates || []; }
catch { cands = []; } catch { cands = []; }
} }
lastCandidates = cands;
if (!cands.length) { presetSel.hidden = true; scanRow.hidden = true; return; } if (!cands.length) { presetSel.hidden = true; scanRow.hidden = true; return; }
presetSel.hidden = false; presetSel.hidden = false;
scanRow.hidden = false; scanRow.hidden = false;
const cur = overlay.querySelector("#imPath").value.trim(); const cur = overlay.querySelector("#imPath").value.trim();
presetSel.innerHTML = cands.map((c) => // The value is an index, not the path: DigiByte lists the same path
`<option value="${esc(c.path)}" ${c.path === cur ? "selected" : ""}>${esc(c.path)} — ${esc(c.note)}</option>` // twice under two different master keys, so the path alone is not a
// unique choice.
presetSel.innerHTML = cands.map((c, i) =>
`<option value="${i}" ${c.path === cur && !c.hmacKey ? "selected" : ""}>${esc(c.path)} — ${esc(c.note)}</option>`
).join("") + `<option value="${CUSTOM}" ${cands.some((c) => c.path === cur) ? "" : "selected"}>Custom…</option>`; ).join("") + `<option value="${CUSTOM}" ${cands.some((c) => c.path === cur) ? "" : "selected"}>Custom…</option>`;
presetSel.onchange = () => { presetSel.onchange = () => {
if (presetSel.value === CUSTOM) { overlay.querySelector("#imPath").focus(); return; } if (presetSel.value === CUSTOM) { overlay.querySelector("#imPath").focus(); return; }
overlay.querySelector("#imPath").value = presetSel.value; const c = cands[Number(presetSel.value)];
if (!c) return;
overlay.querySelector("#imPath").value = c.path;
importHmacKey = c.hmacKey || null;
}; };
// Typing by hand flips the select to Custom rather than leaving it // Typing by hand flips the select to Custom rather than leaving it
// pointing at a preset the field no longer matches. // pointing at a preset the field no longer matches.
overlay.querySelector("#imPath").oninput = () => { overlay.querySelector("#imPath").oninput = () => {
const v = overlay.querySelector("#imPath").value.trim(); const v = overlay.querySelector("#imPath").value.trim();
presetSel.value = cands.some((c) => c.path === v) ? v : CUSTOM; const i = cands.findIndex((c) => c.path === v && !c.hmacKey);
presetSel.value = i >= 0 ? String(i) : CUSTOM;
// Typing a path by hand means the standard master key unless a scan
// result sets it again.
if (i >= 0) importHmacKey = null;
}; };
} }
@ -1944,10 +1959,10 @@ function openImportModal(initialChain) {
: `<span class="hint">nothing</span>`; : `<span class="hint">nothing</span>`;
return `<div class="tx" style="grid-template-columns:1fr auto;cursor:default;align-items:center;margin-top:4px"> return `<div class="tx" style="grid-template-columns:1fr auto;cursor:default;align-items:center;margin-top:4px">
<div> <div>
<div class="mono" style="font-size:12px">${esc(x.path)}</div> <div class="mono" style="font-size:12px">${esc(x.path)}${x.hmacKey ? ` <span class="ttag">${esc(x.hmacKey)}</span>` : ""}</div>
<div class="hint">${esc(x.note)} · ${tag}</div> <div class="hint">${esc(x.note)} · ${tag}</div>
</div> </div>
<button class="btn sm ${funded ? "primary" : ""}" data-usepath="${esc(x.path)}">Use</button> <button class="btn sm ${funded ? "primary" : ""}" data-usepath="${esc(x.path)}" data-usehmac="${esc(x.hmacKey || "")}">Use</button>
</div>`; </div>`;
}).join(""); }).join("");
out.innerHTML = (any out.innerHTML = (any
@ -1956,8 +1971,12 @@ function openImportModal(initialChain) {
+ rows; + rows;
out.querySelectorAll("[data-usepath]").forEach((b) => b.addEventListener("click", () => { out.querySelectorAll("[data-usepath]").forEach((b) => b.addEventListener("click", () => {
overlay.querySelector("#imPath").value = b.dataset.usepath; overlay.querySelector("#imPath").value = b.dataset.usepath;
importHmacKey = b.dataset.usehmac || null;
const ps = overlay.querySelector("#imPathPreset"); const ps = overlay.querySelector("#imPathPreset");
if (ps) ps.value = [...ps.options].some((o) => o.value === b.dataset.usepath) ? b.dataset.usepath : CUSTOM; if (ps) {
const i = (lastCandidates || []).findIndex((c) => c.path === b.dataset.usepath && (c.hmacKey || "") === (b.dataset.usehmac || ""));
ps.value = i >= 0 ? String(i) : CUSTOM;
}
flash(b, "Set"); flash(b, "Set");
})); }));
} catch (e) { } catch (e) {
@ -1981,6 +2000,7 @@ function openImportModal(initialChain) {
if (kind === "mnemonic") { if (kind === "mnemonic") {
payload.mnemonic = overlay.querySelector("#imMnemonic").value.trim(); payload.mnemonic = overlay.querySelector("#imMnemonic").value.trim();
payload.path = overlay.querySelector("#imPath").value.trim(); payload.path = overlay.querySelector("#imPath").value.trim();
if (importHmacKey) payload.hmacKey = importHmacKey;
if (!payload.mnemonic) { msg.textContent = "Mnemonic required."; msg.hidden = false; return; } if (!payload.mnemonic) { msg.textContent = "Mnemonic required."; msg.hidden = false; return; }
// Sia has no BIP44 path — the "path" field carries a u64 address // Sia has no BIP44 path — the "path" field carries a u64 address
// index instead. Rename before sending so the addon reads it via // index instead. Rename before sending so the addon reads it via