From 821cc8e808177131f119f6db6fbc105a8380a210 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 6 Sep 2026 02:49:09 +0200 Subject: [PATCH] =?UTF-8?q?feat(theseus/bchwallet):=20send=20=E2=80=94=20p?= =?UTF-8?q?lan,=20approval=20overlay,=20sign,=20broadcast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Send tab: recipient (cashaddr or legacy, testnet rejected), amount with BCH/sat toggle and Max, 1-5 sat/B fee slider, live fee/total preview via planSend. Sending goes plan -> approval-modal (To/Amount/Fee/Total) -> ECDSA DER + SIGHASH_ALL|FORKID -> blockchain.transaction.broadcast, then shows the txid with an explorer link. Confirmed coins are spent before unconfirmed; dust change folds into the fee. Verified end to end against a fake Fulcrum: broadcast tx re-parsed, sighash recomputed, signature checks. --- bundled-addons/bchwallet/index.js | 36 +++++++++++++++ bundled-addons/bchwallet/panel.html | 27 ++++++++++- bundled-addons/bchwallet/panel.js | 71 +++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/bundled-addons/bchwallet/index.js b/bundled-addons/bchwallet/index.js index d0970fd..cfb221b 100644 --- a/bundled-addons/bchwallet/index.js +++ b/bundled-addons/bchwallet/index.js @@ -93,6 +93,20 @@ async function deriveAndStart() { catch (e) { setPhase("error", e?.message || String(e)); c.api.log("wallet build failed:", e?.message); } } +const fmtBch = (sats) => (Number(sats) / 1e8).toFixed(8).replace(/(\.\d*?[1-9])0+$|\.0+$/, "$1"); +function planFrom(p) { + const c = requireReady(); + const spec = p || {}; + const targets = Array.isArray(spec.outputs) && spec.outputs.length + ? spec.outputs.map((o) => ({ to: o.to, value: o.amount ?? o.value })) + : [{ to: spec.to, value: spec.amount ?? spec.value }]; + return c.wallet.plan({ targets, feeRate: spec.feeRate, sendMax: !!spec.sendMax }); +} +function describePlan(plan) { + const sent = plan.recipients.reduce((a, r) => a + r.value, 0); + return { recipients: plan.recipients, fee: plan.fee, feeRate: plan.feeRate, inputs: plan.inputs.length, change: plan.change, total: sent + plan.fee }; +} + function requireReady() { if (!ctx || ctx.phase !== "ready" || !ctx.wallet) throw new Error("wallet is not ready (vault locked?)"); return ctx; @@ -120,6 +134,28 @@ function registerPanelMessages(api) { if (ctx && ctx.root) buildWallet(); return snapshot(); }); + // Send preview — no side effects, used for the live fee/total summary. + api.onMessage("planSend", (p, m) => { fromPanel(m); return describePlan(planFrom(p)); }); + // Send for real: plan -> approval overlay -> sign -> broadcast. + api.onMessage("send", async (p, m) => { + fromPanel(m); + const c = requireReady(); + const plan = planFrom(p); + const d = describePlan(plan); + const pick = await api.approvalModal({ + title: "Send Bitcoin Cash?", + origin: "Theseus wallet panel", + rows: [ + { label: "To", value: d.recipients[0].to, mono: true }, + { label: "Amount", value: fmtBch(d.recipients[0].value) + " BCH", strong: true }, + { label: "Fee", value: `${d.fee} sat (${d.feeRate} sat/B)` }, + { label: "Total", value: fmtBch(d.total) + " BCH" }, + ], + actions: [{ id: "send", label: "Send", primary: true }], + }); + if (pick !== "send") throw new Error("cancelled"); + return c.wallet.signAndBroadcast(plan); + }); // Recovery info: xpub always; the account xprv only after an explicit // confirmation in the approval overlay. Import the xprv into any BIP32 // wallet (branch 0 receive / 1 change) to move funds without Theseus. diff --git a/bundled-addons/bchwallet/panel.html b/bundled-addons/bchwallet/panel.html index 7a6044c..39028b7 100644 --- a/bundled-addons/bchwallet/panel.html +++ b/bundled-addons/bchwallet/panel.html @@ -120,7 +120,32 @@