fix: race conditions
This commit is contained in:
parent
5eda7b0953
commit
8d12c679ae
7 changed files with 230 additions and 205 deletions
|
|
@ -49,13 +49,20 @@ export async function uploadProfilePicture(
|
|||
contentType: string,
|
||||
contentLength?: number
|
||||
): Promise<string> {
|
||||
let fileExtension = contentType.split('/')[1];
|
||||
// Ensure a valid image extension or default to jpg
|
||||
if (!fileExtension || !['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExtension.toLowerCase())) {
|
||||
fileExtension = 'jpg';
|
||||
if (!contentType || !contentType.startsWith('image/')) {
|
||||
throw new Error('Invalid file type. Only images are allowed.');
|
||||
}
|
||||
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (!allowedTypes.includes(contentType.toLowerCase())) {
|
||||
throw new Error('Unsupported image format. Only JPEG, PNG, GIF, and WebP are allowed.');
|
||||
}
|
||||
|
||||
let fileExtension = contentType.split('/')[1];
|
||||
if (fileExtension === 'jpeg') fileExtension = 'jpg';
|
||||
|
||||
const key = `avatars/${identifier}.${fileExtension}`;
|
||||
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: PUBLIC_B2_BUCKET,
|
||||
Key: key,
|
||||
|
|
@ -74,12 +81,20 @@ export async function uploadCoinIcon(
|
|||
contentType: string,
|
||||
contentLength?: number
|
||||
): Promise<string> {
|
||||
let fileExtension = contentType.split('/')[1];
|
||||
if (!fileExtension || !['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExtension.toLowerCase())) {
|
||||
fileExtension = 'png';
|
||||
if (!contentType || !contentType.startsWith('image/')) {
|
||||
throw new Error('Invalid file type. Only images are allowed.');
|
||||
}
|
||||
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (!allowedTypes.includes(contentType.toLowerCase())) {
|
||||
throw new Error('Unsupported image format. Only JPEG, PNG, GIF, and WebP are allowed.');
|
||||
}
|
||||
|
||||
let fileExtension = contentType.split('/')[1];
|
||||
if (fileExtension === 'jpeg') fileExtension = 'jpg';
|
||||
|
||||
const key = `coins/${coinSymbol.toLowerCase()}.${fileExtension}`;
|
||||
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: PUBLIC_B2_BUCKET,
|
||||
Key: key,
|
||||
|
|
|
|||
|
|
@ -58,66 +58,75 @@ export async function POST({ params, request }) {
|
|||
throw error(400, 'Invalid transaction type');
|
||||
}
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
throw error(400, 'Invalid amount');
|
||||
if (!amount || typeof amount !== 'number' || !Number.isFinite(amount) || amount <= 0) {
|
||||
throw error(400, 'Invalid amount - must be a positive finite number');
|
||||
}
|
||||
|
||||
if (amount > Number.MAX_SAFE_INTEGER) {
|
||||
throw error(400, 'Amount too large');
|
||||
}
|
||||
|
||||
const userId = Number(session.user.id);
|
||||
const normalizedSymbol = coinSymbol.toUpperCase();
|
||||
|
||||
const [coinData] = await db.select().from(coin).where(eq(coin.symbol, normalizedSymbol)).limit(1);
|
||||
|
||||
if (!coinData) {
|
||||
const [coinExists] = await db.select({ id: coin.id }).from(coin).where(eq(coin.symbol, normalizedSymbol)).limit(1);
|
||||
if (!coinExists) {
|
||||
throw error(404, 'Coin not found');
|
||||
}
|
||||
|
||||
if (!coinData.isListed) {
|
||||
throw error(400, 'This coin is delisted and cannot be traded');
|
||||
}
|
||||
return await db.transaction(async (tx) => {
|
||||
const [coinData] = await tx.select().from(coin).where(eq(coin.symbol, normalizedSymbol)).for('update').limit(1);
|
||||
|
||||
const [userData] = await db.select({
|
||||
baseCurrencyBalance: user.baseCurrencyBalance,
|
||||
username: user.username,
|
||||
image: user.image
|
||||
}).from(user).where(eq(user.id, userId)).limit(1);
|
||||
|
||||
if (!userData) {
|
||||
throw error(404, 'User not found');
|
||||
}
|
||||
|
||||
const userBalance = Number(userData.baseCurrencyBalance);
|
||||
const poolCoinAmount = Number(coinData.poolCoinAmount);
|
||||
const poolBaseCurrencyAmount = Number(coinData.poolBaseCurrencyAmount);
|
||||
const currentPrice = Number(coinData.currentPrice);
|
||||
|
||||
let newPrice: number;
|
||||
let totalCost: number;
|
||||
let priceImpact: number = 0;
|
||||
|
||||
if (poolCoinAmount <= 0 || poolBaseCurrencyAmount <= 0) {
|
||||
throw error(400, 'Liquidity pool is not properly initialized or is empty. Trading halted.');
|
||||
}
|
||||
|
||||
if (type === 'BUY') {
|
||||
// AMM BUY: amount = dollars to spend
|
||||
const k = poolCoinAmount * poolBaseCurrencyAmount;
|
||||
const newPoolBaseCurrency = poolBaseCurrencyAmount + amount;
|
||||
const newPoolCoin = k / newPoolBaseCurrency;
|
||||
const coinsBought = poolCoinAmount - newPoolCoin;
|
||||
|
||||
totalCost = amount;
|
||||
newPrice = newPoolBaseCurrency / newPoolCoin;
|
||||
priceImpact = ((newPrice - currentPrice) / currentPrice) * 100;
|
||||
|
||||
if (userBalance < totalCost) {
|
||||
throw error(400, `Insufficient funds. You need *${totalCost.toFixed(6)} BUSS but only have *${userBalance.toFixed(6)} BUSS`);
|
||||
if (!coinData) {
|
||||
throw error(404, 'Coin not found');
|
||||
}
|
||||
|
||||
if (coinsBought <= 0) {
|
||||
throw error(400, 'Trade amount too small - would result in zero tokens');
|
||||
if (!coinData.isListed) {
|
||||
throw error(400, 'This coin is delisted and cannot be traded');
|
||||
}
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
const [userData] = await tx.select({
|
||||
baseCurrencyBalance: user.baseCurrencyBalance,
|
||||
username: user.username,
|
||||
image: user.image
|
||||
}).from(user).where(eq(user.id, userId)).for('update').limit(1);
|
||||
|
||||
if (!userData) {
|
||||
throw error(404, 'User not found');
|
||||
}
|
||||
|
||||
const userBalance = Number(userData.baseCurrencyBalance);
|
||||
const poolCoinAmount = Number(coinData.poolCoinAmount);
|
||||
const poolBaseCurrencyAmount = Number(coinData.poolBaseCurrencyAmount);
|
||||
const currentPrice = Number(coinData.currentPrice);
|
||||
|
||||
let newPrice: number;
|
||||
let totalCost: number;
|
||||
let priceImpact: number = 0;
|
||||
|
||||
if (poolCoinAmount <= 0 || poolBaseCurrencyAmount <= 0) {
|
||||
throw error(400, 'Liquidity pool is not properly initialized or is empty. Trading halted.');
|
||||
}
|
||||
|
||||
if (type === 'BUY') {
|
||||
// AMM BUY: amount = dollars to spend
|
||||
const k = poolCoinAmount * poolBaseCurrencyAmount;
|
||||
const newPoolBaseCurrency = poolBaseCurrencyAmount + amount;
|
||||
const newPoolCoin = k / newPoolBaseCurrency;
|
||||
const coinsBought = poolCoinAmount - newPoolCoin;
|
||||
|
||||
totalCost = amount;
|
||||
newPrice = newPoolBaseCurrency / newPoolCoin;
|
||||
priceImpact = ((newPrice - currentPrice) / currentPrice) * 100;
|
||||
|
||||
if (userBalance < totalCost) {
|
||||
throw error(400, `Insufficient funds. You need *${totalCost.toFixed(6)} BUSS but only have *${userBalance.toFixed(6)} BUSS`);
|
||||
}
|
||||
|
||||
if (coinsBought <= 0) {
|
||||
throw error(400, 'Trade amount too small - would result in zero tokens');
|
||||
}
|
||||
|
||||
await tx.update(user)
|
||||
.set({
|
||||
baseCurrencyBalance: (userBalance - totalCost).toString(),
|
||||
|
|
@ -181,94 +190,91 @@ export async function POST({ params, request }) {
|
|||
})
|
||||
.where(eq(coin.id, coinData.id));
|
||||
|
||||
await redis.publish(`prices:${normalizedSymbol}`, JSON.stringify({
|
||||
const priceUpdateData = {
|
||||
currentPrice: newPrice,
|
||||
marketCap: Number(coinData.circulatingSupply) * newPrice,
|
||||
change24h: metrics.change24h,
|
||||
volume24h: metrics.volume24h,
|
||||
poolCoinAmount: newPoolCoin,
|
||||
poolBaseCurrencyAmount: newPoolBaseCurrency
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
// REDIS
|
||||
const tradeData = {
|
||||
type: 'BUY',
|
||||
username: userData.username,
|
||||
userImage: userData.image || '',
|
||||
amount: coinsBought,
|
||||
coinSymbol: normalizedSymbol,
|
||||
coinName: coinData.name,
|
||||
coinIcon: coinData.icon || '',
|
||||
totalValue: totalCost,
|
||||
price: newPrice,
|
||||
timestamp: Date.now(),
|
||||
userId: userId.toString()
|
||||
};
|
||||
const tradeData = {
|
||||
type: 'BUY',
|
||||
username: userData.username,
|
||||
userImage: userData.image || '',
|
||||
amount: coinsBought,
|
||||
coinSymbol: normalizedSymbol,
|
||||
coinName: coinData.name,
|
||||
coinIcon: coinData.icon || '',
|
||||
totalValue: totalCost,
|
||||
price: newPrice,
|
||||
timestamp: Date.now(),
|
||||
userId: userId.toString()
|
||||
};
|
||||
|
||||
await redis.publish('trades:all', JSON.stringify({
|
||||
type: 'all-trades',
|
||||
data: tradeData
|
||||
}));
|
||||
await redis.publish(`prices:${normalizedSymbol}`, JSON.stringify(priceUpdateData));
|
||||
|
||||
if (totalCost >= 1000) {
|
||||
await redis.publish('trades:large', JSON.stringify({
|
||||
type: 'live-trade',
|
||||
await redis.publish('trades:all', JSON.stringify({
|
||||
type: 'all-trades',
|
||||
data: tradeData
|
||||
}));
|
||||
}
|
||||
// End REDIS
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
type: 'BUY',
|
||||
coinsBought,
|
||||
totalCost,
|
||||
newPrice,
|
||||
priceImpact,
|
||||
newBalance: userBalance - totalCost
|
||||
});
|
||||
if (totalCost >= 1000) {
|
||||
await redis.publish('trades:large', JSON.stringify({
|
||||
type: 'live-trade',
|
||||
data: tradeData
|
||||
}));
|
||||
}
|
||||
|
||||
} else {
|
||||
// AMM SELL: amount = number of coins to sell
|
||||
const [userHolding] = await db
|
||||
.select({ quantity: userPortfolio.quantity })
|
||||
.from(userPortfolio)
|
||||
.where(and(
|
||||
eq(userPortfolio.userId, userId),
|
||||
eq(userPortfolio.coinId, coinData.id)
|
||||
))
|
||||
.limit(1);
|
||||
return json({
|
||||
success: true,
|
||||
type: 'BUY',
|
||||
coinsBought,
|
||||
totalCost,
|
||||
newPrice,
|
||||
priceImpact,
|
||||
newBalance: userBalance - totalCost
|
||||
});
|
||||
|
||||
if (!userHolding || Number(userHolding.quantity) < amount) {
|
||||
throw error(400, `Insufficient coins. You have ${userHolding ? Number(userHolding.quantity) : 0} but trying to sell ${amount}`);
|
||||
}
|
||||
} else {
|
||||
// AMM SELL: amount = number of coins to sell
|
||||
const [userHolding] = await tx
|
||||
.select({ quantity: userPortfolio.quantity })
|
||||
.from(userPortfolio)
|
||||
.where(and(
|
||||
eq(userPortfolio.userId, userId),
|
||||
eq(userPortfolio.coinId, coinData.id)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
// Allow more aggressive selling for rug pull simulation - prevent only mathematical breakdown
|
||||
const maxSellable = Math.floor(poolCoinAmount * 0.995); // 99.5% instead of 99%
|
||||
if (amount > maxSellable) {
|
||||
throw error(400, `Cannot sell more than 99.5% of pool tokens. Max sellable: ${maxSellable} tokens`);
|
||||
}
|
||||
if (!userHolding || Number(userHolding.quantity) < amount) {
|
||||
throw error(400, `Insufficient coins. You have ${userHolding ? Number(userHolding.quantity) : 0} but trying to sell ${amount}`);
|
||||
}
|
||||
|
||||
const k = poolCoinAmount * poolBaseCurrencyAmount;
|
||||
const newPoolCoin = poolCoinAmount + amount;
|
||||
const newPoolBaseCurrency = k / newPoolCoin;
|
||||
const baseCurrencyReceived = poolBaseCurrencyAmount - newPoolBaseCurrency;
|
||||
// Allow more aggressive selling for rug pull simulation - prevent only mathematical breakdown
|
||||
const maxSellable = Math.floor(poolCoinAmount * 0.995);
|
||||
if (amount > maxSellable) {
|
||||
throw error(400, `Cannot sell more than 99.5% of pool tokens. Max sellable: ${maxSellable} tokens`);
|
||||
}
|
||||
|
||||
totalCost = baseCurrencyReceived;
|
||||
newPrice = newPoolBaseCurrency / newPoolCoin;
|
||||
priceImpact = ((newPrice - currentPrice) / currentPrice) * 100;
|
||||
const k = poolCoinAmount * poolBaseCurrencyAmount;
|
||||
const newPoolCoin = poolCoinAmount + amount;
|
||||
const newPoolBaseCurrency = k / newPoolCoin;
|
||||
const baseCurrencyReceived = poolBaseCurrencyAmount - newPoolBaseCurrency;
|
||||
|
||||
// Lower minimum liquidity for more dramatic crashes
|
||||
if (newPoolBaseCurrency < 10) {
|
||||
throw error(400, `Trade would drain pool below minimum liquidity (*10 BUSS). Try selling fewer tokens.`);
|
||||
}
|
||||
totalCost = baseCurrencyReceived;
|
||||
newPrice = newPoolBaseCurrency / newPoolCoin;
|
||||
priceImpact = ((newPrice - currentPrice) / currentPrice) * 100;
|
||||
|
||||
if (totalCost <= 0) {
|
||||
throw error(400, 'Trade amount results in zero base currency received');
|
||||
}
|
||||
if (newPoolBaseCurrency < 10) {
|
||||
throw error(400, `Trade would drain pool below minimum liquidity (*10 BUSS). Try selling fewer tokens.`);
|
||||
}
|
||||
|
||||
if (totalCost <= 0) {
|
||||
throw error(400, 'Trade amount results in zero base currency received');
|
||||
}
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.update(user)
|
||||
.set({
|
||||
baseCurrencyBalance: (userBalance + totalCost).toString(),
|
||||
|
|
@ -323,52 +329,52 @@ export async function POST({ params, request }) {
|
|||
})
|
||||
.where(eq(coin.id, coinData.id));
|
||||
|
||||
await redis.publish(`prices:${normalizedSymbol}`, JSON.stringify({
|
||||
const priceUpdateData = {
|
||||
currentPrice: newPrice,
|
||||
marketCap: Number(coinData.circulatingSupply) * newPrice,
|
||||
change24h: metrics.change24h,
|
||||
volume24h: metrics.volume24h,
|
||||
poolCoinAmount: newPoolCoin,
|
||||
poolBaseCurrencyAmount: newPoolBaseCurrency
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
// REDIS
|
||||
const tradeData = {
|
||||
type: 'SELL',
|
||||
username: userData.username,
|
||||
userImage: userData.image || '',
|
||||
amount: amount,
|
||||
coinSymbol: normalizedSymbol,
|
||||
coinName: coinData.name,
|
||||
coinIcon: coinData.icon || '',
|
||||
totalValue: totalCost,
|
||||
price: newPrice,
|
||||
timestamp: Date.now(),
|
||||
userId: userId.toString()
|
||||
};
|
||||
const tradeData = {
|
||||
type: 'SELL',
|
||||
username: userData.username,
|
||||
userImage: userData.image || '',
|
||||
amount: amount,
|
||||
coinSymbol: normalizedSymbol,
|
||||
coinName: coinData.name,
|
||||
coinIcon: coinData.icon || '',
|
||||
totalValue: totalCost,
|
||||
price: newPrice,
|
||||
timestamp: Date.now(),
|
||||
userId: userId.toString()
|
||||
};
|
||||
|
||||
await redis.publish('trades:all', JSON.stringify({
|
||||
type: 'all-trades',
|
||||
data: tradeData
|
||||
}));
|
||||
await redis.publish(`prices:${normalizedSymbol}`, JSON.stringify(priceUpdateData));
|
||||
|
||||
if (totalCost >= 1000) {
|
||||
await redis.publish('trades:large', JSON.stringify({
|
||||
type: 'live-trade',
|
||||
await redis.publish('trades:all', JSON.stringify({
|
||||
type: 'all-trades',
|
||||
data: tradeData
|
||||
}));
|
||||
}
|
||||
// End REDIS
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
type: 'SELL',
|
||||
coinsSold: amount,
|
||||
totalReceived: totalCost,
|
||||
newPrice,
|
||||
priceImpact,
|
||||
newBalance: userBalance + totalCost
|
||||
});
|
||||
}
|
||||
if (totalCost >= 1000) {
|
||||
await redis.publish('trades:large', JSON.stringify({
|
||||
type: 'live-trade',
|
||||
data: tradeData
|
||||
}));
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
type: 'SELL',
|
||||
coinsSold: amount,
|
||||
totalReceived: totalCost,
|
||||
newPrice,
|
||||
priceImpact,
|
||||
newBalance: userBalance + totalCost
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { auth } from '$lib/auth';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { coin, userPortfolio, user, priceHistory, transaction } from '$lib/server/db/schema';
|
||||
import { coin, user, priceHistory } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { uploadCoinIcon } from '$lib/server/s3';
|
||||
import { CREATION_FEE, FIXED_SUPPLY, STARTING_PRICE, INITIAL_LIQUIDITY, TOTAL_COST, MAX_FILE_SIZE } from '$lib/data/constants';
|
||||
|
|
@ -31,32 +31,6 @@ async function validateInputs(name: string, symbol: string, iconFile: File | nul
|
|||
}
|
||||
}
|
||||
|
||||
async function validateUserBalance(userId: number) {
|
||||
const [userData] = await db
|
||||
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||
.from(user)
|
||||
.where(eq(user.id, userId))
|
||||
.limit(1);
|
||||
|
||||
if (!userData) {
|
||||
throw error(404, 'User not found');
|
||||
}
|
||||
|
||||
const currentBalance = Number(userData.baseCurrencyBalance);
|
||||
if (currentBalance < TOTAL_COST) {
|
||||
throw error(400, `Insufficient funds. You need $${TOTAL_COST.toFixed(2)} but only have $${currentBalance.toFixed(2)}.`);
|
||||
}
|
||||
|
||||
return currentBalance;
|
||||
}
|
||||
|
||||
async function validateSymbolUnique(symbol: string) {
|
||||
const existingCoin = await db.select().from(coin).where(eq(coin.symbol, symbol)).limit(1);
|
||||
if (existingCoin.length > 0) {
|
||||
throw error(400, 'A coin with this symbol already exists');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleIconUpload(iconFile: File | null, symbol: string): Promise<string | null> {
|
||||
if (!iconFile || iconFile.size === 0) {
|
||||
return null;
|
||||
|
|
@ -90,20 +64,31 @@ export async function POST({ request }) {
|
|||
|
||||
await validateInputs(name, normalizedSymbol, iconFile);
|
||||
|
||||
const [currentBalance] = await Promise.all([
|
||||
validateUserBalance(userId),
|
||||
validateSymbolUnique(normalizedSymbol)
|
||||
]);
|
||||
|
||||
let iconKey: string | null = null;
|
||||
try {
|
||||
iconKey = await handleIconUpload(iconFile, normalizedSymbol);
|
||||
} catch (e) {
|
||||
console.error('Icon upload failed, continuing without icon:', e);
|
||||
}
|
||||
|
||||
let createdCoin: any;
|
||||
let iconKey: string | null = null;
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
const existingCoin = await tx.select().from(coin).where(eq(coin.symbol, normalizedSymbol)).limit(1);
|
||||
if (existingCoin.length > 0) {
|
||||
throw error(400, 'A coin with this symbol already exists');
|
||||
}
|
||||
|
||||
const [userData] = await tx
|
||||
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||
.from(user)
|
||||
.where(eq(user.id, userId))
|
||||
.for('update')
|
||||
.limit(1);
|
||||
|
||||
if (!userData) {
|
||||
throw error(404, 'User not found');
|
||||
}
|
||||
|
||||
const currentBalance = Number(userData.baseCurrencyBalance);
|
||||
if (currentBalance < TOTAL_COST) {
|
||||
throw error(400, `Insufficient funds. You need $${TOTAL_COST.toFixed(2)} but only have $${currentBalance.toFixed(2)}.`);
|
||||
}
|
||||
|
||||
await tx.update(user)
|
||||
.set({
|
||||
baseCurrencyBalance: (currentBalance - TOTAL_COST).toString(),
|
||||
|
|
@ -114,7 +99,7 @@ export async function POST({ request }) {
|
|||
const [newCoin] = await tx.insert(coin).values({
|
||||
name,
|
||||
symbol: normalizedSymbol,
|
||||
icon: iconKey,
|
||||
icon: null,
|
||||
creatorId: userId,
|
||||
initialSupply: FIXED_SUPPLY.toString(),
|
||||
circulatingSupply: FIXED_SUPPLY.toString(),
|
||||
|
|
@ -126,14 +111,27 @@ export async function POST({ request }) {
|
|||
|
||||
createdCoin = newCoin;
|
||||
|
||||
|
||||
await tx.insert(priceHistory).values({
|
||||
coinId: newCoin.id,
|
||||
price: STARTING_PRICE.toString()
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
if (iconFile && iconFile.size > 0) {
|
||||
try {
|
||||
iconKey = await handleIconUpload(iconFile, normalizedSymbol);
|
||||
|
||||
await db.update(coin)
|
||||
.set({ icon: iconKey })
|
||||
.where(eq(coin.id, createdCoin.id));
|
||||
|
||||
createdCoin.icon = iconKey;
|
||||
} catch (e) {
|
||||
console.error('Icon upload failed after coin creation:', e);
|
||||
// coin is still created successfully, just without icon
|
||||
}
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
coin: {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||
.from(user)
|
||||
.where(eq(user.id, userId))
|
||||
.for('update')
|
||||
.limit(1);
|
||||
|
||||
if (!userData || Number(userData.baseCurrencyBalance) < MIN_BALANCE_REQUIRED) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||
})
|
||||
.from(promoCode)
|
||||
.where(eq(promoCode.code, normalizedCode))
|
||||
.for('update')
|
||||
.limit(1);
|
||||
|
||||
if (!promoData) {
|
||||
|
|
@ -64,7 +65,8 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||
const [{ totalUses }] = await tx
|
||||
.select({ totalUses: count() })
|
||||
.from(promoCodeRedemption)
|
||||
.where(eq(promoCodeRedemption.promoCodeId, promoData.id));
|
||||
.where(eq(promoCodeRedemption.promoCodeId, promoData.id))
|
||||
.for('update');
|
||||
|
||||
if (totalUses >= promoData.maxUses) {
|
||||
return json({ error: 'This promo code has reached its usage limit' }, { status: 400 });
|
||||
|
|
@ -75,6 +77,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||
.from(user)
|
||||
.where(eq(user.id, userId))
|
||||
.for('update')
|
||||
.limit(1);
|
||||
|
||||
if (!userData) {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||
})
|
||||
.from(user)
|
||||
.where(eq(user.id, userId))
|
||||
.for('update')
|
||||
.limit(1);
|
||||
|
||||
if (!userData[0]) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { validateSearchParams } from '$lib/utils/validation';
|
|||
|
||||
export async function GET({ url }) {
|
||||
const params = validateSearchParams(url.searchParams);
|
||||
const limit = params.getPositiveInt('limit', 100);
|
||||
const requestedLimit = params.getPositiveInt('limit', 100);
|
||||
const limit = Math.min(requestedLimit, 1000);
|
||||
const minValue = params.getNonNegativeFloat('minValue', 0);
|
||||
|
||||
try {
|
||||
|
|
@ -46,7 +47,7 @@ export async function GET({ url }) {
|
|||
totalValue: Number(trade.totalValue),
|
||||
price: Number(trade.price),
|
||||
timestamp: trade.timestamp.getTime(),
|
||||
userId: trade.userId.toString()
|
||||
userId: trade.userId?.toString() ?? ''
|
||||
}));
|
||||
|
||||
return json({ trades: formattedTrades });
|
||||
|
|
|
|||
Reference in a new issue