theseus/bundled-addons/aegis/lib/bcmr.js

192 lines
8.4 KiB
JavaScript
Raw Normal View History

// BCMR (Bitcoin Cash Metadata Registry) fetcher + cache. Resolves a
// CashTokens category hex to human-readable metadata: name, description,
// symbol, decimals, icon URL, and per-NFT metadata when the registry
// carries it.
//
// Registries are plain JSON documents (Bitauth "Bitcoin Cash Metadata
// Registries v2" schema). We support two ways to reach a registry today:
//
// 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).
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.
2026-09-29 00:13:30 +02:00
// 2. A local name the user sets themselves, stored under
// "bcmr/local/<categoryHex>" 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/<categoryHex>" =
// { snapshot, fetchedAt, source }. A metadata refresh runs at most once
// per REFRESH_MIN_MS per category to keep the panel snappy on repaint.
// No signature verification yet (BCMR v2 spec allows authchain-anchored
// signing; adding that is a follow-up once we support arbitrary chain
// script parsing).
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.
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.
2026-09-29 00:13:30 +02:00
// 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 = [
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.
2026-09-29 00:13:30 +02:00
{ id: "otr", label: "OpenTokenRegistry", url: "https://otr.cash/.well-known/bitcoin-cash-metadata-registry.json" },
];
module.exports = function makeBcmr({ storage, log = () => {} }) {
function registryList() {
const custom = storage.get("bcmr/registries", null);
if (Array.isArray(custom) && custom.length) return custom;
return DEFAULT_REGISTRIES.slice();
}
function setRegistries(list) {
const clean = Array.isArray(list) ? list.filter((r) => r && typeof r.url === "string" && /^https?:\/\//i.test(r.url)) : [];
storage.set("bcmr/registries", clean);
}
// Registry lookup: index-into-registry by category. BCMR v2 stores
// identities keyed by category id (hex). Each identity has a history
// array; the newest history[0] entry is the current snapshot.
function pickIdentity(regJson, categoryHex) {
const identities = regJson?.identities || {};
const identity = identities[categoryHex];
if (!identity) return null;
// History is a { <timestamp>: snapshot } map. Newest wins by ISO
// string sort — the schema recommends ISO 8601 timestamps and both
// registries above emit them, so lexicographic sort matches temporal
// sort for anything after 1000 AD.
const entries = Object.entries(identity);
if (!entries.length) return null;
entries.sort((a, b) => (b[0] > a[0] ? 1 : -1));
const [, snap] = entries[0];
return snap;
}
async function fetchRegistry(url) {
const r = await fetch(url, { cache: "no-store" });
if (!r.ok) throw new Error(`bcmr: HTTP ${r.status} from ${url}`);
return r.json();
}
// Attempt every configured registry in parallel; first identity found
// wins. When two registries carry a category, we prefer the one earlier
// in the list (user-configured order = priority).
async function lookup(categoryHex) {
const registries = registryList();
if (!registries.length) return null;
// Try cache first.
const cached = storage.get(`bcmr/${categoryHex}`, null);
if (cached && Date.now() - (cached.fetchedAt || 0) < REFRESH_MIN_MS) return cached;
const attempts = await Promise.all(registries.map(async (reg) => {
try {
const json = await fetchRegistry(reg.url);
const identity = pickIdentity(json, categoryHex);
return identity ? { identity, source: reg.label || reg.id, url: reg.url } : null;
} catch (e) {
log(`bcmr: registry "${reg.label || reg.url}" failed:`, e?.message || e);
return null;
}
}));
const hit = attempts.find((a) => a);
if (!hit) {
// Negative cache with a short TTL so a missing category doesn't
// hammer every registry on every wallet refresh.
const miss = { snapshot: null, fetchedAt: Date.now(), source: null, url: null };
storage.set(`bcmr/${categoryHex}`, miss);
return miss;
}
const entry = {
snapshot: hit.identity,
fetchedAt: Date.now(),
source: hit.source,
url: hit.url,
};
storage.set(`bcmr/${categoryHex}`, entry);
return entry;
}
// Batch lookup — returns { <categoryHex>: cacheEntry }. Reuses individual
// lookup() which handles per-category caching + negative caching.
async function lookupMany(categoryHexes) {
const out = {};
await Promise.all(categoryHexes.map(async (cat) => {
try { out[cat] = await lookup(cat); }
catch (e) { out[cat] = { snapshot: null, error: e?.message || String(e) }; }
}));
return out;
}
// Read-only cached lookup — never hits network. Used for the panel's
// synchronous render path so tokens draw immediately with whatever's
// in the cache; the async lookup() runs in the background afterwards.
function cached(categoryHex) {
return storage.get(`bcmr/${categoryHex}`, null);
}
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.
2026-09-29 00:13:30 +02:00
// ---- 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).
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.
2026-09-29 00:13:30 +02:00
//
// 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 {
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.
2026-09-29 00:13:30 +02:00
name: local?.name || s.name || t.name || null,
symbol: local?.symbol || s.token?.symbol || s.symbol || null,
description: s.description || null,
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.
2026-09-29 00:13:30 +02:00
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,
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.
2026-09-29 00:13:30 +02:00
source: local ? "local" : (entry.source || null),
local: !!local,
};
}
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.
2026-09-29 00:13:30 +02:00
return { lookup, lookupMany, cached, metadataOf, localName, setLocalName,
registryList, setRegistries, DEFAULT_REGISTRIES };
};