260 lines
9.6 KiB
TypeScript
260 lines
9.6 KiB
TypeScript
|
|
import { auth } from '$lib/auth';
|
||
|
|
import { error, json } from '@sveltejs/kit';
|
||
|
|
import { db } from '$lib/server/db';
|
||
|
|
import { user, userPortfolio, coin, transaction } from '$lib/server/db/schema';
|
||
|
|
import { eq, and } from 'drizzle-orm';
|
||
|
|
import type { RequestHandler } from './$types';
|
||
|
|
|
||
|
|
interface TransferRequest {
|
||
|
|
recipientUsername: string;
|
||
|
|
type: 'CASH' | 'COIN';
|
||
|
|
amount: number;
|
||
|
|
coinSymbol?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const POST: RequestHandler = async ({ request }) => {
|
||
|
|
const session = await auth.api.getSession({
|
||
|
|
headers: request.headers
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!session?.user) {
|
||
|
|
throw error(401, 'Not authenticated');
|
||
|
|
} try {
|
||
|
|
const { recipientUsername, type, amount, coinSymbol }: TransferRequest = await request.json();
|
||
|
|
|
||
|
|
if (!recipientUsername || !type || !amount || typeof amount !== 'number' || !Number.isFinite(amount) || amount <= 0) {
|
||
|
|
throw error(400, 'Invalid transfer parameters');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (amount > Number.MAX_SAFE_INTEGER) {
|
||
|
|
throw error(400, 'Transfer amount too large');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (type === 'COIN' && !coinSymbol) {
|
||
|
|
throw error(400, 'Coin symbol required for coin transfers');
|
||
|
|
}
|
||
|
|
|
||
|
|
const senderId = Number(session.user.id);
|
||
|
|
|
||
|
|
return await db.transaction(async (tx) => {
|
||
|
|
const [senderData] = await tx
|
||
|
|
.select({
|
||
|
|
id: user.id,
|
||
|
|
username: user.username,
|
||
|
|
baseCurrencyBalance: user.baseCurrencyBalance
|
||
|
|
})
|
||
|
|
.from(user)
|
||
|
|
.where(eq(user.id, senderId))
|
||
|
|
.for('update')
|
||
|
|
.limit(1);
|
||
|
|
|
||
|
|
if (!senderData) {
|
||
|
|
throw error(404, 'Sender not found');
|
||
|
|
}
|
||
|
|
|
||
|
|
const [recipientData] = await tx
|
||
|
|
.select({
|
||
|
|
id: user.id,
|
||
|
|
username: user.username,
|
||
|
|
baseCurrencyBalance: user.baseCurrencyBalance
|
||
|
|
})
|
||
|
|
.from(user)
|
||
|
|
.where(eq(user.username, recipientUsername))
|
||
|
|
.for('update')
|
||
|
|
.limit(1);
|
||
|
|
|
||
|
|
if (!recipientData) {
|
||
|
|
throw error(404, 'Recipient not found');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (senderData.id === recipientData.id) {
|
||
|
|
throw error(400, 'Cannot transfer to yourself');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (type === 'CASH') {
|
||
|
|
const senderBalance = Number(senderData.baseCurrencyBalance);
|
||
|
|
if (senderBalance < amount) {
|
||
|
|
throw error(400, `Insufficient funds. You have $${senderBalance.toFixed(2)} but trying to send $${amount.toFixed(2)}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const recipientBalance = Number(recipientData.baseCurrencyBalance);
|
||
|
|
|
||
|
|
await tx
|
||
|
|
.update(user)
|
||
|
|
.set({
|
||
|
|
baseCurrencyBalance: (senderBalance - amount).toFixed(8),
|
||
|
|
updatedAt: new Date()
|
||
|
|
})
|
||
|
|
.where(eq(user.id, senderId));
|
||
|
|
|
||
|
|
await tx
|
||
|
|
.update(user)
|
||
|
|
.set({
|
||
|
|
baseCurrencyBalance: (recipientBalance + amount).toFixed(8),
|
||
|
|
updatedAt: new Date()
|
||
|
|
})
|
||
|
|
.where(eq(user.id, recipientData.id));
|
||
|
|
|
||
|
|
await tx.insert(transaction).values({
|
||
|
|
userId: senderId,
|
||
|
|
coinId: 1,
|
||
|
|
type: 'TRANSFER_OUT',
|
||
|
|
quantity: '0',
|
||
|
|
pricePerCoin: '1',
|
||
|
|
totalBaseCurrencyAmount: amount.toString(),
|
||
|
|
timestamp: new Date(),
|
||
|
|
senderUserId: senderId,
|
||
|
|
recipientUserId: recipientData.id
|
||
|
|
});
|
||
|
|
|
||
|
|
await tx.insert(transaction).values({
|
||
|
|
userId: recipientData.id,
|
||
|
|
coinId: 1,
|
||
|
|
type: 'TRANSFER_IN',
|
||
|
|
quantity: '0',
|
||
|
|
pricePerCoin: '1',
|
||
|
|
totalBaseCurrencyAmount: amount.toString(),
|
||
|
|
timestamp: new Date(),
|
||
|
|
senderUserId: senderId,
|
||
|
|
recipientUserId: recipientData.id
|
||
|
|
});
|
||
|
|
|
||
|
|
return json({
|
||
|
|
success: true,
|
||
|
|
type: 'CASH',
|
||
|
|
amount,
|
||
|
|
recipient: recipientData.username,
|
||
|
|
newBalance: senderBalance - amount
|
||
|
|
});
|
||
|
|
|
||
|
|
} else {
|
||
|
|
const normalizedSymbol = coinSymbol!.toUpperCase();
|
||
|
|
|
||
|
|
const [coinData] = await tx
|
||
|
|
.select({ id: coin.id, symbol: coin.symbol, name: coin.name, currentPrice: coin.currentPrice })
|
||
|
|
.from(coin)
|
||
|
|
.where(eq(coin.symbol, normalizedSymbol))
|
||
|
|
.limit(1);
|
||
|
|
|
||
|
|
if (!coinData) {
|
||
|
|
throw error(404, 'Coin not found');
|
||
|
|
}
|
||
|
|
|
||
|
|
const [senderHolding] = await tx
|
||
|
|
.select({
|
||
|
|
quantity: userPortfolio.quantity
|
||
|
|
})
|
||
|
|
.from(userPortfolio)
|
||
|
|
.where(and(
|
||
|
|
eq(userPortfolio.userId, senderId),
|
||
|
|
eq(userPortfolio.coinId, coinData.id)
|
||
|
|
))
|
||
|
|
.for('update')
|
||
|
|
.limit(1);
|
||
|
|
|
||
|
|
if (!senderHolding || Number(senderHolding.quantity) < amount) {
|
||
|
|
const availableAmount = senderHolding ? Number(senderHolding.quantity) : 0;
|
||
|
|
throw error(400, `Insufficient ${coinData.symbol}. You have ${availableAmount.toFixed(6)} but trying to send ${amount.toFixed(6)}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const [recipientHolding] = await tx
|
||
|
|
.select({ quantity: userPortfolio.quantity })
|
||
|
|
.from(userPortfolio)
|
||
|
|
.where(and(
|
||
|
|
eq(userPortfolio.userId, recipientData.id),
|
||
|
|
eq(userPortfolio.coinId, coinData.id)
|
||
|
|
))
|
||
|
|
.for('update')
|
||
|
|
.limit(1);
|
||
|
|
|
||
|
|
const coinPrice = Number(coinData.currentPrice) || 0;
|
||
|
|
const totalValue = amount * coinPrice;
|
||
|
|
|
||
|
|
const newSenderQuantity = Number(senderHolding.quantity) - amount;
|
||
|
|
if (newSenderQuantity > 0.000001) {
|
||
|
|
await tx
|
||
|
|
.update(userPortfolio)
|
||
|
|
.set({
|
||
|
|
quantity: newSenderQuantity.toString(),
|
||
|
|
updatedAt: new Date()
|
||
|
|
})
|
||
|
|
.where(and(
|
||
|
|
eq(userPortfolio.userId, senderId),
|
||
|
|
eq(userPortfolio.coinId, coinData.id)
|
||
|
|
));
|
||
|
|
} else {
|
||
|
|
await tx
|
||
|
|
.delete(userPortfolio)
|
||
|
|
.where(and(
|
||
|
|
eq(userPortfolio.userId, senderId),
|
||
|
|
eq(userPortfolio.coinId, coinData.id)
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (recipientHolding) {
|
||
|
|
const newRecipientQuantity = Number(recipientHolding.quantity) + amount;
|
||
|
|
await tx
|
||
|
|
.update(userPortfolio)
|
||
|
|
.set({
|
||
|
|
quantity: newRecipientQuantity.toString(),
|
||
|
|
updatedAt: new Date()
|
||
|
|
})
|
||
|
|
.where(and(
|
||
|
|
eq(userPortfolio.userId, recipientData.id),
|
||
|
|
eq(userPortfolio.coinId, coinData.id)
|
||
|
|
));
|
||
|
|
} else {
|
||
|
|
await tx
|
||
|
|
.insert(userPortfolio)
|
||
|
|
.values({
|
||
|
|
userId: recipientData.id,
|
||
|
|
coinId: coinData.id,
|
||
|
|
quantity: amount.toString()
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
await tx.insert(transaction).values({
|
||
|
|
userId: senderId,
|
||
|
|
coinId: coinData.id,
|
||
|
|
type: 'TRANSFER_OUT',
|
||
|
|
quantity: amount.toString(),
|
||
|
|
pricePerCoin: coinPrice.toString(),
|
||
|
|
totalBaseCurrencyAmount: totalValue.toString(),
|
||
|
|
timestamp: new Date(),
|
||
|
|
senderUserId: senderId,
|
||
|
|
recipientUserId: recipientData.id
|
||
|
|
});
|
||
|
|
|
||
|
|
await tx.insert(transaction).values({
|
||
|
|
userId: recipientData.id,
|
||
|
|
coinId: coinData.id,
|
||
|
|
type: 'TRANSFER_IN',
|
||
|
|
quantity: amount.toString(),
|
||
|
|
pricePerCoin: coinPrice.toString(),
|
||
|
|
totalBaseCurrencyAmount: totalValue.toString(),
|
||
|
|
timestamp: new Date(),
|
||
|
|
senderUserId: senderId,
|
||
|
|
recipientUserId: recipientData.id
|
||
|
|
});
|
||
|
|
|
||
|
|
return json({
|
||
|
|
success: true,
|
||
|
|
type: 'COIN',
|
||
|
|
amount,
|
||
|
|
coinSymbol: coinData.symbol,
|
||
|
|
coinName: coinData.name,
|
||
|
|
recipient: recipientData.username,
|
||
|
|
newQuantity: newSenderQuantity
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Transfer error:', e);
|
||
|
|
if (e && typeof e === 'object' && 'status' in e) {
|
||
|
|
throw e;
|
||
|
|
}
|
||
|
|
return json({ error: 'Transfer failed' }, { status: 500 });
|
||
|
|
}
|
||
|
|
};
|