deviant/scripts/chipnet-swap-remainder-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

160 lines
9.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* chipnet-swap-remainder-test.mjs — what happens when a swap's source
* backing does NOT divide evenly into the target rate?
*
* Setup (all Wallet C):
* - Escalating asset E (baseRate=10,000, increment=700 [7%], fee=0)
* - Fixed target F (rate=10,000, fee=0) — chosen so remainders are obvious
* - Buy 3 E units, three times, at climbing curve points:
* buy1 (issuedSoFar=0): rate 10,000 → 3 units = 30,000 sats backing
* buy2 (issuedSoFar=3): rate 12,100 → 3 units = 36,300 sats backing
* buy3 (issuedSoFar=6): rate 14,200 → 3 units = 42,600 sats backing
* - Swap each E UTXO into F (rate 10,000):
* 30,000 → 3 F, remainder 0 (control — divides evenly)
* 36,300 → 3 F, remainder 6,300 (non-divisible)
* 42,600 → 4 F, remainder 2,600 (non-divisible)
*
* Demonstrates: the swap mints floor(backing / targetRate) whole units,
* and the remainder (backing mod targetRate) returns to the swapper as
* plain BCH change. No exact equality required; value is conserved:
* fUnits × targetRate + remainder = backing.
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { TestNetWallet } from 'mainnet-js';
import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript';
const ROOT = join(import.meta.dirname, '..');
const STATE_FILE = join(ROOT, 'ChipnetTests', '.chipnet-swap-remainder-test.json');
const CANONICAL_ARTIFACT = join(ROOT, 'contracts/dist/DeviantOpenMintFT.json');
const ESCALATING_ARTIFACT = join(ROOT, 'contracts/dist/DeviantOpenMintFT_Escalating.json');
const KEYS_C = join(ROOT, '.chipnet-keys-c.json');
const RESERVOIR = 1_000_000_000_000_000_000n;
const E_BASE = 10_000n, E_INCR = 700n, E_FEE = 0n; // 7% escalating
const F_RATE = 10_000n, F_FEE = 0n;
const BUY = 3n;
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_FILE)?JSON.parse(readFileSync(STATE_FILE,'utf8')):{buys:[],swaps:[]};}
function save(s){writeFileSync(STATE_FILE,JSON.stringify(s,(k,v)=>typeof v==='bigint'?v.toString():v,2));}
async function eligible(wallet){
let u=await wallet.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 wallet.send([{cashaddr:wallet.cashaddr,value:2000n,unit:'sat'}],bad.length?{utxoIds:bad}:undefined);
console.log(` splitter ${r?.txId??r?.txid}`);await sleep(12000);
u=await wallet.getUtxos();e=u.filter(x=>x.vout===0&&!x.token);
if(!e.length)throw new Error('splitter failed');return e[0];
}
function pick(bch,need){const p=[];let t=0n;for(const u of bch){p.push(u);t+=BigInt(u.satoshis);if(t>=need)break;}return{p,t};}
async function main(){
const canon=JSON.parse(readFileSync(CANONICAL_ARTIFACT,'utf8'));
const esc=JSON.parse(readFileSync(ESCALATING_ARTIFACT,'utf8'));
const {wif}=JSON.parse(readFileSync(KEYS_C,'utf8'));
const C=await TestNetWallet.fromWIF(wif);
const provider=new ElectrumNetworkProvider('chipnet');
const sig=new SignatureTemplate(C.privateKey);
const flb=p2pkh(C.publicKeyHash);
let state=load();
step('Deploy escalating E (base 10,000, +7%/unit) and fixed F (rate 10,000)');
if(!state.E){
const u=await eligible(C);const cat=u.txid;
const c=new Contract(esc,[txidLE(cat),RESERVOIR,E_BASE,E_INCR,E_FEE,flb],{provider});
console.log(` E: ${c.tokenAddress}`);
const r=await C.tokenGenesis({cashaddr:c.tokenAddress,amount:RESERVOIR,value:1000n,nft:{capability:'minting',commitment:''}},[],{utxoIds:[u]});
console.log(` E genesis: ${r?.txId??r?.txid}`);
state.E={categoryId:cat,contractTokenAddress:c.tokenAddress};save(state);await sleep(10000);
} else console.log(` E already: ${state.E.contractTokenAddress}`);
if(!state.F){
const u=await eligible(C);const cat=u.txid;
const c=new Contract(canon,[txidLE(cat),F_RATE,F_FEE,flb],{provider});
console.log(` F: ${c.tokenAddress}`);
const r=await C.tokenGenesis({cashaddr:c.tokenAddress,amount:RESERVOIR,value:1000n,nft:{capability:'minting',commitment:''}},[],{utxoIds:[u]});
console.log(` F genesis: ${r?.txId??r?.txid}`);
state.F={categoryId:cat,contractTokenAddress:c.tokenAddress};save(state);await sleep(10000);
} else console.log(` F already: ${state.F.contractTokenAddress}`);
const eC=new Contract(esc,[txidLE(state.E.categoryId),RESERVOIR,E_BASE,E_INCR,E_FEE,flb],{provider});
const fC=new Contract(canon,[txidLE(state.F.categoryId),F_RATE,F_FEE,flb],{provider});
async function buyE(label){
const ru=await provider.getUtxos(eC.tokenAddress);
const res=ru.find(x=>x.token?.nft?.capability==='minting'&&x.token?.category===state.E.categoryId&&x.token?.amount>0n);
const issued=RESERVOIR-res.token.amount;
const rate=E_BASE+issued*E_INCR;const gross=BUY*rate;
const bch=(await provider.getUtxos(C.cashaddr)).filter(x=>!x.token).sort((a,b)=>Number(b.satoshis)-Number(a.satoshis));
const mf=6000n;const {p,t}=pick(bch,gross+mf+1000n);
const change=BigInt(res.satoshis)+t-1000n-gross-mf;
const tb=new TransactionBuilder({provider});
tb.addInput(res,eC.unlock.claim(BUY,flb));
for(const u of p)tb.addInput(u,sig.unlockP2PKH());
tb.addOutput({to:eC.tokenAddress,amount:1000n,token:{amount:res.token.amount-BUY,category:state.E.categoryId,nft:{capability:'minting',commitment:''}}});
tb.addOutput({to:C.tokenaddr,amount:gross,token:{amount:BUY,category:state.E.categoryId}});
tb.addOutput({to:C.cashaddr,amount:change});
const sent=await tb.send();const txid=typeof sent==='string'?sent:sent?.txid??sent?.txId;
console.log(` ${label}: issuedSoFar=${issued} rate=${rate} → ${BUY} E for ${gross} sats backing — ${txid}`);
await sleep(10000);return {txid,rate:rate.toString(),backing:gross.toString()};
}
async function swap(buyTxid,label){
const cu=await provider.getUtxos(C.cashaddr);
const e=cu.find(x=>x.token?.category===state.E.categoryId&&!x.token?.nft&&x.token?.amount===BUY&&x.txid===buyTxid);
if(!e)throw new Error(`E UTXO from ${buyTxid?.slice(0,12)} not found`);
const released=BigInt(e.satoshis);
const fUnits=released/F_RATE;const fValue=fUnits*F_RATE;const remainder=released-fValue;
const ru=await provider.getUtxos(fC.tokenAddress);
const res=ru.find(x=>x.token?.nft?.capability==='minting'&&x.token?.category===state.F.categoryId&&x.token?.amount>0n);
const mf=6000n;
const bch=cu.filter(x=>!x.token).sort((a,b)=>Number(b.satoshis)-Number(a.satoshis));
const {p,t}=pick(bch,mf+1000n);
// change to C = remainder (from source) + extra bch inputs + reservoir-in - reservoir-out - fValue - minerFee
const change=remainder+t+BigInt(res.satoshis)-1000n-mf;
const tb=new TransactionBuilder({provider,allowImplicitFungibleTokenBurn:true});
tb.addInput(e,sig.unlockP2PKH());
tb.addInput(res,fC.unlock.claim(fUnits,flb));
for(const u of p)tb.addInput(u,sig.unlockP2PKH());
tb.addOutput({to:fC.tokenAddress,amount:1000n,token:{amount:res.token.amount-fUnits,category:state.F.categoryId,nft:{capability:'minting',commitment:''}}});
tb.addOutput({to:C.tokenaddr,amount:fValue,token:{amount:fUnits,category:state.F.categoryId}});
tb.addOutput({to:C.cashaddr,amount:change});
const sent=await tb.send();const txid=typeof sent==='string'?sent:sent?.txid??sent?.txId;
const conserved=(fValue+remainder===released);
console.log(` ${label}: backing ${released} = ${fUnits} F × ${F_RATE} (${fValue}) + remainder ${remainder} [conserved: ${conserved}]`);
console.log(` → minted ${fUnits} F; remainder ${remainder} sats folded into C's BCH change — ${txid}`);
await sleep(10000);
return {txid,released:released.toString(),fUnits:fUnits.toString(),fValue:fValue.toString(),remainder:remainder.toString(),conserved};
}
step('Buy E three times at climbing curve points (3 units each)');
while(state.buys.length<3){state.buys.push(await buyE(`BUY#${state.buys.length+1}`));save(state);}
step('Swap each E UTXO into F (rate 10,000) — watch the remainder');
while(state.swaps.length<3){const i=state.swaps.length;state.swaps.push(await swap(state.buys[i].txid,`SWAP#${i+1}`));save(state);}
step('SUMMARY — swap remainder behavior');
console.log(' | swap | E backing | F minted | F value | remainder → swapper | conserved |');
console.log(' |------|-----------|----------|---------|----------------------|-----------|');
for(let i=0;i<state.swaps.length;i++){
const s=state.swaps[i];
console.log(` | #${i+1} | ${s.released.padStart(9)} | ${s.fUnits.padStart(8)} | ${s.fValue.padStart(7)} | ${s.remainder.padStart(20)} | ${String(s.conserved).padStart(9)} |`);
}
console.log('');
console.log(' ANSWERS:');
console.log(' - Does it swap without exact equality? YES — mints floor(backing / targetRate) units.');
console.log(' - Where do the remaining sats go? Back to the swapper as plain BCH change.');
console.log(' - Only if exactly divisible? NO. Value conserved: fUnits×rate + remainder = backing.');
console.log(' - Boundary: if backing < one target unit (here <10,000), fUnits=0 → claim(0) reverts');
console.log(' (the unbacked/zero case, snapshot 0028).');
divider();
}
main().catch((e)=>{console.error('FAILED:',e?.message??e);if(e?.stack)console.error(e.stack);process.exit(1);});