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
188 lines
9.1 KiB
JavaScript
188 lines
9.1 KiB
JavaScript
// LIVE chipnet check for the pay-by-QR fulfilment path.
|
|
//
|
|
// The claim under test is the one the whole trust story rests on: the registrar
|
|
// funds and signs the minting transaction, yet the certificate lands in a wallet
|
|
// the registrar has no key for, in that same transaction — so there is no moment
|
|
// at which the registrar could walk off with the name.
|
|
//
|
|
// The "buyer" here is a throwaway wallet that is never funded. That is the point:
|
|
// in this flow a buyer needs no coins in the destination wallet, no connector,
|
|
// and no CashTokens support beyond being able to receive.
|
|
//
|
|
// Run: node test/chipnet-paybyqr.mjs
|
|
|
|
import WebSocket from "ws";
|
|
import { BuiltInWallet } from "../src/lib/wallet-web.js";
|
|
import { connect, mintForBuyer, findCertificateUtxo, getBalance } from "../src/lib/registrar.js";
|
|
import { resolveName } from "../src/lib/resolver-web.js";
|
|
import { loadWallet } from "../src/lib/wallet.js";
|
|
import {
|
|
quotePrice, bip21, normaliseBuyerAddress, createOrder,
|
|
depositAddressAt, depositXpub, checkPayment,
|
|
} from "../src/lib/orders.js";
|
|
import { REGISTRAR } from "../src/registrar-config.js";
|
|
import { deriveHdPrivateNodeFromSeed, deriveSeedFromBip39Mnemonic, encodeHdPublicKey } from "@bitauth/libauth";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
const tag = Math.random().toString(16).slice(2, 8);
|
|
const NAME = `qrtest-${tag}.nav`;
|
|
|
|
let failures = 0;
|
|
const check = (ok, label, detail = "") => {
|
|
console.log(` ${ok ? "PASS" : "FAIL"} ${label}${detail ? " — " + detail : ""}`);
|
|
if (!ok) failures++;
|
|
};
|
|
const step = (n, s) => console.log(`\n─── ${n}. ${s} ${"─".repeat(Math.max(0, 56 - s.length))}`);
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
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 {
|
|
console.log(`Pay-by-QR fulfilment — live on ${REGISTRAR.network}`);
|
|
|
|
// ── 1. the buyer: a wallet with nothing in it ────────────────────────────
|
|
step(1, "A buyer who owns no coins and runs no connector");
|
|
const buyer = await BuiltInWallet.create();
|
|
const buyerBalance = await getBalance(client, buyer.watchedAddresses);
|
|
console.log(` buyer address : ${buyer.address}`);
|
|
check(buyerBalance.sats === 0n, "buyer wallet is empty — they only need to receive");
|
|
|
|
// ── 2. quote + deposit address + QR payload ──────────────────────────────
|
|
step(2, "Quote the name and hand out a payment address");
|
|
const priceSats = quotePrice({ serviceFeeSats: REGISTRAR.serviceFee.sats });
|
|
const store = JSON.parse(readFileSync(new URL("../wallets.json", import.meta.url), "utf8"));
|
|
const root = deriveHdPrivateNodeFromSeed(await deriveSeedFromBip39Mnemonic(store.main.seed));
|
|
const orderIndex = Math.floor(Math.random() * 100000);
|
|
const deposit = depositAddressAt(orderIndex, { hdNode: root });
|
|
|
|
// The public order service should never hold a private key — prove the same
|
|
// address comes out of the xpub alone.
|
|
const encoded = encodeHdPublicKey({ network: "testnet", node: depositXpub(root) });
|
|
const xpubString = typeof encoded === "string" ? encoded : encoded.hdPublicKey;
|
|
check(
|
|
depositAddressAt(orderIndex, { xpub: xpubString }) === deposit,
|
|
"deposit address derivable from the xpub alone (no key on the public server)",
|
|
);
|
|
|
|
const order = createOrder({
|
|
index: orderIndex, name: NAME, buyerAddress: buyer.address,
|
|
priceSats, depositAddress: deposit, now: Date.now(),
|
|
});
|
|
const uri = bip21({
|
|
address: deposit, satoshis: priceSats,
|
|
label: "Silent Mode", message: `Register ${order.displayName}`,
|
|
});
|
|
console.log(` name : ${order.displayName}`);
|
|
console.log(` price : ${priceSats} sat`);
|
|
console.log(` deposit addr : ${deposit}`);
|
|
console.log(` QR payload : ${uri}`);
|
|
check(uri.startsWith(deposit), "the QR encodes the order's own address");
|
|
|
|
const before = await checkPayment(client, order);
|
|
check(before.paid === false && before.receivedSats === 0n, "nothing received yet");
|
|
|
|
// ── 3. the buyer pays from some other wallet ─────────────────────────────
|
|
step(3, "Buyer pays from an unrelated wallet (stand-in: the project wallet)");
|
|
const payer = await loadWallet("main");
|
|
const payment = await payer.send([{ cashaddr: deposit, value: Number(priceSats), unit: "sat" }]);
|
|
console.log(` payment txid : ${payment.txId}`);
|
|
|
|
let paid = null;
|
|
for (let i = 0; i < 20; i++) {
|
|
const p = await checkPayment(client, order);
|
|
if (p.paid) { paid = p; break; }
|
|
process.stdout.write(` watching the deposit address… ${i + 1}/20\r`);
|
|
await sleep(3000);
|
|
}
|
|
console.log("");
|
|
check(!!paid, "payment detected on-chain", paid ? `${paid.receivedSats} sat` : "not seen");
|
|
check(paid?.shortfallSats === 0n, "paid in full");
|
|
|
|
// ── 4. fulfilment: registrar mints TO THE BUYER ──────────────────────────
|
|
step(4, "Registrar mints the certificate to the buyer's address");
|
|
// The registrar's minting wallet is its own hot wallet, separate from the
|
|
// deposit branch. Note it derives on BCH's coin type (m/44'/145'/…) while the
|
|
// legacy project wallet in wallets.json uses m/44'/0'/… — same seed, different
|
|
// addresses. Top it up from the project wallet if it has nothing to spend.
|
|
const registrar = await BuiltInWallet.fromMnemonic(store.main.seed);
|
|
let registrarBalance = await getBalance(client, registrar.watchedAddresses);
|
|
if (registrarBalance.sats < 20_000n) {
|
|
console.log(` registrar hot wallet is low (${registrarBalance.sats} sat) — funding it`);
|
|
const top = await payer.send([{ cashaddr: registrar.address, value: 100_000, unit: "sat" }]);
|
|
console.log(` funding txid : ${top.txId}`);
|
|
for (let i = 0; i < 20; i++) {
|
|
registrarBalance = await getBalance(client, registrar.watchedAddresses);
|
|
if (registrarBalance.sats >= 20_000n) break;
|
|
process.stdout.write(` waiting for registrar funds… ${i + 1}/20\r`);
|
|
await sleep(3000);
|
|
}
|
|
console.log("");
|
|
}
|
|
check(registrarBalance.sats >= 20_000n, "registrar can fund a mint", `${registrarBalance.sats} sat`);
|
|
const buyerDest = normaliseBuyerAddress(order.buyerAddress).tokenAddress;
|
|
const result = await mintForBuyer(client, {
|
|
registrarWallet: registrar,
|
|
name: order.name,
|
|
buyerTokenAddress: buyerDest,
|
|
records: { u: "https://silentmode.st" },
|
|
onProgress: (s, d) => console.log(` · ${s}${d ? " " + JSON.stringify(d) : ""}`),
|
|
});
|
|
console.log(` mint txid : ${result.txid}`);
|
|
console.log(` category : ${result.category}`);
|
|
|
|
// ── 5. who holds the name? ───────────────────────────────────────────────
|
|
step(5, "Verify the buyer holds it and the registrar does not");
|
|
let cert = null;
|
|
for (let i = 0; i < 20; i++) {
|
|
cert = await findCertificateUtxo(client, {
|
|
addresses: buyer.watchedAddresses, category: result.category, rawName: result.name,
|
|
});
|
|
if (cert) break;
|
|
process.stdout.write(` waiting for the certificate… ${i + 1}/20\r`);
|
|
await sleep(3000);
|
|
}
|
|
console.log("");
|
|
check(!!cert, "certificate sits at the BUYER's address", cert?.address);
|
|
|
|
const registrarHolds = await findCertificateUtxo(client, {
|
|
addresses: registrar.watchedAddresses, category: result.category, rawName: result.name,
|
|
});
|
|
check(registrarHolds === null, "registrar holds NO certificate for this name");
|
|
|
|
const buyerAfter = await getBalance(client, buyer.watchedAddresses);
|
|
check(
|
|
buyerAfter.certificates.length === 1,
|
|
"buyer's wallet shows exactly one certificate",
|
|
`${buyerAfter.certificates.length} found, ${buyerAfter.sats} spendable sat`,
|
|
);
|
|
|
|
// ── 6. it resolves ───────────────────────────────────────────────────────
|
|
step(6, "The name resolves from the chain");
|
|
let entry = null;
|
|
for (let i = 0; i < 20; i++) {
|
|
entry = await resolveName(result.displayName, { WebSocket });
|
|
if (entry) break;
|
|
process.stdout.write(` waiting for the resolver… ${i + 1}/20\r`);
|
|
await sleep(3000);
|
|
}
|
|
console.log("");
|
|
check(!!entry, `${result.displayName} resolves`, entry ? JSON.stringify(entry.records) : "");
|
|
check(entry?.category === result.category, "resolver reports the category we minted");
|
|
|
|
step("=", failures === 0 ? "RESULT: ALL CHECKS PASSED" : `RESULT: ${failures} CHECK(S) FAILED`);
|
|
console.log(` ${result.displayName} is owned by ${buyer.address}`);
|
|
console.log(` the buyer never connected a wallet, and never held a coin before paying`);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
|
|
process.exit(failures === 0 ? 0 : 1);
|