argus/test/chipnet-acceptance.mjs

199 lines
9.6 KiB
JavaScript
Raw Normal View History

// PHASE 1 ACCEPTANCE TEST — live, on chipnet, with real (worthless) coins.
//
// Proves the whole newcomer path end to end:
// fresh browser wallet -> funded -> registers <name>.bch -> registers
// <name>.p2p -> both certificates are held by the BUYER's key -> both names
// resolve from the chain -> records can be updated by the owner.
//
// The funding step (and only that step) uses the project's chipnet wallet via
// mainnet-js. That is deliberate: mainnet-js is an entirely independent
// implementation, so if our hand-built transactions were malformed the chain
// would reject them while mainnet-js's succeeded. Everything after funding is
// built, signed and broadcast by the code that ships to buyers.
//
// Run: node test/chipnet-acceptance.mjs
// Add --keep to print the throwaway wallet's phrase for reuse.
import WebSocket from "ws";
import { BuiltInWallet } from "../src/lib/wallet-web.js";
import {
connect,
checkAvailability,
registerWithBuiltInWallet,
setRecordsWithBuiltInWallet,
getBalance,
findCertificateUtxo,
} from "../src/lib/registrar.js";
import { REGISTRAR, priceSummary } from "../src/registrar-config.js";
import { resolveName } from "../src/lib/resolver-web.js";
import { loadWallet } from "../src/lib/wallet.js";
const FUND_SATS = 120_000;
const tag = Math.random().toString(16).slice(2, 8);
const NAME_BCH = `smtest-${tag}`;
const NAME_P2P = `smtest-${tag}.p2p`;
const log = (...a) => console.log(...a);
const step = (n, s) => log(`\n─── ${n}. ${s} ${"─".repeat(Math.max(0, 58 - s.length))}`);
const sats = (v) => `${v} sat (${(Number(v) / 1e8).toFixed(8)} tBCH)`;
let failures = 0;
function check(ok, label, detail = "") {
log(` ${ok ? "PASS" : "FAIL"} ${label}${detail ? " — " + detail : ""}`);
if (!ok) failures++;
return ok;
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function waitFor(label, fn, { tries = 20, delay = 3000 } = {}) {
for (let i = 0; i < tries; i++) {
const v = await fn();
if (v) return v;
process.stdout.write(` waiting for ${label}… ${i + 1}/${tries}\r`);
await sleep(delay);
}
return null;
}
let client;
try {
client = await connect({ WebSocket });
} catch (e) {
console.error(`
cannot reach the chain — the test cannot run.
${e.message}
`);
process.exit(2); // 2 = environment, not a product failure
}
try {
log(`BNS registration — Phase 1 acceptance test on ${REGISTRAR.network}`);
log(`Service fee configured: ${sats(REGISTRAR.serviceFee.sats)} -> ${REGISTRAR.serviceFee.address}`);
// ── 1. a brand-new buyer, exactly as a newcomer would get ────────────────
step(1, "Create a fresh in-browser wallet (the buyer)");
const buyer = await BuiltInWallet.create();
log(` address : ${buyer.address}`);
log(` token address : ${buyer.tokenAddress}`);
if (process.argv.includes("--keep")) log(` phrase : ${buyer.mnemonic}`);
check((await getBalance(client, buyer.watchedAddresses)).sats === 0n, "new wallet starts empty");
// ── 2. fund it (stands in for the buyer funding from an exchange/faucet) ──
step(2, "Fund the buyer from the project chipnet wallet");
const funder = await loadWallet("main");
const fundResult = await funder.send([{ cashaddr: buyer.address, value: FUND_SATS, unit: "sat" }]);
log(` funding txid : ${fundResult.txId}`);
const funded = await waitFor("the coins to arrive", async () => {
const b = await getBalance(client, buyer.watchedAddresses);
return b.sats > 0n ? b : null;
});
log("");
check(!!funded && funded.sats === BigInt(FUND_SATS), "buyer received the funds", funded ? sats(funded.sats) : "none");
log(` wallet holds a vout-0 coin already: ${funded?.hasGenesisInput}`
+ (funded?.hasGenesisInput ? "" : " -> the prepare-wallet step will run"));
// ── 3. availability + quote ──────────────────────────────────────────────
step(3, `Check availability of ${NAME_BCH}.bch`);
const avail = await checkAvailability(client, NAME_BCH);
check(avail.available, `${avail.name} is available`);
check(avail.recordBudgetBytes > 50, "records have room in the payload", `${avail.recordBudgetBytes} bytes free`);
// ── 4. register a .bch name ──────────────────────────────────────────────
step(4, `Register ${NAME_BCH}.bch (legacy bare-label form)`);
const reg1 = await registerWithBuiltInWallet(client, {
wallet: buyer,
name: NAME_BCH,
records: { u: "https://silentmode.st" },
onProgress: (s, d) => log(` · ${s}${d ? " " + JSON.stringify(d) : ""}`),
});
log(` txid : ${reg1.txid}`);
log(` category : ${reg1.category}`);
for (const line of priceSummary(reg1.costs)) {
log(` ${line.total ? "TOTAL" : " "} ${line.label.padEnd(26)} ${String(line.sats).padStart(8)} sat${line.note ? " (" + line.note + ")" : ""}`);
}
check(reg1.category === reg1.hex.slice(0, 0) + reg1.category, "category recorded");
check(!!reg1.prepTxid === !funded?.hasGenesisInput, "prepare-wallet step ran only when needed");
// ── 5. register a multi-TLD name — no prep needed this time ──────────────
step(5, `Register ${NAME_P2P} (multi-TLD, straight after — no preparation)`);
const reg2 = await registerWithBuiltInWallet(client, {
wallet: buyer,
name: NAME_P2P,
records: { ip: "203.0.113.9" },
onProgress: (s, d) => log(` · ${s}${d ? " " + JSON.stringify(d) : ""}`),
});
log(` txid : ${reg2.txid}`);
check(
reg2.prepTxid === null,
"second registration needed NO preparation tx (change-at-vout-0 works)",
);
check(reg2.category !== reg1.category, "each certificate has its own category");
// ── 6. self-custody: the buyer holds both certificates ───────────────────
step(6, "Verify self-custody — the certificates are the buyer's, not ours");
const cert1 = await waitFor("certificate 1", () =>
findCertificateUtxo(client, { addresses: buyer.watchedAddresses, category: reg1.category, rawName: reg1.name }));
const cert2 = await waitFor("certificate 2", () =>
findCertificateUtxo(client, { addresses: buyer.watchedAddresses, category: reg2.category, rawName: reg2.name }));
log("");
check(!!cert1, `${reg1.displayName} certificate sits at the buyer's address`, cert1?.address);
check(!!cert2, `${reg2.displayName} certificate sits at the buyer's address`, cert2?.address);
const projectHolds = await findCertificateUtxo(client, {
addresses: [REGISTRAR.serviceFee.address],
category: reg1.category,
rawName: reg1.name,
});
check(projectHolds === null, "the registrar holds NO claim on the name");
// ── 7. the names actually resolve from the chain ─────────────────────────
step(7, "Resolve both names using the shipped zero-dependency resolver");
const r1 = await waitFor(`${reg1.displayName} to resolve`, () =>
resolveName(reg1.displayName, { WebSocket }));
const r2 = await waitFor(`${reg2.displayName} to resolve`, () =>
resolveName(reg2.displayName, { WebSocket }));
log("");
check(!!r1, `${reg1.displayName} resolves`, r1 ? JSON.stringify(r1.records) : "");
check(r1?.records?.u === "https://silentmode.st", "its record survived the round trip");
check(!!r2, `${reg2.displayName} resolves`, r2 ? JSON.stringify(r2.records) : "");
check(r2?.records?.ip === "203.0.113.9", "its record survived the round trip");
check(r1?.category === reg1.category, "resolver reports the category we minted");
// ── 8. the owner — and only the owner — can repoint the name ─────────────
step(8, `Set a hosting record on ${reg2.displayName} (Phase 4 update tx)`);
const upd = await setRecordsWithBuiltInWallet(client, {
wallet: buyer,
name: reg2.displayName,
records: { h: "<h1>hello from the chain</h1>" },
onProgress: (s, d) => log(` · ${s}${d ? " " + JSON.stringify(d) : ""}`),
});
log(` txid : ${upd.txid}`);
const r2b = await waitFor("the update to take effect", async () => {
const e = await resolveName(reg2.displayName, { WebSocket });
return e?.records?.h ? e : null;
});
log("");
check(!!r2b, "the update is live on the chain", r2b ? JSON.stringify(r2b.records) : "");
check(r2b?.category === reg2.category, "the name kept its identity across the update");
// ── 9. the storefront's own availability API agrees ──────────────────────
step(9, "Cross-check against the public availability API (navigate.st)");
try {
const api = await fetch(`https://navigate.st/api/name/${encodeURIComponent(reg1.displayName)}`)
.then((r) => r.json());
check(api.registered === true, "navigate.st reports the name as registered", JSON.stringify(api.records ?? {}));
} catch (e) {
log(` SKIP availability API unreachable (${e.message})`);
}
step("=", `RESULT: ${failures === 0 ? "ALL CHECKS PASSED" : failures + " CHECK(S) FAILED"}`);
log(` names registered: ${reg1.displayName}, ${reg2.displayName}`);
log(` owner address : ${buyer.address}`);
const end = await getBalance(client, buyer.watchedAddresses);
log(` buyer left with : ${sats(end.sats)} and ${end.certificates.length} certificate(s)`);
} finally {
client.close();
}
process.exit(failures === 0 ? 0 : 1);