feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot)
BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…),
BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on
mainnet or testnet3 (paths shift coin type 0 → 1 automatically).
- lib/chain-btc.js: paymentFor(purpose, node, network) returns the
right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr)
keyed off the derivation path's purpose. WalletKeys.entry captures
the family, redeem script (BIP49) and internal x-only pubkey
(BIP86) alongside the standard script/address fields.
bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves.
- Registry: BTC + DGB address families are purpose-only now; a
helper (addressFamiliesFor / defaultAccountPathFor) computes the
concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type
{mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta
expands the list so the panel doesn't need per-chain knowledge.
- Panel: #btcSettings block mirrors #dgbSettings (family select →
path input auto-fill → Apply). The family-select listener + the
fillFamilyPicker() helper are shared between DGB and BTC — the
DOM prefix is the only per-chain input.
- Send is wired for BIP84 (default) and BIP49 (adds redeemScript to
the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and
BIP86 (needs tap-tweaked signer) throw a clear "not yet in this
rev — sweep to BIP84" error so users hit it at plan time, not at
broadcast time. Receive works on all four families today.
- Verified all four families derive the canonical BIP44/49/84/86
spec test vectors for the standard abandon×11 mnemonic — see
scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
This commit is contained in:
parent
a0a22bc69a
commit
af8e167120
4 changed files with 203 additions and 49 deletions
|
|
@ -173,14 +173,16 @@ const COINS = {
|
|||
supportsPageInject: false,
|
||||
// BIP44/49/84/86 address families — the picker lives in the DGB
|
||||
// settings block. Default is BIP84 (dgb1q…), which matches modern
|
||||
// DGB Core, DigiByte-Go, and the SilentCode web-wallet.
|
||||
// DGB Core, DigiByte-Go, and the SilentCode web-wallet. `coinType`
|
||||
// per chain feeds the path builder below.
|
||||
addressFamilies: [
|
||||
{ id: "bip84", purpose: 84, label: "Native SegWit (dgb1q…)", defaultAccountPath: "m/84'/20'/0'" },
|
||||
{ id: "bip86", purpose: 86, label: "Taproot (dgb1p…)", defaultAccountPath: "m/86'/20'/0'" },
|
||||
{ id: "bip49", purpose: 49, label: "Wrapped SegWit (S…)", defaultAccountPath: "m/49'/20'/0'" },
|
||||
{ id: "bip44", purpose: 44, label: "Legacy P2PKH (D…)", defaultAccountPath: "m/44'/20'/0'" },
|
||||
{ id: "bip84", purpose: 84, label: "Native SegWit (dgb1q…)" },
|
||||
{ id: "bip86", purpose: 86, label: "Taproot (dgb1p…)" },
|
||||
{ id: "bip49", purpose: 49, label: "Wrapped SegWit (S…)" },
|
||||
{ id: "bip44", purpose: 44, label: "Legacy P2PKH (D…)" },
|
||||
],
|
||||
defaultAccountPath: "m/84'/20'/0'",
|
||||
coinType: 20,
|
||||
defaultPurpose: 84,
|
||||
networks: {
|
||||
mainnet: {
|
||||
id: "mainnet", label: "Mainnet", testnet: false,
|
||||
|
|
@ -240,10 +242,17 @@ const COINS = {
|
|||
logo: "btc",
|
||||
supportsMessageSign: true,
|
||||
supportsPageInject: false,
|
||||
// BIP84 by default. Family selector can be exposed later, same shape
|
||||
// as DGB's — the adapter already reads accountPath, so a fresh entry
|
||||
// with a different purpose (m/44'/0'/0' etc.) just works.
|
||||
defaultAccountPath: "m/84'/0'/0'",
|
||||
// BIP44/49/84/86 across bc1q… / bc1p… / 3… / 1… on mainnet and
|
||||
// tb1q… / tb1p… / 2… / m/n… on testnet3. Coin type shifts per
|
||||
// network (0 for mainnet, 1 for testnet — the BIP44 convention).
|
||||
addressFamilies: [
|
||||
{ id: "bip84", purpose: 84, label: "Native SegWit (bc1q… / tb1q…)" },
|
||||
{ id: "bip86", purpose: 86, label: "Taproot (bc1p… / tb1p…)" },
|
||||
{ id: "bip49", purpose: 49, label: "Wrapped SegWit (3… / 2…)" },
|
||||
{ id: "bip44", purpose: 44, label: "Legacy P2PKH (1… / m…, n…)" },
|
||||
],
|
||||
coinType: { mainnet: 0, testnet: 1 },
|
||||
defaultPurpose: 84,
|
||||
networks: {
|
||||
mainnet: {
|
||||
id: "mainnet", label: "Mainnet", testnet: false,
|
||||
|
|
@ -257,6 +266,30 @@ const COINS = {
|
|||
},
|
||||
};
|
||||
function chainKey(chain, network) { return `${chain}:${network}`; }
|
||||
// Coin type per (chain, network). A number literal on the COINS entry
|
||||
// (DGB uses a single 20) or a per-network object ({mainnet: 0, testnet: 1}
|
||||
// for BTC). Returns null when the chain doesn't declare a family picker.
|
||||
function coinTypeFor(c, network) {
|
||||
if (!c || c.coinType == null) return null;
|
||||
return typeof c.coinType === "number" ? c.coinType : (c.coinType[network] ?? null);
|
||||
}
|
||||
// Full derivation paths per family for a given (chain, network) — expands
|
||||
// the family list on the fly so each picker knows exactly which path a
|
||||
// pick would produce.
|
||||
function addressFamiliesFor(c, network) {
|
||||
if (!c || !c.addressFamilies) return null;
|
||||
const ct = coinTypeFor(c, network);
|
||||
if (ct == null) return c.addressFamilies;
|
||||
return c.addressFamilies.map((f) => ({
|
||||
...f,
|
||||
defaultAccountPath: `m/${f.purpose}'/${ct}'/0'`,
|
||||
}));
|
||||
}
|
||||
function defaultAccountPathFor(c, network) {
|
||||
const ct = coinTypeFor(c, network);
|
||||
if (ct == null || c.defaultPurpose == null) return null;
|
||||
return `m/${c.defaultPurpose}'/${ct}'/0'`;
|
||||
}
|
||||
function chainMeta(chain, network) {
|
||||
const c = COINS[chain]; const n = c && c.networks[network];
|
||||
if (!c || !n) return null;
|
||||
|
|
@ -266,8 +299,8 @@ function chainMeta(chain, network) {
|
|||
color: c.color, logo: c.logo, coinLabel: c.label, networkLabel: n.label, testnet: !!n.testnet,
|
||||
purposePrefix: n.purposePrefix, startIndex: n.startIndex,
|
||||
supportsMessageSign: !!c.supportsMessageSign, supportsPageInject: !!c.supportsPageInject,
|
||||
addressFamilies: c.addressFamilies || null,
|
||||
defaultAccountPath: c.defaultAccountPath || null,
|
||||
addressFamilies: addressFamiliesFor(c, network),
|
||||
defaultAccountPath: defaultAccountPathFor(c, network),
|
||||
};
|
||||
}
|
||||
function coinsForPanel() {
|
||||
|
|
@ -684,7 +717,11 @@ function registerPanelMessages(api) {
|
|||
if (v && !/^m(\/\d+'?)+$/.test(v)) throw new Error("derivation path must look like m/84'/0'/0'");
|
||||
const list = walletEntries();
|
||||
const idx = list.findIndex((w) => w.id === id);
|
||||
const dflt = entry.chain === "bch" ? "m/44'/145'/0'" : (entry.chain === "btc" ? "m/84'/0'/0'" : "m/84'/20'/0'");
|
||||
// Per-network default: BTC/DGB come from the registry helper, BCH keeps
|
||||
// its historical m/44'/145'/0'.
|
||||
const dflt = entry.chain === "bch"
|
||||
? "m/44'/145'/0'"
|
||||
: (defaultAccountPathFor(COINS[entry.chain], entry.network) || "m/84'/0'/0'");
|
||||
list[idx] = { ...list[idx], accountPath: v || dflt };
|
||||
writeWallets(api, list);
|
||||
const rt = ctx.runtimes.get(id);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ module.exports = function makeBtcAdapter({
|
|||
const { payments, Psbt, networks: bjsNetworks } = bitcoinjs;
|
||||
const bip32 = bip32Factory(ecc);
|
||||
const ECPair = ecpairFactory(ecc);
|
||||
// Taproot (p2tr) address derivation needs bitcoinjs-lib's schnorr backend
|
||||
// wired to a curve implementation — @bitcoinerlab/secp256k1 provides both
|
||||
// ECDSA and schnorr, so initEccLib once at load makes p2tr resolve.
|
||||
try { bitcoinjs.initEccLib && bitcoinjs.initEccLib(ecc); } catch {}
|
||||
|
||||
// Map our network id → bitcoinjs-lib Network object.
|
||||
function bjsNetworkFor(id) {
|
||||
|
|
@ -66,9 +70,45 @@ module.exports = function makeBtcAdapter({
|
|||
};
|
||||
}
|
||||
|
||||
// Address-family shape from the derivation-path purpose. Every field the
|
||||
// PSBT layer might need for signing an input funded by this family is
|
||||
// captured here so signAndBroadcast has one code path per family.
|
||||
function paymentFor(purpose, node, network) {
|
||||
const pubkey = Buffer.from(node.publicKey);
|
||||
if (purpose === 44) {
|
||||
// Legacy P2PKH. Signing needs the full previous transaction
|
||||
// (nonWitnessUtxo) — one extra electrum call per input at send time.
|
||||
const p = payments.p2pkh({ pubkey, network });
|
||||
return { family: "bip44", address: p.address, output: Buffer.from(p.output), send: "p2pkh" };
|
||||
}
|
||||
if (purpose === 49) {
|
||||
// P2SH-wrapped SegWit. PSBT needs the redeem script (the inner p2wpkh
|
||||
// output) alongside the witnessUtxo.
|
||||
const redeem = payments.p2wpkh({ pubkey, network });
|
||||
const p = payments.p2sh({ redeem, network });
|
||||
return { family: "bip49", address: p.address, output: Buffer.from(p.output), redeem: Buffer.from(redeem.output), send: "p2sh-p2wpkh" };
|
||||
}
|
||||
if (purpose === 86) {
|
||||
// BIP86 Taproot key-path. Signing needs the tap-tweaked signer;
|
||||
// ECPair from the current bitcoinjs stack doesn't tweak natively, so
|
||||
// send is deferred — receive still works.
|
||||
const internalPubkey = Buffer.from(pubkey.subarray(1, 33)); // x-only
|
||||
const p = payments.p2tr({ internalPubkey, network });
|
||||
return { family: "bip86", address: p.address, output: Buffer.from(p.output), internalPubkey, send: null };
|
||||
}
|
||||
// Default: BIP84 native SegWit.
|
||||
const p = payments.p2wpkh({ pubkey, network });
|
||||
return { family: "bip84", address: p.address, output: Buffer.from(p.output), send: "p2wpkh" };
|
||||
}
|
||||
function purposeOfPath(accountPath) {
|
||||
const m = /^m\/(\d+)'\//.exec(String(accountPath || ""));
|
||||
return m ? Number(m[1]) : 84;
|
||||
}
|
||||
|
||||
class WalletKeys {
|
||||
constructor(root32, accountPath, bjsNetwork) {
|
||||
this._accountPath = /^m(\/\d+'?)+$/.test(accountPath) ? accountPath : "m/84'/0'/0'";
|
||||
this._purpose = purposeOfPath(this._accountPath);
|
||||
this._network = bjsNetwork;
|
||||
this._root = bip32.fromSeed(Buffer.from(root32), bjsNetwork);
|
||||
this._account = this._root.derivePath(this._accountPath);
|
||||
|
|
@ -78,19 +118,22 @@ module.exports = function makeBtcAdapter({
|
|||
get xpub() { return this._account.neutered().toBase58(); }
|
||||
get xprv() { return this._account.toBase58(); }
|
||||
get accountPath() { return this._accountPath; }
|
||||
get purpose() { return this._purpose; }
|
||||
entry(branch, index) {
|
||||
const k = branch + "/" + index;
|
||||
let e = this._cache.get(k);
|
||||
if (!e) {
|
||||
const node = this._branch[branch].derive(index);
|
||||
const pubkey = Buffer.from(node.publicKey);
|
||||
const p2wpkh = payments.p2wpkh({ pubkey, network: this._network });
|
||||
const script = Buffer.from(p2wpkh.output);
|
||||
const pay = paymentFor(this._purpose, node, this._network);
|
||||
e = {
|
||||
branch, index, path: this._accountPath + "/" + branch + "/" + index,
|
||||
publicKey: pubkey, script, scriptHex: script.toString("hex"),
|
||||
scripthash: scripthashOf(script),
|
||||
address: p2wpkh.address,
|
||||
publicKey: Buffer.from(node.publicKey),
|
||||
family: pay.family, sendKind: pay.send,
|
||||
script: pay.output, scriptHex: pay.output.toString("hex"),
|
||||
scripthash: scripthashOf(pay.output),
|
||||
address: pay.address,
|
||||
redeemScript: pay.redeem || null,
|
||||
tapInternalKey: pay.internalPubkey || null,
|
||||
_node: node,
|
||||
};
|
||||
this._cache.set(k, e);
|
||||
|
|
@ -109,11 +152,18 @@ module.exports = function makeBtcAdapter({
|
|||
}
|
||||
}
|
||||
|
||||
// Fee vsize model — same P2WPKH numbers as DGB (identical script shapes).
|
||||
// Fee vsize model per family. Values are rounded vsize contributions from
|
||||
// standard tx-size tables; the estimator is pessimistic enough to cover a
|
||||
// real broadcast without underpaying.
|
||||
const OVERHEAD_VB = 10.5;
|
||||
const P2WPKH_INPUT_VB = 68;
|
||||
const P2WPKH_OUTPUT_VB = 31;
|
||||
const feeVb = (nIn, nOut, feePerVb) => Math.ceil((OVERHEAD_VB + nIn * P2WPKH_INPUT_VB + nOut * P2WPKH_OUTPUT_VB) * feePerVb);
|
||||
const OUTPUT_VB = 31; // P2WPKH / P2SH / P2PKH outputs are all ~31 vB give or take
|
||||
const INPUT_VB = {
|
||||
p2pkh: 148, // (32+4)+1+107+4 legacy input
|
||||
"p2sh-p2wpkh": 91, // 40 base + ~205/4 witness
|
||||
p2wpkh: 68, // 41 base + 108/4 witness
|
||||
p2tr: 58, // 41 base + 66/4 witness (key-path)
|
||||
};
|
||||
const feeVb = (kind, nIn, nOut, feePerVb) => Math.ceil((OVERHEAD_VB + nIn * (INPUT_VB[kind] || 68) + nOut * OUTPUT_VB) * feePerVb);
|
||||
|
||||
class BtcWallet {
|
||||
constructor(root32, networkId, {
|
||||
|
|
@ -312,15 +362,24 @@ module.exports = function makeBtcAdapter({
|
|||
const dest = String(to || "");
|
||||
try { bitcoinjs.address.toOutputScript(dest, this._bjsNet); }
|
||||
catch (e) { throw new Error(`bad Bitcoin address: ${e?.message || dest}`); }
|
||||
// Family-aware refusal: this rev signs BIP84 + BIP49 natively. BIP44
|
||||
// needs previous-tx fetches (nonWitnessUtxo) and BIP86 needs Taproot
|
||||
// key-path tweaking — both are on the roadmap; error early so the
|
||||
// user isn't surprised at broadcast time.
|
||||
const cur = this.current();
|
||||
if (!cur.sendKind) {
|
||||
throw new Error(`Sending from ${cur.family.toUpperCase()} addresses isn't wired up yet in this Aegis rev. Receiving works; to spend, switch this wallet's address family to BIP84 (Native SegWit) in Settings and sweep funds there.`);
|
||||
}
|
||||
const spendable = this._state.utxos.slice().sort((a, b) => (b.height > 0) - (a.height > 0));
|
||||
const change = this._changeEntry();
|
||||
const kind = cur.sendKind;
|
||||
if (sendMax) {
|
||||
const chosen = spendable;
|
||||
const sum = chosen.reduce((a, u) => a + u.value, 0);
|
||||
const fee = feeVb(chosen.length, 1, rate);
|
||||
const fee = feeVb(kind, chosen.length, 1, rate);
|
||||
if (sum <= fee) throw new Error("balance does not cover the fee");
|
||||
return {
|
||||
_chosen: chosen, _rate: rate, _to: dest, _sendMax: true, _change: change,
|
||||
_chosen: chosen, _rate: rate, _to: dest, _sendMax: true, _change: change, _kind: kind,
|
||||
recipients: [{ to: dest, value: sum - fee }],
|
||||
fee, feeRate: rate, change: 0,
|
||||
total: sum,
|
||||
|
|
@ -331,13 +390,13 @@ module.exports = function makeBtcAdapter({
|
|||
let sum = 0; const chosen = [];
|
||||
for (const u of spendable) {
|
||||
chosen.push(u); sum += u.value;
|
||||
const withChange = feeVb(chosen.length, 2, rate);
|
||||
const withChange = feeVb(kind, chosen.length, 2, rate);
|
||||
if (sum >= value + withChange) {
|
||||
const changeVal = sum - value - withChange;
|
||||
const fee = changeVal > 546 ? withChange : sum - value;
|
||||
return {
|
||||
_chosen: chosen, _rate: rate, _to: dest, _value: value,
|
||||
_change: change, _changeVal: changeVal > 546 ? changeVal : 0,
|
||||
_change: change, _changeVal: changeVal > 546 ? changeVal : 0, _kind: kind,
|
||||
recipients: [{ to: dest, value }],
|
||||
fee, feeRate: rate,
|
||||
change: changeVal > 546 ? changeVal : 0,
|
||||
|
|
@ -351,10 +410,14 @@ module.exports = function makeBtcAdapter({
|
|||
async signAndBroadcast(plan) {
|
||||
const psbt = new Psbt({ network: this._bjsNet });
|
||||
for (const u of plan._chosen) {
|
||||
psbt.addInput({
|
||||
const inp = {
|
||||
hash: u.txid, index: u.vout,
|
||||
witnessUtxo: { script: u.entry.script, value: u.value },
|
||||
});
|
||||
};
|
||||
// BIP49: the redeem script (inner P2WPKH output) is required so
|
||||
// bitcoinjs-lib can finalize the P2SH-wrapped SegWit witness.
|
||||
if (u.entry.family === "bip49" && u.entry.redeemScript) inp.redeemScript = u.entry.redeemScript;
|
||||
psbt.addInput(inp);
|
||||
}
|
||||
const outputs = [{ address: plan._to, value: plan._sendMax ? plan.recipients[0].value : plan._value }];
|
||||
if (!plan._sendMax && plan._changeVal > 0) {
|
||||
|
|
|
|||
|
|
@ -274,6 +274,31 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="btcSettings" hidden>
|
||||
<div class="field">
|
||||
<div class="lbl">Address family</div>
|
||||
<select id="setBtcFamily"></select>
|
||||
<div class="hint">Picks the BIP purpose that shapes your Bitcoin addresses. Switching rebuilds the wallet against a different set of addresses under the same seed — old funds don't move; they still live under the family they were received on.</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="lbl">Derivation path (account)</div>
|
||||
<input type="text" id="setBtcPath" spellcheck="false" placeholder="m/84'/0'/0'">
|
||||
<div class="hint">Auto-filled from the family above. Edit only if you need a non-default account.</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn primary" id="applyBtcPath">Apply</button>
|
||||
</div>
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="lbl">Recovery info</div>
|
||||
<div class="hint">Keys come from your Theseus password vault under <span class="mono" id="btcPurpose"></span>. Any BIP39 tool at coin type 0 (mainnet) / 1 (testnet) and the same purpose can reproduce this wallet.</div>
|
||||
<div class="kv" id="btcRecovery"></div>
|
||||
<div class="actions">
|
||||
<button class="btn" id="showBtcXpub">Show account xpub</button>
|
||||
<button class="btn danger" id="showBtcXprv">Show account private key</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ethSettings" hidden>
|
||||
<div class="field">
|
||||
<div class="lbl">RPC URL</div>
|
||||
|
|
|
|||
|
|
@ -477,8 +477,9 @@ function fillSettings() {
|
|||
const s = sel(); if (!s) return;
|
||||
$("bchSettings").hidden = chain() !== "bch";
|
||||
$("trxSettings").hidden = chain() !== "trx";
|
||||
$("scSettings").hidden = chain() !== "sc";
|
||||
$("scSettings").hidden = chain() !== "sc";
|
||||
$("dgbSettings").hidden = chain() !== "dgb";
|
||||
$("btcSettings").hidden = chain() !== "btc";
|
||||
$("ethSettings").hidden = chain() !== "eth";
|
||||
$("solSettings").hidden = chain() !== "sol";
|
||||
$("removeBtn").disabled = !!s.isLegacy && s.chain === "bch";
|
||||
|
|
@ -507,24 +508,19 @@ function fillSettings() {
|
|||
$("scPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
|
||||
$("scRecovery").innerHTML = "";
|
||||
} else if (chain() === "dgb") {
|
||||
const families = s.meta?.addressFamilies || [];
|
||||
if (!settingsFilled) {
|
||||
$("setDgbPath").value = s.accountPath || "";
|
||||
// Match the current path to a family, falling back to the default.
|
||||
const current = String(s.accountPath || "");
|
||||
let currentFamilyId = families.find((f) => f.defaultAccountPath === current)?.id;
|
||||
if (!currentFamilyId) {
|
||||
const m = /^m\/(\d+)'/.exec(current);
|
||||
const purpose = m ? Number(m[1]) : null;
|
||||
currentFamilyId = families.find((f) => f.purpose === purpose)?.id || families[0]?.id;
|
||||
}
|
||||
$("setDgbFamily").innerHTML = families.map((f) =>
|
||||
`<option value="${esc(f.id)}" data-path="${esc(f.defaultAccountPath)}" ${f.id === currentFamilyId ? "selected" : ""}>${esc(f.label)}</option>`
|
||||
).join("");
|
||||
fillFamilyPicker("Dgb", s);
|
||||
settingsFilled = true;
|
||||
}
|
||||
$("dgbPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
|
||||
$("dgbRecovery").innerHTML = "";
|
||||
} else if (chain() === "btc") {
|
||||
if (!settingsFilled) {
|
||||
fillFamilyPicker("Btc", s);
|
||||
settingsFilled = true;
|
||||
}
|
||||
$("btcPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
|
||||
$("btcRecovery").innerHTML = "";
|
||||
} else if (chain() === "eth") {
|
||||
if (!settingsFilled) {
|
||||
$("setEthRpcUrl").value = s.rpcUrl || "";
|
||||
|
|
@ -615,6 +611,7 @@ document.querySelectorAll("nav button").forEach((b) => b.addEventListener("click
|
|||
$("recovery").innerHTML = "";
|
||||
$("scRecovery").innerHTML = "";
|
||||
$("dgbRecovery").innerHTML = "";
|
||||
$("btcRecovery").innerHTML = "";
|
||||
$("ethRecovery").innerHTML = "";
|
||||
$("solRecovery").innerHTML = "";
|
||||
}
|
||||
|
|
@ -637,13 +634,30 @@ $("showScSeed").addEventListener("click", async () => {
|
|||
} catch (e) { $("scRecovery").textContent = cleanErr(e); }
|
||||
});
|
||||
|
||||
// DGB-specific settings.
|
||||
// Family select → auto-fill the derivation-path input with that family's default.
|
||||
document.addEventListener("change", (e) => {
|
||||
if (e.target && e.target.id === "setDgbFamily") {
|
||||
const opt = e.target.options[e.target.selectedIndex];
|
||||
if (opt && opt.dataset.path) $("setDgbPath").value = opt.dataset.path;
|
||||
// Family-picker helper used by both DGB and BTC. Prefix is "Dgb" or "Btc":
|
||||
// the DOM IDs are #set<Prefix>Family + #set<Prefix>Path.
|
||||
function fillFamilyPicker(prefix, s) {
|
||||
const families = s.meta?.addressFamilies || [];
|
||||
const current = String(s.accountPath || "");
|
||||
let currentId = families.find((f) => f.defaultAccountPath === current)?.id;
|
||||
if (!currentId) {
|
||||
const m = /^m\/(\d+)'/.exec(current);
|
||||
const purpose = m ? Number(m[1]) : null;
|
||||
currentId = families.find((f) => f.purpose === purpose)?.id || families[0]?.id;
|
||||
}
|
||||
$(`set${prefix}Path`).value = current || families[0]?.defaultAccountPath || "";
|
||||
$(`set${prefix}Family`).innerHTML = families.map((f) =>
|
||||
`<option value="${esc(f.id)}" data-path="${esc(f.defaultAccountPath)}" ${f.id === currentId ? "selected" : ""}>${esc(f.label)}</option>`
|
||||
).join("");
|
||||
}
|
||||
// Any family select → auto-fill the sibling path input.
|
||||
document.addEventListener("change", (e) => {
|
||||
const t = e.target;
|
||||
if (!t) return;
|
||||
const m = /^set(Dgb|Btc)Family$/.exec(t.id || "");
|
||||
if (!m) return;
|
||||
const opt = t.options[t.selectedIndex];
|
||||
if (opt && opt.dataset.path) $(`set${m[1]}Path`).value = opt.dataset.path;
|
||||
});
|
||||
$("applyDgbPath").addEventListener("click", async () => {
|
||||
const msg = $("settingsMsg"); msg.hidden = true;
|
||||
|
|
@ -652,6 +666,21 @@ $("applyDgbPath").addEventListener("click", async () => {
|
|||
settingsFilled = false; fillSettings(); render(); flash($("applyDgbPath"), "Applied");
|
||||
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
|
||||
});
|
||||
$("applyBtcPath").addEventListener("click", async () => {
|
||||
const msg = $("settingsMsg"); msg.hidden = true;
|
||||
try {
|
||||
state = await S.invoke("setAccountPath", { id: state.selectedWalletId, accountPath: $("setBtcPath").value.trim() });
|
||||
settingsFilled = false; fillSettings(); render(); flash($("applyBtcPath"), "Applied");
|
||||
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
|
||||
});
|
||||
$("showBtcXpub").addEventListener("click", async () => {
|
||||
try { const r = await S.invoke("recovery", { id: state.selectedWalletId }); $("btcRecovery").innerHTML = recoveryHtml(r); }
|
||||
catch (e) { $("btcRecovery").textContent = cleanErr(e); }
|
||||
});
|
||||
$("showBtcXprv").addEventListener("click", async () => {
|
||||
try { const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true }); $("btcRecovery").innerHTML = recoveryHtml(r); }
|
||||
catch (e) { $("btcRecovery").textContent = cleanErr(e); }
|
||||
});
|
||||
|
||||
// ETH / SOL: RPC URL.
|
||||
$("applyEthRpc").addEventListener("click", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue