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("message", (data) => {
|
||||
el.buf += data.toString();
|
||||
// Newline-delimited messages first — Fulcrum's normal framing.
|
||||
let nl;
|
||||
while ((nl = el.buf.indexOf("\n")) >= 0) {
|
||||
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); }
|
||||
} 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;
|
||||
}
|
||||
|
||||
// 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 ---
|
||||
// GET /api/name/<name> -> { name, tld, registered, records? }
|
||||
if (req.url.startsWith("/api/name/")) {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export {
|
|||
InsufficientFundsError,
|
||||
} from "../lib/register-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 {
|
||||
describeGenesisRequirement,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue