Files
smom-dbis-138/services/token-aggregation/scripts/generate-supply-proof-catalog.ts
defiQUG 76143a8fe3 feat(token-aggregation): reports, PMM quotes, config; Engine X flash vaults
- Expand token-aggregation API (report routes), canonical tokens, pools
- Add flash vault contracts + tests (indexed, DODO cwUSDC, XAUT borrow)
- PMM pools JSON, deploy/export scripts, metamask verified list

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 12:56:30 -07:00

239 lines
8.2 KiB
TypeScript

#!/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<SupplyProofCatalog> | SupplyProof;
const proofs = Array.isArray((parsed as Partial<SupplyProofCatalog>).proofs)
? ((parsed as SupplyProofCatalog).proofs)
: [parsed as SupplyProof];
return proofs.filter((proof) => proof?.network?.chainId && proof?.token?.address);
}
function loadExistingCatalog(seedPaths: string[]): Map<string, SupplyProof> {
const proofs = new Map<string, SupplyProof>();
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<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
let timeout: NodeJS.Timeout | undefined;
const timer = new Promise<never>((_, 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<bigint>,
contract.decimals().catch(() => spec.decimals) as Promise<number>,
contract.symbol().catch(() => undefined) as Promise<string | undefined>,
contract.name().catch(() => undefined) as Promise<string | undefined>,
]),
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);
});