From 6bb3c50d0d02ff63f3fb96bc1b100d0a02a0f374 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Thu, 1 Oct 2026 00:59:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(aegis):=200.21.0=20=E2=80=94=20you=20pick?= =?UTF-8?q?=20which=20wallet=20pays=20and=20which=20one=20dapps=20get?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A WizardConnect pairing could land on an address the user had never seen. Pairing took the selected wallet when it qualified and otherwise the first pairable one in list order, so with the selected wallet ineligible (a WIF import has no xpub) it silently fell through to whichever wallet happened to be first. The approval named that wallet by label only, which does not help when the label is one Aegis generated. Three changes, one idea: the wallet a purpose uses should be something you said, not something that fell out of list order. - Roles. A wallet can be nominated for payments and/or for WizardConnect, set from its manage modal and badged on its row. A role that points at a removed wallet reads back as null instead of being trusted, so a stale pointer can never quietly redirect a payment. - The pairing approval asks. With more than one candidate it offers a dropdown of them, each labelled with its own short address; with only one it shows that wallet's full address. The rows are static, so naming a wallet above a select the user can change would contradict itself — hence one or the other, never both. The panel's own Connect pane now follows the same precedence, because two rules for "which wallet" is how a pairing surprises someone. - Send gets a From row listing the wallets on this coin and network, with balances. It switches the panel selection rather than carrying a separate source: planSend and send resolve the wallet host-side from that, and a second notion of "current" would let the form and the approval disagree. Payments also becomes the opening selection when nothing has been picked yet, which is what nominating it is for. No Theseus release needed — approvalModal has supported a select row all along, and the pick comes back as "allow+wallet=", validated against the options offered. --- bundled-addons/aegis/addon.json | 2 +- bundled-addons/aegis/index.js | 119 +++++++++++++++++++++++++++++--- bundled-addons/aegis/panel.html | 17 +++++ bundled-addons/aegis/panel.js | 107 +++++++++++++++++++++++++++- 4 files changed, 234 insertions(+), 11 deletions(-) diff --git a/bundled-addons/aegis/addon.json b/bundled-addons/aegis/addon.json index e71bfa7e..4f2c53f3 100644 --- a/bundled-addons/aegis/addon.json +++ b/bundled-addons/aegis/addon.json @@ -1,7 +1,7 @@ { "id": "aegis", "name": "Aegis Wallet", - "version": "0.20.0", + "version": "0.21.0", "category": "plugin", "description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites; window.tronWeb / window.tronLink / window.ethereum / window.solana on any https page.", "author": "Silent Mode", diff --git a/bundled-addons/aegis/index.js b/bundled-addons/aegis/index.js index cb329b27..be96c8d9 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -880,6 +880,42 @@ function buildWcApprovalBody(payload) { + "
" + bc + " Signs with SIGHASH_ALL|FORKID|UTXOS.
"; } +// ---- per-purpose default wallets ------------------------------------------- +// The wallet you pay from and the wallet you hand to a dapp are not +// necessarily the same one, and until now both fell out of whatever happened +// to be selected in the panel. That is why a WizardConnect pairing could land +// on an address the user did not recognise: with the selected wallet +// ineligible, pairing silently took the first one in list order. +// +// A role points at a wallet id. An id whose wallet has since been removed +// reads back as null rather than being trusted, so a stale pointer can never +// quietly redirect a payment. +const WALLET_ROLES = ["payments", "wizardconnect"]; + +function walletRoles() { + const raw = ctx.api.storage.get("aegis/roles/v1", null); + const list = readWallets(ctx.api) || []; + const out = {}; + for (const role of WALLET_ROLES) { + const id = raw && typeof raw === "object" ? String(raw[role] || "") : ""; + out[role] = id && list.some((w) => w.id === id) ? id : null; + } + return out; +} + +function setWalletRole(role, walletId) { + if (!WALLET_ROLES.includes(role)) throw new Error("unknown role: " + role); + const next = walletRoles(); + if (walletId == null || walletId === "") next[role] = null; + else { + const list = readWallets(ctx.api) || []; + if (!list.some((w) => w.id === walletId)) throw new Error("no such wallet"); + next[role] = String(walletId); + } + ctx.api.storage.set("aegis/roles/v1", next); + return next; +} + // ---- state / snapshot ------------------------------------------------------- function selectedWalletId() { @@ -887,6 +923,10 @@ function selectedWalletId() { if (!list.length) return null; const saved = String(ctx.api.storage.get("selectedWalletId", "") || ""); if (saved && list.some((w) => w.id === saved)) return saved; + // Nothing picked yet this session. The wallet the user nominated for + // payments is a better opening guess than list order. + const pay = walletRoles().payments; + if (pay) return pay; const dflt = list.find((w) => w.isDefault) || list[0]; return dflt.id; } @@ -974,6 +1014,7 @@ function fullState() { return { overallPhase: overallPhase(), selectedWalletId: selectedWalletId(), + roles: walletRoles(), wallets: walletEntries().map(walletSummary), selected: snapshotForSelected(), bchServers: { @@ -1008,6 +1049,15 @@ function fromPage(m) { return m.origin; } +// Head and tail of an address, with any "bitcoincash:"/"bchtest:" prefix +// dropped — the prefix is the same on every row, so it costs width without +// helping anyone tell two addresses apart. +function shortAddr(addr) { + const s = String(addr || ""); + const body = s.includes(":") ? s.slice(s.indexOf(":") + 1) : s; + return body.length <= 20 ? body : body.slice(0, 10) + "…" + body.slice(-6); +} + const fmtBch = (sats) => (Number(sats) / 1e8).toFixed(8).replace(/(\.\d*?[1-9])0+$|\.0+$/, "$1"); const fmtTrx = (sun) => (Number(sun) / 1e6).toFixed(6).replace(/(\.\d*?[1-9])0+$|\.0+$/, "$1"); function fmtValue(units, decimals) { @@ -1842,22 +1892,61 @@ function registerPanelMessages(api) { if (!candidates.length) { throw new Error("No Bitcoin Cash wallet is ready to pair. Unlock the Aegis vault (or add a BCH wallet) and try again."); } - // Prefer the selected wallet when it qualifies, so the approval matches - // whatever the user currently sees in the panel. + // Which wallet to offer first: the one nominated for WizardConnect, else + // the one the panel is showing, else list order. Whatever wins is only a + // default — with more than one candidate the approval lets the user say. + const wcRole = walletRoles().wizardconnect; const selId = selectedWalletId(); - const chosen = candidates.find((w) => w.id === selId) || candidates[0]; + const preferred = candidates.find((w) => w.id === wcRole) + || candidates.find((w) => w.id === selId) + || candidates[0]; + // Default first: approval.html renders options in order, so the browser + // selects the head of the list. + const ordered = [preferred, ...candidates.filter((w) => w.id !== preferred.id)]; + const addrOf = (w) => { + try { return ctx.runtimes.get(w.id)?.adapter?.snapshot()?.address || ""; } + catch { return ""; } + }; + // The address, not just the label — "I don't recognise the connected + // wallet" is the failure this dialog has to prevent, and a label the user + // never chose ("BCH wallet 2") does not prevent it. + const describe = (w) => { + const a = addrOf(w); + return a ? `${w.label} · ${shortAddr(a)}` : w.label; + }; + const choose = ordered.length > 1; + // With a choice on offer the dropdown is the only honest statement of + // which wallet pairs: the rows are static, so a "Wallet: X" line above a + // select the user just changed to Y would contradict itself. Each option + // therefore carries its own short address, and the full-address row shows + // only when there is nothing to choose. + const rows = choose + ? [{ label: "Pairing code", value: uri.slice(0, 48) + (uri.length > 48 ? "…" : ""), mono: true }] + : [ + { label: "Wallet", value: preferred.label, strong: true }, + { label: "Address", value: addrOf(preferred) || "—", mono: true }, + { label: "Pairing code", value: uri.slice(0, 48) + (uri.length > 48 ? "…" : ""), mono: true }, + ]; return withOriginLock(origin, async () => { const pick = await api.approvalModal({ title: "Pair this site with your wallet?", origin, body: "The site will be able to ask Aegis to sign Bitcoin Cash transactions over WizardConnect. Every signature still needs your approval — pairing on its own moves no funds.", - rows: [ - { label: "Wallet", value: `${chosen.label}` }, - { label: "Pairing code", value: uri.slice(0, 48) + (uri.length > 48 ? "…" : ""), mono: true }, - ], + rows, actions: [{ id: "allow", label: "Pair", primary: true }], + select: choose ? { + id: "wallet", label: "Pair with", + options: ordered.map((w) => ({ value: w.id, label: describe(w) })), + } : null, }); - if (!String(pick || "").startsWith("allow")) throw new Error("pairing declined"); + const [action, ...flags] = String(pick || "").split("+"); + if (action !== "allow") throw new Error("pairing declined"); + const picked = flags.find((f) => f.startsWith("wallet=")); + const pickedId = picked ? picked.slice(7) : ""; + // Re-check against the candidate list: main validates the value came + // from the options we offered, but the wallet could have gone away + // while the dialog was open. + const chosen = candidates.find((w) => w.id === pickedId) || preferred; await ctx.wc.connectUri(chosen.id, uri); return { paired: true, wallet: chosen.label }; }); @@ -1883,6 +1972,20 @@ function registerPanelMessages(api) { return fullState(); }); + // Which wallet each purpose reaches for. Panel-only: a page must never be + // able to read the whole wallet set, let alone re-point a role at one. + api.onMessage("walletRoles", (_p, m) => { fromPanel(m); return walletRoles(); }); + api.onMessage("setWalletRole", (p, m) => { + fromPanel(m); + const role = String(p && p.role || ""); + const id = p && p.walletId ? String(p.walletId) : null; + setWalletRole(role, id); + emitState(); + // Full state, not just the role map — the panel assigns this straight + // onto `state`, and the badges it drives live on the wallet rows. + return fullState(); + }); + api.onMessage("permissions", (_p, m) => { fromPanel(m); return permissions(api); }); api.onMessage("revoke", (p, m) => { fromPanel(m); diff --git a/bundled-addons/aegis/panel.html b/bundled-addons/aegis/panel.html index c454abf6..7c0ba50f 100644 --- a/bundled-addons/aegis/panel.html +++ b/bundled-addons/aegis/panel.html @@ -152,6 +152,13 @@ .portfolio b { color: var(--ink); font-weight: 600; } .portfolio[hidden] { display: none; } .picker-actions { display: flex; align-items: center; gap: 6px; } + /* "this is the one I pay from" / "this is the one dapps get". Deliberately + quiet — it labels a row, it is not a control, and a wallet list where + every row shouts is a list nobody reads. */ + .rolebadge { font-size: 10px; line-height: 1; text-transform: uppercase; letter-spacing: .04em; + border: 1px solid var(--line); color: var(--mut); border-radius: 4px; + padding: 2px 4px; white-space: nowrap; } + .rolebadge.pay { color: var(--acid); border-color: color-mix(in srgb, var(--acid) 40%, var(--line)); } .chip { background: transparent; border: 1px solid var(--line); color: var(--mut); border-radius: 6px; padding: 0 8px; height: 22px; cursor: pointer; font: inherit; font-size: 13.5px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; } @@ -860,6 +867,16 @@ + +