- 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>
239 lines
8.2 KiB
TypeScript
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);
|
|
});
|