feat: implement leaderboard page with top performers and market activity
This commit is contained in:
parent
930d1f41d7
commit
35237c3470
7 changed files with 605 additions and 66 deletions
185
website/src/routes/api/leaderboard/+server.ts
Normal file
185
website/src/routes/api/leaderboard/+server.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { user, transaction, userPortfolio, coin } from '$lib/server/db/schema';
|
||||
import { eq, desc, gte, and, sql, inArray } from 'drizzle-orm';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const topRugpullers = await db
|
||||
.select({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
image: user.image,
|
||||
totalSold: sql<number>`COALESCE(SUM(CASE WHEN ${transaction.type} = 'SELL' THEN CAST(${transaction.totalBaseCurrencyAmount} AS NUMERIC) ELSE 0 END), 0)`,
|
||||
totalBought: sql<number>`COALESCE(SUM(CASE WHEN ${transaction.type} = 'BUY' THEN CAST(${transaction.totalBaseCurrencyAmount} AS NUMERIC) ELSE 0 END), 0)`
|
||||
})
|
||||
.from(transaction)
|
||||
.innerJoin(user, eq(transaction.userId, user.id))
|
||||
.where(gte(transaction.timestamp, twentyFourHoursAgo))
|
||||
.groupBy(user.id, user.username, user.name, user.image)
|
||||
.orderBy(desc(sql`SUM(CASE WHEN ${transaction.type} = 'SELL' THEN CAST(${transaction.totalBaseCurrencyAmount} AS NUMERIC) ELSE 0 END) - SUM(CASE WHEN ${transaction.type} = 'BUY' THEN CAST(${transaction.totalBaseCurrencyAmount} AS NUMERIC) ELSE 0 END)`))
|
||||
.limit(10);
|
||||
|
||||
const userTransactions = await db
|
||||
.select({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
image: user.image,
|
||||
type: transaction.type,
|
||||
coinId: transaction.coinId,
|
||||
totalAmount: sql<number>`CAST(${transaction.totalBaseCurrencyAmount} AS NUMERIC)`,
|
||||
quantity: sql<number>`CAST(${transaction.quantity} AS NUMERIC)`
|
||||
})
|
||||
.from(transaction)
|
||||
.innerJoin(user, eq(transaction.userId, user.id))
|
||||
.where(gte(transaction.timestamp, twentyFourHoursAgo));
|
||||
|
||||
const userNetCalculations = new Map();
|
||||
for (const tx of userTransactions) {
|
||||
if (!userNetCalculations.has(tx.userId)) {
|
||||
userNetCalculations.set(tx.userId, {
|
||||
userId: tx.userId,
|
||||
username: tx.username,
|
||||
name: tx.name,
|
||||
image: tx.image,
|
||||
totalBought: 0,
|
||||
totalSold: 0,
|
||||
coinHoldings: new Map()
|
||||
});
|
||||
}
|
||||
|
||||
const userData = userNetCalculations.get(tx.userId);
|
||||
if (tx.type === 'BUY') {
|
||||
userData.totalBought += Number(tx.totalAmount);
|
||||
const currentHolding = userData.coinHoldings.get(tx.coinId) || 0;
|
||||
userData.coinHoldings.set(tx.coinId, currentHolding + Number(tx.quantity));
|
||||
} else {
|
||||
userData.totalSold += Number(tx.totalAmount);
|
||||
const currentHolding = userData.coinHoldings.get(tx.coinId) || 0;
|
||||
userData.coinHoldings.set(tx.coinId, currentHolding - Number(tx.quantity));
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all unique coin IDs
|
||||
const uniqueCoinIds = new Set();
|
||||
for (const userData of userNetCalculations.values()) {
|
||||
for (const [coinId] of userData.coinHoldings.entries()) {
|
||||
uniqueCoinIds.add(coinId);
|
||||
}
|
||||
} // Batch fetch all coin prices
|
||||
const coinPrices = new Map();
|
||||
if (uniqueCoinIds.size > 0) {
|
||||
const coinPricesData = await db
|
||||
.select({ id: coin.id, currentPrice: coin.currentPrice })
|
||||
.from(coin)
|
||||
.where(inArray(coin.id, Array.from(uniqueCoinIds) as number[]));
|
||||
|
||||
for (const coinData of coinPricesData) {
|
||||
coinPrices.set(coinData.id, Number(coinData.currentPrice || 0));
|
||||
}
|
||||
}
|
||||
|
||||
const biggestLosersData = [];
|
||||
for (const userData of userNetCalculations.values()) {
|
||||
let currentHoldingsValue = 0;
|
||||
|
||||
for (const [coinId, quantity] of userData.coinHoldings.entries()) {
|
||||
if (quantity > 0) {
|
||||
const price = coinPrices.get(coinId) || 0;
|
||||
currentHoldingsValue += quantity * price;
|
||||
}
|
||||
}
|
||||
|
||||
const netLoss = userData.totalBought - userData.totalSold - currentHoldingsValue;
|
||||
if (netLoss > 0) {
|
||||
biggestLosersData.push({
|
||||
userId: userData.userId,
|
||||
username: userData.username,
|
||||
name: userData.name,
|
||||
image: userData.image,
|
||||
moneySpent: userData.totalBought,
|
||||
moneyReceived: userData.totalSold,
|
||||
currentValue: currentHoldingsValue,
|
||||
totalLoss: netLoss
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const [cashKings, paperMillionaires] = await Promise.all([
|
||||
db.select({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
image: user.image,
|
||||
baseCurrencyBalance: user.baseCurrencyBalance,
|
||||
coinValue: sql<number>`COALESCE(SUM(CAST(${userPortfolio.quantity} AS NUMERIC) * CAST(${coin.currentPrice} AS NUMERIC)), 0)`
|
||||
})
|
||||
.from(user)
|
||||
.leftJoin(userPortfolio, eq(userPortfolio.userId, user.id))
|
||||
.leftJoin(coin, eq(coin.id, userPortfolio.coinId))
|
||||
.groupBy(user.id, user.username, user.name, user.image, user.baseCurrencyBalance)
|
||||
.orderBy(desc(sql`CAST(${user.baseCurrencyBalance} AS NUMERIC)`))
|
||||
.limit(10),
|
||||
|
||||
db.select({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
image: user.image,
|
||||
baseCurrencyBalance: user.baseCurrencyBalance,
|
||||
coinValue: sql<number>`COALESCE(SUM(CAST(${userPortfolio.quantity} AS NUMERIC) * CAST(${coin.currentPrice} AS NUMERIC)), 0)`
|
||||
})
|
||||
.from(user)
|
||||
.leftJoin(userPortfolio, eq(userPortfolio.userId, user.id))
|
||||
.leftJoin(coin, eq(coin.id, userPortfolio.coinId))
|
||||
.groupBy(user.id, user.username, user.name, user.image, user.baseCurrencyBalance)
|
||||
.orderBy(desc(sql`CAST(${user.baseCurrencyBalance} AS NUMERIC) + COALESCE(SUM(CAST(${userPortfolio.quantity} AS NUMERIC) * CAST(${coin.currentPrice} AS NUMERIC)), 0)`))
|
||||
.limit(10)
|
||||
]);
|
||||
|
||||
const processUser = (user: any) => {
|
||||
const baseCurrencyBalance = Number(user.baseCurrencyBalance);
|
||||
const coinValue = Number(user.coinValue);
|
||||
const totalPortfolioValue = baseCurrencyBalance + coinValue;
|
||||
|
||||
return {
|
||||
...user,
|
||||
baseCurrencyBalance,
|
||||
coinValue,
|
||||
totalPortfolioValue,
|
||||
liquidityRatio: totalPortfolioValue > 0 ? baseCurrencyBalance / totalPortfolioValue : 0
|
||||
};
|
||||
};
|
||||
|
||||
const processedRugpullers = topRugpullers
|
||||
.map(user => ({ ...user, totalExtracted: Number(user.totalSold) - Number(user.totalBought) }))
|
||||
.filter(user => user.totalExtracted > 0);
|
||||
|
||||
const aggregatedLosers = biggestLosersData
|
||||
.sort((a, b) => b.totalLoss - a.totalLoss)
|
||||
.slice(0, 10);
|
||||
|
||||
const processedCashKings = cashKings.map(processUser);
|
||||
const processedPaperMillionaires = paperMillionaires.map(processUser);
|
||||
|
||||
return json({
|
||||
topRugpullers: processedRugpullers,
|
||||
biggestLosers: aggregatedLosers,
|
||||
cashKings: processedCashKings,
|
||||
paperMillionaires: processedPaperMillionaires
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch leaderboard data:', error);
|
||||
return json({
|
||||
topRugpullers: [],
|
||||
biggestLosers: [],
|
||||
cashKings: [],
|
||||
paperMillionaires: []
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in a new issue