fix: sell all coin holdings instead of disowning in prestige

This commit is contained in:
Face 2025-07-15 18:59:47 +03:00
parent 32bfbf816c
commit edca4d5ceb
3 changed files with 190 additions and 121 deletions

View file

@ -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
};
}

View file

@ -5,43 +5,7 @@ import { coin, userPortfolio, user, transaction, priceHistory } from '$lib/serve
import { eq, and, gte } from 'drizzle-orm'; import { eq, and, gte } from 'drizzle-orm';
import { redis } from '$lib/server/redis'; import { redis } from '$lib/server/redis';
import { createNotification } from '$lib/server/notification'; import { createNotification } from '$lib/server/notification';
import { calculate24hMetrics, executeSellTrade } from '$lib/server/amm';
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)) };
}
export async function POST({ params, request }) { export async function POST({ params, request }) {
const session = await auth.api.getSession({ 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 // 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) { if (amount > maxSellable) {
throw error(400, `Cannot sell more than 99.5% of pool tokens. Max sellable: ${maxSellable} tokens`); throw error(400, `Cannot sell more than 99.5% of pool tokens. Max sellable: ${maxSellable} tokens`);
} }
const k = poolCoinAmount * poolBaseCurrencyAmount; const sellResult = await executeSellTrade(tx, coinData, userId, amount);
const newPoolCoin = poolCoinAmount + amount;
const newPoolBaseCurrency = k / newPoolCoin;
const baseCurrencyReceived = poolBaseCurrencyAmount - newPoolBaseCurrency;
totalCost = baseCurrencyReceived; if (!sellResult.success) {
newPrice = newPoolBaseCurrency / newPoolCoin; throw error(400, 'Trade failed - insufficient liquidity or invalid parameters');
priceImpact = ((newPrice - currentPrice) / currentPrice) * 100;
if (newPoolBaseCurrency < 10) {
throw error(400, `Trade would drain pool below minimum liquidity (*10 BUSS). Try selling fewer tokens.`);
} }
totalCost = sellResult.baseCurrencyReceived ?? 0;
newPrice = sellResult.newPrice;
priceImpact = sellResult.priceImpact;
if (totalCost <= 0) { if (totalCost <= 0) {
throw error(400, 'Trade amount results in zero base currency received'); 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({ const metrics = sellResult.metrics || await calculate24hMetrics(coinData.id, newPrice);
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 priceUpdateData = { const priceUpdateData = {
currentPrice: newPrice, currentPrice: newPrice,
marketCap: Number(coinData.circulatingSupply) * newPrice, marketCap: Number(coinData.circulatingSupply) * newPrice,
change24h: metrics.change24h, change24h: metrics.change24h,
volume24h: metrics.volume24h, volume24h: metrics.volume24h,
poolCoinAmount: newPoolCoin, poolCoinAmount: sellResult.newPoolCoin,
poolBaseCurrencyAmount: newPoolBaseCurrency poolBaseCurrencyAmount: sellResult.newPoolBaseCurrency
}; };
const tradeData = { const tradeData = {

View file

@ -5,6 +5,7 @@ import { user, userPortfolio, transaction, notifications, coin } from '$lib/serv
import { eq, sql } from 'drizzle-orm'; import { eq, sql } from 'drizzle-orm';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
import { formatValue, getPrestigeCost, getPrestigeName } from '$lib/utils'; import { formatValue, getPrestigeCost, getPrestigeName } from '$lib/utils';
import { executeSellTrade } from '$lib/server/amm';
export const POST: RequestHandler = async ({ request, locals }) => { export const POST: RequestHandler = async ({ request, locals }) => {
const session = await auth.api.getSession({ headers: request.headers }); const session = await auth.api.getSession({ headers: request.headers });
@ -44,7 +45,10 @@ export const POST: RequestHandler = async ({ request, locals }) => {
coinId: userPortfolio.coinId, coinId: userPortfolio.coinId,
quantity: userPortfolio.quantity, quantity: userPortfolio.quantity,
currentPrice: coin.currentPrice, currentPrice: coin.currentPrice,
symbol: coin.symbol symbol: coin.symbol,
poolCoinAmount: coin.poolCoinAmount,
poolBaseCurrencyAmount: coin.poolBaseCurrencyAmount,
circulatingSupply: coin.circulatingSupply
}) })
.from(userPortfolio) .from(userPortfolio)
.leftJoin(coin, eq(userPortfolio.coinId, coin.id)) .leftJoin(coin, eq(userPortfolio.coinId, coin.id))
@ -58,18 +62,37 @@ export const POST: RequestHandler = async ({ request, locals }) => {
for (const holding of holdings) { for (const holding of holdings) {
const quantity = Number(holding.quantity); const quantity = Number(holding.quantity);
const price = Number(holding.currentPrice); const currentPrice = Number(holding.currentPrice);
const saleValue = quantity * price;
totalSaleValue += saleValue; if (Number(holding.poolCoinAmount) <= 0 || Number(holding.poolBaseCurrencyAmount) <= 0) {
const fallbackValue = quantity * currentPrice;
totalSaleValue += fallbackValue;
await tx.insert(transaction).values({ await tx.insert(transaction).values({
userId,
coinId: holding.coinId!, coinId: holding.coinId!,
type: 'SELL', type: 'SELL',
quantity: holding.quantity, quantity: holding.quantity,
pricePerCoin: holding.currentPrice || '0', pricePerCoin: holding.currentPrice || '0',
totalBaseCurrencyAmount: saleValue.toString(), totalBaseCurrencyAmount: fallbackValue.toString(),
timestamp: new Date() 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 await tx