fix: sell all coin holdings instead of disowning in prestige
This commit is contained in:
parent
32bfbf816c
commit
edca4d5ceb
3 changed files with 190 additions and 121 deletions
142
website/src/lib/server/amm.ts
Normal file
142
website/src/lib/server/amm.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
||||||
await tx.insert(transaction).values({
|
if (Number(holding.poolCoinAmount) <= 0 || Number(holding.poolBaseCurrencyAmount) <= 0) {
|
||||||
coinId: holding.coinId!,
|
const fallbackValue = quantity * currentPrice;
|
||||||
type: 'SELL',
|
totalSaleValue += fallbackValue;
|
||||||
quantity: holding.quantity,
|
|
||||||
pricePerCoin: holding.currentPrice || '0',
|
await tx.insert(transaction).values({
|
||||||
totalBaseCurrencyAmount: saleValue.toString(),
|
userId,
|
||||||
timestamp: new Date()
|
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
|
await tx
|
||||||
|
|
|
||||||
Reference in a new issue