feat: add CoinIcon component for displaying cryptocurrency icons
feat: implement TradeModal for buying and selling coins with validation and transaction handling feat: create server-side trade API for executing buy/sell transactions and updating user balances feat: add transactions API to fetch user transaction history feat: implement portfolio page to display user's holdings and recent transactions
This commit is contained in:
parent
0784e0f3d3
commit
a278d0c6a5
13 changed files with 1342 additions and 210 deletions
|
|
@ -1,73 +1,196 @@
|
|||
import { error, json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { coin, user, priceHistory } from '$lib/server/db/schema';
|
||||
import { coin, user, priceHistory, transaction } from '$lib/server/db/schema';
|
||||
import { eq, desc } from 'drizzle-orm';
|
||||
|
||||
export async function GET({ params }) {
|
||||
const { coinSymbol } = params;
|
||||
function aggregatePriceHistory(priceData: any[], intervalMinutes: number = 60) {
|
||||
if (priceData.length === 0) return [];
|
||||
|
||||
const sortedData = priceData.sort((a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
|
||||
const intervalMs = intervalMinutes * 60 * 1000;
|
||||
const candlesticks = new Map();
|
||||
|
||||
sortedData.forEach(point => {
|
||||
const timestamp = new Date(point.timestamp).getTime();
|
||||
const intervalStart = Math.floor(timestamp / intervalMs) * intervalMs;
|
||||
|
||||
if (!candlesticks.has(intervalStart)) {
|
||||
candlesticks.set(intervalStart, {
|
||||
time: Math.floor(intervalStart / 1000),
|
||||
open: point.price,
|
||||
high: point.price,
|
||||
low: point.price,
|
||||
close: point.price,
|
||||
firstTimestamp: timestamp,
|
||||
lastTimestamp: timestamp
|
||||
});
|
||||
} else {
|
||||
const candle = candlesticks.get(intervalStart);
|
||||
candle.high = Math.max(candle.high, point.price);
|
||||
candle.low = Math.min(candle.low, point.price);
|
||||
|
||||
if (timestamp < candle.firstTimestamp) {
|
||||
candle.open = point.price;
|
||||
candle.firstTimestamp = timestamp;
|
||||
}
|
||||
|
||||
if (timestamp > candle.lastTimestamp) {
|
||||
candle.close = point.price;
|
||||
candle.lastTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const candleArray = Array.from(candlesticks.values()).sort((a, b) => a.time - b.time);
|
||||
const fixedCandles = [];
|
||||
let lastClose = null;
|
||||
const PRICE_CHANGE_THRESHOLD = 0.01;
|
||||
|
||||
for (const candle of candleArray) {
|
||||
if (lastClose !== null && Math.abs(candle.open - lastClose) > lastClose * PRICE_CHANGE_THRESHOLD) {
|
||||
candle.open = lastClose;
|
||||
candle.high = Math.max(candle.high, lastClose);
|
||||
candle.low = Math.min(candle.low, lastClose);
|
||||
}
|
||||
|
||||
const finalCandle = {
|
||||
time: candle.time,
|
||||
open: candle.open,
|
||||
high: Math.max(candle.open, candle.close, candle.high),
|
||||
low: Math.min(candle.open, candle.close, candle.low),
|
||||
close: candle.close
|
||||
};
|
||||
|
||||
fixedCandles.push(finalCandle);
|
||||
lastClose = finalCandle.close;
|
||||
}
|
||||
|
||||
return fixedCandles;
|
||||
}
|
||||
|
||||
function aggregateVolumeData(transactionData: any[], intervalMinutes: number = 60) {
|
||||
if (transactionData.length === 0) return [];
|
||||
|
||||
const sortedData = transactionData.sort((a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
|
||||
const intervalMs = intervalMinutes * 60 * 1000;
|
||||
const volumeMap = new Map();
|
||||
|
||||
sortedData.forEach(tx => {
|
||||
const timestamp = new Date(tx.timestamp).getTime();
|
||||
const intervalStart = Math.floor(timestamp / intervalMs) * intervalMs;
|
||||
|
||||
if (!volumeMap.has(intervalStart)) {
|
||||
volumeMap.set(intervalStart, {
|
||||
time: Math.floor(intervalStart / 1000),
|
||||
volume: 0
|
||||
});
|
||||
}
|
||||
|
||||
const volumePoint = volumeMap.get(intervalStart);
|
||||
volumePoint.volume += tx.totalBaseCurrencyAmount;
|
||||
});
|
||||
|
||||
return Array.from(volumeMap.values()).sort((a, b) => a.time - b.time);
|
||||
}
|
||||
|
||||
export async function GET({ params, url }) {
|
||||
const coinSymbol = params.coinSymbol?.toUpperCase();
|
||||
const timeframe = url.searchParams.get('timeframe') || '1m';
|
||||
|
||||
if (!coinSymbol) {
|
||||
throw error(400, 'Coin symbol is required');
|
||||
}
|
||||
|
||||
const normalizedSymbol = coinSymbol.toUpperCase();
|
||||
const timeframeMap = {
|
||||
'1m': 1, '5m': 5, '15m': 15, '1h': 60, '4h': 240, '1d': 1440
|
||||
} as const;
|
||||
const intervalMinutes = timeframeMap[timeframe as keyof typeof timeframeMap] || 1;
|
||||
|
||||
const [coinData] = await db
|
||||
.select({
|
||||
id: coin.id,
|
||||
name: coin.name,
|
||||
symbol: coin.symbol,
|
||||
creatorId: coin.creatorId,
|
||||
creatorName: user.name,
|
||||
creatorUsername: user.username,
|
||||
creatorBio: user.bio,
|
||||
creatorImage: user.image,
|
||||
initialSupply: coin.initialSupply,
|
||||
circulatingSupply: coin.circulatingSupply,
|
||||
currentPrice: coin.currentPrice,
|
||||
marketCap: coin.marketCap,
|
||||
icon: coin.icon,
|
||||
volume24h: coin.volume24h,
|
||||
change24h: coin.change24h,
|
||||
poolCoinAmount: coin.poolCoinAmount,
|
||||
poolBaseCurrencyAmount: coin.poolBaseCurrencyAmount,
|
||||
createdAt: coin.createdAt,
|
||||
isListed: coin.isListed
|
||||
})
|
||||
.from(coin)
|
||||
.leftJoin(user, eq(coin.creatorId, user.id))
|
||||
.where(eq(coin.symbol, normalizedSymbol))
|
||||
.limit(1);
|
||||
try {
|
||||
const [coinData] = await db
|
||||
.select({
|
||||
id: coin.id,
|
||||
name: coin.name,
|
||||
symbol: coin.symbol,
|
||||
icon: coin.icon,
|
||||
currentPrice: coin.currentPrice,
|
||||
marketCap: coin.marketCap,
|
||||
volume24h: coin.volume24h,
|
||||
change24h: coin.change24h,
|
||||
poolCoinAmount: coin.poolCoinAmount,
|
||||
poolBaseCurrencyAmount: coin.poolBaseCurrencyAmount,
|
||||
circulatingSupply: coin.circulatingSupply,
|
||||
initialSupply: coin.initialSupply,
|
||||
isListed: coin.isListed,
|
||||
createdAt: coin.createdAt,
|
||||
creatorId: coin.creatorId,
|
||||
creatorName: user.name,
|
||||
creatorUsername: user.username,
|
||||
creatorBio: user.bio
|
||||
})
|
||||
.from(coin)
|
||||
.leftJoin(user, eq(coin.creatorId, user.id))
|
||||
.where(eq(coin.symbol, coinSymbol))
|
||||
.limit(1);
|
||||
|
||||
if (!coinData) {
|
||||
throw error(404, 'Coin not found');
|
||||
}
|
||||
if (!coinData) {
|
||||
throw error(404, 'Coin not found');
|
||||
}
|
||||
|
||||
const priceHistoryData = await db
|
||||
.select({
|
||||
price: priceHistory.price,
|
||||
timestamp: priceHistory.timestamp
|
||||
})
|
||||
.from(priceHistory)
|
||||
.where(eq(priceHistory.coinId, coinData.id))
|
||||
.orderBy(desc(priceHistory.timestamp))
|
||||
.limit(720);
|
||||
const [rawPriceHistory, rawTransactions] = await Promise.all([
|
||||
db.select({ price: priceHistory.price, timestamp: priceHistory.timestamp })
|
||||
.from(priceHistory)
|
||||
.where(eq(priceHistory.coinId, coinData.id))
|
||||
.orderBy(desc(priceHistory.timestamp))
|
||||
.limit(5000),
|
||||
|
||||
return json({
|
||||
coin: {
|
||||
...coinData,
|
||||
currentPrice: Number(coinData.currentPrice),
|
||||
marketCap: Number(coinData.marketCap),
|
||||
volume24h: Number(coinData.volume24h || 0),
|
||||
change24h: Number(coinData.change24h || 0),
|
||||
initialSupply: Number(coinData.initialSupply),
|
||||
circulatingSupply: Number(coinData.circulatingSupply),
|
||||
poolCoinAmount: Number(coinData.poolCoinAmount),
|
||||
poolBaseCurrencyAmount: Number(coinData.poolBaseCurrencyAmount)
|
||||
},
|
||||
priceHistory: priceHistoryData.map(p => ({
|
||||
db.select({
|
||||
totalBaseCurrencyAmount: transaction.totalBaseCurrencyAmount,
|
||||
timestamp: transaction.timestamp
|
||||
})
|
||||
.from(transaction)
|
||||
.where(eq(transaction.coinId, coinData.id))
|
||||
.orderBy(desc(transaction.timestamp))
|
||||
.limit(5000)
|
||||
]);
|
||||
|
||||
const priceData = rawPriceHistory.map(p => ({
|
||||
price: Number(p.price),
|
||||
timestamp: p.timestamp
|
||||
}))
|
||||
});
|
||||
}));
|
||||
|
||||
const transactionData = rawTransactions.map(t => ({
|
||||
totalBaseCurrencyAmount: Number(t.totalBaseCurrencyAmount),
|
||||
timestamp: t.timestamp
|
||||
}));
|
||||
|
||||
const candlestickData = aggregatePriceHistory(priceData, intervalMinutes);
|
||||
const volumeData = aggregateVolumeData(transactionData, intervalMinutes);
|
||||
|
||||
return json({
|
||||
coin: {
|
||||
...coinData,
|
||||
currentPrice: Number(coinData.currentPrice),
|
||||
marketCap: Number(coinData.marketCap),
|
||||
volume24h: Number(coinData.volume24h),
|
||||
change24h: Number(coinData.change24h),
|
||||
poolCoinAmount: Number(coinData.poolCoinAmount),
|
||||
poolBaseCurrencyAmount: Number(coinData.poolBaseCurrencyAmount),
|
||||
circulatingSupply: Number(coinData.circulatingSupply),
|
||||
initialSupply: Number(coinData.initialSupply)
|
||||
},
|
||||
candlestickData,
|
||||
volumeData,
|
||||
timeframe
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error fetching coin data:', e);
|
||||
throw error(500, 'Failed to fetch coin data');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
279
website/src/routes/api/coin/[coinSymbol]/trade/+server.ts
Normal file
279
website/src/routes/api/coin/[coinSymbol]/trade/+server.ts
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
import { auth } from '$lib/auth';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { coin, userPortfolio, user, transaction, priceHistory } from '$lib/server/db/schema';
|
||||
import { eq, and, gte } from 'drizzle-orm';
|
||||
|
||||
async function calculate24hMetrics(coinId: number, currentPrice: number) {
|
||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
// Get price from 24h ago
|
||||
const [priceData] = await db
|
||||
.select({ price: priceHistory.price })
|
||||
.from(priceHistory)
|
||||
.where(and(
|
||||
eq(priceHistory.coinId, coinId),
|
||||
gte(priceHistory.timestamp, twentyFourHoursAgo)
|
||||
))
|
||||
.orderBy(priceHistory.timestamp)
|
||||
.limit(1);
|
||||
|
||||
// Calculate 24h change
|
||||
let change24h = 0;
|
||||
if (priceData) {
|
||||
const priceFrom24hAgo = Number(priceData.price);
|
||||
if (priceFrom24hAgo > 0) {
|
||||
change24h = ((currentPrice - priceFrom24hAgo) / priceFrom24hAgo) * 100;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate 24h volume
|
||||
const volumeData = await db
|
||||
.select({ totalBaseCurrencyAmount: transaction.totalBaseCurrencyAmount })
|
||||
.from(transaction)
|
||||
.where(and(
|
||||
eq(transaction.coinId, coinId),
|
||||
gte(transaction.timestamp, twentyFourHoursAgo)
|
||||
));
|
||||
|
||||
const volume24h = volumeData.reduce((sum, tx) => sum + Number(tx.totalBaseCurrencyAmount), 0);
|
||||
|
||||
return { change24h: Number(change24h.toFixed(4)), volume24h: Number(volume24h.toFixed(4)) };
|
||||
}
|
||||
|
||||
export async function POST({ params, request }) {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
throw error(401, 'Not authenticated');
|
||||
}
|
||||
|
||||
const { coinSymbol } = params;
|
||||
const { type, amount } = await request.json();
|
||||
|
||||
if (!['BUY', 'SELL'].includes(type)) {
|
||||
throw error(400, 'Invalid transaction type');
|
||||
}
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
throw error(400, 'Invalid amount');
|
||||
}
|
||||
|
||||
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) {
|
||||
throw error(404, 'Coin not found');
|
||||
}
|
||||
|
||||
if (!coinData.isListed) {
|
||||
throw error(400, 'This coin is delisted and cannot be traded');
|
||||
}
|
||||
|
||||
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 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;
|
||||
|
||||
if (type === 'BUY') {
|
||||
// Calculate price impact for buying
|
||||
const k = poolCoinAmount * poolBaseCurrencyAmount;
|
||||
const newPoolBaseCurrency = poolBaseCurrencyAmount + (amount * currentPrice);
|
||||
const newPoolCoin = k / newPoolBaseCurrency;
|
||||
const coinsBought = poolCoinAmount - newPoolCoin;
|
||||
|
||||
totalCost = amount * currentPrice;
|
||||
newPrice = newPoolBaseCurrency / newPoolCoin;
|
||||
|
||||
if (userBalance < totalCost) {
|
||||
throw error(400, `Insufficient funds. You need $${totalCost.toFixed(2)} but only have $${userBalance.toFixed(2)}`);
|
||||
}
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
// Update user balance
|
||||
await tx.update(user)
|
||||
.set({
|
||||
baseCurrencyBalance: (userBalance - totalCost).toString(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.id, userId));
|
||||
|
||||
// Update user portfolio
|
||||
const [existingHolding] = await tx
|
||||
.select({ quantity: userPortfolio.quantity })
|
||||
.from(userPortfolio)
|
||||
.where(and(
|
||||
eq(userPortfolio.userId, userId),
|
||||
eq(userPortfolio.coinId, coinData.id)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (existingHolding) {
|
||||
const newQuantity = Number(existingHolding.quantity) + coinsBought;
|
||||
await tx.update(userPortfolio)
|
||||
.set({
|
||||
quantity: newQuantity.toString(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(and(
|
||||
eq(userPortfolio.userId, userId),
|
||||
eq(userPortfolio.coinId, coinData.id)
|
||||
));
|
||||
} else {
|
||||
await tx.insert(userPortfolio).values({
|
||||
userId,
|
||||
coinId: coinData.id,
|
||||
quantity: coinsBought.toString()
|
||||
});
|
||||
}
|
||||
|
||||
// Record transaction
|
||||
await tx.insert(transaction).values({
|
||||
userId,
|
||||
coinId: coinData.id,
|
||||
type: 'BUY',
|
||||
quantity: coinsBought.toString(),
|
||||
pricePerCoin: currentPrice.toString(),
|
||||
totalBaseCurrencyAmount: totalCost.toString()
|
||||
});
|
||||
|
||||
// Record price history
|
||||
await tx.insert(priceHistory).values({
|
||||
coinId: coinData.id,
|
||||
price: newPrice.toString()
|
||||
});
|
||||
|
||||
// Calculate and update 24h metrics
|
||||
const metrics = await calculate24hMetrics(coinData.id, newPrice);
|
||||
|
||||
await tx.update(coin)
|
||||
.set({
|
||||
currentPrice: newPrice.toString(),
|
||||
marketCap: (Number(coinData.circulatingSupply) * newPrice).toString(),
|
||||
poolCoinAmount: newPoolCoin.toString(),
|
||||
poolBaseCurrencyAmount: newPoolBaseCurrency.toString(),
|
||||
change24h: metrics.change24h.toString(),
|
||||
volume24h: metrics.volume24h.toString(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(coin.id, coinData.id));
|
||||
});
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
type: 'BUY',
|
||||
coinsBought,
|
||||
totalCost,
|
||||
newPrice,
|
||||
newBalance: userBalance - totalCost
|
||||
});
|
||||
|
||||
} else {
|
||||
// SELL logic
|
||||
const [userHolding] = await db
|
||||
.select({ quantity: userPortfolio.quantity })
|
||||
.from(userPortfolio)
|
||||
.where(and(
|
||||
eq(userPortfolio.userId, userId),
|
||||
eq(userPortfolio.coinId, coinData.id)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (!userHolding || Number(userHolding.quantity) < amount) {
|
||||
throw error(400, `Insufficient coins. You have ${userHolding ? Number(userHolding.quantity) : 0} but trying to sell ${amount}`);
|
||||
}
|
||||
|
||||
// Calculate price impact for selling
|
||||
const k = poolCoinAmount * poolBaseCurrencyAmount;
|
||||
const newPoolCoin = poolCoinAmount + amount;
|
||||
const newPoolBaseCurrency = k / newPoolCoin;
|
||||
const baseCurrencyReceived = poolBaseCurrencyAmount - newPoolBaseCurrency;
|
||||
|
||||
totalCost = baseCurrencyReceived;
|
||||
newPrice = newPoolBaseCurrency / newPoolCoin;
|
||||
|
||||
// Execute sell transaction
|
||||
await db.transaction(async (tx) => {
|
||||
// Update user balance
|
||||
await tx.update(user)
|
||||
.set({
|
||||
baseCurrencyBalance: (userBalance + totalCost).toString(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.id, userId));
|
||||
|
||||
// Update user portfolio
|
||||
const newQuantity = Number(userHolding.quantity) - amount;
|
||||
if (newQuantity > 0) {
|
||||
await tx.update(userPortfolio)
|
||||
.set({
|
||||
quantity: newQuantity.toString(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(and(
|
||||
eq(userPortfolio.userId, userId),
|
||||
eq(userPortfolio.coinId, coinData.id)
|
||||
));
|
||||
} else {
|
||||
await tx.delete(userPortfolio)
|
||||
.where(and(
|
||||
eq(userPortfolio.userId, userId),
|
||||
eq(userPortfolio.coinId, coinData.id)
|
||||
));
|
||||
}
|
||||
|
||||
// Record transaction
|
||||
await tx.insert(transaction).values({
|
||||
userId,
|
||||
coinId: coinData.id,
|
||||
type: 'SELL',
|
||||
quantity: amount.toString(),
|
||||
pricePerCoin: currentPrice.toString(),
|
||||
totalBaseCurrencyAmount: totalCost.toString()
|
||||
});
|
||||
|
||||
// Record price history
|
||||
await tx.insert(priceHistory).values({
|
||||
coinId: coinData.id,
|
||||
price: newPrice.toString()
|
||||
});
|
||||
|
||||
// Calculate and update 24h metrics - SINGLE coin table update
|
||||
const metrics = await calculate24hMetrics(coinData.id, newPrice);
|
||||
|
||||
await tx.update(coin)
|
||||
.set({
|
||||
currentPrice: newPrice.toString(),
|
||||
marketCap: (Number(coinData.circulatingSupply) * newPrice).toString(),
|
||||
poolCoinAmount: newPoolCoin.toString(),
|
||||
poolBaseCurrencyAmount: newPoolBaseCurrency.toString(),
|
||||
change24h: metrics.change24h.toString(),
|
||||
volume24h: metrics.volume24h.toString(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(coin.id, coinData.id));
|
||||
});
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
type: 'SELL',
|
||||
coinsSold: amount,
|
||||
totalReceived: totalCost,
|
||||
newPrice,
|
||||
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 } from '$lib/server/db/schema';
|
||||
import { coin, userPortfolio, user, priceHistory, transaction } 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';
|
||||
|
|
@ -125,6 +125,15 @@ export async function POST({ request }) {
|
|||
coinId: newCoin.id,
|
||||
price: STARTING_PRICE.toString()
|
||||
});
|
||||
|
||||
await tx.insert(transaction).values({
|
||||
userId,
|
||||
coinId: newCoin.id,
|
||||
type: 'BUY',
|
||||
quantity: FIXED_SUPPLY.toString(),
|
||||
pricePerCoin: STARTING_PRICE.toString(),
|
||||
totalBaseCurrencyAmount: (FIXED_SUPPLY * STARTING_PRICE).toString()
|
||||
});
|
||||
});
|
||||
|
||||
return json({
|
||||
|
|
|
|||
|
|
@ -1,37 +1,38 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { coin } from '$lib/server/db/schema';
|
||||
import { desc, eq } from 'drizzle-orm';
|
||||
import { eq, desc } from 'drizzle-orm';
|
||||
|
||||
export async function GET() {
|
||||
const topCoins = await db
|
||||
.select({
|
||||
id: coin.id,
|
||||
name: coin.name,
|
||||
symbol: coin.symbol,
|
||||
icon: coin.icon,
|
||||
currentPrice: coin.currentPrice,
|
||||
marketCap: coin.marketCap,
|
||||
volume24h: coin.volume24h,
|
||||
change24h: coin.change24h,
|
||||
isListed: coin.isListed
|
||||
})
|
||||
.from(coin)
|
||||
.where(eq(coin.isListed, true))
|
||||
.orderBy(desc(coin.marketCap))
|
||||
.limit(20);
|
||||
try {
|
||||
const coins = await db
|
||||
.select({
|
||||
symbol: coin.symbol,
|
||||
name: coin.name,
|
||||
icon: coin.icon,
|
||||
currentPrice: coin.currentPrice,
|
||||
change24h: coin.change24h, // Read directly from DB
|
||||
marketCap: coin.marketCap,
|
||||
volume24h: coin.volume24h // Read directly from DB
|
||||
})
|
||||
.from(coin)
|
||||
.where(eq(coin.isListed, true))
|
||||
.orderBy(desc(coin.marketCap))
|
||||
.limit(50);
|
||||
|
||||
return json({
|
||||
coins: topCoins.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
const formattedCoins = coins.map(c => ({
|
||||
symbol: c.symbol,
|
||||
name: c.name,
|
||||
icon: c.icon,
|
||||
price: Number(c.currentPrice),
|
||||
change24h: Number(c.change24h),
|
||||
marketCap: Number(c.marketCap),
|
||||
volume24h: Number(c.volume24h || 0),
|
||||
change24h: Number(c.change24h || 0),
|
||||
isListed: c.isListed
|
||||
}))
|
||||
});
|
||||
volume24h: Number(c.volume24h)
|
||||
}));
|
||||
|
||||
return json({ coins: formattedCoins });
|
||||
} catch (e) {
|
||||
console.error('Error fetching top coins:', e);
|
||||
return json({ coins: [] });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import { user, userPortfolio, coin } from '$lib/server/db/schema';
|
|||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers
|
||||
});
|
||||
const session = await auth.api.getSession({ headers: request.headers });
|
||||
|
||||
if (!session?.user) {
|
||||
throw error(401, 'Not authenticated');
|
||||
|
|
@ -15,27 +13,30 @@ export async function GET({ request }) {
|
|||
|
||||
const userId = Number(session.user.id);
|
||||
|
||||
const [userData] = await db
|
||||
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||
.from(user)
|
||||
.where(eq(user.id, userId))
|
||||
.limit(1);
|
||||
const [userData, holdings] = await Promise.all([
|
||||
db.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||
.from(user)
|
||||
.where(eq(user.id, userId))
|
||||
.limit(1),
|
||||
|
||||
if (!userData) {
|
||||
db.select({
|
||||
quantity: userPortfolio.quantity,
|
||||
currentPrice: coin.currentPrice,
|
||||
symbol: coin.symbol,
|
||||
icon: coin.icon,
|
||||
change24h: coin.change24h
|
||||
})
|
||||
.from(userPortfolio)
|
||||
.innerJoin(coin, eq(userPortfolio.coinId, coin.id))
|
||||
.where(eq(userPortfolio.userId, userId))
|
||||
]);
|
||||
|
||||
if (!userData[0]) {
|
||||
throw error(404, 'User not found');
|
||||
}
|
||||
|
||||
const holdings = await db
|
||||
.select({
|
||||
quantity: userPortfolio.quantity,
|
||||
currentPrice: coin.currentPrice,
|
||||
symbol: coin.symbol
|
||||
})
|
||||
.from(userPortfolio)
|
||||
.innerJoin(coin, eq(userPortfolio.coinId, coin.id))
|
||||
.where(eq(userPortfolio.userId, userId));
|
||||
|
||||
let totalCoinValue = 0;
|
||||
|
||||
const coinHoldings = holdings.map(holding => {
|
||||
const quantity = Number(holding.quantity);
|
||||
const price = Number(holding.currentPrice);
|
||||
|
|
@ -44,13 +45,15 @@ export async function GET({ request }) {
|
|||
|
||||
return {
|
||||
symbol: holding.symbol,
|
||||
icon: holding.icon,
|
||||
quantity,
|
||||
currentPrice: price,
|
||||
value
|
||||
value,
|
||||
change24h: Number(holding.change24h)
|
||||
};
|
||||
});
|
||||
|
||||
const baseCurrencyBalance = Number(userData.baseCurrencyBalance);
|
||||
const baseCurrencyBalance = Number(userData[0].baseCurrencyBalance);
|
||||
|
||||
return json({
|
||||
baseCurrencyBalance,
|
||||
|
|
|
|||
51
website/src/routes/api/transactions/+server.ts
Normal file
51
website/src/routes/api/transactions/+server.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { auth } from '$lib/auth';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { transaction, coin } from '$lib/server/db/schema';
|
||||
import { eq, desc } from 'drizzle-orm';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
throw error(401, 'Not authenticated');
|
||||
}
|
||||
|
||||
const userId = Number(session.user.id);
|
||||
|
||||
const transactions = await db
|
||||
.select({
|
||||
id: transaction.id,
|
||||
type: transaction.type,
|
||||
quantity: transaction.quantity,
|
||||
pricePerCoin: transaction.pricePerCoin,
|
||||
totalBaseCurrencyAmount: transaction.totalBaseCurrencyAmount,
|
||||
timestamp: transaction.timestamp,
|
||||
coinSymbol: coin.symbol,
|
||||
coinName: coin.name,
|
||||
coinIcon: coin.icon
|
||||
})
|
||||
.from(transaction)
|
||||
.innerJoin(coin, eq(transaction.coinId, coin.id))
|
||||
.where(eq(transaction.userId, userId))
|
||||
.orderBy(desc(transaction.timestamp))
|
||||
.limit(100);
|
||||
|
||||
return json({
|
||||
transactions: transactions.map(t => ({
|
||||
id: t.id,
|
||||
type: t.type,
|
||||
quantity: Number(t.quantity),
|
||||
pricePerCoin: Number(t.pricePerCoin),
|
||||
totalBaseCurrencyAmount: Number(t.totalBaseCurrencyAmount),
|
||||
timestamp: t.timestamp,
|
||||
coin: {
|
||||
symbol: t.coinSymbol,
|
||||
name: t.coinName,
|
||||
icon: t.coinIcon
|
||||
}
|
||||
}))
|
||||
});
|
||||
}
|
||||
Reference in a new issue