fix(aegis): 0.17.0 — both BCMR registries were dead, and self-minted tokens can now be named

Assets listed but every row read as a hex string. Three separate reasons.

**Both default registries were dead.** Checked 2026-09-29:
raw.githubusercontent.com/cashonize/registry/main/bcmr.json returns 404, and
bcmr.salemkode.com does not resolve. So no token resolved a name on any
chain, mainnet included — and the panel's `.catch(() => {})` meant the
failure was completely silent, indistinguishable from a token nobody has
registered. Replaced with OpenTokenRegistry, which answers and carries
chipnet identities. A dead registry is now reported instead of swallowed:
the reply says whether any registry answered, and the hint says so.

**The tokens in question publish no metadata at all.** Not a wallet problem
and not fixable by any registry list: their genesis transactions carry no
OP_RETURN whatsoever, so there is no BCMR authchain to follow and no
registry entry to find. Their names exist only inside the app that minted
them. So a token can now be named locally, per category, stored under
bcmr/local/<cat> and taking precedence over any registry — with a YOURS tag
so a self-assigned name is never mistaken for a published one. Clearing both
fields removes it and lets a registry entry show through again. Optional
decimals, because a raw fungible amount with no scale is its own kind of
wrong: 15000 became "150 GMX" once told there were two.

**The unnamed row printed the category twice**, once as the name fallback and
once as the sub-line. Unnamed rows now show the category as the identity and
"unnamed token · N UTXOs" beneath it.

The header comment also promised a bundled static registry fallback for
well-known tokens. There is no such directory and no load path for one; the
comment is gone rather than left to mislead.

Verified against the real module: local names beat registry entries, a
category with only a local name resolves instead of returning null, clearing
restores the registry value, out-of-range decimals are dropped, and the new
default registry resolves a real chipnet identity (OTRC, 6 decimals). In the
panel: naming a token repaints it with the tag and rescales the amount, and a
non-numeric decimals entry is refused with the modal left open.
This commit is contained in:
Local Dev 2026-09-29 00:13:30 +02:00
parent b42f304ce8
commit 0f3dd6e9ca
4 changed files with 180 additions and 23 deletions

View file

@ -1,7 +1,7 @@
{ {
"id": "aegis", "id": "aegis",
"name": "Aegis Wallet", "name": "Aegis Wallet",
"version": "0.16.0", "version": "0.17.0",
"category": "plugin", "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.", "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", "author": "Silent Mode",

View file

@ -2245,10 +2245,28 @@ function registerPageMessages(api) {
if (!cats.length) return {}; if (!cats.length) return {};
const entries = await ctx.bcmr.lookupMany(cats); const entries = await ctx.bcmr.lookupMany(cats);
const out = {}; const out = {};
for (const [cat, entry] of Object.entries(entries)) { // Pass the category so a local name can win over the registry — and so
out[cat] = ctx.bcmr.metadataOf(entry); // a category with ONLY a local name still comes back named instead of
} // null, which is the whole point for self-minted tokens.
return out; 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). // Read the configured BCMR registry list (defaults + any user additions).
api.onMessage("bcmrRegistries", (_p, m) => { api.onMessage("bcmrRegistries", (_p, m) => {

View file

@ -9,10 +9,16 @@
// 1. HTTPS URL configured per-user in Settings ("registry endpoints"). // 1. HTTPS URL configured per-user in Settings ("registry endpoints").
// The registry publishes a compact JSON with keyed identities; // The registry publishes a compact JSON with keyed identities;
// lookup by category is O(1). // lookup by category is O(1).
// 2. Static bundled fallback (registries/) for a handful of well-known // 2. A local name the user sets themselves, stored under
// tokens (Cauldron, Fex.cash, TapSwap, ParyonUSD). Ships in the // "bcmr/local/<categoryHex>" and taking precedence over any registry.
// addon so brand-new users see names on the first launch even // Self-minted tokens — anything on chipnet, anything from a project
// before they configure a live registry. // 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/<categoryHex>" = // Cache is on-disk via api.storage under "bcmr/<categoryHex>" =
// { snapshot, fetchedAt, source }. A metadata refresh runs at most once // { 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 // 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 / // any configuration to see names for the top BCH tokens. Users can add /
// remove entries in Settings. // 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 = [ const DEFAULT_REGISTRIES = [
{ id: "cashonize", label: "Cashonize registry", url: "https://raw.githubusercontent.com/cashonize/registry/main/bcmr.json" }, { id: "otr", label: "OpenTokenRegistry", url: "https://otr.cash/.well-known/bitcoin-cash-metadata-registry.json" },
{ id: "salemkode", label: "SalemKode registry", url: "https://bcmr.salemkode.com/registry.json" },
]; ];
module.exports = function makeBcmr({ storage, log = () => {} }) { module.exports = function makeBcmr({ storage, log = () => {} }) {
@ -124,23 +136,56 @@ module.exports = function makeBcmr({ storage, log = () => {} }) {
return storage.get(`bcmr/${categoryHex}`, null); 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, // Compact metadata slice the panel wants: { name, symbol, description,
// decimals, iconUri }. Handles both the top-level identity fields and // decimals, iconUri }. Handles both the top-level identity fields and
// the token subobject (BCMR v2 puts token-specific data there). // 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 s = entry.snapshot;
const t = s.token || {}; const t = s.token || {};
return { return {
name: s.name || t.name || null, name: local?.name || s.name || t.name || null,
symbol: s.token?.symbol || s.symbol || null, symbol: local?.symbol || s.token?.symbol || s.symbol || null,
description: s.description || 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. // Icon URIs live under s.uris.icon per schema; older files use s.icon.
iconUri: s.uris?.icon || s.icon || null, 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 };
}; };

View file

@ -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 = `
<div style="width:min(94vw,380px);background:var(--panel,#12161e);border:1px solid var(--line,#2a2f38);border-radius:10px;padding:14px 14px 12px;box-shadow:0 10px 40px rgba(0,0,0,.4)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
<div style="font-weight:600;flex:1">Name this token</div>
<button class="btn sm" id="tlClose" type="button">✕</button>
</div>
<div class="hint mono" style="margin-bottom:10px;word-break:break-all">${esc(category)}</div>
<div class="field">
<div class="lbl">Name</div>
<input type="text" id="tlName" maxlength="40" placeholder="e.g. Game.X credit" value="${esc(isLocal ? (meta?.name || "") : "")}">
</div>
<div class="field">
<div class="lbl">Ticker</div>
<input type="text" id="tlSymbol" maxlength="12" placeholder="e.g. GMX" value="${esc(isLocal ? (meta?.symbol || "") : "")}">
</div>
<div class="field">
<div class="lbl">Decimals</div>
<input type="text" id="tlDecimals" inputmode="numeric" placeholder="0" value="${esc(isLocal && Number.isFinite(meta?.decimals) ? String(meta.decimals) : "")}">
<div class="hint">How many decimal places the amount should be shown with. Leave blank for whole units.</div>
</div>
<div class="hint" style="margin-top:8px">Stored on this device only. It takes precedence over any BCMR registry; clear both fields to remove it.</div>
<div class="msg err" id="tlMsg" hidden style="margin-top:8px"></div>
<div class="actions" style="justify-content:space-between;margin-top:12px">
${isLocal ? `<button class="btn danger" id="tlClear">Remove name</button>` : `<span></span>`}
<div style="display:flex;gap:6px">
<button class="btn" id="tlCancel">Cancel</button>
<button class="btn primary" id="tlSave">Save</button>
</div>
</div>
</div>`;
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() { function renderTokens() {
const s = sel(); const s = sel();
const card = $("tokensCard"); const card = $("tokensCard");
@ -3451,12 +3516,29 @@ function renderTokens() {
const fungibleTxt = showFungible ? fmtTokenAmount(fungibleRaw, decimals) + (symbol ? " " + symbol : "") : ""; const fungibleTxt = showFungible ? fmtTokenAmount(fungibleRaw, decimals) + (symbol ? " " + symbol : "") : "";
const nftTxt = nftCount ? `${nftCount} NFT${nftCount === 1 ? "" : "s"}` : ""; const nftTxt = nftCount ? `${nftCount} NFT${nftCount === 1 ? "" : "s"}` : "";
const amountLine = [fungibleTxt, nftTxt].filter(Boolean).join(" · ") || "—"; const amountLine = [fungibleTxt, nftTxt].filter(Boolean).join(" · ") || "—";
const nameLine = name ? `${iconHtml}<b>${esc(name)}</b>${symbol && name !== symbol ? ` <span class="hint">${esc(symbol)}</span>` : ""}` : `${iconHtml}<span class="mono hint">${esc(catShort)}</span>`; const localTag = meta?.local ? ` <span class="ttag" title="A name you set — no registry knows this token">YOURS</span>` : "";
return `<div class="tx" style="grid-template-columns:1fr auto;cursor:default;align-items:center"> // Named: name on top, category beneath. Unnamed: the category IS the
<div><div>${nameLine}</div><div class="hint mono">${esc(catShort)}</div></div> // 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}<b>${esc(name)}</b>${symbol && name !== symbol ? ` <span class="hint">${esc(symbol)}</span>` : ""}${localTag}`
: `${iconHtml}<span class="mono">${esc(catShort)}</span>`;
const subLine = name
? `<span class="mono">${esc(catShort)}</span>`
: ["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 `<div class="tx" style="grid-template-columns:1fr auto auto;cursor:default;align-items:center">
<div><div>${nameLine}</div><div class="hint mono">${subLine}</div></div>
<div class="amt2 in" style="align-self:center;text-align:right">${esc(amountLine)}</div> <div class="amt2 in" style="align-self:center;text-align:right">${esc(amountLine)}</div>
<button class="wact" data-tokname="${esc(cat)}" title="Name this token locally" style="align-self:center">✎</button>
</div>`; </div>`;
}).join(""); }).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 // Paint immediately with whatever we know synchronously so the row
// set doesn't wait for the network. Then run the async lookup and // set doesn't wait for the network. Then run the async lookup and
@ -3464,8 +3546,20 @@ function renderTokens() {
draw({}); draw({});
S.invoke("tokenMetadata", { categories: cats }).then((res) => { S.invoke("tokenMetadata", { categories: cats }).then((res) => {
if (chain() !== "bch") return; // user switched away mid-fetch if (chain() !== "bch") return; // user switched away mid-fetch
draw(res || {}); // 0.17.0: the reply carries the map under .meta plus whether any
}).catch(() => {}); // 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; return;
} }
// Every other account-model chain (TRX, ETH, and anything added later) // Every other account-model chain (TRX, ETH, and anything added later)