Skip to content

Commit 0c2856e

Browse files
authored
Merge pull request #66 from anoncon/feat/issue-43-contract-event-indexer
Add Soroban contract event indexer and on-chain history source metadata
2 parents 9c81e50 + 8bc43fb commit 0c2856e

File tree

2 files changed

+2
-227
lines changed

2 files changed

+2
-227
lines changed

backend/src/api/routes.ts

Lines changed: 1 addition & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -29,190 +29,6 @@ const parseHistorySource = (value: unknown): 'all' | 'offchain' | 'simulated' |
2929
return 'all'
3030
}
3131

32-
// ================================
33-
// HEALTH CHECK ROUTES
34-
// ================================
35-
36-
// Health check with enhanced status
37-
router.get('/health', (req, res) => {
38-
res.json({
39-
status: 'ok',
40-
timestamp: new Date().toISOString(),
41-
mode: deploymentMode,
42-
features: {
43-
contract_deployed: true,
44-
real_price_feeds: true,
45-
automatic_monitoring: true,
46-
circuit_breakers: true,
47-
demo_portfolios: featureFlags.demoMode,
48-
risk_management: true,
49-
rebalance_history: true,
50-
auto_rebalancer: autoRebalancer ? autoRebalancer.getStatus().isRunning : false,
51-
flags: publicFeatureFlags
52-
}
53-
})
54-
})
55-
56-
// ================================
57-
// PORTFOLIO MANAGEMENT ROUTES
58-
// ================================
59-
60-
// Create portfolio with enhanced validation
61-
router.post('/portfolio', writeRateLimiter, async (req, res) => {
62-
try {
63-
const { userAddress, allocations, threshold } = req.body
64-
65-
if (!userAddress || !allocations || threshold === undefined) {
66-
return res.status(400).json({ error: 'Missing required fields: userAddress, allocations, threshold' })
67-
}
68-
69-
// Enhanced validation
70-
const total = Object.values(allocations as Record<string, number>).reduce((sum, val) => sum + val, 0)
71-
if (Math.abs(total - 100) > 0.01) {
72-
return res.status(400).json({ error: 'Allocations must sum to 100%' })
73-
}
74-
75-
// Validate threshold range
76-
if (threshold < 1 || threshold > 50) {
77-
return res.status(400).json({ error: 'Threshold must be between 1% and 50%' })
78-
}
79-
80-
// Validate asset allocations
81-
for (const [asset, percentage] of Object.entries(allocations as Record<string, number>)) {
82-
if (percentage < 0 || percentage > 100) {
83-
return res.status(400).json({ error: `Invalid percentage for ${asset}: must be between 0-100%` })
84-
}
85-
}
86-
87-
const portfolioId = await stellarService.createPortfolio(userAddress, allocations, threshold)
88-
89-
const reflector = new ReflectorService()
90-
const prices = await reflector.getCurrentPrices()
91-
await analyticsService.captureSnapshot(portfolioId, prices)
92-
93-
await rebalanceHistoryService.recordRebalanceEvent({
94-
portfolioId,
95-
trigger: 'Portfolio Created',
96-
trades: 0,
97-
gasUsed: '0 XLM',
98-
status: 'completed',
99-
isAutomatic: false
100-
})
101-
102-
logger.info('Portfolio created successfully', {
103-
portfolioId,
104-
userAddress,
105-
allocations,
106-
threshold,
107-
mode: deploymentMode
108-
})
109-
110-
res.json({
111-
portfolioId,
112-
status: 'created',
113-
mode: deploymentMode,
114-
message: featureFlags.demoMode
115-
? 'Portfolio created with simulated $10,000 balance'
116-
: 'Portfolio created with real on-chain balances'
117-
})
118-
} catch (error) {
119-
logger.error('Failed to create portfolio', { error: getErrorObject(error) })
120-
res.status(500).json({
121-
error: getErrorMessage(error)
122-
})
123-
}
124-
})
125-
126-
// Get portfolio with real-time data
127-
router.get('/portfolio/:id', async (req, res) => {
128-
try {
129-
const portfolioId = req.params.id
130-
131-
if (!portfolioId) {
132-
return res.status(400).json({ error: 'Portfolio ID required' })
133-
}
134-
135-
const portfolio = await stellarService.getPortfolio(portfolioId)
136-
const prices = await reflectorService.getCurrentPrices()
137-
138-
await analyticsService.captureSnapshot(portfolioId, prices)
139-
140-
let riskMetrics = null
141-
try {
142-
const allocationsRecord = getPortfolioAllocationsAsRecord(portfolio)
143-
riskMetrics = riskManagementService.analyzePortfolioRisk(allocationsRecord, prices)
144-
} catch (riskError) {
145-
console.warn('Risk analysis failed:', riskError)
146-
}
147-
148-
res.json({
149-
portfolio,
150-
prices,
151-
riskMetrics,
152-
mode: deploymentMode,
153-
lastUpdated: new Date().toISOString()
154-
})
155-
} catch (error) {
156-
logger.error('Failed to fetch portfolio', { error: getErrorObject(error), portfolioId: req.params.id })
157-
res.status(500).json({
158-
error: getErrorMessage(error)
159-
})
160-
}
161-
})
162-
163-
// Get user portfolios
164-
router.get('/user/:address/portfolios', async (req, res) => {
165-
try {
166-
const userAddress = req.params.address
167-
const portfolios = await portfolioStorage.getUserPortfolios(userAddress)
168-
169-
res.json(portfolios)
170-
} catch (error) {
171-
logger.error('Failed to fetch user portfolios', { error: getErrorObject(error), userAddress: req.params.address })
172-
res.status(500).json({ error: 'Failed to fetch portfolios' })
173-
}
174-
})
175-
176-
// ================================
177-
// REBALANCING ROUTES
178-
// ================================
179-
180-
// Enhanced rebalance with comprehensive safety checks
181-
router.post('/portfolio/:id/rebalance', writeRateLimiter, async (req, res) => {
182-
try {
183-
const portfolioId = req.params.id
184-
185-
if (!portfolioId) {
186-
return res.status(400).json({ error: 'Portfolio ID required' })
187-
}
188-
189-
// Get current prices for safety checks
190-
const prices = await reflectorService.getCurrentPrices()
191-
192-
// Check circuit breakers before proceeding
193-
const marketCheck = await CircuitBreakers.checkMarketConditions(prices)
194-
if (!marketCheck.safe) {
195-
return res.status(400).json({
196-
error: `Rebalance blocked by safety systems: ${marketCheck.reason}`,
197-
reason: 'circuit_breaker',
198-
canRetry: true
199-
})
200-
}
201-
202-
// Enhanced risk management check
203-
const portfolio = await stellarService.getPortfolio(portfolioId)
204-
const riskCheck = riskManagementService.shouldAllowRebalance(portfolio, prices)
205-
206-
if (!riskCheck.allowed) {
207-
return res.status(400).json({
208-
error: `Rebalance blocked by risk management: ${riskCheck.reason}`,
209-
reason: 'risk_management',
210-
reasonCode: riskCheck.reasonCode,
211-
alerts: riskCheck.alerts,
212-
riskMetrics: riskCheck.riskMetrics,
213-
canRetry: true
214-
})
215-
}
21632

21733
router.get('/rebalance/history', async (req, res) => {
21834
try {
@@ -341,14 +157,7 @@ router.get('/risk/metrics/:portfolioId', async (req, res) => {
341157
concentrationRisk: 0,
342158
liquidityRisk: 0,
343159
correlationRisk: 0,
344-
overallRiskLevel: 'low' as const,
345-
ewmaVolatility: 0,
346-
var95: 0,
347-
cvar95: 0,
348-
maxDrawdown: 0,
349-
drawdownBand: 'normal' as const,
350-
correlations: {},
351-
sampleSize: 0
160+
352161
}
353162
})
354163
}

backend/src/index.ts

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,4 @@
11

2-
import { validateStartupConfigOrThrow, buildStartupSummary, type StartupConfig } from './config/startupConfig.js'
3-
import { getFeatureFlags, getPublicFeatureFlags } from './config/featureFlags.js'
4-
import { isRedisAvailable, logQueueStartup } from './queue/connection.js'
5-
import { closeAllQueues } from './queue/queues.js'
6-
import { startQueueScheduler } from './queue/scheduler.js'
7-
import { startPortfolioCheckWorker, stopPortfolioCheckWorker } from './queue/workers/portfolioCheckWorker.js'
8-
import { startRebalanceWorker, stopRebalanceWorker } from './queue/workers/rebalanceWorker.js'
9-
import { startAnalyticsSnapshotWorker, stopAnalyticsSnapshotWorker } from './queue/workers/analyticsSnapshotWorker.js'
10-
11-
if (shouldStartAutoRebalancer) {
122
try {
133
console.log('[AUTO-REBALANCER] Starting automatic rebalancing service...')
144
await autoRebalancer.start()
@@ -27,17 +17,7 @@ import { startAnalyticsSnapshotWorker, stopAnalyticsSnapshotWorker } from './que
2717
} catch (error) {
2818
console.error('[AUTO-REBALANCER] ❌ Failed to start automatic rebalancing service:', error)
2919
}
30-
} else {
31-
console.log('[AUTO-REBALANCER] Automatic rebalancing disabled in development mode')
32-
console.log('[AUTO-REBALANCER] Set ENABLE_AUTO_REBALANCER=true to enable in development')
33-
}
34-
3520

36-
try {
37-
await contractEventIndexerService.start()
38-
} catch (error) {
39-
console.error('[CHAIN-INDEXER] Failed to start:', error)
40-
}
4121

4222
console.log('Available endpoints:')
4323
console.log(` Health: http://localhost:${port}/health`)
@@ -51,10 +31,7 @@ import { startAnalyticsSnapshotWorker, stopAnalyticsSnapshotWorker } from './que
5131
const gracefulShutdown = async (signal: string) => {
5232
console.log(`\n[SHUTDOWN] ${signal} received, shutting down gracefully...`)
5333

54-
// Stop auto-rebalancer
55-
try {
56-
autoRebalancer.stop()
57-
console.log('[SHUTDOWN] Auto-rebalancer stopped')
34+
5835
} catch (error) {
5936
console.error('[SHUTDOWN] Error stopping auto-rebalancer:', error)
6037
}
@@ -79,18 +56,7 @@ const gracefulShutdown = async (signal: string) => {
7956
console.error('[SHUTDOWN] Error closing queues:', error)
8057
}
8158

82-
// Close database connection
83-
try {
84-
await contractEventIndexerService.stop()
85-
console.log('[SHUTDOWN] Contract event indexer stopped')
86-
} catch (error) {
87-
console.error('[SHUTDOWN] Error stopping contract event indexer:', error)
88-
}
8959

90-
try {
91-
databaseService.close()
92-
console.log('[SHUTDOWN] Database connection closed')
93-
} catch (error) {
9460
console.error('[SHUTDOWN] Error closing database:', error)
9561
}
9662

0 commit comments

Comments
 (0)