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>
160 lines
9.8 KiB
JavaScript
160 lines
9.8 KiB
JavaScript
#!/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);});
|