sirius.x portal: /api/holdings endpoint + favicon paths + roadmap link
gateway/public-gateway.mjs:
* GET /api/holdings/<scripthash> and POST /api/holdings {scripthashes:[…]}
— server-side wallet-holdings lookup for the portal. Bns-indexer is
beacon-only so it can't answer listunspent for arbitrary addresses; the
gateway does the electrum roundtrip and cross-joins with the cached BCNR
index. Client posts scripthashes (no libauth needed on the server) and
gets back the names owned.
* elConnect now falls through to whole-buffer JSON.parse when a Fulcrum
response lacks a trailing newline — chipnet.bch.ninja does this and
otherwise every elCall to it times out. Same tolerance pattern as
bns-indexer.test.mjs's tiny electrum client.
* verifyElectrum picks a full electrum with a 3s server.version probe so a
hung server fails over to the next in seconds instead of stalling 20.
* Skips silentmode.st/electrum and coinspectrum.duckdns.org:50011 for
/api/holdings — both are bns-indexer routes that don't do listunspent.
web/register-entry.js: export addressToScripthash so the portal can derive
the scripthash for each of its wallet's watched addresses before POSTing.
Browser bundle rebuilt.
site-sirius-x pages:
* favicon + logo hrefs made relative (./assets/… on index/portal,
../assets/… on docs/theseus). Absolute /sirius-x/… broke on the sirius.x
BCNR route where the site is served from root instead of under /sirius-x/.
* Roadmap nav link added back to portal, docs, theseus — the landing has an
anchor to a section on itself, so subpages now link to it explicitly.
* portal.html: loadNames rewritten to POST scripthashes to /api/holdings
instead of doing client-side electrum. Works on browsers whose networks
block chipnet electrum ports; same-origin HTTPS on 443.
* portal.html: bundle import ?v= bumped so Chrome's in-memory ES module
map picks up the new bundle instead of a stale cached copy.
This commit is contained in:
parent
6539f84b24
commit
ff76e4cf94
2 changed files with 129 additions and 1 deletions
|
|
@ -65,6 +65,7 @@ function elConnect(url) {
|
||||||
ws.on("error", (e) => reject(e));
|
ws.on("error", (e) => reject(e));
|
||||||
ws.on("message", (data) => {
|
ws.on("message", (data) => {
|
||||||
el.buf += data.toString();
|
el.buf += data.toString();
|
||||||
|
// Newline-delimited messages first — Fulcrum's normal framing.
|
||||||
let nl;
|
let nl;
|
||||||
while ((nl = el.buf.indexOf("\n")) >= 0) {
|
while ((nl = el.buf.indexOf("\n")) >= 0) {
|
||||||
const line = el.buf.slice(0, nl).trim();
|
const line = el.buf.slice(0, nl).trim();
|
||||||
|
|
@ -76,6 +77,22 @@ function elConnect(url) {
|
||||||
if (p) { el.pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); }
|
if (p) { el.pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); }
|
||||||
} catch { /* ignore stray frame */ }
|
} catch { /* ignore stray frame */ }
|
||||||
}
|
}
|
||||||
|
// Some Fulcrum builds (and chipnet.bch.ninja specifically) omit the
|
||||||
|
// trailing newline. If what's left in the buffer parses as a complete
|
||||||
|
// JSON message, settle it. Same pattern as bns-indexer.test.mjs's
|
||||||
|
// "some servers omit the trailing newline" branch.
|
||||||
|
const rest = el.buf.trim();
|
||||||
|
if (rest) {
|
||||||
|
try {
|
||||||
|
const m = JSON.parse(rest);
|
||||||
|
const p = el.pending.get(m.id);
|
||||||
|
if (p) {
|
||||||
|
el.pending.delete(m.id);
|
||||||
|
el.buf = "";
|
||||||
|
m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result);
|
||||||
|
}
|
||||||
|
} catch { /* still partial — wait for more bytes */ }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -436,6 +453,117 @@ const server = http.createServer(async (req, res) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /api/holdings/<scripthash> -> { scripthash, count, names: [...] }
|
||||||
|
// POST /api/holdings body: {"scripthashes":[…]} -> { count, names: [...] }
|
||||||
|
//
|
||||||
|
// Server-side wallet-holdings lookup for the sirius.x portal (and any other
|
||||||
|
// BCNR-aware client). Bns-indexer only serves the beacon; arbitrary-address
|
||||||
|
// listunspent needs a full electrum, which many browsers cannot reach on
|
||||||
|
// ports 50004/50011/62002 (corporate firewalls, mobile carriers). The
|
||||||
|
// gateway CAN reach them, so it queries and returns the cross-joined
|
||||||
|
// result — client just POSTs a scripthash list and gets back the names.
|
||||||
|
//
|
||||||
|
// Takes scripthash (not cashaddr) to keep the gateway libauth-free; the
|
||||||
|
// client's wallet code already knows the scripthash for each of its
|
||||||
|
// watched addresses.
|
||||||
|
if (req.url === "/api/holdings" || req.url.startsWith("/api/holdings/") || req.url.startsWith("/api/holdings?")) {
|
||||||
|
res.setHeader("access-control-allow-origin", "*");
|
||||||
|
res.setHeader("content-type", "application/json");
|
||||||
|
// Collect scripthashes: URL path for GET, JSON body for POST.
|
||||||
|
let scripthashes = [];
|
||||||
|
if (req.method === "POST") {
|
||||||
|
try {
|
||||||
|
const chunks = [];
|
||||||
|
for await (const c of req) chunks.push(c);
|
||||||
|
const body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
||||||
|
scripthashes = Array.isArray(body.scripthashes) ? body.scripthashes : [];
|
||||||
|
} catch (e) { res.writeHead(400); res.end(JSON.stringify({ error: "invalid JSON body" })); return; }
|
||||||
|
} else {
|
||||||
|
const one = req.url.slice("/api/holdings/".length).split("?")[0];
|
||||||
|
if (one) scripthashes = [one];
|
||||||
|
}
|
||||||
|
scripthashes = scripthashes.filter((s) => typeof s === "string" && /^[0-9a-f]{64}$/i.test(s));
|
||||||
|
if (!scripthashes.length) {
|
||||||
|
res.writeHead(400);
|
||||||
|
res.end(JSON.stringify({ error: "no valid scripthashes (need 64-char hex, POST as {scripthashes:[…]} or GET /api/holdings/<hex>)" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scripthashes.length > 64) {
|
||||||
|
res.writeHead(400); res.end(JSON.stringify({ error: "at most 64 scripthashes per request" })); return;
|
||||||
|
}
|
||||||
|
// Connect to any full electrum. bns-indexer only supports get_history +
|
||||||
|
// transaction.get — no listunspent for arbitrary scripthashes. Skip
|
||||||
|
// every URL that resolves to our own bns-indexer (silentmode.st/electrum
|
||||||
|
// on 443, coinspectrum.duckdns.org:50011 on the direct port).
|
||||||
|
const BNS_INDEXER_URLS = /silentmode\.st\/electrum|coinspectrum\.duckdns\.org:50011/;
|
||||||
|
const candidates = CHIPNET_ELECTRUM.map((s) => s.url).filter((u) => !BNS_INDEXER_URLS.test(u));
|
||||||
|
// Connect + verify with a short timeout, so a hung server (an open TCP
|
||||||
|
// that never speaks JSON-RPC back) fails over to the next candidate in
|
||||||
|
// seconds instead of stalling the request for 20.
|
||||||
|
const verifyElectrum = async (url) => {
|
||||||
|
const c = await elConnect(url);
|
||||||
|
const versionPromise = new Promise((resolve, reject) => {
|
||||||
|
const id = ++c.id;
|
||||||
|
c.pending.set(id, { resolve, reject });
|
||||||
|
c.ws.send(JSON.stringify({ id, method: "server.version", params: ["bns-gateway", "1.4.3"] }) + "\n");
|
||||||
|
setTimeout(() => {
|
||||||
|
if (c.pending.has(id)) { c.pending.delete(id); reject(new Error("server.version timeout")); }
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
try { await versionPromise; return c; }
|
||||||
|
catch (e) { try { c.ws.close(); } catch {} throw e; }
|
||||||
|
};
|
||||||
|
let el, lastErr, elUrl;
|
||||||
|
for (const url of candidates) {
|
||||||
|
try { el = await verifyElectrum(url); elUrl = url; console.log("api/holdings: verified", url); break; }
|
||||||
|
catch (e) { lastErr = e; console.log("api/holdings: skip", url, "-", e.message); }
|
||||||
|
}
|
||||||
|
if (!el) { res.writeHead(503); res.end(JSON.stringify({ error: "no full electrum reachable", detail: String(lastErr?.message || lastErr) })); return; }
|
||||||
|
try {
|
||||||
|
// Fan out listunspent in parallel. Chipnet is small; 64 concurrent
|
||||||
|
// listunspent calls on one WS is well within a public server's budget.
|
||||||
|
const utxoLists = await Promise.all(scripthashes.map((sh) =>
|
||||||
|
elCall(el, "blockchain.scripthash.listunspent", [sh, "include_tokens"]).catch(() => [])
|
||||||
|
));
|
||||||
|
// Collect NFT categories held by any of the scripthashes.
|
||||||
|
const categories = new Set();
|
||||||
|
for (const list of utxoLists) {
|
||||||
|
for (const u of list) if (u.token_data?.nft) categories.add(u.token_data.category);
|
||||||
|
}
|
||||||
|
// Cross-reference with the cached BCNR index.
|
||||||
|
const idx = await getIndex();
|
||||||
|
const names = [];
|
||||||
|
for (const entry of idx.values()) {
|
||||||
|
if (categories.has(entry.category)) {
|
||||||
|
const dot = entry.name.indexOf(".");
|
||||||
|
const displayName = dot < 0 ? entry.name + ".bch" : entry.name;
|
||||||
|
const tld = dot < 0 ? "bch" : entry.name.slice(dot + 1);
|
||||||
|
names.push({
|
||||||
|
name: displayName,
|
||||||
|
tld,
|
||||||
|
category: entry.category,
|
||||||
|
records: entry.records ?? null,
|
||||||
|
txid: entry.txid ?? null,
|
||||||
|
height: entry.height ?? null,
|
||||||
|
updated_txid: entry.updatedTxid ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
const body = req.method === "POST"
|
||||||
|
? { count: names.length, scripthashes_queried: scripthashes.length, names }
|
||||||
|
: { scripthash: scripthashes[0], count: names.length, names };
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify(body, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify({ error: String(e.message || e) }));
|
||||||
|
} finally {
|
||||||
|
try { el.ws.close(); } catch {}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// --- registrar API: availability of a name ---
|
// --- registrar API: availability of a name ---
|
||||||
// GET /api/name/<name> -> { name, tld, registered, records? }
|
// GET /api/name/<name> -> { name, tld, registered, records? }
|
||||||
if (req.url.startsWith("/api/name/")) {
|
if (req.url.startsWith("/api/name/")) {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export {
|
||||||
InsufficientFundsError,
|
InsufficientFundsError,
|
||||||
} from "../lib/register-tx.js";
|
} from "../lib/register-tx.js";
|
||||||
export { signTransaction, txidOf, txidOfHex } from "../lib/sign-tx.js";
|
export { signTransaction, txidOf, txidOfHex } from "../lib/sign-tx.js";
|
||||||
export { toTokenAddress, addressToLockingBytecode, getUtxos, getUtxosForAddresses, broadcast } from "../lib/chain.js";
|
export { toTokenAddress, addressToLockingBytecode, addressToScripthash, getUtxos, getUtxosForAddresses, broadcast } from "../lib/chain.js";
|
||||||
export { resolveName, buildIndex } from "../lib/resolver-web.js";
|
export { resolveName, buildIndex } from "../lib/resolver-web.js";
|
||||||
export {
|
export {
|
||||||
describeGenesisRequirement,
|
describeGenesisRequirement,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue