Files
cross-chain-pmm-lps/scripts/validate-deployment-status.cjs
2026-04-14 07:13:17 -07:00

211 lines
8.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Validates config/deployment-status.json for "minimum viable deployed graph".
* Use in CI so deployment-realistic sim cannot run with half-filled state.
*
* Rules:
* - If bridgeAvailable === true on a chain, cwTokens must include at least cWUSDT and cWUSDC (phase 1).
* - For each pmmPool / pmmPoolsVolatile[]: role in {defense, public_routing, truu_routing},
* feeBps and k present, base/quote (or tokenIn/tokenOut) exist in cwTokens or anchorAddresses.
* TRUU must be listed under anchorAddresses when used as quote (e.g. mainnet chain 1).
* - Any row marked live/routingVisible/publicRoutingEnabled must use a native protocol contract,
* not a placeholder scaffold or aggregator-only/non-native lane.
*
* Exit code: 0 if valid, 1 if invalid (and prints errors to stderr).
*/
const fs = require('fs');
const path = require('path');
const CONFIG_DIR = path.join(__dirname, '..', 'config');
let DEPLOYMENT_STATUS_PATH = path.join(CONFIG_DIR, 'deployment-status.json');
const PHASE1_CW = ['cWUSDT', 'cWUSDC'];
const VALID_ROLES = ['defense', 'public_routing', 'truu_routing'];
const VALID_REFERENCE_PROTOCOLS = ['uniswap_v3', 'balancer', 'curve', '1inch'];
const VALID_NATIVE_REFERENCE_PROTOCOLS = ['uniswap_v3', 'balancer', 'curve'];
function looksPlaceholderAddress(address) {
if (!address || typeof address !== 'string') return false;
const normalized = address.toLowerCase();
if (!/^0x[0-9a-f]{40}$/.test(normalized)) return false;
if (normalized === '0x0000000000000000000000000000000000000000') return true;
const body = normalized.slice(2);
const placeholderPrefixes = ['d0', '71', 'ba', 'c7'];
const zeroCount = body.split('0').length - 1;
return placeholderPrefixes.some((prefix) => body.startsWith(prefix)) && zeroCount >= 24;
}
function isLiveRow(row) {
return row?.live === true || row?.routingVisible === true || row?.publicRoutingEnabled === true;
}
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, 'utf8'));
}
function validatePoolEntries(chainId, pools, listLabel, knownTokens, errors) {
for (let i = 0; i < pools.length; i++) {
const pool = pools[i];
const base = pool.base ?? pool.tokenIn;
const quote = pool.quote ?? pool.tokenOut;
if (!VALID_ROLES.includes(pool.role)) {
errors.push(`Chain ${chainId} ${listLabel}[${i}]: role must be one of ${VALID_ROLES.join(', ')}`);
}
if (pool.feeBps == null || pool.k == null) {
errors.push(`Chain ${chainId} ${listLabel}[${i}]: feeBps and k required`);
}
if (base && !knownTokens.has(base)) {
errors.push(`Chain ${chainId} ${listLabel}[${i}]: base/tokenIn "${base}" not in cwTokens or anchorAddresses`);
}
if (quote && !knownTokens.has(quote)) {
errors.push(`Chain ${chainId} ${listLabel}[${i}]: quote/tokenOut "${quote}" not in cwTokens or anchorAddresses`);
}
const addr = pool.poolAddress;
if (addr != null && addr !== '') {
const z = String(addr).toLowerCase();
if (z === '0x0000000000000000000000000000000000000000') {
errors.push(`Chain ${chainId} ${listLabel}[${i}]: poolAddress must not be zero when set`);
}
if (pool.publicRoutingEnabled === true && looksPlaceholderAddress(z)) {
errors.push(`Chain ${chainId} ${listLabel}[${i}]: live public routing poolAddress must use a native protocol contract, not a placeholder scaffold (${addr})`);
}
}
if (pool.publicRoutingEnabled === true && pool.venue && pool.venue !== 'dodo_pmm') {
errors.push(`Chain ${chainId} ${listLabel}[${i}]: public routing rows must use the native dodo_pmm venue, not "${pool.venue}"`);
}
}
}
function main() {
const status = loadJson(DEPLOYMENT_STATUS_PATH);
const chains = status.chains || {};
const errors = [];
for (const [chainId, chain] of Object.entries(chains)) {
const cwTokens = chain.cwTokens || {};
const gasMirrors = chain.gasMirrors || {};
const anchorAddresses = chain.anchorAddresses || {};
const gasQuoteAddresses = chain.gasQuoteAddresses || {};
const pmmPools = chain.pmmPools || [];
const pmmPoolsVolatile = chain.pmmPoolsVolatile || [];
const gasPmmPools = chain.gasPmmPools || [];
const gasReferenceVenues = chain.gasReferenceVenues || [];
const bridgeAvailable = chain.bridgeAvailable;
if (bridgeAvailable === true) {
for (const sym of PHASE1_CW) {
if (!cwTokens[sym] || typeof cwTokens[sym] !== 'string' || !cwTokens[sym].trim()) {
errors.push(`Chain ${chainId} (${chain.name}): bridgeAvailable=true but cwTokens.${sym} missing or empty`);
}
}
}
const knownTokens = new Set([
...Object.keys(cwTokens),
...Object.keys(gasMirrors),
...Object.keys(anchorAddresses),
...Object.keys(gasQuoteAddresses),
]);
validatePoolEntries(chainId, pmmPools, 'pmmPools', knownTokens, errors);
validatePoolEntries(chainId, pmmPoolsVolatile, 'pmmPoolsVolatile', knownTokens, errors);
validatePoolEntries(chainId, gasPmmPools, 'gasPmmPools', knownTokens, errors);
const gasPoolsByFamily = new Map();
for (const pool of gasPmmPools) {
if (!pool.familyKey || typeof pool.familyKey !== 'string') {
errors.push(`Chain ${chainId} gasPmmPools entry is missing familyKey`);
continue;
}
if (!gasPoolsByFamily.has(pool.familyKey)) gasPoolsByFamily.set(pool.familyKey, []);
gasPoolsByFamily.get(pool.familyKey).push(pool);
}
for (const [familyKey, pools] of gasPoolsByFamily.entries()) {
const poolTypes = new Set(pools.map((pool) => pool.poolType));
if (!poolTypes.has('wrapped_native')) {
errors.push(`Chain ${chainId} gas family ${familyKey}: missing wrapped_native DODO pool`);
}
if (!poolTypes.has('stable_quote')) {
errors.push(`Chain ${chainId} gas family ${familyKey}: missing stable_quote DODO pool`);
}
}
const referenceVenuesByFamily = new Map();
for (let i = 0; i < gasReferenceVenues.length; i++) {
const venue = gasReferenceVenues[i];
if (!VALID_REFERENCE_PROTOCOLS.includes(venue.protocol)) {
errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: protocol must be one of ${VALID_REFERENCE_PROTOCOLS.join(', ')}`);
}
if (!venue.familyKey || typeof venue.familyKey !== 'string') {
errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: familyKey required`);
continue;
}
if (!referenceVenuesByFamily.has(venue.familyKey)) referenceVenuesByFamily.set(venue.familyKey, []);
referenceVenuesByFamily.get(venue.familyKey).push(venue);
if (isLiveRow(venue)) {
if (!VALID_NATIVE_REFERENCE_PROTOCOLS.includes(venue.protocol)) {
errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: live/routingVisible rows must use a native protocol contract, not "${venue.protocol}"`);
}
if (looksPlaceholderAddress(venue.venueAddress)) {
errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: live/routingVisible venueAddress must use a native protocol contract, not a placeholder scaffold (${venue.venueAddress})`);
}
}
}
for (const [familyKey, venues] of referenceVenuesByFamily.entries()) {
const protocols = new Set(venues.map((venue) => venue.protocol));
if (!protocols.has('uniswap_v3')) {
errors.push(`Chain ${chainId} gas family ${familyKey}: missing uniswap_v3 reference venue`);
}
const oneInch = venues.find((venue) => venue.protocol === '1inch');
if (oneInch?.routingVisible === true || oneInch?.live === true) {
const hasUniswap = venues.some((venue) => venue.protocol === 'uniswap_v3' && venue.live === true);
const hasDodo = (gasPoolsByFamily.get(familyKey) || []).some((pool) => pool.publicRoutingEnabled === true);
if (!hasUniswap || !hasDodo) {
errors.push(`Chain ${chainId} gas family ${familyKey}: 1inch cannot be live/routingVisible before DODO and Uniswap venues are live`);
}
}
}
}
if (errors.length > 0) {
errors.forEach((e) => process.stderr.write(e + '\n'));
process.exit(1);
}
process.exit(0);
}
function resolveInputPath(argv) {
const candidate = argv[2];
if (!candidate || candidate === '-h' || candidate === '--help') {
return DEPLOYMENT_STATUS_PATH;
}
return path.isAbsolute(candidate) ? candidate : path.join(process.cwd(), candidate);
}
function printUsage() {
process.stdout.write('Usage: node scripts/validate-deployment-status.cjs [path/to/deployment-status.json]\n');
}
if (require.main === module) {
const argv = process.argv;
if (argv[2] === '-h' || argv[2] === '--help') {
printUsage();
process.exit(0);
}
if (argv[2]) {
DEPLOYMENT_STATUS_PATH = resolveInputPath(argv);
}
main();
}
module.exports = {
looksPlaceholderAddress,
isLiveRow,
validatePoolEntries,
main,
};