Brings the whole project into git (previously only the on-ramp feature and a few parallel files were tracked). Includes: wallet core (src/), CashScript contracts/, the Capacitor shell (shells/), scripts/, tests, and the docs corpus (wiki/, snapshots/, Parameters/, Archive/, KnowledgeBase text). Excluded via .gitignore: node_modules/, dist/, .env (secrets), testnet WIF key files (.chipnet-*.json, devWallets.ts), KnowledgeBase/RAW media (~9.6 GB), transcripts/ (session logs), and machine-local config (.claude/*.local, *.lock). Secret-scanned before commit — no credentials in the tree. Note: src/swapMath.ts, src/swapBuilder.ts, contracts/OpenMintToken.cash are legacy (OP_RETURN era) but still imported; committed as-is, flagged for a separate removal once de-referenced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
219 lines
11 KiB
JavaScript
219 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-checkers-game-test.mjs — BCH AS GAME INFRASTRUCTURE. One move = one
|
|
* transaction. Validates contracts/CheckersGame.cash on live chipnet.
|
|
*
|
|
* The covenant holds a MUTABLE game-state NFT (value == pot). Each move spends
|
|
* and recreates it: the chain enforces TURN ORDER + PER-MOVE PLAYER SIGNATURE +
|
|
* an immutable hash-chained move log. It does NOT referee checkers rules — the
|
|
* game engine (D:\Dev\Checkers) does. Settlement = cooperative 2-of-2, or the
|
|
* arbiter (the server's oracle key, as in backend/src/deviant/arbiter.ts).
|
|
*
|
|
* GAME 1: genesis → NEG (wrong player can't move out of turn) → 4 real moves
|
|
* (A,B,A,B) → cooperative settle (A+B agree B wins, pot → B).
|
|
* GAME 2: genesis → arbiter settle (server declares A wins, pot → A).
|
|
*
|
|
* Roles: playerA=A, playerB=B, arbiter+fee+pot funder=C. Fresh each run.
|
|
* Requires .chipnet-keys{,-b,-c}.json funded.
|
|
*
|
|
* Run: node --import tsx scripts/chipnet-checkers-game-test.mjs
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { randomBytes } from 'node:crypto';
|
|
import { TestNetWallet } from 'mainnet-js';
|
|
import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript';
|
|
import { sha256, hexToBin, binToHex } from '@bitauth/libauth';
|
|
|
|
const ROOT = join(import.meta.dirname, '..');
|
|
const ARTIFACT = JSON.parse(readFileSync(join(ROOT, 'contracts/dist/CheckersGame.json'), 'utf8'));
|
|
const KEYS = { a: '.chipnet-keys.json', b: '.chipnet-keys-b.json', c: '.chipnet-keys-c.json' };
|
|
|
|
const POT1 = 60000n, POT2 = 20000n, FEE = 1500n;
|
|
const utf8 = (s) => new TextEncoder().encode(s);
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const line = (c = '=') => console.log(c.repeat(78));
|
|
const step = (t) => { console.log(''); line(); console.log(t); line(); };
|
|
const txidOf = (s) => (typeof s === 'string' ? s : s?.txid ?? s?.txId);
|
|
function p2pkh(pkh) { const o = new Uint8Array(25); o[0] = 0x76; o[1] = 0xa9; o[2] = 0x14; o.set(pkh, 3); o[23] = 0x88; o[24] = 0xac; return o; }
|
|
|
|
// Compute the next 35-byte commitment EXACTLY as CheckersGame.move() does:
|
|
// turn(1) ++ moveCount(2, LE) ++ sha256(prevStateHash(32) ++ moveData)
|
|
function nextCommitment(prev, moveData) {
|
|
const turn = prev[0];
|
|
const count = prev[1] | (prev[2] << 8);
|
|
const stateHash = prev.slice(3, 35);
|
|
const newCount = count + 1;
|
|
const newHash = sha256.hash(new Uint8Array([...stateHash, ...moveData]));
|
|
const out = new Uint8Array(35);
|
|
out[0] = turn === 0x00 ? 0x01 : 0x00;
|
|
out[1] = newCount & 0xff;
|
|
out[2] = (newCount >> 8) & 0xff;
|
|
out.set(newHash, 3);
|
|
return out;
|
|
}
|
|
|
|
async function main() {
|
|
line();
|
|
console.log('CHIPNET CHECKERS-GAME TEST — BCH as game infrastructure, 1 move = 1 tx');
|
|
line();
|
|
const A = await TestNetWallet.fromWIF(JSON.parse(readFileSync(join(ROOT, KEYS.a), 'utf8')).wif);
|
|
const B = await TestNetWallet.fromWIF(JSON.parse(readFileSync(join(ROOT, KEYS.b), 'utf8')).wif);
|
|
const C = await TestNetWallet.fromWIF(JSON.parse(readFileSync(join(ROOT, KEYS.c), 'utf8')).wif);
|
|
const provider = new ElectrumNetworkProvider('chipnet');
|
|
const sig = { a: new SignatureTemplate(A.privateKey), b: new SignatureTemplate(B.privateKey), c: new SignatureTemplate(C.privateKey) };
|
|
const refundDeadline = BigInt(await provider.getBlockHeight()) + 1000n;
|
|
const contract = new Contract(ARTIFACT, [A.publicKeyCompressed, B.publicKeyCompressed, C.publicKeyCompressed, refundDeadline], { provider });
|
|
console.log(`playerA (A): ${A.cashaddr}`);
|
|
console.log(`playerB (B): ${B.cashaddr}`);
|
|
console.log(`arbiter (C): ${C.cashaddr}`);
|
|
console.log(`contract: ${contract.tokenAddress}`);
|
|
|
|
async function eligible(w, need) {
|
|
let u = await w.getUtxos(); let e = u.filter((x) => x.vout === 0 && !x.token && BigInt(x.satoshis) >= need);
|
|
if (e.length) return e[0];
|
|
await w.send([{ cashaddr: w.cashaddr, value: Number(need), unit: 'sat' }]); // make a vout-0 output >= need
|
|
await sleep(12000);
|
|
u = await w.getUtxos(); e = u.filter((x) => x.vout === 0 && !x.token && BigInt(x.satoshis) >= need);
|
|
if (!e.length) throw new Error('could not produce a sufficient vout-0 genesis UTXO');
|
|
return e[0];
|
|
}
|
|
async function feeInputs(w, need) {
|
|
const bch = (await provider.getUtxos(w.cashaddr)).filter((x) => !x.token).sort((a, b) => Number(b.satoshis - a.satoshis));
|
|
const p = []; let t = 0n;
|
|
for (const x of bch) { p.push(x); t += BigInt(x.satoshis); if (t >= need) break; }
|
|
if (t < need) throw new Error('insufficient BCH for fee');
|
|
return { p, t };
|
|
}
|
|
async function findNft(cat, commitHex, tries = 8) {
|
|
for (let i = 0; i < tries; i++) {
|
|
const u = (await provider.getUtxos(contract.tokenAddress)).find(
|
|
(x) => x.token?.category === cat && x.token?.nft?.capability === 'mutable' && x.token?.nft?.commitment === commitHex,
|
|
);
|
|
if (u) return u;
|
|
await sleep(4000);
|
|
}
|
|
throw new Error(`NFT @ commitment ${commitHex.slice(0, 16)}… not found`);
|
|
}
|
|
|
|
// ---- genesis a game (mutable NFT, value=pot, commitment = turnA|count0|sha256(gameId)) ----
|
|
async function genesis(pot, label) {
|
|
const gameId = randomBytes(16);
|
|
const init = new Uint8Array([0x00, 0x00, 0x00, ...sha256.hash(gameId)]); // 35 bytes
|
|
const elig = await eligible(C, pot + 3000n);
|
|
const cat = elig.txid;
|
|
const r = await C.tokenGenesis(
|
|
{ cashaddr: contract.tokenAddress, amount: 0n, value: pot, nft: { capability: 'mutable', commitment: binToHex(init) } },
|
|
[], { utxoIds: [elig] },
|
|
);
|
|
console.log(` ${label}: genesis ${txidOf(r)} — category ${cat.slice(0, 16)}…, pot ${pot}`);
|
|
await sleep(10000);
|
|
return { cat, commit: binToHex(init), commitBin: init };
|
|
}
|
|
|
|
// ---- one move: mover (whose turn it is) spends+recreates the NFT ----
|
|
async function move(game, mover, moverSig, moveStr) {
|
|
const md = utf8(moveStr);
|
|
const next = nextCommitment(game.commitBin, md);
|
|
const nft = await findNft(game.cat, game.commit);
|
|
const { p, t } = await feeInputs(mover, FEE + 1000n);
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(nft, contract.unlock.move(md, moverSig));
|
|
for (const x of p) tb.addInput(x, moverSig.unlockP2PKH());
|
|
tb.addOutput({ to: contract.tokenAddress, amount: BigInt(nft.satoshis), token: { amount: 0n, category: game.cat, nft: { capability: 'mutable', commitment: binToHex(next) } } });
|
|
tb.addOutput({ to: mover.cashaddr, amount: t - FEE });
|
|
const id = txidOf(await tb.send());
|
|
game.commit = binToHex(next); game.commitBin = next;
|
|
const turnNow = next[0] === 0x00 ? 'A' : 'B';
|
|
const count = next[1] | (next[2] << 8);
|
|
console.log(` move "${moveStr}" by ${mover === A ? 'A' : 'B'} → tx ${id.slice(0, 12)}… | now move #${count}, ${turnNow} to play`);
|
|
await sleep(8000);
|
|
return id;
|
|
}
|
|
|
|
// ================= GAME 1 =================
|
|
step('GAME 1 — genesis, turn-enforcement, four real moves, cooperative settle');
|
|
const g1 = await genesis(POT1, 'GAME1');
|
|
|
|
// NEG: it is A's turn (turn byte 0x00). B must NOT be able to move.
|
|
console.log(' NEG — B tries to move on A\'s turn (must be REJECTED):');
|
|
let negRejected = false;
|
|
try {
|
|
const md = utf8('h6-g5');
|
|
const next = nextCommitment(g1.commitBin, md);
|
|
const nft = await findNft(g1.cat, g1.commit);
|
|
const { p, t } = await feeInputs(B, FEE + 1000n);
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(nft, contract.unlock.move(md, sig.b)); // WRONG player signs
|
|
for (const x of p) tb.addInput(x, sig.b.unlockP2PKH());
|
|
tb.addOutput({ to: contract.tokenAddress, amount: BigInt(nft.satoshis), token: { amount: 0n, category: g1.cat, nft: { capability: 'mutable', commitment: binToHex(next) } } });
|
|
tb.addOutput({ to: B.cashaddr, amount: t - FEE });
|
|
await tb.send();
|
|
} catch (e) { negRejected = true; console.log(` rejected (expected): ${(e?.message ?? e).toString().slice(0, 80)}`); }
|
|
if (!negRejected) throw new Error('SECURITY: out-of-turn move was ACCEPTED — turn gate broken');
|
|
|
|
// Four real Russian-checkers opening moves, alternating A,B,A,B.
|
|
await move(g1, A, sig.a, 'c3-d4');
|
|
await move(g1, B, sig.b, 'f6-e5');
|
|
await move(g1, A, sig.a, 'd4-c5');
|
|
await move(g1, B, sig.b, 'b6-a5');
|
|
|
|
// Cooperative settle: A and B agree B wins → pot to B (NFT burned).
|
|
step('GAME 1 — cooperative settle (A + B co-sign: B wins) → pot to B');
|
|
const nft1 = await findNft(g1.cat, g1.commit);
|
|
const bBefore = BigInt((await provider.getUtxos(B.cashaddr)).reduce((s, u) => s + (u.token ? 0 : Number(u.satoshis)), 0));
|
|
{
|
|
const { p, t } = await feeInputs(C, FEE + 1000n);
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(nft1, contract.unlock.settleCooperative(sig.a, sig.b, p2pkh(B.publicKeyHash)));
|
|
for (const x of p) tb.addInput(x, sig.c.unlockP2PKH());
|
|
tb.addOutput({ to: B.cashaddr, amount: BigInt(nft1.satoshis) }); // full pot → B
|
|
tb.addOutput({ to: C.cashaddr, amount: t - FEE });
|
|
const id = txidOf(await tb.send());
|
|
console.log(` settled cooperatively → ${id} (pot ${nft1.satoshis} sats to B)`);
|
|
await sleep(10000);
|
|
}
|
|
|
|
// ================= GAME 2 =================
|
|
step('GAME 2 — genesis, arbiter settle (server oracle declares A wins) → pot to A');
|
|
const g2 = await genesis(POT2, 'GAME2');
|
|
const nft2 = await findNft(g2.cat, g2.commit);
|
|
{
|
|
const { p, t } = await feeInputs(C, FEE + 1000n);
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(nft2, contract.unlock.settleArbiter(sig.c, p2pkh(A.publicKeyHash)));
|
|
for (const x of p) tb.addInput(x, sig.c.unlockP2PKH());
|
|
tb.addOutput({ to: A.cashaddr, amount: BigInt(nft2.satoshis) }); // pot → A
|
|
tb.addOutput({ to: C.cashaddr, amount: t - FEE });
|
|
const id = txidOf(await tb.send());
|
|
console.log(` settled by arbiter → ${id} (pot ${nft2.satoshis} sats to A)`);
|
|
await sleep(10000);
|
|
}
|
|
|
|
// ================= VERIFY =================
|
|
step('VERIFY');
|
|
const bAfter = BigInt((await provider.getUtxos(B.cashaddr)).reduce((s, u) => s + (u.token ? 0 : Number(u.satoshis)), 0));
|
|
const g1Gone = !(await provider.getUtxos(contract.tokenAddress)).some((x) => x.token?.category === g1.cat);
|
|
const g2Gone = !(await provider.getUtxos(contract.tokenAddress)).some((x) => x.token?.category === g2.cat);
|
|
const checks = [
|
|
['NEG: out-of-turn move rejected (turn gate holds)', negRejected],
|
|
['GAME1: B received the pot (cooperative settle)', bAfter - bBefore >= POT1 - FEE],
|
|
['GAME1: game NFT consumed at settlement', g1Gone],
|
|
['GAME2: arbiter settlement consumed the NFT', g2Gone],
|
|
];
|
|
let ok = true;
|
|
for (const [l, p] of checks) { console.log(` [${p ? 'PASS' : 'FAIL'}] ${l}`); ok = ok && p; }
|
|
|
|
step('SUMMARY — BCH as checkers infrastructure (one move = one transaction)');
|
|
console.log(' Four moves, four transactions; the chain enforced turn order and');
|
|
console.log(' per-move player signatures, and chained an immutable move log in the');
|
|
console.log(' NFT commitment. An out-of-turn move was rejected on-chain. The pot');
|
|
console.log(' settled cooperatively (2-of-2) and by arbiter (server oracle) — the');
|
|
console.log(' same model wired into backend/src/deviant/arbiter.ts. Rules stay in');
|
|
console.log(' the engine; the trust infrastructure is on Bitcoin Cash.');
|
|
line();
|
|
if (!ok) { console.error('VERIFY FAILED'); process.exit(1); }
|
|
process.exit(0);
|
|
}
|
|
|
|
main().catch((e) => { console.error('FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack); process.exit(1); });
|