diff --git a/website/src/lib/server/amm.ts b/website/src/lib/server/amm.ts new file mode 100644 index 0000000..92c7c1c --- /dev/null +++ b/website/src/lib/server/amm.ts @@ -0,0 +1,142 @@ +import { db } from '$lib/server/db'; +import { coin, transaction, priceHistory, userPortfolio } from '$lib/server/db/schema'; +import { eq, and, gte } from 'drizzle-orm'; +import { createNotification } from '$lib/server/notification'; + +export async function calculate24hMetrics(coinId: number, currentPrice: number) { + const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + + const [priceData] = await db + .select({ price: priceHistory.price }) + .from(priceHistory) + .where(and( + eq(priceHistory.coinId, coinId), + gte(priceHistory.timestamp, twentyFourHoursAgo) + )) + .orderBy(priceHistory.timestamp) + .limit(1); + + let change24h = 0; + if (priceData) { + const priceFrom24hAgo = Number(priceData.price); + if (priceFrom24hAgo > 0) { + change24h = ((currentPrice - priceFrom24hAgo) / priceFrom24hAgo) * 100; + } + } + + const volumeData = await db + .select({ totalBaseCurrencyAmount: transaction.totalBaseCurrencyAmount }) + .from(transaction) + .where(and( + eq(transaction.coinId, coinId), + gte(transaction.timestamp, twentyFourHoursAgo) + )); + + const volume24h = volumeData.reduce((sum, tx) => sum + Number(tx.totalBaseCurrencyAmount), 0); + + return { change24h: Number(change24h.toFixed(4)), volume24h: Number(volume24h.toFixed(4)) }; +} + +export async function executeSellTrade( + tx: any, + coinData: any, + userId: number, + quantity: number +) { + const poolCoinAmount = Number(coinData.poolCoinAmount); + const poolBaseCurrencyAmount = Number(coinData.poolBaseCurrencyAmount); + const currentPrice = Number(coinData.currentPrice); + + if (poolCoinAmount <= 0 || poolBaseCurrencyAmount <= 0) { + throw new Error('Liquidity pool is not properly initialized or is empty'); + } + + const k = poolCoinAmount * poolBaseCurrencyAmount; + const newPoolCoin = poolCoinAmount + quantity; + const newPoolBaseCurrency = k / newPoolCoin; + const baseCurrencyReceived = poolBaseCurrencyAmount - newPoolBaseCurrency; + const newPrice = newPoolBaseCurrency / newPoolCoin; + + if (baseCurrencyReceived <= 0 || newPoolBaseCurrency < 1) { + const fallbackValue = quantity * currentPrice; + return { + success: false, + fallbackValue, + newPrice: currentPrice, + priceImpact: 0 + }; + } + + const priceImpact = ((newPrice - currentPrice) / currentPrice) * 100; + + await tx.insert(transaction).values({ + userId, + coinId: coinData.id, + type: 'SELL', + quantity: quantity.toString(), + pricePerCoin: (baseCurrencyReceived / quantity).toString(), + totalBaseCurrencyAmount: baseCurrencyReceived.toString(), + timestamp: new Date() + }); + + await tx.insert(priceHistory).values({ + coinId: coinData.id, + price: newPrice.toString() + }); + + const metrics = await calculate24hMetrics(coinData.id, newPrice); + + await tx.update(coin) + .set({ + currentPrice: newPrice.toString(), + marketCap: (Number(coinData.circulatingSupply) * newPrice).toString(), + poolCoinAmount: newPoolCoin.toString(), + poolBaseCurrencyAmount: newPoolBaseCurrency.toString(), + change24h: metrics.change24h.toString(), + volume24h: metrics.volume24h.toString(), + updatedAt: new Date() + }) + .where(eq(coin.id, coinData.id)); + + const isRugPull = priceImpact < -20 && baseCurrencyReceived > 1000; + if (isRugPull) { + (async () => { + try { + const affectedUsers = await db + .select({ + userId: userPortfolio.userId, + quantity: userPortfolio.quantity + }) + .from(userPortfolio) + .where(eq(userPortfolio.coinId, coinData.id)); + + for (const holder of affectedUsers) { + if (holder.userId === userId) continue; + + const holdingValue = Number(holder.quantity) * newPrice; + if (holdingValue > 10) { + await createNotification( + holder.userId.toString(), + 'RUG_PULL', + 'Coin rugpulled!', + `A coin you owned, ${coinData.name} (*${coinData.symbol}), crashed ${Math.abs(priceImpact).toFixed(1)}%!`, + `/coin/${coinData.symbol}` + ); + } + } + } catch (error) { + console.error('Error sending rug pull notifications:', error); + } + })(); + } + + return { + success: true, + baseCurrencyReceived, + newPrice, + priceImpact, + newPoolCoin, + newPoolBaseCurrency, + metrics + }; +} diff --git a/website/src/routes/api/coin/[coinSymbol]/trade/+server.ts b/website/src/routes/api/coin/[coinSymbol]/trade/+server.ts index ac2a531..91ac60a 100644 --- a/website/src/routes/api/coin/[coinSymbol]/trade/+server.ts +++ b/website/src/routes/api/coin/[coinSymbol]/trade/+server.ts @@ -5,43 +5,7 @@ import { coin, userPortfolio, user, transaction, priceHistory } from '$lib/serve import { eq, and, gte } from 'drizzle-orm'; import { redis } from '$lib/server/redis'; import { createNotification } from '$lib/server/notification'; - -async function calculate24hMetrics(coinId: number, currentPrice: number) { - const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); - - // Get price from 24h ago - const [priceData] = await db - .select({ price: priceHistory.price }) - .from(priceHistory) - .where(and( - eq(priceHistory.coinId, coinId), - gte(priceHistory.timestamp, twentyFourHoursAgo) - )) - .orderBy(priceHistory.timestamp) - .limit(1); - - // Calculate 24h change - let change24h = 0; - if (priceData) { - const priceFrom24hAgo = Number(priceData.price); - if (priceFrom24hAgo > 0) { - change24h = ((currentPrice - priceFrom24hAgo) / priceFrom24hAgo) * 100; - } - } - - // Calculate 24h volume - const volumeData = await db - .select({ totalBaseCurrencyAmount: transaction.totalBaseCurrencyAmount }) - .from(transaction) - .where(and( - eq(transaction.coinId, coinId), - gte(transaction.timestamp, twentyFourHoursAgo) - )); - - const volume24h = volumeData.reduce((sum, tx) => sum + Number(tx.totalBaseCurrencyAmount), 0); - - return { change24h: Number(change24h.toFixed(4)), volume24h: Number(volume24h.toFixed(4)) }; -} +import { calculate24hMetrics, executeSellTrade } from '$lib/server/amm'; export async function POST({ params, request }) { const session = await auth.api.getSession({ @@ -254,24 +218,21 @@ export async function POST({ params, request }) { } // Allow more aggressive selling for rug pull simulation - prevent only mathematical breakdown - const maxSellable = Math.floor(poolCoinAmount * 0.995); + const maxSellable = Math.floor(Number(coinData.poolCoinAmount) * 0.995); if (amount > maxSellable) { throw error(400, `Cannot sell more than 99.5% of pool tokens. Max sellable: ${maxSellable} tokens`); } - const k = poolCoinAmount * poolBaseCurrencyAmount; - const newPoolCoin = poolCoinAmount + amount; - const newPoolBaseCurrency = k / newPoolCoin; - const baseCurrencyReceived = poolBaseCurrencyAmount - newPoolBaseCurrency; + const sellResult = await executeSellTrade(tx, coinData, userId, amount); - totalCost = baseCurrencyReceived; - newPrice = newPoolBaseCurrency / newPoolCoin; - priceImpact = ((newPrice - currentPrice) / currentPrice) * 100; - - if (newPoolBaseCurrency < 10) { - throw error(400, `Trade would drain pool below minimum liquidity (*10 BUSS). Try selling fewer tokens.`); + if (!sellResult.success) { + throw error(400, 'Trade failed - insufficient liquidity or invalid parameters'); } + totalCost = sellResult.baseCurrencyReceived ?? 0; + newPrice = sellResult.newPrice; + priceImpact = sellResult.priceImpact; + if (totalCost <= 0) { throw error(400, 'Trade amount results in zero base currency received'); } @@ -302,72 +263,15 @@ export async function POST({ params, request }) { )); } - await tx.insert(transaction).values({ - userId, - coinId: coinData.id, - type: 'SELL', - quantity: amount.toString(), - pricePerCoin: (totalCost / amount).toString(), - totalBaseCurrencyAmount: totalCost.toString() - }); - - await tx.insert(priceHistory).values({ - coinId: coinData.id, - price: newPrice.toString() - }); - - const metrics = await calculate24hMetrics(coinData.id, newPrice); - - await tx.update(coin) - .set({ - currentPrice: newPrice.toString(), - marketCap: (Number(coinData.circulatingSupply) * newPrice).toString(), - poolCoinAmount: newPoolCoin.toString(), - poolBaseCurrencyAmount: newPoolBaseCurrency.toString(), - change24h: metrics.change24h.toString(), - volume24h: metrics.volume24h.toString(), - updatedAt: new Date() - }) - .where(eq(coin.id, coinData.id)); - - const isRugPull = priceImpact < -20 && totalCost > 1000; - - // Send rug pull notifications to affected users - if (isRugPull) { - (async () => { - const affectedUsers = await db - .select({ - userId: userPortfolio.userId, - quantity: userPortfolio.quantity - }) - .from(userPortfolio) - .where(eq(userPortfolio.coinId, coinData.id)); - - for (const holder of affectedUsers) { - if (holder.userId === userId) continue; - - const holdingValue = Number(holder.quantity) * newPrice; - if (holdingValue > 10) { - const lossAmount = Number(holder.quantity) * (currentPrice - newPrice); - await createNotification( - holder.userId.toString(), - 'RUG_PULL', - 'Coin rugpulled!', - `A coin you owned, ${coinData.name} (*${normalizedSymbol}), crashed ${Math.abs(priceImpact).toFixed(1)}%!`, - `/coin/${normalizedSymbol}` - ); - } - } - })(); - } + const metrics = sellResult.metrics || await calculate24hMetrics(coinData.id, newPrice); const priceUpdateData = { currentPrice: newPrice, marketCap: Number(coinData.circulatingSupply) * newPrice, change24h: metrics.change24h, volume24h: metrics.volume24h, - poolCoinAmount: newPoolCoin, - poolBaseCurrencyAmount: newPoolBaseCurrency + poolCoinAmount: sellResult.newPoolCoin, + poolBaseCurrencyAmount: sellResult.newPoolBaseCurrency }; const tradeData = { @@ -409,4 +313,4 @@ export async function POST({ params, request }) { }); } }); -} +} \ No newline at end of file diff --git a/website/src/routes/api/prestige/+server.ts b/website/src/routes/api/prestige/+server.ts index cb3e018..e60bb73 100644 --- a/website/src/routes/api/prestige/+server.ts +++ b/website/src/routes/api/prestige/+server.ts @@ -5,6 +5,7 @@ import { user, userPortfolio, transaction, notifications, coin } from '$lib/serv import { eq, sql } from 'drizzle-orm'; import type { RequestHandler } from './$types'; import { formatValue, getPrestigeCost, getPrestigeName } from '$lib/utils'; +import { executeSellTrade } from '$lib/server/amm'; export const POST: RequestHandler = async ({ request, locals }) => { const session = await auth.api.getSession({ headers: request.headers }); @@ -44,7 +45,10 @@ export const POST: RequestHandler = async ({ request, locals }) => { coinId: userPortfolio.coinId, quantity: userPortfolio.quantity, currentPrice: coin.currentPrice, - symbol: coin.symbol + symbol: coin.symbol, + poolCoinAmount: coin.poolCoinAmount, + poolBaseCurrencyAmount: coin.poolBaseCurrencyAmount, + circulatingSupply: coin.circulatingSupply }) .from(userPortfolio) .leftJoin(coin, eq(userPortfolio.coinId, coin.id)) @@ -58,18 +62,37 @@ export const POST: RequestHandler = async ({ request, locals }) => { for (const holding of holdings) { const quantity = Number(holding.quantity); - const price = Number(holding.currentPrice); - const saleValue = quantity * price; - totalSaleValue += saleValue; + const currentPrice = Number(holding.currentPrice); - await tx.insert(transaction).values({ - coinId: holding.coinId!, - type: 'SELL', - quantity: holding.quantity, - pricePerCoin: holding.currentPrice || '0', - totalBaseCurrencyAmount: saleValue.toString(), - timestamp: new Date() - }); + if (Number(holding.poolCoinAmount) <= 0 || Number(holding.poolBaseCurrencyAmount) <= 0) { + const fallbackValue = quantity * currentPrice; + totalSaleValue += fallbackValue; + + await tx.insert(transaction).values({ + userId, + coinId: holding.coinId!, + type: 'SELL', + quantity: holding.quantity, + pricePerCoin: holding.currentPrice || '0', + totalBaseCurrencyAmount: fallbackValue.toString(), + timestamp: new Date() + }); + continue; + } + + const sellResult = await executeSellTrade(tx, { + id: holding.coinId, + poolCoinAmount: holding.poolCoinAmount, + poolBaseCurrencyAmount: holding.poolBaseCurrencyAmount, + currentPrice: holding.currentPrice, + circulatingSupply: holding.circulatingSupply + }, userId, quantity); + + if (sellResult.success && sellResult.baseCurrencyReceived) { + totalSaleValue += sellResult.baseCurrencyReceived; + } else { + totalSaleValue += sellResult.fallbackValue || (quantity * currentPrice); + } } await tx