Merge branch 'outpoot:main' into main

This commit is contained in:
Max 2025-06-15 16:48:46 +02:00 committed by GitHub
commit 5e507c3df2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 6196 additions and 109 deletions

View file

@ -49,8 +49,7 @@ async function handleIconUpload(iconFile: File | null, symbol: string): Promise<
return await uploadCoinIcon(
symbol,
new Uint8Array(arrayBuffer),
iconFile.type,
iconFile.size
iconFile.type
);
}

View file

@ -2,7 +2,7 @@ import { auth } from '$lib/auth';
import { json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { predictionQuestion, user, predictionBet } from '$lib/server/db/schema';
import { eq, desc, and, sum, count } from 'drizzle-orm';
import { eq, desc, and, sum, count, or } from 'drizzle-orm';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url, request }) => {
@ -24,9 +24,22 @@ export const GET: RequestHandler = async ({ url, request }) => {
const userId = session?.user ? Number(session.user.id) : null;
try {
let statusFilter;
if (status === 'ACTIVE') {
statusFilter = eq(predictionQuestion.status, 'ACTIVE');
} else if (status === 'RESOLVED') {
statusFilter = or(
eq(predictionQuestion.status, 'RESOLVED'),
eq(predictionQuestion.status, 'CANCELLED')
);
} else {
statusFilter = undefined;
}
const conditions = [];
if (status !== 'ALL') {
conditions.push(eq(predictionQuestion.status, status as any));
if (statusFilter) {
conditions.push(statusFilter);
}
const whereCondition = conditions.length > 0 ? and(...conditions) : undefined;

View file

@ -44,28 +44,52 @@ export async function GET({ request }) {
const value = quantity * price;
totalCoinValue += value;
// Calculate average purchase price from buy transactions
const avgPriceResult = await db.select({
avgPrice: sql<number>`
CASE
WHEN SUM(${transaction.quantity}) > 0
THEN SUM(${transaction.totalBaseCurrencyAmount}) / SUM(${transaction.quantity})
ELSE 0
END
`
const allTransactions = await db.select({
type: transaction.type,
quantity: transaction.quantity,
totalBaseCurrencyAmount: transaction.totalBaseCurrencyAmount,
timestamp: transaction.timestamp
})
.from(transaction)
.where(
and(
eq(transaction.userId, userId),
eq(transaction.coinId, holding.coinId),
eq(transaction.type, 'BUY')
.from(transaction)
.where(
and(
eq(transaction.userId, userId),
eq(transaction.coinId, holding.coinId)
)
)
);
.orderBy(transaction.timestamp);
const avgPurchasePrice = Number(avgPriceResult[0]?.avgPrice || 0);
const percentageChange = avgPurchasePrice > 0
? ((price - avgPurchasePrice) / avgPurchasePrice) * 100
// calculate cost basis
let remainingQuantity = quantity;
let totalCostBasis = 0;
let runningQuantity = 0;
for (const tx of allTransactions) {
const txQuantity = Number(tx.quantity);
const txAmount = Number(tx.totalBaseCurrencyAmount);
if (tx.type === 'BUY') {
runningQuantity += txQuantity;
// if we still need to account for held coins
if (remainingQuantity > 0) {
const quantityToAttribute = Math.min(txQuantity, remainingQuantity);
const avgPrice = txAmount / txQuantity;
totalCostBasis += quantityToAttribute * avgPrice;
remainingQuantity -= quantityToAttribute;
}
} else if (tx.type === 'SELL') {
runningQuantity -= txQuantity;
}
// if we accounted for all held coins, break
if (remainingQuantity <= 0) break;
}
const avgPurchasePrice = quantity > 0 ? totalCostBasis / quantity : 0;
const percentageChange = totalCostBasis > 0
? ((value - totalCostBasis) / totalCostBasis) * 100
: 0;
return {
@ -76,7 +100,8 @@ export async function GET({ request }) {
value,
change24h: Number(holding.change24h),
avgPurchasePrice,
percentageChange
percentageChange,
costBasis: totalCostBasis
};
}));

View file

@ -0,0 +1,171 @@
import { auth } from '$lib/auth';
import { error, json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { user, userPortfolio, transaction, notifications, coin } from '$lib/server/db/schema';
import { eq, sql } from 'drizzle-orm';
import type { RequestHandler } from './$types';
import { formatValue, getPrestigeCost, getPrestigeName } from '$lib/utils';
export const POST: RequestHandler = async ({ request, locals }) => {
const session = await auth.api.getSession({ headers: request.headers });
if (!session?.user) throw error(401, 'Not authenticated');
const userId = Number(session.user.id);
return await db.transaction(async (tx) => {
const [userData] = await tx
.select({
baseCurrencyBalance: user.baseCurrencyBalance,
prestigeLevel: user.prestigeLevel
})
.from(user)
.where(eq(user.id, userId))
.for('update')
.limit(1);
if (!userData) throw error(404, 'User not found');
const currentPrestige = userData.prestigeLevel || 0;
const nextPrestige = currentPrestige + 1;
const prestigeCost = getPrestigeCost(nextPrestige);
const prestigeName = getPrestigeName(nextPrestige);
if (!prestigeCost || !prestigeName) {
throw error(400, 'Maximum prestige level reached');
}
const holdings = await tx
.select({
coinId: userPortfolio.coinId,
quantity: userPortfolio.quantity,
currentPrice: coin.currentPrice,
symbol: coin.symbol
})
.from(userPortfolio)
.leftJoin(coin, eq(userPortfolio.coinId, coin.id))
.where(eq(userPortfolio.userId, userId));
let warningMessage = '';
let totalSaleValue = 0;
if (holdings.length > 0) {
warningMessage = `All ${holdings.length} coin holdings have been sold at current market prices. `;
for (const holding of holdings) {
const quantity = Number(holding.quantity);
const price = Number(holding.currentPrice);
const saleValue = quantity * price;
totalSaleValue += saleValue;
await tx.insert(transaction).values({
coinId: holding.coinId!,
type: 'SELL',
quantity: holding.quantity,
pricePerCoin: holding.currentPrice || '0',
totalBaseCurrencyAmount: saleValue.toString(),
timestamp: new Date()
});
}
await tx
.delete(userPortfolio)
.where(eq(userPortfolio.userId, userId));
}
const currentBalance = Number(userData.baseCurrencyBalance) + totalSaleValue;
if (currentBalance < prestigeCost) {
throw error(400, `Insufficient funds. Need ${formatValue(prestigeCost)}, have ${formatValue(currentBalance)}`);
}
await tx
.update(user)
.set({
baseCurrencyBalance: '100.00000000',
prestigeLevel: nextPrestige,
updatedAt: new Date()
})
.where(eq(user.id, userId));
await tx.delete(userPortfolio).where(eq(userPortfolio.userId, userId));
await tx.insert(notifications).values({
userId: userId,
type: 'SYSTEM',
title: `${prestigeName} Achieved!`,
message: `Congratulations! You have successfully reached ${prestigeName}. Your portfolio has been reset and you can now start fresh with your new prestige badge.`,
});
return json({
success: true,
newPrestigeLevel: nextPrestige,
costPaid: prestigeCost,
coinsSold: holdings.length,
totalSaleValue,
message: `${warningMessage}Congratulations! You've reached Prestige ${nextPrestige}!`
});
});
};
export const GET: RequestHandler = async ({ request }) => {
const session = await auth.api.getSession({ headers: request.headers });
if (!session?.user) throw error(401, 'Not authenticated');
const userId = Number(session.user.id);
const [userProfile] = await db
.select({
id: user.id,
name: user.name,
username: user.username,
bio: user.bio,
image: user.image,
createdAt: user.createdAt,
baseCurrencyBalance: user.baseCurrencyBalance,
isAdmin: user.isAdmin,
loginStreak: user.loginStreak,
prestigeLevel: user.prestigeLevel
})
.from(user)
.where(eq(user.id, userId))
.limit(1);
if (!userProfile) {
throw error(404, 'User not found');
}
const [portfolioStats] = await db
.select({
holdingsCount: sql<number>`COUNT(*)`,
holdingsValue: sql<number>`COALESCE(SUM(CAST(${userPortfolio.quantity} AS NUMERIC) * CAST(${coin.currentPrice} AS NUMERIC)), 0)`
})
.from(userPortfolio)
.leftJoin(coin, eq(userPortfolio.coinId, coin.id))
.where(eq(userPortfolio.userId, userId));
const baseCurrencyBalance = Number(userProfile.baseCurrencyBalance);
const holdingsValue = Number(portfolioStats?.holdingsValue || 0);
const holdingsCount = Number(portfolioStats?.holdingsCount || 0);
const totalPortfolioValue = baseCurrencyBalance + holdingsValue;
return json({
profile: {
...userProfile,
baseCurrencyBalance,
totalPortfolioValue,
prestigeLevel: userProfile.prestigeLevel || 0
},
stats: {
totalPortfolioValue,
baseCurrencyBalance,
holdingsValue,
holdingsCount,
coinsCreated: 0,
totalTransactions: 0,
totalBuyVolume: 0,
totalSellVolume: 0,
transactions24h: 0,
buyVolume24h: 0,
sellVolume24h: 0
}
});
};

View file

@ -91,8 +91,7 @@ export async function POST({ request }) {
const key = await uploadProfilePicture(
session.user.id,
new Uint8Array(arrayBuffer),
avatarFile.type,
avatarFile.size
avatarFile.type
);
updates.image = key;
} catch (e) {

View file

@ -25,6 +25,7 @@ export async function GET({ params }) {
baseCurrencyBalance: true,
isAdmin: true,
loginStreak: true,
prestigeLevel: true,
}
});