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