deviant/scripts/chipnet-anyhedge-liquidation-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

233 lines
10 KiB
JavaScript
Raw Permalink 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-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); });