diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index b7773653..e1ccc057 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -50,9 +50,11 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.PKG_GITHUB_TOKEN }} - - name: Run linting, formatting check, and type checking - run: | - pnpm run lint & - pnpm prettier --check . & - pnpm run typecheck & - wait + - name: Run linting + run: pnpm run lint + + - name: Run formatting check + run: pnpm prettier --check . + + - name: Run type checking + run: pnpm run typecheck diff --git a/app/api/stats/external-transfer-logs/route.ts b/app/api/stats/external-transfer-logs/route.ts index c43193a2..066aee52 100644 --- a/app/api/stats/external-transfer-logs/route.ts +++ b/app/api/stats/external-transfer-logs/route.ts @@ -1,3 +1,7 @@ +import { NextRequest } from "next/server" + +import { kv } from "@vercel/kv" + import { BucketData, ExternalTransferData, @@ -5,8 +9,7 @@ import { INFLOWS_SET_KEY, } from "@/app/api/stats/constants" import { getAllSetMembers } from "@/app/lib/kv-utils" -import { kv } from "@vercel/kv" -import { NextRequest } from "next/server" + export const runtime = "edge" export const dynamic = "force-dynamic" @@ -16,51 +19,59 @@ function startOfPeriod(timestamp: number, intervalMs: number): number { export async function GET(req: NextRequest) { try { - const intervalMs = parseInt(req.nextUrl.searchParams.get("interval") || "86400000") + const intervalMs = parseInt( + req.nextUrl.searchParams.get("interval") || "86400000", + ) - const transactionHashes = await getAllSetMembers(kv, INFLOWS_SET_KEY); + const transactionHashes = await getAllSetMembers(kv, INFLOWS_SET_KEY) // Use pipelining to fetch all data in a single round-trip - const pipeline = kv.pipeline(); - transactionHashes.forEach(hash => pipeline.get(`${INFLOWS_KEY}:${hash}`)); - const data = await pipeline.exec(); + const pipeline = kv.pipeline() + transactionHashes.forEach((hash) => pipeline.get(`${INFLOWS_KEY}:${hash}`)) + const data = await pipeline.exec() - const buckets: Record = {}; + const buckets: Record = {} data.forEach((item) => { if (item && typeof item === "object" && "timestamp" in item) { - const transfer = item as ExternalTransferData; - const bucketTimestamp = startOfPeriod(transfer.timestamp, intervalMs); - const bucketKey = bucketTimestamp.toString(); + const transfer = item as ExternalTransferData + const bucketTimestamp = startOfPeriod(transfer.timestamp, intervalMs) + const bucketKey = bucketTimestamp.toString() if (!buckets[bucketKey]) { buckets[bucketKey] = { timestamp: bucketKey, depositAmount: 0, withdrawalAmount: 0, - }; + } } if (transfer.isWithdrawal) { - buckets[bucketKey].withdrawalAmount += transfer.amount; + buckets[bucketKey].withdrawalAmount += transfer.amount } else { - buckets[bucketKey].depositAmount += transfer.amount; + buckets[bucketKey].depositAmount += transfer.amount } } - }); + }) const sortedBucketData = Object.values(buckets).sort( - (a, b) => parseInt(a.timestamp) - parseInt(b.timestamp) - ); + (a, b) => parseInt(a.timestamp) - parseInt(b.timestamp), + ) - return new Response(JSON.stringify({ data: sortedBucketData, intervalMs }), { - headers: { "Content-Type": "application/json" }, - }); + return new Response( + JSON.stringify({ data: sortedBucketData, intervalMs }), + { + headers: { "Content-Type": "application/json" }, + }, + ) } catch (error) { - return new Response(JSON.stringify({ error: "Failed to fetch external transfer logs" }), { - status: 500, - headers: { "Content-Type": "application/json" }, - }); + return new Response( + JSON.stringify({ error: "Failed to fetch external transfer logs" }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ) } } diff --git a/app/api/stats/historical-volume-kv/route.ts b/app/api/stats/historical-volume-kv/route.ts index 12114267..0302cb45 100644 --- a/app/api/stats/historical-volume-kv/route.ts +++ b/app/api/stats/historical-volume-kv/route.ts @@ -1,5 +1,7 @@ import { NextRequest } from "next/server" + import { kv } from "@vercel/kv" + import { HISTORICAL_VOLUME_SET_KEY } from "@/app/api/stats/constants" import { getAllSetMembers } from "@/app/lib/kv-utils" @@ -20,45 +22,50 @@ export const dynamic = "force-dynamic" export async function GET(req: NextRequest) { try { - const allKeys = await getAllSetMembers(kv, HISTORICAL_VOLUME_SET_KEY); + const allKeys = await getAllSetMembers(kv, HISTORICAL_VOLUME_SET_KEY) // Use pipelining to fetch all data in a single round-trip - const pipeline = kv.pipeline(); - allKeys.forEach(key => pipeline.get(key)); - const data = await pipeline.exec(); + const pipeline = kv.pipeline() + allKeys.forEach((key) => pipeline.get(key)) + const data = await pipeline.exec() - const volumeData: VolumeDataPoint[] = []; - let startTimestamp = Infinity; - let endTimestamp = -Infinity; + const volumeData: VolumeDataPoint[] = [] + let startTimestamp = Infinity + let endTimestamp = -Infinity data.forEach((item) => { - if (item && typeof item === "object" && "timestamp" in item && "volume" in item) { - const dataPoint = item as VolumeDataPoint; - volumeData.push(dataPoint); - startTimestamp = Math.min(startTimestamp, dataPoint.timestamp); - endTimestamp = Math.max(endTimestamp, dataPoint.timestamp); + if ( + item && + typeof item === "object" && + "timestamp" in item && + "volume" in item + ) { + const dataPoint = item as VolumeDataPoint + volumeData.push(dataPoint) + startTimestamp = Math.min(startTimestamp, dataPoint.timestamp) + endTimestamp = Math.max(endTimestamp, dataPoint.timestamp) } - }); + }) - volumeData.sort((a, b) => a.timestamp - b.timestamp); + volumeData.sort((a, b) => a.timestamp - b.timestamp) const response: HistoricalVolumeResponse = { data: volumeData, startTimestamp: startTimestamp !== Infinity ? startTimestamp : 0, endTimestamp: endTimestamp !== -Infinity ? endTimestamp : 0, totalPoints: volumeData.length, - }; + } return new Response(JSON.stringify(response), { headers: { "Content-Type": "application/json" }, - }); + }) } catch (error) { return new Response( JSON.stringify({ error: "Failed to fetch historical volume data" }), { status: 500, headers: { "Content-Type": "application/json" }, - } - ); + }, + ) } } diff --git a/app/api/stats/net-flow/route.ts b/app/api/stats/net-flow/route.ts index d566800a..1aebc933 100644 --- a/app/api/stats/net-flow/route.ts +++ b/app/api/stats/net-flow/route.ts @@ -1,31 +1,39 @@ -import { NET_FLOW_KEY } from "@/app/api/stats/constants" -import { kv } from "@vercel/kv" import { NextRequest } from "next/server" +import { kv } from "@vercel/kv" + +import { NET_FLOW_KEY } from "@/app/api/stats/constants" + export interface NetFlowResponse { - netFlow: number - timestamp: number + netFlow: number + timestamp: number } export const runtime = "edge" export const dynamic = "force-dynamic" export async function GET(req: NextRequest) { - try { - const data = await kv.get(NET_FLOW_KEY) - if (data) { - return new Response(JSON.stringify(data), { - headers: { "Content-Type": "application/json" }, - }) - } - return new Response(JSON.stringify({ error: "Net flow data not available" }), { - status: 404, - headers: { "Content-Type": "application/json" }, - }) - } catch (error) { - return new Response(JSON.stringify({ error: "Failed to retrieve net flow data" }), { - status: 500, - headers: { "Content-Type": "application/json" }, - }) + try { + const data = await kv.get(NET_FLOW_KEY) + if (data) { + return new Response(JSON.stringify(data), { + headers: { "Content-Type": "application/json" }, + }) } + return new Response( + JSON.stringify({ error: "Net flow data not available" }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + }, + ) + } catch (error) { + return new Response( + JSON.stringify({ error: "Failed to retrieve net flow data" }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ) + } } diff --git a/app/api/stats/set-historical-volume-kv/route.ts b/app/api/stats/set-historical-volume-kv/route.ts index 01ed2bb1..554ef634 100644 --- a/app/api/stats/set-historical-volume-kv/route.ts +++ b/app/api/stats/set-historical-volume-kv/route.ts @@ -17,27 +17,29 @@ interface VolumeData { } interface SearchParams { - to: number; - from: number; - interval: number; + to: number + from: number + interval: number } const DEFAULT_PARAMS: SearchParams = { to: Math.floor(Date.now() / 1000), from: 1693958400, interval: 24 * 60 * 60, -}; +} export async function GET(req: NextRequest) { console.log("Starting cron job: set-volume-kv") try { const ddog = new DDogClient() - const { searchParams } = new URL(req.url); + const { searchParams } = new URL(req.url) const params: SearchParams = { to: parseInt(searchParams.get("to") ?? String(DEFAULT_PARAMS.to)), from: parseInt(searchParams.get("from") ?? String(DEFAULT_PARAMS.from)), - interval: parseInt(searchParams.get("interval") ?? String(DEFAULT_PARAMS.interval)), - }; + interval: parseInt( + searchParams.get("interval") ?? String(DEFAULT_PARAMS.interval), + ), + } console.log(`Parameters: ${JSON.stringify(params)}`) @@ -52,7 +54,11 @@ export async function GET(req: NextRequest) { console.log( `Fetching match volume from ${params.from} to ${params.to} with interval ${params.interval}`, ) - const res = await ddog.getMatchVolumePerInterval(params.from, params.to, params.interval) + const res = await ddog.getMatchVolumePerInterval( + params.from, + params.to, + params.interval, + ) console.log(`DDogClient response status: ${res.status}`) if ( diff --git a/app/api/stats/set-net-flow-kv/route.ts b/app/api/stats/set-net-flow-kv/route.ts index e142f645..8e981b80 100644 --- a/app/api/stats/set-net-flow-kv/route.ts +++ b/app/api/stats/set-net-flow-kv/route.ts @@ -1,61 +1,85 @@ -import { INFLOWS_KEY, INFLOWS_SET_KEY, NET_FLOW_KEY } from "@/app/api/stats/constants" +import { kv } from "@vercel/kv" + +import { + INFLOWS_KEY, + INFLOWS_SET_KEY, + NET_FLOW_KEY, +} from "@/app/api/stats/constants" import { NetFlowResponse } from "@/app/api/stats/net-flow/route" import { getAllSetMembers } from "@/app/lib/kv-utils" -import { kv } from "@vercel/kv" const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000 // 24 hours in milliseconds export async function GET() { - console.log("Starting net flow calculation cron job") - try { - const now = Date.now() - const twentyFourHoursAgo = now - TWENTY_FOUR_HOURS - console.log(`Calculating net flow from ${new Date(twentyFourHoursAgo).toISOString()} to ${new Date(now).toISOString()}`) - - const transactionHashes = await getAllSetMembers(kv, INFLOWS_SET_KEY) - console.log(`Retrieved ${transactionHashes.length} transaction hashes`) - - const pipeline = kv.pipeline() - transactionHashes.forEach(hash => pipeline.get(`${INFLOWS_KEY}:${hash}`)) - const data = await pipeline.exec() - console.log(`Fetched data for ${data.length} transactions`) - - let netFlow = 0 - let validTransactions = 0 - let skippedTransactions = 0 - - data.forEach(item => { - if (item && typeof item === "object" && "timestamp" in item && "amount" in item) { - const transfer = item as { timestamp: number; amount: number; isWithdrawal: boolean } - if (transfer.timestamp >= twentyFourHoursAgo) { - netFlow += transfer.isWithdrawal ? -transfer.amount : transfer.amount - validTransactions++ - } else { - skippedTransactions++ - } - } - }) - - console.log(`Processed ${validTransactions} valid transactions, skipped ${skippedTransactions} outdated transactions`) - console.log(`Calculated net flow: ${netFlow}`) - - const response: NetFlowResponse = { - netFlow, - timestamp: now, + console.log("Starting net flow calculation cron job") + try { + const now = Date.now() + const twentyFourHoursAgo = now - TWENTY_FOUR_HOURS + console.log( + `Calculating net flow from ${new Date(twentyFourHoursAgo).toISOString()} to ${new Date(now).toISOString()}`, + ) + + const transactionHashes = await getAllSetMembers(kv, INFLOWS_SET_KEY) + console.log(`Retrieved ${transactionHashes.length} transaction hashes`) + + const pipeline = kv.pipeline() + transactionHashes.forEach((hash) => pipeline.get(`${INFLOWS_KEY}:${hash}`)) + const data = await pipeline.exec() + console.log(`Fetched data for ${data.length} transactions`) + + let netFlow = 0 + let validTransactions = 0 + let skippedTransactions = 0 + + data.forEach((item) => { + if ( + item && + typeof item === "object" && + "timestamp" in item && + "amount" in item + ) { + const transfer = item as { + timestamp: number + amount: number + isWithdrawal: boolean + } + if (transfer.timestamp >= twentyFourHoursAgo) { + netFlow += transfer.isWithdrawal ? -transfer.amount : transfer.amount + validTransactions++ + } else { + skippedTransactions++ } + } + }) + + console.log( + `Processed ${validTransactions} valid transactions, skipped ${skippedTransactions} outdated transactions`, + ) + console.log(`Calculated net flow: ${netFlow}`) - await kv.set(NET_FLOW_KEY, response) - console.log("Net flow data updated successfully") - - return new Response(JSON.stringify({ message: "Net flow data updated successfully" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }) - } catch (error) { - console.error("Error updating net flow data:", error) - return new Response(JSON.stringify({ error: "Failed to update net flow data" }), { - status: 500, - headers: { "Content-Type": "application/json" }, - }) + const response: NetFlowResponse = { + netFlow, + timestamp: now, } -} \ No newline at end of file + + await kv.set(NET_FLOW_KEY, response) + console.log("Net flow data updated successfully") + + return new Response( + JSON.stringify({ message: "Net flow data updated successfully" }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ) + } catch (error) { + console.error("Error updating net flow data:", error) + return new Response( + JSON.stringify({ error: "Failed to update net flow data" }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ) + } +} diff --git a/app/hooks/useNetFlow.ts b/app/hooks/useNetFlow.ts index 3b6752cc..91ec265e 100644 --- a/app/hooks/useNetFlow.ts +++ b/app/hooks/useNetFlow.ts @@ -1,17 +1,18 @@ -import { NetFlowResponse } from '@/app/api/stats/net-flow/route' -import { useQuery } from '@tanstack/react-query' +import { useQuery } from "@tanstack/react-query" + +import { NetFlowResponse } from "@/app/api/stats/net-flow/route" async function fetchNetFlow(): Promise { - const response = await fetch('/api/stats/net-flow') - if (!response.ok) { - throw new Error('Failed to fetch net flow data') - } - return response.json() + const response = await fetch("/api/stats/net-flow") + if (!response.ok) { + throw new Error("Failed to fetch net flow data") + } + return response.json() } export function useNetFlow() { - return useQuery({ - queryKey: ['stats', 'netFlow'], - queryFn: fetchNetFlow, - }) -} \ No newline at end of file + return useQuery({ + queryKey: ["stats", "netFlow"], + queryFn: fetchNetFlow, + }) +} diff --git a/app/lib/kv-utils.ts b/app/lib/kv-utils.ts index d4d9a163..868c0578 100644 --- a/app/lib/kv-utils.ts +++ b/app/lib/kv-utils.ts @@ -1,12 +1,22 @@ -import { VercelKV } from "@vercel/kv"; +import { VercelKV } from "@vercel/kv" -export async function getAllSetMembers(kv: VercelKV, key: string, batchSize: number = 100): Promise { - let cursor = 0; - const allMembers: string[] = []; - do { - const [nextCursor, members] = await kv.sscan(key, cursor.toString(), { count: batchSize }); - cursor = typeof nextCursor === 'string' ? parseInt(nextCursor) : nextCursor; - allMembers.push(...members.filter((member): member is string => typeof member === 'string')); - } while (cursor !== 0); - return allMembers; -} \ No newline at end of file +export async function getAllSetMembers( + kv: VercelKV, + key: string, + batchSize: number = 100, +): Promise { + let cursor = 0 + const allMembers: string[] = [] + do { + const [nextCursor, members] = await kv.sscan(key, cursor.toString(), { + count: batchSize, + }) + cursor = typeof nextCursor === "string" ? parseInt(nextCursor) : nextCursor + allMembers.push( + ...members.filter( + (member): member is string => typeof member === "string", + ), + ) + } while (cursor !== 0) + return allMembers +} diff --git a/app/stats/hooks/use-external-transfer-data.ts b/app/stats/hooks/use-external-transfer-data.ts index 6dddd629..764e111e 100644 --- a/app/stats/hooks/use-external-transfer-data.ts +++ b/app/stats/hooks/use-external-transfer-data.ts @@ -19,7 +19,9 @@ export function useExternalTransferLogs(intervalMs: number = 86400000) { const fetchExternalTransferLogs = async ( intervalMs: number, ): Promise => { - const response = await fetch(`/api/stats/external-transfer-logs?interval=${intervalMs}`) + const response = await fetch( + `/api/stats/external-transfer-logs?interval=${intervalMs}`, + ) if (!response.ok) { throw new Error("Failed to fetch external transfer logs") } diff --git a/vercel.json b/vercel.json index 46f2b016..7de771c7 100644 --- a/vercel.json +++ b/vercel.json @@ -14,7 +14,7 @@ } ], "git": { - "deploymentEnabled":{ + "deploymentEnabled": { "main": false } }