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

154 lines
7.4 KiB
TypeScript

#!/usr/bin/env tsx
/**
* eth-trade-only-smoke.
*
* Part 122 cp47 sentinel: ETH (Ethereum) must be
* `canPayListingFee: false` AND `canBeTraded: true` in BOTH the
* canonical and frontend asset registries.
*
* Memory #23 invariant pinned from two directions: canonical
* registry's `canPayListingFee: true → ticker ∈ {BLURT, BTC,
* XMR}` rule means a future contributor flipping ETH's flag to
* true would fail asset-registry-smoke first. This smoke is the
* ETH-specific sentinel.
*
* Also asserts ETH ships single-network (mainnet only),
* privacyWarningKey: null (decentralized PoS post-Merge),
* optInPrivacyTech: null (Ethereum has no native protocol-level
* mixing), 18 decimals (wei — EVM-standard ERC-20 precision),
* and addressShape accepts 0x-prefixed 40-hex strings.
*
* Address-validator coverage: 42 chars total (0x + 40 hex).
* Rejects non-hex chars, wrong length, missing prefix, etc.
*
* LL #50 NOTE: ETH addresses share their shape with USDT-ERC20,
* USDC-ERC20, DAI-ERC20, USDC-Base, USDC-Polygon, USDC-Arbitrum,
* DAI-Polygon, DAI-Arbitrum, DAI-Base — every EVM token-account
* address. This is by design — Ethereum addresses ARE 20-byte
* hex regardless of whether they hold native ETH or an ERC-20
* token on any EVM chain. The asset field (plus network field
* for multi-network assets) disambiguates at the order layer;
* cp42 address-shape-overlap-smoke documents the intentional
* cross-asset overlaps.
*/
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── eth-trade-only smoke ──────────────────────────────\n');
// ── Scenario 1 — canonical registry has ETH with correct flags ──
const canonEth = CANONICAL.find((a) => a.ticker === 'ETH');
if (!canonEth) {
fail('canonical registry contains ETH entry', "no ASSETS entry with ticker==='ETH'");
} else {
pass('canonical registry contains ETH entry');
if (canonEth.canBeTraded === true) pass('canonical ETH.canBeTraded === true');
else fail('canonical ETH.canBeTraded === true', `actual: ${canonEth.canBeTraded}`);
if (canonEth.canPayListingFee === false) pass('canonical ETH.canPayListingFee === false (memory #23)');
else fail('canonical ETH.canPayListingFee === false (memory #23)', `actual: ${canonEth.canPayListingFee}`);
if (canonEth.decimals === 18) pass('canonical ETH.decimals === 18 (wei)');
else fail('canonical ETH.decimals === 18 (wei)', `actual: ${canonEth.decimals}`);
if (Array.isArray(canonEth.supportedNetworks) && canonEth.supportedNetworks.length === 1 && canonEth.supportedNetworks[0] === 'mainnet') {
pass('canonical ETH.supportedNetworks === [mainnet]');
} else {
fail('canonical ETH.supportedNetworks === [mainnet]', `actual: ${JSON.stringify(canonEth.supportedNetworks)}`);
}
if (canonEth.defaultNetwork === 'mainnet') pass('canonical ETH.defaultNetwork === mainnet');
else fail('canonical ETH.defaultNetwork === mainnet', `actual: ${canonEth.defaultNetwork}`);
if (canonEth.privacyWarningKey === null) pass('canonical ETH.privacyWarningKey === null (decentralized PoS)');
else fail('canonical ETH.privacyWarningKey === null', `actual: ${canonEth.privacyWarningKey}`);
const pf = canonEth.privacyFeatures;
if (pf && pf.freshAddressAdvice === 'hd-derived') pass('canonical ETH.freshAddressAdvice === hd-derived');
else fail('canonical ETH.freshAddressAdvice === hd-derived', `actual: ${JSON.stringify(pf)}`);
if (pf && pf.optInPrivacyTech === null) {
pass('canonical ETH.optInPrivacyTech === null (no native mixing protocol; matches XMR/SOL convention)');
} else {
fail('canonical ETH.optInPrivacyTech === null', `actual: ${JSON.stringify(pf?.optInPrivacyTech)}`);
}
if (pf && pf.privacyGuideKey === 'eth') pass('canonical ETH.privacyGuideKey === eth');
else fail('canonical ETH.privacyGuideKey === eth', `actual: ${pf?.privacyGuideKey}`);
}
// ── Scenario 2 — frontend registry parity ──
const feEth = FRONTEND.find((a) => a.ticker === 'eth');
if (!feEth) {
fail('frontend registry contains eth entry', "no ASSETS entry with ticker==='eth'");
} else {
pass('frontend registry contains eth entry');
if (feEth.canBeTraded === true) pass('frontend eth.canBeTraded === true');
else fail('frontend eth.canBeTraded === true', `actual: ${feEth.canBeTraded}`);
if (feEth.canBeUsedForListingFee === false) pass('frontend eth.canBeUsedForListingFee === false (memory #23)');
else fail('frontend eth.canBeUsedForListingFee === false', `actual: ${feEth.canBeUsedForListingFee}`);
if (feEth.decimals === 18) pass('frontend eth.decimals === 18');
else fail('frontend eth.decimals === 18', `actual: ${feEth.decimals}`);
}
// ── Scenario 3 — address validator coverage ──
if (canonEth) {
const VALID_LOWER = '0x' + 'a'.repeat(40);
const VALID_MIXED = '0xAbCdEf0123456789abcDEF0123456789aBcDeF01'; // EIP-55-shaped (not real checksum)
const VALID_REAL = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; // vitalik.eth's known address
if (canonEth.addressShape.test(VALID_LOWER)) pass('addressShape ACCEPTS lowercase 0x+40hex');
else fail('addressShape ACCEPTS lowercase 0x+40hex', `regex: ${canonEth.addressShape}`);
if (canonEth.addressShape.test(VALID_MIXED)) pass('addressShape ACCEPTS EIP-55 mixed-case');
else fail('addressShape ACCEPTS EIP-55 mixed-case', `regex: ${canonEth.addressShape}`);
if (canonEth.addressShape.test(VALID_REAL)) pass('addressShape ACCEPTS real-shape address (vitalik.eth)');
else fail('addressShape ACCEPTS real-shape address', `regex: ${canonEth.addressShape}`);
const REJECTS: Array<[string, string]> = [
['empty', ''],
['no 0x prefix', 'a'.repeat(40)],
['0x but 39 hex (1 short)', '0x' + 'a'.repeat(39)],
['0x but 41 hex (1 long)', '0x' + 'a'.repeat(41)],
['0X uppercase prefix', '0X' + 'a'.repeat(40)],
['contains g (not hex)', '0x' + 'g'.repeat(40)],
['contains z (not hex)', '0x' + 'z'.repeat(40)],
['contains spaces', '0x ' + 'a'.repeat(39)],
['ENS name (alice.eth)', 'alice.eth'],
['ENS name (vitalik.eth)', 'vitalik.eth'],
['SQL injection', "'; DROP TABLE--abcdef0123456789abcdef01"],
['XSS', '<script>alert(1)</script>'],
['null byte', '\u0000' + 'a'.repeat(40)],
['leading whitespace', ' 0x' + 'a'.repeat(40)],
['trailing whitespace', '0x' + 'a'.repeat(40) + ' '],
['newline', '0x' + 'a'.repeat(40) + '\n'],
['100K-char DoS', '0x' + 'a'.repeat(100000)],
// Cross-chain rejection
['BTC P2PKH', '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'],
['SOL address', '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM'],
['XMR address (95-char)', '4' + 'A'.repeat(94)],
];
let rejectAllPassed = true;
let rejectCount = 0;
for (const [name, input] of REJECTS) {
if (canonEth.addressShape.test(input)) {
fail(`addressShape REJECTS ${name}`, `accepted input: ${input.slice(0, 60)}`);
rejectAllPassed = false;
}
rejectCount++;
}
if (rejectAllPassed) pass(`addressShape REJECTS all ${rejectCount} adversarial inputs`);
}
const total = passed + failed;
console.log(`\n${passed} passed, ${failed} failed (${total} total)`);
if (failed > 0) {
console.error('\neth-trade-only smoke FAILED');
process.exit(1);
}
console.log(`✓ all ${total} eth-trade-only scenarios passed`);