// LIVE chipnet test: pay from an exchange, fail to sign, get refunded. // // THE SCENARIO // 1. The buyer starts a purchase. We generate their destination wallet in the // browser there and then, so the destination is always a key they hold. // 2. They pay from somewhere else entirely — an exchange withdrawal, which // cannot sign for anything and cannot receive a CashToken. // 3. Before minting, they must sign a challenge with the destination key, // proving they still have the wallet we gave them. // 4. They never sign — the tab was closed, the phrase was never written down. // 5. The quote expires and the escrow refunds the payment. // // WHAT THIS MUST PROVE // - `magicworld.x` is NEVER minted. An irrevocable certificate locked to a key // nobody holds is the worst outcome in the whole product, worse than a failed // sale, and it is silent. // - The money goes back to whoever actually paid, recovered from the payment // transaction rather than from anything the buyer typed. // - A signature from the wrong key does not open the gate. // // Run: node test/chipnet-escrow-refund.mjs import WebSocket from "ws"; import { readFileSync } from "node:fs"; import { deriveHdPrivateNodeFromSeed, deriveSeedFromBip39Mnemonic } from "@bitauth/libauth"; import { BuiltInWallet } from "../src/lib/wallet-web.js"; import { connect, checkAvailability, getBalance } from "../src/lib/registrar.js"; import { loadWallet } from "../src/lib/wallet.js"; import { payerAddressOf, addressToLockingBytecode, broadcast, getUtxos } from "../src/lib/chain.js"; import { signTransaction } from "../src/lib/sign-tx.js"; import { controlChallenge, verifyControl, signMessageBip137 } from "../src/lib/sign-message.js"; import { quotePrice, bip21, createOrder, depositAddressAt, depositKeyAt, checkPayment, buildRefundTx, ORDER_STATUS, } from "../src/lib/orders.js"; import { REGISTRAR } from "../src/registrar-config.js"; const NAME = "magicworld.x"; 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, 54 - s.length))}`); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); let client; try { client = await connect({ WebSocket }); } catch (e) { console.error(`\ncannot reach the chain — the test cannot run.\n${e.message}\n`); process.exit(2); } try { console.log(`Escrow refund on failure-to-sign — live on ${REGISTRAR.network}`); // ── 1. destination wallet, created for the buyer during checkout ───────── step(1, "Generate the buyer's destination wallet in-browser"); const buyer = await BuiltInWallet.create(); console.log(` destination : ${buyer.address}`); check( (await getBalance(client, buyer.watchedAddresses)).sats === 0n, "brand-new wallet, usable immediately — the buyer could fund it right now", ); const avail = await checkAvailability(client, NAME); check(avail.available, `${NAME} is available before we start`); if (!avail.available) throw new Error(`${NAME} is already registered — pick another name for this test`); // ── 2. quote ───────────────────────────────────────────────────────────── step(2, "Quote the name and open the escrow"); 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() * 1_000_000); const deposit = depositAddressAt(orderIndex, { hdNode: root }); const priceSats = quotePrice({ serviceFeeSats: REGISTRAR.serviceFee.sats }); const order = createOrder({ index: orderIndex, name: NAME, buyerAddress: buyer.address, priceSats, depositAddress: deposit, now: Date.now(), }); console.log(` escrow addr : ${deposit}`); console.log(` price : ${priceSats} sat`); console.log(` QR : ${bip21({ address: deposit, satoshis: priceSats, message: `Register ${NAME}` })}`); check(order.status === ORDER_STATUS.quoted, "order starts quoted"); // ── 3. payment from an "exchange" ──────────────────────────────────────── step(3, "Buyer pays from an exchange (cannot sign, cannot hold tokens)"); const exchange = await loadWallet("main"); const payment = await exchange.send([{ cashaddr: deposit, value: Number(priceSats), unit: "sat" }]); console.log(` payment txid : ${payment.txId}`); let paid = null; for (let i = 0; i < 25; i++) { const p = await checkPayment(client, order); if (p.paid) { paid = p; break; } process.stdout.write(` watching escrow… ${i + 1}/25\r`); await sleep(3000); } console.log(""); check(!!paid, "escrow received the payment", paid ? `${paid.receivedSats} sat` : "nothing"); order.status = ORDER_STATUS.awaitingSignature; // ── 4. the gate ────────────────────────────────────────────────────────── step(4, "Gate the mint on proof the buyer still holds the destination key"); const challenge = controlChallenge({ name: NAME, destination: buyer.address, orderIndex }); console.log(` challenge : ${challenge}`); // The buyer could sign — the key is right there in their browser. Prove the // gate opens for the right key, so a later failure is meaningful. const goodSig = signMessageBip137(challenge, buyer.receive[0].privateKey); check( verifyControl({ message: challenge, signature: goodSig, expectedAddress: buyer.address }).ok, "the real destination key opens the gate", ); // Someone else's key must not. const impostor = await BuiltInWallet.create(); const badSig = signMessageBip137(challenge, impostor.receive[0].privateKey); const badResult = verifyControl({ message: challenge, signature: badSig, expectedAddress: buyer.address }); check(!badResult.ok, "a different key does NOT open the gate", badResult.reason); // Neither does a signature for a different order. const replay = signMessageBip137( controlChallenge({ name: NAME, destination: buyer.address, orderIndex: orderIndex + 1 }), buyer.receive[0].privateKey, ); check( !verifyControl({ message: challenge, signature: replay, expectedAddress: buyer.address }).ok, "a signature from another order cannot be replayed", ); // ── 5. …but this buyer never signs ─────────────────────────────────────── step(5, "The buyer never signs — tab closed, phrase never saved"); const submitted = null; // nothing arrives before the deadline const gateOpen = submitted ? verifyControl({ message: challenge, signature: submitted, expectedAddress: buyer.address }).ok : false; check(!gateOpen, "the mint gate stays shut"); // ── 6. expire and refund ───────────────────────────────────────────────── step(6, "Quote expires — escrow refunds the payer"); order.expiresAt = Date.now() - 1; // deadline reached check(Date.now() > order.expiresAt, "quote has expired"); order.status = ORDER_STATUS.refunding; // Where does the money go? Read it off the payment, not off anything typed. const payerAddress = await payerAddressOf(client, payment.txId); console.log(` payer (recovered from the payment): ${payerAddress}`); check(!!payerAddress, "payer address recovered from the payment transaction"); check( payerAddress === exchange.cashaddr, "recovered address matches the actual sender", `expected ${exchange.cashaddr}`, ); const depositKey = depositKeyAt(orderIndex, root); const escrowUtxos = await getUtxos(client, deposit); const refund = buildRefundTx({ utxos: escrowUtxos, toAddress: payerAddress, addressToLockingBytecode, }); const signedRefund = signTransaction(refund.transaction, refund.sourceOutputs, (h) => h === Buffer.from(depositKey.lockingBytecode).toString("hex") ? { privateKey: depositKey.privateKey, publicKey: depositKey.publicKey } : null, ); const refundTxid = await broadcast(client, signedRefund.hex); order.status = ORDER_STATUS.refunded; console.log(` refund txid : ${refundTxid}`); console.log(` refunded : ${refund.costs.refundedSats} sat (fee ${refund.costs.chainFeeSats})`); let emptied = false; for (let i = 0; i < 25; i++) { const p = await checkPayment(client, order); if (p.receivedSats === 0n) { emptied = true; break; } process.stdout.write(` waiting for the escrow to drain… ${i + 1}/25\r`); await sleep(3000); } console.log(""); check(emptied, "escrow is empty — the money left"); check( refund.costs.refundedSats > priceSats - 1000n, "buyer got back essentially everything", `${refund.costs.refundedSats} of ${priceSats} sat`, ); // ── 7. the thing that must never have happened ─────────────────────────── step(7, "Verify the name was never minted"); const after = await checkAvailability(client, NAME); check(after.available, `${NAME} is STILL unregistered — no certificate was created`); check( (await getBalance(client, buyer.watchedAddresses)).certificates.length === 0, "the buyer's wallet holds no certificate", ); step("=", failures === 0 ? "RESULT: ALL CHECKS PASSED" : `RESULT: ${failures} CHECK(S) FAILED`); console.log(` unsigned purchase refunded to ${payerAddress}`); console.log(` ${NAME} never left the shelf`); } finally { client.close(); } process.exit(failures === 0 ? 0 : 1);