199 lines
9.6 KiB
JavaScript
199 lines
9.6 KiB
JavaScript
|
|
// 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);
|