pummmp.fun/app/api/trade/route.ts
2026-07-04 12:49:09 -07:00

320 lines
10 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { connectToDatabase } from '@/lib/mongodb'
import { ObjectId } from 'mongodb'
import { checkRateLimit } from '@/lib/rate-limit'
const DISCORD_WEBHOOK_URL = '';
const TRADE_ROLE_ID = '1465848690785652900';
async function sendDiscordPriceAlert(coin: any, trade: any, priceChangePercent: number, oldPrice: number, newPrice: number) {
try {
const embed = {
title: `${coin.name} Price Alert :bangbang:`,
description: `**[$${coin.ticker}](https://pummmp.fun/coin/${coin._id})** price ${priceChangePercent > 0 ? 'increased' : 'decreased'} by **${Math.abs(priceChangePercent).toFixed(2)}%** ${priceChangePercent > 0 ? '↗' : '↘'}`,
color: priceChangePercent > 0 ? 0x00ff00 : 0xff0000,
fields: [
{
name: 'New Price <:price:1465842044986724425>',
value: `\`$${newPrice.toFixed(9)}\``,
inline: true
},
{
name: 'Market Cap <a:marketcap:1465842943674810626>',
value: `\`${(coin.marketCap * (newPrice / coin.price)).toLocaleString()} SOL\``,
inline: true
},
{
name: 'Trader <:trade:1465843050604396738>',
value: trade.userVerified ? `${trade.username} <:verified:1465841044275859722>` : trade.username,
inline: false
},
{
name: `Action ${trade.type === 'buy' ? '<:greencandle:1465842982556139590>' : '<:redcandle:1465843012222324767>'}`,
value: `${trade.type === 'buy' ? 'Bought' : 'Sold'} **${trade.amount.toLocaleString()}** $${coin.ticker}`,
inline: true
},
{
name: 'Value',
value: `> **${trade.total.toFixed(4)} SOL**`,
inline: false
},
{
name: 'Change',
value: `> \`$${oldPrice.toFixed(9)}\`\`$${newPrice.toFixed(9)}\``,
inline: true
}
],
timestamp: new Date().toISOString(),
footer: {
text: 'pummmp.fun',
icon_url: 'https://pummmp.fun/logo.png'
}
}
await fetch(DISCORD_WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: `<@&${TRADE_ROLE_ID}>`,
embeds: [embed]
})
})
} catch (error) {
console.error('[/api/trade] Failed to send Discord webhook:', error)
}
}
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
if (checkRateLimit(session.user.id, 'trade', 20, 60000)) {
return NextResponse.json({ error: 'Rate limit exceeded. Please slow down.' }, { status: 429 })
}
const { db } = await connectToDatabase()
const body = await request.json()
const { coinId, type, amount } = body
if (!coinId || !type || !amount || amount <= 0) {
return NextResponse.json({ error: 'Invalid trade parameters' }, { status: 400 })
}
const coin = await db.collection('coins').findOne({ _id: new ObjectId(coinId) })
if (!coin) {
return NextResponse.json({ error: 'Coin not found' }, { status: 404 })
}
const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
// Check trade ban
if (user.tradeBannedUntil) {
const banExpiry = new Date(user.tradeBannedUntil);
const nowCheck = new Date();
if (banExpiry > nowCheck) {
const isPermanent = banExpiry.getFullYear && banExpiry.getFullYear() === 9999;
return NextResponse.json({ error: `You are banned from trading ${isPermanent ? 'permanently' : 'for a while'}` }, { status: 403 })
} else {
try { await db.collection('users').updateOne({ _id: user._id }, { $unset: { tradeBannedUntil: 1 } }) } catch (e) {}
}
}
const vSol = coin.virtualSolReserves || 30;
const vTokens = coin.virtualTokenReserves || 1073000000;
const k = vSol * vTokens;
let solAmount = 0;
let newPrice = coin.price;
let newVSol = vSol;
let newVTokens = vTokens;
if (type === 'buy') {
if (amount >= vTokens - 1) {
return NextResponse.json({ error: 'Cannot buy entire supply' }, { status: 400 })
}
newVTokens = vTokens - amount;
newVSol = k / newVTokens;
solAmount = newVSol - vSol;
if (user.balance < solAmount) {
return NextResponse.json({ error: 'Insufficient balance' }, { status: 400 })
}
newPrice = newVSol / newVTokens;
const existingPosition = user.portfolio?.find((p: any) => p.coinId === coinId)
if (existingPosition) {
const newAmount = existingPosition.amount + amount
const newAvgPrice = (existingPosition.amount * existingPosition.avgBuyPrice + solAmount) / newAmount
await db.collection('users').updateOne(
{ _id: new ObjectId(session.user.id), 'portfolio.coinId': coinId },
{
$inc: { balance: -solAmount },
$set: {
'portfolio.$.amount': newAmount,
'portfolio.$.avgBuyPrice': newAvgPrice,
},
}
)
} else {
await db.collection('users').updateOne(
{ _id: new ObjectId(session.user.id) },
{
$inc: { balance: -solAmount },
$push: {
portfolio: {
coinId,
amount,
avgBuyPrice: solAmount / amount,
},
} as any,
}
)
}
// check for graduation (if virtualSolReserves > 85)
// real pump.fun is ~85 SOL.
const GRADUATION_THRESHOLD = 85;
const shouldGraduate = !coin.graduated && newVSol >= GRADUATION_THRESHOLD;
// if graduating, we "lock" a portion of liquidity or just mark it
// we also notify via socket (handled by client listening to trade)
const updateData: any = {
$set: {
price: newPrice,
marketCap: newPrice * coin.supply,
virtualSolReserves: newVSol,
virtualTokenReserves: newVTokens,
},
$inc: {
volume24h: solAmount, // volume in SOL
holders: existingPosition ? 0 : 1,
liquidity: solAmount, // increase real liquidity
},
$push: {
priceHistory: {
timestamp: new Date(),
price: newPrice,
volume: solAmount,
},
} as any,
}
if (shouldGraduate) {
updateData.$set.graduated = true;
updateData.$set.graduatedAt = new Date();
}
await db.collection('coins').updateOne(
{ _id: new ObjectId(coinId) },
updateData
)
} else if (type === 'sell') {
const position = user.portfolio?.find((p: any) => p.coinId === coinId)
if (!position || position.amount < amount) {
return NextResponse.json({ error: 'Insufficient tokens' }, { status: 400 })
}
newVTokens = vTokens + amount;
newVSol = k / newVTokens;
solAmount = vSol - newVSol; // ? this is what user receives
newPrice = newVSol / newVTokens;
const newAmount = position.amount - amount
if (newAmount === 0) {
await db.collection('users').updateOne(
{ _id: new ObjectId(session.user.id) },
{
$inc: { balance: solAmount },
$pull: { portfolio: { coinId } } as any,
}
)
await db.collection('coins').updateOne(
{ _id: new ObjectId(coinId) },
{ $inc: { holders: -1 } }
)
} else {
await db.collection('users').updateOne(
{ _id: new ObjectId(session.user.id), 'portfolio.coinId': coinId },
{
$inc: { balance: solAmount },
$set: { 'portfolio.$.amount': newAmount },
}
)
}
await db.collection('coins').updateOne(
{ _id: new ObjectId(coinId) },
{
$set: {
price: newPrice,
marketCap: newPrice * coin.supply,
virtualSolReserves: newVSol,
virtualTokenReserves: newVTokens,
},
$inc: {
volume24h: solAmount,
liquidity: -solAmount, // decrease real liquidity
},
$push: {
priceHistory: {
timestamp: new Date(),
price: newPrice,
volume: solAmount,
},
} as any,
}
)
}
const updatedCoin = await db.collection('coins').findOne({ _id: new ObjectId(coinId) });
if (updatedCoin && !updatedCoin.graduated) {
const GRADUATION_THRESHOLD = 45;
const currentVirtualSol = updatedCoin.virtualSolReserves || 0;
const shouldGraduate = currentVirtualSol >= GRADUATION_THRESHOLD;
if (shouldGraduate) {
await db.collection('coins').updateOne(
{ _id: new ObjectId(coinId) },
{
$set: {
graduated: true,
graduatedAt: new Date(),
}
}
);
console.log(`Coin ${coinId} graduated with ${currentVirtualSol} virtual SOL reserves`);
}
}
// record trade
const trade = {
userId: session.user.id,
username: user.name || 'Anonymous',
userImage: user.image || '',
userVerified: !!user.verified,
userIsAdmin: !!user.isAdmin,
userIsBetaTester: !!user.isBetaTester,
userIsBugHunter: !!user.isBugHunter,
coinId,
type,
amount, // ? token amount
price: newPrice,
total: solAmount, // ? SOL value
timestamp: new Date(),
}
const result = await db.collection('trades').insertOne(trade)
const savedTrade = { ...trade, _id: result.insertedId }
const oldPrice = coin.price
const priceChangePercent = ((newPrice - oldPrice) / oldPrice) * 100
// Send alert for price changes > 25%
if (Math.abs(priceChangePercent) >= 25) {
await sendDiscordPriceAlert(coin, savedTrade, priceChangePercent, oldPrice, newPrice)
}
return NextResponse.json({ success: true, trade: savedTrade })
} catch (error) {
console.error('Error executing trade:', error)
return NextResponse.json({ error: 'Failed to execute trade' }, { status: 500 })
}
}