deviant/scripts/chipnet-clob-mba-test.mjs
walerikus 599202a721 chore: checkpoint working tree — shell Tier-B retirement + accumulated WIP
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>
2026-06-17 01:01:37 +02:00

264 lines
18 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-clob-mba-test.mjs — Family D (ADR-0030) config 2: an MBA-COLLATERALIZED
* binary share market ("split / merge", Polymarket model). The MBA sibling of
* chipnet-clob-bch-test.mjs — collateral is a backed Deviant asset, held in a
* co-spent peg-enforced vault (mirrors chipnet-tote2-mba-test.mjs). NFT shares.
*
* MBA = canonical DeviantOpenMintFT, rate 1000 (finely denominated, ADR-0025).
* 1. MBA genesis + mint C 20,10,1(seed)
* 2. genesis share category; relocate state minting NFT + 1-unit MBA vault seed
* 3. split 20 MBA -> YES(20)+NO(20) ; split 10 -> YES(10)+NO(10) [vault 1->21->31]
* 4. merge the 10 pair -> 10 MBA released [vault 31->21]
* 5. TRADE: C transfers YES(20) to B
* 6. attest outcome 0 (YES wins)
* 7. NEG: C redeems NO(20) (loser) -> covenant MUST REJECT on-chain
* 8. redeem: B redeems YES(20) -> 20 MBA ; vault drains to seed (1)
*
* Roles: C = founder/operator/funder/oracle owner; B = the share buyer. Resumable.
*/
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-clob-mba-test.json');
const MBA_ARTIFACT = join(ROOT, 'contracts/dist/DeviantOpenMintFT.json');
const ARTIFACT = join(ROOT, 'contracts/dist/BinaryShareVaultMBA.json');
const KEYS = { 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, FEE = 0n;
const SEED = 1n; // 1-unit MBA vault seed (keeps the FT > 0; the stranded wart)
const DUST = 1000n; // NFT-UTXO dust
const 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);}
const yesCommit=(amt)=>'00'+u64le(amt);
const noCommit =(amt)=>'01'+u64le(amt);
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};}
const isReject=(e)=>/BinaryShareVaultMBA\.cash|Require statement failed|mandatory-script|failed to verify|Verify/i.test((e?.message??e).toString());
async function main(){
const mbaArt=JSON.parse(readFileSync(MBA_ARTIFACT,'utf8'));
const art=JSON.parse(readFileSync(ARTIFACT,'utf8'));
for(const [k,f] of Object.entries(KEYS)) if(!existsSync(f)) throw new Error(`missing key ${k}`);
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={b:new SignatureTemplate(B.privateKey),c:new SignatureTemplate(C.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}`);
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));const {p,t}=pickBch(u,need);if(t<need)throw new Error(`C low BCH ${t}<${need}`);return {p,t};}
// ---- MBA genesis + mint (founder C) ----
step('Deploy MBA (DeviantOpenMintFT, 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)} — category ${cat}`);st.mba=cat;save(st);await sleep(10000);
} else console.log(` MBA ${st.mba}`);
const mbaCat=st.mba;
const mba=new Contract(mbaArt,[txidLE(mbaCat),RATE,FEE,flbC],{provider});
async function mint(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 {p,t}=await cBch(gross+MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(res,mba.unlock.claim(amount,flbC));
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
tb.addOutput({to:mba.tokenAddress,amount:1000n,token:{amount:res.token.amount-amount,category:mbaCat,nft:{capability:'minting',commitment:''}}});
tb.addOutput({to:C.tokenaddr,amount:gross,token:{amount,category:mbaCat}});
tb.addOutput({to:C.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=[['m20',20n],['m10',10n],['seed',1n]];
step('Mint MBA: 20 + 10 (collateral) + 1 (seed)');
for(const [k,amt] of mints){ if(!st.mints[k]){await mint(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 mbaBal(who){return (await provider.getUtxos(who.cashaddr)).filter(x=>x.token?.category===mbaCat&&!x.token?.nft).reduce((s,x)=>s+BigInt(x.token.amount),0n);}
// ---- Genesis the share NFT category ----
step('Genesis share NFT category (vault minting NFT)');
const OPEN='0000';
if(!st.cat){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:OPEN}},[],{utxoIds:[u]});console.log(` share genesis ${txidOf(r)} — ${cat}`);st.cat=cat;save(st);await sleep(10000);} else console.log(` share cat ${st.cat}`);
const cat=st.cat;
if(!st.deadlines){const tip=await provider.getBlockHeight();st.deadlines={attest:tip};st.eventId=binToHex(randomBytes(16));save(st);}
const ev=hexToBin(st.eventId);
const vault=new Contract(art,[txidLE(cat),txidLE(mbaCat),RATE,oraclePub,ev,BigInt(st.deadlines.attest)],{provider});
console.log(` vault ${vault.tokenAddress} (attestDeadline ${st.deadlines.attest})`);
async function stateUtxo(){return (await provider.getUtxos(vault.tokenAddress)).find(x=>x.token?.category===cat&&x.token?.nft?.capability==='minting');}
async function vaultUtxo(){return (await provider.getUtxos(vault.tokenAddress)).find(x=>x.token?.category===mbaCat&&!x.token?.nft);}
async function findShare(w,commit){return (await provider.getUtxos(w.cashaddr)).find(x=>x.token?.category===cat&&x.token?.nft?.capability==='none'&&x.token?.nft?.commitment===commit);}
// ---- Setup: relocate state minting NFT + 1-unit MBA vault seed ----
step('Setup — state NFT + 1-unit MBA vault seed into the covenant');
if(!st.setup){
const g=(await provider.getUtxos(C.cashaddr)).find(x=>x.token?.category===cat&&x.token?.nft?.capability==='minting');
const seedU=await findMba(C,SEED);
if(!g||!seedU)throw new Error(`setup: state=${!!g} seed=${!!seedU}`);
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:vault.tokenAddress,amount:DUST,token:{amount:0n,category:cat,nft:{capability:'minting',commitment:OPEN}}}); // [0] state
tb.addOutput({to:vault.tokenAddress,amount:SEED*RATE,token:{amount:SEED,category:mbaCat}}); // [1] vault seed
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})`);
// ---- SPLIT: lock `amount` MBA; mint YES(amount)+NO(amount) to C ----
async function split(amount){
const sU=await stateUtxo(), vU=await vaultUtxo();if(!sU||!vU)throw new Error('split: state/vault missing');
const stakeU=await findMba(C,amount);if(!stakeU)throw new Error(`split: ${amount} MBA UTXO missing`);
const newPool=BigInt(vU.token.amount)+amount;
const {p,t}=await cBch(2n*DUST+MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(sU,vault.unlock.split(amount)); // [0] state
tb.addInput(vU,vault.unlock.split(amount)); // [1] vault
tb.addInput(stakeU,sig.c.unlockP2PKH()); // [2] C collateral MBA
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
tb.addOutput({to:vault.tokenAddress,amount:BigInt(sU.satoshis),token:{amount:0n,category:cat,nft:{capability:'minting',commitment:OPEN}}}); // [0] state
tb.addOutput({to:vault.tokenAddress,amount:newPool*RATE,token:{amount:newPool,category:mbaCat}}); // [1] vault
tb.addOutput({to:C.tokenaddr,amount:DUST,token:{amount:0n,category:cat,nft:{capability:'none',commitment:yesCommit(amount)}}}); // [2] YES
tb.addOutput({to:C.tokenaddr,amount:DUST,token:{amount:0n,category:cat,nft:{capability:'none',commitment:noCommit(amount)}}}); // [3] NO
tb.addOutput({to:C.cashaddr,amount:BigInt(sU.satoshis)+BigInt(vU.satoshis)+BigInt(stakeU.satoshis)+t-BigInt(sU.satoshis)-newPool*RATE-2n*DUST-MF}); // [4] change
return txidOf(await tb.send());
}
step('SPLIT 20 -> YES(20)+NO(20)');
if(!st.split1){st.split1=await split(20n);save(st);console.log(` ${st.split1}`);await sleep(10000);} else console.log(` (split1 ${st.split1})`);
step('SPLIT 10 -> YES(10)+NO(10) [vault -> seed+30]');
if(!st.split2){st.split2=await split(10n);save(st);console.log(` ${st.split2}`);await sleep(10000);} else console.log(` (split2 ${st.split2})`);
// ---- MERGE the 10 pair: burn YES(10)+NO(10), release 10 MBA ----
step('MERGE the 10 pair -> 10 MBA released [vault -> seed+20]');
if(!st.merge){
const sU=await stateUtxo(), vU=await vaultUtxo();
const yes=await findShare(C,yesCommit(10n)),no=await findShare(C,noCommit(10n));
if(!yes||!no)throw new Error('merge: 10 pair missing at C');
const newPool=BigInt(vU.token.amount)-10n;
const {p,t}=await cBch(MF+1000n);
const tb=new TransactionBuilder({provider,allowImplicitFungibleTokenBurn:true});
tb.addInput(sU,vault.unlock.merge(flbC)); // [0] state
tb.addInput(vU,vault.unlock.merge(flbC)); // [1] vault
tb.addInput(yes,sig.c.unlockP2PKH()); // [2] YES(10)
tb.addInput(no,sig.c.unlockP2PKH()); // [3] NO(10)
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
tb.addOutput({to:vault.tokenAddress,amount:BigInt(sU.satoshis),token:{amount:0n,category:cat,nft:{capability:'minting',commitment:OPEN}}}); // [0] state
tb.addOutput({to:vault.tokenAddress,amount:newPool*RATE,token:{amount:newPool,category:mbaCat}}); // [1] vault
tb.addOutput({to:C.tokenaddr,amount:10n*RATE,token:{amount:10n,category:mbaCat}}); // [2] released MBA
tb.addOutput({to:C.cashaddr,amount:BigInt(sU.satoshis)+BigInt(vU.satoshis)+BigInt(yes.satoshis)+BigInt(no.satoshis)+t-BigInt(sU.satoshis)-newPool*RATE-10n*RATE-MF}); // [3] change
st.merge=txidOf(await tb.send());save(st);console.log(` merged — ${st.merge}`);await sleep(10000);
} else console.log(` (merged ${st.merge})`);
// ---- TRADE: C -> B transfer of YES(20) ----
step('TRADE — C transfers YES(20) to B');
if(!st.trade){
const yes=await findShare(C,yesCommit(20n));if(!yes)throw new Error('trade: YES(20) missing');
const {p,t}=await cBch(MF);
const tb=new TransactionBuilder({provider});
tb.addInput(yes,sig.c.unlockP2PKH());
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
tb.addOutput({to:B.tokenaddr,amount:DUST,token:{amount:0n,category:cat,nft:{capability:'none',commitment:yesCommit(20n)}}});
tb.addOutput({to:C.cashaddr,amount:BigInt(yes.satoshis)+t-DUST-MF});
st.trade=txidOf(await tb.send());save(st);console.log(` YES(20) -> B — ${st.trade}`);await sleep(10000);
} else console.log(` (traded ${st.trade})`);
// ---- ATTEST outcome 0 ----
step('ATTEST — oracle says outcome 0 (YES wins)');
if(!st.attest){
const sU=await stateUtxo(), vU=await vaultUtxo();
const {message,sig:oSig}=oracleSign(st.oracle.priv,ev,0x00);
const {p,t}=await cBch(MF+1000n);
const tb=new TransactionBuilder({provider});
tb.setLocktime(Number(st.deadlines.attest));
tb.addInput(sU,vault.unlock.attest(oSig,message));
tb.addInput(vU,vault.unlock.attest(oSig,message));
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
tb.addOutput({to:vault.tokenAddress,amount:BigInt(sU.satoshis),token:{amount:0n,category:cat,nft:{capability:'minting',commitment:'01'+'00'}}});
tb.addOutput({to:vault.tokenAddress,amount:BigInt(vU.satoshis),token:{amount:vU.token.amount,category:mbaCat}});
tb.addOutput({to:C.cashaddr,amount:t-MF});
st.attest=txidOf(await tb.send());save(st);console.log(` attested — ${st.attest}`);await sleep(10000);
} else console.log(` (attest ${st.attest})`);
// ---- REDEEM helper ----
async function redeem(w,s,commit){
const sU=await stateUtxo(), vU=await vaultUtxo();
const share=await findShare(w,commit);if(!share)throw new Error('redeem: share missing');
const amount=BigInt('0x'+commit.slice(2).match(/../g).reverse().join(''));
const newPool=BigInt(vU.token.amount)-amount;
const {p,t}=await cBch(MF+1000n);
const tb=new TransactionBuilder({provider,allowImplicitFungibleTokenBurn:true});
tb.addInput(sU,vault.unlock.redeem(p2pkh(w.publicKeyHash))); // [0] state
tb.addInput(vU,vault.unlock.redeem(p2pkh(w.publicKeyHash))); // [1] vault
tb.addInput(share,s.unlockP2PKH()); // [2] share
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
tb.addOutput({to:vault.tokenAddress,amount:BigInt(sU.satoshis),token:{amount:0n,category:cat,nft:{capability:'minting',commitment:'01'+'00'}}}); // [0] state
tb.addOutput({to:vault.tokenAddress,amount:newPool*RATE,token:{amount:newPool,category:mbaCat}}); // [1] vault
tb.addOutput({to:w.tokenaddr,amount:amount*RATE,token:{amount,category:mbaCat}}); // [2] payout MBA
tb.addOutput({to:C.cashaddr,amount:BigInt(sU.satoshis)+BigInt(vU.satoshis)+BigInt(share.satoshis)+t-BigInt(sU.satoshis)-newPool*RATE-amount*RATE-MF}); // [3] change
return {txid:txidOf(await tb.send()),amount};
}
step('NEG — C redeems NO(20) (loser) -> covenant must REJECT on-chain');
if(!st.loserTested){let r=false;try{await redeem(C,sig.c,noCommit(20n));}catch(e){if(isReject(e)){r=true;console.log(` rejected on-chain: ${(e?.message??e).toString().slice(0,110)}`);}else throw e;}if(!r)throw new Error('SECURITY: loser redeem accepted');st.loserTested=true;save(st);} else console.log(' (loser ok)');
step('REDEEM — B redeems YES(20) -> 20 MBA; vault drains to seed (1)');
if(!st.redeem){const r=await redeem(B,sig.b,yesCommit(20n));st.redeem=r.txid;save(st);console.log(` B redeemed ${r.amount} MBA — ${r.txid}`);await sleep(10000);} else console.log(` (redeemed ${st.redeem})`);
step('VERIFY');
const vU=await vaultUtxo();const vaultAmt=vU?BigInt(vU.token.amount):-1n;const bBal=await mbaBal(B);
const checks=[
['split + merge ran (peg-enforced MBA vault grew then released)', !!st.split1&&!!st.split2&&!!st.merge],
['YES(20) traded C -> B', !!st.trade],
['attest bound to named oracle (checkDataSig)', !!st.attest],
['loser (NO) redeem rejected on-chain (require side==outcome)', !!st.loserTested],
[`winner (YES) redeemed 20 MBA (B holds ${bBal})`, !!st.redeem&&bBal>=20n],
[`vault drained to seed (== ${SEED} MBA, got ${vaultAmt})`, vaultAmt===SEED],
];
let ok=true;for(const [l,p] of checks){console.log(` [${p?'PASS':'FAIL'}] ${l}`);ok=ok&&p;}
step('SUMMARY — Family D config 2: MBA-collateralized binary share market');
console.log(` share cat ${cat}\n mba ${mbaCat}\n oracle ${st.oracle.pub}`);
console.log(` vault ${vault.tokenAddress}`);
console.log(` split1/split2/merge: ${st.split1} / ${st.split2} / ${st.merge}`);
console.log(` trade/attest: ${st.trade} / ${st.attest}`);
console.log(` redeem(B): ${st.redeem}`);
console.log(' Covenant minted YES+NO pairs against a peg-enforced MBA vault, burned a');
console.log(' pair on merge, and paid the oracle-named winner 1:1 in MBA. YES+NO == MBA.');
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);});