#!/usr/bin/env node /** * Validate the ALL Mainnet pool-creation matrix. * * This file is an operational dependency: pool rows are used to decide what can * be created, funded, or promoted. The checks here keep the matrix internally * consistent and cross-check token addresses against the repo token lists. */ import { existsSync, readFileSync } from "node:fs"; import { basename, resolve } from "node:path"; const repoRoot = resolve(new URL("../..", import.meta.url).pathname); const defaultMatrix = "config/all-mainnet-pool-creation-matrix.json"; const defaultTokenLists = [ "token-lists/lists/all-mainnet.tokenlist.json", "token-lists/lists/dbis-138.tokenlist.json", "token-lists/lists/ethereum-mainnet.tokenlist.json", "token-lists/lists/arbitrum.tokenlist.json", "token-lists/lists/avalanche.tokenlist.json", "token-lists/lists/cronos.tokenlist.json", "metamask-integration/config/token-list.json", "smom-dbis-138/metamask/token-list.json", ]; const requiredVaultRoles = [ "treasury_reserve", "bridge_liquidity", "single_sided_inventory", "protocol_adapter", "emergency_withdraw", ]; const statusesRequiringPoolAddress = new Set(["created", "funded", "live_read", "canary_passed", "production"]); const addressPattern = /^0x[a-fA-F0-9]{40}$/; function parseArgs() { const args = process.argv.slice(2); if (args.includes("--help") || args.includes("-h")) { console.log(`Usage: node scripts/validation/validate-pool-creation-matrix.mjs [matrix-path]\n\nDefaults to ${defaultMatrix}.`); process.exit(0); } return args[0] || defaultMatrix; } function readJson(file, errors) { const abs = resolve(repoRoot, file); if (!existsSync(abs)) { errors.push(`${file}: missing`); return null; } try { return JSON.parse(readFileSync(abs, "utf8")); } catch (error) { errors.push(`${file}: invalid JSON: ${error.message}`); return null; } } function tokenKey(chainId, symbol) { return `${chainId}:${String(symbol).toUpperCase()}`; } function buildTokenIndex(warnings) { const index = new Map(); for (const file of defaultTokenLists) { const abs = resolve(repoRoot, file); if (!existsSync(abs)) { warnings.push(`${file}: token list missing; address cross-check skipped for that file`); continue; } let list; try { list = JSON.parse(readFileSync(abs, "utf8")); } catch (error) { warnings.push(`${file}: invalid JSON; address cross-check skipped for that file: ${error.message}`); continue; } if (!Array.isArray(list.tokens)) { warnings.push(`${file}: missing tokens[]; address cross-check skipped for that file`); continue; } for (const token of list.tokens) { if (!token?.chainId || !token?.symbol || !addressPattern.test(String(token.address || ""))) { continue; } const key = tokenKey(token.chainId, token.symbol); if (!index.has(key)) { index.set(key, new Map()); } index.get(key).set(String(token.address).toLowerCase(), `${file}:${token.symbol}`); } } return index; } function ref(row, index) { return `rows[${index}] ${row.poolId || ""}`; } function slug(value) { return String(value || "").toLowerCase(); } function sortedStrings(values) { return [...values].sort((a, b) => a.localeCompare(b)); } function countBy(rows, key) { const counts = {}; for (const row of rows) { const value = row[key]; counts[value] = (counts[value] || 0) + 1; } return counts; } function sameCounts(actual, expected) { const keys = new Set([...Object.keys(actual || {}), ...Object.keys(expected || {})]); for (const key of keys) { if ((actual?.[key] || 0) !== (expected?.[key] || 0)) { return false; } } return true; } function validateAddress(value, path, errors, { allowNull = true } = {}) { if (value === null && allowNull) { return; } if (typeof value !== "string" || !addressPattern.test(value)) { errors.push(`${path}: expected ${allowNull ? "null or " : ""}0x-prefixed 20-byte address`); } } function validateTokenAddress(row, index, side, tokenIndex, errors, warnings) { const token = row[side]; const rowRef = ref(row, index); if (!token || typeof token !== "object") { errors.push(`${rowRef}: ${side} must be an object`); return; } if (typeof token.symbol !== "string" || token.symbol.length === 0) { errors.push(`${rowRef}: ${side}.symbol is required`); } validateAddress(token.address, `${rowRef}: ${side}.address`, errors); const known = tokenIndex.get(tokenKey(row.chainId, token.symbol)); if (!known || known.size !== 1) { return; } const [knownAddress, source] = [...known.entries()][0]; if (token.address === null) { if (row.status !== "planned" || row.publicRoutingEnabled === true) { errors.push(`${rowRef}: ${side} ${token.symbol} address is missing but ${source} has ${knownAddress}`); } return; } if (String(token.address).toLowerCase() !== knownAddress) { warnings.push(`${rowRef}: ${side} ${token.symbol} address ${token.address} differs from ${source} ${knownAddress}`); } } function validateMatrix(file, matrix, tokenIndex) { const errors = []; const warnings = []; if (!matrix || typeof matrix !== "object" || Array.isArray(matrix)) { errors.push(`${file}: root must be an object`); return { errors, warnings }; } if (typeof matrix.version !== "string" || matrix.version.length === 0) { errors.push(`${file}: version is required`); } if (typeof matrix.generatedAt !== "string" || Number.isNaN(Date.parse(matrix.generatedAt))) { errors.push(`${file}: generatedAt must be an ISO-like date string`); } if (!Array.isArray(matrix.rows) || matrix.rows.length === 0) { errors.push(`${file}: rows[] is required`); return { errors, warnings }; } if (!Array.isArray(matrix.lifecycle) || matrix.lifecycle.length === 0) { errors.push(`${file}: lifecycle[] is required`); } if (!Array.isArray(matrix.protocolRolloutOrder) || matrix.protocolRolloutOrder.length === 0) { errors.push(`${file}: protocolRolloutOrder[] is required`); } const protocolCounts = countBy(matrix.rows, "protocol"); const statusCounts = countBy(matrix.rows, "status"); if (!sameCounts(protocolCounts, matrix.protocolCounts)) { errors.push(`${file}: protocolCounts does not match rows (${JSON.stringify(protocolCounts)})`); } if (!sameCounts(statusCounts, matrix.statusCounts)) { errors.push(`${file}: statusCounts does not match rows (${JSON.stringify(statusCounts)})`); } const lifecycle = new Set(matrix.lifecycle || []); const rollout = new Set(matrix.protocolRolloutOrder || []); const poolIds = new Set(); matrix.rows.forEach((row, index) => { const rowRef = ref(row, index); if (!row || typeof row !== "object" || Array.isArray(row)) { errors.push(`rows[${index}]: row must be an object`); return; } if (typeof row.poolId !== "string" || row.poolId.length === 0) { errors.push(`${rowRef}: poolId is required`); } else if (poolIds.has(row.poolId)) { errors.push(`${rowRef}: duplicate poolId`); } else { poolIds.add(row.poolId); } if (!Number.isInteger(row.chainId)) { errors.push(`${rowRef}: chainId must be an integer`); } if (!rollout.has(row.protocol)) { errors.push(`${rowRef}: protocol ${row.protocol} is not in protocolRolloutOrder`); } if (!lifecycle.has(row.status)) { errors.push(`${rowRef}: status ${row.status} is not in lifecycle`); } const expectedPoolId = `${row.chainId}-${row.protocol}-${slug(row.baseToken?.symbol)}-${slug(row.quoteToken?.symbol)}`; if (row.poolId && row.poolId !== expectedPoolId) { errors.push(`${rowRef}: poolId should be ${expectedPoolId}`); } validateTokenAddress(row, index, "baseToken", tokenIndex, errors, warnings); validateTokenAddress(row, index, "quoteToken", tokenIndex, errors, warnings); validateAddress(row.factoryAddress, `${rowRef}: factoryAddress`, errors); validateAddress(row.routerAddress, `${rowRef}: routerAddress`, errors); validateAddress(row.poolAddress, `${rowRef}: poolAddress`, errors, { allowNull: !statusesRequiringPoolAddress.has(row.status), }); validateAddress(row.vaultAddress, `${rowRef}: vaultAddress`, errors); if (statusesRequiringPoolAddress.has(row.status) && row.poolAddress === null) { errors.push(`${rowRef}: status ${row.status} requires poolAddress`); } const shouldBeSingleSided = row.protocol === "single_sided_pmm" || row.poolType === "single_sided"; if (row.singleSided !== shouldBeSingleSided) { errors.push(`${rowRef}: singleSided should be ${shouldBeSingleSided}`); } if (!Array.isArray(row.vaultAssignments)) { errors.push(`${rowRef}: vaultAssignments[] is required`); } else { const roles = row.vaultAssignments.map((assignment) => assignment?.role); const roleSet = new Set(roles); const expectedRoles = new Set(requiredVaultRoles); if (roleSet.size !== expectedRoles.size || requiredVaultRoles.some((role) => !roleSet.has(role))) { errors.push(`${rowRef}: vaultAssignments roles must be ${requiredVaultRoles.join(",")}`); } for (const assignment of row.vaultAssignments) { if (!assignment || typeof assignment !== "object") { errors.push(`${rowRef}: vaultAssignments entries must be objects`); continue; } validateAddress(assignment.vaultAddress, `${rowRef}: vaultAssignments.${assignment.role || ""}.vaultAddress`, errors); if (typeof assignment.requiredBeforeFunding !== "boolean") { errors.push(`${rowRef}: vaultAssignments.${assignment.role || ""}.requiredBeforeFunding must be boolean`); } } const actualMissing = sortedStrings( row.vaultAssignments .filter((assignment) => assignment?.requiredBeforeFunding === true && assignment.vaultAddress === null) .map((assignment) => assignment.role), ); const declaredMissing = sortedStrings(row.missingRequiredVaultRoles || []); if (actualMissing.join("|") !== declaredMissing.join("|")) { errors.push(`${rowRef}: missingRequiredVaultRoles should be [${actualMissing.join(", ")}]`); } const expectedStatus = actualMissing.length > 0 ? "missing_required_vaults" : "ready"; if (row.vaultAssignmentStatus !== expectedStatus) { errors.push(`${rowRef}: vaultAssignmentStatus should be ${expectedStatus}`); } } const tiers = row.fundingTiersUsd; if (!tiers || typeof tiers !== "object") { errors.push(`${rowRef}: fundingTiersUsd is required`); } else if (!(tiers.seed > 0 && tiers.smoke >= tiers.seed && tiers.productionMinimum >= tiers.smoke)) { errors.push(`${rowRef}: fundingTiersUsd must satisfy seed > 0, smoke >= seed, productionMinimum >= smoke`); } const policy = row.policy; if (!policy || typeof policy !== "object") { errors.push(`${rowRef}: policy is required`); } else { for (const key of ["maxPriceImpactBps", "minReserveUsd", "refillTriggerBps"]) { if (typeof policy[key] !== "number" || policy[key] < 0) { errors.push(`${rowRef}: policy.${key} must be a non-negative number`); } } if (typeof policy.pauseOnReserveReadFailure !== "boolean") { errors.push(`${rowRef}: policy.pauseOnReserveReadFailure must be boolean`); } } if (!Array.isArray(row.notes)) { errors.push(`${rowRef}: notes[] is required`); } }); return { errors, warnings }; } const matrixPath = parseArgs(); const bootstrapErrors = []; const bootstrapWarnings = []; const matrix = readJson(matrixPath, bootstrapErrors); const tokenIndex = buildTokenIndex(bootstrapWarnings); const { errors, warnings } = matrix ? validateMatrix(matrixPath, matrix, tokenIndex) : { errors: [], warnings: [] }; const allErrors = [...bootstrapErrors, ...errors]; const allWarnings = [...bootstrapWarnings, ...warnings]; for (const warning of allWarnings) { console.warn(`[WARN] ${warning}`); } if (allErrors.length > 0) { console.error(`[ERROR] Pool-creation matrix validation failed with ${allErrors.length} issue(s):`); for (const error of allErrors) { console.error(` - ${error}`); } process.exit(1); } console.log(`[OK] ${basename(matrixPath)} valid: ${matrix.rows.length} row(s), ${Object.keys(matrix.protocolCounts || {}).length} protocol(s).`);