deviant/Parameters/Pari_P2P/scripts/chipnet-che-escrow-test.mjs
walerikus 35076606c7 chore: checkpoint in-flight work across shell, contracts, and docs
Snapshot of the shared working tree's outstanding changes (multiple
concurrent workstreams), excluding generated runtime state files
(.football-tote-state.json, .tote-platform-state.json, .usdm-historical-chart.html).

Spans: wallet shell (connect/, keystore MultiKeyStore+vault, screens,
currency picker, sign-message), verify artifacts, new Potidaea/DRA/tote
contracts + regtest suites, cross-chain/T6 + WalletConnect Parameters,
a telegram-orderbook subproject, design mockups, and docs. See
shells/app/WALLET-COORDINATION.md for the per-file ownership split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 02:02:05 +02:00

193 lines
13 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-che-escrow-test.mjs — GamePoolEscrowFlat re-run on a NEW denominated
* game currency: CheckersCoin (CHE).
*
* 1 CHE = 0.001 BCH = 100,000 sats (whole-unit backing rate — a valid tier)
* decimals = 2 → atomic FT = 1 "cent" = 1,000 sats (per-atomic rate)
*
* The contract operates in ATOMIC FTs (cents) and is rate-agnostic, so the
* on-chain math is the same shape as the rate-1000 flat test; what changes is
* the denomination layer: pots, stakes and fees are framed/reported in CHE
* (cents / 100), exercising the decimals=2 display path.
*
* Fee model unchanged from the brief: feeAmount = max( $0.01 , floor(1% of pot) ),
* computed off-chain in WHOLE atomic FTs (cents) via the price feed, baked into
* the contract, client re-derived & asserted before co-signing, enforced on-chain.
*
* Two regimes (players A & C, arbiter B):
* A) big pot 10.00 CHE (1000 cents) → 1% = 10 cents (0.10 CHE) > $0.01 floor → fee follows 1%
* B) tiny pot 0.50 CHE ( 50 cents) → 1% floors to 0 cents → $0.01 (~3 cents = 0.03 CHE) floor wins
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { TestNetWallet } from 'mainnet-js';
import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript';
const ROOT = join(import.meta.dirname, '..');
const STATE = join(ROOT, 'ChipnetTests', '.chipnet-che-escrow-test.json');
const MBA_ARTIFACT = join(ROOT, 'contracts/dist/DeviantOpenMintFT.json');
const FLAT_ARTIFACT = join(ROOT, 'contracts/dist/GamePoolEscrowFlat.json');
const KEYS = { a: join(ROOT, '.chipnet-keys.json'), b: join(ROOT, '.chipnet-keys-b.json'), c: join(ROOT, '.chipnet-keys-c.json') };
const RESERVOIR = 1_000_000_000_000_000_000n;
const RATE = 1000n; // per-atomic (cent) backing rate, sats/cent
const DECIMALS = 2n; // CHE has 2 decimals → 100 cents per CHE
const WHOLE_UNIT_RATE = RATE * (10n ** DECIMALS); // 100,000 sats per whole CHE = 0.001 BCH
const GENESIS_FEE = 0n;
// stakes are PER PLAYER, in atomic FTs (cents)
const SCENARIOS = [
{ key: 'big', stake: 500n, label: 'big pot 10.00 CHE (1% dominates)' },
{ key: 'tiny', stake: 25n, label: 'tiny pot 0.50 CHE ($0.01 floor dominates; proportional → 0)' },
];
// --- CHE display helpers (decimals = 2) ---
function che(cents){ // atomic FTs (cents) → "X.YY CHE" string
const neg = cents < 0n; const v = neg ? -cents : cents;
const whole = v / 100n, frac = v % 100n;
return `${neg?'-':''}${whole}.${frac.toString().padStart(2,'0')} CHE`;
}
function hexToBin(h){const o=new Uint8Array(h.length/2);for(let i=0;i<o.length;i++)o[i]=parseInt(h.slice(i*2,i*2+2),16);return o;}
function txidLE(h){const d=hexToBin(h);const o=new Uint8Array(d.length);for(let i=0;i<d.length;i++)o[i]=d[d.length-1-i];return o;}
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;}
const sleep=(ms)=>new Promise(r=>setTimeout(r,ms));
function divider(c='='){console.log(c.repeat(80));}
function step(t){console.log('');divider();console.log(t);divider();}
function load(){return existsSync(STATE)?JSON.parse(readFileSync(STATE,'utf8')):{s:{}};}
function save(s){writeFileSync(STATE,JSON.stringify(s,(k,v)=>typeof v==='bigint'?v.toString():v,2));}
function pickBch(utxos,need){const p=[];let t=0n;for(const u of utxos){p.push(u);t+=BigInt(u.satoshis);if(t>=need)break;}return{p,t};}
async function getBchUsd(){
if(process.env.BCH_USD)return Number(process.env.BCH_USD);
try{const ac=new AbortController();const t=setTimeout(()=>ac.abort(),8000);
const r=await fetch('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin-cash&vs_currencies=usd',{signal:ac.signal});
clearTimeout(t);const j=await r.json();const p=j?.['bitcoin-cash']?.usd;
if(p>0){console.log(` BCH price (CoinGecko): $${p}`);return Number(p);}
}catch(e){console.log(` (price fetch failed: ${e.message?.split('\n')[0]})`);}
console.log(' BCH price fallback: $500');return 500;
}
/** Server-side fee model: feeAmount = max( $0.01 in atomic FT , floor(1% of pot in atomic FT) ). */
function computeFlatFee(poolAmount, rateSats, bchUsd){
const onePctFt = poolAmount * 100n / 10000n; // floor(1% of pot), atomic FT (cents)
const centSats = Math.ceil(0.01 / bchUsd * 1e8); // $0.01 in sats
const centFt = BigInt(Math.ceil(centSats / Number(rateSats)));// $0.01 in atomic FT (ceil)
const fee = onePctFt > centFt ? onePctFt : centFt;
return { fee, onePctFt, centFt, centSats };
}
async function main(){
const mbaArt=JSON.parse(readFileSync(MBA_ARTIFACT,'utf8'));
const flatArt=JSON.parse(readFileSync(FLAT_ARTIFACT,'utf8'));
const A=await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.a,'utf8')).wif);
const B=await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.b,'utf8')).wif);
const C=await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.c,'utf8')).wif);
const provider=new ElectrumNetworkProvider('chipnet');
const sigA=new SignatureTemplate(A.privateKey), sigB=new SignatureTemplate(B.privateKey), sigC=new SignatureTemplate(C.privateKey);
const flbC=p2pkh(C.publicKeyHash);
let state=load();state.s=state.s||{};
step(`BCH price + CheckersCoin (CHE) MBA — 1 CHE = ${WHOLE_UNIT_RATE} sats (0.001 BCH), decimals ${DECIMALS}, atomic=1 cent=${RATE} sats`);
const bchUsd=await getBchUsd();state.bchUsd=bchUsd;save(state);
if(!state.mba){
const u=(await (async()=>{let x=await C.getUtxos();let e=x.filter(y=>y.vout===0&&!y.token);if(e.length)return e[0];const bad=x.filter(y=>!(y.vout===0&&!y.token));const r=await C.send([{cashaddr:C.cashaddr,value:2000n,unit:'sat'}],bad.length?{utxoIds:bad}:undefined);await sleep(12000);x=await C.getUtxos();return x.filter(y=>y.vout===0&&!y.token)[0];})());
const cat=u.txid;const c=new Contract(mbaArt,[txidLE(cat),RATE,GENESIS_FEE,flbC],{provider});
const r=await C.tokenGenesis({cashaddr:c.tokenAddress,amount:RESERVOIR,value:1000n,nft:{capability:'minting',commitment:''}},[],{utxoIds:[u]});
console.log(` CHE genesis ${r?.txId??r?.txid} — ${cat}`);
state.mba={categoryId:cat,contractTokenAddress:c.tokenAddress};save(state);await sleep(10000);
} else console.log(` CHE: ${state.mba.categoryId}`);
const mba=new Contract(mbaArt,[txidLE(state.mba.categoryId),RATE,GENESIS_FEE,flbC],{provider});
async function mint(who,sig,amount,label){
const ru=await provider.getUtxos(mba.tokenAddress);
const res=ru.find(x=>x.token?.nft?.capability==='minting'&&x.token?.category===state.mba.categoryId&&x.token?.amount>0n);
const gross=amount*RATE;
const bch=(await provider.getUtxos(who.cashaddr)).filter(x=>!x.token).sort((a,b)=>Number(b.satoshis)-Number(a.satoshis));
const mf=6000n;const {p,t}=pickBch(bch,gross+mf+1000n);
if(t<gross+mf+1000n)throw new Error(`${label}: insufficient BCH ${t}`);
const tb=new TransactionBuilder({provider});
tb.addInput(res,mba.unlock.claim(amount,p2pkh(who.publicKeyHash)));
for(const x of p)tb.addInput(x,sig.unlockP2PKH());
tb.addOutput({to:mba.tokenAddress,amount:1000n,token:{amount:res.token.amount-amount,category:state.mba.categoryId,nft:{capability:'minting',commitment:''}}});
tb.addOutput({to:who.tokenaddr,amount:gross,token:{amount,category:state.mba.categoryId}});
tb.addOutput({to:who.cashaddr,amount:BigInt(res.satoshis)+t-1000n-gross-mf});
await tb.send();console.log(` ${label}`);await sleep(10000);
}
const totalStake=SCENARIOS.reduce((a,s)=>a+s.stake,0n);
state.mints=state.mints||{};
if(!state.mints.a){await mint(A,sigA,totalStake,`A claims ${totalStake} cents (${che(totalStake)})`);state.mints.a=true;save(state);}
if(!state.mints.c){await mint(C,sigC,totalStake,`C claims ${totalStake} cents (${che(totalStake)})`);state.mints.c=true;save(state);}
const summary=[];
for(const sc of SCENARIOS){
const STAKE=sc.stake, POOL=STAKE*2n, POOL_SATS=POOL*RATE, STAKE_SATS=STAKE*RATE;
const { fee, onePctFt, centFt } = computeFlatFee(POOL, RATE, bchUsd);
const winnerAmount=POOL-fee;
state.s[sc.key]=state.s[sc.key]||{};const S=state.s[sc.key];
step(`SCENARIO ${sc.key} — ${sc.label}`);
console.log(` pot=${POOL} cents (${che(POOL)}, ${POOL_SATS} sats); 1%=${onePctFt} cents (${che(onePctFt)}); $0.01=${centFt} cents (${che(centFt)})`);
console.log(` → feeAmount=max=${fee} cents (${che(fee)})`);
const check=computeFlatFee(POOL,RATE,bchUsd).fee;
if(check!==fee)throw new Error('client fee re-derivation mismatch');
console.log(` client re-derived feeAmount=${check} cents (${che(check)}) — matches baked value ✓ (would co-sign)`);
const contract=new Contract(flatArt,[
txidLE(state.mba.categoryId), POOL, POOL_SATS, STAKE, STAKE_SATS,
A.publicKeyHash, C.publicKeyHash, B.publicKeyCompressed, hexToBin('00'.repeat(32)), 9999999n, fee,
],{provider});
if(!S.funded){
const aFt=(await provider.getUtxos(A.cashaddr)).find(x=>x.token?.category===state.mba.categoryId&&!x.token?.nft&&x.token?.amount>=STAKE);
const cFt=(await provider.getUtxos(C.cashaddr)).find(x=>x.token?.category===state.mba.categoryId&&!x.token?.nft&&x.token?.amount>=STAKE);
if(!aFt||!cFt)throw new Error(`missing stake (A=${!!aFt} C=${!!cFt})`);
const aRem=aFt.token.amount-STAKE, cRem=cFt.token.amount-STAKE, mf=6000n;
const cBch=(await provider.getUtxos(C.cashaddr)).filter(x=>!x.token).sort((a,b)=>Number(b.satoshis)-Number(a.satoshis));
const {p,t}=pickBch(cBch,mf+POOL_SATS+1000n);
const cChange=BigInt(aFt.satoshis)+BigInt(cFt.satoshis)+t-POOL_SATS-aRem*RATE-cRem*RATE-mf;
const tb=new TransactionBuilder({provider});
tb.addInput(aFt,sigA.unlockP2PKH());tb.addInput(cFt,sigC.unlockP2PKH());
for(const x of p)tb.addInput(x,sigC.unlockP2PKH());
tb.addOutput({to:contract.tokenAddress,amount:POOL_SATS,token:{amount:POOL,category:state.mba.categoryId}});
if(aRem>0n)tb.addOutput({to:A.tokenaddr,amount:aRem*RATE,token:{amount:aRem,category:state.mba.categoryId}});
if(cRem>0n)tb.addOutput({to:C.tokenaddr,amount:cRem*RATE,token:{amount:cRem,category:state.mba.categoryId}});
tb.addOutput({to:C.cashaddr,amount:cChange});
await tb.send();console.log(` funded pot ${POOL} cents (${che(POOL)})`);S.funded=true;save(state);await sleep(10000);
}
if(!S.resolved){
const pool=(await provider.getUtxos(contract.tokenAddress)).find(x=>x.token?.category===state.mba.categoryId&&!x.token?.nft&&x.token?.amount===POOL);
if(!pool)throw new Error('pool not found');
const mf=6000n;const cBch=(await provider.getUtxos(C.cashaddr)).filter(x=>!x.token).sort((a,b)=>Number(b.satoshis)-Number(a.satoshis));
const {p,t}=pickBch(cBch,mf+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(pool,contract.unlock.resolveArbiter(sigB,p2pkh(A.publicKeyHash)));
for(const x of p)tb.addInput(x,sigC.unlockP2PKH());
tb.addOutput({to:A.tokenaddr,amount:winnerAmount*RATE,token:{amount:winnerAmount,category:state.mba.categoryId}});
if(fee>0n)tb.addOutput({to:B.tokenaddr,amount:fee*RATE,token:{amount:fee,category:state.mba.categoryId}});
tb.addOutput({to:C.cashaddr,amount:BigInt(pool.satoshis)+t-winnerAmount*RATE-(fee>0n?fee*RATE:0n)-mf});
const sent=await tb.send();const txid=typeof sent==='string'?sent:sent?.txid??sent?.txId;
console.log(` resolved (B→A): winner ${winnerAmount} cents (${che(winnerAmount)}), arbiter ${fee} cents (${che(fee)}) — ${txid}`);
S.resolved={txid,fee:fee.toString(),winner:winnerAmount.toString()};save(state);await sleep(10000);
}
summary.push({key:sc.key,pool:POOL,onePctFt,centFt,fee,winnerAmount,regime:onePctFt>centFt?'1%':'$0.01 floor'});
}
step(`SUMMARY — CHE (1 CHE=${WHOLE_UNIT_RATE} sats, 2 dec) · GamePoolEscrowFlat fee=max($0.01,1%) @ BCH $${bchUsd}`);
console.log(' | scenario | pot | 1% | $0.01 | feeAmount | regime | winner |');
console.log(' |----------|-----------|-----------|-----------|-----------|-------------|-----------|');
for(const s of summary){
console.log(` | ${s.key.padEnd(8)} | ${che(s.pool).padStart(9)} | ${che(s.onePctFt).padStart(9)} | ${che(s.centFt).padStart(9)} | ${che(s.fee).padStart(9)} | ${s.regime.padEnd(11)} | ${che(s.winnerAmount).padStart(9)} |`);
}
console.log('');
console.log(` Denomination: contract math runs in atomic FTs (cents); CHE = cents/100 (decimals 2).`);
console.log(' Big pot: 1% (0.10 CHE) > $0.01 floor → fee follows 1%.');
console.log(' Tiny pot: 1% floors to 0.00 CHE (proportional would pay the arbiter NOTHING) →');
console.log(' the $0.01 floor (0.03 CHE) kicks in, so the arbiter still earns a real fee.');
console.log(' Contract enforced the baked feeAmount; client re-derived & matched before co-signing.');
console.log(' Peg preserved on both winner and fee outputs at the per-cent rate (1000 sats).');
divider();
}
main().catch((e)=>{console.error('FAILED:',e?.message??e);if(e?.stack)console.error(e.stack);process.exit(1);});