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

286 lines
9.4 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 SWAP_ROLE_ID = '1465848761862590495';
async function sendDiscordSwapAlert(fromCoin: any, toCoin: any, user: any, swapData: any) {
try {
const embed = {
title: `Massive Swap Alert!!! <:trade:1465843050604396738>`,
description: `**${user.name}** swapped **${swapData.intermediateSol.toFixed(4)} SOL**!`,
color: 0x0099ff, // Blue for swaps
fields: [
{
name: 'From Coin',
value: `[$${fromCoin.ticker}](https://pummmp.fun/coin/${fromCoin._id}) (${fromCoin.name})`,
inline: true
},
{
name: 'To Coin',
value: `[$${toCoin.ticker}](https://pummmp.fun/coin/${toCoin._id}) (${toCoin.name})`,
inline: true
},
{
name: 'Trader <:trade:1465843050604396738>',
value: user.verified ? `${user.name} <:verified:1465841044275859722>` : user.name,
inline: true
},
{
name: 'Sold Amount <:redcandle:1465843012222324767>',
value: `${swapData.sold.toLocaleString()} $${fromCoin.ticker}`,
inline: true
},
{
name: 'Received Amount <:greencandle:1465842982556139590>',
value: `${swapData.received.toLocaleString()} $${toCoin.ticker}`,
inline: true
},
],
timestamp: new Date().toISOString(),
footer: {
text: 'pummmp.fun',
icon_url: 'https://pummmp.fun/logo.ico'
}
}
await fetch(DISCORD_WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: `<@&${SWAP_ROLE_ID}>`,
embeds: [embed]
})
})
} catch (error) {
console.error('Failed to send Discord swap 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, 'swap', 10, 60000)) {
return NextResponse.json({ error: 'Rate limit exceeded. Please slow down.' }, { status: 429 })
}
const body = await request.json()
const { fromCoinId, toCoinId, amount } = body
if (!fromCoinId || !toCoinId || !amount || amount <= 0) {
return NextResponse.json({ error: 'Invalid swap parameters' }, { status: 400 })
}
if (fromCoinId === toCoinId) {
return NextResponse.json({ error: 'Cannot swap same coin' }, { status: 400 })
}
const { db } = await connectToDatabase()
// Fetch resources
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) {
return NextResponse.json({ error: 'You are banned from swapping/trading' }, { status: 403 })
} else {
try { await db.collection('users').updateOne({ _id: user._id }, { $unset: { tradeBannedUntil: 1 } }) } catch (e) {}
}
}
const fromCoin = await db.collection('coins').findOne({ _id: new ObjectId(fromCoinId) })
if (!fromCoin) return NextResponse.json({ error: 'Source coin not found' }, { status: 404 })
const toCoin = await db.collection('coins').findOne({ _id: new ObjectId(toCoinId) })
if (!toCoin) return NextResponse.json({ error: 'Target coin not found' }, { status: 404 })
// Verify Balance
const fromPosition = user.portfolio?.find((p: any) => p.coinId === fromCoinId)
if (!fromPosition || fromPosition.amount < amount) {
return NextResponse.json({ error: 'Insufficient token balance' }, { status: 400 })
}
// --- STEP 1: SELL 'fromCoin' ---
const fromVSol = fromCoin.virtualSolReserves || 30;
const fromVTokens = fromCoin.virtualTokenReserves || 1073000000;
const fromK = fromVSol * fromVTokens;
const newFromVTokens = fromVTokens + amount;
const newFromVSol = fromK / newFromVTokens;
const solProceeds = fromVSol - newFromVSol;
if (solProceeds <= 0) {
return NextResponse.json({ error: 'Slippage too high (Zero value)' }, { status: 400 })
}
const newFromPrice = newFromVSol / newFromVTokens;
// --- STEP 2: BUY 'toCoin' ---
const toVSol = toCoin.virtualSolReserves || 30;
const toVTokens = toCoin.virtualTokenReserves || 1073000000;
const toK = toVSol * toVTokens;
// Buying with `solProceeds`
const newToVSol = toVSol + solProceeds;
const newToVTokens = toK / newToVSol;
const tokensReceived = toVTokens - newToVTokens;
if (tokensReceived <= 0) {
return NextResponse.json({ error: 'Output amount too low' }, { status: 400 })
}
// Check limit
if (tokensReceived >= toVTokens * 0.9) {
return NextResponse.json({ error: 'Price impact too high' }, { status: 400 })
}
const newToPrice = newToVSol / newToVTokens;
// --- EXECUTE UPDATES ---
const now = new Date();
// 1. Update FromCoin
await db.collection('coins').updateOne(
{ _id: new ObjectId(fromCoinId) },
{
$set: {
price: newFromPrice,
marketCap: newFromPrice * fromCoin.supply,
virtualSolReserves: newFromVSol,
virtualTokenReserves: newFromVTokens,
},
$inc: {
volume24h: solProceeds,
liquidity: -solProceeds, // Remove liquidity from FromCoin
},
$push: {
priceHistory: { timestamp: now, price: newFromPrice, volume: solProceeds }
} as any
}
);
// 2. Update ToCoin
await db.collection('coins').updateOne(
{ _id: new ObjectId(toCoinId) },
{
$set: {
price: newToPrice,
marketCap: newToPrice * toCoin.supply,
virtualSolReserves: newToVSol,
virtualTokenReserves: newToVTokens,
},
$inc: {
volume24h: solProceeds,
liquidity: solProceeds, // Add liquidity to ToCoin
},
$push: {
priceHistory: { timestamp: now, price: newToPrice, volume: solProceeds }
} as any
}
);
// 3. Update User Portfolio
// Decrement FromCoin
const remainingFrom = fromPosition.amount - amount;
if (remainingFrom > 0) {
await db.collection('users').updateOne(
{ _id: new ObjectId(session.user.id), 'portfolio.coinId': fromCoinId },
{ $set: { 'portfolio.$.amount': remainingFrom } }
);
} else {
await db.collection('users').updateOne(
{ _id: new ObjectId(session.user.id) },
{ $pull: { portfolio: { coinId: fromCoinId } } } as any
);
}
// Increment ToCoin
const existingToPos = user.portfolio?.find((p: any) => p.coinId === toCoinId);
if (existingToPos) {
const newTotal = existingToPos.amount + tokensReceived;
// recalculate avg buy price ? complicated for swap.
// Logic: (oldAmount * oldAvg + solProceeds) / newTotal
const newAvg = (existingToPos.amount * existingToPos.avgBuyPrice + solProceeds) / newTotal;
await db.collection('users').updateOne(
{ _id: new ObjectId(session.user.id), 'portfolio.coinId': toCoinId },
{
$inc: { 'portfolio.$.amount': tokensReceived },
$set: { 'portfolio.$.avgBuyPrice': newAvg }
}
);
} else {
await db.collection('users').updateOne(
{ _id: new ObjectId(session.user.id) },
{
$push: {
portfolio: {
coinId: toCoinId,
amount: tokensReceived,
avgBuyPrice: solProceeds / tokensReceived
}
} as any
}
);
}
// 4. Record Trades (Sell then Buy)
await db.collection('trades').insertMany([
{
userId: session.user.id,
coinId: fromCoinId,
type: 'sell',
amount: amount,
price: newFromPrice,
total: solProceeds,
timestamp: now,
isSwap: true
},
{
userId: session.user.id,
coinId: toCoinId,
type: 'buy',
amount: tokensReceived,
price: newToPrice,
total: solProceeds,
timestamp: now,
isSwap: true
}
]);
// Send Discord alert for large swaps (> 6 SOL)
if (solProceeds >= 6) {
await sendDiscordSwapAlert(fromCoin, toCoin, user, {
sold: amount,
received: tokensReceived,
intermediateSol: solProceeds
})
}
return NextResponse.json({
success: true,
swapped: {
sold: amount,
received: tokensReceived,
intermediateSol: solProceeds
}
});
} catch (err: any) {
console.error('Swap error:', err);
return NextResponse.json({ error: 'Swap failed' }, { status: 500 })
}
}