// PHASE 1 ACCEPTANCE TEST — live, on chipnet, with real (worthless) coins. // // Proves the whole newcomer path end to end: // fresh browser wallet -> funded -> registers .bch -> registers // .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: "

hello from the chain

" }, 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);