argus/src/wallet-app.js
Local Dev a45320db7d Initial commit — Silent Mode baseline (2026-07-29)
Snapshot of the decentralized-web stack at the point of the resolver+Theseus
rebuild deploy. Includes:

- Argus (BNS engine + resolver daemon + Sia gateway)
- AriadneResolver (Windows Inno installer bundle + Android APK sources +
  Firefox extension)
- TheseusNavigator (Electron browser)
- site/ (silentmode.st content, deployed to Sia at bns/silentmode/)
- design docs, roadmap, protocol spec

Secrets excluded via .gitignore: Argus/sia-s3.json, Argus/wallets.json,
Argus/ca/*.key,*.crt. Build outputs, node_modules, and bundled runtimes
also excluded.

Shipped hashes on dl.silentmode.st at this commit:
  AriadneResolver-Setup-0.1.0.exe    5bcb216eef31ea28ed767e4134ab74bd5ac69dfbd365fd249e9e6938e55c986a
  TheseusNavigator-Setup-0.0.1.exe   7c735e88bad2da3347145adba3016c8f626a18b8422289c8c6ba471972e2952b
  TheseusNavigator-0.0.1-portable.exe 008fd84445babeabb401b2bca40ea9466b24b0e6d6c85104da7640c5c5c84521
  ariadne-v0.2.apk                   635c8f04d44ef855a8390b9eeddb8cd2d50622e81cc4e5004e8daffc1bb0425c
2026-07-29 13:54:34 +02:00

165 lines
7 KiB
JavaScript

// BNS wallet: a wallet app with an embedded .bch resolver and browser pane.
// This is the "resolver inside the wallet" architecture: the wallet holds keys,
// resolves names straight from the BCH chain, and renders the result in-app.
import express from "express";
import { loadWallet } from "./lib/wallet.js";
import { buildIndex, resolveName, registerName, updateName, normalizeName } from "./lib/bns.js";
import { APP_PORT, TLD } from "./config.js";
const wallet = await loadWallet();
const app = express();
app.use(express.json());
// tiny index cache so browsing doesn't re-scan the beacon on every keystroke
let cache = { at: 0, index: new Map() };
async function index() {
if (Date.now() - cache.at > 15_000) {
cache = { at: Date.now(), index: await buildIndex(wallet.provider) };
}
return cache.index;
}
app.get("/api/status", async (_req, res) => {
res.json({
cashaddr: wallet.cashaddr,
tokenaddr: wallet.tokenaddr,
balanceSat: String(await wallet.getBalance("sat")),
names: [...(await index()).values()].map((e) => ({ ...e })),
});
});
app.get("/api/resolve/:name", async (req, res) => {
try {
const entry = (await index()).get(normalizeName(req.params.name));
if (!entry) return res.status(404).json({ error: "NXDOMAIN" });
res.json(entry);
} catch (e) {
res.status(400).json({ error: e.message });
}
});
app.post("/api/register", async (req, res) => {
try {
const { name, records } = req.body;
cache.at = 0;
res.json(await registerName(wallet, name, records));
} catch (e) {
res.status(400).json({ error: e.message });
}
});
app.post("/api/update", async (req, res) => {
try {
const { name, records } = req.body;
cache.at = 0;
res.json(await updateName(wallet, name, records));
} catch (e) {
res.status(400).json({ error: e.message });
}
});
// The "browser" endpoint: renders what a .bch name points to.
app.get("/view/:name", async (req, res) => {
let entry;
try {
entry = (await index()).get(normalizeName(req.params.name));
} catch (e) {
return res.status(400).send(`<pre>${e.message}</pre>`);
}
if (!entry) {
return res.status(404).send(
`<div style="font-family:system-ui;padding:2rem"><h2>NXDOMAIN</h2>
<p><code>${req.params.name}</code> is not registered on the chain.</p></div>`);
}
if (entry.records.h) return res.send(entry.records.h); // on-chain website
if (entry.records.u) return res.redirect(entry.records.u);
res.send(`<pre style="padding:2rem">${JSON.stringify(entry, null, 2)}</pre>`);
});
app.get("/", (_req, res) => {
res.send(UI);
});
const UI = /* html */ `<!doctype html>
<meta charset="utf8">
<title>BNS Wallet — .bch browser</title>
<style>
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
body { margin: 0; display: grid; grid-template-columns: 320px 1fr; height: 100vh; }
aside { border-right: 1px solid #8884; padding: 14px; overflow-y: auto; }
main { display: flex; flex-direction: column; }
h1 { font-size: 1.05rem; margin: 0 0 10px; }
h2 { font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; opacity: .6; margin: 18px 0 6px; }
code, .addr { font-family: ui-monospace, monospace; font-size: .72rem; word-break: break-all; }
.bar { display: flex; gap: 8px; padding: 10px; border-bottom: 1px solid #8884; }
.bar input { flex: 1; padding: 8px 12px; border-radius: 999px; border: 1px solid #8886; font-size: .95rem; }
button { padding: 7px 14px; border-radius: 8px; border: 1px solid #8886; cursor: pointer; background: #4b7bec; color: #fff; }
button.ghost { background: transparent; color: inherit; }
iframe { flex: 1; border: 0; width: 100%; }
.name { padding: 6px 8px; border: 1px solid #8884; border-radius: 8px; margin: 4px 0; cursor: pointer; }
.name:hover { background: #4b7bec22; }
input, textarea { width: 100%; box-sizing: border-box; margin: 3px 0; padding: 6px; border-radius: 6px; border: 1px solid #8886; background: transparent; color: inherit; }
#status { font-size: .8rem; opacity: .75; padding: 4px 12px; border-bottom: 1px solid #8884; min-height: 1.2em; }
</style>
<aside>
<h1>⛓️ BNS Wallet</h1>
<h2>Wallet</h2>
<div class="addr" id="addr">…</div>
<div>Balance: <b id="bal">…</b> sat</div>
<h2>Registered names (on-chain)</h2>
<div id="names">loading…</div>
<h2>Register a name</h2>
<input id="rname" placeholder="name (a-z, 0-9, -)">
<input id="rurl" placeholder="https:// URL (optional)">
<textarea id="rhtml" rows="3" placeholder="…or inline HTML stored on-chain"></textarea>
<button onclick="registerName()">Register on BCH</button>
</aside>
<main>
<div class="bar">
<button class="ghost" onclick="go()">⟳</button>
<input id="url" placeholder="name${TLD} — resolved from the BCH chain, not DNS" onkeydown="if(event.key==='Enter')go()">
<button onclick="go()">Go</button>
</div>
<div id="status"></div>
<iframe id="page" src="about:blank"></iframe>
</main>
<script>
const $ = (id) => document.getElementById(id);
async function refresh() {
const s = await (await fetch("/api/status")).json();
$("addr").textContent = s.cashaddr;
$("bal").textContent = s.balanceSat;
$("names").innerHTML = s.names.length
? s.names.map(n => \`<div class="name" onclick="open2('\${n.name}')"><b>\${n.name}${TLD}</b><br><code>\${JSON.stringify(n.records)}</code></div>\`).join("")
: "<i>none yet</i>";
}
function open2(name) { $("url").value = name + "${TLD}"; go(); }
async function go() {
const q = $("url").value.trim(); if (!q) return;
$("status").textContent = "resolving " + q + " via BCH chain…";
const r = await fetch("/api/resolve/" + encodeURIComponent(q));
if (!r.ok) { $("status").textContent = q + " → NXDOMAIN (not on chain)"; $("page").src = "/view/" + encodeURIComponent(q); return; }
const e = await r.json();
$("status").textContent = q + " → " + (e.records.u ?? (e.records.h ? "on-chain HTML, " + e.records.h.length + " bytes" : JSON.stringify(e.records)))
+ " [certificate category " + e.category.slice(0, 16) + "…, reg tx " + e.txid.slice(0, 16) + "…]";
$("page").src = "/view/" + encodeURIComponent(q);
}
async function registerName() {
const records = {};
if ($("rurl").value.trim()) records.u = $("rurl").value.trim();
if ($("rhtml").value.trim()) records.h = $("rhtml").value.trim();
if (!Object.keys(records).length) { alert("give a URL or inline HTML"); return; }
$("status").textContent = "broadcasting registration…";
const r = await fetch("/api/register", { method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ name: $("rname").value.trim(), records }) });
const j = await r.json();
$("status").textContent = r.ok ? "registered! txid " + j.txid : "error: " + j.error;
refresh();
}
refresh(); setInterval(refresh, 20000);
</script>`;
app.listen(APP_PORT, () => {
console.log(`BNS wallet+browser: http://localhost:${APP_PORT}`);
console.log(`wallet: ${wallet.cashaddr}`);
});