Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ import rateLimitRoutes from './routes/rateLimit.routes';
import debtTokenRoutes from './routes/debtToken.routes';
import bridgeRoutes from './routes/bridge.routes';
import complianceRoutes from './routes/v1/compliance';
import interestRoutes from './routes/interest';
import eventsRoutes from './routes/events';

import compression from 'compression';
import { errorHandler } from './middleware/errorHandler';
Expand Down Expand Up @@ -277,6 +279,8 @@ app.use('/api/compliance', complianceRoutes);
app.use('/api/rate-limit', rateLimitRoutes);
app.use('/api/debt-token', debtTokenRoutes);
app.use('/api/bridge', bridgeRoutes);
app.use('/api/interest', interestRoutes);
app.use('/api/events', eventsRoutes);

app.use(errorHandler);

Expand Down
40 changes: 40 additions & 0 deletions api/src/controllers/events.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Request, Response } from 'express';
import { getIndexedEvents, getEventTypes, getEventStats } from '../services/events.service';

export class EventsController {
async getEvents(req: Request, res: Response): Promise<void> {
try {
const { type, address, from, to, limit = 50 } = req.query;
const events = await getIndexedEvents({
type: type as string | undefined,
address: address as string | undefined,
from: from ? Number(from) : undefined,
to: to ? Number(to) : undefined,
limit: Number(limit),
});
res.json(events);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch events' });
}
}

async getEventTypes(_req: Request, res: Response): Promise<void> {
try {
const types = getEventTypes();
res.json(types);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch event types' });
}
}

async getEventStats(_req: Request, res: Response): Promise<void> {
try {
const stats = getEventStats();
res.json(stats);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch event stats' });
}
}
}

export const eventsController = new EventsController();
36 changes: 36 additions & 0 deletions api/src/controllers/interest.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Request, Response } from 'express';
import { getCurrentBorrowRate, getCurrentSupplyRate, getCurrentUtilization, getRateHistory, simulateRateAtUtilization } from '../services/interest.service';

export class InterestController {
async getCurrentRates(_req: Request, res: Response): Promise<void> {
try {
const borrowRate = await getCurrentBorrowRate();
const supplyRate = await getCurrentSupplyRate();
const utilization = await getCurrentUtilization();
res.json({ borrowRateBps: borrowRate, supplyRateBps: supplyRate, utilizationBps: utilization });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch interest rates' });
}
}

async getRateHistory(_req: Request, res: Response): Promise<void> {
try {
const history = await getRateHistory();
res.json(history);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch rate history' });
}
}

async simulateRate(req: Request, res: Response): Promise<void> {
try {
const utilizationBps = Number(req.body?.utilizationBps ?? 0);
const rate = await simulateRateAtUtilization(utilizationBps);
res.json({ simulatedBorrowRateBps: rate });
} catch (error) {
res.status(500).json({ error: 'Failed to simulate rate' });
}
}
}

export const interestController = new InterestController();
61 changes: 61 additions & 0 deletions api/src/routes/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Router } from 'express';
import { eventsController } from '../controllers/events.controller';

const router: Router = Router();

/**
* @openapi
* /events:
* get:
* summary: Query indexed contract events with filters
* description: Returns paginated contract events filtered by type, address, or time range
* tags:
* - Events
* parameters:
* - in: query
* name: type
* schema:
* type: string
* - in: query
* name: address
* schema:
* type: string
* - in: query
* name: from
* schema:
* type: integer
* - in: query
* name: to
* schema:
* type: integer
* - in: query
* name: limit
* schema:
* type: integer
* default: 50
*/
router.get('/', eventsController.getEvents);

/**
* @openapi
* /events/types:
* get:
* summary: List all known event types
* description: Returns distinct event types available for filtering
* tags:
* - Events
*/
router.get('/types', eventsController.getEventTypes);

/**
* @openapi
* /events/stats:
* get:
* summary: Event volume statistics
* description: Returns counts and trends for recent events
* tags:
* - Events
*/
router.get('/stats', eventsController.getEventStats);

export default router;
57 changes: 57 additions & 0 deletions api/src/routes/interest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Router } from 'express';
import { interestController } from '../controllers/interest.controller';

const router: Router = Router();

/**
* @openapi
* /interest/current:
* get:
* summary: Current interest rates for the protocol
* description: Returns current borrow and supply rates in basis points
* tags:
* - Interest
* responses:
* 200:
* description: Current interest rates
*/
router.get('/current', interestController.getCurrentRates);

/**
* @openapi
* /interest/history:
* get:
* summary: Historical interest rate snapshots
* description: Returns bounded history of rate snapshots
* tags:
* - Interest
* parameters:
* - in: query
* name: limit
* schema:
* type: integer
* default: 100
*/
router.get('/history', interestController.getRateHistory);

/**
* @openapi
* /interest/simulate:
* post:
* summary: Simulate borrow rate at a target utilization
* description: Returns simulated borrow rate without modifying state
* tags:
* - Interest
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* utilizationBps:
* type: integer
*/
router.post('/simulate', interestController.simulateRate);

export default router;
79 changes: 79 additions & 0 deletions api/src/services/events.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { StellarService } from './stellar.service';
import { redisCacheService } from './redisCache.service';
import logger from '../utils/logger';

const EVENTS_CACHE_TTL_S = 60;

export interface IndexedEvent {
id: string;
type: string;
contract: string;
topic?: string[];
data: Record<string, any>;
timestamp: number;
ledger: number;
}

export interface EventStats {
totalEvents: number;
eventTypeCounts: Record<string, number>;
lastUpdated: number;
}

const stellarService = new StellarService();

export async function getIndexedEvents(filters: {
type?: string;
address?: string;
from?: number;
to?: number;
limit: number;
}): Promise<IndexedEvent[]> {
const cacheKey = redisCacheService.buildKey(
'events',
`query:${filters.type ?? 'all'}:${filters.address ?? 'all'}:${filters.from ?? 0}:${filters.to ?? 0}:${filters.limit}`
);
const cached = await redisCacheService.get<IndexedEvent[]>(cacheKey);
if (cached) return cached;

try {
const events = await stellarService.readContract('get_indexed_events', filters);
const result = Array.isArray(events) ? events : [];
await redisCacheService.set(cacheKey, result, EVENTS_CACHE_TTL_S);
return result;
} catch (error) {
logger.warn('Failed to fetch indexed events from contract', { error, filters });
return [];
}
}

export function getEventTypes(): string[] {
return [
'deposit',
'withdraw',
'borrow',
'repay',
'liquidation',
'flash_loan',
'admin_action',
'price_updated',
'risk_params_updated',
'pause_state_changed',
'position_updated',
'analytics_updated',
'user_activity_tracked',
];
}

export function getEventStats(): EventStats {
const eventTypeCounts: Record<string, number> = {};
for (const type of getEventTypes()) {
eventTypeCounts[type] = 0;
}

return {
totalEvents: 0,
eventTypeCounts,
lastUpdated: Date.now(),
};
}
Loading