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>
233 lines
10 KiB
JavaScript
233 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* chipnet-anyhedge-liquidation-test.mjs — Turn 1.5 (small): validates the
|
||
* SECOND branch of AnyHedge v0.12 `payout()` — liquidation triggered by
|
||
* price-out-of-bounds, BEFORE maturity.
|
||
*
|
||
* The maturity branch was validated in Turn 1 (snapshot 0097, settlement
|
||
* TXID 364700760a952d…). The liquidation branch uses the same `payout()`
|
||
* function but with a settlement timestamp BEFORE maturity and a price
|
||
* that clamps to either lowLiquidationPrice or highLiquidationPrice
|
||
* (triggering the priceOutOfBounds condition in the contract's gauntlet).
|
||
*
|
||
* Test setup (LOW-bound liquidation):
|
||
* LOW_LIQ_PRICE = 30_000 cents/BCH ($300) ← changed from Turn 1's $250
|
||
* so payout math gives a clean
|
||
* 165k/33k split that leaves
|
||
* room for chipnet relay fee
|
||
* HIGH_LIQ_PRICE = 100_000 cents/BCH ($1000)
|
||
* SETTLE_PRICE = 20_000 cents/BCH ($200) — clamps to 30_000 = lowLiq
|
||
* → priceOutOfBounds = true
|
||
* settlement timestamp = BEFORE maturity (uses the liquidation branch,
|
||
* not the maturity branch)
|
||
*
|
||
* Expected payout at clampedPrice = 30_000:
|
||
* shortSats = 4_950_000_000 / 30_000 = 165_000
|
||
* longSats = max(DUST, 198_000 − 165_000) = 33_000
|
||
* Sum = 198_000 = payoutSats ✓
|
||
*
|
||
* The fresh constructor params (different liquidation bounds) yield a
|
||
* different contract address from Turn 1 — clean test with no state from
|
||
* the prior run. Resumable via .chipnet-anyhedge-liquidation-test.json.
|
||
*
|
||
* Run: node --import tsx scripts/chipnet-anyhedge-liquidation-test.mjs
|
||
*/
|
||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
import { TestNetWallet } from 'mainnet-js';
|
||
import {
|
||
Contract,
|
||
ElectrumNetworkProvider,
|
||
TransactionBuilder,
|
||
} from 'cashscript';
|
||
import {
|
||
generateOracleKeypair,
|
||
buildSettlementPair,
|
||
projectedPayoutSats,
|
||
} from './lib/anyhedge-oracle.mjs';
|
||
|
||
const ROOT = join(import.meta.dirname, '..');
|
||
const ARTIFACT = join(ROOT, '.external/anyhedge-artifacts/AnyHedge_v0_12.json');
|
||
const STATE_FILE = join(ROOT, 'ChipnetTests', '.chipnet-anyhedge-liquidation-test.json');
|
||
const ORACLE_KEYS = join(ROOT, 'ChipnetTests', '.chipnet-anyhedge-test-oracle.json'); // reuse Turn 1 oracle
|
||
const KEYS_B = join(ROOT, '.chipnet-keys-b.json');
|
||
const KEYS_C = join(ROOT, '.chipnet-keys-c.json');
|
||
const KEYS_D = join(ROOT, '.chipnet-keys-d.json');
|
||
|
||
const PAYOUT_SATS = 198_000n;
|
||
const FUND_SATS = 200_000n;
|
||
const NOMINAL = 4_950_000_000;
|
||
const SATS_FOR_HIGH_LIQ = 0;
|
||
const LOW_LIQ_PRICE = 30_000; // $300 (changed from Turn 1's $250)
|
||
const HIGH_LIQ_PRICE = 100_000; // $1000
|
||
const SETTLE_PRICE = 20_000; // $200 — clamps to lowLiq, triggers liquidation
|
||
|
||
function hexToBin(hex) {
|
||
const out = new Uint8Array(hex.length / 2);
|
||
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||
return out;
|
||
}
|
||
function binToHex(bin) {
|
||
return Array.from(bin, (b) => b.toString(16).padStart(2, '0')).join('');
|
||
}
|
||
function p2pkhLockingBytecode(pkh) {
|
||
const out = new Uint8Array(25);
|
||
out[0] = 0x76; out[1] = 0xa9; out[2] = 0x14; out.set(pkh, 3); out[23] = 0x88; out[24] = 0xac;
|
||
return out;
|
||
}
|
||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
function divider(ch = '=') { console.log(ch.repeat(80)); }
|
||
function step(n, title) { console.log(''); divider(); console.log(`STEP ${n} — ${title}`); divider(); }
|
||
|
||
async function loadWallet(path, label) {
|
||
const { wif } = JSON.parse(readFileSync(path, 'utf8'));
|
||
const w = await TestNetWallet.fromWIF(wif);
|
||
console.log(` ${label}: ${w.cashaddr}`);
|
||
return w;
|
||
}
|
||
|
||
function loadOrCreateOracle() {
|
||
if (existsSync(ORACLE_KEYS)) {
|
||
const k = JSON.parse(readFileSync(ORACLE_KEYS, 'utf8'));
|
||
return { privateKey: hexToBin(k.privateKeyHex), publicKey: hexToBin(k.publicKeyHex) };
|
||
}
|
||
const kp = generateOracleKeypair();
|
||
writeFileSync(ORACLE_KEYS, JSON.stringify({
|
||
privateKeyHex: binToHex(kp.privateKey),
|
||
publicKeyHex: binToHex(kp.publicKey),
|
||
createdAt: new Date().toISOString(),
|
||
note: 'Reused across AnyHedge chipnet tests. NOT for mainnet.',
|
||
}, null, 2));
|
||
return kp;
|
||
}
|
||
|
||
async function main() {
|
||
console.log('');
|
||
divider();
|
||
console.log(' Turn 1.5 — chipnet AnyHedge LIQUIDATION-path test (LOW-bound)');
|
||
divider();
|
||
|
||
step(0, 'Setup');
|
||
const artifact = JSON.parse(readFileSync(ARTIFACT, 'utf8'));
|
||
const provider = new ElectrumNetworkProvider('chipnet');
|
||
const walletB = await loadWallet(KEYS_B, 'Wallet B (funder)');
|
||
const walletC = await loadWallet(KEYS_C, 'Wallet C (Hedge target)');
|
||
const walletD = await loadWallet(KEYS_D, 'Wallet D (Long target)');
|
||
const oracle = loadOrCreateOracle();
|
||
console.log(` oracle pubkey: ${binToHex(oracle.publicKey)} (reused from Turn 1)`);
|
||
|
||
step(1, 'Compute constructor params');
|
||
const shortLockScript = p2pkhLockingBytecode(walletC.publicKeyHash);
|
||
const longLockScript = p2pkhLockingBytecode(walletD.publicKeyHash);
|
||
let state = existsSync(STATE_FILE) ? JSON.parse(readFileSync(STATE_FILE, 'utf8')) : null;
|
||
const now = Math.floor(Date.now() / 1000);
|
||
const startTimestamp = state?.startTimestamp ?? now;
|
||
const maturityTimestamp = state?.maturityTimestamp ?? (startTimestamp + 3600);
|
||
const inertPubkey = hexToBin(binToHex(walletB.publicKey));
|
||
|
||
const constructorArgs = [
|
||
inertPubkey, inertPubkey, 0n,
|
||
shortLockScript, longLockScript,
|
||
oracle.publicKey,
|
||
BigInt(NOMINAL), BigInt(SATS_FOR_HIGH_LIQ), PAYOUT_SATS,
|
||
BigInt(LOW_LIQ_PRICE), BigInt(HIGH_LIQ_PRICE),
|
||
BigInt(startTimestamp), BigInt(maturityTimestamp),
|
||
];
|
||
console.log(` liquidation bounds: [$${LOW_LIQ_PRICE / 100}, $${HIGH_LIQ_PRICE / 100}]`);
|
||
console.log(` settle price (oracle): $${SETTLE_PRICE / 100} → clamps to lowLiq = $${LOW_LIQ_PRICE / 100}`);
|
||
console.log(` start=${startTimestamp}, maturity=${maturityTimestamp} (settlement BEFORE maturity)`);
|
||
|
||
step(2, 'Instantiate AnyHedge contract');
|
||
const contract = new Contract(artifact, constructorArgs, { provider });
|
||
console.log(` contract address: ${contract.address}`);
|
||
const projected = projectedPayoutSats({
|
||
clampedPrice: LOW_LIQ_PRICE, // contract clamps SETTLE_PRICE up to LOW_LIQ_PRICE
|
||
nominalUnitsXSatsPerBch: NOMINAL,
|
||
satsForNominalUnitsAtHighLiquidation: SATS_FOR_HIGH_LIQ,
|
||
payoutSats: Number(PAYOUT_SATS),
|
||
});
|
||
console.log(` projected payout @ clamped=${LOW_LIQ_PRICE}: shortSats=${projected.shortSats}, longSats=${projected.longSats}`);
|
||
|
||
step(3, 'Fund the box');
|
||
let contractUtxos = await provider.getUtxos(contract.address);
|
||
let fundUtxo = contractUtxos.find((u) => BigInt(u.satoshis) === FUND_SATS);
|
||
let fundingTxid = state?.fundingTxid;
|
||
|
||
if (!fundUtxo) {
|
||
console.log(' building funding tx');
|
||
const result = await walletB.send([{ cashaddr: contract.address, value: Number(FUND_SATS), unit: 'sat' }]);
|
||
fundingTxid = result.txId || result.txid;
|
||
console.log(` funding TXID: ${fundingTxid}`);
|
||
await sleep(5000);
|
||
contractUtxos = await provider.getUtxos(contract.address);
|
||
fundUtxo = contractUtxos.find((u) => BigInt(u.satoshis) === FUND_SATS);
|
||
if (!fundUtxo) throw new Error(`UTXO not visible after broadcast`);
|
||
} else {
|
||
console.log(` resumed funding UTXO ${fundUtxo.txid}:${fundUtxo.vout}`);
|
||
fundingTxid = fundUtxo.txid;
|
||
}
|
||
|
||
state = { ...(state || {}), contractAddress: contract.address, startTimestamp, maturityTimestamp, fundingTxid };
|
||
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
||
|
||
step(4, 'Build oracle settlement pair (LIQUIDATION case: pre-maturity + out-of-bounds)');
|
||
const pair = buildSettlementPair({
|
||
previousTimestamp: startTimestamp + 60, // shortly after start, BEFORE maturity
|
||
previousContentSequence: 200,
|
||
previousPrice: SETTLE_PRICE,
|
||
settlementTimestamp: startTimestamp + 120, // 1 min after previous, STILL BEFORE maturity
|
||
settlementContentSequence: 201,
|
||
settlementPrice: SETTLE_PRICE, // clamps to lowLiq → out of bounds
|
||
privateKey: oracle.privateKey,
|
||
contractMaturityTimestamp: maturityTimestamp,
|
||
contractStartTimestamp: startTimestamp,
|
||
contractLowLiqPrice: LOW_LIQ_PRICE,
|
||
contractHighLiqPrice: HIGH_LIQ_PRICE,
|
||
});
|
||
console.log(' pair built — liquidation invariants pass locally');
|
||
|
||
step(5, 'Broadcast settlement tx');
|
||
const balanceCBefore = BigInt(await walletC.getBalance('sat'));
|
||
const balanceDBefore = BigInt(await walletD.getBalance('sat'));
|
||
console.log(` C pre: ${balanceCBefore} D pre: ${balanceDBefore}`);
|
||
|
||
const tb = new TransactionBuilder({ provider });
|
||
tb.addInput(fundUtxo, contract.unlock.payout(
|
||
pair.settlementMessage, pair.settlementSignature,
|
||
pair.previousMessage, pair.previousSignature,
|
||
));
|
||
tb.addOutput({ to: walletC.cashaddr, amount: BigInt(projected.shortSats) });
|
||
tb.addOutput({ to: walletD.cashaddr, amount: BigInt(projected.longSats) });
|
||
|
||
const settlementTxid = await tb.send();
|
||
const sid = typeof settlementTxid === 'string' ? settlementTxid : settlementTxid?.txid;
|
||
console.log(` settlement TXID: ${sid}`);
|
||
|
||
state.settlementTxid = sid;
|
||
state.settlementCompletedAt = new Date().toISOString();
|
||
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
||
|
||
step(6, 'Validate');
|
||
await sleep(5000);
|
||
const balanceCAfter = BigInt(await walletC.getBalance('sat'));
|
||
const balanceDAfter = BigInt(await walletD.getBalance('sat'));
|
||
const deltaC = balanceCAfter - balanceCBefore;
|
||
const deltaD = balanceDAfter - balanceDBefore;
|
||
console.log(` C post: ${balanceCAfter} (Δ ${deltaC >= 0n ? '+' : ''}${deltaC})`);
|
||
console.log(` D post: ${balanceDAfter} (Δ ${deltaD >= 0n ? '+' : ''}${deltaD})`);
|
||
const passC = deltaC === BigInt(projected.shortSats);
|
||
const passD = deltaD === BigInt(projected.longSats);
|
||
console.log(` expected shortSats: ${projected.shortSats} → Δ Hedge ${deltaC} ${passC ? '✓' : '✗'}`);
|
||
console.log(` expected longSats: ${projected.longSats} → Δ Long ${deltaD} ${passD ? '✓' : '✗'}`);
|
||
|
||
console.log('');
|
||
divider();
|
||
if (passC && passD) {
|
||
console.log(' ✓ Turn 1.5 SUCCESS — liquidation-path settles to expected sats.');
|
||
} else {
|
||
console.log(' ✗ Turn 1.5 MISMATCH');
|
||
}
|
||
divider();
|
||
}
|
||
|
||
main().catch((e) => { console.error('\nFATAL:', e.message); console.error(e.stack); process.exit(1); });
|