From 916a748889a9db68c9c4006623940eb78a669564 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 4 Oct 2026 02:26:15 +0200 Subject: [PATCH] Aegis: sends name their wallet, amounts with spaces are refused, BCMR cleanup - send, sendToken and consolidate now require the wallet id the panel reviewed them for; a missing id used to skip the check, and the Solana token send sent none, so a selection change under the PIN pad sent from another wallet. - "1 0" was read as 10: a space inside an amount is now refused. - A BCMR registry list saved before https was enforced is filtered on read, and names lose the soft hyphen, Arabic letter mark, Mongolian vowel separator, line/paragraph separators and Unicode tag characters too. --- bundled-addons/aegis/index.js | 11 +++++++++-- bundled-addons/aegis/lib/bcmr.js | 14 ++++++++++---- bundled-addons/aegis/panel.js | 6 ++++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/bundled-addons/aegis/index.js b/bundled-addons/aegis/index.js index 2eff0291..afca59a8 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -1806,6 +1806,10 @@ function registerPanelMessages(api) { api.onMessage("sendToken", async (p, m) => { fromPanel(m); requirePanelTxPin(); + // Same rule as send: the token send names the wallet it was reviewed for. + if (!p || !p.walletId || String(p.walletId) !== selectedWalletId()) { + throw new Error("the selected wallet changed — review the send and try again"); + } const rt = requireSelected(); if (rt.entry.chain !== "sol") throw new Error("token send is Solana-only"); const plan = await rt.adapter.planTokenTransfer(p || {}); @@ -1834,7 +1838,10 @@ function registerPanelMessages(api) { // The panel names the wallet its form was built for. If the selection // moved since (another surface, a late state push), refuse rather than // send this amount from a different wallet. - if (p && p.walletId && String(p.walletId) !== selectedWalletId()) { + // Required, not optional: a send that does not say which wallet it was + // reviewed for cannot be checked against the selection. + if (!p || !p.walletId) throw new Error("send names no wallet — review the send and try again"); + if (String(p.walletId) !== selectedWalletId()) { throw new Error("the selected wallet changed — review the send and try again"); } const rt = requireSelected(); @@ -1936,7 +1943,7 @@ function registerPanelMessages(api) { // The destination is the wallet the PREVIEW was drawn for, not whatever // is selected by the time the button is pressed: a preview painted for A // followed by a switch to B used to sweep everything into B. - if (p && p.destinationWalletId && String(p.destinationWalletId) !== destId) { + if (!p || !p.destinationWalletId || String(p.destinationWalletId) !== destId) { throw new Error("the selected wallet changed since this preview — reopen Consolidate"); } const destEntry = walletEntries().find((w) => w.id === destId); diff --git a/bundled-addons/aegis/lib/bcmr.js b/bundled-addons/aegis/lib/bcmr.js index 8c7b7574..889407fa 100644 --- a/bundled-addons/aegis/lib/bcmr.js +++ b/bundled-addons/aegis/lib/bcmr.js @@ -46,8 +46,11 @@ const DEFAULT_REGISTRIES = [ module.exports = function makeBcmr({ storage, log = () => {} }) { function registryList() { + // Filtered on read as well: a list saved before setRegistries enforced + // https still carries its http entries. const custom = storage.get("bcmr/registries", null); - if (Array.isArray(custom) && custom.length) return custom; + const clean = Array.isArray(custom) ? custom.filter((r) => r && typeof r.url === "string" && /^https:\/\//i.test(r.url)) : []; + if (clean.length) return clean; return DEFAULT_REGISTRIES.slice(); } function setRegistries(list) { @@ -164,10 +167,13 @@ module.exports = function makeBcmr({ storage, log = () => {} }) { // // A local name wins over the registry: the user typed it for this exact // category, which is better evidence than a third-party document. - // Control, bidi-override, zero-width and BOM characters. Built from code + // Control, bidi-override, zero-width and BOM characters, plus the soft + // hyphen, the Arabic letter mark, the Mongolian vowel separator, the + // line/paragraph separators and the Unicode tag block. Built from code // points so no invisible character has to live in this source file. - const rng = (a, b) => String.fromCharCode(a) + "-" + String.fromCharCode(b); - const INVISIBLE = new RegExp("[" + rng(0x00, 0x1f) + rng(0x7f, 0x9f) + rng(0x200b, 0x200f) + rng(0x202a, 0x202e) + rng(0x2060, 0x2069) + rng(0xfeff, 0xfeff) + "]", "g"); + const rng = (a, b) => String.fromCodePoint(a) + "-" + String.fromCodePoint(b); + const INVISIBLE = new RegExp("[" + rng(0x00, 0x1f) + rng(0x7f, 0x9f) + rng(0xad, 0xad) + rng(0x61c, 0x61c) + rng(0x180e, 0x180e) + + rng(0x200b, 0x200f) + rng(0x2028, 0x202e) + rng(0x2060, 0x2069) + rng(0xfeff, 0xfeff) + rng(0xe0000, 0xe007f) + "]", "gu"); function cleanText(v, max) { if (typeof v !== "string") return null; const s = v.replace(INVISIBLE, "").trim().slice(0, max); diff --git a/bundled-addons/aegis/panel.js b/bundled-addons/aegis/panel.js index 09f97eb0..ca7bb954 100644 --- a/bundled-addons/aegis/panel.js +++ b/bundled-addons/aegis/panel.js @@ -4135,8 +4135,10 @@ function amountDecimals() { return sendAsset ? Number(sendAsset.decimals) || 0 : // guess: the old `.replace(/,/g, "")` turned a decimal comma ("0,5") into 5, // and Number() accepted "1e3", "0x10" and "-0.5". function parseAmountText(text) { - let raw = String(text == null ? "" : text).trim().replace(/\s/g, ""); + let raw = String(text == null ? "" : text).trim(); if (!raw) return { empty: true }; + // Space inside the number is refused, not deleted: "1 0" used to become 10. + if (/\s/.test(raw)) return { error: "Remove the space from the amount" }; if (/^\d{1,3}(,\d{3})+\.\d*$/.test(raw) || /^\d{1,3}(,\d{3}){2,}$/.test(raw)) { raw = raw.replace(/,/g, ""); // 1,234.5 · 1,234,567 } else if (/^\d{1,3},\d{3}$/.test(raw)) { @@ -4610,7 +4612,7 @@ $("sendBtn").addEventListener("click", async () => { try { const isToken = !!(req.mint && lastPlan._token); const r = isToken - ? await S.invoke("sendToken", { mint: req.mint, to: req.to, amount: req.amount }) + ? await S.invoke("sendToken", { walletId: req.walletId, mint: req.mint, to: req.to, amount: req.amount }) : await S.invoke("send", { walletId: req.walletId, to: req.to, amount: req.amount, feeRate: req.feeRate, sendMax: req.sendMax, memo: req.memo }); // A broadcast that returned no txid is still a broadcast: never report // it as a failure and leave a filled form inviting a second send.