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 @@ - Sending arrives in the next step. + + Recipient + + + + + Amount + + + BCHsat + Max + + + + Fee + 1 sat/B + + + + Amount— + Network fee— + Total— + + + Send + diff --git a/bundled-addons/bchwallet/panel.js b/bundled-addons/bchwallet/panel.js index fd237cb..c00ca43 100644 --- a/bundled-addons/bchwallet/panel.js +++ b/bundled-addons/bchwallet/panel.js @@ -111,6 +111,77 @@ function flash(btn, text) { setTimeout(() => { btn.textContent = old; }, 1200); } +// ---- send ------------------------------------------------------------------ +let unit = "bch"; +let sendMax = false; +let planTimer = null; +let lastPlan = null; +function amountSats() { + const raw = $("sendAmt").value.trim().replace(/,/g, ""); + if (!raw) return 0; + if (unit === "sat") return Math.round(Number(raw)); + const [w, f = ""] = raw.split("."); + return Number(w || 0) * 1e8 + Math.round(Number("0." + (f || "0")) * 1e8); +} +function setUnit(u) { + if (u === unit) return; + const s = amountSats(); + unit = u; + $("unitBch").classList.toggle("on", u === "bch"); $("unitSat").classList.toggle("on", u === "sat"); + $("sendAmt").placeholder = u === "bch" ? "0.00" : "0"; + if (s) $("sendAmt").value = u === "bch" ? fmtBch(s) : String(s); +} +$("unitBch").addEventListener("click", () => setUnit("bch")); +$("unitSat").addEventListener("click", () => setUnit("sat")); +$("sendMax").addEventListener("click", () => { + sendMax = !sendMax; + $("sendMax").classList.toggle("primary", sendMax); + $("sendAmt").disabled = sendMax; + if (!sendMax) $("sendAmt").value = ""; + schedulePlan(); +}); +$("feeRate").addEventListener("input", () => { $("feeLbl").textContent = $("feeRate").value + " sat/B"; schedulePlan(); }); +["sendTo", "sendAmt"].forEach((id) => $(id).addEventListener("input", () => { if (id === "sendAmt" && sendMax) return; schedulePlan(); })); +function schedulePlan() { clearTimeout(planTimer); planTimer = setTimeout(updatePlan, 250); } +async function updatePlan() { + const to = $("sendTo").value.trim(); + const msg = $("sendMsg"); msg.hidden = true; + lastPlan = null; $("sendBtn").disabled = true; + $("sumAmt").textContent = $("sumFee").textContent = $("sumTotal").textContent = "—"; + $("sendToHint").textContent = ""; + if (!to || (!sendMax && !amountSats())) return; + try { + const p = await S.invoke("planSend", { to, amount: amountSats(), feeRate: Number($("feeRate").value), sendMax }); + lastPlan = p; + $("sendToHint").textContent = p.recipients[0].to !== to ? "→ " + p.recipients[0].to : ""; + $("sumAmt").textContent = fmtBch(p.recipients[0].value) + " BCH"; + $("sumFee").textContent = fmtSats(p.fee) + " sat"; + $("sumTotal").textContent = fmtBch(p.total) + " BCH"; + if (sendMax) $("sendAmt").value = unit === "bch" ? fmtBch(p.recipients[0].value) : String(p.recipients[0].value); + $("sendBtn").disabled = false; + } catch (e) { + msg.className = "msg err"; msg.textContent = cleanErr(e); msg.hidden = false; + } +} +$("sendBtn").addEventListener("click", async () => { + if (!lastPlan) return; + const msg = $("sendMsg"); msg.hidden = true; + $("sendBtn").disabled = true; $("sendBtn").textContent = "Waiting for approval…"; + try { + const r = await S.invoke("send", { to: $("sendTo").value.trim(), amount: amountSats(), feeRate: Number($("feeRate").value), sendMax }); + msg.className = "msg ok"; + msg.innerHTML = `Sent. ${esc(r.txid.slice(0, 16))}…`; + msg.querySelector("a").addEventListener("click", () => openUrl(state.explorerTx + r.txid)); + msg.hidden = false; + $("sendTo").value = ""; $("sendAmt").value = ""; sendMax = false; $("sendMax").classList.remove("primary"); $("sendAmt").disabled = false; + lastPlan = null; + } catch (e) { + const t = cleanErr(e); + if (t !== "cancelled") { msg.className = "msg err"; msg.textContent = t; msg.hidden = false; } + $("sendBtn").disabled = !lastPlan; + } finally { $("sendBtn").textContent = "Send"; } +}); + // ---- settings -------------------------------------------------------------- let settingsFilled = false; function fillSettings() {