- Apply decimal-aware liquidity normalization on /tokens and reports - Add GET /report/token-price/:symbol compact evidence snapshot - Extend route tests for normalization and token-price endpoint Co-authored-by: Cursor <cursoragent@cursor.com>
1784 lines
69 KiB
TypeScript
1784 lines
69 KiB
TypeScript
/**
|
|
* CMC and CoinGecko reporting API: all tokens, liquidity, volume, and reportable data.
|
|
* Use for listing submissions and external aggregator sync.
|
|
*/
|
|
|
|
import { Router, Request, Response } from 'express';
|
|
import { existsSync, readFileSync } from 'fs';
|
|
import path from 'path';
|
|
import { TokenRepository } from '../../database/repositories/token-repo';
|
|
import { MarketDataRepository } from '../../database/repositories/market-data-repo';
|
|
import { PoolRepository } from '../../database/repositories/pool-repo';
|
|
import {
|
|
CANONICAL_TOKENS,
|
|
getCanonicalTokensByChain,
|
|
getLogoUriForSpec,
|
|
getTokenRegistryFamily,
|
|
} from '../../config/canonical-tokens';
|
|
import { resolvePoolTokenDisplays } from '../../services/token-display';
|
|
import { getSupportedChainIds } from '../../config/chains';
|
|
import { cacheMiddleware } from '../middleware/cache';
|
|
import { fetchRemoteJson } from '../utils/fetch-remote-json';
|
|
import { buildCrossChainReport } from '../../indexer/cross-chain-indexer';
|
|
import { logger } from '../../utils/logger';
|
|
import {
|
|
filterPoolsForExposure,
|
|
getActiveTransportPairs,
|
|
getGruTransportMetadata,
|
|
type GruTransportGasAssetFamily,
|
|
type GruTransportPair,
|
|
} from '../../config/gru-transport';
|
|
import {
|
|
buildCwRegistryChains,
|
|
buildGasRegistryChains,
|
|
loadDeploymentStatusFile,
|
|
type DeploymentStatusFile,
|
|
type CwRegistryChain,
|
|
} from '../../config/deployment-status';
|
|
import { getGruV2DeploymentPoolRows } from '../../config/gru-v2-deployment-pools';
|
|
import { getCanonicalPriceSnapshotGeneratedAt, getCanonicalPriceUsd } from '../../services/canonical-price-oracle';
|
|
import { pmmVaultReserveFromChain, resolvePmmQuoteRpcUrl } from '../../services/pmm-onchain-quote';
|
|
|
|
const router: Router = Router();
|
|
const tokenRepo = new TokenRepository();
|
|
const marketDataRepo = new MarketDataRepository();
|
|
const poolRepo = new PoolRepository();
|
|
|
|
const MAINNET_CWUSDC_ADDRESS = '0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a';
|
|
const DEFAULT_SUPPLY_CATALOG_RELATIVE_PATH = 'config/supply-proof-catalog.json';
|
|
const DEFAULT_LIVE_UNISWAP_V2_POOL_CATALOG_RELATIVE_PATH = 'config/live-uniswap-v2-pool-catalog.json';
|
|
const DEFAULT_PUBLIC_REPORT_BASE_URL = 'https://explorer.d-bis.org';
|
|
const DBIS_CHAIN_138_LOGO_PATH = '/api/v1/report/logo/chain-138';
|
|
const CWUSDC_SUPPLY_PROOF_FALLBACK = {
|
|
schema: 'mainnet-cwusdc-supply-proof/v1',
|
|
generatedAt: '2026-05-08T03:16:54Z',
|
|
network: {
|
|
chainId: 1,
|
|
name: 'Ethereum Mainnet',
|
|
referenceBlock: 25047586,
|
|
},
|
|
token: {
|
|
address: MAINNET_CWUSDC_ADDRESS,
|
|
name: 'Wrapped cUSDC',
|
|
symbol: 'cWUSDC',
|
|
decimals: 6,
|
|
totalSupplyRaw: '10451316981309788',
|
|
totalSupplyUnits: '10451316981.309788',
|
|
},
|
|
circulatingSupplyMethodology: {
|
|
status: 'ready_for_tracker_review',
|
|
recommendedFormula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances',
|
|
currentConservativeReportableCirculatingSupplyUnits: '10451316981.309788',
|
|
notes: [
|
|
'No public tracker has accepted a circulating-supply value for this contract yet.',
|
|
'If a tracker requires exclusion of operator, treasury, bridge, or protocol-controlled balances, use the knownBalances section plus any additional signed treasury inventory supplied at submission time.',
|
|
'The value above is an on-chain supply proof, not a third-party listing approval.',
|
|
],
|
|
},
|
|
};
|
|
|
|
type SupplyProofSnapshot = typeof CWUSDC_SUPPLY_PROOF_FALLBACK & {
|
|
_sourcePath?: string;
|
|
_source?: string;
|
|
};
|
|
|
|
type SupplyProofEnrichment = {
|
|
totalSupply?: string;
|
|
totalSupplyRaw?: string;
|
|
circulatingSupply?: string;
|
|
circulatingSupplyFormula?: string;
|
|
marketCapUsd?: number;
|
|
supplyProofProvenance: {
|
|
source: string;
|
|
path?: string;
|
|
schema?: string;
|
|
generatedAt?: string;
|
|
referenceBlock?: number;
|
|
status?: string;
|
|
};
|
|
trackerCaveats: string[];
|
|
};
|
|
|
|
type ReportPoolEntry = {
|
|
poolAddress: string;
|
|
dex: string;
|
|
token0: string;
|
|
token1: string;
|
|
token0Symbol?: string;
|
|
token1Symbol?: string;
|
|
tvl: number;
|
|
volume24h: number;
|
|
source?: string;
|
|
status?: string;
|
|
statusReason?: string;
|
|
role?: string;
|
|
section?: string;
|
|
publicRoutingEnabled?: boolean;
|
|
reserveSource?: string;
|
|
reserveUpdatedAt?: string;
|
|
reserve0Raw?: string;
|
|
reserve1Raw?: string;
|
|
};
|
|
|
|
type LiveUniswapV2PoolCatalogRow = {
|
|
chainId: number;
|
|
poolAddress: string;
|
|
dex?: string;
|
|
baseSymbol: string;
|
|
quoteSymbol: string;
|
|
baseAddress: string;
|
|
quoteAddress: string;
|
|
totalLiquidityUsd: number;
|
|
reserve0Usd?: number;
|
|
reserve1Usd?: number;
|
|
depthOk?: boolean;
|
|
parityOk?: boolean;
|
|
healthy?: boolean;
|
|
deviationBps?: string;
|
|
};
|
|
|
|
type LiveUniswapV2PoolCatalog = {
|
|
schema?: string;
|
|
generatedAt?: string;
|
|
pools?: LiveUniswapV2PoolCatalogRow[];
|
|
};
|
|
|
|
function resolveRepoRoot(): string {
|
|
const candidates = [
|
|
process.env.PROXMOX_REPO_ROOT,
|
|
process.env.PROJECT_ROOT,
|
|
path.resolve(process.cwd(), '..', '..', '..'),
|
|
path.resolve(process.cwd(), '..'),
|
|
process.cwd(),
|
|
].filter(Boolean) as string[];
|
|
|
|
for (const candidate of candidates) {
|
|
if (
|
|
existsSync(path.join(candidate, DEFAULT_SUPPLY_CATALOG_RELATIVE_PATH)) ||
|
|
existsSync(path.join(candidate, DEFAULT_LIVE_UNISWAP_V2_POOL_CATALOG_RELATIVE_PATH)) ||
|
|
existsSync(path.join(candidate, 'reports/status/mainnet-cwusdc-supply-proof-20260508.json'))
|
|
) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return process.cwd();
|
|
}
|
|
|
|
function loadLiveUniswapV2PoolCatalog(): LiveUniswapV2PoolCatalogRow[] {
|
|
const configuredCatalog = process.env.TOKEN_AGGREGATION_LIVE_UNISWAP_V2_POOL_CATALOG_JSON?.trim();
|
|
const repoRoot = resolveRepoRoot();
|
|
const candidates = [
|
|
configuredCatalog,
|
|
path.join(repoRoot, DEFAULT_LIVE_UNISWAP_V2_POOL_CATALOG_RELATIVE_PATH),
|
|
].filter(Boolean) as string[];
|
|
|
|
for (const candidate of candidates) {
|
|
if (!existsSync(candidate)) continue;
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(candidate, 'utf8')) as LiveUniswapV2PoolCatalog;
|
|
return (parsed.pools ?? []).filter(
|
|
(pool) =>
|
|
Number.isFinite(pool.chainId) &&
|
|
pool.poolAddress?.startsWith('0x') &&
|
|
pool.baseAddress?.startsWith('0x') &&
|
|
pool.quoteAddress?.startsWith('0x')
|
|
);
|
|
} catch (error) {
|
|
logger.warn('Unable to parse live Uniswap V2 pool catalog; skipping file', { candidate, error });
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function normalizeSupplyProofKey(chainId: number, address: string): string {
|
|
return `${chainId}:${address.toLowerCase()}`;
|
|
}
|
|
|
|
function loadSupplyProofFile(proofPath: string, source: string): SupplyProofSnapshot[] {
|
|
if (!proofPath || !existsSync(proofPath)) return [];
|
|
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(proofPath, 'utf8')) as { proofs?: SupplyProofSnapshot[] } | SupplyProofSnapshot;
|
|
const proofs: SupplyProofSnapshot[] = Array.isArray((parsed as { proofs?: SupplyProofSnapshot[] })?.proofs)
|
|
? ((parsed as { proofs: SupplyProofSnapshot[] }).proofs)
|
|
: [parsed as SupplyProofSnapshot];
|
|
return proofs
|
|
.filter((proof) => proof?.network?.chainId && proof?.token?.address)
|
|
.map((proof) => ({
|
|
...proof,
|
|
_source: source,
|
|
_sourcePath: proofPath,
|
|
}));
|
|
} catch (error) {
|
|
logger.warn('Unable to parse supply proof file; skipping file', { proofPath, error });
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function loadSupplyProofCatalog(): Map<string, SupplyProofSnapshot> {
|
|
const configuredCatalog = process.env.TOKEN_AGGREGATION_SUPPLY_PROOF_CATALOG_JSON?.trim();
|
|
const configuredSingleProof = process.env.CWUSDC_SUPPLY_PROOF_JSON?.trim();
|
|
const repoRoot = resolveRepoRoot();
|
|
const candidates = [
|
|
configuredCatalog ? { path: configuredCatalog, source: 'configured-supply-proof-catalog' } : null,
|
|
{ path: path.join(repoRoot, DEFAULT_SUPPLY_CATALOG_RELATIVE_PATH), source: 'repo-supply-proof-catalog' },
|
|
configuredSingleProof ? { path: configuredSingleProof, source: 'configured-supply-proof-file' } : null,
|
|
{ path: path.join(repoRoot, 'reports/status/mainnet-cwusdc-supply-proof-20260508.json'), source: 'repo-supply-proof-file' },
|
|
].filter(Boolean) as Array<{ path: string; source: string }>;
|
|
|
|
const byToken = new Map<string, SupplyProofSnapshot>();
|
|
for (const candidate of candidates) {
|
|
for (const proof of loadSupplyProofFile(candidate.path, candidate.source)) {
|
|
const key = normalizeSupplyProofKey(Number(proof.network.chainId), String(proof.token.address));
|
|
if (!byToken.has(key)) {
|
|
byToken.set(key, proof);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!byToken.has(normalizeSupplyProofKey(1, MAINNET_CWUSDC_ADDRESS))) {
|
|
byToken.set(normalizeSupplyProofKey(1, MAINNET_CWUSDC_ADDRESS), {
|
|
...CWUSDC_SUPPLY_PROOF_FALLBACK,
|
|
_source: 'embedded-fallback-snapshot',
|
|
});
|
|
}
|
|
|
|
return byToken;
|
|
}
|
|
|
|
function isGruSupplyTrackedCandidate(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 || '')))
|
|
);
|
|
}
|
|
|
|
const EXTERNAL_OFFICIAL_QUOTE_ASSETS = new Set([
|
|
'1:cUSDC:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
|
'1:cUSDT:0xdac17f958d2ee523a2206206994597c13d831ec7',
|
|
'10:cUSDC:0x0b2c639c533813f4aa9d7837caf62653d097ff85',
|
|
'10:cUSDT:0x94b008aa00579c1307b0ef2c499ad98a8ce58e58',
|
|
'25:cUSDC:0xc21223249ca28397b4b6541dffaecc539bff0c59',
|
|
'25:cUSDT:0x66e428c3f67a68878562e79a0234c1f83c208770',
|
|
'56:cUSDC:0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d',
|
|
'56:cUSDT:0x55d398326f99059ff775485246999027b3197955',
|
|
'100:cUSDC:0xddafbb505ad214d7b80b1f830fccc89b60fb7a83',
|
|
'100:cUSDT:0x4ecaba5870353805a9f068101a40e0f32ed605c6',
|
|
'137:cUSDC:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359',
|
|
'137:cUSDT:0xc2132d05d31c914a87c6611c10748aeb04b58e8f',
|
|
'8453:cUSDC:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
|
|
'8453:cUSDT:0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
|
|
'42161:cUSDC:0xaf88d065e77c8cc2239327c5edb3a432268e5831',
|
|
'42161:cUSDT:0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9',
|
|
'42220:cUSDC:0xceba9300f2b948710d2653dd7b07f33a8b32118c',
|
|
'42220:cUSDT:0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e',
|
|
'43114:cUSDC:0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e',
|
|
'43114:cUSDT:0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7',
|
|
'1111:cUSDC:0xe3f5a90f9cb311505cd691a46596599aa1a0ad7d',
|
|
'1111:cUSDT:0xa649325aa7c5093d12d6f98eb4378deae68ce23f',
|
|
'651940:cUSDC:0xa95eed79f84e6a0151eaeb9d441f9ffd50e8e881',
|
|
'651940:cUSDT:0x015b1897ed5279930bc2be46f661894d219292a6',
|
|
]);
|
|
|
|
function hasExternalOfficialQuoteLiquidity(token: { chainId: number; symbol: string; address: string }): boolean {
|
|
return EXTERNAL_OFFICIAL_QUOTE_ASSETS.has(`${token.chainId}:${token.symbol}:${token.address.toLowerCase()}`);
|
|
}
|
|
|
|
function isDeterministicPlaceholderAddress(address: string): boolean {
|
|
const normalized = address.toLowerCase();
|
|
return /^0x[a-f0-9]{4}0{24,}[a-f0-9]{1,8}$/.test(normalized);
|
|
}
|
|
|
|
function buildSupplyProofEnrichment(
|
|
chainId: number,
|
|
address: string,
|
|
symbol: string,
|
|
type: string | undefined,
|
|
registryFamily: string | undefined,
|
|
priceUsd?: number
|
|
): SupplyProofEnrichment | undefined {
|
|
const proof = loadSupplyProofCatalog().get(normalizeSupplyProofKey(chainId, address));
|
|
|
|
if (!proof) {
|
|
if (!isGruSupplyTrackedCandidate(symbol, type, registryFamily)) return undefined;
|
|
if (isDeterministicPlaceholderAddress(address)) {
|
|
return {
|
|
supplyProofProvenance: {
|
|
source: 'deterministic-placeholder-address',
|
|
status: 'non_reportable_until_erc20_deployed',
|
|
},
|
|
trackerCaveats: [
|
|
'This token binding is a deterministic placeholder address and does not currently behave as an ERC-20 contract.',
|
|
'Do not submit totalSupply, circulatingSupply, marketCapUsd, or liquidity claims for this asset until a deployed ERC-20 binding replaces the placeholder.',
|
|
'The placeholder is kept visible for registry and routing roadmap traceability only.',
|
|
],
|
|
};
|
|
}
|
|
return {
|
|
supplyProofProvenance: {
|
|
source: 'missing-supply-proof',
|
|
status: 'proof_required',
|
|
},
|
|
trackerCaveats: [
|
|
'No token-specific on-chain supply proof artifact is currently attached to this report response.',
|
|
'Do not submit totalSupply, circulatingSupply, or marketCapUsd for this asset until a current proof artifact is generated and linked.',
|
|
'Registry and pool visibility are not the same as tracker-grade supply proof.',
|
|
],
|
|
};
|
|
}
|
|
|
|
const totalSupply = String(proof.token?.totalSupplyUnits ?? '');
|
|
const totalSupplyRaw = String(proof.token?.totalSupplyRaw ?? '');
|
|
const circulatingSupply = String(
|
|
proof.circulatingSupplyMethodology?.currentConservativeReportableCirculatingSupplyUnits ?? totalSupply
|
|
);
|
|
const parsedCirculatingSupply = Number(circulatingSupply);
|
|
const marketCapUsd =
|
|
priceUsd !== undefined && Number.isFinite(parsedCirculatingSupply) ? parsedCirculatingSupply * priceUsd : undefined;
|
|
|
|
return {
|
|
totalSupply,
|
|
totalSupplyRaw,
|
|
circulatingSupply,
|
|
circulatingSupplyFormula: String(
|
|
proof.circulatingSupplyMethodology?.recommendedFormula ??
|
|
'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances'
|
|
),
|
|
marketCapUsd,
|
|
supplyProofProvenance: {
|
|
source: proof._source ?? 'repo-supply-proof-file',
|
|
path: proof._sourcePath,
|
|
schema: proof.schema,
|
|
generatedAt: proof.generatedAt,
|
|
referenceBlock: proof.network?.referenceBlock,
|
|
status: proof.circulatingSupplyMethodology?.status,
|
|
},
|
|
trackerCaveats: proof.circulatingSupplyMethodology?.notes ?? [
|
|
'Tracker acceptance is external and not implied by this API response.',
|
|
],
|
|
};
|
|
}
|
|
|
|
function normalizePossiblyRawLiquidityUsd(
|
|
liquidityUsd: number | undefined,
|
|
decimals: number,
|
|
priceUsd: number | undefined,
|
|
totalSupplyUnits: string | undefined
|
|
): number | undefined {
|
|
if (liquidityUsd === undefined || !Number.isFinite(liquidityUsd) || liquidityUsd <= 0) return liquidityUsd;
|
|
if (priceUsd === undefined || !Number.isFinite(priceUsd) || priceUsd <= 0) return liquidityUsd;
|
|
if (!totalSupplyUnits) {
|
|
if (decimals === 6 && liquidityUsd >= 1_000_000_000_000 && priceUsd <= 10) {
|
|
return liquidityUsd / 10 ** decimals;
|
|
}
|
|
return liquidityUsd;
|
|
}
|
|
|
|
const supplyUnits = Number(totalSupplyUnits);
|
|
if (!Number.isFinite(supplyUnits) || supplyUnits <= 0) return liquidityUsd;
|
|
|
|
const normalizedSupplyValue = supplyUnits * priceUsd;
|
|
const divisor = 10 ** decimals;
|
|
const decimalAdjustedLiquidity = liquidityUsd / divisor;
|
|
|
|
if (
|
|
liquidityUsd > normalizedSupplyValue &&
|
|
decimalAdjustedLiquidity > 0 &&
|
|
decimalAdjustedLiquidity <= normalizedSupplyValue * 1.25
|
|
) {
|
|
return decimalAdjustedLiquidity;
|
|
}
|
|
|
|
return liquidityUsd;
|
|
}
|
|
|
|
function resolveGruV2ReserveRpcUrl(chainId: number): string {
|
|
const configured = resolvePmmQuoteRpcUrl();
|
|
if (chainId === 138 && process.env.NODE_ENV !== 'test') return configured || 'http://192.168.11.211:8545';
|
|
if (chainId === 56 && process.env.NODE_ENV !== 'test') {
|
|
return process.env.BSC_RPC_URL || process.env.BSC_MAINNET_RPC || process.env.RPC_URL_56 || 'https://bsc-rpc.publicnode.com';
|
|
}
|
|
if (chainId === 43114 && process.env.NODE_ENV !== 'test') {
|
|
return (
|
|
process.env.AVALANCHE_RPC_URL ||
|
|
process.env.AVALANCHE_MAINNET_RPC ||
|
|
process.env.RPC_URL_43114 ||
|
|
'https://avalanche-c-chain-rpc.publicnode.com'
|
|
);
|
|
}
|
|
if (chainId === 1111 && process.env.NODE_ENV !== 'test') {
|
|
return process.env.WEMIX_MAINNET_RPC || process.env.WEMIX_RPC || process.env.RPC_URL_1111 || 'https://api.wemix.com';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function decimalFromRawForReport(raw: bigint, decimals: number): number {
|
|
const scale = 10 ** decimals;
|
|
return Number(raw) / scale;
|
|
}
|
|
|
|
function fallbackPoolDecimals(symbol?: string): number {
|
|
const normalized = String(symbol || '').toUpperCase();
|
|
if (normalized === 'WETH' || normalized === 'ETH') return 18;
|
|
return 6;
|
|
}
|
|
|
|
function fallbackPoolUsdPrice(symbol?: string): number {
|
|
const normalized = String(symbol || '').toUpperCase();
|
|
if (normalized.includes('XAU')) return Number(process.env.TOKEN_AGGREGATION_XAU_USD || '0') || 0;
|
|
if (normalized === 'WETH' || normalized === 'ETH') return Number(process.env.TOKEN_AGGREGATION_ETH_USD || '0') || 0;
|
|
return 1;
|
|
}
|
|
|
|
async function enrichGruV2FallbackPoolWithReserves(chainId: number, pool: ReportPoolEntry): Promise<ReportPoolEntry> {
|
|
const rpcUrl = resolveGruV2ReserveRpcUrl(chainId);
|
|
if (!rpcUrl) return pool;
|
|
|
|
const reserves = await pmmVaultReserveFromChain({ rpcUrl, poolAddress: pool.poolAddress });
|
|
if (!reserves) return pool;
|
|
|
|
const baseUnits = decimalFromRawForReport(reserves.baseReserveRaw, fallbackPoolDecimals(pool.token0Symbol));
|
|
const quoteUnits = decimalFromRawForReport(reserves.quoteReserveRaw, fallbackPoolDecimals(pool.token1Symbol));
|
|
const reserveUsd =
|
|
baseUnits * fallbackPoolUsdPrice(pool.token0Symbol) + quoteUnits * fallbackPoolUsdPrice(pool.token1Symbol);
|
|
|
|
if (!Number.isFinite(reserveUsd) || reserveUsd <= 0) {
|
|
return {
|
|
...pool,
|
|
reserveSource: 'onchain-gru-v2-pmm-getVaultReserve',
|
|
reserveUpdatedAt: new Date().toISOString(),
|
|
reserve0Raw: reserves.baseReserveRaw.toString(),
|
|
reserve1Raw: reserves.quoteReserveRaw.toString(),
|
|
};
|
|
}
|
|
|
|
return {
|
|
...pool,
|
|
tvl: reserveUsd,
|
|
status: pool.status ?? 'reserve_visible',
|
|
statusReason:
|
|
pool.statusReason ??
|
|
'Pool is present in deployment-status and has positive on-chain reserves via getVaultReserve().',
|
|
reserveSource: 'onchain-gru-v2-pmm-getVaultReserve',
|
|
reserveUpdatedAt: new Date().toISOString(),
|
|
reserve0Raw: reserves.baseReserveRaw.toString(),
|
|
reserve1Raw: reserves.quoteReserveRaw.toString(),
|
|
};
|
|
}
|
|
|
|
async function buildGruV2FallbackPoolsForToken(
|
|
chainId: number,
|
|
tokenAddress: string,
|
|
existingPools: ReportPoolEntry[]
|
|
): Promise<ReportPoolEntry[]> {
|
|
const token = tokenAddress.toLowerCase();
|
|
const existing = new Set(existingPools.map((pool) => pool.poolAddress.toLowerCase()));
|
|
const pools = getGruV2DeploymentPoolRows()
|
|
.filter((pool) => pool.chainId === chainId)
|
|
.filter((pool) => pool.baseAddress.toLowerCase() === token || pool.quoteAddress.toLowerCase() === token)
|
|
.filter((pool) => !existing.has(pool.poolAddress.toLowerCase()))
|
|
.map((pool) => ({
|
|
poolAddress: pool.poolAddress,
|
|
dex: pool.venue ?? 'gru_v2_pmm',
|
|
token0: pool.baseAddress,
|
|
token1: pool.quoteAddress,
|
|
token0Symbol: pool.baseSymbol,
|
|
token1Symbol: pool.quoteSymbol,
|
|
tvl: 0,
|
|
volume24h: 0,
|
|
source: 'gru-v2-deployment-status',
|
|
status: pool.status,
|
|
statusReason: pool.statusReason,
|
|
role: pool.role,
|
|
section: pool.section,
|
|
publicRoutingEnabled: pool.publicRoutingEnabled,
|
|
}));
|
|
|
|
return Promise.all(pools.map((pool) => enrichGruV2FallbackPoolWithReserves(chainId, pool)));
|
|
}
|
|
|
|
function buildLiveUniswapV2FallbackPoolsForToken(
|
|
chainId: number,
|
|
tokenAddress: string,
|
|
existingPools: ReportPoolEntry[]
|
|
): ReportPoolEntry[] {
|
|
const token = tokenAddress.toLowerCase();
|
|
const existing = new Set(existingPools.map((pool) => pool.poolAddress.toLowerCase()));
|
|
return loadLiveUniswapV2PoolCatalog()
|
|
.filter((pool) => pool.chainId === chainId)
|
|
.filter((pool) => pool.baseAddress.toLowerCase() === token || pool.quoteAddress.toLowerCase() === token)
|
|
.filter((pool) => !existing.has(pool.poolAddress.toLowerCase()))
|
|
.map((pool) => ({
|
|
poolAddress: pool.poolAddress,
|
|
dex: pool.dex ?? 'uniswap_v2',
|
|
token0: pool.baseAddress,
|
|
token1: pool.quoteAddress,
|
|
token0Symbol: pool.baseSymbol,
|
|
token1Symbol: pool.quoteSymbol,
|
|
tvl: Number(pool.totalLiquidityUsd ?? 0),
|
|
volume24h: 0,
|
|
source: 'live-uniswap-v2-pair-discovery',
|
|
status: pool.healthy ? 'indexed_live_healthy' : 'indexed_live_needs_repair',
|
|
statusReason: pool.healthy
|
|
? 'Live Uniswap V2 pair discovered with on-chain reserves, depth, and parity checks passing.'
|
|
: `Live Uniswap V2 pair discovered; depthOk=${pool.depthOk === true}, parityOk=${pool.parityOk === true}, deviationBps=${pool.deviationBps ?? 'unknown'}.`,
|
|
role: 'public_indexable_liquidity',
|
|
publicRoutingEnabled: pool.healthy === true,
|
|
}));
|
|
}
|
|
|
|
function resolveLocalLogoUri(remoteLogoUri: string, symbol: string): string | undefined {
|
|
if (remoteLogoUri.startsWith('/api/v1/report/logo/')) {
|
|
return remoteLogoUri;
|
|
}
|
|
if (remoteLogoUri.includes('/token-lists/logos/gru/')) {
|
|
const fileName = remoteLogoUri.split('/').pop()?.replace(/\.svg$/i, '');
|
|
return fileName ? `/api/v1/report/logo/${fileName}` : undefined;
|
|
}
|
|
if (remoteLogoUri.includes('/blockchains/bitcoin/info/logo.png')) {
|
|
return '/api/v1/report/logo/cWBTC';
|
|
}
|
|
if (remoteLogoUri.includes('/ipfs/')) {
|
|
const cid = remoteLogoUri.split('/').pop();
|
|
return cid ? `/api/v1/report/logo/ipfs-${cid}` : undefined;
|
|
}
|
|
if (symbol === 'cWUSDC') {
|
|
return '/api/v1/report/logo/cUSDC';
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function resolvePublicBaseUrl(req: Request): string {
|
|
const configured = (
|
|
process.env.TOKEN_AGGREGATION_PUBLIC_BASE_URL ??
|
|
process.env.PUBLIC_API_BASE_URL ??
|
|
process.env.PUBLIC_BASE_URL ??
|
|
''
|
|
).trim();
|
|
if (configured) return configured.replace(/\/+$/, '');
|
|
|
|
const host = String(req.get('x-forwarded-host') || req.get('host') || '').split(',')[0].trim();
|
|
if (host) {
|
|
let proto = String(req.get('x-forwarded-proto') || 'https').split(',')[0].trim() || 'https';
|
|
if (proto === 'http' && !/^(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i.test(host)) {
|
|
proto = 'https';
|
|
}
|
|
return `${proto}://${host}`.replace(/\/+$/, '');
|
|
}
|
|
|
|
return DEFAULT_PUBLIC_REPORT_BASE_URL;
|
|
}
|
|
|
|
function absolutePublicUrl(req: Request, value: string | undefined): string | undefined {
|
|
if (!value) return undefined;
|
|
if (/^https?:\/\//i.test(value)) return value;
|
|
if (!value.startsWith('/')) return value;
|
|
return `${resolvePublicBaseUrl(req)}${value}`;
|
|
}
|
|
|
|
function absoluteLogoUri(req: Request, remoteLogoUri: string, symbol: string): string {
|
|
const localLogoURI = resolveLocalLogoUri(remoteLogoUri, symbol);
|
|
return absolutePublicUrl(req, localLogoURI) ?? remoteLogoUri;
|
|
}
|
|
|
|
function absoluteReportLogoUri(remoteLogoUri: string, symbol: string): string {
|
|
const localLogoURI = resolveLocalLogoUri(remoteLogoUri, symbol);
|
|
if (localLogoURI?.startsWith('/')) return `${DEFAULT_PUBLIC_REPORT_BASE_URL}${localLogoURI}`;
|
|
return localLogoURI ?? remoteLogoUri;
|
|
}
|
|
|
|
/** Build token entries with DB market/pool data for a chain */
|
|
async function buildTokenReport(chainId: number) {
|
|
const canonical = getCanonicalTokensByChain(chainId);
|
|
const out: Array<{
|
|
chainId: number;
|
|
address: string;
|
|
symbol: string;
|
|
name: string;
|
|
type: string;
|
|
decimals: number;
|
|
currencyCode?: string;
|
|
registryFamily?: string;
|
|
familySymbol?: string;
|
|
deploymentVersion?: string;
|
|
deploymentStatus?: string;
|
|
preferredForX402?: boolean;
|
|
liquiditySourceSymbol?: string;
|
|
logoURI?: string;
|
|
originalLogoURI?: string;
|
|
market?: {
|
|
priceUsd?: number;
|
|
volume24h: number;
|
|
volume7d: number;
|
|
volume30d: number;
|
|
marketCapUsd?: number;
|
|
liquidityUsd: number;
|
|
lastUpdated: string;
|
|
};
|
|
totalSupply?: string;
|
|
totalSupplyRaw?: string;
|
|
circulatingSupply?: string;
|
|
circulatingSupplyFormula?: string;
|
|
supplyProofProvenance?: SupplyProofEnrichment['supplyProofProvenance'];
|
|
trackerCaveats?: string[];
|
|
pools: ReportPoolEntry[];
|
|
fromDb: boolean;
|
|
}> = [];
|
|
|
|
for (const spec of canonical) {
|
|
const address = spec.addresses[chainId];
|
|
if (!address || String(address).trim() === '') continue;
|
|
|
|
const [dbToken, marketData, pools] = await Promise.all([
|
|
tokenRepo.getToken(chainId, address),
|
|
marketDataRepo.getMarketData(chainId, address),
|
|
poolRepo.getPoolsByToken(chainId, address),
|
|
]);
|
|
const exposedPools = filterPoolsForExposure(chainId, pools);
|
|
|
|
const resolvedPools = await Promise.all(
|
|
exposedPools.map(async (p) => {
|
|
const { token0, token1 } = await resolvePoolTokenDisplays(tokenRepo, chainId, p.token0Address, p.token1Address);
|
|
return {
|
|
poolAddress: p.poolAddress,
|
|
dex: p.dexType,
|
|
token0,
|
|
token1,
|
|
tvl: p.totalLiquidityUsd,
|
|
volume24h: p.volume24h,
|
|
};
|
|
})
|
|
);
|
|
|
|
const fallbackPriceUsd = getCanonicalPriceUsd(chainId, address);
|
|
|
|
const market = marketData
|
|
? {
|
|
priceUsd: marketData.priceUsd ?? fallbackPriceUsd,
|
|
volume24h: marketData.volume24h,
|
|
volume7d: marketData.volume7d,
|
|
volume30d: marketData.volume30d,
|
|
marketCapUsd: marketData.marketCapUsd,
|
|
liquidityUsd: marketData.liquidityUsd,
|
|
lastUpdated: marketData.lastUpdated?.toISOString() ?? '',
|
|
}
|
|
: fallbackPriceUsd !== undefined
|
|
? {
|
|
priceUsd: fallbackPriceUsd,
|
|
volume24h: 0,
|
|
volume7d: 0,
|
|
volume30d: 0,
|
|
liquidityUsd: 0,
|
|
lastUpdated: `${getCanonicalPriceSnapshotGeneratedAt()}T00:00:00.000Z`,
|
|
}
|
|
: undefined;
|
|
const registryFamily = getTokenRegistryFamily(spec);
|
|
const originalLogoURI = getLogoUriForSpec(spec);
|
|
const logoURI = absoluteReportLogoUri(originalLogoURI, spec.symbol);
|
|
const supplyProof = buildSupplyProofEnrichment(chainId, address, spec.symbol, spec.type, registryFamily, market?.priceUsd);
|
|
if (supplyProof?.marketCapUsd !== undefined && market) {
|
|
market.marketCapUsd = supplyProof.marketCapUsd;
|
|
}
|
|
if (market) {
|
|
market.liquidityUsd =
|
|
normalizePossiblyRawLiquidityUsd(market.liquidityUsd, spec.decimals, market.priceUsd, supplyProof?.totalSupply) ??
|
|
market.liquidityUsd;
|
|
}
|
|
|
|
const dbPoolEntries: ReportPoolEntry[] = resolvedPools.map((p) => ({
|
|
poolAddress: p.poolAddress,
|
|
dex: p.dex,
|
|
token0: p.token0.address,
|
|
token1: p.token1.address,
|
|
token0Symbol: p.token0.symbol,
|
|
token1Symbol: p.token1.symbol,
|
|
tvl: p.tvl,
|
|
volume24h: p.volume24h,
|
|
source: 'indexed-db',
|
|
status: 'indexed',
|
|
statusReason: 'Pool is present in the token-aggregation indexed pool repository.',
|
|
}));
|
|
const liveUniswapV2PoolEntries = buildLiveUniswapV2FallbackPoolsForToken(chainId, address, dbPoolEntries);
|
|
const gruV2FallbackPoolEntries = await buildGruV2FallbackPoolsForToken(chainId, address, [
|
|
...dbPoolEntries,
|
|
...liveUniswapV2PoolEntries,
|
|
]);
|
|
const reportPools = [
|
|
...dbPoolEntries,
|
|
...liveUniswapV2PoolEntries,
|
|
...gruV2FallbackPoolEntries,
|
|
];
|
|
|
|
out.push({
|
|
chainId,
|
|
address: address.toLowerCase(),
|
|
symbol: spec.symbol,
|
|
name: dbToken?.name ?? spec.name,
|
|
type: spec.type,
|
|
decimals: spec.decimals,
|
|
currencyCode: spec.currencyCode,
|
|
registryFamily,
|
|
familySymbol: spec.familySymbol,
|
|
deploymentVersion: spec.deploymentVersion,
|
|
deploymentStatus: spec.deploymentStatus,
|
|
preferredForX402: spec.preferredForX402,
|
|
liquiditySourceSymbol: spec.liquiditySourceSymbol,
|
|
logoURI,
|
|
originalLogoURI,
|
|
market,
|
|
totalSupply: supplyProof?.totalSupply,
|
|
totalSupplyRaw: supplyProof?.totalSupplyRaw,
|
|
circulatingSupply: supplyProof?.circulatingSupply,
|
|
circulatingSupplyFormula: supplyProof?.circulatingSupplyFormula,
|
|
supplyProofProvenance: supplyProof?.supplyProofProvenance,
|
|
trackerCaveats: supplyProof?.trackerCaveats,
|
|
pools: reportPools,
|
|
fromDb: !!dbToken,
|
|
});
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
function describeToken(spec: { currencyCode?: string; registryFamily?: string }): string | undefined {
|
|
const family = String(spec.registryFamily || '').trim();
|
|
const code = String(spec.currencyCode || '').trim().toUpperCase();
|
|
if (!code) return undefined;
|
|
if (family === 'gas_native') {
|
|
return `Governance-approved gas-native ${code} compliant token`;
|
|
}
|
|
if (family === 'monetary_unit') {
|
|
return `GRU monetary-unit ${code} compliant token`;
|
|
}
|
|
if (family === 'commodity') {
|
|
return `Governance-approved commodity ${code} compliant token`;
|
|
}
|
|
return `ISO-4217 ${code} compliant token`;
|
|
}
|
|
|
|
function buildGruTransportOverview() {
|
|
const gruTransportMetadata = getGruTransportMetadata();
|
|
const deploymentStatus = loadDeploymentStatusFile()?.data;
|
|
|
|
if (!gruTransportMetadata && !deploymentStatus) return undefined;
|
|
|
|
const activeTransportPairs = getActiveTransportPairs();
|
|
const fallbackGasAssetFamilies = deploymentStatus ? buildGasAssetFamiliesFromDeploymentStatus(deploymentStatus) : [];
|
|
const fallbackRuntimePairs = deploymentStatus ? buildGasRuntimePairsFromDeploymentStatus(deploymentStatus) : [];
|
|
const gasAssetFamilies = gruTransportMetadata?.gasAssetFamilies ?? fallbackGasAssetFamilies;
|
|
const gasRedeemGroups = gruTransportMetadata?.gasRedeemGroups ?? [];
|
|
const gasProtocolExposure = gruTransportMetadata?.gasProtocolExposure ?? [];
|
|
const runtimePairs = activeTransportPairs.length > 0 ? activeTransportPairs : fallbackRuntimePairs;
|
|
|
|
return {
|
|
system: gruTransportMetadata?.system ?? {
|
|
name: 'GRU Monetary Transport Layer',
|
|
shortName: 'GRU Transport',
|
|
canonicalChainId: 138,
|
|
canonicalChainName: 'DeFi Oracle Meta Chain 138',
|
|
transportClass: 'deployment-status-fallback',
|
|
},
|
|
summary: gruTransportMetadata?.counts ?? {
|
|
enabledCanonicalTokens: 0,
|
|
enabledDestinationChains: new Set(runtimePairs.map((pair) => pair.destinationChainId)).size,
|
|
approvedBridgePeers: 0,
|
|
transportPairs: runtimePairs.length,
|
|
gasAssetFamilies: gasAssetFamilies.length,
|
|
gasRedeemGroups: gasRedeemGroups.length,
|
|
gasProtocolExposure: gasProtocolExposure.length,
|
|
gasTransportPairs: runtimePairs.length,
|
|
runtimeReadyTransportPairs: runtimePairs.filter((pair) => pair.runtimeReady === true).length,
|
|
publicPools: 0,
|
|
},
|
|
gasAssetFamilies,
|
|
gasRedeemGroups,
|
|
gasProtocolExposure,
|
|
activeTransportPairs: runtimePairs.map((pair) => ({
|
|
key: pair.key,
|
|
canonicalSymbol: pair.canonicalSymbol,
|
|
mirroredSymbol: pair.mirroredSymbol,
|
|
destinationChainId: pair.destinationChainId,
|
|
destinationChainName: pair.destinationChainName ?? null,
|
|
assetClass: pair.assetClass,
|
|
familyKey: pair.familyKey,
|
|
backingMode: pair.backingMode,
|
|
redeemPolicy: pair.redeemPolicy,
|
|
wrappedNativeQuoteSymbol: pair.wrappedNativeQuoteSymbol ?? null,
|
|
stableQuoteSymbol: pair.stableQuoteSymbol ?? null,
|
|
eligible: pair.eligible === true,
|
|
runtimeReady: pair.runtimeReady === true,
|
|
supplyInvariantSatisfied: pair.supplyInvariantSatisfied ?? null,
|
|
eligibilityBlockers: Array.isArray(pair.eligibilityBlockers)
|
|
? pair.eligibilityBlockers
|
|
: [],
|
|
runtimeMissingRequirements: Array.isArray(pair.runtimeMissingRequirements)
|
|
? pair.runtimeMissingRequirements
|
|
: [],
|
|
})),
|
|
};
|
|
}
|
|
|
|
function gasFamilyDefaults(familyKey: string, mirroredSymbol: string) {
|
|
const byFamily: Record<string, { canonicalSymbol138: string; assetClass: string; backingMode: string; redeemPolicy: string; laneGroup: string }> = {
|
|
eth_mainnet: {
|
|
canonicalSymbol138: 'cETH',
|
|
assetClass: 'gas_native',
|
|
backingMode: 'strict_escrow',
|
|
redeemPolicy: 'same_family_only',
|
|
laneGroup: 'eth_mainnet',
|
|
},
|
|
eth_l2: {
|
|
canonicalSymbol138: 'cETHL2',
|
|
assetClass: 'gas_native',
|
|
backingMode: 'hybrid_cap',
|
|
redeemPolicy: 'same_family_only',
|
|
laneGroup: 'eth_l2',
|
|
},
|
|
bnb: {
|
|
canonicalSymbol138: 'cBNB',
|
|
assetClass: 'gas_native',
|
|
backingMode: 'strict_escrow',
|
|
redeemPolicy: 'same_family_only',
|
|
laneGroup: 'bnb',
|
|
},
|
|
pol: {
|
|
canonicalSymbol138: 'cPOL',
|
|
assetClass: 'gas_native',
|
|
backingMode: 'strict_escrow',
|
|
redeemPolicy: 'same_family_only',
|
|
laneGroup: 'pol',
|
|
},
|
|
avax: {
|
|
canonicalSymbol138: 'cAVAX',
|
|
assetClass: 'gas_native',
|
|
backingMode: 'strict_escrow',
|
|
redeemPolicy: 'same_family_only',
|
|
laneGroup: 'avax',
|
|
},
|
|
cro: {
|
|
canonicalSymbol138: 'cCRO',
|
|
assetClass: 'gas_native',
|
|
backingMode: 'strict_escrow',
|
|
redeemPolicy: 'same_family_only',
|
|
laneGroup: 'cro',
|
|
},
|
|
xdai: {
|
|
canonicalSymbol138: 'cXDAI',
|
|
assetClass: 'gas_native',
|
|
backingMode: 'strict_escrow',
|
|
redeemPolicy: 'same_family_only',
|
|
laneGroup: 'xdai',
|
|
},
|
|
celo: {
|
|
canonicalSymbol138: 'cCELO',
|
|
assetClass: 'gas_native',
|
|
backingMode: 'strict_escrow',
|
|
redeemPolicy: 'same_family_only',
|
|
laneGroup: 'celo',
|
|
},
|
|
wemix: {
|
|
canonicalSymbol138: 'cWEMIX',
|
|
assetClass: 'gas_native',
|
|
backingMode: 'strict_escrow',
|
|
redeemPolicy: 'same_family_only',
|
|
laneGroup: 'wemix',
|
|
},
|
|
};
|
|
|
|
return byFamily[familyKey] ?? {
|
|
canonicalSymbol138: mirroredSymbol.replace(/^cW/, 'c'),
|
|
assetClass: 'gas_native',
|
|
backingMode: 'strict_escrow',
|
|
redeemPolicy: 'same_family_only',
|
|
laneGroup: familyKey,
|
|
};
|
|
}
|
|
|
|
function buildGasAssetFamiliesFromDeploymentStatus(data: DeploymentStatusFile): GruTransportGasAssetFamily[] {
|
|
const byFamily = new Map<string, GruTransportGasAssetFamily>();
|
|
|
|
for (const [chainIdText, chain] of Object.entries(data.chains ?? {})) {
|
|
const chainId = Number(chainIdText);
|
|
for (const pool of chain.gasPmmPools ?? []) {
|
|
const familyKey = typeof pool.familyKey === 'string' ? pool.familyKey : '';
|
|
const mirroredSymbol = typeof pool.base === 'string' ? pool.base : '';
|
|
if (!familyKey || !mirroredSymbol) continue;
|
|
const defaults = gasFamilyDefaults(familyKey, mirroredSymbol);
|
|
const existing = byFamily.get(familyKey) ?? {
|
|
familyKey,
|
|
active: true,
|
|
status: 'deployment_status_fallback',
|
|
canonicalSymbol138: defaults.canonicalSymbol138,
|
|
mirroredSymbol,
|
|
assetClass: defaults.assetClass,
|
|
originChains: [],
|
|
laneGroup: defaults.laneGroup,
|
|
backingMode: defaults.backingMode,
|
|
redeemPolicy: defaults.redeemPolicy,
|
|
wrappedNativeQuoteSymbol: 'WETH',
|
|
stableQuoteSymbol: 'USDC',
|
|
referenceVenue: 'deployment-status',
|
|
};
|
|
if (!existing.originChains.includes(chainId)) {
|
|
existing.originChains.push(chainId);
|
|
}
|
|
if (typeof pool.quote === 'string' && ['WETH', 'WBNB', 'WPOL', 'WAVAX', 'WCRO', 'WXDAI', 'CELO', 'WEMIX'].includes(pool.quote)) {
|
|
existing.wrappedNativeQuoteSymbol = pool.quote;
|
|
}
|
|
if (typeof pool.quote === 'string' && ['USDC', 'USDT', 'DAI'].includes(pool.quote)) {
|
|
existing.stableQuoteSymbol = pool.quote;
|
|
}
|
|
byFamily.set(familyKey, existing);
|
|
}
|
|
}
|
|
|
|
return Array.from(byFamily.values()).sort((a, b) => a.familyKey.localeCompare(b.familyKey));
|
|
}
|
|
|
|
function buildGasRuntimePairsFromDeploymentStatus(data: DeploymentStatusFile): GruTransportPair[] {
|
|
const familiesByKey = new Map(buildGasAssetFamiliesFromDeploymentStatus(data).map((family) => [family.familyKey, family]));
|
|
const pairs: GruTransportPair[] = [];
|
|
|
|
for (const [chainIdText, chain] of Object.entries(data.chains ?? {})) {
|
|
const destinationChainId = Number(chainIdText);
|
|
const seen = new Set<string>();
|
|
for (const pool of chain.gasPmmPools ?? []) {
|
|
const familyKey = typeof pool.familyKey === 'string' ? pool.familyKey : '';
|
|
const mirroredSymbol = typeof pool.base === 'string' ? pool.base : '';
|
|
if (!familyKey || !mirroredSymbol || seen.has(familyKey)) continue;
|
|
seen.add(familyKey);
|
|
const family = familiesByKey.get(familyKey);
|
|
const canonicalSymbol = family?.canonicalSymbol138 ?? gasFamilyDefaults(familyKey, mirroredSymbol).canonicalSymbol138;
|
|
pairs.push({
|
|
key: `138-${destinationChainId}-${canonicalSymbol}-${mirroredSymbol}`,
|
|
canonicalChainId: 138,
|
|
destinationChainId,
|
|
destinationChainName: chain.name ?? `Chain ${chainIdText}`,
|
|
active: true,
|
|
status: 'deployment_status_fallback',
|
|
canonicalSymbol,
|
|
mirroredSymbol,
|
|
mappingKey: `${canonicalSymbol}:${destinationChainId}`,
|
|
peerKey: `chain-${destinationChainId}`,
|
|
assetClass: 'gas_native',
|
|
familyKey,
|
|
laneGroup: family?.laneGroup,
|
|
backingMode: family?.backingMode,
|
|
redeemPolicy: family?.redeemPolicy,
|
|
wrappedNativeQuoteSymbol: family?.wrappedNativeQuoteSymbol,
|
|
stableQuoteSymbol: family?.stableQuoteSymbol,
|
|
mirrorDeploymentAddress: chain.gasMirrors?.[mirroredSymbol],
|
|
mirrorDeployed: !!chain.gasMirrors?.[mirroredSymbol],
|
|
canonicalEnabled: true,
|
|
destinationEnabled: true,
|
|
bridgeAvailable: chain.bridgeAvailable === true,
|
|
bridgePeerConfigured: chain.bridgeAvailable === true,
|
|
runtimeBridgeReady: chain.bridgeAvailable === true,
|
|
runtimeReady: chain.bridgeAvailable === true && !!chain.gasMirrors?.[mirroredSymbol],
|
|
eligible: true,
|
|
runtimeMissingRequirements: chain.bridgeAvailable === true ? [] : ['bridgeAvailable is not true in deployment-status'],
|
|
eligibilityBlockers: [],
|
|
});
|
|
}
|
|
}
|
|
|
|
return pairs.sort((a, b) => a.destinationChainId - b.destinationChainId || a.familyKey!.localeCompare(b.familyKey!));
|
|
}
|
|
|
|
function buildCanonicalCwFallback(chainIdFilter?: number | null): CwRegistryChain[] {
|
|
const grouped = new Map<number, CwRegistryChain>();
|
|
|
|
for (const spec of CANONICAL_TOKENS) {
|
|
if (spec.type !== 'w') continue;
|
|
for (const [chainIdText, address] of Object.entries(spec.addresses)) {
|
|
const chainId = Number(chainIdText);
|
|
if (!address || Number.isNaN(chainId)) continue;
|
|
if (chainIdFilter && chainId !== chainIdFilter) continue;
|
|
|
|
const existing = grouped.get(chainId) ?? {
|
|
chainId,
|
|
chainIdText,
|
|
name: `Chain ${chainIdText}`,
|
|
tokens: [],
|
|
};
|
|
|
|
existing.tokens.push({ symbol: spec.symbol, address });
|
|
grouped.set(chainId, existing);
|
|
}
|
|
}
|
|
|
|
return Array.from(grouped.values())
|
|
.map((row) => ({
|
|
...row,
|
|
tokens: row.tokens.sort((a, b) => a.symbol.localeCompare(b.symbol)),
|
|
}))
|
|
.sort((a, b) => a.chainId - b.chainId);
|
|
}
|
|
|
|
/** GET /report/assets/* — packaged report assets such as controlled token logos. */
|
|
router.get(/^\/assets\/(.+)$/, (req: Request, res: Response) => {
|
|
const assetPath = String(req.params[0] ?? '');
|
|
const publicRoot = path.resolve(__dirname, '../../../public');
|
|
const resolved = path.resolve(publicRoot, assetPath);
|
|
|
|
if (!resolved.startsWith(publicRoot) || !existsSync(resolved)) {
|
|
res.status(404).json({ error: 'Asset not found' });
|
|
return;
|
|
}
|
|
|
|
res.set('Cache-Control', 'public, max-age=86400, immutable');
|
|
res.sendFile(resolved);
|
|
});
|
|
|
|
/** GET /report/logo/:symbol — extensionless public logo route for proxies that block static file extensions. */
|
|
router.get('/logo/:symbol', (req: Request, res: Response) => {
|
|
const rawSymbol = String(req.params.symbol ?? '').trim();
|
|
const publicRoot = path.resolve(__dirname, '../../../public/token-logos');
|
|
const gruSymbol = rawSymbol.replace(/^cW(?=USD|EUR|GBP|AUD|JPY|CHF|CAD|XAU)/, 'c');
|
|
const candidates = [
|
|
rawSymbol === 'cWBTC' ? path.join(publicRoot, 'trustwallet/btc-logo.png') : '',
|
|
rawSymbol.startsWith('ipfs-') ? path.join(publicRoot, 'ipfs', rawSymbol.replace(/^ipfs-/, '')) : '',
|
|
path.join(publicRoot, 'gru', `${gruSymbol}.svg`),
|
|
].filter(Boolean);
|
|
|
|
const resolved = candidates.find((candidate) => existsSync(candidate));
|
|
if (!resolved) {
|
|
res.status(404).json({ error: 'Logo not found' });
|
|
return;
|
|
}
|
|
|
|
res.set('Cache-Control', 'public, max-age=86400, immutable');
|
|
res.sendFile(resolved);
|
|
});
|
|
|
|
/** GET /report/cross-chain — cross-chain pools, bridge volume, atomic swaps (Chain 138, ALL Mainnet) */
|
|
router.get(
|
|
'/cross-chain',
|
|
cacheMiddleware(2 * 60 * 1000),
|
|
async (req: Request, res: Response) => {
|
|
try {
|
|
const chainId = parseInt(req.query.chainId as string, 10) || 138;
|
|
const report = await buildCrossChainReport(chainId);
|
|
res.json({
|
|
...report,
|
|
format: 'cross-chain-report',
|
|
documentation: 'Use for CMC/CoinGecko submission alongside single-chain reports. Includes CCIP, Alltra, Trustless bridge events and volume by lane.',
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error building report/cross-chain:', error);
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
crossChainPools: [],
|
|
volumeByLane: [],
|
|
atomicSwapVolume24h: 0,
|
|
bridgeVolume24hTotal: 0,
|
|
events: [],
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
/** GET /report/all — all tokens, pools, liquidity, volume (unified) + cross-chain */
|
|
router.get(
|
|
'/all',
|
|
cacheMiddleware(2 * 60 * 1000),
|
|
async (req: Request, res: Response) => {
|
|
try {
|
|
const chainIdParam = req.query.chainId as string | undefined;
|
|
const chainIds = chainIdParam
|
|
? [parseInt(chainIdParam, 10)].filter((n) => !isNaN(n))
|
|
: getSupportedChainIds();
|
|
|
|
const tokensByChain: Record<number, Awaited<ReturnType<typeof buildTokenReport>>> = {};
|
|
const poolsByChain: Record<number, Awaited<ReturnType<typeof poolRepo.getPoolsByChain>>> = {};
|
|
|
|
for (const chainId of chainIds) {
|
|
tokensByChain[chainId] = await buildTokenReport(chainId);
|
|
poolsByChain[chainId] = filterPoolsForExposure(chainId, await poolRepo.getPoolsByChain(chainId));
|
|
}
|
|
|
|
const crossChainReport = await buildCrossChainReport(138).catch(() => null);
|
|
const gruTransport = buildGruTransportOverview();
|
|
|
|
const totalLiquidityByChain: Record<number, number> = {};
|
|
const totalVolume24hByChain: Record<number, number> = {};
|
|
for (const chainId of chainIds) {
|
|
const pools = poolsByChain[chainId] || [];
|
|
totalLiquidityByChain[chainId] = pools.reduce((s, p) => s + (p.totalLiquidityUsd || 0), 0);
|
|
totalVolume24hByChain[chainId] = pools.reduce((s, p) => s + (p.volume24h || 0), 0);
|
|
}
|
|
|
|
res.json({
|
|
generatedAt: new Date().toISOString(),
|
|
chains: chainIds,
|
|
tokens: tokensByChain,
|
|
pools: poolsByChain,
|
|
summary: {
|
|
totalLiquidityUsdByChain: totalLiquidityByChain,
|
|
totalVolume24hUsdByChain: totalVolume24hByChain,
|
|
tokenCountByChain: Object.fromEntries(
|
|
chainIds.map((c) => [c, (tokensByChain[c] || []).length])
|
|
),
|
|
poolCountByChain: Object.fromEntries(
|
|
chainIds.map((c) => [c, (poolsByChain[c] || []).length])
|
|
),
|
|
crossChainBridgeVolume24h: crossChainReport?.bridgeVolume24hTotal,
|
|
crossChainAtomicSwapVolume24h: crossChainReport?.atomicSwapVolume24h,
|
|
},
|
|
crossChain: crossChainReport
|
|
? {
|
|
crossChainPools: crossChainReport.crossChainPools,
|
|
volumeByLane: crossChainReport.volumeByLane,
|
|
atomicSwapVolume24h: crossChainReport.atomicSwapVolume24h,
|
|
bridgeVolume24hTotal: crossChainReport.bridgeVolume24hTotal,
|
|
}
|
|
: undefined,
|
|
gruTransport,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error building report/all:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
}
|
|
);
|
|
|
|
/** GET /report/coingecko — format suitable for CoinGecko submission / API */
|
|
router.get(
|
|
'/coingecko',
|
|
cacheMiddleware(2 * 60 * 1000),
|
|
async (req: Request, res: Response) => {
|
|
try {
|
|
const chainId = parseInt(req.query.chainId as string, 10) || 138;
|
|
const tokens = await buildTokenReport(chainId);
|
|
|
|
const coingeckoFormat = tokens.map((t) => ({
|
|
chain_id: chainId,
|
|
contract_address: t.address,
|
|
id: `${t.symbol.toLowerCase()}-${chainId}`,
|
|
symbol: t.symbol,
|
|
name: t.name,
|
|
asset_platform_id: chainId === 138 ? 'defi-oracle-meta' : chainId === 651940 ? 'all-mainnet' : `chain-${chainId}`,
|
|
decimals: t.decimals,
|
|
logo_uri: t.logoURI,
|
|
original_logo_uri: t.originalLogoURI,
|
|
description: describeToken(t),
|
|
total_supply: t.totalSupply ? Number(t.totalSupply) : undefined,
|
|
total_supply_raw: t.totalSupplyRaw,
|
|
circulating_supply: t.circulatingSupply ? Number(t.circulatingSupply) : undefined,
|
|
circulating_supply_formula: t.circulatingSupplyFormula,
|
|
supply_proof_provenance: t.supplyProofProvenance,
|
|
tracker_caveats: t.trackerCaveats,
|
|
market_data: t.market
|
|
? {
|
|
current_price: { usd: t.market.priceUsd },
|
|
total_volume: t.market.volume24h,
|
|
market_cap: t.market.marketCapUsd,
|
|
liquidity_usd: t.market.liquidityUsd,
|
|
last_updated: t.market.lastUpdated,
|
|
}
|
|
: undefined,
|
|
liquidity_pools: t.pools.map((p) => ({
|
|
pool_address: p.poolAddress,
|
|
dex_id: p.dex,
|
|
tvl_usd: p.tvl,
|
|
volume_24h_usd: p.volume24h,
|
|
source: p.source,
|
|
status: p.status,
|
|
status_reason: p.statusReason,
|
|
role: p.role,
|
|
section: p.section,
|
|
public_routing_enabled: p.publicRoutingEnabled,
|
|
base_symbol: p.token0Symbol,
|
|
quote_symbol: p.token1Symbol,
|
|
})),
|
|
}));
|
|
|
|
const crossChainReport = await buildCrossChainReport(chainId).catch(() => null);
|
|
|
|
res.json({
|
|
generatedAt: new Date().toISOString(),
|
|
chainId,
|
|
format: 'coingecko-submission',
|
|
tokens: coingeckoFormat,
|
|
crossChain: crossChainReport
|
|
? {
|
|
crossChainPools: crossChainReport.crossChainPools,
|
|
volumeByLane: crossChainReport.volumeByLane,
|
|
atomicSwapVolume24h: crossChainReport.atomicSwapVolume24h,
|
|
bridgeVolume24hTotal: crossChainReport.bridgeVolume24hTotal,
|
|
}
|
|
: undefined,
|
|
documentation: 'https://www.coingecko.com/en/api/documentation',
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error building report/coingecko:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
}
|
|
);
|
|
|
|
/** GET /report/cmc — format suitable for CoinMarketCap submission / API */
|
|
router.get(
|
|
'/cmc',
|
|
cacheMiddleware(2 * 60 * 1000),
|
|
async (req: Request, res: Response) => {
|
|
try {
|
|
const chainId = parseInt(req.query.chainId as string, 10) || 138;
|
|
const tokens = await buildTokenReport(chainId);
|
|
|
|
const cmcFormat = tokens.map((t) => ({
|
|
chain_id: chainId,
|
|
contract_address: t.address,
|
|
symbol: t.symbol,
|
|
name: t.name,
|
|
decimals: t.decimals,
|
|
logo_url: t.logoURI,
|
|
original_logo_url: t.originalLogoURI,
|
|
total_supply: t.totalSupply ? Number(t.totalSupply) : undefined,
|
|
total_supply_raw: t.totalSupplyRaw,
|
|
circulating_supply: t.circulatingSupply ? Number(t.circulatingSupply) : undefined,
|
|
circulating_supply_formula: t.circulatingSupplyFormula,
|
|
supply_proof_provenance: t.supplyProofProvenance,
|
|
tracker_caveats: t.trackerCaveats,
|
|
volume_24h: t.market?.volume24h,
|
|
market_cap: t.market?.marketCapUsd,
|
|
liquidity_usd: t.market?.liquidityUsd ?? t.pools.reduce((s, p) => s + p.tvl, 0),
|
|
pairs: t.pools.map((p) => ({
|
|
pair_address: p.poolAddress,
|
|
dex_id: p.dex,
|
|
base: t.address,
|
|
quote: p.token0 === t.address ? p.token1 : p.token0,
|
|
liquidity_usd: p.tvl,
|
|
volume_24h_usd: p.volume24h,
|
|
source: p.source,
|
|
status: p.status,
|
|
status_reason: p.statusReason,
|
|
role: p.role,
|
|
section: p.section,
|
|
public_routing_enabled: p.publicRoutingEnabled,
|
|
})),
|
|
}));
|
|
|
|
const crossChainReport = await buildCrossChainReport(chainId).catch(() => null);
|
|
|
|
res.json({
|
|
generatedAt: new Date().toISOString(),
|
|
chainId,
|
|
format: 'coinmarketcap-dex',
|
|
tokens: cmcFormat,
|
|
crossChain: crossChainReport
|
|
? {
|
|
crossChainPools: crossChainReport.crossChainPools,
|
|
volumeByLane: crossChainReport.volumeByLane,
|
|
atomicSwapVolume24h: crossChainReport.atomicSwapVolume24h,
|
|
bridgeVolume24hTotal: crossChainReport.bridgeVolume24hTotal,
|
|
}
|
|
: undefined,
|
|
documentation: 'https://coinmarketcap.com/api/documentation',
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error building report/cmc:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
}
|
|
);
|
|
|
|
/** GET /report/token-price/:symbol — compact reviewer-facing price and evidence snapshot. */
|
|
router.get(
|
|
'/token-price/:symbol',
|
|
cacheMiddleware(60 * 1000),
|
|
async (req: Request, res: Response) => {
|
|
try {
|
|
const symbol = String(req.params.symbol || '').trim();
|
|
const chainId = parseInt(req.query.chainId as string, 10) || 1;
|
|
const tokens = await buildTokenReport(chainId);
|
|
const token = tokens.find((entry) => entry.symbol.toLowerCase() === symbol.toLowerCase());
|
|
|
|
if (!token) {
|
|
res.status(404).json({
|
|
error: 'Token not found',
|
|
symbol,
|
|
chainId,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const poolLiquidityUsd = token.pools.reduce((sum, pool) => sum + (pool.tvl || 0), 0);
|
|
const marketLiquidityUsd = token.market?.liquidityUsd ?? 0;
|
|
const liquidityUsd = marketLiquidityUsd > 0 ? marketLiquidityUsd : poolLiquidityUsd;
|
|
const priceUsd = token.market?.priceUsd;
|
|
const circulatingSupply = token.circulatingSupply ? Number(token.circulatingSupply) : undefined;
|
|
const totalSupply = token.totalSupply ? Number(token.totalSupply) : undefined;
|
|
const marketCapUsd =
|
|
token.market?.marketCapUsd ??
|
|
(priceUsd !== undefined && circulatingSupply !== undefined ? priceUsd * circulatingSupply : undefined);
|
|
|
|
res.json({
|
|
generatedAt: new Date().toISOString(),
|
|
schema: 'dbis-token-price-evidence/v1',
|
|
chainId,
|
|
token: {
|
|
address: token.address,
|
|
symbol: token.symbol,
|
|
name: token.name,
|
|
decimals: token.decimals,
|
|
type: token.type,
|
|
registryFamily: token.registryFamily,
|
|
logoURI: token.logoURI,
|
|
},
|
|
price: {
|
|
usd: priceUsd,
|
|
source: token.market ? 'token-aggregation' : priceUsd !== undefined ? 'canonical-fallback' : 'unavailable',
|
|
lastUpdated: token.market?.lastUpdated,
|
|
caveat:
|
|
chainId === 1 && token.symbol === 'cWUSDC'
|
|
? 'This is DBIS tracker-submission evidence. Etherscan USD Value appears only after Etherscan/CoinGecko/Dex indexers accept a public price source.'
|
|
: undefined,
|
|
},
|
|
supply: {
|
|
totalSupply,
|
|
totalSupplyRaw: token.totalSupplyRaw,
|
|
circulatingSupply,
|
|
circulatingSupplyFormula: token.circulatingSupplyFormula,
|
|
proof: token.supplyProofProvenance,
|
|
caveats: token.trackerCaveats ?? [],
|
|
},
|
|
valuation: {
|
|
marketCapUsd,
|
|
liquidityUsd,
|
|
volume24hUsd: token.market?.volume24h ?? 0,
|
|
},
|
|
pools: token.pools.map((pool) => ({
|
|
poolAddress: pool.poolAddress,
|
|
dexId: pool.dex,
|
|
tvlUsd: pool.tvl,
|
|
volume24hUsd: pool.volume24h,
|
|
source: pool.source,
|
|
status: pool.status,
|
|
statusReason: pool.statusReason,
|
|
role: pool.role,
|
|
publicRoutingEnabled: pool.publicRoutingEnabled,
|
|
token0: {
|
|
address: pool.token0,
|
|
symbol: pool.token0Symbol,
|
|
},
|
|
token1: {
|
|
address: pool.token1,
|
|
symbol: pool.token1Symbol,
|
|
},
|
|
})),
|
|
submissionLinks: {
|
|
coingeckoReport: `${resolvePublicBaseUrl(req)}/api/v1/report/coingecko?chainId=${chainId}`,
|
|
cmcReport: `${resolvePublicBaseUrl(req)}/api/v1/report/cmc?chainId=${chainId}`,
|
|
tokenList: `${resolvePublicBaseUrl(req)}/api/v1/report/token-list?chainId=${chainId}`,
|
|
etherscan:
|
|
chainId === 1
|
|
? `https://etherscan.io/token/${token.address}`
|
|
: undefined,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error building report/token-price:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
}
|
|
);
|
|
|
|
/** GET /report/token-list — flat list of all canonical tokens (Uniswap token list format with logoURI).
|
|
* If TOKEN_LIST_JSON_URL is set (e.g. GitHub raw URL), fetches and returns that JSON; optional ?chainId= filters tokens.
|
|
*/
|
|
router.get(
|
|
'/token-list',
|
|
cacheMiddleware(5 * 60 * 1000),
|
|
async (req: Request, res: Response) => {
|
|
try {
|
|
const tokenListUrl = process.env.TOKEN_LIST_JSON_URL?.trim();
|
|
if (tokenListUrl) {
|
|
try {
|
|
const data = (await fetchRemoteJson(tokenListUrl)) as {
|
|
name?: string;
|
|
version?: string;
|
|
timestamp?: string;
|
|
logoURI?: string;
|
|
tokens?: Array<{ chainId?: number; address?: string; symbol?: string; name?: string; decimals?: number; [key: string]: unknown }>;
|
|
};
|
|
const chainIdParam = req.query.chainId as string | undefined;
|
|
const chainIdFilter = chainIdParam ? parseInt(chainIdParam, 10) : null;
|
|
let tokens = Array.isArray(data.tokens) ? data.tokens : [];
|
|
if (!isNaN(chainIdFilter as number)) {
|
|
tokens = tokens.filter((t) => t.chainId === chainIdFilter);
|
|
}
|
|
const normalizedTokens = tokens.map((token) => ({
|
|
...token,
|
|
logoURI: absolutePublicUrl(req, typeof token.logoURI === 'string' ? token.logoURI : undefined) ?? token.logoURI,
|
|
}));
|
|
return res.json({
|
|
name: data.name ?? 'Token List',
|
|
version: data.version ?? '1.0.0',
|
|
timestamp: data.timestamp ?? new Date().toISOString(),
|
|
logoURI: absolutePublicUrl(req, data.logoURI) ?? data.logoURI,
|
|
tokens: normalizedTokens,
|
|
});
|
|
} catch (err) {
|
|
logger.error('TOKEN_LIST_JSON_URL fetch failed, using built-in token list:', err);
|
|
}
|
|
}
|
|
|
|
const chainIdParam = req.query.chainId as string | undefined;
|
|
const chainIds = chainIdParam
|
|
? [parseInt(chainIdParam, 10)].filter((n) => !isNaN(n))
|
|
: getSupportedChainIds();
|
|
|
|
const list: Array<{
|
|
chainId: number;
|
|
address: string;
|
|
symbol: string;
|
|
name: string;
|
|
decimals: number;
|
|
type: string;
|
|
logoURI: string;
|
|
originalLogoURI?: string;
|
|
registryFamily?: string;
|
|
familySymbol?: string;
|
|
deploymentVersion?: string;
|
|
deploymentStatus?: string;
|
|
preferredForX402?: boolean;
|
|
}> = [];
|
|
|
|
for (const chainId of chainIds) {
|
|
const specs = getCanonicalTokensByChain(chainId);
|
|
for (const spec of specs) {
|
|
const address = spec.addresses[chainId];
|
|
if (address) {
|
|
const originalLogoURI = getLogoUriForSpec(spec);
|
|
list.push({
|
|
chainId,
|
|
address: address.toLowerCase(),
|
|
symbol: spec.symbol,
|
|
name: spec.name,
|
|
decimals: spec.decimals,
|
|
type: spec.type,
|
|
logoURI: absoluteLogoUri(req, originalLogoURI, spec.symbol),
|
|
originalLogoURI,
|
|
registryFamily: getTokenRegistryFamily(spec),
|
|
familySymbol: spec.familySymbol,
|
|
deploymentVersion: spec.deploymentVersion,
|
|
deploymentStatus: spec.deploymentStatus,
|
|
preferredForX402: spec.preferredForX402,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
name: 'GRU Canonical Token List',
|
|
version: '1.0.0',
|
|
timestamp: new Date().toISOString(),
|
|
logoURI: absolutePublicUrl(req, DBIS_CHAIN_138_LOGO_PATH),
|
|
tokens: list,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error building report/token-list:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
}
|
|
);
|
|
|
|
/** GET /report/cw-registry — live cW* registry from deployment-status.json when available. */
|
|
router.get('/cw-registry', async (req: Request, res: Response) => {
|
|
try {
|
|
const chainIdParam = req.query.chainId as string | undefined;
|
|
const chainIdFilter = chainIdParam ? parseInt(chainIdParam, 10) : null;
|
|
const fileBackedRegistry = loadDeploymentStatusFile();
|
|
|
|
let chains = fileBackedRegistry
|
|
? buildCwRegistryChains(fileBackedRegistry.data)
|
|
: buildCanonicalCwFallback(chainIdFilter);
|
|
|
|
if (chainIdFilter && !Number.isNaN(chainIdFilter)) {
|
|
chains = chains.filter((row) => row.chainId === chainIdFilter);
|
|
}
|
|
|
|
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
|
|
res.json({
|
|
generatedAt: new Date().toISOString(),
|
|
source: fileBackedRegistry ? 'deployment-status-file' : 'canonical-fallback',
|
|
complete: !!fileBackedRegistry,
|
|
version: fileBackedRegistry?.data.version,
|
|
updated: fileBackedRegistry?.data.updated,
|
|
lastModified: fileBackedRegistry?.lastModified,
|
|
chains,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error building report/cw-registry:', error);
|
|
res.status(500).json({ error: 'Internal server error', chains: [] });
|
|
}
|
|
});
|
|
|
|
/** GET /report/gru-v2-pmm-pools — all GRU v2 PMM pools from deployment-status (stable, volatile, gas) with resolved token addresses. */
|
|
router.get('/gru-v2-pmm-pools', async (req: Request, res: Response) => {
|
|
try {
|
|
const chainIdParam = req.query.chainId as string | undefined;
|
|
const chainIdFilter = chainIdParam ? parseInt(chainIdParam, 10) : null;
|
|
const fileBacked = loadDeploymentStatusFile();
|
|
let pools = getGruV2DeploymentPoolRows();
|
|
|
|
if (chainIdFilter && !Number.isNaN(chainIdFilter)) {
|
|
pools = pools.filter((p) => p.chainId === chainIdFilter);
|
|
}
|
|
|
|
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
|
|
res.json({
|
|
generatedAt: new Date().toISOString(),
|
|
source: fileBacked ? 'deployment-status-file' : 'none',
|
|
complete: !!fileBacked,
|
|
version: fileBacked?.data.version,
|
|
updated: fileBacked?.data.updated,
|
|
lastModified: fileBacked?.lastModified,
|
|
homeChainId: fileBacked?.data.homeChainId,
|
|
count: pools.length,
|
|
pools,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error building report/gru-v2-pmm-pools:', error);
|
|
res.status(500).json({ error: 'Internal server error', pools: [] });
|
|
}
|
|
});
|
|
|
|
/** GET /report/adoption-readiness — summary gates for institutional and tracker/listing readiness. */
|
|
router.get('/adoption-readiness', async (_req: Request, res: Response) => {
|
|
try {
|
|
const chainIds = getSupportedChainIds();
|
|
const tokensByChain: Record<number, Awaited<ReturnType<typeof buildTokenReport>>> = {};
|
|
for (const chainId of chainIds) {
|
|
tokensByChain[chainId] = await buildTokenReport(chainId);
|
|
}
|
|
|
|
const allTokens = Object.values(tokensByChain).flat();
|
|
const candidates = allTokens.filter((token) =>
|
|
isGruSupplyTrackedCandidate(token.symbol, token.type, token.registryFamily)
|
|
);
|
|
const nonReportablePlaceholder = candidates.filter(
|
|
(token) => token.supplyProofProvenance?.source === 'deterministic-placeholder-address'
|
|
);
|
|
const reportableCandidates = candidates.filter(
|
|
(token) => token.supplyProofProvenance?.source !== 'deterministic-placeholder-address'
|
|
);
|
|
const proved = reportableCandidates.filter((token) => token.supplyProofProvenance && token.supplyProofProvenance.source !== 'missing-supply-proof');
|
|
const proofRequired = reportableCandidates.filter((token) => token.supplyProofProvenance?.source === 'missing-supply-proof');
|
|
const silent = reportableCandidates.filter((token) => !token.supplyProofProvenance);
|
|
const externalOfficialQuoteLiquidity = reportableCandidates.filter(hasExternalOfficialQuoteLiquidity);
|
|
const liquidityPositive = reportableCandidates.filter(
|
|
(token) => (token.market?.liquidityUsd ?? 0) > 0 || token.pools.some((pool) => pool.tvl > 0) || hasExternalOfficialQuoteLiquidity(token)
|
|
);
|
|
const poolIndexed = reportableCandidates.filter((token) => token.pools.length > 0);
|
|
const liquidityMissing = reportableCandidates.filter(
|
|
(token) => (token.market?.liquidityUsd ?? 0) <= 0 && !token.pools.some((pool) => pool.tvl > 0) && !hasExternalOfficialQuoteLiquidity(token)
|
|
);
|
|
const liquidityMissingWithPools = liquidityMissing.filter((token) => token.pools.length > 0);
|
|
const liquidityMissingWithoutPools = liquidityMissing.filter((token) => token.pools.length === 0);
|
|
const gruPools = getGruV2DeploymentPoolRows();
|
|
const groupTokensByChain = (tokens: typeof candidates) =>
|
|
tokens
|
|
.reduce<Array<{ chainId: number; count: number; symbols: string[] }>>((groups, token) => {
|
|
let group = groups.find((row) => row.chainId === token.chainId);
|
|
if (!group) {
|
|
group = { chainId: token.chainId, count: 0, symbols: [] };
|
|
groups.push(group);
|
|
}
|
|
group.count += 1;
|
|
group.symbols.push(token.symbol);
|
|
return groups;
|
|
}, [])
|
|
.map((group) => ({
|
|
...group,
|
|
symbols: Array.from(new Set(group.symbols)).sort(),
|
|
}))
|
|
.sort((a, b) => a.chainId - b.chainId);
|
|
|
|
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
|
|
res.json({
|
|
generatedAt: new Date().toISOString(),
|
|
scope: 'gru-c-and-cw-assets',
|
|
counts: {
|
|
candidates: candidates.length,
|
|
reportableCandidates: reportableCandidates.length,
|
|
nonReportablePlaceholder: nonReportablePlaceholder.length,
|
|
proved: proved.length,
|
|
proofRequired: proofRequired.length,
|
|
silent: silent.length,
|
|
poolIndexed: poolIndexed.length,
|
|
liquidityPositive: liquidityPositive.length,
|
|
liquidityMissing: liquidityMissing.length,
|
|
liquidityMissingWithPools: liquidityMissingWithPools.length,
|
|
liquidityMissingWithoutPools: liquidityMissingWithoutPools.length,
|
|
externalOfficialQuoteLiquidity: externalOfficialQuoteLiquidity.length,
|
|
gruV2Pools: gruPools.length,
|
|
gruV2PoolsWithStatus: gruPools.filter((pool) => !!pool.status).length,
|
|
},
|
|
institutional: {
|
|
score: Math.round(
|
|
100 *
|
|
((silent.length === 0 ? 0.25 : 0) +
|
|
(proved.length / Math.max(reportableCandidates.length, 1)) * 0.35 +
|
|
(poolIndexed.length / Math.max(reportableCandidates.length, 1)) * 0.2 +
|
|
(gruPools.length > 0 && gruPools.every((pool) => !!pool.status) ? 0.2 : 0))
|
|
),
|
|
blockers: [
|
|
proofRequired.length > 0 ? `${proofRequired.length} assets still require supply-proof artifacts.` : null,
|
|
silent.length > 0 ? `${silent.length} assets have no proof state.` : null,
|
|
gruPools.some((pool) => !pool.status) ? 'One or more GRU v2 pools lacks lifecycle status.' : null,
|
|
].filter(Boolean),
|
|
},
|
|
cryptoListing: {
|
|
score: Math.round(
|
|
100 *
|
|
((proved.length / Math.max(reportableCandidates.length, 1)) * 0.45 +
|
|
(liquidityPositive.length / Math.max(reportableCandidates.length, 1)) * 0.35 +
|
|
(poolIndexed.length / Math.max(reportableCandidates.length, 1)) * 0.2)
|
|
),
|
|
blockers: [
|
|
proofRequired.length > 0 ? 'Most c*/cW* assets are proof-gated for supply and market-cap claims.' : null,
|
|
liquidityPositive.length < reportableCandidates.length ? 'Not every reportable c*/cW* asset has positive indexed liquidity in the report API.' : null,
|
|
].filter(Boolean),
|
|
},
|
|
blockerInventory: {
|
|
proofRequiredByChain: groupTokensByChain(proofRequired),
|
|
liquidityMissingByChain: groupTokensByChain(liquidityMissing),
|
|
liquidityMissingWithPoolsByChain: groupTokensByChain(liquidityMissingWithPools),
|
|
liquidityMissingWithoutPoolsByChain: groupTokensByChain(liquidityMissingWithoutPools),
|
|
liquidityMissingDetails: liquidityMissing
|
|
.map((token) => ({
|
|
chainId: token.chainId,
|
|
symbol: token.symbol,
|
|
address: token.address,
|
|
poolCount: token.pools.length,
|
|
zeroTvlPoolCount: token.pools.filter((pool) => pool.tvl <= 0).length,
|
|
marketLiquidityUsd: token.market?.liquidityUsd ?? 0,
|
|
category: token.pools.length > 0 ? 'configured_or_indexed_pools_zero_tvl' : 'no_visible_pool_binding',
|
|
}))
|
|
.sort((a, b) => a.chainId - b.chainId || a.symbol.localeCompare(b.symbol)),
|
|
externalOfficialQuoteLiquidityByChain: groupTokensByChain(externalOfficialQuoteLiquidity),
|
|
nonReportablePlaceholderByChain: groupTokensByChain(nonReportablePlaceholder),
|
|
gruV2PoolsMissingStatus: gruPools
|
|
.filter((pool) => !pool.status)
|
|
.map((pool) => ({
|
|
chainId: pool.chainId,
|
|
poolAddress: pool.poolAddress,
|
|
baseSymbol: pool.baseSymbol,
|
|
quoteSymbol: pool.quoteSymbol,
|
|
})),
|
|
notes: [
|
|
'proof_required means the report intentionally withholds totalSupply, circulatingSupply, and marketCap claims for that asset.',
|
|
'nonReportablePlaceholder means the configured address is intentionally visible for roadmap traceability but is not a deployed ERC-20 proof surface.',
|
|
'liquidityMissing means no positive indexed liquidity is currently visible through the report API for that asset.',
|
|
'External official quote mirrors (public-chain USDC/USDT and ALL AUSDC/AUSDT) are not GRU pool blockers when their canonical token binding is proved; their public venue liquidity is maintained by the issuer/external market and should not be confused with repo-native cW*/Chain 138 liquidity.',
|
|
'A configured pool or registry entry is not the same thing as tracker-grade supply proof or live positive liquidity.',
|
|
],
|
|
},
|
|
proofRequiredSample: proofRequired.slice(0, 20).map((token) => ({
|
|
chainId: token.chainId,
|
|
symbol: token.symbol,
|
|
address: token.address,
|
|
status: token.supplyProofProvenance?.status,
|
|
})),
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error building report/adoption-readiness:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/** GET /report/gas-registry — live gas-family rollout registry from deployment-status.json plus GRU transport metadata. */
|
|
router.get('/gas-registry', async (req: Request, res: Response) => {
|
|
try {
|
|
const chainIdParam = req.query.chainId as string | undefined;
|
|
const chainIdFilter = chainIdParam ? parseInt(chainIdParam, 10) : null;
|
|
const fileBackedRegistry = loadDeploymentStatusFile();
|
|
const gruTransport = buildGruTransportOverview();
|
|
const loaderRuntimePairs = getActiveTransportPairs();
|
|
const fallbackRuntimePairs = fileBackedRegistry ? buildGasRuntimePairsFromDeploymentStatus(fileBackedRegistry.data) : [];
|
|
const runtimeGasPairs = (loaderRuntimePairs.length > 0 ? loaderRuntimePairs : fallbackRuntimePairs)
|
|
.filter((pair) => pair.assetClass === 'gas_native')
|
|
.map((pair) => ({
|
|
key: pair.key,
|
|
destinationChainId: pair.destinationChainId,
|
|
destinationChainName: pair.destinationChainName ?? null,
|
|
familyKey: pair.familyKey ?? null,
|
|
canonicalSymbol: pair.canonicalSymbol,
|
|
mirroredSymbol: pair.mirroredSymbol,
|
|
wrappedNativeQuoteSymbol: pair.wrappedNativeQuoteSymbol ?? null,
|
|
stableQuoteSymbol: pair.stableQuoteSymbol ?? null,
|
|
backingMode: pair.backingMode ?? null,
|
|
redeemPolicy: pair.redeemPolicy ?? null,
|
|
runtimeReady: pair.runtimeReady === true,
|
|
supplyInvariantSatisfied: pair.supplyInvariantSatisfied ?? null,
|
|
eligibilityBlockers: Array.isArray(pair.eligibilityBlockers) ? pair.eligibilityBlockers : [],
|
|
runtimeMissingRequirements: Array.isArray(pair.runtimeMissingRequirements)
|
|
? pair.runtimeMissingRequirements
|
|
: [],
|
|
}));
|
|
|
|
let chains = fileBackedRegistry ? buildGasRegistryChains(fileBackedRegistry.data) : [];
|
|
if (chainIdFilter && !Number.isNaN(chainIdFilter)) {
|
|
chains = chains.filter((row) => row.chainId === chainIdFilter);
|
|
}
|
|
|
|
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
|
|
res.json({
|
|
generatedAt: new Date().toISOString(),
|
|
source: fileBackedRegistry ? 'deployment-status-file' : 'transport-config-only',
|
|
complete: !!fileBackedRegistry,
|
|
version: fileBackedRegistry?.data.version,
|
|
updated: fileBackedRegistry?.data.updated,
|
|
lastModified: fileBackedRegistry?.lastModified,
|
|
gasAssetFamilies: gruTransport?.gasAssetFamilies ?? [],
|
|
gasRedeemGroups: gruTransport?.gasRedeemGroups ?? [],
|
|
gasProtocolExposure: gruTransport?.gasProtocolExposure ?? [],
|
|
runtimePairs: runtimeGasPairs,
|
|
chains,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error building report/gas-registry:', error);
|
|
res.status(500).json({ error: 'Internal server error', chains: [] });
|
|
}
|
|
});
|
|
|
|
/** GET /report/canonical — raw canonical spec list (no DB merge) */
|
|
router.get(
|
|
'/canonical',
|
|
cacheMiddleware(10 * 60 * 1000),
|
|
async (req: Request, res: Response) => {
|
|
try {
|
|
const gruTransport = buildGruTransportOverview();
|
|
res.json({
|
|
generatedAt: new Date().toISOString(),
|
|
gruTransport,
|
|
tokens: CANONICAL_TOKENS.map((t) => ({
|
|
symbol: t.symbol,
|
|
name: t.name,
|
|
type: t.type,
|
|
decimals: t.decimals,
|
|
currencyCode: t.currencyCode,
|
|
registryFamily: getTokenRegistryFamily(t),
|
|
familySymbol: t.familySymbol,
|
|
deploymentVersion: t.deploymentVersion,
|
|
deploymentStatus: t.deploymentStatus,
|
|
preferredForX402: t.preferredForX402,
|
|
liquiditySourceSymbol: t.liquiditySourceSymbol,
|
|
addresses: t.addresses,
|
|
})),
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error building report/canonical:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
}
|
|
);
|
|
|
|
export default router;
|