#!/usr/bin/env ts-node import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import path from 'path'; import { Contract, JsonRpcProvider, formatUnits } from 'ethers'; import { CANONICAL_TOKENS, getTokenRegistryFamily } from '../src/config/canonical-tokens'; import { getChainConfig } from '../src/config/chains'; import { NETWORKS } from '../src/config/networks'; const ERC20_ABI = [ 'function totalSupply() view returns (uint256)', 'function decimals() view returns (uint8)', 'function symbol() view returns (string)', 'function name() view returns (string)', ]; type SupplyProof = { schema: string; generatedAt: string; network: { chainId: number; name: string; referenceBlock: number; }; token: { address: string; name: string; symbol: string; onchainName?: string; onchainSymbol?: string; decimals: number; totalSupplyRaw: string; totalSupplyUnits: string; }; circulatingSupplyMethodology: { status: string; recommendedFormula: string; currentConservativeReportableCirculatingSupplyUnits: string; notes: string[]; }; knownBalances?: unknown; trackerSurfaces?: unknown; [key: string]: unknown; }; type SupplyProofCatalog = { schema: string; generatedAt: string; proofs: SupplyProof[]; proofFailures?: Array<{ chainId: number; symbol: string; address: string; reason: string; }>; }; function isCandidate(symbol: string, type?: string, registryFamily?: string): boolean { return ( /^c[A-Z0-9]/.test(symbol) && (type === 'base' || type === 'c' || type === 'w' || ['iso4217', 'monetary_unit', 'gas_native', 'commodity'].includes(String(registryFamily || ''))) ); } function catalogKey(chainId: number, address: string): string { return `${chainId}:${address.toLowerCase()}`; } function rpcUrlsForChain(chainId: number, primary?: string): string[] { const network = NETWORKS.find((row) => row.chainIdDecimal === chainId); return Array.from(new Set([primary, ...(network?.rpcUrls ?? [])].filter(Boolean) as string[])); } function loadProofFile(proofPath: string): SupplyProof[] { if (!existsSync(proofPath)) return []; const parsed = JSON.parse(readFileSync(proofPath, 'utf8')) as Partial | SupplyProof; const proofs = Array.isArray((parsed as Partial).proofs) ? ((parsed as SupplyProofCatalog).proofs) : [parsed as SupplyProof]; return proofs.filter((proof) => proof?.network?.chainId && proof?.token?.address); } function loadExistingCatalog(seedPaths: string[]): Map { const proofs = new Map(); for (const seedPath of seedPaths) { for (const proof of loadProofFile(seedPath)) { proofs.set(catalogKey(proof.network.chainId, proof.token.address), proof); } } return proofs; } function shouldPreserveExistingProof(proof: SupplyProof | undefined): boolean { if (!proof) return false; return ( proof.knownBalances !== undefined || proof.trackerSurfaces !== undefined || proof.circulatingSupplyMethodology?.status === 'ready_for_tracker_review' ); } async function withTimeout(promise: Promise, timeoutMs: number, label: string): Promise { let timeout: NodeJS.Timeout | undefined; const timer = new Promise((_, reject) => { timeout = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs); }); try { return await Promise.race([promise, timer]); } finally { if (timeout) clearTimeout(timeout); } } async function main() { const serviceRoot = path.resolve(__dirname, '..'); const catalogPath = path.resolve( process.env.TOKEN_AGGREGATION_SUPPLY_PROOF_CATALOG_JSON || path.join(serviceRoot, 'config/supply-proof-catalog.json') ); const repoRoot = path.resolve(serviceRoot, '..', '..', '..'); const timeoutMs = Number(process.env.SUPPLY_PROOF_RPC_TIMEOUT_MS || 12000); const generatedAt = new Date().toISOString(); const existing = loadExistingCatalog([ catalogPath, path.join(repoRoot, 'reports/status/mainnet-cwusdc-supply-proof-20260508.json'), ]); const proofs = new Map(existing); const failures: SupplyProofCatalog['proofFailures'] = []; for (const spec of CANONICAL_TOKENS) { const registryFamily = getTokenRegistryFamily(spec); if (!isCandidate(spec.symbol, spec.type, registryFamily)) continue; for (const [chainIdText, rawAddress] of Object.entries(spec.addresses)) { const chainId = Number(chainIdText); const address = String(rawAddress || '').trim(); if (!address.startsWith('0x')) continue; const chain = getChainConfig(chainId); if (!chain?.rpcUrl) { failures.push({ chainId, symbol: spec.symbol, address, reason: 'missing_rpc_url' }); continue; } let proved = false; const reasons: string[] = []; for (const rpcUrl of rpcUrlsForChain(chainId, chain.rpcUrl)) { try { const provider = new JsonRpcProvider(rpcUrl, chainId, { staticNetwork: true }); const contract = new Contract(address, ERC20_ABI, provider); const [referenceBlock, totalSupplyRaw, decimals, onchainSymbol, onchainName] = await withTimeout( Promise.all([ provider.getBlockNumber(), contract.totalSupply() as Promise, contract.decimals().catch(() => spec.decimals) as Promise, contract.symbol().catch(() => undefined) as Promise, contract.name().catch(() => undefined) as Promise, ]), timeoutMs, `${chainId}:${spec.symbol}` ); const decimalsNumber = Number(decimals); const totalSupplyRawText = totalSupplyRaw.toString(); const totalSupplyUnits = formatUnits(totalSupplyRaw, decimalsNumber); const key = catalogKey(chainId, address); if (!shouldPreserveExistingProof(proofs.get(key))) { proofs.set(key, { schema: 'token-aggregation-supply-proof/v1', generatedAt, network: { chainId, name: chain.name, referenceBlock, }, token: { address, name: spec.name, symbol: spec.symbol, onchainName, onchainSymbol, decimals: decimalsNumber, totalSupplyRaw: totalSupplyRawText, totalSupplyUnits, }, circulatingSupplyMethodology: { status: 'onchain_total_supply_proved_circulating_review_required', recommendedFormula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances', currentConservativeReportableCirculatingSupplyUnits: totalSupplyUnits, notes: [ 'totalSupply was read from the mapped token contract at the reference block.', 'circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.', 'Tracker acceptance is external and not implied by this API response.', ], }, }); } proved = true; break; } catch (error) { reasons.push(`${rpcUrl}: ${error instanceof Error ? error.message : String(error)}`); } } if (!proved) { failures.push({ chainId, symbol: spec.symbol, address, reason: reasons.join(' | '), }); } } } const catalog: SupplyProofCatalog = { schema: 'token-aggregation-supply-proof-catalog/v1', generatedAt, proofs: Array.from(proofs.values()).sort( (a, b) => a.network.chainId - b.network.chainId || a.token.symbol.localeCompare(b.token.symbol) ), proofFailures: failures, }; mkdirSync(path.dirname(catalogPath), { recursive: true }); writeFileSync(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`); console.log(JSON.stringify({ catalogPath, proofs: catalog.proofs.length, failures: failures.length, }, null, 2)); } main().catch((error) => { console.error(error); process.exit(1); });