286 lines
9.4 KiB
TypeScript
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 })
|
|
}
|
|
}
|