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>
207 lines
12 KiB
JavaScript
207 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-tote4-test.mjs — 4-party pari-mutuel tote on chipnet (ADR-0024).
|
|
*
|
|
* Extends the 3-bettor ParimutuelTote validation (snapshot 0045) to FOUR
|
|
* bettors with two on each side, so the winning side ALWAYS splits the pool
|
|
* pro-rata between two winners — proving 2-winner pro-rata on either outcome:
|
|
*
|
|
* TOTE4 (Family C, ParimutuelTote4) — A(30) + B(10) back side 0; C(20) + D(10)
|
|
* back side 1. Pool = 70 FT. Oracle signs outcome 0 → side 0 splits the pool
|
|
* pro-rata by stake: A = 30*70/40 = 52 FT (floor of 52.5), B = remainder = 18
|
|
* FT. C and D lose their stake. (Sum 52+18 = 70 → no FT lost to rounding.)
|
|
*
|
|
* The result-oracle signs {eventId(16) ++ outcome(1)} off-chain; the covenant
|
|
* verifies via checkDataSig and computes the payout — no party declares the
|
|
* winner. If the resolution tx confirms, the covenant's pro-rata outputs matched
|
|
* exactly: that IS the proof.
|
|
*
|
|
* Roles: founder = C; bettors = A, B, C, D. Miner fees from C. Resumable via
|
|
* state (.chipnet-tote4-test.json). Requires .chipnet-keys{,-b,-c,-d}.json,
|
|
* each funded on chipnet (run chipnet:setup{,-b,-c,-d} + faucet first).
|
|
*/
|
|
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-tote4-test.json');
|
|
const MBA_ARTIFACT = join(ROOT, 'contracts/dist/DeviantOpenMintFT.json');
|
|
const TOTE_ARTIFACT = join(ROOT, 'contracts/dist/ParimutuelTote4.json');
|
|
const KEYS = {
|
|
a: join(ROOT, '.chipnet-keys.json'),
|
|
b: join(ROOT, '.chipnet-keys-b.json'),
|
|
c: join(ROOT, '.chipnet-keys-c.json'),
|
|
d: join(ROOT, '.chipnet-keys-d.json'),
|
|
};
|
|
|
|
const RESERVOIR = 1_000_000_000_000_000_000n;
|
|
const RATE = 1000n, FEE = 0n;
|
|
const FAR = 9_999_999n; // far-future deadline → resolves via oracle, not refund
|
|
const MF = 6000n; // miner-fee headroom
|
|
|
|
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;}
|
|
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(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};}
|
|
function txidOf(sent){return typeof sent==='string'?sent:sent?.txid??sent?.txId;}
|
|
|
|
// Result-oracle: sign {eventId ++ outcome} the way OP_CHECKDATASIG expects —
|
|
// an ECDSA DER signature over sha256(message), no sighash byte.
|
|
function oracleSign(privHex, eventIdBin, outcomeByte){
|
|
const message = new Uint8Array([...eventIdBin, outcomeByte]);
|
|
const sig = secp256k1.signMessageHashDER(hexToBin(privHex), sha256.hash(message));
|
|
if (typeof sig === 'string') throw new Error(`oracle sign failed: ${sig}`);
|
|
return { message, sig };
|
|
}
|
|
|
|
async function main(){
|
|
const mbaArt=JSON.parse(readFileSync(MBA_ARTIFACT,'utf8'));
|
|
const toteArt=JSON.parse(readFileSync(TOTE_ARTIFACT,'utf8'));
|
|
for(const [k,f] of Object.entries(KEYS)) if(!existsSync(f)) throw new Error(`missing key file for wallet ${k.toUpperCase()}: ${f} (run chipnet:setup-${k==='a'?'':k} + faucet)`);
|
|
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 D=await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.d,'utf8')).wif);
|
|
const provider=new ElectrumNetworkProvider('chipnet');
|
|
const sigA=new SignatureTemplate(A.privateKey), sigB=new SignatureTemplate(B.privateKey);
|
|
const sigC=new SignatureTemplate(C.privateKey), sigD=new SignatureTemplate(D.privateKey);
|
|
const flbC=p2pkh(C.publicKeyHash);
|
|
let state=load();
|
|
|
|
// ---- Result-oracle key (generated once, reused) ----
|
|
if(!state.oracle){
|
|
let priv; do { priv=randomBytes(32); } while (typeof secp256k1.derivePublicKeyCompressed(priv)==='string');
|
|
const pub=secp256k1.derivePublicKeyCompressed(priv);
|
|
state.oracle={priv:binToHex(priv),pub:binToHex(pub)};save(state);
|
|
}
|
|
const oraclePub=hexToBin(state.oracle.pub);
|
|
console.log(`Result-oracle pubkey: ${state.oracle.pub}`);
|
|
console.log(`Bettors: A=${A.cashaddr}\n B=${B.cashaddr}\n C=${C.cashaddr}\n D=${D.cashaddr}`);
|
|
|
|
// ---- MBA genesis (founder C) ----
|
|
step('Deploy MBA (canonical DeviantOpenMintFT, rate 1000, fee 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];
|
|
}
|
|
if(!state.mba){
|
|
const u=await eligible(C);const cat=u.txid;
|
|
const c=new Contract(mbaArt,[txidLE(cat),RATE,FEE,flbC],{provider});
|
|
const r=await C.tokenGenesis({cashaddr:c.tokenAddress,amount:RESERVOIR,value:1000n,nft:{capability:'minting',commitment:''}},[],{utxoIds:[u]});
|
|
console.log(` MBA genesis ${txidOf(r)} — category ${cat}`);
|
|
state.mba={categoryId:cat};save(state);await sleep(10000);
|
|
} else console.log(` MBA already: ${state.mba.categoryId}`);
|
|
const cat=state.mba.categoryId;
|
|
const mba=new Contract(mbaArt,[txidLE(cat),RATE,FEE,flbC],{provider});
|
|
|
|
// claim exactly `amount` FT into `who` as its own UTXO
|
|
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===cat&&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 {p,t}=pickBch(bch,gross+MF+1000n);
|
|
if(t<gross+MF+1000n)throw new Error(`${label}: insufficient BCH ${t} < ${gross+MF+1000n}`);
|
|
const change=BigInt(res.satoshis)+t-1000n-gross-MF;
|
|
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:cat,nft:{capability:'minting',commitment:''}}});
|
|
tb.addOutput({to:who.tokenaddr,amount:gross,token:{amount,category:cat}});
|
|
tb.addOutput({to:who.cashaddr,amount:change});
|
|
console.log(` ${label}: minted ${amount} FT — ${txidOf(await tb.send())}`);await sleep(10000);
|
|
}
|
|
state.mints=state.mints||{};
|
|
const mints=[['a30',A,sigA,30n],['b10',B,sigB,10n],['c20',C,sigC,20n],['d10',D,sigD,10n]];
|
|
step('Mint stakes to A(30), B(10), C(20), D(10)');
|
|
for(const [k,w,s,amt] of mints){ if(!state.mints[k]){await mint(w,s,amt,`mint ${k}`);state.mints[k]=true;save(state);} else console.log(` ${k} already minted`); }
|
|
|
|
// find a FT UTXO held by `who` with EXACTLY `amount` of our category
|
|
async function findExact(who,amount){
|
|
const u=await provider.getUtxos(who.cashaddr);
|
|
return u.find(x=>x.token?.category===cat&&!x.token?.nft&&BigInt(x.token?.amount)===amount);
|
|
}
|
|
async function cBchFee(need){
|
|
const cBch=(await provider.getUtxos(C.cashaddr)).filter(x=>!x.token).sort((a,b)=>Number(b.satoshis)-Number(a.satoshis));
|
|
return pickBch(cBch,need);
|
|
}
|
|
async function ftAt(who,amount){ // verify: who holds a UTXO of exactly `amount` FT of our category
|
|
const u=await provider.getUtxos(who.cashaddr);
|
|
return u.some(x=>x.token?.category===cat&&!x.token?.nft&&BigInt(x.token?.amount)===amount);
|
|
}
|
|
|
|
// ================= TOTE4 (Family C, 4 bettors) =================
|
|
step('TOTE4 — A(30)+B(10) side0, C(20)+D(10) side1; oracle says outcome 0 → A=52, B=18 pro-rata');
|
|
state.tote=state.tote||{};
|
|
if(!state.tote.eventId){state.tote.eventId=binToHex(randomBytes(16));save(state);}
|
|
const toteEvent=hexToBin(state.tote.eventId);
|
|
const tote=new Contract(toteArt,[txidLE(cat),RATE,oraclePub,toteEvent,FAR,
|
|
A.publicKeyHash,30n,B.publicKeyHash,10n,C.publicKeyHash,20n,D.publicKeyHash,10n],{provider});
|
|
console.log(` contract: ${tote.tokenAddress}`);
|
|
if(!state.tote.fund){
|
|
const a30=await findExact(A,30n), b10=await findExact(B,10n), c20=await findExact(C,20n), d10=await findExact(D,10n);
|
|
if(!a30||!b10||!c20||!d10)throw new Error(`tote4: missing stake (A30=${!!a30} B10=${!!b10} C20=${!!c20} D10=${!!d10})`);
|
|
const {p,t}=await cBchFee(MF+2000n);
|
|
const tb=new TransactionBuilder({provider});
|
|
tb.addInput(a30,sigA.unlockP2PKH());
|
|
tb.addInput(b10,sigB.unlockP2PKH());
|
|
tb.addInput(c20,sigC.unlockP2PKH());
|
|
tb.addInput(d10,sigD.unlockP2PKH());
|
|
for(const x of p)tb.addInput(x,sigC.unlockP2PKH());
|
|
tb.addOutput({to:tote.tokenAddress,amount:70000n,token:{amount:70n,category:cat}}); // pool 70 FT
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(a30.satoshis)+BigInt(b10.satoshis)+BigInt(c20.satoshis)+BigInt(d10.satoshis)+t-70000n-MF});
|
|
state.tote.fund=txidOf(await tb.send());save(state);
|
|
console.log(` funded pool 70 FT / 70000 sat — ${state.tote.fund}`);await sleep(10000);
|
|
} else console.log(` (funded: ${state.tote.fund})`);
|
|
if(!state.tote.resolve){
|
|
const pu=(await provider.getUtxos(tote.tokenAddress)).find(x=>x.token?.category===cat&&!x.token?.nft&&BigInt(x.token?.amount)===70n);
|
|
if(!pu)throw new Error('tote4: pool UTXO not found');
|
|
const {message,sig}=oracleSign(state.oracle.priv,toteEvent,0x00);
|
|
const {p,t}=await cBchFee(MF+1000n);
|
|
const tb=new TransactionBuilder({provider});
|
|
tb.addInput(pu,tote.unlock.resolveOracle(sig,message));
|
|
for(const x of p)tb.addInput(x,sigC.unlockP2PKH());
|
|
tb.addOutput({to:A.tokenaddr,amount:52000n,token:{amount:52n,category:cat}}); // A pro-rata 30*70/40 = 52
|
|
tb.addOutput({to:B.tokenaddr,amount:18000n,token:{amount:18n,category:cat}}); // B remainder = 18
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(pu.satoshis)+t-52000n-18000n-MF});
|
|
state.tote.resolve=txidOf(await tb.send());save(state);
|
|
console.log(` oracle(outcome 0) → A=52, B=18 (C=0, D=0) — ${state.tote.resolve}`);await sleep(10000);
|
|
} else console.log(` (resolved: ${state.tote.resolve})`);
|
|
|
|
// ================= VERIFY =================
|
|
step('VERIFY — confirm winners hold the exact pro-rata amounts');
|
|
const checks=[
|
|
['TOTE4 A holds 52 FT (30*70/40 floor)', await ftAt(A,52n)],
|
|
['TOTE4 B holds 18 FT (remainder)', await ftAt(B,18n)],
|
|
];
|
|
let ok=true;
|
|
for(const [label,pass] of checks){console.log(` [${pass?'PASS':'FAIL'}] ${label}`);ok=ok&&pass;}
|
|
|
|
step('SUMMARY — 4-party pari-mutuel tote (ADR-0024)');
|
|
console.log(` MBA category: ${cat}`);
|
|
console.log(` Oracle pubkey: ${state.oracle.pub}`);
|
|
console.log(` TOTE4 fund/resolve: ${state.tote.fund} / ${state.tote.resolve}`);
|
|
console.log('');
|
|
console.log(' 4-bettor covenant-enforced pari-mutuel pro-rata (two winners on the');
|
|
console.log(' winning side) validated on live chipnet consensus.');
|
|
divider();
|
|
if(!ok){console.error('VERIFY FAILED');process.exit(1);}
|
|
process.exit(0); // mainnet-js holds wss sockets open; exit explicitly so the run ends
|
|
}
|
|
|
|
main().catch((e)=>{console.error('FAILED:',e?.message??e);if(e?.stack)console.error(e.stack);process.exit(1);});
|