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>
193 lines
13 KiB
JavaScript
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);});
|