argus/test/portfolio-index.test.mjs
Local Dev f9a5eb367f Theseus navigator UI overhaul, BNS-only indexer, site restructure
Theseus Navigator:
- Firefox-style toolbar: SVG back/fwd/reload/home buttons
- security padlock badge in the address bar opens a floating site-info
  overlay VIEW (on top of the page, never pushes content down)
- Edge-style favorites bar (shown only on the new-tab/home page)
- search-engine picker (DuckDuckGo/Google/Bing/Brave/Startpage)
- Theseus logo moved right, opens Settings; native menu removed
- address bar doubles as search; bottom resolver status line

Argus:
- BNS-only indexer: an electrum endpoint over a bare BCHN node
  (watches just the beacon, caches txs so no txindex needed) + test
- registry lifecycle / portfolio / name-index modules

Site restructure (nav/apps/store/hermes/choose) + runbook notes.
2026-07-30 08:16:55 +02:00

292 lines
11 KiB
JavaScript

// The two integrity properties: search reveals nothing, and holdings cannot hide.
//
// Offline — fake clients only. Both properties are about what the software makes
// impossible, so they have to be provable without a network.
import test from "node:test";
import assert from "node:assert/strict";
import { NameIndex, buildFromCache } from "../src/lib/name-index.js";
import { verifyPortfolio, renderPortfolio, heldNames, PORTFOLIO_STATUS } from "../src/lib/portfolio.js";
import { addressToLockingBytecode } from "../src/lib/chain.js";
const OPERATOR = "bchtest:qzew3dqcsj0guwe5y3uxf9q2xs5ajgy9lca4z093cu";
const OTHER = "bchtest:qqmcn373plhjrvs5mrswuvevdvepxum8x58jhawqxs";
const utf8hex = (s) => [...new TextEncoder().encode(s)].map((b) => b.toString(16).padStart(2, "0")).join("");
/** A REG transaction the resolver will accept. */
function regTx(txid, name, category) {
const payload = "BNS1" + JSON.stringify({ o: "REG", n: name, r: {} });
const hex = [...new TextEncoder().encode(payload)].map((b) => b.toString(16).padStart(2, "0")).join("");
const len = (hex.length / 2).toString(16).padStart(2, "0");
return {
txid,
vin: [],
vout: [
{ n: 0, scriptPubKey: { hex: `6a4c${len}${hex}` } },
{ n: 1, scriptPubKey: { hex: "76a914" + "11".repeat(20) + "88ac" },
tokenData: { category, amount: "0", nft: { capability: "none", commitment: utf8hex(name) } } },
],
};
}
/** Electrum stand-in: scripted history, transactions and UTXOs. */
function fakeClient({ history = [], txs = {}, utxos = {} } = {}) {
const calls = [];
return {
calls,
async call(method, params) {
calls.push({ method, params });
if (method === "blockchain.scripthash.get_history") return history;
if (method === "blockchain.transaction.get") {
const tx = txs[params[0]];
if (!tx) throw new Error("unknown tx");
return tx;
}
if (method === "blockchain.scripthash.listunspent") {
// Map scripthash back to whichever address we were given UTXOs for.
for (const [addr, list] of Object.entries(utxos)) {
const sh = shOf(addr);
if (sh === params[0]) return list;
}
return [];
}
if (method === "server.version") return ["fake", "1.4.2"];
return null;
},
close() {},
};
}
// scripthash helper mirroring chain.js
let _sha;
function shOf(address) {
if (!_sha) {
// lazily to keep the import list short
_sha = require("@bitauth/libauth");
}
const bytes = addressToLockingBytecode(address);
return _sha.binToHex(_sha.sha256.hash(bytes).slice().reverse());
}
const CAT_A = "aa".repeat(32);
const CAT_B = "bb".repeat(32);
const TX_A = "1a".repeat(32);
const TX_B = "1b".repeat(32);
// ---------- search reveals nothing ----------
test("after one sync, lookups touch the network zero times", async () => {
const client = fakeClient({
history: [{ tx_hash: TX_A, height: 100 }],
txs: { [TX_A]: regTx(TX_A, "alice", CAT_A) },
});
const idx = new NameIndex();
await idx.sync(client);
const afterSync = client.calls.length;
// The property: searching cannot be observed, because there is nothing to observe.
for (const n of ["alice.bch", "bob.bch", "verysecretplan.bch", "acquisition-target.bch"]) {
idx.lookup(n);
}
assert.equal(client.calls.length, afterSync, "a lookup must not produce a single request");
assert.equal(idx.lookup("alice.bch")?.name, "alice.bch");
assert.equal(idx.lookup("bob.bch"), null);
});
test("lookup before sync fails loudly rather than silently answering wrong", async () => {
const idx = new NameIndex();
assert.throws(() => idx.lookup("alice.bch"), /call sync\(\) before lookup\(\)/);
});
test("a second sync fetches only what is new", async () => {
const txs = { [TX_A]: regTx(TX_A, "alice", CAT_A) };
const client = fakeClient({ history: [{ tx_hash: TX_A, height: 100 }], txs });
const idx = new NameIndex();
const first = await idx.sync(client);
assert.equal(first.fetched, 1);
// A new registration appears.
txs[TX_B] = regTx(TX_B, "bob", CAT_B);
const client2 = fakeClient({
history: [{ tx_hash: TX_A, height: 100 }, { tx_hash: TX_B, height: 101 }],
txs,
});
const second = await idx.sync(client2);
assert.equal(second.fetched, 1, "only the unseen transaction is fetched");
assert.equal(second.cached, 1, "the already-known one is reused");
assert.equal(idx.lookup("bob.bch")?.name, "bob.bch");
assert.equal(idx.lookup("alice.bch")?.name, "alice.bch", "the old entry survives");
});
test("the cache survives a reload through storage", async () => {
const store = new Map();
const storage = {
getItem: (k) => store.get(k) ?? null,
setItem: (k, v) => store.set(k, v),
removeItem: (k) => store.delete(k),
};
const client = fakeClient({
history: [{ tx_hash: TX_A, height: 100 }],
txs: { [TX_A]: regTx(TX_A, "alice", CAT_A) },
});
const a = new NameIndex({ storage });
await a.sync(client);
const b = new NameIndex({ storage });
assert.equal(b.load(), true, "a fresh instance picks up the cache");
const fresh = fakeClient({
history: [{ tx_hash: TX_A, height: 100 }],
txs: { [TX_A]: regTx(TX_A, "alice", CAT_A) },
});
const r = await b.sync(fresh);
assert.equal(r.fetched, 0, "nothing refetched after a reload");
assert.equal(b.lookup("alice.bch")?.name, "alice.bch");
});
test("a shrinking history drops the cache instead of merging disagreeing views", async () => {
const idx = new NameIndex();
await idx.sync(fakeClient({
history: [{ tx_hash: TX_A, height: 100 }, { tx_hash: TX_B, height: 101 }],
txs: { [TX_A]: regTx(TX_A, "alice", CAT_A), [TX_B]: regTx(TX_B, "bob", CAT_B) },
}));
assert.equal(idx.size, 2);
// A reorg, or a server with less data. Refetch rather than trust a merge.
const shrunk = fakeClient({
history: [{ tx_hash: TX_A, height: 100 }],
txs: { [TX_A]: regTx(TX_A, "alice", CAT_A) },
});
const r = await idx.sync(shrunk);
assert.equal(r.fetched, 1, "everything is refetched");
assert.equal(idx.size, 1);
});
test("a pool's union history is preferred over a single server's view", async () => {
// The pool defends against a server hiding a registration; the index must use
// it when available rather than silently taking one server's word.
let unionUsed = false;
const pool = {
async unionHistory() { unionUsed = true; return { history: [{ tx_hash: TX_A, height: 100 }] }; },
async call(method, params) {
if (method === "blockchain.transaction.get") return regTx(TX_A, "alice", CAT_A);
return null;
},
};
const idx = new NameIndex();
await idx.sync(pool);
assert.equal(unionUsed, true, "unionHistory must be used when the client offers it");
assert.equal(idx.lookup("alice.bch")?.name, "alice.bch");
});
test("index rules are not reimplemented — buildFromCache defers to the resolver", async () => {
const history = [{ tx_hash: TX_A, height: 100 }];
const txs = new Map([[TX_A, regTx(TX_A, "alice", CAT_A)]]);
const built = await buildFromCache(history, txs);
assert.ok(built instanceof Map);
assert.equal(built.get("alice.bch")?.category, CAT_A);
});
// ---------- holdings cannot hide ----------
const tokenUtxo = (txid, category, name) => ({
tx_hash: txid, tx_pos: 1, value: 1000, height: 100,
token_data: { category, amount: "0", nft: { capability: "none", commitment: utf8hex(name) } },
});
function portfolioClient({ regs, utxosByAddress }) {
const history = regs.map((r) => ({ tx_hash: r.txid, height: r.height ?? 100 }));
const txs = Object.fromEntries(regs.map((r) => [r.txid, regTx(r.txid, r.name, r.category)]));
return fakeClient({ history, txs, utxos: utxosByAddress });
}
test("declared and held reconciles cleanly", async () => {
const client = portfolioClient({
regs: [{ txid: TX_A, name: "alice", category: CAT_A }],
utxosByAddress: { [OPERATOR]: [tokenUtxo(TX_A, CAT_A, "alice")] },
});
const report = await verifyPortfolio(client, {
addresses: [OPERATOR],
manifest: [{ name: "alice.bch", acquiredAt: "2026-07-01", askingSats: 5_000_000n }],
});
assert.equal(report.complete, true);
assert.equal(report.accurate, true);
assert.equal(report.entries[0].status, PORTFOLIO_STATUS.ok);
assert.equal(report.entries[0].forSale, true);
});
test("a name held but NOT declared is reported — the disqualifying case", async () => {
const client = portfolioClient({
regs: [
{ txid: TX_A, name: "alice", category: CAT_A },
{ txid: TX_B, name: "bob", category: CAT_B },
],
utxosByAddress: {
[OPERATOR]: [tokenUtxo(TX_A, CAT_A, "alice"), tokenUtxo(TX_B, CAT_B, "bob")],
},
});
const report = await verifyPortfolio(client, {
addresses: [OPERATOR],
manifest: [{ name: "alice.bch" }], // bob quietly omitted
});
assert.equal(report.complete, false, "an undeclared holding must not pass");
assert.equal(report.undisclosed.length, 1);
assert.equal(report.undisclosed[0].name, "bob.bch");
assert.equal(report.undisclosed[0].status, PORTFOLIO_STATUS.undisclosed);
});
test("publishing is refused while a holding is undeclared", async () => {
const client = portfolioClient({
regs: [
{ txid: TX_A, name: "alice", category: CAT_A },
{ txid: TX_B, name: "bob", category: CAT_B },
],
utxosByAddress: {
[OPERATOR]: [tokenUtxo(TX_A, CAT_A, "alice"), tokenUtxo(TX_B, CAT_B, "bob")],
},
});
const report = await verifyPortfolio(client, { addresses: [OPERATOR], manifest: [{ name: "alice.bch" }] });
assert.throws(
() => renderPortfolio(report),
(e) => e.code === "UNDISCLOSED_HOLDINGS" && /bob\.bch/.test(e.message),
"a page that silently omits a holding is worse than no page",
);
});
test("declared but not held is flagged separately from undisclosed", async () => {
const client = portfolioClient({
regs: [{ txid: TX_A, name: "alice", category: CAT_A }],
utxosByAddress: { [OPERATOR]: [] }, // sold, or never held
});
const report = await verifyPortfolio(client, { addresses: [OPERATOR], manifest: [{ name: "alice.bch" }] });
assert.equal(report.accurate, false, "a stale manifest entry is not accurate");
assert.equal(report.complete, true, "but nothing is hidden, which is the stronger property");
assert.equal(report.entries[0].status, PORTFOLIO_STATUS.missing);
// Stale is embarrassing, not disqualifying — publishing is still allowed.
assert.doesNotThrow(() => renderPortfolio(report));
});
test("someone else's names are not counted as ours", async () => {
const client = portfolioClient({
regs: [{ txid: TX_B, name: "bob", category: CAT_B }],
utxosByAddress: { [OTHER]: [tokenUtxo(TX_B, CAT_B, "bob")] },
});
const held = await heldNames(client, [OPERATOR]);
assert.deepEqual(held, [], "only the addresses we were asked about count");
});
test("the published form carries the txid so a reader can check it themselves", async () => {
const client = portfolioClient({
regs: [{ txid: TX_A, name: "alice", category: CAT_A }],
utxosByAddress: { [OPERATOR]: [tokenUtxo(TX_A, CAT_A, "alice")] },
});
const report = await verifyPortfolio(client, {
addresses: [OPERATOR],
manifest: [{ name: "alice.bch", acquiredAt: "2026-07-01", paidSats: 13_000n }],
});
const page = renderPortfolio(report, { network: "chipnet" });
assert.equal(page.complete, true);
assert.equal(page.names[0].registeredTxid, TX_A, "verifiable, not merely asserted");
assert.equal(page.names[0].paidSats, "13000");
assert.equal(page.network, "chipnet");
});