From 469ff482eae2bce1711036ea2a249e03b2e0d7a5 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sat, 3 Oct 2026 14:13:25 +0200 Subject: [PATCH] feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- bundled-addons/aegis/index.js | 86 ++++++++++++++++++----- bundled-addons/aegis/lib/dgb/core/hd.js | 16 ++++- bundled-addons/aegis/lib/dgb/deps.js | 4 +- bundled-addons/aegis/lib/import-derive.js | 39 ++++++++-- bundled-addons/aegis/panel.js | 34 +++++++-- 5 files changed, 145 insertions(+), 34 deletions(-) diff --git a/bundled-addons/aegis/index.js b/bundled-addons/aegis/index.js index 25db68a9..abb5d5c0 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -35,8 +35,11 @@ let ctx = null; async function loadDeps(api) { const { secp256k1 } = await api.import("@noble/curves/secp256k1.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"); + // 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 { keccak_256 } = await api.import("@noble/hashes/sha3.js"); const { blake2b } = await api.import("@noble/hashes/blake2.js"); @@ -89,6 +92,7 @@ async function loadDeps(api) { bip32Factory: BIP32Factory, ecpairFactory: ECPairFactory, bip39: api.require("bip39"), + hmac, sha512, }); 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); @@ -125,7 +129,7 @@ async function loadDeps(api) { // chain-native private key). Used by the importWallet handler to compute // the address client-side before wallet-imports.enc stores the material. 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, ecpairFactory: ECPairFactory, ecc, bip39, dgbCore, }); @@ -1220,7 +1224,19 @@ function registerPanelMessages(api) { } else if (p && p.mnemonic) { spec.kind = "seed"; spec.seed = der.mnemonicToSeedHex(String(p.mnemonic).trim()); 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"); } // Theseus's api.vault.imports.add validates a `cashaddr` field (from // 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/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": [ { 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" }, @@ -2141,7 +2170,7 @@ function registerPanelMessages(api) { api.onMessage("seedPathCandidates", (p, m) => { fromPanel(m); 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) => { @@ -2158,20 +2187,39 @@ function registerPanelMessages(api) { try { seedHex = ctx.d.derive.mnemonicToSeedHex(mnemonic); } catch (e) { throw new Error("That does not look like a BIP39 seed phrase: " + (e?.message || e)); } - const prefix = network === "mainnet" ? "bitcoincash" : "bchtest"; - const servers = network === "mainnet" - ? bchServerList(api) - : ["wss://chipnet.imaginary.cash:50004", "wss://chipnet.bch.ninja:50004"]; + // Per-chain: where to ask, how to turn a path into an address, and how to + // key a script for Electrum. Everything below is chain-agnostic. + let servers, addressAt, scripthashOf; + 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); - // 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 // address is spent clean still has history, and funds often sit further // along the branch. @@ -2188,7 +2236,7 @@ function registerPanelMessages(api) { let balance = 0, txCount = 0, firstAddress = null, fundedAddress = null; for (const [idx, pth] of probes.entries()) { let addr; - try { addr = deriveCashaddrFromSeed(seedHex, pth, prefix); } + try { addr = addressAt(pth, c.hmacKey); } catch { continue; } if (idx === 0 || firstAddress === null) firstAddress = firstAddress || addr; const sh = scripthashOf(addr); @@ -2202,7 +2250,7 @@ function registerPanelMessages(api) { } catch (e) { api.log("scanSeedPaths:", pth, e?.message || e); } } 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, }); } diff --git a/bundled-addons/aegis/lib/dgb/core/hd.js b/bundled-addons/aegis/lib/dgb/core/hd.js index 12063427..c1c9f11c 100644 --- a/bundled-addons/aegis/lib/dgb/core/hd.js +++ b/bundled-addons/aegis/lib/dgb/core/hd.js @@ -1,13 +1,23 @@ -import { bip32 } from '../deps.js'; +import { bip32, hmac, sha512 } from '../deps.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 = { 44: 'BIP44 legacy P2PKH', 49: 'BIP49 P2SH-wrapped SegWit', 84: 'BIP84 native SegWit v0', 86: 'BIP86 Taproot (SegWit v1)', }; -export function rootFromSeed(seed, network = digibyte) { - return bip32().fromSeed(seed, network); +export function rootFromSeed(seed, network = digibyte, hmacKey = HMAC_BITCOIN_SEED) { + 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'. // account defaults to 0 (the first account). diff --git a/bundled-addons/aegis/lib/dgb/deps.js b/bundled-addons/aegis/lib/dgb/deps.js index dcc4eb71..07b6368c 100644 --- a/bundled-addons/aegis/lib/dgb/deps.js +++ b/bundled-addons/aegis/lib/dgb/deps.js @@ -19,7 +19,7 @@ let deps = null; 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) { 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 ecc = () => need().ecc; 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. let eccInstalled = false; diff --git a/bundled-addons/aegis/lib/import-derive.js b/bundled-addons/aegis/lib/import-derive.js index be171a78..f5bef35d 100644 --- a/bundled-addons/aegis/lib/import-derive.js +++ b/bundled-addons/aegis/lib/import-derive.js @@ -7,7 +7,7 @@ // npm packages — same "hand it in" pattern the other adapters use. 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, dgbCore, }) { @@ -15,6 +15,17 @@ module.exports = function makeImportDerive({ // imported SC wallets end up with byte-identical addresses to what // Sia Central Lite or walletd would show for the same seed. 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 fromHex = (h) => { const s = String(h || "").replace(/^0x/i, ""); @@ -49,6 +60,7 @@ module.exports = function makeImportDerive({ if (purpose === 86) { // Taproot — bitcoinjs.p2tr wants the 32-byte x-only pubkey. const xonly = pk.slice(1, 33); + ensureEcc(); return bitcoinjs.payments.p2tr({ internalPubkey: xonly, 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]; if (purpose === 86) { const xonly = pk.slice(1, 33); + ensureEcc(); return bitcoinjs.payments.p2tr({ internalPubkey: xonly, 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"); 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); + 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 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 m = /^m\/(\d+)'/.exec(String(path || "")); const purpose = m ? Number(m[1]) : 84; if (purpose === 86) { const xonly = pk.slice(1, 33); + ensureEcc(); return bitcoinjs.payments.p2tr({ internalPubkey: xonly, 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 purpose = hint || 84; const pk = kp.publicKey; + ensureEcc(); 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 === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address; @@ -250,7 +278,10 @@ module.exports = function makeImportDerive({ return { mnemonicToSeedHex, 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 }, trx: { fromSeed: deriveTrxFromSeed, fromPrivHex: deriveTrxFromPrivHex }, sol: { fromSeed: deriveSolFromSeed, fromPrivHex: deriveSolFromPrivHex, fromBase58: deriveSolFromBase58 }, diff --git a/bundled-addons/aegis/panel.js b/bundled-addons/aegis/panel.js index 819ec08b..d5817974 100644 --- a/bundled-addons/aegis/panel.js +++ b/bundled-addons/aegis/panel.js @@ -1887,6 +1887,10 @@ function openImportModal(initialChain) { // 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. 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() { const presetSel = overlay.querySelector("#imPathPreset"); const scanRow = overlay.querySelector("#imScanRow"); @@ -1900,22 +1904,33 @@ function openImportModal(initialChain) { try { cands = (await S.invoke("seedPathCandidates", { chain: curChain, network: netId }))?.candidates || []; } catch { cands = []; } } + lastCandidates = cands; if (!cands.length) { presetSel.hidden = true; scanRow.hidden = true; return; } presetSel.hidden = false; scanRow.hidden = false; 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 + // twice under two different master keys, so the path alone is not a + // unique choice. + presetSel.innerHTML = cands.map((c, i) => + `` ).join("") + ``; presetSel.onchange = () => { 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 // pointing at a preset the field no longer matches. overlay.querySelector("#imPath").oninput = () => { 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) { : `nothing`; return `
-
${esc(x.path)}
+
${esc(x.path)}${x.hmacKey ? ` ${esc(x.hmacKey)}` : ""}
${esc(x.note)} · ${tag}
- +
`; }).join(""); out.innerHTML = (any @@ -1956,8 +1971,12 @@ function openImportModal(initialChain) { + rows; out.querySelectorAll("[data-usepath]").forEach((b) => b.addEventListener("click", () => { overlay.querySelector("#imPath").value = b.dataset.usepath; + importHmacKey = b.dataset.usehmac || null; 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"); })); } catch (e) { @@ -1981,6 +2000,7 @@ function openImportModal(initialChain) { if (kind === "mnemonic") { payload.mnemonic = overlay.querySelector("#imMnemonic").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; } // Sia has no BIP44 path — the "path" field carries a u64 address // index instead. Rename before sending so the addon reads it via