Snapshot of accumulated work-in-progress on top of the baseline commit. This session — retire Tier B from the wallet shell (single-tier model, glossary "Tiers & trust"): - SendFlow.tsx: BCH-only; removed the non-functional Tier-B send path, the D-4/D-5 raw-BCH co-display, and the ADR-0013 friction ladder. - ArtemisionScreen.tsx: Create wizard collapsed to a single 5-step on-chain genesis flow; tier-selection step + Tier-B branch (prepareTierBImport) deleted. - QuickActions.tsx / SettingsScreen.tsx: receive note reworded; "Send friction thresholds" + "Tier explainer" rows removed; registry-trust note reworded. - Verified: tsc clean, vitest 74/74, full preview walkthrough, no console errors. Pre-existing uncommitted work also folded in: - USDm -> FIATm family rename/reorg (Parameters/FIATm/, design records) - ~130 chipnet + sim-fiatm scripts (catalytic-converter, hyperinflation backtests) - snapshots 0153-0168; wiki updates (glossary, decisions, index); ADR-0044/0045 - mockups + additional shell screens (Agora, Apps, MainScreen, AssetDetail, ...) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
163 lines
12 KiB
JavaScript
163 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-tote2-rs-test.mjs — exercise the Tier-2 tote's REFUND and SWEEP paths
|
|
* on live chipnet (the two backstop branches of contracts/ParimutuelToteOpen.cash,
|
|
* BCH variant). The core bet/attest/claim flow is validated by snapshot 0048;
|
|
* this closes the remaining two code paths.
|
|
*
|
|
* REFUND pool — bets placed, NEVER attested; after sweepDeadline a bettor
|
|
* reclaims their own stake (receipt consumed → no double-refund). phase 0.
|
|
* SWEEP pool — bets placed, attested, NEVER claimed; after sweepDeadline the
|
|
* unclaimed remainder is swept to sweepPkh. phase 1.
|
|
*
|
|
* Both use a PAST sweepDeadline so the timelocked paths are spendable immediately
|
|
* (locktime = current tip). Two separate tote categories (deadlines differ →
|
|
* different covenant addresses). Roles: founder/sweep recipient/fees = C.
|
|
*/
|
|
import { existsSync, readFileSync, writeFileSync } 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 { secp256k1, sha256 } from '@bitauth/libauth';
|
|
|
|
const ROOT = join(import.meta.dirname, '..');
|
|
const STATE = join(ROOT, 'ChipnetTests', '.chipnet-tote2-rs-test.json');
|
|
const ART = join(ROOT, 'contracts/dist/ParimutuelToteOpen.json');
|
|
const KEYS = { a: join(ROOT, '.chipnet-keys.json'), b: join(ROOT, '.chipnet-keys-b.json'), c: join(ROOT, '.chipnet-keys-c.json') };
|
|
const SEED = 1000n, DUST = 1000n, MF = 6000n;
|
|
|
|
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 binToHex(b){return Array.from(b,(x)=>x.toString(16).padStart(2,'0')).join('');}
|
|
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;}
|
|
function u64le(n){let v=BigInt(n);const o=new Uint8Array(8);for(let i=0;i<8;i++){o[i]=Number(v&0xffn);v>>=8n;}return binToHex(o);}
|
|
function stateCommit(ph,oc,A,B,rws){return ph+oc+u64le(A)+u64le(B)+u64le(rws);}
|
|
function receiptCommit(c,s,k){return c+s+u64le(k);}
|
|
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')):{};}
|
|
function save(s){writeFileSync(STATE,JSON.stringify(s,(k,v)=>typeof v==='bigint'?v.toString():v,2));}
|
|
function pickBch(u,need){const p=[];let t=0n;for(const x of u){p.push(x);t+=BigInt(x.satoshis);if(t>=need)break;}return{p,t};}
|
|
function txidOf(s){return typeof s==='string'?s:s?.txid??s?.txId;}
|
|
function oracleSign(privHex,evBin,outByte){const m=new Uint8Array([...evBin,outByte]);const sig=secp256k1.signMessageHashDER(hexToBin(privHex),sha256.hash(m));if(typeof sig==='string')throw new Error('sign '+sig);return{message:m,sig};}
|
|
|
|
async function main(){
|
|
const art=JSON.parse(readFileSync(ART,'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 sig={a:new SignatureTemplate(A.privateKey),b:new SignatureTemplate(B.privateKey),c:new SignatureTemplate(C.privateKey)};
|
|
let st=load();
|
|
if(!st.oracle){let p;do{p=randomBytes(32);}while(typeof secp256k1.derivePublicKeyCompressed(p)==='string');st.oracle={priv:binToHex(p),pub:binToHex(secp256k1.derivePublicKeyCompressed(p))};save(st);}
|
|
const oraclePub=hexToBin(st.oracle.pub);
|
|
const PHASE0=stateCommit('00','00',0,0,0);
|
|
|
|
async function eligible(w){let u=await w.getUtxos();let e=u.filter(x=>x.vout===0&&!x.token);if(e.length)return e[0];const bad=u.filter(x=>!(x.vout===0&&!x.token));const r=await w.send([{cashaddr:w.cashaddr,value:2000n,unit:'sat'}],bad.length?{utxoIds:bad}:undefined);console.log(` splitter ${txidOf(r)}`);await sleep(12000);u=await w.getUtxos();e=u.filter(x=>x.vout===0&&!x.token);if(!e.length)throw new Error('splitter failed');return e[0];}
|
|
async function cBch(need){const u=(await provider.getUtxos(C.cashaddr)).filter(x=>!x.token).sort((a,b)=>Number(b.satoshis)-Number(a.satoshis));return pickBch(u,need);}
|
|
async function bchOf(w){return (await provider.getUtxos(w.cashaddr)).filter(x=>!x.token).sort((a,b)=>Number(b.satoshis)-Number(a.satoshis));}
|
|
async function balance(w){return (await provider.getUtxos(w.cashaddr)).filter(x=>!x.token).reduce((s,u)=>s+BigInt(u.satoshis),0n);}
|
|
|
|
// genesis a tote NFT, deploy covenant with given deadlines, relocate NFT into it
|
|
async function setupPool(tag,betDL,sweepDL){
|
|
st[tag]=st[tag]||{};
|
|
if(!st[tag].cat){const u=await eligible(C);st[tag].cat=u.txid;const r=await C.tokenGenesis({cashaddr:C.tokenaddr,amount:0n,value:SEED,nft:{capability:'minting',commitment:PHASE0}},[],{utxoIds:[u]});console.log(` [${tag}] genesis ${txidOf(r)} — ${u.txid}`);st[tag].event=binToHex(randomBytes(16));st[tag].betDL=betDL;st[tag].sweepDL=sweepDL;save(st);await sleep(10000);}
|
|
const cat=st[tag].cat;
|
|
const tote=new Contract(art,[txidLE(cat),oraclePub,hexToBin(st[tag].event),BigInt(st[tag].betDL),BigInt(st[tag].sweepDL),C.publicKeyHash,0n,C.publicKeyHash],{provider}); // rakeBps=0 (no rake)
|
|
if(!st[tag].relocate){const g=(await provider.getUtxos(C.cashaddr)).find(x=>x.token?.category===cat&&x.token?.nft?.capability==='minting');if(!g)throw new Error(`${tag}: genesis NFT missing`);const {p,t}=await cBch(MF+SEED);const tb=new TransactionBuilder({provider});tb.addInput(g,sig.c.unlockP2PKH());for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());tb.addOutput({to:tote.tokenAddress,amount:SEED,token:{amount:0n,category:cat,nft:{capability:'minting',commitment:PHASE0}}});tb.addOutput({to:C.cashaddr,amount:BigInt(g.satoshis)+t-SEED-MF});st[tag].relocate=txidOf(await tb.send());save(st);console.log(` [${tag}] pool at ${tote.tokenAddress}`);await sleep(10000);}
|
|
return {tote,cat};
|
|
}
|
|
async function poolUtxo(tote,cat){return (await provider.getUtxos(tote.tokenAddress)).find(x=>x.token?.category===cat&&x.token?.nft?.capability==='minting');}
|
|
async function placeBet(tag,tote,cat,w,s,sideHex,stake){
|
|
st[tag].bets=st[tag].bets||{};st[tag].tot=st[tag].tot||{A:'0',B:'0'};
|
|
const key=`${sideHex}_${stake}`;if(st[tag].bets[key]){return;}
|
|
const pool=await poolUtxo(tote,cat);
|
|
let tA=BigInt(st[tag].tot.A),tB=BigInt(st[tag].tot.B);if(sideHex==='00')tA+=stake;else tB+=stake;
|
|
const newCommit=stateCommit('00','00',tA,tB,0);
|
|
const bch=await bchOf(w);const {p,t}=pickBch(bch,stake+DUST+MF+1000n);if(t<stake+DUST+MF+1000n)throw new Error(`${tag} bet: low BCH`);
|
|
const tb=new TransactionBuilder({provider});
|
|
tb.addInput(pool,tote.unlock.bet(stake,hexToBin(sideHex)));
|
|
for(const x of p)tb.addInput(x,s.unlockP2PKH());
|
|
tb.addOutput({to:tote.tokenAddress,amount:BigInt(pool.satoshis)+stake,token:{amount:0n,category:cat,nft:{capability:'minting',commitment:newCommit}}});
|
|
tb.addOutput({to:w.tokenaddr,amount:DUST,token:{amount:0n,category:cat,nft:{capability:'mutable',commitment:receiptCommit('00',sideHex,stake)}}});
|
|
tb.addOutput({to:w.cashaddr,amount:t-stake-DUST-MF});
|
|
console.log(` [${tag}] bet ${stake} side ${sideHex} — ${txidOf(await tb.send())}`);
|
|
st[tag].bets[key]=true;st[tag].tot={A:tA.toString(),B:tB.toString()};save(st);await sleep(10000);
|
|
}
|
|
async function findReceipt(w,cat,sideHex,stake,claimedHex){const rc=receiptCommit(claimedHex,sideHex,stake);return (await provider.getUtxos(w.cashaddr)).find(x=>x.token?.category===cat&&x.token?.nft?.capability==='mutable'&&x.token?.nft?.commitment===rc);}
|
|
|
|
// ============ REFUND ============
|
|
step('REFUND — bet then reclaim own stake after sweepDeadline (no attest)');
|
|
const tip=await provider.getBlockHeight();
|
|
const {tote:rT,cat:rCat}=await setupPool('refund',tip,tip-1);
|
|
await placeBet('refund',rT,rCat,A,sig.a,'00',5000n);
|
|
if(!st.refund.done){
|
|
const pool=await poolUtxo(rT,rCat);
|
|
const rcpt=await findReceipt(A,rCat,'00',5000n,'00');if(!rcpt)throw new Error('refund: receipt missing');
|
|
const before=await balance(A);
|
|
const {p,t}=await cBch(MF+1000n);
|
|
const tb=new TransactionBuilder({provider});
|
|
tb.setLocktime(await provider.getBlockHeight());
|
|
tb.addInput(pool,rT.unlock.refund(p2pkh(A.publicKeyHash))); // [0] pool
|
|
tb.addInput(rcpt,sig.a.unlockP2PKH()); // [1] receipt
|
|
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH()); // [2+] fee
|
|
tb.addOutput({to:rT.tokenAddress,amount:BigInt(pool.satoshis)-5000n,token:{amount:0n,category:rCat,nft:{capability:'minting',commitment:pool.token.nft.commitment}}}); // [0] pool back
|
|
tb.addOutput({to:A.cashaddr,amount:5000n}); // [1] stake back
|
|
tb.addOutput({to:A.tokenaddr,amount:DUST,token:{amount:0n,category:rCat,nft:{capability:'mutable',commitment:receiptCommit('01','00',5000n)}}}); // [2] spent receipt
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(rcpt.satoshis)+t-5000n-DUST-MF});
|
|
st.refund.done=txidOf(await tb.send());st.refund.before=before.toString();save(st);
|
|
console.log(` refunded 5000 to A — ${st.refund.done}`);await sleep(10000);
|
|
} else console.log(` (refunded ${st.refund.done})`);
|
|
|
|
// ============ SWEEP ============
|
|
step('SWEEP — bet, attest, then sweep unclaimed remainder to sweepPkh (C)');
|
|
const tip2=await provider.getBlockHeight();
|
|
const {tote:sT,cat:sCat}=await setupPool('sweep',tip2-2,tip2-1);
|
|
await placeBet('sweep',sT,sCat,A,sig.a,'00',5000n);
|
|
await placeBet('sweep',sT,sCat,B,sig.b,'01',3000n);
|
|
if(!st.sweep.attest){
|
|
const pool=await poolUtxo(sT,sCat);
|
|
const tA=BigInt(st.sweep.tot.A),tB=BigInt(st.sweep.tot.B);const totalPool=tA+tB,totalWin=tA;
|
|
const {message,sig:oSig}=oracleSign(st.oracle.priv,hexToBin(st.sweep.event),0x00);
|
|
const newCommit=stateCommit('01','00',totalPool,totalWin,totalWin);
|
|
const {p,t}=await cBch(MF+1000n);
|
|
const tb=new TransactionBuilder({provider});
|
|
tb.setLocktime(await provider.getBlockHeight());
|
|
tb.addInput(pool,sT.unlock.attest(oSig,message));
|
|
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
|
|
tb.addOutput({to:sT.tokenAddress,amount:BigInt(pool.satoshis),token:{amount:0n,category:sCat,nft:{capability:'minting',commitment:newCommit}}});
|
|
tb.addOutput({to:C.cashaddr,amount:t-MF});
|
|
st.sweep.attest=txidOf(await tb.send());save(st);console.log(` attested — ${st.sweep.attest}`);await sleep(10000);
|
|
} else console.log(` (attested ${st.sweep.attest})`);
|
|
if(!st.sweep.done){
|
|
const pool=await poolUtxo(sT,sCat);
|
|
const before=await balance(C);
|
|
const {p,t}=await cBch(MF+1000n);
|
|
const tb=new TransactionBuilder({provider,allowImplicitFungibleTokenBurn:true});
|
|
tb.setLocktime(await provider.getBlockHeight());
|
|
tb.addInput(pool,sT.unlock.sweep());
|
|
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(pool.satoshis)}); // sweepPkh == C
|
|
tb.addOutput({to:C.cashaddr,amount:t-MF});
|
|
st.sweep.done=txidOf(await tb.send());st.sweep.swept=BigInt(pool.satoshis).toString();save(st);
|
|
console.log(` swept ${pool.satoshis} to C — ${st.sweep.done}`);await sleep(10000);
|
|
} else console.log(` (swept ${st.sweep.done})`);
|
|
|
|
step('VERIFY');
|
|
const aHasRefundSpent=!!(await findReceipt(A,rCat,'00',5000n,'01'));
|
|
const checks=[
|
|
['REFUND landed (A reclaimed stake)', !!st.refund.done],
|
|
['REFUND receipt marked claimed=1', aHasRefundSpent],
|
|
['SWEEP landed (remainder → sweepPkh)', !!st.sweep.done],
|
|
[`SWEEP drained ${st.sweep.swept} sats`, !!st.sweep.swept],
|
|
];
|
|
let ok=true;for(const [l,p] of checks){console.log(` [${p?'PASS':'FAIL'}] ${l}`);ok=ok&&p;}
|
|
step('SUMMARY — Tier-2 refund + sweep backstops');
|
|
console.log(` refund: ${st.refund.done}\n sweep: ${st.sweep.attest} / ${st.sweep.done}`);
|
|
divider();
|
|
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);});
|