morphit/packages/asset-registry/scripts/fee-method-enum-frozen-smoke.ts

142 lines
5.2 KiB
TypeScript

#!/usr/bin/env tsx
/**
* fee-method-enum-frozen-smoke.
*
* Memory #23 (2026-05-13) declared a hard invariant:
*
* Listing fees can ONLY be paid in BLURT, XMR, or BTC.
* New tradable assets (USDT, ARRR, etc.) are peer-to-peer
* trading only. The indexer's `fee_method` enum stays
* frozen: `'blurt' | 'waived_first_buy' | 'btc' | 'xmr'`.
*
* This smoke is a sentinel that fails LOUDLY if anyone in the
* future tries to expand the enum (adding 'usdt', 'ltc', etc.).
* The asset-registry-smoke catches the registry side
* (canPayListingFee: true must imply ticker ∈ {BLURT,BTC,XMR});
* THIS smoke catches the wire-format side (the indexer's order
* handler hardcodes the union type).
*
* Belt + suspenders: a future contributor would have to update
* BOTH the registry AND this smoke AND the indexer order handler
* to add a new fee_method, which is exactly the friction we
* want — the invariant is documented in three places and any
* of them being inconsistent triggers a CI failure.
*
* Sentinel-grep against `apps/indexer/src/indexer/handlers/order.ts`:
* - The `fee_method` type union must appear exactly as the
* frozen 4-member set.
* - No string-literal `'usdt'`, `'ltc'`, `'doge'`, `'zec'`, `'arrr'`, `'dcr'`, `'sol'`, `'eth'`, `'xrp'` (the 'arrr' sentinel was a forward guard in cp30; it stays now that ARRR is a real asset at cp41 — both invariants (ARRR is a valid tradable; ARRR is NOT a valid fee_method) coexist on different axes),
* etc. on a `fee_method` line.
*
* Usage:
* tsx packages/asset-registry/scripts/fee-method-enum-frozen-smoke.ts
*/
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const REPO_ROOT = join(__dirname, '..', '..', '..');
const ORDER_HANDLER = join(
REPO_ROOT,
'apps/indexer/src/indexer/handlers/order.ts'
);
let failed = 0;
let passed = 0;
function pass(name: string): void {
console.log(`${name}`);
passed++;
}
function fail(name: string, detail: string): void {
console.error(`${name}`);
console.error(` ${detail}`);
failed++;
}
console.log('\n── fee-method-enum-frozen smoke ────────────────────────\n');
let body: string;
try {
body = readFileSync(ORDER_HANDLER, 'utf8');
} catch (err) {
fail(
'order.ts readable',
`could not read ${ORDER_HANDLER}: ${err}`
);
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(1);
}
// ── Scenario 1 — frozen union appears in the field type ──────────
const FROZEN_UNION = /readonly fee_method:\s*'blurt'\s*\|\s*'waived_first_buy'\s*\|\s*'btc'\s*\|\s*'xmr'/;
if (FROZEN_UNION.test(body)) {
pass(`fee_method field type union is frozen at BLURT/waived/BTC/XMR`);
} else {
fail(
`fee_method field type union is frozen at BLURT/waived/BTC/XMR`,
`expected 'readonly fee_method: 'blurt' | 'waived_first_buy' | 'btc' | 'xmr'' in order.ts; not found. Did someone add a new fee_method value?`
);
}
// ── Scenario 2 — initializer line uses the frozen union ──────────
const INITIALIZER_UNION = /let fee_method:\s*'blurt'\s*\|\s*'waived_first_buy'\s*\|\s*'btc'\s*\|\s*'xmr'/;
if (INITIALIZER_UNION.test(body)) {
pass(`fee_method initializer uses frozen union`);
} else {
fail(
`fee_method initializer uses frozen union`,
`expected 'let fee_method: 'blurt' | 'waived_first_buy' | 'btc' | 'xmr'' in order.ts; not found`
);
}
// ── Scenario 3 — no expansion tickers leaked in on fee_method ────
// If anyone added 'usdt', 'ltc', 'doge', 'arrr', 'eth', 'sol' as
// a fee_method value, the wire-format enum is no longer frozen.
const FORBIDDEN_TICKERS = ['usdt', 'ltc', 'doge', 'zec', 'arrr', 'eth', 'sol', 'bch', 'xlm', 'dash'];
let expansionFound = false;
for (const line of body.split('\n')) {
if (!line.includes('fee_method')) continue;
for (const tkr of FORBIDDEN_TICKERS) {
const re = new RegExp(`['"]${tkr}['"]`, 'i');
if (re.test(line)) {
fail(
`no expansion tickers in fee_method enum`,
`line contains '${tkr}' string literal next to fee_method: ${line.trim().slice(0, 120)}`
);
expansionFound = true;
}
}
}
if (!expansionFound) {
pass(`no expansion tickers (USDT/LTC/DOGE/ZEC/etc.) in fee_method enum`);
}
// ── Scenario 4 — the four required values are individually present ─
// Belt-on-the-belt: each member must appear on its own as a
// string literal in the file so future refactors don't silently
// drop one of them.
const REQUIRED_LITERALS = ["'blurt'", "'waived_first_buy'", "'btc'", "'xmr'"];
for (const lit of REQUIRED_LITERALS) {
if (body.includes(lit)) {
pass(`fee_method literal ${lit} present in order.ts`);
} else {
fail(
`fee_method literal ${lit} present in order.ts`,
`required literal ${lit} not found anywhere in order.ts`
);
}
}
const total = passed + failed;
console.log(`\n${passed} passed, ${failed} failed (${total} total)`);
if (failed > 0) {
console.error('\nfee-method-enum-frozen smoke FAILED');
process.exit(1);
}
// Canonical success line — run-smokes.sh greps for `^✓ all` to tally
console.log(`✓ all ${total} fee-method-enum-frozen scenarios passed`);