import express, { Express, Request, Response, NextFunction, type RequestHandler } from 'express'; import { Server } from 'http'; import path from 'path'; import { readFileSync, existsSync } from 'fs'; import cors from 'cors'; import compression from 'compression'; import { apiRateLimiter, strictRateLimiter } from './middleware/rate-limit'; import tokenRoutes from './routes/tokens'; import reportRoutes from './routes/report'; import adminRoutes from './routes/admin'; import configRoutes from './routes/config'; import bridgeRoutes from './routes/bridge'; import quoteRoutes from './routes/quote'; import routeTreeRoutes from './routes/routes'; import tokenMappingRoutes from './routes/token-mapping'; import polygonMapperReportRoutes from './routes/polygon-mapper'; import officialProtocolsReportRoutes from './routes/official-protocols'; import explorerRegistryOpenApiRoutes from './routes/explorer-registry-openapi'; import heatmapRoutes from './routes/heatmap'; import arbitrageRoutes from './routes/arbitrage'; import aggregatorRouteMatrixRoutes from './routes/aggregator-routes'; import partnerPayloadRoutes from './routes/partner-payloads'; import plannerV2Routes from './routes/planner-v2'; import omnlRoutes from './routes/omnl'; import omnlIpsasRoutes from './routes/omnl-ipsas'; import omnlComplianceRoutes from './routes/omnl-compliance-routes'; import omnlTerminalRoutes from './routes/omnl-terminal-routes'; import checkpointRoutes from './routes/checkpoint'; import reserveCapacityRoutes from './routes/reserve-capacity'; import { MultiChainIndexer } from '../indexer/chain-indexer'; import { OmnlEventPoller } from '../indexer/omnl-event-poller'; import { getDatabasePool } from '../database/client'; import winston from 'winston'; // Setup logger const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ), }), ], }); export class ApiServer { private app: Express; private port: number; 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(); const normalized = raw.toLowerCase(); if (normalized === 'true') return true; if (normalized === 'false') return false; if (/^\d+$/.test(raw)) return parseInt(raw, 10); return raw; } constructor() { this.app = express(); this.port = parseInt(process.env.PORT || '3000', 10); 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(); this.setupErrorHandling(); } private resolveFeatureFlag(name: string, fallback: boolean): boolean { const raw = (process.env[name] || '').trim().toLowerCase(); if (!raw) return fallback; if (['1', 'true', 'yes', 'on'].includes(raw)) return true; if (['0', 'false', 'no', 'off'].includes(raw)) return false; return fallback; } private setupMiddleware(): void { const trustProxy = this.resolveTrustProxySetting(); this.app.set('trust proxy', trustProxy); // CORS this.app.use(cors()); // Compression (Express 5 typings — runtime-compatible) this.app.use(compression() as unknown as RequestHandler); // Body parsing this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); // Rate limiting this.app.use('/api/v1', apiRateLimiter); // Request logging this.app.use((req: Request, res: Response, next: NextFunction) => { logger.info(`${req.method} ${req.path}`, { ip: req.ip, forwardedFor: req.get('x-forwarded-for'), trustProxy, userAgent: req.get('user-agent'), }); next(); }); const publicPath = path.join(__dirname, '../../public'); if (existsSync(publicPath)) { const staticOpts = { immutable: true, maxAge: '1d', } as const; this.app.use('/static', express.static(publicPath, staticOpts)); this.app.use('/omnl/static', express.static(publicPath, staticOpts)); this.app.use('/reserve/static', express.static(publicPath, staticOpts)); this.app.use( '/chain138-attestation', express.static(path.join(publicPath, 'chain138-attestation'), staticOpts) ); } } private setupRoutes(): void { // Health check this.app.get('/health', async (req: Request, res: Response) => { if (process.env.OMNL_SETTLEMENT_TERMINAL_ONLY === '1') { res.json({ status: 'healthy', mode: 'omnl-settlement-terminal-only', timestamp: new Date().toISOString(), services: { database: 'skipped', indexer: 'disabled', }, }); return; } try { // Check database connection const pool = getDatabasePool(); await pool.query('SELECT 1'); res.json({ status: 'healthy', timestamp: new Date().toISOString(), services: { database: 'connected', indexer: this.indexerEnabled ? 'running' : 'disabled', }, }); } catch (error) { res.status(503).json({ status: 'unhealthy', timestamp: new Date().toISOString(), error: error instanceof Error ? error.message : 'Unknown error', }); } }); const dashboardPath = path.join(__dirname, '../../public/omnl-dashboard.html'); const complianceConsolePath = path.join(__dirname, '../../public/omnl-compliance-console.html'); const settlementTerminalPath = path.join(__dirname, '../../public/omnl-settlement-terminal.html'); const reserveDashboardPath = path.join(__dirname, '../../public/reserve-dashboard.html'); const reserveInstitutionalPath = path.join(__dirname, '../../public/reserve-institutional.html'); const authorizeOmnlHtml = (req: Request, res: Response): boolean => { const tok = process.env.OMNL_DASHBOARD_TOKEN?.trim(); if (!tok) return true; const q = String(req.query.access_token ?? '').trim(); const h = String(req.headers['x-omnl-dashboard-token'] ?? '').trim(); if (q !== tok && h !== tok) { res.status(401).type('text/plain').send('Unauthorized: set access_token query or X-OMNL-Dashboard-Token header'); return false; } return true; }; this.app.get('/omnl/compliance', (req: Request, res: Response) => { if (!authorizeOmnlHtml(req, res)) return; if (!existsSync(complianceConsolePath)) { res.status(404).type('text/plain').send('omnl-compliance-console.html missing'); return; } res.type('html').send(readFileSync(complianceConsolePath, 'utf8')); }); this.app.get('/omnl/dashboard', (req: Request, res: Response) => { if (!authorizeOmnlHtml(req, res)) return; if (!existsSync(dashboardPath)) { res.status(404).type('text/plain').send('omnl-dashboard.html missing'); return; } res.type('html').send(readFileSync(dashboardPath, 'utf8')); }); this.app.get('/omnl/terminal', (req: Request, res: Response) => { if (!authorizeOmnlHtml(req, res)) return; if (!existsSync(settlementTerminalPath)) { res.status(404).type('text/plain').send('omnl-settlement-terminal.html missing'); return; } res.type('html').send(readFileSync(settlementTerminalPath, 'utf8')); }); /** Public semi-public reserve capacity dashboard (no auth). */ this.app.get('/reserve', (_req: Request, res: Response) => { if (!existsSync(reserveDashboardPath)) { res.status(404).type('text/plain').send('reserve-dashboard.html missing'); return; } res.type('html').send(readFileSync(reserveDashboardPath, 'utf8')); }); /** Institutional wrapper (same API; used by reserve.d-bis.org NPM upstream). */ this.app.get('/reserve/institutional', (_req: Request, res: Response) => { const file = existsSync(reserveInstitutionalPath) ? reserveInstitutionalPath : reserveDashboardPath; if (!existsSync(file)) { res.status(404).type('text/plain').send('reserve dashboard missing'); return; } res.type('html').send(readFileSync(file, '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', reportPolygonMapper: '/api/v1/report/polygon-mapper', reportPolygonMapperOfficialExport: '/api/v1/report/polygon-mapper/official-export', reportOfficialProtocols: '/api/v1/report/official-protocols', reportOpenApi: '/api/v1/report/openapi.json', reportTokenList: '/api/v1/report/token-list', reportTokenListEthereumMainnet: '/api/v1/report/token-list/static/ethereum-mainnet', 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', reserveCapacity: '/api/v1/reserve/capacity', reserveProofIndex: '/api/v1/reserve/proof-index', reserveDashboard: '/reserve', }, }); }; 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); this.app.use('/api/v1/report', reportRoutes); this.app.use('/api/v1/report', polygonMapperReportRoutes); this.app.use('/api/v1/report', officialProtocolsReportRoutes); this.app.use('/api/v1/report', explorerRegistryOpenApiRoutes); this.app.use('/api/v1/bridge', bridgeRoutes); this.app.use('/api/v1', routeTreeRoutes); this.app.use('/api/v1/token-mapping', tokenMappingRoutes); this.app.use('/api/v1', quoteRoutes); this.app.use('/api/v1', heatmapRoutes); this.app.use('/api/v1', arbitrageRoutes); this.app.use('/api/v1', aggregatorRouteMatrixRoutes); this.app.use('/api/v1', partnerPayloadRoutes); this.app.use('/api/v1', checkpointRoutes); this.app.use('/api/v1', reserveCapacityRoutes); this.app.use('/api/v1', omnlComplianceRoutes); this.app.use('/api/v1', omnlTerminalRoutes); this.app.use('/api/v1', omnlRoutes); this.app.use('/api/v1', omnlIpsasRoutes); this.app.use('/api/v2', plannerV2Routes); // Admin routes (stricter rate limit) this.app.use('/api/v1/admin', strictRateLimiter, adminRoutes); // Root this.app.get('/', (req: Request, res: Response) => { res.json({ name: 'Token Aggregation Service', version: '1.0.0', endpoints: { health: '/health', api: '/api/v1', apiV2: '/api/v2', omnlOpenApi: '/api/v1/omnl/openapi.json', omnlCatalog: '/api/v1/omnl/catalog', omnlDashboard: '/omnl/dashboard', reserveDashboard: '/reserve', }, }); }); } private setupErrorHandling(): void { // 404 handler this.app.use((req: Request, res: Response) => { res.status(404).json({ error: 'Not found' }); }); // Error handler // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Express error handler requires 4-arg signature this.app.use((err: Error, req: Request, res: Response, _next: NextFunction) => { logger.error('Unhandled error:', err); res.status(500).json({ error: 'Internal server error', message: process.env.NODE_ENV === 'development' ? err.message : undefined, }); }); } async start(): Promise { 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) { 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(); } catch (error) { logger.error('Failed to start server:', error); process.exit(1); } } async stop(): Promise { this.omnlPoller?.stop(); this.indexer?.stopAll(); if (this.server) { await new Promise((resolve, reject) => { this.server?.close((error) => { if (error) { reject(error); return; } resolve(); }); }); this.server = null; } logger.info('Server stopped'); } }