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>
103 lines
8.1 KiB
JavaScript
103 lines
8.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-pegstate-test.mjs — single-oracle USDm price-adjustment (PegState) proof.
|
|
* The simplest config (maturity-ladder launch tier): one oracle (E), ~10-min cadence,
|
|
* deadband. Proves the epoch-peg recalibration core:
|
|
* 1. genesis PegState commitment = rate(8 LE) || lastHeight(4 LE) (rate 2000, band 5%)
|
|
* 2. recalibrate to 2050 (+2.5%, WITHIN band) → REJECTED (no churn on small moves)
|
|
* 3. recalibrate to 2200 (+10%, BEYOND band) → ACCEPTED, rate → 2200 (CLTV epoch gate honoured)
|
|
* Funder/genesis = B. Oracle = E (checkDataSig). Writes .chipnet-pegstate-test.json
|
|
* Run: node --import tsx scripts/chipnet-pegstate-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 = join(ROOT, 'ChipnetTests', '.chipnet-pegstate-test.json');
|
|
const ARTIFACT = join(ROOT, 'contracts/dist/PegState.json');
|
|
const EPOCH_BLOCKS = 1n, BAND_BPS = 500n, START_RATE = 2000n, POSITION_VALUE = 1000n;
|
|
|
|
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 u64le = (n) => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(n), true); return b; };
|
|
const leToInt = (bin) => { let n = 0n; for (let i = bin.length - 1; i >= 0; i--) n = (n << 8n) | BigInt(bin[i]); return n; };
|
|
const cat = (...a) => { const t = a.reduce((n, x) => n + x.length, 0); const o = new Uint8Array(t); let p = 0; for (const x of a) { o.set(x, p); p += x.length; } return o; };
|
|
const omsg = (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; };
|
|
const osign = (priv, m) => { const s = secp256k1.signMessageHashSchnorr(priv, laSha256.hash(m)); if (typeof s === 'string') throw new Error(s); return s; };
|
|
const commit = (rate, h) => binToHex(cat(u64le(rate), u32le(h)));
|
|
|
|
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 r = await w.send([{ cashaddr: w.cashaddr, value: 5000n, unit: 'sat' }]); 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'); 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 E = await TestNetWallet.fromWIF(JSON.parse(readFileSync(join(ROOT, '.chipnet-keys-e.json'), 'utf8')).wif);
|
|
const sigB = new SignatureTemplate(B.privateKey);
|
|
const ctor = [E.publicKey, EPOCH_BLOCKS, BAND_BPS];
|
|
const contract = new Contract(artifact, ctor, { provider });
|
|
const st = existsSync(STATE) ? JSON.parse(readFileSync(STATE, 'utf8')) : {};
|
|
const save = () => writeFileSync(STATE, JSON.stringify(st, null, 2));
|
|
const tip = await provider.getBlockHeight();
|
|
console.log(`PegState ${contract.tokenAddress}\noracle=E · epochBlocks ${EPOCH_BLOCKS} · band ${BAND_BPS}bps · tip ${tip}`);
|
|
|
|
const pegUtxo = async () => { const ru = await provider.getUtxos(contract.tokenAddress); return ru.find((u) => u.token?.nft && u.token?.category === st.categoryId); };
|
|
|
|
// 1. genesis (lastHeight in the recent past so the epoch gate is satisfiable)
|
|
if (!st.categoryId) {
|
|
const utxo = await eligible(B); const lastHeight = tip - 5;
|
|
const r = await B.tokenGenesis({ cashaddr: contract.tokenAddress, amount: 0n, value: POSITION_VALUE,
|
|
nft: { capability: 'mutable', commitment: commit(START_RATE, lastHeight) } }, [], { utxoIds: [utxo] });
|
|
console.log(`genesis ${r?.txId ?? r?.txid} · rate ${START_RATE} · lastHeight ${lastHeight} · category ${utxo.txid}`);
|
|
st.categoryId = utxo.txid; st.lastHeight = lastHeight; save(); await sleep(10_000);
|
|
}
|
|
|
|
async function recalibrate(newRate, { expectFail }) {
|
|
const pos = await pegUtxo(); if (!pos) throw new Error('PegState not found');
|
|
const curRate = leToInt(hexToBin(pos.token.nft.commitment).slice(0, 8));
|
|
const curHeight = leToInt(hexToBin(pos.token.nft.commitment).slice(8, 12));
|
|
const locktime = await provider.getBlockHeight(); // >= lastHeight + epochBlocks
|
|
const msg = omsg(newRate), sig = osign(E.privateKey, msg);
|
|
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(pos.satoshis) + t - POSITION_VALUE - minerFee;
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.setLocktime(locktime);
|
|
tb.addInput(pos, contract.unlock.recalibrate(sig, msg), { sequence: 0xfffffffe });
|
|
for (const u of p) tb.addInput(u, sigB.unlockP2PKH(), { sequence: 0xfffffffe });
|
|
tb.addOutput({ to: contract.tokenAddress, amount: POSITION_VALUE, token: { amount: 0n, category: st.categoryId, nft: { capability: 'mutable', commitment: commit(newRate, locktime) } } });
|
|
if (change >= 546n) tb.addOutput({ to: B.cashaddr, amount: change });
|
|
try {
|
|
const sent = await tb.send(); const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
|
|
if (expectFail) { console.log(` ✗ UNEXPECTED: recalibrate ${curRate}→${newRate} accepted (${txid})`); return { txid, ok: false }; }
|
|
console.log(` ✓ recalibrate ${curRate}→${newRate} ACCEPTED (locktime ${locktime}) · ${txid}`); return { txid, newRate: newRate.toString() };
|
|
} catch (e) {
|
|
if (expectFail) { console.log(` ✓ recalibrate ${curRate}→${newRate} correctly REJECTED (within ±${Number(BAND_BPS)/100}% band): ${String(e?.message ?? e).split('\n')[0].slice(0, 70)}`); return { rejected: true }; }
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// 2. within-band → reject
|
|
if (!st.withinBand) { console.log(`\nrecalibrate 2000→2050 (+2.5%, within 5% band) — expect REJECT`); st.withinBand = await recalibrate(2050n, { expectFail: true }); save(); await sleep(3_000); }
|
|
// 3. beyond-band → accept
|
|
if (!st.beyondBand) { console.log(`\nrecalibrate 2000→2200 (+10%, beyond 5% band) — expect ACCEPT`); st.beyondBand = await recalibrate(2200n, { expectFail: false }); save(); await sleep(9_000); }
|
|
|
|
// read back
|
|
const pos = await pegUtxo();
|
|
const onRate = pos ? leToInt(hexToBin(pos.token.nft.commitment).slice(0, 8)) : null;
|
|
st.finalRate = onRate?.toString() ?? null; save();
|
|
console.log(`\n=== RESULT — PegState single-oracle price adjustment ===`);
|
|
console.log(` within-band 2050: ${st.withinBand.rejected ? 'REJECTED ✓' : 'NOT rejected ✗'}`);
|
|
console.log(` beyond-band 2200: ${st.beyondBand.txid ?? 'n/a'}`);
|
|
console.log(` on-chain rate now: ${onRate} (expected 2200) ${onRate === 2200n ? '✓' : '✗'}`);
|
|
}
|
|
main().then(() => process.exit(0)).catch((e) => { console.error('FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack); process.exit(1); });
|