morphit/apps/web/scripts/economics-canonical-smoke.ts

175 lines
7.9 KiB
TypeScript

#!/usr/bin/env tsx
/**
* economics-canonical-smoke.
*
* Locks the CANONICAL economics — the single source of truth in
* `@morphit/asset-registry` (inlined in `index.ts`) that the frontend
* quote and the indexer validation both import. People's money rides
* on these numbers, so this smoke pins:
*
* 1. The USD targets ($1 first order; 25¢ BTC/XMR; 12.5¢ BLURT)
* and that they're frozen.
* 2. The 50% BLURT-discount invariant (BLURT fee == half BTC/XMR).
* 3. The price→amount derivation helpers (and that they return
* null on an unusable price so callers fall back safely).
* 4. The static FEE_FALLBACK amounts implied by the reference
* prices.
* 5. isFeeCapableAsset matches the frozen fee_method assets.
* 6. The canonical fee-capable set, the registry's per-asset
* canPayListingFee flags, the LISTING_FEE_USD keys, and the
* assumed smallest-unit decimals all AGREE (no drift between
* the two sources of truth).
*
* If any of these change, that's an economics change — it must be
* deliberate, and every doc/FAQ/locale that states a cost has to
* move with it. This smoke is the tripwire.
*/
import {
FIRST_ORDER_MIN_USD,
LISTING_FEE_USD,
FEE_REFERENCE_PRICE_USD,
FEE_PRICE_TOLERANCE,
FEE_FALLBACK,
listingFeeBlurtBase,
listingFeeSatoshis,
listingFeePiconero,
isFeeCapableAsset,
ASSETS
} from '@morphit/asset-registry';
let scenarios = 0;
let failures = 0;
function check(name: string, cond: boolean): void {
scenarios++;
if (cond) {
console.log(`${name}`);
} else {
console.log(`${name}`);
failures++;
}
}
function main(): void {
console.log('economics-canonical-smoke\n');
// 1. USD targets + frozen
check('FIRST_ORDER_MIN_USD === 1.0', FIRST_ORDER_MIN_USD === 1.0);
check('LISTING_FEE_USD.blurt === 0.125', LISTING_FEE_USD.blurt === 0.125);
check('LISTING_FEE_USD.btc === 0.25', LISTING_FEE_USD.btc === 0.25);
check('LISTING_FEE_USD.xmr === 0.25', LISTING_FEE_USD.xmr === 0.25);
check('LISTING_FEE_USD is frozen', Object.isFrozen(LISTING_FEE_USD));
check('FEE_REFERENCE_PRICE_USD is frozen', Object.isFrozen(FEE_REFERENCE_PRICE_USD));
check('FEE_FALLBACK is frozen', Object.isFrozen(FEE_FALLBACK));
// 2. The 50% BLURT discount invariant — BLURT must be exactly half BTC/XMR.
check('BLURT fee == 50% of BTC fee', LISTING_FEE_USD.blurt === LISTING_FEE_USD.btc / 2);
check('BLURT fee == 50% of XMR fee', LISTING_FEE_USD.blurt === LISTING_FEE_USD.xmr / 2);
check('BTC fee == XMR fee', LISTING_FEE_USD.btc === LISTING_FEE_USD.xmr);
// 3. Tolerance band is a sane price-drift band (well above a rounding band, well below 1).
check('FEE_PRICE_TOLERANCE === 0.15', FEE_PRICE_TOLERANCE === 0.15);
check('FEE_PRICE_TOLERANCE in (0.01, 0.5)', FEE_PRICE_TOLERANCE > 0.01 && FEE_PRICE_TOLERANCE < 0.5);
// 4. Derivation helpers — target ÷ live price.
check('listingFeeBlurtBase(0.002) === 62.5', listingFeeBlurtBase(0.002) === 62.5);
check('listingFeeBlurtBase doubles when price halves', listingFeeBlurtBase(0.001) === 125);
check('listingFeeSatoshis(60000) === 417', listingFeeSatoshis(60000) === 417);
check('listingFeeSatoshis(30000) === 833', listingFeeSatoshis(30000) === 833); // round(0.25/30000*1e8)=833
check('listingFeePiconero(320) === 781250000n', listingFeePiconero(320) === 781250000n);
check('listingFeePiconero returns bigint', typeof listingFeePiconero(320) === 'bigint');
// Unusable price → null (so the caller falls back rather than dividing by zero).
check('listingFeeBlurtBase(0) === null', listingFeeBlurtBase(0) === null);
check('listingFeeBlurtBase(-1) === null', listingFeeBlurtBase(-1) === null);
check('listingFeeBlurtBase(NaN) === null', listingFeeBlurtBase(Number.NaN) === null);
check('listingFeeSatoshis(0) === null', listingFeeSatoshis(0) === null);
check('listingFeePiconero(0) === null', listingFeePiconero(0) === null);
check('listingFeeSatoshis(Infinity) === null', listingFeeSatoshis(Number.POSITIVE_INFINITY) === null);
// Black-hat: a garbage price feed must never crash or produce an
// absurd/zero fee. Tiny price → would-be ∞ (BigInt(∞) throws) → null.
// Huge price → would-be 0 satoshi/piconero (free listing) → null.
let piconeroThrew = false;
try {
listingFeePiconero(1e-310);
} catch {
piconeroThrew = true;
}
check('listingFeePiconero(tiny) does NOT throw', !piconeroThrew);
check('listingFeePiconero(1e-310) === null', listingFeePiconero(1e-310) === null);
check('listingFeeBlurtBase(1e-310) === null (no ∞)', listingFeeBlurtBase(1e-310) === null);
check('listingFeeSatoshis(1e-310) === null (no ∞)', listingFeeSatoshis(1e-310) === null);
check('listingFeeSatoshis(1e12) === null (no free listing)', listingFeeSatoshis(1e12) === null);
check('listingFeePiconero(1e12) === null (no free listing)', listingFeePiconero(1e12) === null);
// 5. FEE_FALLBACK == what the reference prices imply (the no-price safety net).
check('FEE_FALLBACK.blurtBase === 62.5', FEE_FALLBACK.blurtBase === 62.5);
check('FEE_FALLBACK.satoshis === 417', FEE_FALLBACK.satoshis === 417);
check('FEE_FALLBACK.piconero === 781250000n', FEE_FALLBACK.piconero === 781250000n);
check(
'FEE_FALLBACK.blurtBase === listingFeeBlurtBase(reference)',
FEE_FALLBACK.blurtBase === listingFeeBlurtBase(FEE_REFERENCE_PRICE_USD.blurt)
);
check(
'FEE_FALLBACK.satoshis === listingFeeSatoshis(reference)',
FEE_FALLBACK.satoshis === listingFeeSatoshis(FEE_REFERENCE_PRICE_USD.btc)
);
check(
'FEE_FALLBACK.piconero === listingFeePiconero(reference)',
FEE_FALLBACK.piconero === listingFeePiconero(FEE_REFERENCE_PRICE_USD.xmr)
);
// 6. Fee-capable assets match the frozen fee_method set (blurt|btc|xmr only).
check("isFeeCapableAsset('BLURT')", isFeeCapableAsset('BLURT'));
check("isFeeCapableAsset('BTC')", isFeeCapableAsset('BTC'));
check("isFeeCapableAsset('XMR')", isFeeCapableAsset('XMR'));
check("!isFeeCapableAsset('USDT')", !isFeeCapableAsset('USDT'));
check("!isFeeCapableAsset('USDC')", !isFeeCapableAsset('USDC'));
check("!isFeeCapableAsset('DOGE')", !isFeeCapableAsset('DOGE'));
// 7. ENFORCE the "mirrors canPayListingFee" claim across the WHOLE
// registry — isFeeCapableAsset and the per-asset canPayListingFee
// flag are two expressions of one truth; if a new asset is ever
// added with one but not the other, this fails before the drift
// can ship. (index.ts hardcodes the set to stay tied to the
// FROZEN fee_method enum; this proves the registry agrees.)
for (const a of ASSETS) {
check(
`isFeeCapableAsset('${a.ticker}') === canPayListingFee (${a.canPayListingFee})`,
isFeeCapableAsset(a.ticker) === a.canPayListingFee
);
}
// The fee-capable set is EXACTLY {BTC, XMR, BLURT} and LISTING_FEE_USD
// has exactly those three keys (lowercased) — no missing/extra.
const feeCapable = ASSETS.filter((a) => a.canPayListingFee)
.map((a) => a.ticker)
.sort();
check(
`fee-capable set is exactly BLURT/BTC/XMR (got ${feeCapable.join('/')})`,
feeCapable.length === 3 &&
feeCapable.includes('BLURT') &&
feeCapable.includes('BTC') &&
feeCapable.includes('XMR')
);
check(
'LISTING_FEE_USD keys === the fee-capable tickers (lowercased)',
Object.keys(LISTING_FEE_USD).sort().join(',') ===
feeCapable.map((t) => t.toLowerCase()).sort().join(',')
);
// The decimals the canonical derivation assumes (8 sat / 12 pico /
// 3 milliBLURT — baked into the FEE_FALLBACK + the helpers) must
// match the registry's per-asset decimals, or a derived amount
// would be off by orders of magnitude.
const dec = (t: string): number | undefined => ASSETS.find((a) => a.ticker === t)?.decimals;
check(`registry BTC decimals === 8 (got ${dec('BTC')})`, dec('BTC') === 8);
check(`registry XMR decimals === 12 (got ${dec('XMR')})`, dec('XMR') === 12);
check(`registry BLURT decimals === 3 (got ${dec('BLURT')})`, dec('BLURT') === 3);
console.log(
`\n${failures === 0 ? '✓ all' : '✗'} ${scenarios - failures}${failures === 0 ? '' : '/' + scenarios} scenarios passed`
);
process.exit(failures === 0 ? 0 : 1);
}
main();