diff --git a/src/app/api/analytics/protocol/route.ts b/src/app/api/analytics/protocol/route.ts new file mode 100644 index 0000000..dfbdab8 --- /dev/null +++ b/src/app/api/analytics/protocol/route.ts @@ -0,0 +1,130 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { + ChainCommitment, + getUserCommitmentsFromChain, +} from '@/lib/backend/services/contracts'; +import { + BackendError, + normalizeBackendError, + toBackendErrorResponse, +} from '@/lib/backend/errors'; +import { isFeatureEnabled } from '@/lib/backend/config'; +import { ok, fail } from '@/lib/backend/apiResponse'; + +/** + * Protocol-level analytics aggregating all commitments across the network. + */ +interface ProtocolAnalyticsResponse { + totalCommitments: number; + activeCommitments: number; + violatedCommitments: number; + settledCommitments: number; + totalValueCommitted: string; + averageDurationDays: number; + averageComplianceScore: number; + timestamp: string; +} + +/** + * Sums a numeric field across all commitments, handling string-to-number conversions. + */ +function sumNumericStringField( + commitments: ChainCommitment[], + field: 'amount' | 'feeEarned' +): string { + const total = commitments.reduce((acc, commitment) => { + const value = Number(commitment[field]); + return Number.isFinite(value) ? acc + value : acc; + }, 0); + + return total.toFixed(2); +} + +/** + * Builds protocol-level analytics from all commitments. + * + * @param commitments - Array of all chain commitments + * @returns Protocol analytics payload + */ +function buildProtocolAnalytics(commitments: ChainCommitment[]): ProtocolAnalyticsResponse { + const totalCommitments = commitments.length; + + // Count commitments by status + const activeCommitments = commitments.filter( + (commitment) => commitment.status === 'ACTIVE' + ).length; + const violatedCommitments = commitments.filter( + (commitment) => commitment.status === 'VIOLATED' + ).length; + const settledCommitments = commitments.filter( + (commitment) => commitment.status === 'SETTLED' + ).length; + + // Calculate average compliance score + const averageComplianceScore = + totalCommitments === 0 + ? 0 + : commitments.reduce( + (acc, commitment) => acc + commitment.complianceScore, + 0 + ) / totalCommitments; + + // TODO: Calculate averageDurationDays from actual commitment data + // when chain data structure includes duration or timestamps + const averageDurationDays = 0; + + return { + totalCommitments, + activeCommitments, + violatedCommitments, + settledCommitments, + totalValueCommitted: sumNumericStringField(commitments, 'amount'), + averageDurationDays, + averageComplianceScore: Number(averageComplianceScore.toFixed(2)), + timestamp: new Date().toISOString(), + }; +} + +/** + * Retrieves all commitments from the chain. + * + * TODO: Implement getAllCommitmentsFromChain for protocol-level queries. + * Current implementation uses a fallback approach: + * - In production, this should query indexed on-chain data (e.g., Stellar subgraph, caching layer) + * - For now, returns empty array to signal that data aggregation is not yet fully implemented + */ +async function getAllCommitmentsFromChain(): Promise { + // TODO: Replace with actual chain query once indexing is available + // This would typically involve: + // 1. Querying a Soroban contract method like "get_all_commitments" + // 2. Using an on-chain indexer or off-chain cache + // 3. Implementing pagination for large datasets + return []; +} + +export async function GET(req: NextRequest) { + if (!isFeatureEnabled('analyticsProtocol')) { + return fail( + 'NOT_FOUND', + 'Protocol analytics endpoint is disabled.', + { feature: 'analyticsProtocol' }, + 404 + ); + } + + try { + const commitments = await getAllCommitmentsFromChain(); + const analytics = buildProtocolAnalytics(commitments); + return ok(analytics); + } catch (error) { + const normalized = normalizeBackendError(error, { + code: 'INTERNAL_ERROR', + message: 'Failed to compute protocol analytics.', + status: 500, + }); + + return NextResponse.json(toBackendErrorResponse(normalized), { + status: normalized.status, + }); + } +} diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index 0ffaafd..4bf97c1 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,22 +1,11 @@ -import { NextRequest, NextResponse } from 'next/server' - -export async function GET(_request: NextRequest) { - return NextResponse.json( - { - status: 'healthy', - timestamp: new Date().toISOString(), - version: '0.1.0', - }, - { status: 200 } - ) -import { NextRequest, NextResponse } from "next/server"; -import { logInfo } from "@/lib/backend/logger"; -import { attachSecurityHeaders } from "@/utils/response"; +import { NextRequest, NextResponse } from 'next/server'; +import { logInfo } from '@/lib/backend/logger'; +import { attachSecurityHeaders } from '@/utils/response'; export async function GET(req: NextRequest) { - logInfo(req, "Healthcheck requested"); + logInfo(req, 'Healthcheck requested'); const response = NextResponse.json({ - status: "ok", + status: 'ok', timestamp: new Date().toISOString(), }); return attachSecurityHeaders(response); diff --git a/src/lib/backend/config.ts b/src/lib/backend/config.ts index f701c09..71fb2af 100644 --- a/src/lib/backend/config.ts +++ b/src/lib/backend/config.ts @@ -180,6 +180,7 @@ function isTestEnvironment(): boolean { export interface BackendFeatureFlags { analyticsUser: boolean; + analyticsProtocol: boolean; marketplace: boolean; } @@ -203,6 +204,10 @@ function parseFeatureFlagsJson(): Partial { typeof parsed.analyticsUser === 'boolean' ? parsed.analyticsUser : undefined, + analyticsProtocol: + typeof parsed.analyticsProtocol === 'boolean' + ? parsed.analyticsProtocol + : undefined, marketplace: typeof parsed.marketplace === 'boolean' ? parsed.marketplace @@ -222,6 +227,9 @@ export function getFeatureFlags(): BackendFeatureFlags { analyticsUser: fromJson.analyticsUser ?? parseBooleanFlag(process.env.COMMITLABS_FEATURE_ANALYTICS_USER, false), + analyticsProtocol: + fromJson.analyticsProtocol ?? + parseBooleanFlag(process.env.COMMITLABS_FEATURE_ANALYTICS_PROTOCOL, false), marketplace: fromJson.marketplace ?? parseBooleanFlag(process.env.COMMITLABS_FEATURE_MARKETPLACE, false)