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:
parent
b42f304ce8
commit
0f3dd6e9ca
4 changed files with 180 additions and 23 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue