Skip to content

Commit b92debf

Browse files
Merge remote-tracking branch 'origin/master' into federiconardelli7/academy-course-x402-fundamentals
2 parents 45642b9 + 82f4bf1 commit b92debf

File tree

72 files changed

+8810
-1199
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+8810
-1199
lines changed

app/(home)/stats/playground/page.tsx

Lines changed: 1042 additions & 0 deletions
Large diffs are not rendered by default.

app/api/chain-stats/[chainId]/route.ts

Lines changed: 99 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,26 @@ let cachedData: Map<string, { data: ChainMetrics; timestamp: number; icmTimeRang
3030
async function getTimeSeriesData(
3131
metricType: string,
3232
chainId: string,
33-
timeRange: string,
33+
timeRange: string,
34+
startTimestamp?: number,
35+
endTimestamp?: number,
3436
pageSize: number = 365,
3537
fetchAllPages: boolean = false
3638
): Promise<TimeSeriesDataPoint[]> {
3739
try {
38-
const { startTimestamp, endTimestamp } = getTimestampsFromTimeRange(timeRange);
40+
// Use provided timestamps if available, otherwise use timeRange
41+
let finalStartTimestamp: number;
42+
let finalEndTimestamp: number;
43+
44+
if (startTimestamp !== undefined && endTimestamp !== undefined) {
45+
finalStartTimestamp = startTimestamp;
46+
finalEndTimestamp = endTimestamp;
47+
} else {
48+
const timestamps = getTimestampsFromTimeRange(timeRange);
49+
finalStartTimestamp = timestamps.startTimestamp;
50+
finalEndTimestamp = timestamps.endTimestamp;
51+
}
52+
3953
let allResults: any[] = [];
4054

4155
const avalanche = new Avalanche({
@@ -46,8 +60,8 @@ async function getTimeSeriesData(
4660
const params: any = {
4761
chainId: chainId,
4862
metric: metricType as any,
49-
startTimestamp,
50-
endTimestamp,
63+
startTimestamp: finalStartTimestamp,
64+
endTimestamp: finalEndTimestamp,
5165
timeInterval: "day",
5266
pageSize,
5367
};
@@ -82,19 +96,29 @@ async function getTimeSeriesData(
8296
}
8397
}
8498

85-
async function getICMData(chainId: string, timeRange: string): Promise<ICMDataPoint[]> {
99+
async function getICMData(chainId: string, timeRange: string, startTimestamp?: number, endTimestamp?: number): Promise<ICMDataPoint[]> {
86100
try {
87-
const getDaysFromTimeRange = (range: string): number => {
88-
switch (range) {
89-
case '7d': return 7;
90-
case '30d': return 30;
91-
case '90d': return 90;
92-
case 'all': return 365;
93-
default: return 30;
94-
}
95-
};
101+
let days: number;
102+
103+
if (startTimestamp !== undefined && endTimestamp !== undefined) {
104+
// Calculate days from timestamps
105+
const startDate = new Date(startTimestamp * 1000);
106+
const endDate = new Date(endTimestamp * 1000);
107+
const diffTime = Math.abs(endDate.getTime() - startDate.getTime());
108+
days = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
109+
} else {
110+
const getDaysFromTimeRange = (range: string): number => {
111+
switch (range) {
112+
case '7d': return 7;
113+
case '30d': return 30;
114+
case '90d': return 90;
115+
case 'all': return 365;
116+
default: return 30;
117+
}
118+
};
119+
days = getDaysFromTimeRange(timeRange);
120+
}
96121

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

111-
return data
135+
let filteredData = data
112136
.sort((a: any, b: any) => b.timestamp - a.timestamp)
113137
.map((item: any) => ({
114138
timestamp: item.timestamp,
@@ -117,6 +141,15 @@ async function getICMData(chainId: string, timeRange: string): Promise<ICMDataPo
117141
incomingCount: item.incomingCount || 0,
118142
outgoingCount: item.outgoingCount || 0,
119143
}));
144+
145+
// Filter by timestamps if provided
146+
if (startTimestamp !== undefined && endTimestamp !== undefined) {
147+
filteredData = filteredData.filter((item: ICMDataPoint) => {
148+
return item.timestamp >= startTimestamp && item.timestamp <= endTimestamp;
149+
});
150+
}
151+
152+
return filteredData;
120153
} catch (error) {
121154
console.warn(`Failed to fetch ICM data for chain ${chainId}:`, error);
122155
return [];
@@ -131,6 +164,8 @@ export async function GET(
131164
try {
132165
const { searchParams } = new URL(request.url);
133166
const timeRange = searchParams.get('timeRange') || '30d';
167+
const startTimestampParam = searchParams.get('startTimestamp');
168+
const endTimestampParam = searchParams.get('endTimestamp');
134169
const resolvedParams = await params;
135170
const chainId = resolvedParams.chainId;
136171

@@ -141,7 +176,34 @@ export async function GET(
141176
);
142177
}
143178

144-
const cacheKey = `${chainId}-${timeRange}`;
179+
// Parse timestamps if provided
180+
const startTimestamp = startTimestampParam ? parseInt(startTimestampParam, 10) : undefined;
181+
const endTimestamp = endTimestampParam ? parseInt(endTimestampParam, 10) : undefined;
182+
183+
// Validate timestamps
184+
if (startTimestamp !== undefined && isNaN(startTimestamp)) {
185+
return NextResponse.json(
186+
{ error: 'Invalid startTimestamp parameter' },
187+
{ status: 400 }
188+
);
189+
}
190+
if (endTimestamp !== undefined && isNaN(endTimestamp)) {
191+
return NextResponse.json(
192+
{ error: 'Invalid endTimestamp parameter' },
193+
{ status: 400 }
194+
);
195+
}
196+
if (startTimestamp !== undefined && endTimestamp !== undefined && startTimestamp > endTimestamp) {
197+
return NextResponse.json(
198+
{ error: 'startTimestamp must be less than or equal to endTimestamp' },
199+
{ status: 400 }
200+
);
201+
}
202+
203+
// Create cache key including timestamps if provided
204+
const cacheKey = startTimestamp !== undefined && endTimestamp !== undefined
205+
? `${chainId}-${startTimestamp}-${endTimestamp}`
206+
: `${chainId}-${timeRange}`;
145207

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

152214
if (cached && Date.now() - cached.timestamp < STATS_CONFIG.CACHE.LONG_DURATION) {
153-
if (cached.icmTimeRange !== timeRange) {
215+
// Only refetch ICM data if timeRange changed (not for timestamp-based queries)
216+
if (startTimestamp === undefined && endTimestamp === undefined && cached.icmTimeRange !== timeRange) {
154217
try {
155218
const newICMData = await getICMData(chainId, timeRange);
156219
cached.data.icmMessages = createICMMetric(newICMData);
@@ -199,24 +262,24 @@ export async function GET(
199262
feesPaidData,
200263
icmData,
201264
] = await Promise.all([
202-
getTimeSeriesData('activeAddresses', chainId, timeRange, pageSize, fetchAllPages),
203-
getTimeSeriesData('activeSenders', chainId, timeRange, pageSize, fetchAllPages),
204-
getTimeSeriesData('cumulativeAddresses', chainId, timeRange, pageSize, fetchAllPages),
205-
getTimeSeriesData('cumulativeDeployers', chainId, timeRange, pageSize, fetchAllPages),
206-
getTimeSeriesData('txCount', chainId, timeRange, pageSize, fetchAllPages),
207-
getTimeSeriesData('cumulativeTxCount', chainId, timeRange, pageSize, fetchAllPages),
208-
getTimeSeriesData('cumulativeContracts', chainId, timeRange, pageSize, fetchAllPages),
209-
getTimeSeriesData('contracts', chainId, timeRange, pageSize, fetchAllPages),
210-
getTimeSeriesData('deployers', chainId, timeRange, pageSize, fetchAllPages),
211-
getTimeSeriesData('gasUsed', chainId, timeRange, pageSize, fetchAllPages),
212-
getTimeSeriesData('avgGps', chainId, timeRange, pageSize, fetchAllPages),
213-
getTimeSeriesData('maxGps', chainId, timeRange, pageSize, fetchAllPages),
214-
getTimeSeriesData('avgTps', chainId, timeRange, pageSize, fetchAllPages),
215-
getTimeSeriesData('maxTps', chainId, timeRange, pageSize, fetchAllPages),
216-
getTimeSeriesData('avgGasPrice', chainId, timeRange, pageSize, fetchAllPages),
217-
getTimeSeriesData('maxGasPrice', chainId, timeRange, pageSize, fetchAllPages),
218-
getTimeSeriesData('feesPaid', chainId, timeRange, pageSize, fetchAllPages),
219-
getICMData(chainId, timeRange),
265+
getTimeSeriesData('activeAddresses', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
266+
getTimeSeriesData('activeSenders', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
267+
getTimeSeriesData('cumulativeAddresses', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
268+
getTimeSeriesData('cumulativeDeployers', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
269+
getTimeSeriesData('txCount', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
270+
getTimeSeriesData('cumulativeTxCount', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
271+
getTimeSeriesData('cumulativeContracts', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
272+
getTimeSeriesData('contracts', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
273+
getTimeSeriesData('deployers', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
274+
getTimeSeriesData('gasUsed', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
275+
getTimeSeriesData('avgGps', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
276+
getTimeSeriesData('maxGps', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
277+
getTimeSeriesData('avgTps', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
278+
getTimeSeriesData('maxTps', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
279+
getTimeSeriesData('avgGasPrice', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
280+
getTimeSeriesData('maxGasPrice', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
281+
getTimeSeriesData('feesPaid', chainId, timeRange, startTimestamp, endTimestamp, pageSize, fetchAllPages),
282+
getICMData(chainId, timeRange, startTimestamp, endTimestamp),
220283
]);
221284

222285
const metrics: ChainMetrics = {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { prisma } from '@/prisma/prisma';
3+
4+
// POST /api/playground/[id]/view - Increment view count
5+
export async function POST(
6+
req: NextRequest,
7+
{ params }: { params: Promise<{ id: string }> }
8+
) {
9+
try {
10+
const { id: playgroundId } = await params;
11+
12+
if (!playgroundId) {
13+
return NextResponse.json({ error: 'Playground ID is required' }, { status: 400 });
14+
}
15+
16+
// Increment view count atomically
17+
const playground = await prisma.statsPlayground.update({
18+
where: { id: playgroundId },
19+
data: {
20+
view_count: {
21+
increment: 1
22+
}
23+
},
24+
select: {
25+
view_count: true
26+
}
27+
});
28+
29+
return NextResponse.json({
30+
success: true,
31+
view_count: playground.view_count
32+
});
33+
} catch (error) {
34+
console.error('Error incrementing view count:', error);
35+
// Don't fail the request if view tracking fails
36+
return NextResponse.json({
37+
success: false,
38+
error: 'Failed to track view'
39+
}, { status: 500 });
40+
}
41+
}
42+
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { getAuthSession } from '@/lib/auth/authSession';
3+
import { prisma } from '@/prisma/prisma';
4+
5+
// POST /api/playground/favorite - Favorite a playground
6+
export async function POST(req: NextRequest) {
7+
try {
8+
const session = await getAuthSession();
9+
if (!session?.user) {
10+
return NextResponse.json({ error: 'Unauthorized, please sign in to continue.' }, { status: 401 });
11+
}
12+
13+
const body = await req.json();
14+
const { playgroundId } = body;
15+
16+
if (!playgroundId) {
17+
return NextResponse.json({ error: 'Playground ID is required.' }, { status: 400 });
18+
}
19+
20+
// Verify playground exists and is public or owned by user
21+
const playground = await prisma.statsPlayground.findFirst({
22+
where: {
23+
id: playgroundId,
24+
OR: [
25+
{ user_id: session.user.id },
26+
{ is_public: true }
27+
]
28+
}
29+
});
30+
31+
if (!playground) {
32+
return NextResponse.json({ error: 'Playground not found' }, { status: 404 });
33+
}
34+
35+
// Check if already favorited
36+
const existingFavorite = await prisma.statsPlaygroundFavorite.findUnique({
37+
where: {
38+
playground_id_user_id: {
39+
playground_id: playgroundId,
40+
user_id: session.user.id
41+
}
42+
}
43+
});
44+
45+
if (existingFavorite) {
46+
return NextResponse.json({ error: 'Playground already favorited' }, { status: 400 });
47+
}
48+
49+
// Create favorite
50+
await prisma.statsPlaygroundFavorite.create({
51+
data: {
52+
playground_id: playgroundId,
53+
user_id: session.user.id
54+
}
55+
});
56+
57+
// Get updated favorite count
58+
const favoriteCount = await prisma.statsPlaygroundFavorite.count({
59+
where: { playground_id: playgroundId }
60+
});
61+
62+
return NextResponse.json({
63+
success: true,
64+
favorite_count: favoriteCount
65+
});
66+
} catch (error) {
67+
console.error('Error favoriting playground:', error);
68+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
69+
}
70+
}
71+
72+
// DELETE /api/playground/favorite - Unfavorite a playground
73+
export async function DELETE(req: NextRequest) {
74+
try {
75+
const session = await getAuthSession();
76+
if (!session?.user) {
77+
return NextResponse.json({ error: 'Unauthorized, please sign in to continue.' }, { status: 401 });
78+
}
79+
80+
const { searchParams } = new URL(req.url);
81+
const playgroundId = searchParams.get('playgroundId');
82+
83+
if (!playgroundId) {
84+
return NextResponse.json({ error: 'Playground ID is required.' }, { status: 400 });
85+
}
86+
87+
// Delete favorite
88+
await prisma.statsPlaygroundFavorite.deleteMany({
89+
where: {
90+
playground_id: playgroundId,
91+
user_id: session.user.id
92+
}
93+
});
94+
95+
// Get updated favorite count
96+
const favoriteCount = await prisma.statsPlaygroundFavorite.count({
97+
where: { playground_id: playgroundId }
98+
});
99+
100+
return NextResponse.json({
101+
success: true,
102+
favorite_count: favoriteCount
103+
});
104+
} catch (error) {
105+
console.error('Error unfavoriting playground:', error);
106+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
107+
}
108+
}
109+

0 commit comments

Comments
 (0)