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>
180 lines
16 KiB
JavaScript
180 lines
16 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-tote2-mba-rake-test.mjs — MBA-denominated Tier-2 tote WITH operator rake
|
|
* (closes the deferred validation from snapshot 0052). Same 2-UTXO state-NFT +
|
|
* MBA vault model as chipnet-tote2-mba-test.mjs, but rakeBps>0 so attest() pays a
|
|
* peg-correct MBA FT rake to the operator and winners split the net pool.
|
|
*
|
|
* rakeBps=1000 (10%). Bettors: A=20, B=20 (side 0); D=10 (side 1). Operator = C.
|
|
* grossPool=50 -> rake=5 MBA to C, net=45. Oracle outcome 0 -> side 0 wins:
|
|
* A = floor(20*45/40) = 22; B last drains the vault remainder (24 = its 22 + the
|
|
* 1-unit seed + residual). D loses. Operator collects 5 MBA regardless.
|
|
*
|
|
* Roles: founder/operator(rakePkh)/sweepPkh/fees = C. Resumable state JSON.
|
|
*/
|
|
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-mba-rake-test.json');
|
|
const MBA_ARTIFACT = join(ROOT, 'contracts/dist/DeviantOpenMintFT.json');
|
|
const TOTE_ARTIFACT = join(ROOT, 'contracts/dist/ParimutuelToteOpenMBA.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, SEED = 1n, DUST = 1000n, MF = 6000n, RAKE_BPS = 1000n;
|
|
|
|
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 u64leToBig(hex){let v=0n;for(let i=0;i<8;i++){v|=BigInt(parseInt(hex.slice(i*2,i*2+2),16))<<BigInt(8*i);}return v;}
|
|
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 mbaArt=JSON.parse(readFileSync(MBA_ARTIFACT,'utf8'));
|
|
const art=JSON.parse(readFileSync(TOTE_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 D=await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.d,'utf8')).wif);
|
|
const provider=new ElectrumNetworkProvider('chipnet');
|
|
const sig={a:new SignatureTemplate(A.privateKey),b:new SignatureTemplate(B.privateKey),c:new SignatureTemplate(C.privateKey),d:new SignatureTemplate(D.privateKey)};
|
|
const flbC=p2pkh(C.publicKeyHash);
|
|
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);
|
|
console.log(`Oracle ${st.oracle.pub} | operator(rakePkh)=C | rakeBps ${RAKE_BPS}`);
|
|
|
|
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);}
|
|
|
|
step('Deploy MBA (rate 1000)');
|
|
if(!st.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)} — ${cat}`);st.mba=cat;save(st);await sleep(10000);}
|
|
const mbaCat=st.mba;
|
|
const mba=new Contract(mbaArt,[txidLE(mbaCat),RATE,FEE,flbC],{provider});
|
|
async function mint(who,s,amount,label){
|
|
const ru=await provider.getUtxos(mba.tokenAddress);const res=ru.find(x=>x.token?.nft?.capability==='minting'&&x.token?.category===mbaCat&&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}: low BCH`);
|
|
const tb=new TransactionBuilder({provider});tb.addInput(res,mba.unlock.claim(amount,p2pkh(who.publicKeyHash)));for(const x of p)tb.addInput(x,s.unlockP2PKH());
|
|
tb.addOutput({to:mba.tokenAddress,amount:1000n,token:{amount:res.token.amount-amount,category:mbaCat,nft:{capability:'minting',commitment:''}}});
|
|
tb.addOutput({to:who.tokenaddr,amount:gross,token:{amount,category:mbaCat}});tb.addOutput({to:who.cashaddr,amount:BigInt(res.satoshis)+t-1000n-gross-MF});
|
|
console.log(` ${label}: ${amount} MBA — ${txidOf(await tb.send())}`);await sleep(10000);
|
|
}
|
|
st.mints=st.mints||{};
|
|
const mints=[['a',A,sig.a,20n],['b',B,sig.b,20n],['d',D,sig.d,10n],['seed',C,sig.c,1n]];
|
|
step('Mint MBA stakes A20 B20 D10 + 1 seed');
|
|
for(const [k,w,s,amt] of mints){ if(!st.mints[k]){await mint(w,s,amt,`mint ${k}`);st.mints[k]=true;save(st);} else console.log(` ${k} minted`); }
|
|
async function findMba(who,amt){return (await provider.getUtxos(who.cashaddr)).find(x=>x.token?.category===mbaCat&&!x.token?.nft&&BigInt(x.token?.amount)===amt);}
|
|
async function findMbaFt(who,amt){return (await provider.getUtxos(who.cashaddr)).find(x=>x.token?.category===mbaCat&&!x.token?.nft&&BigInt(x.token?.amount)===amt);}
|
|
|
|
step('Genesis tote NFT + deploy MBA rake covenant');
|
|
const PHASE0=stateCommit('00','00',0,0,0);
|
|
if(!st.tote){const u=await eligible(C);const cat=u.txid;const r=await C.tokenGenesis({cashaddr:C.tokenaddr,amount:0n,value:DUST,nft:{capability:'minting',commitment:PHASE0}},[],{utxoIds:[u]});console.log(` tote genesis ${txidOf(r)} — ${cat}`);st.tote=cat;save(st);await sleep(10000);}
|
|
const toteCat=st.tote;
|
|
if(!st.deadlines){const tip=await provider.getBlockHeight();st.deadlines={bet:tip,sweep:tip+500000};st.eventId=binToHex(randomBytes(16));save(st);}
|
|
const ev=hexToBin(st.eventId);
|
|
const tote=new Contract(art,[txidLE(toteCat),txidLE(mbaCat),RATE,oraclePub,ev,BigInt(st.deadlines.bet),BigInt(st.deadlines.sweep),C.publicKeyHash,RAKE_BPS,C.publicKeyHash],{provider});
|
|
console.log(` contract ${tote.tokenAddress}`);
|
|
async function stateUtxo(){return (await provider.getUtxos(tote.tokenAddress)).find(x=>x.token?.category===toteCat&&x.token?.nft?.capability==='minting');}
|
|
async function vaultUtxo(){return (await provider.getUtxos(tote.tokenAddress)).find(x=>x.token?.category===mbaCat&&!x.token?.nft);}
|
|
|
|
step('Setup — state NFT + 1-unit MBA vault into the covenant');
|
|
if(!st.setup){
|
|
const g=(await provider.getUtxos(C.cashaddr)).find(x=>x.token?.category===toteCat&&x.token?.nft?.capability==='minting');const seedU=await findMba(C,SEED);if(!g||!seedU)throw new Error('setup: missing state/seed');
|
|
const {p,t}=await cBch(MF+DUST+1000n);const tb=new TransactionBuilder({provider});tb.addInput(g,sig.c.unlockP2PKH());tb.addInput(seedU,sig.c.unlockP2PKH());for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
|
|
tb.addOutput({to:tote.tokenAddress,amount:DUST,token:{amount:0n,category:toteCat,nft:{capability:'minting',commitment:PHASE0}}});
|
|
tb.addOutput({to:tote.tokenAddress,amount:SEED*RATE,token:{amount:SEED,category:mbaCat}});
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(g.satoshis)+BigInt(seedU.satoshis)+t-DUST-SEED*RATE-MF});
|
|
st.setup=txidOf(await tb.send());save(st);console.log(` setup — ${st.setup}`);await sleep(10000);
|
|
} else console.log(` (setup ${st.setup})`);
|
|
|
|
const bets=[['a',A,sig.a,'00',20n],['b',B,sig.b,'00',20n],['d',D,sig.d,'01',10n]];
|
|
step('BET — A,B side0; D side1');
|
|
st.bets=st.bets||{};st.tot=st.tot||{A:'0',B:'0'};
|
|
for(const [k,w,s,sideHex,stake] of bets){
|
|
if(st.bets[k]){console.log(` ${k} bet`);continue;}
|
|
const sUtxo=await stateUtxo(),vUtxo=await vaultUtxo();const stakeU=await findMba(w,stake);if(!sUtxo||!vUtxo||!stakeU)throw new Error(`bet ${k}: missing`);
|
|
let tA=BigInt(st.tot.A),tB=BigInt(st.tot.B);if(sideHex==='00')tA+=stake;else tB+=stake;
|
|
const newCommit=stateCommit('00','00',tA,tB,0);const newPool=BigInt(vUtxo.token.amount)+stake;
|
|
const {p,t}=await cBch(MF+DUST+1000n);const tb=new TransactionBuilder({provider});
|
|
tb.addInput(sUtxo,tote.unlock.bet(stake,hexToBin(sideHex)));tb.addInput(vUtxo,tote.unlock.bet(stake,hexToBin(sideHex)));tb.addInput(stakeU,s.unlockP2PKH());for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
|
|
tb.addOutput({to:tote.tokenAddress,amount:DUST,token:{amount:0n,category:toteCat,nft:{capability:'minting',commitment:newCommit}}});
|
|
tb.addOutput({to:tote.tokenAddress,amount:newPool*RATE,token:{amount:newPool,category:mbaCat}});
|
|
tb.addOutput({to:w.tokenaddr,amount:DUST,token:{amount:0n,category:toteCat,nft:{capability:'mutable',commitment:receiptCommit('00',sideHex,stake)}}});
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(sUtxo.satoshis)+BigInt(stakeU.satoshis)+BigInt(vUtxo.satoshis)+t-DUST-DUST-newPool*RATE-MF});
|
|
console.log(` ${k} bets ${stake} side ${sideHex} — ${txidOf(await tb.send())}`);st.bets[k]=true;st.tot={A:tA.toString(),B:tB.toString()};save(st);await sleep(10000);
|
|
}
|
|
const grossPool=BigInt(st.tot.A)+BigInt(st.tot.B);const rake=grossPool*RAKE_BPS/10000n,netPool=grossPool-rake,totalWin=BigInt(st.tot.A);
|
|
|
|
step(`ATTEST + RAKE — gross=${grossPool}, rake=${rake} MBA -> C, net=${netPool}`);
|
|
if(!st.attest){
|
|
const sUtxo=await stateUtxo(),vUtxo=await vaultUtxo();const {message,sig:oSig}=oracleSign(st.oracle.priv,ev,0x00);
|
|
const newCommit=stateCommit('01','00',netPool,totalWin,totalWin);const inVault=BigInt(vUtxo.token.amount);
|
|
const {p,t}=await cBch(MF+1000n);const tb=new TransactionBuilder({provider});tb.setLocktime(Number(st.deadlines.bet));
|
|
tb.addInput(sUtxo,tote.unlock.attest(oSig,message));tb.addInput(vUtxo,tote.unlock.attest(oSig,message));for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
|
|
tb.addOutput({to:tote.tokenAddress,amount:BigInt(sUtxo.satoshis),token:{amount:0n,category:toteCat,nft:{capability:'minting',commitment:newCommit}}}); // [0] state
|
|
tb.addOutput({to:tote.tokenAddress,amount:(inVault-rake)*RATE,token:{amount:inVault-rake,category:mbaCat}}); // [1] vault - rake
|
|
tb.addOutput({to:C.tokenaddr,amount:rake*RATE,token:{amount:rake,category:mbaCat}}); // [2] rake MBA -> operator
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(vUtxo.satoshis)-(inVault-rake)*RATE-rake*RATE+t-MF});
|
|
st.attest=txidOf(await tb.send());save(st);console.log(` attested + rake ${rake} MBA — ${st.attest}`);await sleep(10000);
|
|
} else console.log(` (attest ${st.attest})`);
|
|
|
|
async function findReceipt(w,sideHex,stake){const rc=receiptCommit('00',sideHex,stake);return (await provider.getUtxos(w.cashaddr)).find(x=>x.token?.category===toteCat&&x.token?.nft?.capability==='mutable'&&x.token?.nft?.commitment===rc);}
|
|
async function notLast(w,s,sideHex,stake){
|
|
const sUtxo=await stateUtxo(),vUtxo=await vaultUtxo();const rcpt=await findReceipt(w,sideHex,stake);if(!rcpt)throw new Error('claim: receipt missing');
|
|
const rwsNow=u64leToBig(sUtxo.token.nft.commitment.slice(36,52));const payout=stake*netPool/totalWin;const newCommit=stateCommit('01','00',netPool,totalWin,rwsNow-stake);const poolIn=BigInt(vUtxo.token.amount);
|
|
const {p,t}=await cBch(MF+DUST+1000n);const tb=new TransactionBuilder({provider});
|
|
tb.addInput(sUtxo,tote.unlock.claim(p2pkh(w.publicKeyHash)));tb.addInput(vUtxo,tote.unlock.claim(p2pkh(w.publicKeyHash)));tb.addInput(rcpt,s.unlockP2PKH());for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
|
|
tb.addOutput({to:tote.tokenAddress,amount:BigInt(sUtxo.satoshis),token:{amount:0n,category:toteCat,nft:{capability:'minting',commitment:newCommit}}});
|
|
tb.addOutput({to:tote.tokenAddress,amount:(poolIn-payout)*RATE,token:{amount:poolIn-payout,category:mbaCat}});
|
|
tb.addOutput({to:w.tokenaddr,amount:payout*RATE,token:{amount:payout,category:mbaCat}});
|
|
tb.addOutput({to:w.tokenaddr,amount:DUST,token:{amount:0n,category:toteCat,nft:{capability:'mutable',commitment:receiptCommit('01',sideHex,stake)}}});
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(vUtxo.satoshis)+BigInt(rcpt.satoshis)+t-(poolIn-payout)*RATE-payout*RATE-DUST-MF});
|
|
return {txid:txidOf(await tb.send()),payout};
|
|
}
|
|
async function last(w,s,sideHex,stake){
|
|
const sUtxo=await stateUtxo(),vUtxo=await vaultUtxo();const rcpt=await findReceipt(w,sideHex,stake);if(!rcpt)throw new Error('last: receipt missing');const poolIn=BigInt(vUtxo.token.amount);
|
|
const {p,t}=await cBch(MF+DUST+1000n);const tb=new TransactionBuilder({provider,allowImplicitFungibleTokenBurn:true});
|
|
tb.addInput(sUtxo,tote.unlock.claim(p2pkh(w.publicKeyHash)));tb.addInput(vUtxo,tote.unlock.claim(p2pkh(w.publicKeyHash)));tb.addInput(rcpt,s.unlockP2PKH());for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
|
|
tb.addOutput({to:w.tokenaddr,amount:poolIn*RATE,token:{amount:poolIn,category:mbaCat}});
|
|
tb.addOutput({to:w.tokenaddr,amount:DUST,token:{amount:0n,category:toteCat,nft:{capability:'mutable',commitment:receiptCommit('01',sideHex,stake)}}});
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(sUtxo.satoshis)+BigInt(vUtxo.satoshis)+BigInt(rcpt.satoshis)+t-poolIn*RATE-DUST-MF});
|
|
return {txid:txidOf(await tb.send()),drained:poolIn};
|
|
}
|
|
|
|
step('CLAIM A — floor 20*45/40 = 22 on NET pool');
|
|
if(!st.claimA){const r=await notLast(A,sig.a,'00',20n);st.claimA=r.txid;st.payA=r.payout.toString();save(st);console.log(` A=${r.payout} — ${r.txid}`);await sleep(10000);} else console.log(` (A ${st.claimA})`);
|
|
step('CLAIM B (last) — drains net remainder + seed');
|
|
if(!st.claimB){const r=await last(B,sig.b,'00',20n);st.claimB=r.txid;st.drainedB=r.drained.toString();save(st);console.log(` B drained ${r.drained} — ${r.txid}`);await sleep(10000);} else console.log(` (B ${st.claimB})`);
|
|
|
|
step('VERIFY');
|
|
const rakeUtxo=await findMbaFt(C,rake);
|
|
const checks=[
|
|
[`operator rake ${rake} MBA paid to C (peg FT)`, !!rakeUtxo],
|
|
[`A pro-rata ${st.payA} on NET pool (=22)`, st.payA==='22'],
|
|
[`B last-claim drained ${st.drainedB}`, !!st.claimB],
|
|
['state NFT + vault consumed', !(await stateUtxo())&&!(await vaultUtxo())],
|
|
];
|
|
let ok=true;for(const [l,p] of checks){console.log(` [${p?'PASS':'FAIL'}] ${l}`);ok=ok&&p;}
|
|
step('SUMMARY — MBA Tier-2 tote with operator rake (peg FT)');
|
|
console.log(` tote ${toteCat}\n mba ${mbaCat}\n attest+rake ${st.attest}\n claimA/claimB ${st.claimA} / ${st.claimB}`);
|
|
console.log(` Rake ${rake} MBA (peg-correct FT, ${RAKE_BPS} bps) off the top at attest; winners split net ${netPool}.`);
|
|
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);});
|