morphit/apps/indexer/scripts/api-response-shape-smoke.ts

625 lines
27 KiB
TypeScript
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.

/**
* /v1/listing-fee + /v1/stranger-fee-quote response-shape smokes.
*
* Exercises the pure response-body builders extracted from the
* Hono routes:
* - buildListingFeeBody (apps/indexer/src/api/listingFeeBody.ts)
* - buildStrangerFeeQuoteBody (apps/indexer/src/api/strangerFeeQuoteBody.ts)
* - isAccountName (apps/indexer/src/api/shared.ts)
*
* Why pure-helper smokes instead of full Hono route smokes:
* the sandbox doesn't have node_modules installed, so we can't
* load `hono` at runtime. The route handlers stay thin wrappers
* around the pure helpers; this smoke covers the interesting
* gating and response-shape logic. The Hono routing itself
* (URL matching, status codes for invalid input) is exercised
* indirectly via the helpers + isAccountName.
*
* Closes the API-endpoint smoke gap from §F.11 close-out
* residual #6.
*
* Usage (from apps/indexer):
* tsx scripts/api-response-shape-smoke.ts
*/
import { buildListingFeeBody } from '../src/api/listingFeeBody.ts';
import { buildStrangerFeeQuoteBody } from '../src/api/strangerFeeQuoteBody.ts';
import { buildStatsResponse } from '../src/api/stats.ts';
import { buildRpcEndpointsResponse } from '../src/api/rpcHealth.ts';
import type { EndpointState } from '@morphit/rpc-pool';
import { isAccountName } from '../src/api/shared.ts';
import { fakeConfig } from '../test/testutils/context.ts';
import type { BlurtPriceSource } from '../src/indexer/price/source.ts';
import type { Queryable } from '../src/indexer/strangerFeePricing.ts';
let failures = 0;
let scenarios = 0;
function scenario(name: string, fn: () => void | Promise<void>): Promise<void> {
scenarios++;
return Promise.resolve()
.then(fn)
.then(
() => {
console.log(`${name}`);
},
(err) => {
failures++;
console.log(`${name}`);
console.log(` ${err instanceof Error ? err.message : String(err)}`);
}
);
}
function assertEqual(actual: unknown, expected: unknown, label: string): void {
const a = JSON.stringify(actual);
const e = JSON.stringify(expected);
if (a !== e) {
throw new Error(`${label}: expected ${e}, got ${a}`);
}
}
function assertContains(haystack: Record<string, unknown>, key: string): void {
if (!(key in haystack)) {
throw new Error(`expected key ${key} in response body, missing`);
}
}
function assertAbsent(haystack: Record<string, unknown>, key: string): void {
if (key in haystack) {
throw new Error(`expected key ${key} ABSENT, but it's present`);
}
}
function makePriceSource(opts: { price: number; stale: boolean }): BlurtPriceSource {
return {
current: () => opts.price,
currentDetailed: () => ({
price: opts.price,
source: 'test',
updated_at: new Date(0),
stale: opts.stale
}),
start: () => undefined,
stop: () => undefined
};
}
function mockClientWithCount(count: number): Queryable {
return {
query: async <R extends Record<string, unknown>>(
text: string,
_params: readonly unknown[]
): Promise<{ rows: R[]; rowCount: number | null }> => {
if (!text.includes('paid_at >')) {
throw new Error(`unexpected query: ${text.slice(0, 80)}`);
}
return {
rows: [{ count: String(count) } as unknown as R],
rowCount: 1
};
}
};
}
// ─── /v1/listing-fee body ───────────────────────────────────────
await scenario('listing-fee body: returns base + feature-fee + ttl', () => {
const cfg = fakeConfig({ feeBaseBlurt: 60, featureFeeBlurtPerHour: 50 });
const body = buildListingFeeBody(cfg, null);
assertEqual(body.base_fee_blurt, 60, 'base_fee_blurt');
assertEqual(body.feature_fee_blurt_per_hour, 50, 'feature_fee_blurt_per_hour');
assertEqual(body.quote_ttl_seconds, 300, 'quote_ttl_seconds');
});
await scenario('listing-fee body: omits fiat echo when priceSource is null', () => {
const cfg = fakeConfig({ feeBaseBlurt: 60 });
const body = buildListingFeeBody(cfg, null);
assertAbsent(body, 'base_fee_fiat');
assertAbsent(body, 'blurt_price_fiat');
assertAbsent(body, 'denomination_fiat');
});
await scenario('listing-fee body: includes fiat echo when price is non-stale and positive', () => {
const cfg = fakeConfig({ feeBaseBlurt: 60, priceFeedEnabled: true });
const ps = makePriceSource({ price: 0.002, stale: false });
const body = buildListingFeeBody(cfg, ps);
assertContains(body, 'base_fee_fiat');
assertContains(body, 'blurt_price_fiat');
assertContains(body, 'denomination_fiat');
// Canonical Model A: base tracks LISTING_FEE_USD.blurt ÷ price =
// 62.5 BLURT at the $0.002 reference, so the echo is the canonical
// ~$0.125 — NOT feeBaseBlurt × price.
assertEqual(body.base_fee_fiat, 0.125, 'base_fee_fiat');
assertEqual(body.blurt_price_fiat, 0.002, 'blurt_price_fiat');
// cp128: default config denomination is 'USD'; operators in
// non-USD markets configure differently. The fakeConfig helper
// defaults to 'USD' to match.
assertEqual(body.denomination_fiat, 'USD', 'denomination_fiat');
});
await scenario('listing-fee body: omits fiat echo when price is stale', () => {
const cfg = fakeConfig({ feeBaseBlurt: 60, priceFeedEnabled: true });
const ps = makePriceSource({ price: 0.002, stale: true });
const body = buildListingFeeBody(cfg, ps);
assertAbsent(body, 'base_fee_fiat');
assertAbsent(body, 'blurt_price_fiat');
assertAbsent(body, 'denomination_fiat');
});
await scenario('listing-fee body: omits fiat echo when price is zero', () => {
const cfg = fakeConfig({ feeBaseBlurt: 60, priceFeedEnabled: true });
const ps = makePriceSource({ price: 0, stale: false });
const body = buildListingFeeBody(cfg, ps);
assertAbsent(body, 'base_fee_fiat');
assertAbsent(body, 'blurt_price_fiat');
assertAbsent(body, 'denomination_fiat');
});
await scenario('listing-fee body: omits fiat echo when price is negative', () => {
const cfg = fakeConfig({ feeBaseBlurt: 60, priceFeedEnabled: true });
const ps = makePriceSource({ price: -1, stale: false });
const body = buildListingFeeBody(cfg, ps);
assertAbsent(body, 'base_fee_fiat');
assertAbsent(body, 'blurt_price_fiat');
assertAbsent(body, 'denomination_fiat');
});
await scenario('listing-fee body: tracks operator-tunable feeBaseBlurt', () => {
// Operator runs feeBaseBlurt=80 — endpoint reports 80, the
// frontend (post-Option-1) reads it for fee math. This is
// the federation-tunability invariant in API-body form.
const cfg = fakeConfig({ feeBaseBlurt: 80 });
const body = buildListingFeeBody(cfg, null);
assertEqual(body.base_fee_blurt, 80, 'base_fee_blurt');
});
await scenario('listing-fee body: canonical display ignores feeBaseBlurt for the live amount', () => {
// Pre-cp372 the displayed fee was feeBaseBlurt-driven (80 × $0.002
// = $0.16). Under canonical Model A the DISPLAY tracks the
// canonical target (~$0.125) regardless of the operator's
// feeBaseBlurt — that value is now the enforcement floor, not the
// quote. (No-price fallback still reports feeBaseBlurt; see below.)
const cfg = fakeConfig({ feeBaseBlurt: 80, priceFeedEnabled: true });
const ps = makePriceSource({ price: 0.002, stale: false });
const body = buildListingFeeBody(cfg, ps);
assertEqual(body.base_fee_fiat, 0.125, 'base_fee_fiat is canonical, not 80×price');
assertEqual(body.base_fee_blurt, 62.5, 'base_fee_blurt is canonical 62.5, not 80');
});
// ── Model A (cp372): displayed base tracks the operator's USD fee ──
const approxEq = (actual: unknown, expected: number, label: string, eps = 1e-9): void => {
if (typeof actual !== 'number' || Math.abs(actual - expected) > eps) {
throw new Error(`${label}: expected ≈${expected}, got ${JSON.stringify(actual)}`);
}
};
await scenario('listing-fee body (Model A): USD base is canonical 62.5 at the reference price', () => {
const cfg = fakeConfig({ feeBaseBlurt: 60, priceFeedEnabled: true });
const ps = makePriceSource({ price: 0.002, stale: false }); // == reference
const body = buildListingFeeBody(cfg, ps);
approxEq(body.base_fee_blurt, 62.5, 'base_fee_blurt canonical at reference (not feeBaseBlurt)');
assertEqual(body.base_fee_blurt_live, true, 'base_fee_blurt_live');
});
await scenario('listing-fee body (Model A): BLURT appreciated → fewer BLURT, USD value held', () => {
// Price doubled to $0.004: canonical amount halves to 31.25 BLURT,
// USD value stays the canonical target ($0.125).
const cfg = fakeConfig({ feeBaseBlurt: 60, priceFeedEnabled: true });
const ps = makePriceSource({ price: 0.004, stale: false });
const body = buildListingFeeBody(cfg, ps);
approxEq(body.base_fee_blurt, 31.25, 'base_fee_blurt halves on 2× price');
approxEq(body.base_fee_fiat, 0.125, 'base_fee_fiat held at canonical target');
assertEqual(body.base_fee_blurt_live, true, 'base_fee_blurt_live');
});
await scenario('listing-fee body (Model A): BLURT depreciated → more BLURT, USD value held', () => {
// Price halved to $0.001: canonical amount doubles to 125 BLURT.
const cfg = fakeConfig({ feeBaseBlurt: 60, priceFeedEnabled: true });
const ps = makePriceSource({ price: 0.001, stale: false });
const body = buildListingFeeBody(cfg, ps);
approxEq(body.base_fee_blurt, 125, 'base_fee_blurt doubles on ½ price');
approxEq(body.base_fee_fiat, 0.125, 'base_fee_fiat held at canonical target');
});
await scenario('listing-fee body (Model A): canonical display is independent of feeBaseBlurt', () => {
// Two operators with different floors (60 vs 80) quote the SAME
// canonical amount — the floor never leaks into the quote.
const ps = makePriceSource({ price: 0.004, stale: false });
const a = buildListingFeeBody(fakeConfig({ feeBaseBlurt: 60, priceFeedEnabled: true }), ps);
const b = buildListingFeeBody(fakeConfig({ feeBaseBlurt: 80, priceFeedEnabled: true }), ps);
approxEq(a.base_fee_blurt, 31.25, 'operator A canonical');
approxEq(b.base_fee_blurt, 31.25, 'operator B canonical (same, ignores 80)');
});
await scenario('listing-fee body (Model A): base_fee_blurt_live false when no price', () => {
const cfg = fakeConfig({ feeBaseBlurt: 60 });
const body = buildListingFeeBody(cfg, null);
assertEqual(body.base_fee_blurt, 60, 'base_fee_blurt is pinned fallback');
assertEqual(body.base_fee_blurt_live, false, 'base_fee_blurt_live false');
});
await scenario('listing-fee body (Model A): base_fee_blurt_live false when price stale', () => {
const cfg = fakeConfig({ feeBaseBlurt: 60, priceFeedEnabled: true });
const ps = makePriceSource({ price: 0.002, stale: true });
const body = buildListingFeeBody(cfg, ps);
assertEqual(body.base_fee_blurt, 60, 'base_fee_blurt is pinned fallback when stale');
assertEqual(body.base_fee_blurt_live, false, 'base_fee_blurt_live false');
});
await scenario('listing-fee body (Model A): non-USD denomination keeps pinned base (no USD scaling)', () => {
// EUR operator: live USD figure isn't available in this route, so
// the base stays the operator's pinned feeBaseBlurt (graceful).
const cfg = fakeConfig({ feeBaseBlurt: 60, priceFeedEnabled: true, priceFeedDenominationFiat: 'EUR' });
const ps = makePriceSource({ price: 0.0018, stale: false });
const body = buildListingFeeBody(cfg, ps);
assertEqual(body.base_fee_blurt, 60, 'base_fee_blurt unscaled for non-USD');
assertEqual(body.base_fee_blurt_live, false, 'base_fee_blurt_live false for non-USD');
});
await scenario('listing-fee body (cp128): EUR-denominated operator returns EUR in denomination_fiat', () => {
const cfg = fakeConfig({
feeBaseBlurt: 60,
priceFeedEnabled: true,
priceFeedDenominationFiat: 'EUR'
});
const ps = makePriceSource({ price: 0.0018, stale: false });
const body = buildListingFeeBody(cfg, ps);
assertEqual(body.denomination_fiat, 'EUR', 'denomination_fiat');
assertEqual(body.blurt_price_fiat, 0.0018, 'blurt_price_fiat');
assertEqual(body.base_fee_fiat, 0.108, 'base_fee_fiat'); // 60 * 0.0018
});
await scenario('listing-fee body (cp128): XAU-denominated operator returns XAU in denomination_fiat', () => {
// Gold-denominated instance — pricing in fractional ounces.
const cfg = fakeConfig({
feeBaseBlurt: 60,
priceFeedEnabled: true,
priceFeedDenominationFiat: 'XAU'
});
const ps = makePriceSource({ price: 0.00000037, stale: false }); // ~$0.002 at $5500/oz
const body = buildListingFeeBody(cfg, ps);
assertEqual(body.denomination_fiat, 'XAU', 'denomination_fiat');
assertEqual(body.blurt_price_fiat, 0.00000037, 'blurt_price_fiat');
});
// ── Model A (cp372): live BTC/XMR fee amounts (USD-denominated) ──
await scenario('listing-fee body (Model A): BTC live amount equals pinned at the reference price', () => {
const cfg = fakeConfig({ priceFeedEnabled: true }); // btcFeeSatoshis defaults to 417
const btc = makePriceSource({ price: 60_000, stale: false });
const body = buildListingFeeBody(cfg, null, btc, null);
assertEqual(body.btc_fee_satoshis, 417, 'btc_fee_satoshis at reference');
approxEq(body.btc_fee_fiat, 0.2502, 'btc_fee_fiat ≈ $0.25', 1e-6);
assertEqual(body.btc_fee_live, true, 'btc_fee_live');
assertEqual(body.btc_price_fiat, 60_000, 'btc_price_fiat');
});
await scenario('listing-fee body (Model A): BTC depreciated → more satoshis, USD value held', () => {
// BTC halved to $30k: canonical $0.25 ÷ $30k = 833 sats (rounded).
const cfg = fakeConfig({ priceFeedEnabled: true });
const btc = makePriceSource({ price: 30_000, stale: false });
const body = buildListingFeeBody(cfg, null, btc, null);
assertEqual(body.btc_fee_satoshis, 833, 'btc_fee_satoshis ≈ canonical $0.25 worth at $30k');
approxEq(body.btc_fee_fiat, 0.25, 'btc_fee_fiat held at canonical target', 1e-3);
});
await scenario('listing-fee body (Model A): XMR live amount equals pinned at the reference price', () => {
const cfg = fakeConfig({ priceFeedEnabled: true }); // xmrFeePiconero defaults to 781250000
const xmr = makePriceSource({ price: 320, stale: false });
const body = buildListingFeeBody(cfg, null, null, xmr);
assertEqual(body.xmr_fee_piconero, '781250000', 'xmr_fee_piconero at reference');
approxEq(body.xmr_fee_fiat, 0.25, 'xmr_fee_fiat ≈ $0.25', 1e-6);
assertEqual(body.xmr_fee_live, true, 'xmr_fee_live');
assertEqual(body.xmr_price_fiat, 320, 'xmr_price_fiat');
});
await scenario('listing-fee body (Model A): XMR depreciated → more piconero, USD value held', () => {
// XMR halved to $160: the quoted piconero doubles.
const cfg = fakeConfig({ priceFeedEnabled: true });
const xmr = makePriceSource({ price: 160, stale: false });
const body = buildListingFeeBody(cfg, null, null, xmr);
assertEqual(body.xmr_fee_piconero, '1562500000', 'xmr_fee_piconero doubles on ½ price');
approxEq(body.xmr_fee_fiat, 0.25, 'xmr_fee_fiat held at target', 1e-6);
});
await scenario('listing-fee body (Model A): no BTC/XMR fields when sources absent', () => {
const cfg = fakeConfig({ priceFeedEnabled: true });
const body = buildListingFeeBody(cfg, null, null, null);
assertAbsent(body, 'btc_fee_satoshis');
assertAbsent(body, 'xmr_fee_piconero');
});
await scenario('listing-fee body (Model A): no BTC fields when BTC price stale', () => {
const cfg = fakeConfig({ priceFeedEnabled: true });
const btc = makePriceSource({ price: 60_000, stale: true });
const body = buildListingFeeBody(cfg, null, btc, null);
assertAbsent(body, 'btc_fee_satoshis');
});
await scenario('listing-fee body (Model A): no BTC fields when btcFeeSatoshis is 0 (asset not accepted)', () => {
const cfg = fakeConfig({ priceFeedEnabled: true, btcFeeSatoshis: 0 });
const btc = makePriceSource({ price: 60_000, stale: false });
const body = buildListingFeeBody(cfg, null, btc, null);
assertAbsent(body, 'btc_fee_satoshis');
});
await scenario('listing-fee body (Model A): non-USD denomination omits BTC/XMR live amounts', () => {
// No FX in this route → a non-USD operator can't express the USD
// target here; the UI falls back to the chain-pinned /v1/release
// amount instead.
const cfg = fakeConfig({ priceFeedEnabled: true, priceFeedDenominationFiat: 'EUR' });
const btc = makePriceSource({ price: 55_000, stale: false });
const xmr = makePriceSource({ price: 290, stale: false });
const body = buildListingFeeBody(cfg, null, btc, xmr);
assertAbsent(body, 'btc_fee_satoshis');
assertAbsent(body, 'xmr_fee_piconero');
});
// ─── /v1/stranger-fee-quote body ────────────────────────────────
await scenario('stranger-fee-quote body: base price for sender with no recent fees', async () => {
const body = await buildStrangerFeeQuoteBody(mockClientWithCount(0), 'alice');
assertEqual(body.account, 'alice', 'account');
assertEqual(body.base_price_blurt, 5, 'base_price_blurt');
assertEqual(body.price_blurt, 5, 'price_blurt');
assertEqual(body.multiplier, 1, 'multiplier');
assertEqual(body.recent_count, 0, 'recent_count');
assertEqual(body.window_minutes, 5, 'window_minutes');
assertEqual(body.capped, false, 'capped');
assertEqual(body.max_multiplier, 128, 'max_multiplier');
});
await scenario('stranger-fee-quote body: 1 recent fee → 2× multiplier (10 BLURT)', async () => {
const body = await buildStrangerFeeQuoteBody(mockClientWithCount(1), 'alice');
assertEqual(body.multiplier, 2, 'multiplier');
assertEqual(body.price_blurt, 10, 'price_blurt');
assertEqual(body.recent_count, 1, 'recent_count');
assertEqual(body.capped, false, 'capped');
});
await scenario(
'stranger-fee-quote body: 7 recent fees → 128× multiplier (cap, 640 BLURT)',
async () => {
const body = await buildStrangerFeeQuoteBody(mockClientWithCount(7), 'alice');
assertEqual(body.multiplier, 128, 'multiplier');
assertEqual(body.price_blurt, 640, 'price_blurt');
assertEqual(body.recent_count, 7, 'recent_count');
assertEqual(body.capped, true, 'capped');
}
);
await scenario(
'stranger-fee-quote body: 50 recent fees still capped at 128× (no overflow)',
async () => {
// Defense against unbounded multiplier growth — the cap holds
// even on absurd recent-counts. Without it, a sender at count
// 50 would face 2^50 × base = financial irrelevance.
const body = await buildStrangerFeeQuoteBody(mockClientWithCount(50), 'alice');
assertEqual(body.multiplier, 128, 'multiplier');
assertEqual(body.price_blurt, 640, 'price_blurt');
assertEqual(body.capped, true, 'capped');
}
);
await scenario('stranger-fee-quote body: account name passed through verbatim', async () => {
const body = await buildStrangerFeeQuoteBody(mockClientWithCount(0), 'bob-example-account');
assertEqual(body.account, 'bob-example-account', 'account');
});
// ─── isAccountName (the route's pre-validation) ─────────────────
await scenario('isAccountName: accepts a normal account name', () => {
if (!isAccountName('alice')) {
throw new Error('alice should be accepted');
}
if (!isAccountName('alice-2026')) {
throw new Error('alice-2026 should be accepted');
}
if (!isAccountName('bob123')) {
throw new Error('bob123 should be accepted');
}
});
await scenario('isAccountName: rejects empty string', () => {
if (isAccountName('')) {
throw new Error('empty string should be rejected');
}
});
await scenario('isAccountName: rejects too-short names (<3 chars)', () => {
if (isAccountName('ab')) {
throw new Error('ab (2 chars) should be rejected');
}
});
await scenario('isAccountName: rejects too-long names (>16 chars)', () => {
if (isAccountName('abcdefghijklmnopq')) {
throw new Error('17-char name should be rejected');
}
});
await scenario('isAccountName: rejects uppercase', () => {
if (isAccountName('Alice')) {
throw new Error('uppercase should be rejected');
}
});
await scenario('isAccountName: rejects underscores', () => {
if (isAccountName('my_account')) {
throw new Error('underscore should be rejected');
}
});
await scenario('isAccountName: rejects names starting with a digit', () => {
if (isAccountName('1alice')) {
throw new Error('digit-leading should be rejected');
}
});
await scenario('isAccountName: rejects non-string input', () => {
if (isAccountName(null)) throw new Error('null should be rejected');
if (isAccountName(undefined)) throw new Error('undefined should be rejected');
if (isAccountName(123)) throw new Error('number should be rejected');
});
// ─── /v1/stats (buildStatsResponse) ──────────────────────────────
await scenario('stats: shapes counts from aggregate rows', () => {
const r = buildStatsResponse(
{ active: '12', total: '340', assets_active: '4', fiats_active: '7' },
{ completed_total: '88', completed_30d: '19' },
[],
new Date('2026-07-02T00:00:00.000Z')
);
assertEqual(r.orders, { active: 12, total: 340 }, 'stats.orders');
assertEqual(r.trades, { completed_total: 88, completed_30d: 19 }, 'stats.trades');
assertEqual(r.assets.with_active_orders, 4, 'stats.assets.with_active_orders');
assertEqual(r.fiat_currencies.with_active_orders, 7, 'stats.fiats');
assertEqual(r.network, 'morphit', 'stats.network');
assertEqual(r.generated_at, '2026-07-02T00:00:00.000Z', 'stats.generated_at');
});
await scenario('stats: supported assets = canonical minus disabled (case-insensitive)', () => {
const r = buildStatsResponse(undefined, undefined, ['xmr', 'DOGE']);
if (r.assets.supported.includes('XMR')) throw new Error('XMR should be filtered (disabled)');
if (r.assets.supported.includes('DOGE')) throw new Error('DOGE should be filtered (disabled)');
if (!r.assets.supported.includes('BTC')) throw new Error('BTC should remain supported');
// 16 canonical tickers minus 2 disabled = 14.
assertEqual(r.assets.supported.length, 14, 'supported count');
});
await scenario('stats: PRIVACY — response has ONLY aggregate keys, nothing per-account', () => {
const r = buildStatsResponse(
{ active: '1', total: '1', assets_active: '1', fiats_active: '1' },
{ completed_total: '1', completed_30d: '1' },
[]
) as unknown as Record<string, unknown>;
// Exact top-level key set — any new field must be a deliberate, reviewed
// change, not an accidental leak of per-account data.
const keys = Object.keys(r).sort();
assertEqual(
keys,
['assets', 'fiat_currencies', 'generated_at', 'network', 'orders', 'trades'],
'stats top-level keys'
);
// Belt-and-suspenders: explicitly forbid any per-account / de-anonymizing shape.
for (const forbidden of [
'accounts',
'users',
'by_account',
'per_user',
'traders',
'account',
'top_accounts',
'volume',
'balances'
]) {
assertAbsent(r, forbidden);
}
});
await scenario('stats: missing rows → zeros (empty DB / query miss)', () => {
const r = buildStatsResponse(undefined, undefined, []);
assertEqual(r.orders, { active: 0, total: 0 }, 'orders zeros');
assertEqual(r.trades, { completed_total: 0, completed_30d: 0 }, 'trades zeros');
assertEqual(r.assets.with_active_orders, 0, 'assets zero');
assertEqual(r.fiat_currencies.with_active_orders, 0, 'fiats zero');
});
await scenario('stats: garbage / negative counts coerce to 0', () => {
const r = buildStatsResponse(
{ active: 'abc', total: '-5', assets_active: '', fiats_active: 'NaN' },
{ completed_total: '-1', completed_30d: 'x' },
[]
);
assertEqual(r.orders, { active: 0, total: 0 }, 'garbage orders → 0');
assertEqual(r.trades, { completed_total: 0, completed_30d: 0 }, 'garbage trades → 0');
assertEqual(r.assets.with_active_orders, 0, 'garbage assets → 0');
});
// ─── /v1/rpc-endpoints (buildRpcEndpointsResponse) ───────────────
const NOW = 1_700_000_000_000;
const mkState = (o: Partial<EndpointState> & { url: string }): EndpointState => ({
url: o.url,
ewmaLatencyMs: o.ewmaLatencyMs ?? null,
consecutiveFailures: o.consecutiveFailures ?? 0,
cooldownUntil: o.cooldownUntil ?? 0,
// cp474 — the RPS pacer's per-endpoint cursor. Present only so this fixture
// satisfies EndpointState; `buildRpcEndpointsResponse` projects named fields
// and deliberately does NOT publish it (an internal pacing cursor is not
// operator-facing), which is exactly what the shape assertions below pin.
nextAllowedAt: o.nextAllowedAt ?? 0,
lastSuccessAt: o.lastSuccessAt ?? 0
});
const CANON = ['https://a.example', 'https://b.example', 'https://c.example'];
await scenario('rpc-endpoints: PRIVACY — operator-custom URLs are filtered out', () => {
const snap = [
mkState({ url: 'https://a.example', ewmaLatencyMs: 120 }),
mkState({ url: 'https://secret-internal.op', ewmaLatencyMs: 5 })
];
const r = buildRpcEndpointsResponse(snap, CANON, NOW);
assertEqual(
r.endpoints.map((e) => e.url),
['https://a.example'],
'only the canonical URL is present; the custom one is dropped'
);
});
await scenario('rpc-endpoints: order follows the canonical list; missing nodes omitted', () => {
const snap = [
mkState({ url: 'https://c.example', ewmaLatencyMs: 30 }),
mkState({ url: 'https://a.example', ewmaLatencyMs: 10 })
];
const r = buildRpcEndpointsResponse(snap, CANON, NOW);
// b.example is canonical but not in this pool → omitted; a before c (canon order).
assertEqual(r.endpoints.map((e) => e.url), ['https://a.example', 'https://c.example'], 'canon order, b omitted');
});
await scenario('rpc-endpoints: healthy = no failures AND not in cooldown; latency passes through', () => {
const r = buildRpcEndpointsResponse([mkState({ url: 'https://a.example', ewmaLatencyMs: 140 })], CANON, NOW);
assertEqual(r.endpoints[0]?.healthy, true, 'healthy');
assertEqual(r.endpoints[0]?.latency_ms, 140, 'latency passthrough');
assertEqual(r.endpoints[0]?.cooldown_ms, 0, 'no cooldown');
});
await scenario('rpc-endpoints: consecutive failures → not healthy', () => {
const r = buildRpcEndpointsResponse(
[mkState({ url: 'https://a.example', consecutiveFailures: 3 })],
CANON,
NOW
);
assertEqual(r.endpoints[0]?.healthy, false, 'failing → unhealthy');
assertEqual(r.endpoints[0]?.consecutive_failures, 3, 'failure count surfaced');
});
await scenario('rpc-endpoints: in cooldown → not healthy, cooldown_ms is remaining time', () => {
const r = buildRpcEndpointsResponse(
[mkState({ url: 'https://a.example', cooldownUntil: NOW + 45_000 })],
CANON,
NOW
);
assertEqual(r.endpoints[0]?.healthy, false, 'cooling down → unhealthy');
assertEqual(r.endpoints[0]?.cooldown_ms, 45_000, 'remaining cooldown');
});
await scenario('rpc-endpoints: past cooldown clamps to 0 (not negative)', () => {
const r = buildRpcEndpointsResponse(
[mkState({ url: 'https://a.example', cooldownUntil: NOW - 10_000 })],
CANON,
NOW
);
assertEqual(r.endpoints[0]?.cooldown_ms, 0, 'expired cooldown clamps to 0');
assertEqual(r.endpoints[0]?.healthy, true, 'expired cooldown + no failures → healthy');
});
// ─── Final report ───────────────────────────────────────────────
console.log();
console.log('────────────────────────────────────────────────────────────');
if (failures > 0) {
console.log(`${failures}/${scenarios} scenarios failed`);
process.exit(1);
}
console.log(`✓ all ${scenarios} scenarios passed`);