2026-03-02 12:14:09 -08:00
|
|
|
import express, { Express, Request, Response, NextFunction } from 'express';
|
|
|
|
|
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';
|
2026-03-27 12:02:36 -07:00
|
|
|
import routeTreeRoutes from './routes/routes';
|
2026-03-02 12:14:09 -08:00
|
|
|
import tokenMappingRoutes from './routes/token-mapping';
|
2026-03-04 02:00:09 -08:00
|
|
|
import heatmapRoutes from './routes/heatmap';
|
|
|
|
|
import arbitrageRoutes from './routes/arbitrage';
|
2026-03-27 12:02:36 -07:00
|
|
|
import aggregatorRouteMatrixRoutes from './routes/aggregator-routes';
|
|
|
|
|
import partnerPayloadRoutes from './routes/partner-payloads';
|
2026-03-02 12:14:09 -08:00
|
|
|
import { MultiChainIndexer } from '../indexer/chain-indexer';
|
|
|
|
|
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 indexer: MultiChainIndexer;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
this.app = express();
|
|
|
|
|
this.port = parseInt(process.env.PORT || '3000', 10);
|
|
|
|
|
this.indexer = new MultiChainIndexer();
|
|
|
|
|
|
|
|
|
|
this.setupMiddleware();
|
|
|
|
|
this.setupRoutes();
|
|
|
|
|
this.setupErrorHandling();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupMiddleware(): void {
|
|
|
|
|
// 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,
|
|
|
|
|
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: 'running',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(503).json({
|
|
|
|
|
status: 'unhealthy',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-03-27 12:02:36 -07:00
|
|
|
this.app.use('/api/v1', routeTreeRoutes);
|
2026-03-02 12:14:09 -08:00
|
|
|
this.app.use('/api/v1/token-mapping', tokenMappingRoutes);
|
|
|
|
|
this.app.use('/api/v1', quoteRoutes);
|
2026-03-04 02:00:09 -08:00
|
|
|
this.app.use('/api/v1', heatmapRoutes);
|
|
|
|
|
this.app.use('/api/v1', arbitrageRoutes);
|
2026-03-27 12:02:36 -07:00
|
|
|
this.app.use('/api/v1', aggregatorRouteMatrixRoutes);
|
|
|
|
|
this.app.use('/api/v1', partnerPayloadRoutes);
|
2026-03-02 12:14:09 -08:00
|
|
|
|
|
|
|
|
// 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',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupErrorHandling(): void {
|
|
|
|
|
// 404 handler
|
|
|
|
|
this.app.use((req: Request, res: Response) => {
|
|
|
|
|
res.status(404).json({ error: 'Not found' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Error handler
|
2026-03-04 02:00:09 -08:00
|
|
|
// 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) => {
|
2026-03-02 12:14:09 -08:00
|
|
|
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 {
|
|
|
|
|
// Initialize indexer
|
|
|
|
|
await this.indexer.initialize();
|
|
|
|
|
|
|
|
|
|
// Start indexing
|
|
|
|
|
await this.indexer.startAll();
|
|
|
|
|
|
|
|
|
|
// 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.indexer.stopAll();
|
|
|
|
|
logger.info('Server stopped');
|
|
|
|
|
}
|
|
|
|
|
}
|