diff --git a/addon-inject-preload.js b/addon-inject-preload.js index f4e90a8..50711b9 100644 --- a/addon-inject-preload.js +++ b/addon-inject-preload.js @@ -14,7 +14,8 @@ const { contextBridge, ipcRenderer } = require("electron"); let injections = []; -try { injections = ipcRenderer.sendSync("addon-inject-scripts", location.href) || []; } catch {} +try { injections = ipcRenderer.sendSync("addon-inject-scripts", location.href) || []; } +catch (e) { console.warn("[theseus] add-on inject query failed:", e?.message || e); } for (const inj of injections) { const id = String(inj.id); const theseus = Object.freeze({ diff --git a/bundled-addons/bchwallet/index.js b/bundled-addons/bchwallet/index.js index cfb221b..e658650 100644 --- a/bundled-addons/bchwallet/index.js +++ b/bundled-addons/bchwallet/index.js @@ -176,11 +176,112 @@ function registerPanelMessages(api) { }); } +// ---- dapp bridge (window.bitcoincash) --------------------------------------- +// Permissions live in storage as { [origin]: { readAddress: true } }. Only +// readAddress can be remembered; signing and sending ask every time. +const pendingByOrigin = new Set(); +function permissions(api) { const p = api.storage.get("permissions", {}); return p && typeof p === "object" ? p : {}; } +function fromPage(m) { + if (!m || m.from !== "page" || !m.origin) throw new Error("page-only message"); + return m.origin; +} +// One approval in flight per origin — a page can't stack modals. +async function withOriginLock(origin, fn) { + if (pendingByOrigin.has(origin)) throw new Error("a wallet request from this site is already waiting for approval"); + pendingByOrigin.add(origin); + try { return await fn(); } finally { pendingByOrigin.delete(origin); } +} +const MAGIC = "Bitcoin Signed Message:\n"; +function messageDigest(sha256, message) { + const enc = new TextEncoder(); + const varstr = (s) => { const b = enc.encode(s); if (b.length >= 0xfd) throw new Error("message too long"); return Uint8Array.from([b.length, ...b]); }; + const payload = Uint8Array.from([...varstr(MAGIC), ...varstr(String(message))]); + return sha256(sha256(payload)); +} + +function registerPageMessages(api) { + api.onMessage("getAddress", async (_p, m) => { + const origin = fromPage(m); + const c = requireReady(); + const perms = permissions(api); + if (perms[origin] && perms[origin].readAddress) return c.wallet.current().address; + return withOriginLock(origin, async () => { + const pick = await api.approvalModal({ + title: "Share your Bitcoin Cash address?", + origin, + body: "The site will see your current receiving address and can look up its balance and history on the public chain.", + rows: [{ label: "Address", value: c.wallet.current().address, mono: true }], + actions: [{ id: "allow", label: "Share", primary: true }], + checkbox: { id: "always", label: "Always allow this site to see my address" }, + }); + if (!pick.startsWith("allow")) throw new Error("user rejected"); + if (pick === "allow+always") { perms[origin] = { ...(perms[origin] || {}), readAddress: true }; api.storage.set("permissions", perms); emitState(); } + return c.wallet.current().address; + }); + }); + api.onMessage("signAndSend", async (p, m) => { + const origin = fromPage(m); + const c = requireReady(); + return withOriginLock(origin, async () => { + let plan; + // Never echo the shortfall to a page — it would let a site probe the + // balance by bisecting amounts. + try { plan = planFrom(p); } + catch (e) { throw new Error(/insufficient funds|too small/i.test(e?.message) ? "insufficient funds" : e?.message || String(e)); } + const d = describePlan(plan); + if (d.recipients.length > 8) throw new Error("too many outputs"); + const rows = d.recipients.map((r, i) => ({ label: d.recipients.length > 1 ? `To #${i + 1}` : "To", value: r.to, mono: true })); + rows.push({ label: "Amount", value: fmtBch(d.recipients.reduce((a, r) => a + r.value, 0)) + " BCH", strong: true }); + rows.push({ label: "Fee", value: `${d.fee} sat (${d.feeRate} sat/B)` }); + rows.push({ label: "Total", value: fmtBch(d.total) + " BCH" }); + const pick = await api.approvalModal({ + title: "Send Bitcoin Cash?", + origin, + body: "This site is asking your wallet to pay. Check the address and amount.", + rows, + actions: [{ id: "send", label: "Send", primary: true }], + }); + if (pick !== "send") throw new Error("user rejected"); + const r = await c.wallet.signAndBroadcast(plan); + return { txid: r.txid }; + }); + }); + api.onMessage("signMessage", async (p, m) => { + const origin = fromPage(m); + const c = requireReady(); + const message = String(p && p.message != null ? p.message : ""); + if (message.length > 4096) throw new Error("message too long"); + return withOriginLock(origin, async () => { + const entry = c.wallet.current(); + const pick = await api.approvalModal({ + title: "Sign a message?", + origin, + body: "Signing proves you control the address below. It moves no coins.", + rows: [{ label: "Message", value: message.length > 400 ? message.slice(0, 400) + "…" : message, mono: true }, { label: "Address", value: entry.address, mono: true }], + actions: [{ id: "sign", label: "Sign", primary: true }], + }); + if (pick !== "sign") throw new Error("user rejected"); + const sig = c.keys.signRecoverable(entry, messageDigest(c.d.sha256, message)); + return { address: entry.address, signature: Buffer.from(sig).toString("base64") }; + }); + }); + // Panel-side management of remembered sites. + api.onMessage("permissions", (_p, m) => { fromPanel(m); return permissions(api); }); + api.onMessage("revoke", (p, m) => { + fromPanel(m); + const perms = permissions(api); + delete perms[String(p && p.origin || "")]; + api.storage.set("permissions", perms); + return perms; + }); +} + module.exports = { activate(api) { api.registerSidebarPanel({ id: "main", title: "Wallet", icon: "₿", page: "panel.html" }); const c = ctx = { api, d: null, keys: null, wallet: null, client: null, root: null, phase: "locked", error: null }; registerPanelMessages(api); + registerPageMessages(api); deps(api).then((d) => { if (ctx !== c) return; c.d = d; diff --git a/bundled-addons/bchwallet/panel.html b/bundled-addons/bchwallet/panel.html index 39028b7..537d2f8 100644 --- a/bundled-addons/bchwallet/panel.html +++ b/bundled-addons/bchwallet/panel.html @@ -174,6 +174,11 @@ +