Skip to content
Open
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
130 changes: 130 additions & 0 deletions src/app/api/analytics/protocol/route.ts
Original file line number Diff line number Diff line change
@@ -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<ChainCommitment[]> {
// 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,
});
}
}
21 changes: 5 additions & 16 deletions src/app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/lib/backend/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ function isTestEnvironment(): boolean {

export interface BackendFeatureFlags {
analyticsUser: boolean;
analyticsProtocol: boolean;
marketplace: boolean;
}

Expand All @@ -203,6 +204,10 @@ function parseFeatureFlagsJson(): Partial<BackendFeatureFlags> {
typeof parsed.analyticsUser === 'boolean'
? parsed.analyticsUser
: undefined,
analyticsProtocol:
typeof parsed.analyticsProtocol === 'boolean'
? parsed.analyticsProtocol
: undefined,
marketplace:
typeof parsed.marketplace === 'boolean'
? parsed.marketplace
Expand All @@ -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)
Expand Down