import express, { Express, Request, Response, NextFunction } from 'express'; 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 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 { 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 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.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 this.app.use(compression()); // 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(); }); } private setupRoutes(): void { // Health check this.app.get('/health', async (req: Request, res: Response) => { 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'); this.app.get('/omnl/dashboard', (req: Request, res: Response) => { const tok = process.env.OMNL_DASHBOARD_TOKEN?.trim(); if (tok) { 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; } } if (!existsSync(dashboardPath)) { res.status(404).type('text/plain').send('omnl-dashboard.html missing'); return; } res.type('html').send(readFileSync(dashboardPath, 'utf8')); }); // 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/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', 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', }, }); }); } 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 { if (this.indexer) { await this.indexer.initialize(); await this.indexer.startAll(); } 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); } } async stop(): Promise { this.omnlPoller?.stop(); this.indexer?.stopAll(); logger.info('Server stopped'); } }