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

151 lines
7.4 KiB
TypeScript

#!/usr/bin/env tsx
/**
* dcr-trade-only-smoke.
*
* Part 122 cp43 sentinel: DCR (Decred) 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:
* - Canonical registry's `canPayListingFee: true → ticker ∈
* {BLURT, BTC, XMR}` rule means a future contributor flipping
* DCR's flag to true would fail the asset-registry-smoke first.
* - This smoke is the DCR-specific sentinel: DCR must be
* trade-only. If the frontend's canBeUsedForListingFee drifts
* from the canonical's canPayListingFee, the user-facing form
* would offer DCR as a fee option while the indexer would
* reject it — confusing UX.
*
* Also asserts DCR ships single-network (mainnet only),
* defaultNetwork: 'mainnet' (no picker shown), and
* privacyWarningKey: null (Decred is decentralized via hybrid
* PoW+PoS consensus and offers opt-in wallet-side CSPP mixing for
* users who want transaction-level privacy). optInPrivacyTech
* includes 'csppmix' to reflect the CoinShuffle++ wallet-side
* mixing protocol integrated into dcrwallet — NEW tech tag
* introduced at cp43.
*
* Address-validator coverage: Ds P2PKH-Secp256k1 (35 chars, most
* common), Dc P2SH (35 chars, multisig/escrow). Rejects Dp/Dr/De
* (extended pubkey/privkey/Edwards-curve — NOT used for receive).
*/
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── dcr-trade-only smoke ──────────────────────────────\n');
// ── Scenario 1 — canonical registry has DCR with correct flags ──
const canonDcr = CANONICAL.find((a) => a.ticker === 'DCR');
if (!canonDcr) {
fail('canonical registry contains DCR entry', "no ASSETS entry with ticker==='DCR'");
} else {
pass('canonical registry contains DCR entry');
if (canonDcr.canBeTraded === true) pass('canonical DCR.canBeTraded === true');
else fail('canonical DCR.canBeTraded === true', `actual: ${canonDcr.canBeTraded}`);
if (canonDcr.canPayListingFee === false) pass('canonical DCR.canPayListingFee === false (memory #23)');
else fail('canonical DCR.canPayListingFee === false (memory #23)', `actual: ${canonDcr.canPayListingFee}`);
if (canonDcr.decimals === 8) pass('canonical DCR.decimals === 8');
else fail('canonical DCR.decimals === 8', `actual: ${canonDcr.decimals}`);
if (Array.isArray(canonDcr.supportedNetworks) && canonDcr.supportedNetworks.length === 1 && canonDcr.supportedNetworks[0] === 'mainnet') {
pass('canonical DCR.supportedNetworks === [mainnet]');
} else {
fail('canonical DCR.supportedNetworks === [mainnet]', `actual: ${JSON.stringify(canonDcr.supportedNetworks)}`);
}
if (canonDcr.defaultNetwork === 'mainnet') pass('canonical DCR.defaultNetwork === mainnet');
else fail('canonical DCR.defaultNetwork === mainnet', `actual: ${canonDcr.defaultNetwork}`);
if (canonDcr.privacyWarningKey === null) pass('canonical DCR.privacyWarningKey === null (decentralized hybrid PoW/PoS)');
else fail('canonical DCR.privacyWarningKey === null', `actual: ${canonDcr.privacyWarningKey}`);
const pf = canonDcr.privacyFeatures;
if (pf && pf.freshAddressAdvice === 'hd-derived') pass('canonical DCR.privacyFeatures.freshAddressAdvice === hd-derived');
else fail('canonical DCR.privacyFeatures.freshAddressAdvice === hd-derived', `actual: ${JSON.stringify(pf)}`);
if (pf && Array.isArray(pf.optInPrivacyTech) && pf.optInPrivacyTech.includes('csppmix')) {
pass('canonical DCR.optInPrivacyTech includes csppmix (CoinShuffle++ wallet mixing)');
} else {
fail('canonical DCR.optInPrivacyTech includes csppmix', `actual: ${JSON.stringify(pf?.optInPrivacyTech)}`);
}
if (pf && pf.privacyGuideKey === 'dcr') pass('canonical DCR.privacyGuideKey === dcr');
else fail('canonical DCR.privacyGuideKey === dcr', `actual: ${pf?.privacyGuideKey}`);
}
// ── Scenario 2 — frontend registry parity ──
const feDcr = FRONTEND.find((a) => a.ticker === 'dcr');
if (!feDcr) {
fail('frontend registry contains dcr entry', "no ASSETS entry with ticker==='dcr'");
} else {
pass('frontend registry contains dcr entry');
if (feDcr.canBeTraded === true) pass('frontend dcr.canBeTraded === true');
else fail('frontend dcr.canBeTraded === true', `actual: ${feDcr.canBeTraded}`);
if (feDcr.canBeUsedForListingFee === false) pass('frontend dcr.canBeUsedForListingFee === false (memory #23)');
else fail('frontend dcr.canBeUsedForListingFee === false', `actual: ${feDcr.canBeUsedForListingFee}`);
if (feDcr.decimals === 8) pass('frontend dcr.decimals === 8');
else fail('frontend dcr.decimals === 8', `actual: ${feDcr.decimals}`);
}
// ── Scenario 3 — address validator coverage ──
if (canonDcr) {
// Specimens — valid Ds + Dc
const DS_VALID = 'Ds' + '1'.repeat(33); // 35 chars total
const DC_VALID = 'Dc' + '1'.repeat(33);
if (canonDcr.addressShape.test(DS_VALID)) pass('addressShape ACCEPTS valid Ds');
else fail('addressShape ACCEPTS valid Ds', `regex: ${canonDcr.addressShape}`);
if (canonDcr.addressShape.test(DC_VALID)) pass('addressShape ACCEPTS valid Dc');
else fail('addressShape ACCEPTS valid Dc', `regex: ${canonDcr.addressShape}`);
// Adversarial — should all REJECT
const REJECTS: Array<[string, string]> = [
['empty', ''],
['too short (34 chars)', 'Ds' + '1'.repeat(32)],
['too long (36 chars)', 'Ds' + '1'.repeat(34)],
['Dp extended pubkey (reject — not for receive)', 'Dp' + '1'.repeat(33)],
['Dr extended privkey (reject — SENSITIVE; never share)', 'Dr' + '1'.repeat(33)],
['De Edwards-curve (reject — not for regular receive)', 'De' + '1'.repeat(33)],
['Da invalid prefix', 'Da' + '1'.repeat(33)],
['ds lowercase d', 'ds' + '1'.repeat(33)],
['DS uppercase second char', 'DS' + '1'.repeat(33)],
['contains 0 (excluded from base58)', 'Ds0' + '1'.repeat(32)],
['contains O (excluded from base58)', 'DsO' + '1'.repeat(32)],
['contains I (excluded from base58)', 'DsI' + '1'.repeat(32)],
['contains l (excluded from base58)', 'Dsl' + '1'.repeat(32)],
['BTC P2PKH (1-prefix)', '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'],
['DASH X-address', 'XwnLY8qaFu8aGM9XRdcaQ9XgnnQ8AbnXkc'],
['DOGE D-address (DPHwLrG5... — DP not Ds/Dc)', 'DPHwLrG5Cm5R8KCNNfMcXSEoVbCRBkXxhf'],
['SQL injection', "Ds'; DROP TABLE--" + '1'.repeat(15)],
['XSS', '<script>alert(1)</script>'],
['null byte', 'Ds\u0000' + '1'.repeat(32)],
['leading whitespace', ' Ds' + '1'.repeat(33)],
['trailing whitespace', 'Ds' + '1'.repeat(33) + ' '],
['100K-char DoS', '1'.repeat(100000)]
];
let rejectAllPassed = true;
for (const [name, input] of REJECTS) {
if (canonDcr.addressShape.test(input)) {
fail(`addressShape REJECTS ${name}`, `accepted input: ${input.slice(0, 60)}`);
rejectAllPassed = false;
}
}
if (rejectAllPassed) pass(`addressShape REJECTS all ${REJECTS.length} adversarial inputs`);
}
const total = passed + failed;
console.log(`\n${passed} passed, ${failed} failed (${total} total)`);
if (failed > 0) {
console.error('\ndcr-trade-only smoke FAILED');
process.exit(1);
}
console.log(`✓ all ${total} dcr-trade-only scenarios passed`);