Files
smom-dbis-138/services/token-aggregation/src/api/server.ts
defiQUG c55fab257c feat(token-aggregation): add Chain 138 wallet routing surfaces
Add MetaMask Snap metadata, wallet image helpers, and n-hop planning surfaces so Chain 138 routing and wallet-add flows have consistent API coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 09:57:09 -07:00

410 lines
15 KiB
TypeScript

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, { sendMetamaskConfig } 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 omnlPayoutJournalRoutes from './routes/omnl-payout-journal-routes';
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 hoLiquidityRoutes from './routes/ho-liquidity-routes';
import metamaskPriceRoutes from './routes/metamask-prices';
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) {
if (this.resolveFeatureFlag('TOKEN_AGGREGATION_DB_OPTIONAL', false)) {
res.json({
status: 'degraded',
mode: 'database-optional',
timestamp: new Date().toISOString(),
services: {
database: 'unavailable',
indexer: this.indexerEnabled ? 'running' : 'disabled',
},
error: error instanceof Error ? error.message : 'Unknown error',
});
return;
}
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',
bridgeMessages: '/api/v1/bridge/messages?lane=138-monad',
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',
hostedDbisTokenList: 'https://tokens.d-bis.org/lists/dbis-138.tokenlist.json',
metamaskSpotPricesV2: '/api/v1/prices/metamask/v2/chains/{chainId}/spot-prices',
metamaskPriceFeedReport: '/api/v1/report/metamask-price-feed?chainId=138',
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);
// Keep wallet add-chain config on a direct route so it cannot be shadowed by other mounts.
this.app.get('/api/v1/config/metamask', sendMetamaskConfig);
this.app.get('/api/v1/metamask', sendMetamaskConfig);
// 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', hoLiquidityRoutes);
this.app.use('/api/v1/prices/metamask', metamaskPriceRoutes);
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/v1', omnlPayoutJournalRoutes);
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<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) {
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<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');
}
}