morphit/packages/asset-registry/scripts/usdc-trade-only-smoke.ts

213 lines
8.4 KiB
TypeScript

#!/usr/bin/env tsx
/**
* usdc-trade-only-smoke.
*
* Part 122 cp30 sentinel: USDC must be `canPayListingFee: false`
* AND `canBeTraded: true` in BOTH the canonical and frontend
* asset registries. If a future contributor toggles either
* value the wrong way, this smoke fails loudly.
*
* Memory #23 invariant pinned from two directions (same as
* usdt-trade-only-smoke):
* - Canonical registry's `canPayListingFee: true → ticker ∈
* {BLURT, BTC, XMR}` rule means a future contributor
* flipping USDC's flag to true would fail the
* asset-registry-smoke first (good defence-in-depth).
* - This smoke is the USDC-specific sentinel: USDC must
* be trade-only. If the frontend's canBeUsedForListingFee
* drifts from the canonical's canPayListingFee, the user-
* facing form would offer USDC as a fee option while the
* indexer would reject it — confusing UX.
*
* Also asserts USDC carries the four supported networks
* (ERC-20, SPL, Base, Polygon), defaultNetwork: null (forcing
* explicit choice every trade), and privacyWarningKey:
* 'usdc_centralized'.
*
* Bonus scenarios specific to USDC (not in USDT smoke):
* - BEP-20 is INTENTIONALLY NOT in supportedNetworks (ADR-0028
* decline rationale: Binance-Peg wrapper + 18-decimal divergence)
* - TRC-20 is similarly excluded (Circle doesn't issue on Tron)
* - decimals === 6 (Circle's standard on all four supported
* networks; a future contributor adding BEP-20 USDC would need
* to confront the 18-decimal divergence first)
*/
import { ASSETS as CANONICAL } from '../src/index';
import { ASSETS as FRONTEND } from '../../../apps/web/src/lib/assets/registry';
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── usdc-trade-only smoke ─────────────────────────────\n');
// ── Scenario 1 — USDC is in the canonical registry ───────────────
const canonUsdc = CANONICAL.find((a) => a.ticker === 'USDC');
if (canonUsdc) {
pass('USDC is in canonical registry');
} else {
fail('USDC is in canonical registry', 'no USDC entry found in @morphit/asset-registry ASSETS');
}
// ── Scenario 2 — canonical USDC.canPayListingFee === false ───────
if (canonUsdc && canonUsdc.canPayListingFee === false) {
pass('canonical USDC.canPayListingFee === false (memory #23)');
} else {
fail(
'canonical USDC.canPayListingFee === false (memory #23)',
`USDC must be trade-only. fee_method enum is frozen at BLURT/BTC/XMR; USDC must not pay listing fees. Got canPayListingFee=${canonUsdc?.canPayListingFee}`
);
}
// ── Scenario 3 — canonical USDC.canBeTraded === true ─────────────
if (canonUsdc && canonUsdc.canBeTraded === true) {
pass('canonical USDC.canBeTraded === true');
} else {
fail(
'canonical USDC.canBeTraded === true',
`USDC must be tradable. Got canBeTraded=${canonUsdc?.canBeTraded}`
);
}
// ── Scenario 4 — canonical USDC supports 4 networks ──────────────
if (canonUsdc) {
const networks = [...canonUsdc.supportedNetworks].sort();
const expected = ['base', 'erc20', 'polygon', 'spl'];
if (JSON.stringify(networks) === JSON.stringify(expected)) {
pass('canonical USDC.supportedNetworks = [erc20, spl, base, polygon]');
} else {
fail(
'canonical USDC.supportedNetworks = [erc20, spl, base, polygon]',
`got ${JSON.stringify(networks)}, expected ${JSON.stringify(expected)}`
);
}
}
// ── Scenario 5 — BEP-20 is NOT in supportedNetworks (ADR-0028) ───
// Sentinel against a future contributor adding BEP-20 USDC without
// reading ADR-0028's decline rationale (Binance-Peg = 2 custodians
// + 18-decimal divergence). If you're here adding BEP-20 USDC,
// first read ADR-0028; update the network metadata module
// (decimals must be per-network if BEP-20 is added at 18); then
// update this smoke.
if (canonUsdc && !canonUsdc.supportedNetworks.includes('bep20')) {
pass('canonical USDC excludes bep20 (ADR-0028 decline)');
} else {
fail(
'canonical USDC excludes bep20 (ADR-0028 decline)',
'BEP-20 USDC is Binance-Peg (2 custodians) and uses 18 decimals — read ADR-0028 before adding'
);
}
// ── Scenario 6 — TRC-20 is NOT in supportedNetworks ──────────────
// Circle does not natively issue USDC on Tron — any presence
// there is community-bridged.
if (canonUsdc && !canonUsdc.supportedNetworks.includes('trc20')) {
pass('canonical USDC excludes trc20 (no native Circle issuance on Tron)');
} else {
fail(
'canonical USDC excludes trc20 (no native Circle issuance on Tron)',
'TRC-20 USDC is community-bridged, not Circle-native'
);
}
// ── Scenario 7 — defaultNetwork is null (forces explicit choice) ─
if (canonUsdc && canonUsdc.defaultNetwork === null) {
pass('canonical USDC.defaultNetwork === null (no default — forces explicit choice)');
} else {
fail(
'canonical USDC.defaultNetwork === null',
`Cross-network sends lose funds. Especially important here: ERC-20, Base, Polygon all use the EVM 0x[40 hex] shape — picker is the only disambiguator. Got ${canonUsdc?.defaultNetwork}`
);
}
// ── Scenario 8 — privacyWarningKey set ───────────────────────────
if (canonUsdc && canonUsdc.privacyWarningKey === 'usdc_centralized') {
pass('canonical USDC.privacyWarningKey === "usdc_centralized"');
} else {
fail(
'canonical USDC.privacyWarningKey === "usdc_centralized"',
`USDC must surface a privacy warning (Memory #19 priority #1). Got ${canonUsdc?.privacyWarningKey}`
);
}
// ── Scenario 9 — decimals === 6 ──────────────────────────────────
// Circle uses 6-decimal precision for USDC on all four supported
// networks. A future contributor adding BEP-20 (which uses 18
// decimals as the BSC token-standard default) would have to break
// this invariant.
if (canonUsdc && canonUsdc.decimals === 6) {
pass('canonical USDC.decimals === 6 (Circle standard across all 4 networks)');
} else {
fail(
'canonical USDC.decimals === 6 (Circle standard across all 4 networks)',
`Got decimals=${canonUsdc?.decimals}. If you're adding BEP-20 USDC (18 decimals), see ADR-0028 first.`
);
}
// ── Scenario 10 — frontend USDC entry mirrors canonical ──────────
const feUsdc = FRONTEND.find((a) => a.ticker === 'usdc');
if (feUsdc) {
pass('frontend USDC entry exists');
} else {
fail('frontend USDC entry exists', 'no usdc entry in apps/web/src/lib/assets/registry.ts');
}
// ── Scenario 11 — frontend canBeUsedForListingFee === false ──────
if (feUsdc && feUsdc.canBeUsedForListingFee === false) {
pass('frontend USDC.canBeUsedForListingFee === false (mirrors canonical)');
} else {
fail(
'frontend USDC.canBeUsedForListingFee === false',
`Frontend must mirror canonical. Drift = user-facing form offers USDC as a fee option while indexer rejects it. Got canBeUsedForListingFee=${feUsdc?.canBeUsedForListingFee}`
);
}
// ── Scenario 12 — frontend USDC.canBeTraded === true ─────────────
if (feUsdc && feUsdc.canBeTraded === true) {
pass('frontend USDC.canBeTraded === true');
} else {
fail(
'frontend USDC.canBeTraded === true',
`Frontend USDC must be tradable. Got canBeTraded=${feUsdc?.canBeTraded}`
);
}
// ── Scenario 13 — frontend USDC.defaultNetwork === null ──────────
if (feUsdc && feUsdc.defaultNetwork === null) {
pass('frontend USDC.defaultNetwork === null');
} else {
fail(
'frontend USDC.defaultNetwork === null',
`Got ${feUsdc?.defaultNetwork}`
);
}
// ── Scenario 14 — frontend USDC.privacyWarningKey set ────────────
if (feUsdc && feUsdc.privacyWarningKey === 'usdc_centralized') {
pass('frontend USDC.privacyWarningKey === "usdc_centralized"');
} else {
fail(
'frontend USDC.privacyWarningKey === "usdc_centralized"',
`Got ${feUsdc?.privacyWarningKey}`
);
}
const total = passed + failed;
console.log(`\n${passed} passed, ${failed} failed (${total} total)`);
if (failed > 0) {
console.error('\nusdc-trade-only smoke FAILED');
process.exit(1);
}
console.log(`✓ all ${total} usdc-trade-only scenarios passed`);