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>
173 lines
11 KiB
JavaScript
173 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-clob-orders-test.mjs — Family D (ADR-0030) on-chain RESTING LIMIT ORDERS
|
|
* (ShareLimitOrderBCH). The fully-on-chain order-book leg: a resting limit order is
|
|
* an ASM swap-offer (fixed-rate shares <-> sats) parked in a covenant UTXO, with
|
|
* partial-fill + cancel. No off-chain relay — orders live at covenant addresses.
|
|
*
|
|
* 1. genesis a fungible "YES share" token (C holds the supply)
|
|
* 2. C posts ASK #1: escrow 10000 shares at 6 sats/share
|
|
* 3. B takes 4000 -> pays 24000 to C, gets 4000; order RE-CREATED with 6000 (partial fill)
|
|
* 4. B takes 6000 -> pays 36000 to C, gets 6000; order consumed (full fill)
|
|
* 5. C posts ASK #2: escrow 5000 shares at 7 sats/share; C CANCELS -> 5000 back
|
|
*
|
|
* Proves: permissionless take, on-chain partial fill (resting order re-creation),
|
|
* full fill, and maker cancel. Settlement is the ASM-style covenant swap.
|
|
* (price is sats/share; a real market denominates shares finely so price = probability.)
|
|
*
|
|
* Roles: C = maker; B = taker. Resumable state JSON.
|
|
*/
|
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { TestNetWallet } from 'mainnet-js';
|
|
import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript';
|
|
import { secp256k1 } from '@bitauth/libauth';
|
|
|
|
const ROOT = join(import.meta.dirname, '..');
|
|
const STATE = join(ROOT, 'ChipnetTests', '.chipnet-clob-orders-test.json');
|
|
const ARTIFACT = join(ROOT, 'contracts/dist/ShareLimitOrderBCH.json');
|
|
const KEYS = { b: join(ROOT, '.chipnet-keys-b.json'), c: join(ROOT, '.chipnet-keys-c.json') };
|
|
const SUPPLY = 100000n;
|
|
const DUST = 1000n;
|
|
const MF = 2000n;
|
|
const PRICE1 = 6n, PRICE2 = 7n;
|
|
|
|
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')):{};}
|
|
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;}
|
|
|
|
async function main(){
|
|
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)};
|
|
let st=load();
|
|
|
|
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 pick(w,need){const u=(await provider.getUtxos(w.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(`low BCH ${t}<${need}`);return {p,t};}
|
|
async function shareUtxo(w,min){return (await provider.getUtxos(w.cashaddr)).find(x=>x.token?.category===st.cat&&!x.token?.nft&&BigInt(x.token?.amount)>=min);}
|
|
async function shareBal(w){return (await provider.getUtxos(w.cashaddr)).filter(x=>x.token?.category===st.cat&&!x.token?.nft).reduce((s,x)=>s+BigInt(x.token.amount),0n);}
|
|
async function bchBal(w){return (await provider.getUtxos(w.cashaddr)).filter(x=>!x.token).reduce((s,x)=>s+BigInt(x.satoshis),0n);}
|
|
|
|
// ---- Genesis a fungible "YES share" token ----
|
|
step('Genesis fungible YES-share token (C holds the supply)');
|
|
if(!st.cat){
|
|
const u=await eligible(C);const cat=u.txid;
|
|
const r=await C.tokenGenesis({cashaddr:C.tokenaddr,amount:SUPPLY,value:1000n},[],{utxoIds:[u]});
|
|
console.log(` genesis ${txidOf(r)} — category ${cat}`);st.cat=cat;save(st);await sleep(10000);
|
|
} else console.log(` category ${st.cat}`);
|
|
const cat=st.cat;
|
|
|
|
const order1=new Contract(art,[txidLE(cat),PRICE1,C.publicKeyHash],{provider});
|
|
const order2=new Contract(art,[txidLE(cat),PRICE2,C.publicKeyHash],{provider});
|
|
console.log(` order#1 (price ${PRICE1}) ${order1.tokenAddress}`);
|
|
console.log(` order#2 (price ${PRICE2}) ${order2.tokenAddress}`);
|
|
async function orderUtxo(o){return (await provider.getUtxos(o.tokenAddress)).find(x=>x.token?.category===cat&&!x.token?.nft);}
|
|
|
|
// ---- Post ASK #1: escrow 10000 shares at PRICE1 ----
|
|
step(`POST ASK #1 — escrow 10000 shares @ ${PRICE1} sats/share`);
|
|
if(!st.post1){
|
|
const su=await shareUtxo(C,10000n);if(!su)throw new Error('post1: shares missing');
|
|
const {p,t}=await pick(C,DUST+MF+1000n);
|
|
const tb=new TransactionBuilder({provider});
|
|
tb.addInput(su,sig.c.unlockP2PKH());
|
|
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
|
|
tb.addOutput({to:order1.tokenAddress,amount:DUST,token:{amount:10000n,category:cat}}); // [0] resting order
|
|
tb.addOutput({to:C.tokenaddr,amount:DUST,token:{amount:BigInt(su.token.amount)-10000n,category:cat}}); // [1] share change
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(su.satoshis)+t-2n*DUST-MF}); // [2] BCH change
|
|
st.post1=txidOf(await tb.send());save(st);console.log(` posted — ${st.post1}`);await sleep(10000);
|
|
} else console.log(` (posted ${st.post1})`);
|
|
|
|
// ---- TAKE helper (B buys `take` shares from an order) ----
|
|
async function take(o,price,takeAmt){
|
|
const ou=await orderUtxo(o);if(!ou)throw new Error('take: order missing');
|
|
const offered=BigInt(ou.token.amount);
|
|
const pay=takeAmt*price;
|
|
const remaining=offered-takeAmt;
|
|
const {p,t}=await pick(B,pay+DUST+(remaining>0n?DUST:0n)+MF);
|
|
const tb=new TransactionBuilder({provider});
|
|
tb.addInput(ou,o.unlock.take(takeAmt,p2pkh(B.publicKeyHash))); // [0] order UTXO
|
|
for(const x of p)tb.addInput(x,sig.b.unlockP2PKH());
|
|
tb.addOutput({to:C.cashaddr,amount:pay}); // [0] payment to maker C
|
|
tb.addOutput({to:B.tokenaddr,amount:DUST,token:{amount:takeAmt,category:cat}}); // [1] shares to taker B
|
|
let idx=2,spent=pay+DUST;
|
|
if(remaining>0n){tb.addOutput({to:o.tokenAddress,amount:DUST,token:{amount:remaining,category:cat}});spent+=DUST;idx++;} // [2] re-created order
|
|
tb.addOutput({to:B.cashaddr,amount:BigInt(ou.satoshis)+t-spent-MF}); // [last] B change
|
|
return {txid:txidOf(await tb.send()),pay,remaining};
|
|
}
|
|
|
|
step(`TAKE — B buys 4000 @ ${PRICE1} (partial; order re-created with 6000)`);
|
|
if(!st.take1){const r=await take(order1,PRICE1,4000n);st.take1=r.txid;save(st);console.log(` B paid ${r.pay}, order now ${r.remaining} — ${r.txid}`);await sleep(10000);}
|
|
else console.log(` (take1 ${st.take1})`);
|
|
|
|
step(`TAKE — B buys 6000 @ ${PRICE1} (full; order consumed)`);
|
|
if(!st.take2){const r=await take(order1,PRICE1,6000n);st.take2=r.txid;save(st);console.log(` B paid ${r.pay}, order now ${r.remaining} — ${r.txid}`);await sleep(10000);}
|
|
else console.log(` (take2 ${st.take2})`);
|
|
|
|
// ---- Post ASK #2 then CANCEL ----
|
|
step(`POST ASK #2 — escrow 5000 shares @ ${PRICE2} sats/share`);
|
|
if(!st.post2){
|
|
const su=await shareUtxo(C,5000n);if(!su)throw new Error('post2: shares missing');
|
|
const {p,t}=await pick(C,DUST+MF+1000n);
|
|
const tb=new TransactionBuilder({provider});
|
|
tb.addInput(su,sig.c.unlockP2PKH());
|
|
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
|
|
tb.addOutput({to:order2.tokenAddress,amount:DUST,token:{amount:5000n,category:cat}});
|
|
tb.addOutput({to:C.tokenaddr,amount:DUST,token:{amount:BigInt(su.token.amount)-5000n,category:cat}});
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(su.satoshis)+t-2n*DUST-MF});
|
|
st.post2=txidOf(await tb.send());save(st);console.log(` posted — ${st.post2}`);await sleep(10000);
|
|
} else console.log(` (posted ${st.post2})`);
|
|
|
|
step('CANCEL ASK #2 — maker C reclaims 5000 shares');
|
|
if(!st.cancel){
|
|
const ou=await orderUtxo(order2);if(!ou)throw new Error('cancel: order missing');
|
|
const {p,t}=await pick(C,MF+1000n);
|
|
const tb=new TransactionBuilder({provider});
|
|
const cPub=secp256k1.derivePublicKeyCompressed(C.privateKey); // compressed pk matches the address pkh
|
|
tb.addInput(ou,order2.unlock.cancel(cPub,sig.c,p2pkh(C.publicKeyHash))); // [0] order
|
|
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
|
|
tb.addOutput({to:C.tokenaddr,amount:DUST,token:{amount:BigInt(ou.token.amount),category:cat}}); // [0] shares back to maker
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(ou.satoshis)+t-DUST-MF});
|
|
st.cancel=txidOf(await tb.send());save(st);console.log(` cancelled — ${st.cancel}`);await sleep(10000);
|
|
} else console.log(` (cancelled ${st.cancel})`);
|
|
|
|
step('VERIFY');
|
|
const bShares=await shareBal(B);
|
|
const o1=await orderUtxo(order1), o2=await orderUtxo(order2);
|
|
const checks=[
|
|
['ASK #1 posted', !!st.post1],
|
|
['partial take (4000) re-created the resting order', !!st.take1],
|
|
['full take (6000) consumed the order', !!st.take2&&!o1],
|
|
[`taker B holds 10000 shares (got ${bShares})`, bShares===10000n],
|
|
['ASK #2 posted + cancelled (order consumed)', !!st.post2&&!!st.cancel&&!o2],
|
|
];
|
|
let ok=true;for(const [l,p] of checks){console.log(` [${p?'PASS':'FAIL'}] ${l}`);ok=ok&&p;}
|
|
|
|
step('SUMMARY — Family D on-chain resting limit orders (ShareLimitOrderBCH)');
|
|
console.log(` category ${cat}`);
|
|
console.log(` order#1 ${order1.tokenAddress}\n order#2 ${order2.tokenAddress}`);
|
|
console.log(` post1/take1/take2: ${st.post1} / ${st.take1} / ${st.take2}`);
|
|
console.log(` post2/cancel: ${st.post2} / ${st.cancel}`);
|
|
console.log(' A resting limit order = an ASM offer (shares<->sats at fixed rate) in a');
|
|
console.log(' covenant UTXO; permissionless partial/full fills + maker cancel. No off-chain book.');
|
|
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);});
|