deviant/scripts/chipnet-usdm-roundtrip-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

149 lines
10 KiB
JavaScript
Raw 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-usdm-roundtrip-test.mjs — DeviantOracleMBA (USD stablecoin) round-trip.
*
* Founder/oracle = E (low balance — watch fee accrue). Funder/claimer = B. Recipient = C.
* Assumed chipnet price: 1 BCH = $500 ⇒ 1 USD = 200,000 sats; 2-decimal divisible
* ⇒ price per ATOMIC unit (0.01 USD) = 2,000 sats. Founder fee 0.05% (5 bps).
*
* 1. genesis USDm FT reservoir (B funds) at the oracle-MBA contract
* 2. FORWARD swap: B claims 6.00 USDm (600 atomic) — oracle-signed price; locks
* 1,200,000 sats; founder E receives 0.05% = 600 sats (clears dust)
* 3. SEND: B → C transfers the USDm UTXO (backing travels with it, no oracle)
* 4. BACKWARD swap: C redeems USDm → BCH by plain P2PKH spend — NO oracle —
* recovering the exact locked BCH (self-backed)
*
* Writes .chipnet-usdm-roundtrip-test.json
* Run: node --import tsx scripts/chipnet-usdm-roundtrip-test.mjs --yes
*/
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, sha256 as laSha256 } from '@bitauth/libauth';
const ROOT = join(import.meta.dirname, '..');
const STATE_FILE = join(ROOT, 'ChipnetTests', '.chipnet-usdm-roundtrip-test.json');
const ARTIFACT = join(ROOT, 'contracts/dist/DeviantOracleMBA.json');
const DECIMALS = 2n, SATS_PER_USD = 200000n, PRICE_PER_ATOMIC = SATS_PER_USD / (10n ** DECIMALS); // 2000
const FEE_BPS = 5n; // 0.05%
const SUPPLY = 1000000n, POSITION_VALUE = 1000n;
const CLAIM_USD_CENTS = 600n; // 6.00 USDm = 600 atomic units
const 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; };
const binToHex = (b) => Buffer.from(b).toString('hex');
const txidToConsensusBytes = (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; };
const 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));
const u32le = (n) => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, Number(n), true); return b; };
const oracleMessage = (price) => { const m = new Uint8Array(16); m.set(u32le(1), 0); m.set(u32le(1), 4); m.set(u32le(1), 8); m.set(u32le(price), 12); return m; };
function oracleSign(privKey, message) { const sig = secp256k1.signMessageHashSchnorr(privKey, laSha256.hash(message)); if (typeof sig === 'string') throw new Error('oracle sign: ' + sig); return sig; }
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: 5000n, unit: 'sat' }], bad.length ? { utxoIds: bad } : undefined);
console.log(` splitter ${r?.txId ?? r?.txid}`); await sleep(12_000);
u = await w.getUtxos(); e = u.filter((x) => x.vout === 0 && !x.token);
if (!e.length) throw new Error('no eligible vout-0'); return e[0];
}
const pickCash = (cash, need) => { const p = []; let t = 0n; for (const u of cash) { p.push(u); t += BigInt(u.satoshis); if (t >= need) break; } if (t < need) throw new Error(`insufficient ${t} < ${need}`); return { p, t }; };
async function main() {
if (!process.argv.includes('--yes')) { console.log('Re-run with --yes.'); return; }
const artifact = JSON.parse(readFileSync(ARTIFACT, 'utf8'));
const provider = new ElectrumNetworkProvider('chipnet');
const B = await TestNetWallet.fromWIF(JSON.parse(readFileSync(join(ROOT, '.chipnet-keys-b.json'), 'utf8')).wif);
const C = await TestNetWallet.fromWIF(JSON.parse(readFileSync(join(ROOT, '.chipnet-keys-c.json'), 'utf8')).wif);
const E = await TestNetWallet.fromWIF(JSON.parse(readFileSync(join(ROOT, '.chipnet-keys-e.json'), 'utf8')).wif);
const sigB = new SignatureTemplate(B.privateKey), sigC = new SignatureTemplate(C.privateKey);
const oraclePk = E.publicKey, founderLB = p2pkh(E.publicKeyHash);
const state = existsSync(STATE_FILE) ? JSON.parse(readFileSync(STATE_FILE, 'utf8')) : {};
const save = () => writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
const ctor = (cat) => [txidToConsensusBytes(cat), oraclePk, FEE_BPS, founderLB];
console.log(`USDm: price/atomic ${PRICE_PER_ATOMIC} sats (1 USD = ${SATS_PER_USD} sats, ${DECIMALS}dp) · fee ${FEE_BPS}bps · oracle/founder=E ${E.cashaddr.slice(0,18)}…`);
// 1. genesis reservoir (B funds)
if (!state.categoryId) {
const utxo = await eligible(B);
const cat = utxo.txid;
const ct = new Contract(artifact, ctor(cat), { provider });
const r = await B.tokenGenesis({ cashaddr: ct.tokenAddress, amount: SUPPLY, value: POSITION_VALUE }, [], { utxoIds: [utxo] });
console.log(`contract ${ct.tokenAddress}\ngenesis ${r?.txId ?? r?.txid} · category ${cat}`);
state.categoryId = cat; state.contractTokenAddress = ct.tokenAddress; save(); await sleep(10_000);
}
const contract = new Contract(artifact, ctor(state.categoryId), { provider });
// 2. FORWARD swap — B claims 6.00 USDm, oracle-priced
if (!state.claim) {
const ru = await provider.getUtxos(contract.tokenAddress);
const pos = ru.find((u) => u.token && u.token.category === state.categoryId && !u.token.nft && u.token.amount > 0n);
if (!pos) throw new Error('reservoir not found');
const reservoir = BigInt(pos.token.amount);
const tokenAmount = CLAIM_USD_CENTS;
const grossSats = tokenAmount * PRICE_PER_ATOMIC;
const feeSats = grossSats * FEE_BPS / 10000n;
const msg = oracleMessage(PRICE_PER_ATOMIC);
const osig = oracleSign(E.privateKey, msg);
const recipientLB = p2pkh(B.publicKeyHash);
console.log(`\nFORWARD: claim ${Number(tokenAmount)/100} USDm (${tokenAmount} atomic) · gross ${grossSats} sats · fee ${feeSats} → E`);
const cash = (await provider.getUtxos(B.cashaddr)).filter((u) => !u.token).sort((a, b) => Number(BigInt(b.satoshis) - BigInt(a.satoshis)));
const minerFee = 3000n;
const { p, t } = pickCash(cash, grossSats + feeSats + minerFee + 1000n);
const change = BigInt(pos.satoshis) + t - POSITION_VALUE - grossSats - feeSats - minerFee;
const tb = new TransactionBuilder({ provider });
tb.addInput(pos, contract.unlock.claim(tokenAmount, recipientLB, osig, msg));
for (const u of p) tb.addInput(u, sigB.unlockP2PKH());
tb.addOutput({ to: contract.tokenAddress, amount: POSITION_VALUE, token: { amount: reservoir - tokenAmount, category: state.categoryId } });
tb.addOutput({ to: B.tokenaddr, amount: grossSats, token: { amount: tokenAmount, category: state.categoryId } });
if (feeSats >= 546n) tb.addOutput({ to: E.cashaddr, amount: feeSats });
if (change >= 546n) tb.addOutput({ to: B.cashaddr, amount: change });
const sent = await tb.send(); const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
console.log(` ✓ claim ${txid}`);
state.claim = { tokenAmount: tokenAmount.toString(), grossSats: grossSats.toString(), feeSats: feeSats.toString(), txid }; save(); await sleep(9_000);
}
// 3. SEND B → C (token transfer; backing travels)
if (!state.send) {
const bu = await provider.getUtxos(B.tokenaddr);
const tok = bu.find((u) => u.token && u.token.category === state.categoryId && !u.token.nft && u.token.amount > 0n);
if (!tok) throw new Error('B USDm not found');
const cash = (await provider.getUtxos(B.cashaddr)).filter((u) => !u.token).sort((a, b) => Number(BigInt(b.satoshis) - BigInt(a.satoshis)));
const minerFee = 2000n; const { p, t } = pickCash(cash, minerFee + 1000n);
const change = BigInt(tok.satoshis) + t - BigInt(tok.satoshis) - minerFee; // keep USDm value intact, fee from cash
const tb = new TransactionBuilder({ provider });
tb.addInput(tok, sigB.unlockP2PKH());
for (const u of p) tb.addInput(u, sigB.unlockP2PKH());
tb.addOutput({ to: C.tokenaddr, amount: BigInt(tok.satoshis), token: { amount: BigInt(tok.token.amount), category: state.categoryId } });
if (change >= 546n) tb.addOutput({ to: B.cashaddr, amount: change });
const sent = await tb.send(); const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
console.log(`\nSEND: B → C ${BigInt(tok.token.amount)} atomic USDm (carrying ${tok.satoshis} sats backing) · ${txid}`);
state.send = { amount: tok.token.amount.toString(), backing: tok.satoshis.toString(), txid }; save(); await sleep(9_000);
}
// 4. BACKWARD swap — C redeems USDm → BCH (no oracle, plain P2PKH spend)
if (!state.redeem) {
const cu = await provider.getUtxos(C.tokenaddr);
const tok = cu.find((u) => u.token && u.token.category === state.categoryId && !u.token.nft && u.token.amount > 0n);
if (!tok) throw new Error('C USDm not found');
const minerFee = 1000n;
const recovered = BigInt(tok.satoshis) - minerFee;
const tb = new TransactionBuilder({ provider, allowImplicitFungibleTokenBurn: true }); // redeem burns the FT
tb.addInput(tok, sigC.unlockP2PKH()); // spend the USDm UTXO; FT burned (not re-emitted)
tb.addOutput({ to: C.cashaddr, amount: recovered }); // pure BCH out — the locked backing, no oracle
const sent = await tb.send(); const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
console.log(`\nBACKWARD: C redeems USDm → BCH (no oracle) · recovered ${recovered} sats · ${txid}`);
state.redeem = { recovered: recovered.toString(), txid }; save();
}
console.log(`\n=== RESULT — USDm oracle-priced MBA round-trip ===`);
console.log(` FORWARD claim (oracle): ${Number(state.claim.tokenAmount)/100} USDm locked ${state.claim.grossSats} sats · fee ${state.claim.feeSats} → E · ${state.claim.txid}`);
console.log(` SEND B→C: ${state.send.txid}`);
console.log(` BACKWARD redeem (no oracle): recovered ${state.redeem.recovered} sats · ${state.redeem.txid}`);
console.log(` Backing conserved: locked ${state.claim.grossSats} → recovered ${state.redeem.recovered} (− miner fees)`);
}
main().then(() => process.exit(0)).catch((e) => { console.error('FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack); process.exit(1); });