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>
This commit is contained in:
defiQUG
2026-05-10 12:56:30 -07:00
parent 27f8e3a500
commit 76143a8fe3
67 changed files with 6972 additions and 136 deletions

View File

@@ -78,6 +78,22 @@ function uniquePaths(paths: Array<string | undefined | null>): string[] {
return out;
}
/** Non-empty built-in CCIP / trustless lane counts for /bridge/metrics (telemetry-friendly). */
function summarizeBuiltInBridgeLanes() {
const payload = buildDefaultBridgeRoutes();
const trustlessKeys = payload.routes.trustless ? Object.keys(payload.routes.trustless) : [];
const chain138 = payload.chain138Bridges as Record<string, string | undefined>;
return {
weth9Destinations: Object.keys(payload.routes.weth9).length,
weth10Destinations: Object.keys(payload.routes.weth10).length,
trustlessDestinations: trustlessKeys.length,
chain138ConfiguredBridges: Object.keys(chain138).filter((k) => {
const v = chain138[k];
return typeof v === 'string' && v.startsWith('0x');
}),
};
}
function resolveBridgeRoutesPath(): string | null {
const candidates = uniquePaths([
process.env.BRIDGE_LIST_JSON_PATH,
@@ -176,16 +192,35 @@ router.get('/status', (_req: Request, res: Response) => {
router.get('/metrics', (_req: Request, res: Response) => {
const gruTransport = buildGruTransportStatus();
const builtIn = summarizeBuiltInBridgeLanes();
res.json({
ok: true,
lanes: [],
lanes: [
{
kind: 'ccip-weth9',
label: 'WETH9 bridge destinations (built-in catalog)',
count: builtIn.weth9Destinations,
},
{
kind: 'ccip-weth10',
label: 'WETH10 bridge destinations (built-in catalog)',
count: builtIn.weth10Destinations,
},
{
kind: 'trustless',
label: 'Trustless / Lockbox destinations (env-backed)',
count: builtIn.trustlessDestinations,
},
],
chain138Bridges: builtIn.chain138ConfiguredBridges,
gruTransport: gruTransport
? {
system: gruTransport.system,
summary: gruTransport.summary,
}
: null,
message: 'Bridge metrics include GRU Transport summary counts. Use /api/v1/report/cross-chain for aggregated data.',
message:
'Lane counts reflect the built-in CCIP route catalog plus GRU Transport summary. Use /api/v1/bridge/routes for full JSON and /api/v1/report/cross-chain for volumes.',
});
});

View File

@@ -139,4 +139,83 @@ describe('Config API runtime networks loader', () => {
await new Promise<void>((resolve, reject) => remoteServer.close((err) => (err ? reject(err) : resolve())));
}
});
it('serves wallet-facing MetaMask aliases with absolute token images', async () => {
const networksRes = await fetch(`${baseUrl}/api/v1/config/networks`);
expect(networksRes.status).toBe(200);
const networksBody = (await networksRes.json()) as Record<string, any>;
expect(networksBody.networks).toEqual(
expect.arrayContaining([
expect.objectContaining({
chainIdDecimal: 138,
iconUrls: expect.arrayContaining([
'https://explorer.d-bis.org/token-icons/chain-138.png',
'https://explorer.d-bis.org/api/v1/report/logo/chain-138',
]),
}),
])
);
const metamaskRes = await fetch(`${baseUrl}/api/v1/config/metamask?chainId=138`);
expect(metamaskRes.status).toBe(200);
const metamaskBody = (await metamaskRes.json()) as Record<string, any>;
expect(metamaskBody.addEthereumChain).toMatchObject({
chainId: '0x8a',
chainName: 'DeFi Oracle Meta Mainnet',
});
expect(metamaskBody.watchAssets).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'ERC20',
options: expect.objectContaining({
symbol: 'cUSDC',
image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/cUSDC\?v=20260510$/),
}),
}),
expect.objectContaining({
type: 'ERC20',
options: expect.objectContaining({
address: '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03',
symbol: 'LINK',
image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/LINK\?v=20260510$/),
}),
}),
expect.objectContaining({
type: 'ERC20',
options: expect.objectContaining({
symbol: 'WETH',
image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/ETH\?v=20260510$/),
}),
}),
expect.objectContaining({
type: 'ERC20',
options: expect.objectContaining({
symbol: 'cWEMIX',
image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/WEMIX\?v=20260510$/),
}),
}),
])
);
expect(metamaskBody.watchAssets).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'ERC20',
options: expect.objectContaining({
address: '0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d',
symbol: 'cUSDC',
}),
metadata: expect.objectContaining({
catalogSymbol: 'cUSDC_V2',
familySymbol: 'cUSDC',
deploymentVersion: 'v2',
}),
}),
])
);
expect(
metamaskBody.watchAssets.some(
(entry: Record<string, any>) => entry.options?.symbol === 'cUSDC_V2' || entry.options?.symbol === 'cUSDT_V2'
)
).toBe(false);
});
});

View File

@@ -1,12 +1,75 @@
import { Router, Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import { getNetworks, getConfigByChain, API_VERSION } from '../../config/networks';
import { getNetworks, getConfigByChain, API_VERSION, type NetworkEntry } from '../../config/networks';
import { getCanonicalTokensByChain, getLogoUriForSpec, getTokenRegistryFamily } from '../../config/canonical-tokens';
import { cacheMiddleware } from '../middleware/cache';
import { fetchRemoteJson } from '../utils/fetch-remote-json';
import { logger } from '../../utils/logger';
const router: Router = Router();
const DEFAULT_PUBLIC_BASE_URL = 'https://explorer.d-bis.org';
const DEFAULT_WALLET_METADATA_VERSION = '20260510';
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_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 appendWalletMetadataVersion(value: string | undefined): string | undefined {
if (!value) return undefined;
const version = (process.env.WALLET_METADATA_IMAGE_VERSION || DEFAULT_WALLET_METADATA_VERSION).trim();
if (!version) return value;
const separator = value.includes('?') ? '&' : '?';
return `${value}${separator}v=${encodeURIComponent(version)}`;
}
function localLogoPathForSymbol(symbol: string, originalLogoUri: string): string {
if (originalLogoUri.includes('/token-lists/logos/gru/')) {
const fileName = originalLogoUri.split('/').pop()?.replace(/\.svg$/i, '');
if (fileName) return `/api/v1/report/logo/${fileName}`;
}
if (originalLogoUri.includes('/blockchains/bitcoin/info/logo.png')) return '/api/v1/report/logo/cWBTC';
if (originalLogoUri.includes('/ipfs/')) {
const cid = originalLogoUri.split('/').pop();
if (cid) return `/api/v1/report/logo/ipfs-${cid}`;
}
if (symbol === 'cWUSDC') return '/api/v1/report/logo/cUSDC';
return originalLogoUri;
}
function resolveWalletWatchAssetSymbol(spec: { symbol: string; familySymbol?: string; deploymentVersion?: string }): string {
// MetaMask validates wallet_watchAsset.symbol against ERC-20 symbol().
// Staged V2 Chain 138 deployments currently keep the family symbol on-chain
// (for example cUSDC), while the catalog symbol distinguishes the row
// (for example cUSDC_V2). Keep the catalog identity in metadata and send the
// contract-facing symbol in options.symbol so EIP-747 succeeds.
if (spec.deploymentVersion && spec.familySymbol) return spec.familySymbol;
return spec.symbol;
}
type RuntimeNetworksPayload = {
version?: string | { major?: number; minor?: number; patch?: number };
@@ -124,16 +187,77 @@ async function resolveNetworksPayload(): Promise<NetworksPayload> {
};
}
async function sendNetworks(_req: Request, res: Response): Promise<void> {
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
const payload = await resolveNetworksPayload();
res.json(payload);
}
/**
* GET /api/v1/networks
* Full EIP-3085 chain params for wallet_addEthereumChain (Chain 138, 1, 651940).
* If NETWORKS_JSON_URL is set (e.g. GitHub raw URL), fetches and returns that JSON; otherwise uses built-in networks.
*/
router.get('/networks', cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
router.get(['/networks', '/config/networks', '/metamask/networks'], cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
try {
await sendNetworks(req, res);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /api/v1/config/metamask
* Wallet-facing aliases for Chain 138 add-chain params and watchAsset token entries.
*/
router.get(['/config/metamask', '/metamask'], cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
try {
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
const chainId = parseInt(String(req.query.chainId ?? '138'), 10) || 138;
const payload = await resolveNetworksPayload();
res.json(payload);
const networks = payload.networks as NetworkEntry[];
const network = networks.find((entry) => Number(entry.chainIdDecimal) === chainId);
if (!network) {
res.status(404).json({ error: 'Chain not found', chainId });
return;
}
const watchAssets = getCanonicalTokensByChain(chainId)
.map((spec) => {
const address = spec.addresses[chainId];
if (!address) return null;
const originalLogoURI = getLogoUriForSpec(spec);
return {
type: 'ERC20',
options: {
address,
symbol: resolveWalletWatchAssetSymbol(spec),
decimals: spec.decimals,
image: appendWalletMetadataVersion(absolutePublicUrl(req, localLogoPathForSymbol(spec.symbol, originalLogoURI))),
},
metadata: {
name: spec.name,
catalogSymbol: spec.symbol,
registryFamily: getTokenRegistryFamily(spec),
familySymbol: spec.familySymbol,
deploymentVersion: spec.deploymentVersion,
deploymentStatus: spec.deploymentStatus,
},
};
})
.filter(Boolean);
res.json({
source: payload.source,
version: payload.version,
chainId,
addEthereumChain: network,
watchAssets,
caveats: [
'MetaMask custom-token prices are controlled by MetaMask and its upstream asset/price providers; this endpoint supplies wallet metadata, logo URLs, and token-add payloads but cannot force MetaMask to render fiat prices.',
'After metadata changes, remove and re-add the custom network/token in MetaMask or use the companion UI to call wallet_addEthereumChain and wallet_watchAsset again.',
],
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}

View File

@@ -91,6 +91,24 @@ describe('Report API', () => {
const body = (await res.json()) as Record<string, unknown>;
expect(body.chainId).toBe(651940);
});
it('enriches Mainnet cWUSDC with supply proof fields for CMC reports', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/cmc?chainId=1`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
expect(cwusdc).toMatchObject({
contract_address: '0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a',
total_supply: 10451316981.309788,
total_supply_raw: '10451316981309788',
circulating_supply: 10451316981.309788,
market_cap: 10451316981.309788,
supply_proof_provenance: expect.objectContaining({
status: 'ready_for_tracker_review',
}),
});
expect(cwusdc.tracker_caveats).toEqual(expect.arrayContaining([expect.stringContaining('on-chain supply proof')]));
});
});
describe('GET /api/v1/report/coingecko', () => {
@@ -104,6 +122,62 @@ describe('Report API', () => {
expect(body).toHaveProperty('tokens');
expect(Array.isArray(body.tokens)).toBe(true);
});
it('enriches Mainnet cWUSDC with supply proof, circulating supply, and market cap', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=1`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
expect(cwusdc).toMatchObject({
contract_address: '0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a',
total_supply: 10451316981.309788,
total_supply_raw: '10451316981309788',
circulating_supply: 10451316981.309788,
circulating_supply_formula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances',
supply_proof_provenance: expect.objectContaining({
schema: 'mainnet-cwusdc-supply-proof/v1',
referenceBlock: 25047586,
}),
market_data: expect.objectContaining({
current_price: { usd: 1 },
market_cap: 10451316981.309788,
}),
});
expect(cwusdc.tracker_caveats).toEqual(expect.arrayContaining([expect.stringContaining('No public tracker')]));
});
it('surfaces GRU v2 deployment-status pools in tracker-facing token reports', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=1`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
expect(cwusdc.liquidity_pools).toEqual(
expect.arrayContaining([
expect.objectContaining({
pool_address: '0x69776fc607e9eda8042e320e7e43f54d06c68f0e',
source: 'gru-v2-deployment-status',
status: 'live',
role: 'defense',
}),
])
);
});
it('surfaces explicit supply-proof gaps for Mainnet GRU assets without proof artifacts', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=1`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
const cwusdt = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDT');
expect(cwusdt).toMatchObject({
contract_address: '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae',
supply_proof_provenance: {
source: 'missing-supply-proof',
status: 'proof_required',
},
});
expect(cwusdt).not.toHaveProperty('total_supply');
expect(cwusdt.tracker_caveats).toEqual(expect.arrayContaining([expect.stringContaining('tracker-grade supply proof')]));
});
});
describe('GET /api/v1/report/all', () => {
@@ -149,6 +223,72 @@ describe('Report API', () => {
}),
});
});
it('includes Mainnet cWUSDC supply proof enrichment in unified reports', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=1`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
const cwusdc = body.tokens?.['1']?.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
expect(cwusdc).toMatchObject({
totalSupply: '10451316981.309788',
totalSupplyRaw: '10451316981309788',
circulatingSupply: '10451316981.309788',
circulatingSupplyFormula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances',
market: expect.objectContaining({
priceUsd: 1,
marketCapUsd: 10451316981.309788,
}),
supplyProofProvenance: expect.objectContaining({
schema: 'mainnet-cwusdc-supply-proof/v1',
referenceBlock: 25047586,
}),
});
});
it('distinguishes proof-gated Mainnet cW assets from deterministic placeholder bindings', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=1`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
const proofGated = body.tokens?.['1']?.find((entry: Record<string, any>) => entry.symbol === 'cWUSDT');
expect(proofGated).toMatchObject({
supplyProofProvenance: {
source: 'missing-supply-proof',
status: 'proof_required',
},
});
expect(proofGated.totalSupply).toBeUndefined();
expect(proofGated.trackerCaveats).toEqual(expect.arrayContaining([expect.stringContaining('proof artifact')]));
const placeholderSymbols = ['cWBTC', 'cWETH'];
for (const symbol of placeholderSymbols) {
const token = body.tokens?.['1']?.find((entry: Record<string, any>) => entry.symbol === symbol);
expect(token).toMatchObject({
supplyProofProvenance: {
source: 'deterministic-placeholder-address',
status: 'non_reportable_until_erc20_deployed',
},
});
expect(token.totalSupply).toBeUndefined();
expect(token.trackerCaveats).toEqual(expect.arrayContaining([expect.stringContaining('deterministic placeholder')]));
}
});
it('marks proofless base GRU c assets as proof gated instead of leaving silent supply fields', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=138`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
const cusdc = body.tokens?.['138']?.find((entry: Record<string, any>) => entry.symbol === 'cUSDC');
expect(cusdc).toMatchObject({
type: 'base',
registryFamily: 'iso4217',
supplyProofProvenance: {
source: 'missing-supply-proof',
status: 'proof_required',
},
});
expect(cusdc.totalSupply).toBeUndefined();
expect(cusdc.trackerCaveats).toEqual(expect.arrayContaining([expect.stringContaining('tracker-grade supply proof')]));
});
});
describe('GET /api/v1/report/gas-registry', () => {
@@ -185,6 +325,63 @@ describe('Report API', () => {
});
});
describe('GET /api/v1/report/adoption-readiness', () => {
it('summarizes proved, proof-gated, pool-indexed, and scoring gates', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/adoption-readiness`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.scope).toBe('gru-c-and-cw-assets');
expect(body.counts).toMatchObject({
candidates: expect.any(Number),
reportableCandidates: expect.any(Number),
nonReportablePlaceholder: expect.any(Number),
proved: expect.any(Number),
proofRequired: expect.any(Number),
silent: 0,
liquidityMissing: expect.any(Number),
liquidityMissingWithPools: expect.any(Number),
liquidityMissingWithoutPools: expect.any(Number),
gruV2PoolsWithStatus: expect.any(Number),
});
expect(body.institutional.score).toEqual(expect.any(Number));
expect(body.cryptoListing.score).toEqual(expect.any(Number));
expect(Array.isArray(body.blockerInventory.proofRequiredByChain)).toBe(true);
expect(Array.isArray(body.blockerInventory.liquidityMissingByChain)).toBe(true);
expect(Array.isArray(body.blockerInventory.liquidityMissingWithPoolsByChain)).toBe(true);
expect(Array.isArray(body.blockerInventory.liquidityMissingWithoutPoolsByChain)).toBe(true);
expect(Array.isArray(body.blockerInventory.liquidityMissingDetails)).toBe(true);
expect(Array.isArray(body.blockerInventory.externalOfficialQuoteLiquidityByChain)).toBe(true);
expect(Array.isArray(body.blockerInventory.nonReportablePlaceholderByChain)).toBe(true);
expect(Array.isArray(body.blockerInventory.gruV2PoolsMissingStatus)).toBe(true);
expect(Array.isArray(body.blockerInventory.notes)).toBe(true);
});
it('does not treat external official USDC/USDT mirrors as GRU liquidity blockers', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/adoption-readiness`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.counts.externalOfficialQuoteLiquidity).toBeGreaterThan(0);
expect(body.blockerInventory.externalOfficialQuoteLiquidityByChain).toEqual(
expect.arrayContaining([
expect.objectContaining({
chainId: 1,
symbols: expect.arrayContaining(['cUSDC', 'cUSDT']),
}),
expect.objectContaining({
chainId: 56,
symbols: expect.arrayContaining(['cUSDC', 'cUSDT']),
}),
])
);
expect(body.blockerInventory.liquidityMissingDetails).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ chainId: 1, symbol: 'cUSDT' }),
expect.objectContaining({ chainId: 56, symbol: 'cUSDC' }),
])
);
});
});
describe('GET /api/v1/report/token-list', () => {
it('surfaces both V1 and V2 Chain 138 canonical GRU deployments explicitly', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=138`);
@@ -379,6 +576,17 @@ describe('Report API', () => {
])
);
});
it('uses packaged DBIS-level local logo assets while preserving original logo references', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=1`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
expect(cwusdc).toMatchObject({
logoURI: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/cUSDC$/),
originalLogoURI: expect.stringContaining('/token-lists/logos/gru/cUSDC.svg'),
});
});
});
describe('GET /api/v1/report/cw-registry', () => {
@@ -488,6 +696,8 @@ describe('Report API', () => {
expect((body.pools as Array<{ poolAddress: string }>)[0]).toMatchObject({
poolAddress: '0x1111111111111111111111111111111111111111',
section: 'pmmPools',
status: 'routing_enabled',
statusReason: expect.stringContaining('public routing is enabled'),
});
} finally {
await import('fs/promises').then((fs) => fs.unlink(tempPath).catch(() => undefined));

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import express, { Express, Request, Response, NextFunction } from 'express';
import { Server } from 'http';
import path from 'path';
import { readFileSync, existsSync } from 'fs';
import cors from 'cors';
@@ -48,6 +49,7 @@ export class ApiServer {
private indexerEnabled: boolean;
private indexer: MultiChainIndexer | null;
private omnlPoller: OmnlEventPoller | null;
private server: Server | null;
private resolveTrustProxySetting(): boolean | number | string {
const raw = (process.env.EXPRESS_TRUST_PROXY ?? process.env.TRUST_PROXY ?? '1').trim();
@@ -65,6 +67,7 @@ export class ApiServer {
this.indexerEnabled = this.resolveFeatureFlag('ENABLE_INDEXER', true);
this.indexer = this.indexerEnabled ? new MultiChainIndexer() : null;
this.omnlPoller = this.resolveFeatureFlag('ENABLE_OMNL_EVENT_POLLER', false) ? new OmnlEventPoller() : null;
this.server = null;
this.setupMiddleware();
this.setupRoutes();
@@ -106,6 +109,14 @@ export class ApiServer {
});
next();
});
const publicPath = path.join(__dirname, '../../public');
if (existsSync(publicPath)) {
this.app.use('/static', express.static(publicPath, {
immutable: true,
maxAge: '1d',
}));
}
}
private setupRoutes(): void {
@@ -151,6 +162,39 @@ export class ApiServer {
res.type('html').send(readFileSync(dashboardPath, 'utf8'));
});
// Public API catalog (register before routers so GET /api/v1 is not swallowed by middleware-only mounts)
const sendApiV1Catalog = (_req: Request, res: Response) => {
res.json({
service: 'token-aggregation',
version: '1.0.0',
note: 'Prefix paths with your public API origin (e.g. https://explorer.d-bis.org).',
paths: {
catalog: '/api/v1',
health: '/health',
chains: '/api/v1/chains',
networks: '/api/v1/networks',
config: '/api/v1/config',
tokens: '/api/v1/tokens',
tokenDetail: '/api/v1/tokens/{address}',
quote: '/api/v1/quote',
bridgeRoutes: '/api/v1/bridge/routes',
bridgeStatus: '/api/v1/bridge/status',
bridgeMetrics: '/api/v1/bridge/metrics',
bridgePreflight: '/api/v1/bridge/preflight',
tokenMappingPairs: '/api/v1/token-mapping/pairs',
tokenMappingResolve: '/api/v1/token-mapping/resolve',
reportTokenList: '/api/v1/report/token-list',
routesTree: '/api/v1/routes/tree',
plannerProvidersCapabilities: '/api/v2/providers/capabilities',
plannerRoutesPlan: '/api/v2/routes/plan',
plannerIntentsPlan: '/api/v2/intents/plan',
plannerInternalExecutionPlan: '/api/v2/routes/internal-execution-plan',
},
});
};
this.app.get('/api/v1', sendApiV1Catalog);
this.app.get('/api/v1/', sendApiV1Catalog);
// API routes
this.app.use('/api/v1', tokenRoutes);
this.app.use('/api/v1', configRoutes);
@@ -206,21 +250,25 @@ export class ApiServer {
async start(): Promise<void> {
try {
// Start server
this.server = this.app.listen(this.port, () => {
logger.info(`Token Aggregation Service listening on port ${this.port}`);
logger.info(`Health check: http://localhost:${this.port}/health`);
logger.info(`API: http://localhost:${this.port}/api/v1`);
});
if (this.indexer) {
await this.indexer.initialize();
await this.indexer.startAll();
this.indexer
.initialize()
.then(() => this.indexer?.startAll())
.catch((error) => {
logger.error('Token aggregation indexer failed after API startup:', error);
});
} else {
logger.info('Token aggregation indexer disabled by ENABLE_INDEXER flag');
}
this.omnlPoller?.start();
// Start server
this.app.listen(this.port, () => {
logger.info(`Token Aggregation Service listening on port ${this.port}`);
logger.info(`Health check: http://localhost:${this.port}/health`);
logger.info(`API: http://localhost:${this.port}/api/v1`);
});
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
@@ -230,6 +278,18 @@ export class ApiServer {
async stop(): Promise<void> {
this.omnlPoller?.stop();
this.indexer?.stopAll();
if (this.server) {
await new Promise<void>((resolve, reject) => {
this.server?.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
this.server = null;
}
logger.info('Server stopped');
}
}