166 lines
8.1 KiB
TypeScript
166 lines
8.1 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* xrp-trade-only-smoke.
|
|
*
|
|
* Part 122 cp49 sentinel: XRP (Ripple) 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 XRP's flag to
|
|
* true would fail asset-registry-smoke first. This smoke is the
|
|
* XRP-specific sentinel.
|
|
*
|
|
* Also asserts XRP ships single-network (XRPL mainnet only),
|
|
* privacyWarningKey: null (native XRP cannot be frozen — freeze
|
|
* flag applies only to issued tokens/IOUs not native XRP),
|
|
* optInPrivacyTech: null (XRPL has no native protocol-level
|
|
* mixing), 6 decimals (drops — 1 XRP = 10^6 drops), and
|
|
* addressShape accepts r-prefixed 24-34 base58 strings.
|
|
*
|
|
* Address-validator coverage: 25-35 chars total (r + 24-34 base58).
|
|
* Rejects non-base58 chars, wrong length, missing 'r' prefix.
|
|
*
|
|
* LL #50 NOTE: XRP addresses use the 'r' prefix which is unique
|
|
* among Morphit assets. No cross-asset overlap expected — but
|
|
* the address-shape-overlap-smoke at cp42 captures any unexpected
|
|
* collisions.
|
|
*
|
|
* XRPL-SPECIFIC UX NOTES (documented in privacy.guides.xrp):
|
|
* - Destination tags (32-bit integers) ride in the URI query
|
|
* string `?dt=N`, not in the address regex. Sending to an
|
|
* exchange-hosted address without the required tag practically
|
|
* loses funds (recoverable via exchange support only).
|
|
* - Reserve requirement: XRPL accounts need ≥1 XRP base reserve
|
|
* to exist. Sending <1 XRP to a never-funded address fails.
|
|
*/
|
|
|
|
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── xrp-trade-only smoke ──────────────────────────────\n');
|
|
|
|
// ── Scenario 1 — canonical registry has XRP with correct flags ──
|
|
const canonXrp = CANONICAL.find((a) => a.ticker === 'XRP');
|
|
if (!canonXrp) {
|
|
fail('canonical registry contains XRP entry', "no ASSETS entry with ticker==='XRP'");
|
|
} else {
|
|
pass('canonical registry contains XRP entry');
|
|
if (canonXrp.canBeTraded === true) pass('canonical XRP.canBeTraded === true');
|
|
else fail('canonical XRP.canBeTraded === true', `actual: ${canonXrp.canBeTraded}`);
|
|
if (canonXrp.canPayListingFee === false) pass('canonical XRP.canPayListingFee === false (memory #23)');
|
|
else fail('canonical XRP.canPayListingFee === false (memory #23)', `actual: ${canonXrp.canPayListingFee}`);
|
|
if (canonXrp.decimals === 6) pass('canonical XRP.decimals === 6 (drops)');
|
|
else fail('canonical XRP.decimals === 6 (drops)', `actual: ${canonXrp.decimals}`);
|
|
if (Array.isArray(canonXrp.supportedNetworks) && canonXrp.supportedNetworks.length === 1 && canonXrp.supportedNetworks[0] === 'mainnet') {
|
|
pass('canonical XRP.supportedNetworks === [mainnet]');
|
|
} else {
|
|
fail('canonical XRP.supportedNetworks === [mainnet]', `actual: ${JSON.stringify(canonXrp.supportedNetworks)}`);
|
|
}
|
|
if (canonXrp.defaultNetwork === 'mainnet') pass('canonical XRP.defaultNetwork === mainnet');
|
|
else fail('canonical XRP.defaultNetwork === mainnet', `actual: ${canonXrp.defaultNetwork}`);
|
|
if (canonXrp.privacyWarningKey === null) {
|
|
pass('canonical XRP.privacyWarningKey === null (native XRP cannot be frozen — freeze applies only to IOUs)');
|
|
} else {
|
|
fail('canonical XRP.privacyWarningKey === null', `actual: ${canonXrp.privacyWarningKey}`);
|
|
}
|
|
const pf = canonXrp.privacyFeatures;
|
|
if (pf && pf.freshAddressAdvice === 'hd-derived') pass('canonical XRP.freshAddressAdvice === hd-derived');
|
|
else fail('canonical XRP.freshAddressAdvice === hd-derived', `actual: ${JSON.stringify(pf)}`);
|
|
if (pf && pf.optInPrivacyTech === null) {
|
|
pass('canonical XRP.optInPrivacyTech === null (no native mixing protocol; matches XMR/SOL/ETH convention)');
|
|
} else {
|
|
fail('canonical XRP.optInPrivacyTech === null', `actual: ${JSON.stringify(pf?.optInPrivacyTech)}`);
|
|
}
|
|
if (pf && pf.privacyGuideKey === 'xrp') pass('canonical XRP.privacyGuideKey === xrp');
|
|
else fail('canonical XRP.privacyGuideKey === xrp', `actual: ${pf?.privacyGuideKey}`);
|
|
}
|
|
|
|
// ── Scenario 2 — frontend registry parity ──
|
|
const feXrp = FRONTEND.find((a) => a.ticker === 'xrp');
|
|
if (!feXrp) {
|
|
fail('frontend registry contains xrp entry', "no ASSETS entry with ticker==='xrp'");
|
|
} else {
|
|
pass('frontend registry contains xrp entry');
|
|
if (feXrp.canBeTraded === true) pass('frontend xrp.canBeTraded === true');
|
|
else fail('frontend xrp.canBeTraded === true', `actual: ${feXrp.canBeTraded}`);
|
|
if (feXrp.canBeUsedForListingFee === false) pass('frontend xrp.canBeUsedForListingFee === false (memory #23)');
|
|
else fail('frontend xrp.canBeUsedForListingFee === false', `actual: ${feXrp.canBeUsedForListingFee}`);
|
|
if (feXrp.decimals === 6) pass('frontend xrp.decimals === 6');
|
|
else fail('frontend xrp.decimals === 6', `actual: ${feXrp.decimals}`);
|
|
}
|
|
|
|
// ── Scenario 3 — address validator coverage ──
|
|
if (canonXrp) {
|
|
// Real-world XRPL addresses for shape testing
|
|
const VALID_BITSTAMP = 'rDsbeomae4FXwgQTJp9Rs64Qg9vDiTCdBv'; // Bitstamp hot wallet (publicly known)
|
|
const VALID_BINANCE = 'rEy8TFcrAPvhpKrwyrscNYyqBGUkE9hKaJ'; // Binance hot wallet (publicly known)
|
|
const VALID_RIPPLE = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'; // Ripple's known address
|
|
if (canonXrp.addressShape.test(VALID_BITSTAMP)) pass('addressShape ACCEPTS real-shape Bitstamp address');
|
|
else fail('addressShape ACCEPTS real-shape Bitstamp address', `regex: ${canonXrp.addressShape}`);
|
|
if (canonXrp.addressShape.test(VALID_BINANCE)) pass('addressShape ACCEPTS real-shape Binance address');
|
|
else fail('addressShape ACCEPTS real-shape Binance address', `regex: ${canonXrp.addressShape}`);
|
|
if (canonXrp.addressShape.test(VALID_RIPPLE)) pass('addressShape ACCEPTS real-shape Ripple address');
|
|
else fail('addressShape ACCEPTS real-shape Ripple address', `regex: ${canonXrp.addressShape}`);
|
|
|
|
const REJECTS: Array<[string, string]> = [
|
|
['empty', ''],
|
|
['no r prefix', 'Hb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'],
|
|
['uppercase R prefix', 'RHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'],
|
|
['1-prefix (BTC P2PKH-shape)', '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'],
|
|
['too short (r + 23)', 'r' + '1'.repeat(23)],
|
|
['too long (r + 35)', 'r' + '1'.repeat(35)],
|
|
['contains 0 (excluded base58)', 'r0' + '1'.repeat(30)],
|
|
['contains O (excluded base58)', 'rO' + '1'.repeat(30)],
|
|
['contains I (excluded base58)', 'rI' + '1'.repeat(30)],
|
|
['contains l (excluded base58)', 'rl' + '1'.repeat(30)],
|
|
['contains spaces', 'r ' + '1'.repeat(30)],
|
|
['has destination tag suffix', 'rDsbeomae4FXwgQTJp9Rs64Qg9vDiTCdBv?dt=12345'],
|
|
['has destination tag colon', 'rDsbeomae4FXwgQTJp9Rs64Qg9vDiTCdBv:12345'],
|
|
['SQL injection', "'; DROP TABLE--"],
|
|
['XSS', '<script>alert(1)</script>'],
|
|
['null byte', '\u0000rHb9CJAWyB4rj91VRWn96Dku'],
|
|
['leading whitespace', ' rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'],
|
|
['trailing whitespace', 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh '],
|
|
['newline', 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh\n'],
|
|
['100K-char DoS', 'r' + '1'.repeat(100000)],
|
|
// Cross-chain rejection
|
|
['BTC P2PKH', '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'],
|
|
['SOL address', '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM'],
|
|
['ETH address', '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'],
|
|
['XMR address (95-char)', '4' + 'A'.repeat(94)],
|
|
];
|
|
let rejectAllPassed = true;
|
|
let rejectCount = 0;
|
|
for (const [name, input] of REJECTS) {
|
|
if (canonXrp.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('\nxrp-trade-only smoke FAILED');
|
|
process.exit(1);
|
|
}
|
|
console.log(`✓ all ${total} xrp-trade-only scenarios passed`);
|