Skip to content

Commit 9302295

Browse files
authored
Merge pull request #77 from OpenSourceCOntr/fix/portfolio-id-collision
fix: resolve portfolio ID collision risk using NextPortfolioId counte…
2 parents ffce4dd + c1c5eeb commit 9302295

17 files changed

+1607
-616
lines changed

backend/src/api/routes.ts

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
1-
2-
import { Router } from 'express'
1+
import { Router, Request, Response } from 'express'
32
import { StellarService } from '../services/stellar.js'
43
import { ReflectorService } from '../services/reflector.js'
5-
import { RebalanceHistoryService } from '../services/rebalanceHistory.js'
6-
import { RiskManagementService } from '../services/riskManagements.js'
4+
import { riskManagementService, rebalanceHistoryService } from '../services/serviceContainer.js'
75
import { portfolioStorage } from '../services/portfolioStorage.js'
86
import { CircuitBreakers } from '../services/circuitBreakers.js'
97
import { analyticsService } from '../services/analyticsService.js'
108
import { notificationService } from '../services/notificationService.js'
119
import { contractEventIndexerService } from '../services/contractEventIndexer.js'
10+
import { AutoRebalancerService } from '../services/autoRebalancer.js'
1211
import { logger } from '../utils/logger.js'
1312
import { idempotencyMiddleware } from '../middleware/idempotency.js'
13+
import { requireAdmin } from '../middleware/auth.js'
14+
import { writeRateLimiter } from '../middleware/rateLimit.js'
15+
import { blockDebugInProduction } from '../middleware/debugGate.js'
16+
import { getFeatureFlags, getPublicFeatureFlags } from '../config/featureFlags.js'
17+
import { getQueueMetrics } from '../queue/queueMetrics.js'
18+
import { getErrorMessage, getErrorObject, parseOptionalBoolean } from '../utils/helpers.js'
19+
20+
const router = Router()
21+
const stellarService = new StellarService()
22+
const reflectorService = new ReflectorService()
23+
const autoRebalancer = new AutoRebalancerService()
24+
const featureFlags = getFeatureFlags()
25+
const publicFeatureFlags = getPublicFeatureFlags()
1426

1527
const parseOptionalTimestamp = (value: unknown): string | undefined => {
1628
if (value === undefined || value === null || value === '') return undefined
@@ -20,17 +32,17 @@ const parseOptionalTimestamp = (value: unknown): string | undefined => {
2032
return ts.toISOString()
2133
}
2234

23-
const parseHistorySource = (value: unknown): 'all' | 'offchain' | 'simulated' | 'onchain' => {
24-
if (typeof value !== 'string') return 'all'
35+
const parseHistorySource = (value: unknown): 'offchain' | 'simulated' | 'onchain' | undefined => {
36+
if (typeof value !== 'string') return undefined
2537
const normalized = value.trim().toLowerCase()
2638
if (normalized === 'offchain') return 'offchain'
2739
if (normalized === 'simulated') return 'simulated'
2840
if (normalized === 'onchain') return 'onchain'
29-
return 'all'
41+
return undefined
3042
}
3143

3244

33-
router.get('/rebalance/history', async (req, res) => {
45+
router.get('/rebalance/history', async (req: Request, res: Response) => {
3446
try {
3547
const portfolioId = req.query.portfolioId as string
3648
const limit = parseInt(req.query.limit as string) || 50
@@ -77,7 +89,7 @@ router.get('/rebalance/history', async (req, res) => {
7789
})
7890

7991
// Record new rebalance event
80-
router.post('/rebalance/history', idempotencyMiddleware, async (req, res) => {
92+
router.post('/rebalance/history', idempotencyMiddleware, async (req: Request, res: Response) => {
8193
try {
8294
const eventData = req.body
8395

@@ -102,7 +114,7 @@ router.post('/rebalance/history', idempotencyMiddleware, async (req, res) => {
102114
}
103115
})
104116

105-
router.post('/rebalance/history/sync-onchain', requireAdmin, async (req, res) => {
117+
router.post('/rebalance/history/sync-onchain', requireAdmin, async (req: Request, res: Response) => {
106118
try {
107119
const result = await contractEventIndexerService.syncOnce()
108120
res.json({
@@ -124,7 +136,7 @@ router.post('/rebalance/history/sync-onchain', requireAdmin, async (req, res) =>
124136
// ================================
125137

126138
// Get risk metrics for a portfolio
127-
router.get('/risk/metrics/:portfolioId', async (req, res) => {
139+
router.get('/risk/metrics/:portfolioId', async (req: Request, res: Response) => {
128140
try {
129141
const { portfolioId } = req.params
130142

@@ -134,7 +146,14 @@ router.get('/risk/metrics/:portfolioId', async (req, res) => {
134146
const prices = await reflectorService.getCurrentPrices()
135147

136148
// Calculate risk metrics with proper type conversion
137-
const allocationsRecord = getPortfolioAllocationsAsRecord(portfolio)
149+
const allocationsRecord: Record<string, number> = {}
150+
if (Array.isArray(portfolio.allocations)) {
151+
portfolio.allocations.forEach((a: any) => {
152+
allocationsRecord[a.asset] = a.target
153+
})
154+
} else {
155+
Object.assign(allocationsRecord, portfolio.allocations)
156+
}
138157
const riskMetrics = riskManagementService.analyzePortfolioRisk(allocationsRecord, prices)
139158
const recommendations = riskManagementService.getRecommendations(riskMetrics, allocationsRecord)
140159
const circuitBreakers = riskManagementService.getCircuitBreakerStatus()
@@ -164,7 +183,7 @@ router.get('/risk/metrics/:portfolioId', async (req, res) => {
164183
})
165184

166185
// Check if rebalancing should be allowed based on risk conditions
167-
router.get('/risk/check/:portfolioId', async (req, res) => {
186+
router.get('/risk/check/:portfolioId', async (req: Request, res: Response) => {
168187
try {
169188
const { portfolioId } = req.params
170189

@@ -198,7 +217,7 @@ router.get('/risk/check/:portfolioId', async (req, res) => {
198217
// ================================
199218

200219
// Get current prices - FIXED to return direct format for frontend
201-
router.get('/prices', async (req, res) => {
220+
router.get('/prices', async (req: Request, res: Response) => {
202221
try {
203222
console.log('[DEBUG] Fetching prices for frontend...')
204223
const prices = await reflectorService.getCurrentPrices()
@@ -232,7 +251,7 @@ router.get('/prices', async (req, res) => {
232251
})
233252

234253
// Enhanced prices endpoint with risk analysis
235-
router.get('/prices/enhanced', async (req, res) => {
254+
router.get('/prices/enhanced', async (req: Request, res: Response) => {
236255
try {
237256
console.log('[INFO] Fetching enhanced prices with risk analysis')
238257

@@ -275,7 +294,7 @@ router.get('/prices/enhanced', async (req, res) => {
275294
})
276295

277296
// Get detailed market data for specific asset
278-
router.get('/market/:asset/details', async (req, res) => {
297+
router.get('/market/:asset/details', async (req: Request, res: Response) => {
279298
try {
280299
const asset = req.params.asset.toUpperCase()
281300
const reflector = new ReflectorService()
@@ -292,7 +311,7 @@ router.get('/market/:asset/details', async (req, res) => {
292311
})
293312

294313
// Get price charts for frontend
295-
router.get('/market/:asset/chart', async (req, res) => {
314+
router.get('/market/:asset/chart', async (req: Request, res: Response) => {
296315
try {
297316
const asset = req.params.asset.toUpperCase()
298317
const days = parseInt(req.query.days as string) || 7
@@ -316,7 +335,7 @@ router.get('/market/:asset/chart', async (req, res) => {
316335
// AUTO-REBALANCER ROUTES
317336
// ================================
318337

319-
router.get('/auto-rebalancer/status', async (req, res) => {
338+
router.get('/auto-rebalancer/status', async (req: Request, res: Response) => {
320339
try {
321340
if (!autoRebalancer) {
322341
return res.json({
@@ -343,7 +362,7 @@ router.get('/auto-rebalancer/status', async (req, res) => {
343362
}
344363
})
345364

346-
router.post('/auto-rebalancer/start', requireAdmin, (req, res) => {
365+
router.post('/auto-rebalancer/start', requireAdmin, (req: Request, res: Response) => {
347366
try {
348367
if (!autoRebalancer) {
349368
return res.status(500).json({
@@ -368,7 +387,7 @@ router.post('/auto-rebalancer/start', requireAdmin, (req, res) => {
368387
}
369388
})
370389

371-
router.post('/auto-rebalancer/stop', requireAdmin, (req, res) => {
390+
router.post('/auto-rebalancer/stop', requireAdmin, (req: Request, res: Response) => {
372391
try {
373392
if (!autoRebalancer) {
374393
return res.status(500).json({
@@ -393,7 +412,7 @@ router.post('/auto-rebalancer/stop', requireAdmin, (req, res) => {
393412
}
394413
})
395414

396-
router.post('/auto-rebalancer/force-check', requireAdmin, async (req, res) => {
415+
router.post('/auto-rebalancer/force-check', requireAdmin, async (req: Request, res: Response) => {
397416
try {
398417
if (!autoRebalancer) {
399418
return res.status(500).json({
@@ -417,7 +436,7 @@ router.post('/auto-rebalancer/force-check', requireAdmin, async (req, res) => {
417436
}
418437
})
419438

420-
router.get('/auto-rebalancer/history', requireAdmin, async (req, res) => {
439+
router.get('/auto-rebalancer/history', requireAdmin, async (req: Request, res: Response) => {
421440
try {
422441
const portfolioId = req.query.portfolioId as string
423442
const limit = parseInt(req.query.limit as string) || 50
@@ -450,7 +469,7 @@ router.get('/auto-rebalancer/history', requireAdmin, async (req, res) => {
450469
// ================================
451470

452471
// Get comprehensive system status
453-
router.get('/system/status', async (req, res) => {
472+
router.get('/system/status', async (req: Request, res: Response) => {
454473
try {
455474
const portfolioCount = await portfolioStorage.getPortfolioCount()
456475
const historyStats = await rebalanceHistoryService.getHistoryStats()
@@ -518,7 +537,7 @@ router.get('/system/status', async (req, res) => {
518537
// ANALYTICS ROUTES
519538
// ================================
520539

521-
router.get('/portfolio/:id/analytics', async (req, res) => {
540+
router.get('/portfolio/:id/analytics', async (req: Request, res: Response) => {
522541
try {
523542
const portfolioId = req.params.id
524543
const days = parseInt(req.query.days as string) || 30
@@ -551,7 +570,7 @@ router.get('/portfolio/:id/analytics', async (req, res) => {
551570
}
552571
})
553572

554-
router.get('/portfolio/:id/performance-summary', async (req, res) => {
573+
router.get('/portfolio/:id/performance-summary', async (req: Request, res: Response) => {
555574
try {
556575
const portfolioId = req.params.id
557576

@@ -586,7 +605,7 @@ router.get('/portfolio/:id/performance-summary', async (req, res) => {
586605
// ================================
587606

588607
// Subscribe to notifications
589-
router.post('/notifications/subscribe', writeRateLimiter, idempotencyMiddleware, async (req, res) => {
608+
router.post('/notifications/subscribe', writeRateLimiter, idempotencyMiddleware, async (req: Request, res: Response) => {
590609
try {
591610
const { userId, emailEnabled, emailAddress, webhookEnabled, webhookUrl, events } = req.body
592611

@@ -666,7 +685,7 @@ router.post('/notifications/subscribe', writeRateLimiter, idempotencyMiddleware,
666685
})
667686

668687
// Get notification preferences
669-
router.get('/notifications/preferences', async (req, res) => {
688+
router.get('/notifications/preferences', async (req: Request, res: Response) => {
670689
try {
671690
const userId = req.query.userId as string
672691

@@ -702,7 +721,7 @@ router.get('/notifications/preferences', async (req, res) => {
702721
})
703722

704723
// Unsubscribe from notifications
705-
router.delete('/notifications/unsubscribe', async (req, res) => {
724+
router.delete('/notifications/unsubscribe', async (req: Request, res: Response) => {
706725
try {
707726
const userId = req.query.userId as string
708727

@@ -736,7 +755,7 @@ router.delete('/notifications/unsubscribe', async (req, res) => {
736755
// ================================
737756

738757
// Test notification delivery
739-
// router.post('/notifications/test', async (req, res) => {
758+
// router.post('/notifications/test', async (req: Request, res: Response) => {
740759
// try {
741760
// const { userId, eventType } = req.body
742761

@@ -840,7 +859,7 @@ router.delete('/notifications/unsubscribe', async (req, res) => {
840859
// })
841860

842861
// Test all notification types at once
843-
// router.post('/notifications/test-all', async (req, res) => {
862+
// router.post('/notifications/test-all', async (req: Request, res: Response) => {
844863
// try {
845864
// const { userId } = req.body
846865

@@ -931,7 +950,7 @@ router.delete('/notifications/unsubscribe', async (req, res) => {
931950
// DEBUG ROUTES
932951
// ================================
933952

934-
router.get('/debug/coingecko-test', blockDebugInProduction, async (req, res) => {
953+
router.get('/debug/coingecko-test', blockDebugInProduction, async (req: Request, res: Response) => {
935954
try {
936955
const apiKey = process.env.COINGECKO_API_KEY
937956
console.log('[DEBUG] API Key exists:', !!apiKey)
@@ -971,7 +990,7 @@ router.get('/debug/coingecko-test', blockDebugInProduction, async (req, res) =>
971990
}
972991
})
973992

974-
router.get('/debug/force-fresh-prices', blockDebugInProduction, async (req, res) => {
993+
router.get('/debug/force-fresh-prices', blockDebugInProduction, async (req: Request, res: Response) => {
975994
try {
976995
console.log('[DEBUG] Clearing cache and forcing fresh prices...')
977996

@@ -1000,7 +1019,7 @@ router.get('/debug/force-fresh-prices', blockDebugInProduction, async (req, res)
10001019
}
10011020
})
10021021

1003-
router.get('/debug/reflector-test', blockDebugInProduction, async (req, res) => {
1022+
router.get('/debug/reflector-test', blockDebugInProduction, async (req: Request, res: Response) => {
10041023
try {
10051024
console.log('[DEBUG] Testing reflector service...')
10061025

@@ -1027,7 +1046,7 @@ router.get('/debug/reflector-test', blockDebugInProduction, async (req, res) =>
10271046
}
10281047
})
10291048

1030-
router.get('/debug/env', blockDebugInProduction, async (req, res) => {
1049+
router.get('/debug/env', blockDebugInProduction, async (req: Request, res: Response) => {
10311050
try {
10321051
res.json({
10331052
environment: process.env.NODE_ENV,
@@ -1046,7 +1065,7 @@ router.get('/debug/env', blockDebugInProduction, async (req, res) => {
10461065
}
10471066
})
10481067

1049-
router.get('/debug/auto-rebalancer-test', blockDebugInProduction, async (req, res) => {
1068+
router.get('/debug/auto-rebalancer-test', blockDebugInProduction, async (req: Request, res: Response) => {
10501069
try {
10511070
if (!autoRebalancer) {
10521071
return res.json({
@@ -1086,7 +1105,7 @@ router.get('/debug/auto-rebalancer-test', blockDebugInProduction, async (req, re
10861105
* Returns BullMQ queue depths and Redis connectivity status.
10871106
* Used for worker health monitoring and alerting (issue #38).
10881107
*/
1089-
router.get('/queue/health', async (req, res) => {
1108+
router.get('/queue/health', async (req: Request, res: Response) => {
10901109
try {
10911110
const metrics = await getQueueMetrics()
10921111
const httpStatus = metrics.redisConnected ? 200 : 503

backend/src/utils/helpers.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export function getErrorMessage(error: unknown): string {
2+
if (error instanceof Error) return error.message;
3+
return String(error);
4+
}
5+
6+
export function getErrorObject(error: unknown): Record<string, unknown> {
7+
if (error instanceof Error) return { message: error.message, stack: error.stack, name: error.name };
8+
return { error: String(error) };
9+
}
10+
11+
export function parseOptionalBoolean(value: unknown): boolean | undefined {
12+
if (value === undefined || value === null) return undefined;
13+
if (typeof value === 'boolean') return value;
14+
if (typeof value === 'string') {
15+
const lower = value.trim().toLowerCase();
16+
if (lower === 'true' || lower === '1') return true;
17+
if (lower === 'false' || lower === '0') return false;
18+
}
19+
return undefined;
20+
}

0 commit comments

Comments
 (0)