From 26d9666f32e9aa7340700aa2139add5a5595ead9 Mon Sep 17 00:00:00 2001 From: Oyinade247 Date: Sat, 27 Jun 2026 01:18:40 +0100 Subject: [PATCH] refactor: improve type safety across core modules by replacing any with specific types and unknown --- eslint.config.mjs | 2 +- prisma/schema.prisma | 22 +++++++------ src/agent/loop.ts | 2 +- src/agent/router.ts | 11 ++++--- src/agent/scanner.ts | 30 ++++++++--------- src/agent/snapshotter.ts | 6 ++-- src/config/env.ts | 4 +-- src/middleware/adminAuth.ts | 2 +- src/middleware/corsandbody.ts | 4 +-- src/middleware/errorHandler.ts | 2 +- src/middleware/rateLimiter.ts | 12 +++++-- src/middleware/validate.ts | 8 ++--- src/routes/admin.ts | 20 ++++++------ src/routes/portfolio.ts | 16 ++++----- src/routes/protocols.ts | 2 +- src/services/alerting.ts | 8 ++--- src/stellar/contract.ts | 8 ++--- src/stellar/dlq.ts | 59 +++++++++++++++++++--------------- src/stellar/events.ts | 26 ++++++++------- src/utils/api-formatters.ts | 6 ++-- src/utils/errorResponse.ts | 22 ++++++------- src/utils/errors.ts | 4 +-- src/utils/fetchWithRetry.ts | 6 ++-- src/utils/logger.ts | 4 +-- src/utils/pagination.ts | 2 +- 25 files changed, 155 insertions(+), 133 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 24178d6..743cc15 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -19,7 +19,7 @@ export default [ }, rules: { ...tseslint.configs.recommended.rules, - '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-require-imports': 'off', '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-unsafe-function-type': 'off', diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2d1efd3..7ed6c87 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -40,6 +40,7 @@ enum AgentAction { REBALANCE ANALYZE ALERT + SCAN CLAIM_YIELD } @@ -95,16 +96,17 @@ model Session { } model AdminApiKey { - id String @id @default(uuid()) - name String @unique - role String - scopes String[] - hash String - expiresAt DateTime? - revokedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - lastUsedAt DateTime? + id String @id @default(uuid()) + name String @unique + role String + scopes String[] + hash String + tokenPrefix String? + expiresAt DateTime? + revokedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastUsedAt DateTime? auditLogs AdminAuditLog[] diff --git a/src/agent/loop.ts b/src/agent/loop.ts index f0ab294..d065dc9 100644 --- a/src/agent/loop.ts +++ b/src/agent/loop.ts @@ -110,7 +110,7 @@ async function rebalanceCheckJob(): Promise { for (const [protocol, protocolPositions] of byProtocol.entries()) { const result = await executeRebalanceIfNeeded( protocol, - protocolPositions.map((p: any) => ({ + protocolPositions.map((p) => ({ id: p.id, amount: p.currentValue.toString(), })), diff --git a/src/agent/router.ts b/src/agent/router.ts index 7ce9034..b0580ba 100644 --- a/src/agent/router.ts +++ b/src/agent/router.ts @@ -4,6 +4,7 @@ import { logger } from '../utils/logger'; import { getCorrelationId } from '../utils/correlation'; +import type { AgentAction, AgentStatus } from '@prisma/client'; import { ProtocolComparison, RebalanceDetails, RebalanceThresholds } from './types'; import { scanAllProtocols, getCurrentOnChainApy } from './scanner'; import { triggerRebalance as submitRebalance } from '../stellar/contract'; @@ -178,7 +179,7 @@ export async function triggerRebalance( network: representativePosition.user.network, protocolName: toProtocol, memo: `Agent rebalance from ${fromProtocol} to ${toProtocol}`, - } as any, + }, }); } else { logger.warn('No position found to persist rebalance transaction', { @@ -307,8 +308,8 @@ export async function executeRebalanceIfNeeded( * a null userId so it is distinguishable from user-level actions. */ export async function logAgentAction( - action: string, - status: 'SUCCESS' | 'FAILED' | 'SKIPPED', + action: AgentAction, + status: AgentStatus, data?: Record, userId?: string, positionId?: string, @@ -327,8 +328,8 @@ export async function logAgentAction( data: { userId: userId ?? null, positionId: positionId ?? null, - action: action as any, - status: status as any, + action, + status, inputData: inputWithCorrelation ? JSON.stringify(inputWithCorrelation) : data?.input ? JSON.stringify(data.input) : undefined, outputData: data?.output ? JSON.stringify(data.output) : undefined, reasoning: data?.reasoning as string | undefined, diff --git a/src/agent/scanner.ts b/src/agent/scanner.ts index cb5dcdc..412ac6f 100644 --- a/src/agent/scanner.ts +++ b/src/agent/scanner.ts @@ -2,6 +2,7 @@ * Scanner - Fetches real APY rates from Stellar yield protocols */ +import { Network } from '@prisma/client'; import { logger } from '../utils/logger'; import { YieldProtocol, ProtocolRate } from './types'; import db from '../db'; @@ -40,13 +41,12 @@ async function fetchBlendApy(): Promise { const poolId = process.env.BLEND_POOL_ID || 'GBUQWP3BOUZX34PISXEAMBNIZJLNCLVNX77MHAHVXHVVB4CMYAOK6BAC'; - const data = await fetchWithRetry( + const data = await fetchWithRetry<{ reserves?: Array<{ asset?: { code?: string; symbol?: string }; supplyApy?: string; totalSupply?: string }> }>( `${network}/api/v1/pool/${poolId}`, { timeout: 5000, retries: 3 } ); - // Extract USDC reserve APY and TVL from response - const reserve = data?.reserves?.find((r: any) => + const reserve = data?.reserves?.find((r) => r.asset?.code === 'USDC' || r.asset?.symbol === 'USDC' ); @@ -87,7 +87,7 @@ async function fetchStellarDexApy(): Promise { const horizonUrl = process.env.HORIZON_URL || 'https://horizon.stellar.org'; const usdcIssuer = process.env.USDC_ISSUER || 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN'; - const data = await fetchWithRetry( + const data = await fetchWithRetry<{ _embedded?: { records?: Array<{ total_shares?: string; fee_bp?: string }> } }>( `${horizonUrl}/liquidity_pools?reserves=${ASSET_SYMBOL}:${usdcIssuer}&limit=10&order=desc`, { timeout: 5000, retries: 3 } ); @@ -134,16 +134,16 @@ async function fetchLumaApy(): Promise { try { const lumaUrl = process.env.LUMA_API_URL || 'https://api.luma.finance'; - const data = await fetchWithRetry( + const data = await fetchWithRetry<{ rates?: Array<{ asset?: string; symbol?: string; apy?: string; tvl?: string }> }>( `${lumaUrl}/v1/rates?asset=${ASSET_SYMBOL}`, { timeout: 5000, retries: 3 } ); - const rate = data?.rates?.find((r: any) => + const rate = data?.rates?.find((r) => r.asset === ASSET_SYMBOL || r.symbol === ASSET_SYMBOL ); - if (!rate) throw new Error('USDC rate not found in Luma response'); + if (!rate?.apy) throw new Error('USDC rate not found in Luma response'); const apyRate = parseFloat(rate.apy) * 100; const tvl = rate.tvl ? parseFloat(rate.tvl) : undefined; @@ -209,15 +209,16 @@ export async function scanAllProtocols(): Promise { return filtered; } -function normalizeNetwork(): string { +function normalizeNetwork(): Network { const network = process.env.STELLAR_NETWORK?.toLowerCase(); - const validNetworks = ['mainnet', 'testnet', 'futurenet']; - if (!network || !validNetworks.includes(network)) { + const validNetworks: Network[] = ['MAINNET', 'TESTNET', 'FUTURENET']; + const upper = network?.toUpperCase() as Network | undefined; + if (!upper || !validNetworks.includes(upper)) { throw new Error( `Invalid STELLAR_NETWORK: "${process.env.STELLAR_NETWORK}". Must be one of: ${validNetworks.join(', ')}` ); } - return network.toUpperCase(); + return upper; } /** @@ -231,10 +232,9 @@ async function saveProtocolRates(protocols: YieldProtocol[]): Promise { data: { protocolName: protocol.name, assetSymbol: protocol.assetSymbol, - supplyApy: protocol.apy as any, - tvl: protocol.tvl === undefined ? undefined : (protocol.tvl as any), - network: networkLabel as any, - rawResponse: JSON.stringify({ fetchedAt: new Date(), source: protocol.name }), + supplyApy: protocol.apy, + tvl: protocol.tvl === undefined ? undefined : protocol.tvl, + network: networkLabel, }, }); } diff --git a/src/agent/snapshotter.ts b/src/agent/snapshotter.ts index f54faac..42db442 100644 --- a/src/agent/snapshotter.ts +++ b/src/agent/snapshotter.ts @@ -35,7 +35,7 @@ export async function captureAllUserBalances(): Promise { // CRITICAL FIX: Use batch insert (createMany) instead of individual awaits // This scales much better as user base grows - const snapshotData = positions.map((pos: any) => { + const snapshotData = positions.map((pos) => { const yearsActive = calculateYearsActive(pos.openedAt); const apy = calculateApy( pos.depositedAmount.toNumber(), @@ -46,7 +46,7 @@ export async function captureAllUserBalances(): Promise { return { positionId: pos.id, // Coerce computed APY (number) into Prisma Decimal field. - apy: apy as any, + apy, yieldAmount: pos.yieldEarned, principalAmount: pos.depositedAmount, }; @@ -122,7 +122,7 @@ export async function getPositionHistory( }, }); - return snapshots.map((snapshot: any) => ({ + return snapshots.map((snapshot) => ({ userId: snapshot.position.userId, walletAddress: snapshot.position.user.walletAddress, positionId, diff --git a/src/config/env.ts b/src/config/env.ts index 7532636..9a83437 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -113,7 +113,7 @@ function validateAllRequiredEnvVars(): void { // ── 10. NODE_ENV: must be one of the known deployment environments ──────── const nodeEnv = process.env.NODE_ENV const validNodeEnvs = ['development', 'staging', 'production', 'test'] as const - if (nodeEnv && !validNodeEnvs.includes(nodeEnv as any)) { + if (nodeEnv && !(validNodeEnvs as readonly string[]).includes(nodeEnv)) { errors.push( `NODE_ENV is invalid: "${nodeEnv}". Must be one of: ${validNodeEnvs.join(' | ')}` ) @@ -137,7 +137,7 @@ function validateStellarNetwork(network: string): 'testnet' | 'mainnet' | 'futur const validNetworks = ['testnet', 'mainnet', 'futurenet'] as const const lowerNetwork = network.toLowerCase() - if (!validNetworks.includes(lowerNetwork as any)) { + if (!(validNetworks as readonly string[]).includes(lowerNetwork)) { throw new Error( `Invalid STELLAR_NETWORK: "${network}". Must be one of: ${validNetworks.join(', ')}` ) diff --git a/src/middleware/adminAuth.ts b/src/middleware/adminAuth.ts index 50e0a73..0c96c31 100644 --- a/src/middleware/adminAuth.ts +++ b/src/middleware/adminAuth.ts @@ -5,7 +5,7 @@ import db from '../db' import { logger } from '../utils/logger' import { recordAuthFailure } from '../utils/metrics' -const prisma = db as any +const prisma = db export interface AdminAuthContext { id: string diff --git a/src/middleware/corsandbody.ts b/src/middleware/corsandbody.ts index 98dbb34..db37897 100644 --- a/src/middleware/corsandbody.ts +++ b/src/middleware/corsandbody.ts @@ -115,12 +115,12 @@ export const urlencodedBodyParser = express.urlencoded({ * this converts those into a consistent JSON response. */ export function payloadSizeErrorHandler( - err: any, + err: unknown, _req: Request, res: Response, next: NextFunction ): void { - if (err.type === 'entity.too.large') { + if (err && typeof err === 'object' && 'type' in err && (err as Record).type === 'entity.too.large') { res.status(413).json({ success: false, error: 'Payload Too Large', diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts index 546bf9d..7d8cb96 100644 --- a/src/middleware/errorHandler.ts +++ b/src/middleware/errorHandler.ts @@ -20,7 +20,7 @@ export function errorHandler( const isDevelopment = process.env.NODE_ENV === 'development' const errorResponse = ErrorResponses.internalError( 'Internal server error', - requestId, + requestId ?? 'unknown', isDevelopment ? { message: err.message } : undefined ) diff --git a/src/middleware/rateLimiter.ts b/src/middleware/rateLimiter.ts index e53cd2c..1087fac 100644 --- a/src/middleware/rateLimiter.ts +++ b/src/middleware/rateLimiter.ts @@ -1,4 +1,10 @@ import { type Request, type Response, type NextFunction } from 'express' + +interface RateLimitRequest extends Request { + rateLimit?: { + resetTime?: Date; + }; +} import rateLimit from 'express-rate-limit' import { config } from '../config/env' import { recordRateLimitHit } from '../utils/metrics' @@ -64,8 +70,8 @@ function getRouteGroup(path: string): string { * Handler called when rate limit is exceeded. Sets Retry-After before responding. */ function handleRateLimitExceeded( - req: any, - res: any, + req: RateLimitRequest, + res: Response, options: { limiterType: string; windowMs: number } ): void { const routeGroup = getRouteGroup(req.path) @@ -117,7 +123,7 @@ export function buildRateLimiter( legacyHeaders: false, skip: opts.skip, message: { error: opts.message ?? 'Too many requests. Please try again later.' }, - handler: (req: any, res: any) => + handler: (req: RateLimitRequest, res: Response) => handleRateLimitExceeded(req, res, { limiterType: opts.limiterType, windowMs: opts.windowMs, diff --git a/src/middleware/validate.ts b/src/middleware/validate.ts index cdcbc9e..2a2c6c2 100644 --- a/src/middleware/validate.ts +++ b/src/middleware/validate.ts @@ -9,10 +9,10 @@ export interface ValidationSchemas { errorMessage?: string; } -type SchemasOrSchema = ValidationSchemas | ZodSchema | ZodTypeAny; +type SchemasOrSchema = ValidationSchemas | ZodTypeAny; -function isZodSchema(val: any): val is ZodSchema | ZodTypeAny { - return val && typeof val.safeParseAsync === 'function'; +function isZodSchema(val: unknown): val is ZodTypeAny { + return val !== null && typeof val === 'object' && 'safeParseAsync' in val && typeof (val as Record).safeParseAsync === 'function'; } function formatZodErrors(err: ZodError) { @@ -37,7 +37,7 @@ export const validate = (schemasOrSchema: SchemasOrSchema) => { } // Merge parsed results back into req if present - const data: any = parsed.data || {}; + const data = (parsed.data ?? {}) as Record; if (data.body !== undefined) req.body = data.body; if (data.query !== undefined) Object.defineProperty(req, 'query', { value: data.query, writable: true, configurable: true }); if (data.params !== undefined) Object.defineProperty(req, 'params', { value: data.params, writable: true, configurable: true }); diff --git a/src/routes/admin.ts b/src/routes/admin.ts index 5e1a63a..6d9909c 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -10,6 +10,7 @@ */ import { Router, Request, Response } from 'express' +import type { Prisma } from '@prisma/client' import { getEventMetrics } from '../stellar/events' import { DeadLetterQueue } from '../stellar/dlq' import { logger } from '../utils/logger' @@ -17,14 +18,14 @@ import { requireAdminAuth, requireAdminScope } from '../middleware/adminAuth' import db from '../db' const router = Router() -const prisma = db as any +const prisma = db function auditLog( req: Request, res: Response, action: string, result: string, - details?: Record, + details?: Record, ): void { const adminAuth = res.locals.adminAuth const auditPayload = { @@ -52,7 +53,7 @@ function auditLog( action, target: req.originalUrl || req.path, result, - details, + details: details as unknown as Prisma.InputJsonValue, ipAddress: req.ip, userAgent: req.get('user-agent') || null, method: req.method, @@ -168,14 +169,14 @@ router.get( if (timeRangeStart) { const startDate = new Date(timeRangeStart as string) if (!isNaN(startDate.getTime())) { - filtered = filtered.filter(e => e.createdAt >= startDate) + filtered = filtered.filter(e => new Date(e.createdAt) >= startDate) } } if (timeRangeEnd) { const endDate = new Date(timeRangeEnd as string) if (!isNaN(endDate.getTime())) { - filtered = filtered.filter(e => e.createdAt <= endDate) + filtered = filtered.filter(e => new Date(e.createdAt) <= endDate) } } @@ -605,9 +606,10 @@ router.post( }, timestamp: new Date().toISOString(), }) - } catch (error: any) { + } catch (error: unknown) { // Unique constraint violation — name already taken - if (error?.code === 'P2002') { + const prismaError = error as { code?: string }; + if (prismaError?.code === 'P2002') { return res.status(409).json({ success: false, error: 'A key with that name already exists' }) } logger.error('[Admin] Failed to create admin key', { @@ -631,7 +633,7 @@ router.delete( requireAdminScope('keys:write'), async (req: Request, res: Response) => { try { - const { id } = req.params + const id = req.params.id as string const existing = await prisma.adminApiKey.findUnique({ where: { id }, @@ -647,7 +649,7 @@ router.delete( } await prisma.adminApiKey.update({ - where: { id }, + where: { id: id }, data: { revokedAt: new Date() }, }) diff --git a/src/routes/portfolio.ts b/src/routes/portfolio.ts index 3d86304..3dabc9f 100644 --- a/src/routes/portfolio.ts +++ b/src/routes/portfolio.ts @@ -43,13 +43,13 @@ router.get('/:userId', requireAuth, enforceUserAccess, validate(portfolioSchema) where: { userId }, }) - const totalBalance = userPositions.reduce((sum: number, position: any) => { + const totalBalance = userPositions.reduce((sum: number, position) => { return sum + Number(position.currentValue) }, 0) - const totalEarnings = userPositions.reduce((sum: number, position: any) => { + const totalEarnings = userPositions.reduce((sum: number, position) => { return sum + Number(position.yieldEarned) }, 0) - const activePositions = userPositions.filter((p: any) => p.status === 'ACTIVE').length + const activePositions = userPositions.filter((p) => p.status === 'ACTIVE').length const positions = userPositions.map(mapPositionToResponse) @@ -100,7 +100,7 @@ router.get( take: 30, }) - const points = snapshots.map((snapshot: any) => ({ + const points = snapshots.map((snapshot) => ({ date: snapshot.snapshotAt.toISOString().slice(0, 10), yieldAmount: Number(snapshot.yieldAmount), })) @@ -110,7 +110,7 @@ router.get( period: req.query.period, points, whatsappReply: formatPortfolioHistoryReply({ - period: req.query.period as any, + period: req.query.period as '7d' | '30d' | '90d', points, }), }) @@ -142,16 +142,16 @@ router.get( take: 30, }) - const totalEarnings = userPositions.reduce((sum: number, position: any) => { + const totalEarnings = userPositions.reduce((sum: number, position) => { return sum + Number(position.yieldEarned) }, 0) - const periodEarnings = snapshots.reduce((sum: number, snapshot: any) => { + const periodEarnings = snapshots.reduce((sum: number, snapshot) => { return sum + Number(snapshot.yieldAmount) }, 0) const averageApy = snapshots.length > 0 ? snapshots.reduce( - (sum: number, snapshot: any) => sum + Number(snapshot.apy), + (sum: number, snapshot) => sum + Number(snapshot.apy), 0 ) / snapshots.length diff --git a/src/routes/protocols.ts b/src/routes/protocols.ts index 5540f76..8e69678 100644 --- a/src/routes/protocols.ts +++ b/src/routes/protocols.ts @@ -19,7 +19,7 @@ router.get('/rates', async (req: Request, res: Response) => { take: 10, }) - const items = rates.map((rate: any) => ({ + const items = rates.map((rate) => ({ protocolName: rate.protocolName, assetSymbol: rate.assetSymbol, supplyApy: Number(rate.supplyApy), diff --git a/src/services/alerting.ts b/src/services/alerting.ts index c7b60e5..f014fe3 100644 --- a/src/services/alerting.ts +++ b/src/services/alerting.ts @@ -17,7 +17,7 @@ export interface AlertPayload { description: string severity: 'info' | 'warning' | 'critical' component: string - metadata?: Record + metadata?: Record } export interface DLQAlertPayload extends AlertPayload { @@ -228,7 +228,7 @@ class AlertingService { : 'good' const dlqPayload = payload as DLQAlertPayload - let blocks: any[] = [ + const blocks: Array> = [ { type: 'header', text: { @@ -248,7 +248,7 @@ class AlertingService { // Add DLQ-specific metadata if (dlqPayload.dlqSize !== undefined) { - const fields: any[] = [ + const fields: Array> = [ { type: 'mrkdwn', text: `*Current Size:*\n${dlqPayload.dlqSize} events`, @@ -351,7 +351,7 @@ class AlertingService { ? 'warning' : 'info' - let customDetails: Record = { + let customDetails: Record = { ...payload.metadata, } diff --git a/src/stellar/contract.ts b/src/stellar/contract.ts index 34d43c2..eb7ced7 100644 --- a/src/stellar/contract.ts +++ b/src/stellar/contract.ts @@ -114,7 +114,7 @@ async function executeCustodialVaultOperation( /** * Simulate and parse contract read call */ -async function simulateRead(method: string, args: xdr.ScVal[] = []): Promise { +async function simulateRead(method: string, args: xdr.ScVal[] = []): Promise { const tx = await buildContractCall(method, args); const simulation = await simulateTransaction(tx); @@ -135,7 +135,7 @@ async function simulateRead(method: string, args: xdr.ScVal[] = []): Promise { const addressScVal = nativeToScVal(userAddress, { type: 'address' }); - const result = await simulateRead('get_balance', [addressScVal]); + const result = await simulateRead('get_balance', [addressScVal]) as { balance?: unknown; shares?: unknown }; return { balance: result.balance?.toString() || '0', @@ -147,7 +147,7 @@ export async function getOnChainBalance(userAddress: string): Promise { - const apyBasisPoints = await simulateRead('get_apy'); + const apyBasisPoints = await simulateRead('get_apy') as number; return apyBasisPoints / 100; // Convert basis points to percentage } @@ -155,7 +155,7 @@ export async function getOnChainAPY(): Promise { * Get active protocol */ export async function getActiveProtocol(): Promise { - return await simulateRead('get_active_protocol'); + return await simulateRead('get_active_protocol') as string; } /** diff --git a/src/stellar/dlq.ts b/src/stellar/dlq.ts index f33d1c3..d0178a4 100644 --- a/src/stellar/dlq.ts +++ b/src/stellar/dlq.ts @@ -9,6 +9,7 @@ */ import * as fs from 'fs' import * as path from 'path' +import type { Prisma } from '@prisma/client' import { xdr } from '@stellar/stellar-sdk' import { logger } from '../utils/logger' import db from '../db' @@ -26,7 +27,7 @@ export interface DeadLetterEvent { eventType: string ledger: number error: string - payload: any + payload: unknown status: DeadLetterEventStatus retryCount: number createdAt: string @@ -54,7 +55,7 @@ function deserializeScVal(value: unknown): unknown { } } -function serializePayload(event: any): any { +function serializePayload(event: Record): Record { return { ...event, topics: Array.isArray(event?.topics) @@ -64,7 +65,7 @@ function serializePayload(event: any): any { } } -function buildPayload(event: any): any { +function buildPayload(event: Record): Record { const correlationId = getCorrelationId() ?? event?.correlationId const serialized = serializePayload(event) if (correlationId) { @@ -76,7 +77,7 @@ function buildPayload(event: any): any { return serialized } -function deserializePayload(event: any): any { +function deserializePayload(event: Record): Record { return { ...event, topics: Array.isArray(event?.topics) @@ -122,16 +123,26 @@ function toDomain(row: PrismaDeadLetterRow): DeadLetterEvent { } } +interface StellarEventPayload { + contractId?: string; + txHash?: string; + type?: string; + ledger?: number; + topics?: unknown[]; + value?: unknown; + correlationId?: string; +} + export class DeadLetterQueue { - static async add(event: any, errorMsg: string): Promise { - const row = await (db as any).deadLetterEvent.create({ + static async add(event: StellarEventPayload, errorMsg: string): Promise { + const row = await db.deadLetterEvent.create({ data: { contractId: event?.contractId ?? 'unknown', txHash: event?.txHash ?? 'unknown', eventType: event?.type ?? 'unknown', ledger: typeof event?.ledger === 'number' ? event.ledger : 0, error: errorMsg, - payload: buildPayload(event), + payload: buildPayload(event as unknown as Record) as unknown as Prisma.InputJsonValue, status: 'PENDING' as const, retryCount: 0, }, @@ -146,24 +157,20 @@ export class DeadLetterQueue { } static async getAll(): Promise { - const rows: PrismaDeadLetterRow[] = await ( - db as any - ).deadLetterEvent.findMany({ + const rows = await db.deadLetterEvent.findMany({ orderBy: { createdAt: 'asc' }, }) return rows.map(toDomain) } static async getSize(): Promise { - return (db as any).deadLetterEvent.count() + return db.deadLetterEvent.count() } static async retryAll( - retryFn: (event: any) => Promise + retryFn: (event: unknown) => Promise ): Promise<{ resolved: number; failed: number }> { - const rows: PrismaDeadLetterRow[] = await ( - db as any - ).deadLetterEvent.findMany({ + const rows = await db.deadLetterEvent.findMany({ where: { status: { in: ['PENDING', 'RETRIED'] } }, orderBy: { createdAt: 'asc' }, }) @@ -173,15 +180,15 @@ export class DeadLetterQueue { for (const row of rows) { try { - await retryFn(deserializePayload(row.payload)) - await (db as any).deadLetterEvent.update({ + await retryFn(deserializePayload(row.payload as Record)) + await db.deadLetterEvent.update({ where: { id: row.id }, data: { status: 'RESOLVED', retryCount: row.retryCount + 1 }, }) resolved++ logger.info(`[DLQ Retry] Successfully retried event ${row.id}`) } catch (error) { - await (db as any).deadLetterEvent.update({ + await db.deadLetterEvent.update({ where: { id: row.id }, data: { status: 'RETRIED', retryCount: row.retryCount + 1 }, }) @@ -204,7 +211,7 @@ export class DeadLetterQueue { static async resolve(id: string): Promise { try { - await (db as any).deadLetterEvent.update({ + await db.deadLetterEvent.update({ where: { id }, data: { status: 'RESOLVED' }, }) @@ -243,7 +250,7 @@ export class DeadLetterQueue { let skipped = 0 for (const event of rows) { - const existing = await (db as any).deadLetterEvent.findFirst({ + const existing = await db.deadLetterEvent.findFirst({ where: { contractId: event.contractId, txHash: event.txHash, @@ -257,14 +264,14 @@ export class DeadLetterQueue { continue } - await (db as any).deadLetterEvent.create({ + await db.deadLetterEvent.create({ data: { contractId: event.contractId, txHash: event.txHash, eventType: event.eventType, ledger: event.ledger, error: event.error, - payload: event.payload, + payload: event.payload as unknown as Prisma.InputJsonValue, status: event.status, retryCount: event.retryCount, }, @@ -352,13 +359,13 @@ export class DeadLetterQueue { retried: number resolved: number }> { - const pending = await (db as any).deadLetterEvent.count({ + const pending = await db.deadLetterEvent.count({ where: { status: 'PENDING' }, }) - const retried = await (db as any).deadLetterEvent.count({ + const retried = await db.deadLetterEvent.count({ where: { status: 'RETRIED' }, }) - const resolved = await (db as any).deadLetterEvent.count({ + const resolved = await db.deadLetterEvent.count({ where: { status: 'RESOLVED' }, }) @@ -369,7 +376,7 @@ export class DeadLetterQueue { * Get the oldest pending event (for age tracking) */ private static async getOldestPendingEvent(): Promise { - return (db as any).deadLetterEvent.findFirst({ + return db.deadLetterEvent.findFirst({ where: { status: 'PENDING' }, orderBy: { createdAt: 'asc' }, }) diff --git a/src/stellar/events.ts b/src/stellar/events.ts index fb6147a..d54a148 100644 --- a/src/stellar/events.ts +++ b/src/stellar/events.ts @@ -201,10 +201,12 @@ function parseRebalanceEvent(event: ContractEvent): RebalanceEvent { /** * Handle deposit event - persist to database */ -async function handleDepositEvent(depositData: DepositEvent, event: ContractEvent, tx: any = db): Promise { +type TxClient = Omit; + +async function handleDepositEvent(depositData: DepositEvent, event: ContractEvent, tx: TxClient = db): Promise { const user = await timedDbOperation(() => tx.user.findUnique({ where: { walletAddress: depositData.user } }) - ) as any; + ); if (!user) { logger.warn(`[Deposit] User not found for wallet: ${depositData.user}`); @@ -226,13 +228,13 @@ async function handleDepositEvent(depositData: DepositEvent, event: ContractEven confirmedAt: new Date(), }, }) - ) as any; + ); const position = await timedDbOperation(() => tx.position.findFirst({ where: { userId: user.id, protocolName: depositData.protocolName, assetSymbol: depositData.assetSymbol, status: 'ACTIVE' }, }) - ) as any; + ); if (position) { await timedDbOperation(() => @@ -260,7 +262,7 @@ async function handleDepositEvent(depositData: DepositEvent, event: ContractEven yieldEarned: 0, }, }) - ) as any; + ); await timedDbOperation(() => tx.transaction.update({ where: { id: transaction.id }, data: { positionId: newPosition.id } }) ); @@ -270,10 +272,10 @@ async function handleDepositEvent(depositData: DepositEvent, event: ContractEven /** * Handle withdraw event - persist to database */ -async function handleWithdrawEvent(withdrawData: WithdrawEvent, event: ContractEvent, tx: any = db): Promise { +async function handleWithdrawEvent(withdrawData: WithdrawEvent, event: ContractEvent, tx: TxClient = db): Promise { const user = await timedDbOperation(() => tx.user.findUnique({ where: { walletAddress: withdrawData.user } }) - ) as any; + ); if (!user) { logger.warn(`[Withdraw] User not found for wallet: ${withdrawData.user}`); @@ -295,13 +297,13 @@ async function handleWithdrawEvent(withdrawData: WithdrawEvent, event: ContractE confirmedAt: new Date(), }, }) - ) as any; + ); const position = await timedDbOperation(() => tx.position.findFirst({ where: { userId: user.id, protocolName: withdrawData.protocolName, assetSymbol: withdrawData.assetSymbol, status: 'ACTIVE' }, }) - ) as any; + ); if (position) { const newDepositedAmount = new Decimal(position.depositedAmount).minus(withdrawData.amount); @@ -322,7 +324,7 @@ async function handleWithdrawEvent(withdrawData: WithdrawEvent, event: ContractE /** * Handle rebalance event - persist to database */ -async function handleRebalanceEvent(rebalanceData: RebalanceEvent, event: ContractEvent, tx: any = db): Promise { +async function handleRebalanceEvent(rebalanceData: RebalanceEvent, event: ContractEvent, tx: TxClient = db): Promise { await timedDbOperation(() => tx.protocolRate.create({ data: { @@ -341,7 +343,7 @@ async function handleRebalanceEvent(rebalanceData: RebalanceEvent, event: Contra /** * Handle contract event with persistence, idempotency, and validation (Issue #53) */ -export async function handleEvent(event: ContractEvent, tx: any = db): Promise { +export async function handleEvent(event: ContractEvent, tx: TxClient = db): Promise { const correlationId = generateCorrelationId(); return runWithCorrelationIdAsync(correlationId, async () => { const eventWithCorrelation = { ...event, correlationId }; @@ -614,7 +616,7 @@ export async function backfillEvents(startLedger: number, endLedger?: number): P export async function retryDeadLetterEvents(): Promise { logger.info(`[DLQ] Starting manual intervention retry for all DLQ events`); await DeadLetterQueue.retryAll(async (eventPayload) => { - await handleEvent(eventPayload, db); + await handleEvent(eventPayload as ContractEvent, db); }); } diff --git a/src/utils/api-formatters.ts b/src/utils/api-formatters.ts index 6b89322..e170849 100644 --- a/src/utils/api-formatters.ts +++ b/src/utils/api-formatters.ts @@ -1,4 +1,6 @@ -export const mapTransactionToResponse = (tx: any) => ({ +import type { Transaction, Position } from '@prisma/client' + +export const mapTransactionToResponse = (tx: Transaction) => ({ id: tx.id, txHash: tx.txHash, type: tx.type, @@ -9,7 +11,7 @@ export const mapTransactionToResponse = (tx: any) => ({ createdAt: tx.createdAt.toISOString(), }) -export const mapPositionToResponse = (position: any) => ({ +export const mapPositionToResponse = (position: Position) => ({ id: position.id, protocolName: position.protocolName, assetSymbol: position.assetSymbol, diff --git a/src/utils/errorResponse.ts b/src/utils/errorResponse.ts index 224f389..65a6a19 100644 --- a/src/utils/errorResponse.ts +++ b/src/utils/errorResponse.ts @@ -17,7 +17,7 @@ export interface ErrorResponse { error: { code: string message: string - details?: Record + details?: Record } requestId: string timestamp: string @@ -42,7 +42,7 @@ export function buildErrorResponse( code: string, message: string, requestId: string, - details?: Record + details?: Record ): ErrorResponse { return { error: { @@ -59,30 +59,30 @@ export function buildErrorResponse( * Error response builders for common HTTP status codes. */ export const ErrorResponses = { - badRequest: (message: string, requestId: string, details?: Record) => + badRequest: (message: string, requestId: string, details?: Record) => buildErrorResponse(ErrorCodes.BAD_REQUEST, message, requestId, details), - unauthorized: (message: string, requestId: string, details?: Record) => + unauthorized: (message: string, requestId: string, details?: Record) => buildErrorResponse(ErrorCodes.UNAUTHORIZED, message, requestId, details), - forbidden: (message: string, requestId: string, details?: Record) => + forbidden: (message: string, requestId: string, details?: Record) => buildErrorResponse(ErrorCodes.FORBIDDEN, message, requestId, details), - notFound: (message: string, requestId: string, details?: Record) => + notFound: (message: string, requestId: string, details?: Record) => buildErrorResponse(ErrorCodes.NOT_FOUND, message, requestId, details), - conflict: (message: string, requestId: string, details?: Record) => + conflict: (message: string, requestId: string, details?: Record) => buildErrorResponse(ErrorCodes.CONFLICT, message, requestId, details), - rateLimited: (message: string, requestId: string, details?: Record) => + rateLimited: (message: string, requestId: string, details?: Record) => buildErrorResponse(ErrorCodes.RATE_LIMITED, message, requestId, details), - validationError: (message: string, requestId: string, details?: Record) => + validationError: (message: string, requestId: string, details?: Record) => buildErrorResponse(ErrorCodes.VALIDATION_ERROR, message, requestId, details), - internalError: (message: string, requestId: string, details?: Record) => + internalError: (message: string, requestId: string, details?: Record) => buildErrorResponse(ErrorCodes.INTERNAL_ERROR, message, requestId, details), - serviceUnavailable: (message: string, requestId: string, details?: Record) => + serviceUnavailable: (message: string, requestId: string, details?: Record) => buildErrorResponse(ErrorCodes.SERVICE_UNAVAILABLE, message, requestId, details), } diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 3a9d5a4..84421bc 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -4,14 +4,14 @@ export class AppError extends Error { constructor( public statusCode: number, public message: string, - public details?: any + public details?: unknown ) { super(message) Object.setPrototypeOf(this, AppError.prototype) } } -export const sendError = (res: Response, statusCode: number, message: string, details?: any) => { +export const sendError = (res: Response, statusCode: number, message: string, details?: unknown) => { return res.status(statusCode).json({ error: message, details, diff --git a/src/utils/fetchWithRetry.ts b/src/utils/fetchWithRetry.ts index 58127b1..9bf4ffa 100644 --- a/src/utils/fetchWithRetry.ts +++ b/src/utils/fetchWithRetry.ts @@ -18,10 +18,10 @@ const circuitBreakers: Record = {}; const CIRCUIT_OPEN_DURATION = 60000; // 1 minute const FAILURE_THRESHOLD = 3; -export async function fetchWithRetry( +export async function fetchWithRetry( url: string, options: FetchOptions = {} -): Promise { +): Promise { const { timeout = 5000, retries = 3, retryDelay = 1000 } = options; // Check circuit breaker @@ -50,7 +50,7 @@ export async function fetchWithRetry( // Reset circuit breaker on success circuitBreakers[url] = { failures: 0, lastFailure: 0, isOpen: false }; - return await res.json(); + return (await res.json()) as T; } catch (err) { lastError = err instanceof Error ? err : new Error(String(err)); if (attempt < retries - 1) { diff --git a/src/utils/logger.ts b/src/utils/logger.ts index c5a26de..e2dcaec 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -51,7 +51,7 @@ const correlationFormat = winston.format((info) => { // Custom format that redacts sensitive data const redactFormat = winston.format.printf(({ timestamp, level, message, ...meta }) => { const safeMessage = typeof message === 'string' ? redactSensitiveData(message) : message - const safeMeta: any = {} + const safeMeta: Record = {} for (const [key, value] of Object.entries(meta)) { safeMeta[key] = typeof value === 'string' ? redactSensitiveData(value) : value } @@ -144,7 +144,7 @@ export function logBackgroundJob( status: 'success' | 'failed', durationSeconds: number, correlationId?: string, - details?: Record + details?: Record ): void { const logContext = { jobName, diff --git a/src/utils/pagination.ts b/src/utils/pagination.ts index a2ab23f..bc8df6f 100644 --- a/src/utils/pagination.ts +++ b/src/utils/pagination.ts @@ -5,7 +5,7 @@ export const paginationSchema = z.object({ limit: z.coerce.number().int().min(1).max(50).default(5), }) -export function getPaginationParams(query: any) { +export function getPaginationParams(query: Record) { const page = Number(query.page) || 1 const limit = Number(query.limit) || 5 const skip = (page - 1) * limit