Files
smom-dbis-138/services/token-aggregation/src/api/server.ts

236 lines
7.7 KiB
TypeScript
Raw Normal View History

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<void> {
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<void> {
this.omnlPoller?.stop();
this.indexer?.stopAll();
logger.info('Server stopped');
}
}