@@ -19,6 +19,241 @@ import { startPortfolioCheckWorker, stopPortfolioCheckWorker } from './queue/wor
1919import { startRebalanceWorker , stopRebalanceWorker } from './queue/workers/rebalanceWorker.js'
2020import { startAnalyticsSnapshotWorker , stopAnalyticsSnapshotWorker } from './queue/workers/analyticsSnapshotWorker.js'
2121
22+ let startupConfig : StartupConfig
23+ try {
24+ startupConfig = validateStartupConfigOrThrow ( process . env )
25+ logger . info ( '[STARTUP-CONFIG] Validation successful' , buildStartupSummary ( startupConfig ) )
26+ } catch ( error ) {
27+ const message = error instanceof Error ? error . message : String ( error )
28+ console . error ( message )
29+ process . exit ( 1 )
30+ }
31+
32+ const app = express ( )
33+ const port = startupConfig . port
34+ const featureFlags = getFeatureFlags ( )
35+ const publicFeatureFlags = getPublicFeatureFlags ( )
36+
37+ const isProduction = startupConfig . nodeEnv === 'production'
38+ const allowedOrigins = startupConfig . corsOrigins
39+
40+ const corsOptions : cors . CorsOptions = {
41+ origin : isProduction
42+ ? allowedOrigins . length > 0
43+ ? ( origin , cb ) => {
44+ if ( ! origin || allowedOrigins . includes ( origin ) ) cb ( null , origin || true )
45+ else cb ( new Error ( 'Not allowed by CORS' ) )
46+ }
47+ : false
48+ : true ,
49+ credentials : true ,
50+ methods : [ 'GET' , 'POST' , 'PUT' , 'DELETE' , 'OPTIONS' , 'PATCH' , 'HEAD' ] ,
51+ allowedHeaders : [ 'Content-Type' , 'Authorization' , 'Accept' , 'Origin' , 'X-Requested-With' , 'X-Public-Key' , 'X-Message' , 'X-Signature' ]
52+ }
53+ app . use ( cors ( corsOptions ) )
54+
55+ app . options ( '*' , ( req , res ) => {
56+ const origin = req . get ( 'Origin' )
57+ if ( isProduction && allowedOrigins . length > 0 ) {
58+ if ( origin && allowedOrigins . includes ( origin ) ) res . setHeader ( 'Access-Control-Allow-Origin' , origin )
59+ } else {
60+ res . setHeader ( 'Access-Control-Allow-Origin' , origin || '*' )
61+ }
62+ res . setHeader ( 'Access-Control-Allow-Methods' , 'GET, POST, PUT, DELETE, OPTIONS, PATCH' )
63+ res . setHeader ( 'Access-Control-Allow-Headers' , 'Content-Type, Authorization, Accept, Origin, X-Requested-With, X-Public-Key, X-Message, X-Signature' )
64+ res . status ( 204 ) . end ( )
65+ } )
66+
67+ // Trust proxy
68+ app . set ( 'trust proxy' , 1 )
69+
70+ // Body parsing
71+ app . use ( express . json ( { limit : '10mb' } ) )
72+ app . use ( express . urlencoded ( { extended : true , limit : '10mb' } ) )
73+
74+ // Basic logging
75+ app . use ( ( req , res , next ) => {
76+ console . log ( `${ req . method } ${ req . url } ` )
77+ next ( )
78+ } )
79+
80+ app . use ( globalRateLimiter )
81+
82+ // Create auto-rebalancer instance
83+ const autoRebalancer = new AutoRebalancerService ( )
84+
85+ // Health check endpoint
86+ app . get ( '/health' , ( req , res ) => {
87+ res . json ( {
88+ status : 'healthy' ,
89+ timestamp : new Date ( ) . toISOString ( ) ,
90+ environment : process . env . NODE_ENV || 'development' ,
91+ autoRebalancer : autoRebalancer ? autoRebalancer . getStatus ( ) : { isRunning : false }
92+ } )
93+ } )
94+
95+ // CORS test endpoint
96+ app . get ( '/test/cors' , ( req , res ) => {
97+ if ( ! featureFlags . enableDebugRoutes ) {
98+ return res . status ( 404 ) . json ( { error : 'Route not found' } )
99+ }
100+ res . json ( {
101+ success : true ,
102+ message : 'CORS working!' ,
103+ origin : req . get ( 'Origin' ) ,
104+ timestamp : new Date ( ) . toISOString ( )
105+ } )
106+ } )
107+
108+ // CoinGecko test endpoint with detailed debugging
109+ app . get ( '/test/coingecko' , async ( req , res ) => {
110+ if ( ! featureFlags . enableDebugRoutes ) {
111+ return res . status ( 404 ) . json ( { error : 'Route not found' } )
112+ }
113+ try {
114+ console . log ( '[TEST] Testing CoinGecko API...' )
115+ const { ReflectorService } = await import ( './services/reflector.js' )
116+ const reflector = new ReflectorService ( )
117+
118+ // Test connectivity first
119+ const testResult = await reflector . testApiConnectivity ( )
120+
121+ if ( ! testResult . success ) {
122+ return res . status ( 500 ) . json ( {
123+ success : false ,
124+ error : testResult . error
125+ } )
126+ }
127+
128+ // Try to get actual prices
129+ reflector . clearCache ( )
130+ const prices = await reflector . getCurrentPrices ( )
131+
132+ res . json ( {
133+ success : true ,
134+ prices,
135+ testResult,
136+ environment : process . env . NODE_ENV
137+ } )
138+ } catch ( error ) {
139+ res . status ( 500 ) . json ( {
140+ success : false ,
141+ error : error instanceof Error ? error . message : String ( error )
142+ } )
143+ }
144+ } )
145+
146+ // Root route
147+ app . get ( '/' , ( req , res ) => {
148+ res . json ( {
149+ message : 'Stellar Portfolio Rebalancer API' ,
150+ status : 'running' ,
151+ version : '1.0.0' ,
152+ timestamp : new Date ( ) . toISOString ( ) ,
153+ features : {
154+ automaticRebalancing : ! ! autoRebalancer ?. getStatus ( ) . isRunning ,
155+ priceFeeds : true ,
156+ riskManagement : true ,
157+ portfolioManagement : true ,
158+ featureFlags : publicFeatureFlags
159+ } ,
160+ endpoints : {
161+ health : '/health' ,
162+ corsTest : '/test/cors' ,
163+ coinGeckoTest : '/test/coingecko' ,
164+ autoRebalancerStatus : '/api/auto-rebalancer/status' ,
165+ queueHealth : '/api/queue/health'
166+ }
167+ } )
168+ } )
169+
170+ // Mount API routes
171+ app . use ( '/api' , portfolioRouter )
172+ app . use ( '/' , portfolioRouter )
173+
174+ // 404 handler
175+ app . use ( ( req , res ) => {
176+ console . log ( `404 - Route not found: ${ req . method } ${ req . url } ` )
177+ res . status ( 404 ) . json ( {
178+ error : 'Route not found' ,
179+ method : req . method ,
180+ url : req . url ,
181+ availableEndpoints : {
182+ health : '/health' ,
183+ api : '/api/*' ,
184+ autoRebalancer : '/api/auto-rebalancer/*' ,
185+ queueHealth : '/api/queue/health'
186+ }
187+ } )
188+ } )
189+
190+ // Error handler
191+ app . use ( ( error : any , req : express . Request , res : express . Response , next : express . NextFunction ) => {
192+ console . error ( 'Server error:' , error )
193+ res . status ( 500 ) . json ( {
194+ error : 'Internal server error' ,
195+ message : error . message || 'Unknown error'
196+ } )
197+ } )
198+
199+ // Create server
200+ const server = createServer ( app )
201+
202+ // WebSocket setup
203+ const wss = new WebSocketServer ( { server } )
204+
205+ wss . on ( 'connection' , ( ws ) => {
206+ console . log ( 'WebSocket connection established' )
207+ ws . send ( JSON . stringify ( {
208+ type : 'connection' ,
209+ message : 'Connected' ,
210+ autoRebalancerStatus : autoRebalancer . getStatus ( )
211+ } ) )
212+
213+ ws . on ( 'error' , ( error ) => {
214+ console . error ( 'WebSocket error:' , error )
215+ } )
216+ } )
217+
218+ // Start existing rebalancing service (now queue-backed, no cron)
219+ try {
220+ const rebalancingService = new RebalancingService ( wss )
221+ rebalancingService . start ( )
222+ console . log ( '[REBALANCING-SERVICE] Monitoring service started (queue-backed)' )
223+ } catch ( error ) {
224+ console . error ( 'Failed to start rebalancing service:' , error )
225+ }
226+
227+ // Start server
228+ server . listen ( port , async ( ) => {
229+ console . log ( `🚀 Server running on port ${ port } ` )
230+ console . log ( `Environment: ${ process . env . NODE_ENV || 'development' } ` )
231+ console . log ( `CoinGecko API Key: ${ ! ! process . env . COINGECKO_API_KEY ? 'SET' : 'NOT SET' } ` )
232+
233+ // ── BullMQ / Redis setup ────────────────────────────────────────────────
234+ const redisAvailable = await isRedisAvailable ( )
235+ logQueueStartup ( redisAvailable )
236+
237+ if ( redisAvailable ) {
238+ // Start all three workers
239+ startPortfolioCheckWorker ( )
240+ startRebalanceWorker ( )
241+ startAnalyticsSnapshotWorker ( )
242+
243+ // Register repeatable jobs (scheduler)
244+ try {
245+ await startQueueScheduler ( )
246+ console . log ( '[SCHEDULER] ✅ Queue scheduler registered' )
247+ } catch ( err ) {
248+ console . error ( '[SCHEDULER] ❌ Failed to register scheduler:' , err )
249+ }
250+ }
251+
252+ // ── Auto-rebalancer (queue-backed) ──────────────────────────────────────
253+ const shouldStartAutoRebalancer =
254+ process . env . NODE_ENV === 'production' ||
255+ process . env . ENABLE_AUTO_REBALANCER === 'true'
256+
22257 if ( shouldStartAutoRebalancer ) {
23258 try {
24259 console . log ( '[AUTO-REBALANCER] Starting automatic rebalancing service...' )
@@ -43,13 +278,6 @@ import { startAnalyticsSnapshotWorker, stopAnalyticsSnapshotWorker } from './que
43278 console . log ( '[AUTO-REBALANCER] Set ENABLE_AUTO_REBALANCER=true to enable in development' )
44279 }
45280
46- // Contract event indexer (on-chain source-of-truth history)
47- try {
48- await contractEventIndexerService . start ( )
49- } catch ( error ) {
50- console . error ( '[CHAIN-INDEXER] Failed to start:' , error )
51- }
52-
53281 console . log ( 'Available endpoints:' )
54282 console . log ( ` Health: http://localhost:${ port } /health` )
55283 console . log ( ` CORS Test: http://localhost:${ port } /test/cors` )
@@ -91,13 +319,6 @@ const gracefulShutdown = async (signal: string) => {
91319 }
92320
93321 // Close database connection
94- try {
95- await contractEventIndexerService . stop ( )
96- console . log ( '[SHUTDOWN] Contract event indexer stopped' )
97- } catch ( error ) {
98- console . error ( '[SHUTDOWN] Error stopping contract event indexer:' , error )
99- }
100-
101322 try {
102323 databaseService . close ( )
103324 console . log ( '[SHUTDOWN] Database connection closed' )
0 commit comments