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
1,042 changes: 1,042 additions & 0 deletions app/(home)/stats/playground/page.tsx

Large diffs are not rendered by default.

135 changes: 99 additions & 36 deletions app/api/chain-stats/[chainId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,26 @@ let cachedData: Map<string, { data: ChainMetrics; timestamp: number; icmTimeRang
async function getTimeSeriesData(
metricType: string,
chainId: string,
timeRange: string,
timeRange: string,
startTimestamp?: number,
endTimestamp?: number,
pageSize: number = 365,
fetchAllPages: boolean = false
): Promise<TimeSeriesDataPoint[]> {
try {
const { startTimestamp, endTimestamp } = getTimestampsFromTimeRange(timeRange);
// Use provided timestamps if available, otherwise use timeRange
let finalStartTimestamp: number;
let finalEndTimestamp: number;

if (startTimestamp !== undefined && endTimestamp !== undefined) {
finalStartTimestamp = startTimestamp;
finalEndTimestamp = endTimestamp;
} else {
const timestamps = getTimestampsFromTimeRange(timeRange);
finalStartTimestamp = timestamps.startTimestamp;
finalEndTimestamp = timestamps.endTimestamp;
}

let allResults: any[] = [];

const avalanche = new Avalanche({
Expand All @@ -46,8 +60,8 @@ async function getTimeSeriesData(
const params: any = {
chainId: chainId,
metric: metricType as any,
startTimestamp,
endTimestamp,
startTimestamp: finalStartTimestamp,
endTimestamp: finalEndTimestamp,
timeInterval: "day",
pageSize,
};
Expand Down Expand Up @@ -82,19 +96,29 @@ async function getTimeSeriesData(
}
}

async function getICMData(chainId: string, timeRange: string): Promise<ICMDataPoint[]> {
async function getICMData(chainId: string, timeRange: string, startTimestamp?: number, endTimestamp?: number): Promise<ICMDataPoint[]> {
try {
const getDaysFromTimeRange = (range: string): number => {
switch (range) {
case '7d': return 7;
case '30d': return 30;
case '90d': return 90;
case 'all': return 365;
default: return 30;
}
};
let days: number;

if (startTimestamp !== undefined && endTimestamp !== undefined) {
// Calculate days from timestamps
const startDate = new Date(startTimestamp * 1000);
const endDate = new Date(endTimestamp * 1000);
const diffTime = Math.abs(endDate.getTime() - startDate.getTime());
days = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
} else {
const getDaysFromTimeRange = (range: string): number => {
switch (range) {
case '7d': return 7;
case '30d': return 30;
case '90d': return 90;
case 'all': return 365;
default: return 30;
}
};
days = getDaysFromTimeRange(timeRange);
}

const days = getDaysFromTimeRange(timeRange);
const response = await fetch(`https://idx6.solokhin.com/api/${chainId}/metrics/dailyMessageVolume?days=${days}`, {
headers: { 'Accept': 'application/json' },
});
Expand All @@ -108,7 +132,7 @@ async function getICMData(chainId: string, timeRange: string): Promise<ICMDataPo
return [];
}

return data
let filteredData = data
.sort((a: any, b: any) => b.timestamp - a.timestamp)
.map((item: any) => ({
timestamp: item.timestamp,
Expand All @@ -117,6 +141,15 @@ async function getICMData(chainId: string, timeRange: string): Promise<ICMDataPo
incomingCount: item.incomingCount || 0,
outgoingCount: item.outgoingCount || 0,
}));

// Filter by timestamps if provided
if (startTimestamp !== undefined && endTimestamp !== undefined) {
filteredData = filteredData.filter((item: ICMDataPoint) => {
return item.timestamp >= startTimestamp && item.timestamp <= endTimestamp;
});
}

return filteredData;
} catch (error) {
console.warn(`Failed to fetch ICM data for chain ${chainId}:`, error);
return [];
Expand All @@ -131,6 +164,8 @@ export async function GET(
try {
const { searchParams } = new URL(request.url);
const timeRange = searchParams.get('timeRange') || '30d';
const startTimestampParam = searchParams.get('startTimestamp');
const endTimestampParam = searchParams.get('endTimestamp');
const resolvedParams = await params;
const chainId = resolvedParams.chainId;

Expand All @@ -141,7 +176,34 @@ export async function GET(
);
}

const cacheKey = `${chainId}-${timeRange}`;
// Parse timestamps if provided
const startTimestamp = startTimestampParam ? parseInt(startTimestampParam, 10) : undefined;
const endTimestamp = endTimestampParam ? parseInt(endTimestampParam, 10) : undefined;

// Validate timestamps
if (startTimestamp !== undefined && isNaN(startTimestamp)) {
return NextResponse.json(
{ error: 'Invalid startTimestamp parameter' },
{ status: 400 }
);
}
if (endTimestamp !== undefined && isNaN(endTimestamp)) {
return NextResponse.json(
{ error: 'Invalid endTimestamp parameter' },
{ status: 400 }
);
}
if (startTimestamp !== undefined && endTimestamp !== undefined && startTimestamp > endTimestamp) {
return NextResponse.json(
{ error: 'startTimestamp must be less than or equal to endTimestamp' },
{ status: 400 }
);
}

// Create cache key including timestamps if provided
const cacheKey = startTimestamp !== undefined && endTimestamp !== undefined
? `${chainId}-${startTimestamp}-${endTimestamp}`
: `${chainId}-${timeRange}`;

if (searchParams.get('clearCache') === 'true') {
cachedData.clear();
Expand All @@ -150,7 +212,8 @@ export async function GET(
const cached = cachedData.get(cacheKey);

if (cached && Date.now() - cached.timestamp < STATS_CONFIG.CACHE.LONG_DURATION) {
if (cached.icmTimeRange !== timeRange) {
// Only refetch ICM data if timeRange changed (not for timestamp-based queries)
if (startTimestamp === undefined && endTimestamp === undefined && cached.icmTimeRange !== timeRange) {
try {
const newICMData = await getICMData(chainId, timeRange);
cached.data.icmMessages = createICMMetric(newICMData);
Expand Down Expand Up @@ -199,24 +262,24 @@ export async function GET(
feesPaidData,
icmData,
] = await Promise.all([
getTimeSeriesData('activeAddresses', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('activeSenders', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('cumulativeAddresses', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('cumulativeDeployers', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('txCount', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('cumulativeTxCount', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('cumulativeContracts', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('contracts', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('deployers', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('gasUsed', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('avgGps', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('maxGps', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('avgTps', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('maxTps', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('avgGasPrice', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('maxGasPrice', chainId, timeRange, pageSize, fetchAllPages),
getTimeSeriesData('feesPaid', chainId, timeRange, pageSize, fetchAllPages),
getICMData(chainId, timeRange),
getTimeSeriesData('activeAddresses', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('activeSenders', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('cumulativeAddresses', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('cumulativeDeployers', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('txCount', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('cumulativeTxCount', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('cumulativeContracts', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('contracts', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('deployers', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('gasUsed', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('avgGps', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('maxGps', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('avgTps', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('maxTps', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('avgGasPrice', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('maxGasPrice', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getTimeSeriesData('feesPaid', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
getICMData(chainId, timeRange, startTimestamp, endTimestamp),
]);

const metrics: ChainMetrics = {
Expand Down
42 changes: 42 additions & 0 deletions app/api/playground/[id]/view/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/prisma/prisma';

// POST /api/playground/[id]/view - Increment view count
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: playgroundId } = await params;

if (!playgroundId) {
return NextResponse.json({ error: 'Playground ID is required' }, { status: 400 });
}

// Increment view count atomically
const playground = await prisma.statsPlayground.update({
where: { id: playgroundId },
data: {
view_count: {
increment: 1
}
},
select: {
view_count: true
}
});

return NextResponse.json({
success: true,
view_count: playground.view_count
});
} catch (error) {
console.error('Error incrementing view count:', error);
// Don't fail the request if view tracking fails
return NextResponse.json({
success: false,
error: 'Failed to track view'
}, { status: 500 });
}
}

109 changes: 109 additions & 0 deletions app/api/playground/favorite/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthSession } from '@/lib/auth/authSession';
import { prisma } from '@/prisma/prisma';

// POST /api/playground/favorite - Favorite a playground
export async function POST(req: NextRequest) {
try {
const session = await getAuthSession();
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized, please sign in to continue.' }, { status: 401 });
}

const body = await req.json();
const { playgroundId } = body;

if (!playgroundId) {
return NextResponse.json({ error: 'Playground ID is required.' }, { status: 400 });
}

// Verify playground exists and is public or owned by user
const playground = await prisma.statsPlayground.findFirst({
where: {
id: playgroundId,
OR: [
{ user_id: session.user.id },
{ is_public: true }
]
}
});

if (!playground) {
return NextResponse.json({ error: 'Playground not found' }, { status: 404 });
}

// Check if already favorited
const existingFavorite = await prisma.statsPlaygroundFavorite.findUnique({
where: {
playground_id_user_id: {
playground_id: playgroundId,
user_id: session.user.id
}
}
});

if (existingFavorite) {
return NextResponse.json({ error: 'Playground already favorited' }, { status: 400 });
}

// Create favorite
await prisma.statsPlaygroundFavorite.create({
data: {
playground_id: playgroundId,
user_id: session.user.id
}
});

// Get updated favorite count
const favoriteCount = await prisma.statsPlaygroundFavorite.count({
where: { playground_id: playgroundId }
});

return NextResponse.json({
success: true,
favorite_count: favoriteCount
});
} catch (error) {
console.error('Error favoriting playground:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}

// DELETE /api/playground/favorite - Unfavorite a playground
export async function DELETE(req: NextRequest) {
try {
const session = await getAuthSession();
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized, please sign in to continue.' }, { status: 401 });
}

const { searchParams } = new URL(req.url);
const playgroundId = searchParams.get('playgroundId');

if (!playgroundId) {
return NextResponse.json({ error: 'Playground ID is required.' }, { status: 400 });
}

// Delete favorite
await prisma.statsPlaygroundFavorite.deleteMany({
where: {
playground_id: playgroundId,
user_id: session.user.id
}
});

// Get updated favorite count
const favoriteCount = await prisma.statsPlaygroundFavorite.count({
where: { playground_id: playgroundId }
});

return NextResponse.json({
success: true,
favorite_count: favoriteCount
});
} catch (error) {
console.error('Error unfavoriting playground:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}

Loading