diff --git a/bundled-addons/aegis/addon.json b/bundled-addons/aegis/addon.json index f7b2f020..d9f82af2 100644 --- a/bundled-addons/aegis/addon.json +++ b/bundled-addons/aegis/addon.json @@ -1,7 +1,7 @@ { "id": "aegis", "name": "Aegis Wallet", - "version": "0.16.0", + "version": "0.17.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 2c7e7009..cb329b27 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -2245,10 +2245,28 @@ function registerPageMessages(api) { if (!cats.length) return {}; const entries = await ctx.bcmr.lookupMany(cats); const out = {}; - for (const [cat, entry] of Object.entries(entries)) { - out[cat] = ctx.bcmr.metadataOf(entry); - } - return out; + // Pass the category so a local name can win over the registry — and so + // a category with ONLY a local name still comes back named instead of + // null, which is the whole point for self-minted tokens. + for (const cat of cats) out[cat] = ctx.bcmr.metadataOf(entries[cat], cat); + // Tell the panel whether the registries themselves are reachable. A dead + // registry used to be indistinguishable from an unregistered token, and + // both defaults were 404 for who knows how long because of it. + const reachable = Object.values(entries).some((e) => e && e.snapshot); + return { meta: out, registriesAnswered: reachable, registries: ctx.bcmr.registryList().length }; + }); + + // Name a token yourself. The only way to label a category that publishes + // no metadata anywhere — no registry entry, and commonly not even an + // OP_RETURN in its genesis transaction. + api.onMessage("setTokenLabel", (p, m) => { + fromPanel(m); + const cat = String(p?.category || ""); + if (!/^[0-9a-f]{64}$/i.test(cat)) throw new Error("not a token category"); + const rec = ctx.bcmr.setLocalName(cat, { + name: p?.name, symbol: p?.symbol, decimals: p?.decimals, + }); + return { category: cat, local: rec }; }); // Read the configured BCMR registry list (defaults + any user additions). api.onMessage("bcmrRegistries", (_p, m) => { diff --git a/bundled-addons/aegis/lib/bcmr.js b/bundled-addons/aegis/lib/bcmr.js index 74718d02..f597ee0f 100644 --- a/bundled-addons/aegis/lib/bcmr.js +++ b/bundled-addons/aegis/lib/bcmr.js @@ -9,10 +9,16 @@ // 1. HTTPS URL configured per-user in Settings ("registry endpoints"). // The registry publishes a compact JSON with keyed identities; // lookup by category is O(1). -// 2. Static bundled fallback (registries/) for a handful of well-known -// tokens (Cauldron, Fex.cash, TapSwap, ParyonUSD). Ships in the -// addon so brand-new users see names on the first launch even -// before they configure a live registry. +// 2. A local name the user sets themselves, stored under +// "bcmr/local/" and taking precedence over any registry. +// Self-minted tokens — anything on chipnet, anything from a project +// that keeps its own names client-side — publish no metadata at all: +// no registry entry, and often not even an OP_RETURN in the genesis +// transaction, so there is nowhere for a wallet to look. A local label +// is the only thing that can name those. +// +// (An earlier comment here promised a bundled static fallback for well-known +// tokens. There is no such directory and never was; it is not a load path.) // // Cache is on-disk via api.storage under "bcmr/" = // { snapshot, fetchedAt, source }. A metadata refresh runs at most once @@ -26,9 +32,15 @@ const REFRESH_MIN_MS = 6 * 60 * 60 * 1000; // 6 hours // Well-known registries seeded on first run so a fresh wallet doesn't need // any configuration to see names for the top BCH tokens. Users can add / // remove entries in Settings. +// Both of the previous defaults were dead — checked 2026-09-29: +// raw.githubusercontent.com/cashonize/registry/main/bcmr.json -> 404 +// bcmr.salemkode.com/registry.json -> DNS fail +// So NO token ever resolved a name, on any chain, and the panel's +// .catch(() => {}) meant the failure was completely silent. Anything added +// here should be re-checked rather than trusted; a registry that 404s is +// indistinguishable from a token nobody has registered. const DEFAULT_REGISTRIES = [ - { id: "cashonize", label: "Cashonize registry", url: "https://raw.githubusercontent.com/cashonize/registry/main/bcmr.json" }, - { id: "salemkode", label: "SalemKode registry", url: "https://bcmr.salemkode.com/registry.json" }, + { id: "otr", label: "OpenTokenRegistry", url: "https://otr.cash/.well-known/bitcoin-cash-metadata-registry.json" }, ]; module.exports = function makeBcmr({ storage, log = () => {} }) { @@ -124,23 +136,56 @@ module.exports = function makeBcmr({ storage, log = () => {} }) { return storage.get(`bcmr/${categoryHex}`, null); } + // ---- local names --------------------------------------------------------- + // A name the user typed for a category no registry knows about. Kept + // separate from the BCMR cache so a later registry fetch cannot clobber it, + // and so clearing it falls back to whatever the registry says. + function localName(categoryHex) { + const v = storage.get(`bcmr/local/${categoryHex}`, null); + return v && (v.name || v.symbol) ? v : null; + } + function setLocalName(categoryHex, { name, symbol, decimals } = {}) { + const key = `bcmr/local/${categoryHex}`; + const n = String(name || "").trim().slice(0, 40); + const s = String(symbol || "").trim().slice(0, 12); + const d = Number(decimals); + if (!n && !s) { storage.set(key, null); return null; } + const rec = { name: n || null, symbol: s || null }; + if (Number.isFinite(d) && d >= 0 && d <= 18) rec.decimals = Math.floor(d); + storage.set(key, rec); + return rec; + } + // Compact metadata slice the panel wants: { name, symbol, description, // decimals, iconUri }. Handles both the top-level identity fields and // the token subobject (BCMR v2 puts token-specific data there). - function metadataOf(entry) { - if (!entry || !entry.snapshot) return null; + // + // A local name wins over the registry: the user typed it for this exact + // category, which is better evidence than a third-party document. + function metadataOf(entry, categoryHex) { + const local = categoryHex ? localName(categoryHex) : null; + if (!entry || !entry.snapshot) { + return local + ? { name: local.name, symbol: local.symbol, description: null, + decimals: Number.isFinite(local.decimals) ? local.decimals : 0, + iconUri: null, source: "local", local: true } + : null; + } const s = entry.snapshot; const t = s.token || {}; return { - name: s.name || t.name || null, - symbol: s.token?.symbol || s.symbol || null, + name: local?.name || s.name || t.name || null, + symbol: local?.symbol || s.token?.symbol || s.symbol || null, description: s.description || null, - decimals: Number.isFinite(Number(t.decimals)) ? Number(t.decimals) : 0, + decimals: Number.isFinite(local?.decimals) ? local.decimals + : (Number.isFinite(Number(t.decimals)) ? Number(t.decimals) : 0), // Icon URIs live under s.uris.icon per schema; older files use s.icon. iconUri: s.uris?.icon || s.icon || null, - source: entry.source || null, + source: local ? "local" : (entry.source || null), + local: !!local, }; } - return { lookup, lookupMany, cached, metadataOf, registryList, setRegistries, DEFAULT_REGISTRIES }; + return { lookup, lookupMany, cached, metadataOf, localName, setLocalName, + registryList, setRegistries, DEFAULT_REGISTRIES }; }; diff --git a/bundled-addons/aegis/panel.js b/bundled-addons/aegis/panel.js index 72bc4d45..e3ede775 100644 --- a/bundled-addons/aegis/panel.js +++ b/bundled-addons/aegis/panel.js @@ -3398,6 +3398,71 @@ function setAssetsCount(n) { } } +// Name a token yourself. Self-minted categories publish nothing — no +// registry entry, and usually not even an OP_RETURN in the genesis — so +// without this they can only ever read as a hex string. Clearing both fields +// deletes the local name and lets any registry entry show through again. +function openTokenLabelModal(category, meta) { + const overlay = document.createElement("div"); + overlay.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.55);display:flex;align-items:flex-start;justify-content:center;z-index:100000;padding-top:24px"; + const isLocal = !!meta?.local; + overlay.innerHTML = ` +
+
+
Name this token
+ +
+
${esc(category)}
+
+
Name
+ +
+
+
Ticker
+ +
+
+
Decimals
+ +
How many decimal places the amount should be shown with. Leave blank for whole units.
+
+
Stored on this device only. It takes precedence over any BCMR registry; clear both fields to remove it.
+ +
+ ${isLocal ? `` : ``} +
+ + +
+
+
`; + document.body.appendChild(overlay); + const close = () => { try { overlay.remove(); } catch (_e) {} }; + overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); }); + overlay.querySelector("#tlClose").addEventListener("click", close); + overlay.querySelector("#tlCancel").addEventListener("click", close); + const save = async (clear) => { + const msg = overlay.querySelector("#tlMsg"); msg.hidden = true; + const dRaw = overlay.querySelector("#tlDecimals").value.trim(); + if (!clear && dRaw && !/^\d{1,2}$/.test(dRaw)) { + msg.textContent = "Decimals must be a whole number between 0 and 18."; msg.hidden = false; return; + } + try { + await S.invoke("setTokenLabel", { + category, + name: clear ? "" : overlay.querySelector("#tlName").value, + symbol: clear ? "" : overlay.querySelector("#tlSymbol").value, + decimals: clear || !dRaw ? undefined : Number(dRaw), + }); + close(); + renderTokens(); + } catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; } + }; + overlay.querySelector("#tlSave").addEventListener("click", () => save(false)); + if (isLocal) overlay.querySelector("#tlClear").addEventListener("click", () => save(true)); + overlay.querySelector("#tlName").focus(); +} + function renderTokens() { const s = sel(); const card = $("tokensCard"); @@ -3451,12 +3516,29 @@ function renderTokens() { const fungibleTxt = showFungible ? fmtTokenAmount(fungibleRaw, decimals) + (symbol ? " " + symbol : "") : ""; const nftTxt = nftCount ? `${nftCount} NFT${nftCount === 1 ? "" : "s"}` : ""; const amountLine = [fungibleTxt, nftTxt].filter(Boolean).join(" · ") || "—"; - const nameLine = name ? `${iconHtml}${esc(name)}${symbol && name !== symbol ? ` ${esc(symbol)}` : ""}` : `${iconHtml}${esc(catShort)}`; - return `
-
${nameLine}
${esc(catShort)}
+ const localTag = meta?.local ? ` YOURS` : ""; + // Named: name on top, category beneath. Unnamed: the category IS the + // only identity, so it goes on top and the sub-line says what it is + // rather than repeating the same hex twice. + const utxoTxt = b.utxoCount ? `${b.utxoCount} UTXO${b.utxoCount === 1 ? "" : "s"}` : ""; + const nameLine = name + ? `${iconHtml}${esc(name)}${symbol && name !== symbol ? ` ${esc(symbol)}` : ""}${localTag}` + : `${iconHtml}${esc(catShort)}`; + const subLine = name + ? `${esc(catShort)}` + : ["unnamed token", utxoTxt].filter(Boolean).join(" · "); + // ✎ per row: these tokens often publish no metadata at all, so + // naming one locally is the only way it will ever read as anything + // but a hex string. + return `
+
${nameLine}
${subLine}
${esc(amountLine)}
+
`; }).join(""); + el.querySelectorAll("[data-tokname]").forEach((b) => b.addEventListener("click", () => { + openTokenLabelModal(b.dataset.tokname, metaMap?.[b.dataset.tokname] || null); + })); }; // Paint immediately with whatever we know synchronously so the row // set doesn't wait for the network. Then run the async lookup and @@ -3464,8 +3546,20 @@ function renderTokens() { draw({}); S.invoke("tokenMetadata", { categories: cats }).then((res) => { if (chain() !== "bch") return; // user switched away mid-fetch - draw(res || {}); - }).catch(() => {}); + // 0.17.0: the reply carries the map under .meta plus whether any + // registry actually answered. The old shape was the bare map and the + // failure path was .catch(() => {}), so two dead default registries + // went unnoticed — every token looked simply unregistered. + const map = res && res.meta ? res.meta : (res || {}); + draw(map); + if (hintEl && res && res.registriesAnswered === false) { + hintEl.textContent = res.registries + ? `No configured BCMR registry answered, so names are unavailable — check the registry list in Settings, or name a token yourself with ✎.` + : `No BCMR registry configured, so names are unavailable — add one in Settings, or name a token yourself with ✎.`; + } + }).catch((e) => { + if (hintEl) hintEl.textContent = `Token names unavailable: ${cleanErr(e)}. You can still name a token yourself with ✎.`; + }); return; } // Every other account-model chain (TRX, ETH, and anything added later)