feat: update database schema for precision + AMM behavior

This commit is contained in:
Face 2025-05-23 21:45:41 +03:00
parent a278d0c6a5
commit 930d1f41d7
8 changed files with 893 additions and 108 deletions

View file

@ -87,23 +87,32 @@ export async function POST({ params, request }) {
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') {
// Calculate price impact for buying
// AMM BUY: amount = dollars to spend
const k = poolCoinAmount * poolBaseCurrencyAmount;
const newPoolBaseCurrency = poolBaseCurrencyAmount + (amount * currentPrice);
const newPoolBaseCurrency = poolBaseCurrencyAmount + amount;
const newPoolCoin = k / newPoolBaseCurrency;
const coinsBought = poolCoinAmount - newPoolCoin;
totalCost = amount * currentPrice;
totalCost = amount;
newPrice = newPoolBaseCurrency / newPoolCoin;
priceImpact = ((newPrice - currentPrice) / currentPrice) * 100;
if (userBalance < totalCost) {
throw error(400, `Insufficient funds. You need $${totalCost.toFixed(2)} but only have $${userBalance.toFixed(2)}`);
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 db.transaction(async (tx) => {
// Update user balance
await tx.update(user)
.set({
baseCurrencyBalance: (userBalance - totalCost).toString(),
@ -111,7 +120,6 @@ export async function POST({ params, request }) {
})
.where(eq(user.id, userId));
// Update user portfolio
const [existingHolding] = await tx
.select({ quantity: userPortfolio.quantity })
.from(userPortfolio)
@ -140,23 +148,20 @@ export async function POST({ params, request }) {
});
}
// Record transaction
await tx.insert(transaction).values({
userId,
coinId: coinData.id,
type: 'BUY',
quantity: coinsBought.toString(),
pricePerCoin: currentPrice.toString(),
pricePerCoin: (totalCost / coinsBought).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)
@ -178,11 +183,12 @@ export async function POST({ params, request }) {
coinsBought,
totalCost,
newPrice,
priceImpact,
newBalance: userBalance - totalCost
});
} else {
// SELL logic
// AMM SELL: amount = number of coins to sell
const [userHolding] = await db
.select({ quantity: userPortfolio.quantity })
.from(userPortfolio)
@ -196,7 +202,12 @@ export async function POST({ params, request }) {
throw error(400, `Insufficient coins. You have ${userHolding ? Number(userHolding.quantity) : 0} but trying to sell ${amount}`);
}
// Calculate price impact for selling
// 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`);
}
const k = poolCoinAmount * poolBaseCurrencyAmount;
const newPoolCoin = poolCoinAmount + amount;
const newPoolBaseCurrency = k / newPoolCoin;
@ -204,10 +215,18 @@ export async function POST({ params, request }) {
totalCost = baseCurrencyReceived;
newPrice = newPoolBaseCurrency / newPoolCoin;
priceImpact = ((newPrice - currentPrice) / currentPrice) * 100;
// 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.`);
}
if (totalCost <= 0) {
throw error(400, 'Trade amount results in zero base currency received');
}
// Execute sell transaction
await db.transaction(async (tx) => {
// Update user balance
await tx.update(user)
.set({
baseCurrencyBalance: (userBalance + totalCost).toString(),
@ -215,9 +234,8 @@ export async function POST({ params, request }) {
})
.where(eq(user.id, userId));
// Update user portfolio
const newQuantity = Number(userHolding.quantity) - amount;
if (newQuantity > 0) {
if (newQuantity > 0.000001) {
await tx.update(userPortfolio)
.set({
quantity: newQuantity.toString(),
@ -235,23 +253,20 @@ export async function POST({ params, request }) {
));
}
// Record transaction
await tx.insert(transaction).values({
userId,
coinId: coinData.id,
type: 'SELL',
quantity: amount.toString(),
pricePerCoin: currentPrice.toString(),
pricePerCoin: (totalCost / amount).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)
@ -273,6 +288,7 @@ export async function POST({ params, request }) {
coinsSold: amount,
totalReceived: totalCost,
newPrice,
priceImpact,
newBalance: userBalance + totalCost
});
}

View file

@ -115,25 +115,12 @@ export async function POST({ request }) {
createdCoin = newCoin;
await tx.insert(userPortfolio).values({
userId,
coinId: newCoin.id,
quantity: FIXED_SUPPLY.toString()
});
await tx.insert(priceHistory).values({
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({
@ -147,6 +134,7 @@ export async function POST({ request }) {
feePaid: CREATION_FEE,
liquidityDeposited: INITIAL_LIQUIDITY,
initialPrice: STARTING_PRICE,
supply: FIXED_SUPPLY
supply: FIXED_SUPPLY,
message: "Coin created! All tokens are in the liquidity pool. Buy some if you want to hold them."
});
}

View file

@ -112,7 +112,7 @@
function generateVolumeData(candlestickData: any[], volumeData: any[]) {
return candlestickData.map((candle, index) => {
// Find corresponding volume data for this time period
const volumePoint = volumeData.find(v => v.time === candle.time);
const volumePoint = volumeData.find((v) => v.time === candle.time);
const volume = volumePoint ? volumePoint.volume : 0;
return {
@ -179,10 +179,14 @@
priceFormat: { type: 'price', precision: 8, minMove: 0.00000001 }
});
const volumeSeries = chart.addSeries(HistogramSeries, {
priceFormat: { type: 'volume' },
priceScaleId: 'volume'
}, 1);
const volumeSeries = chart.addSeries(
HistogramSeries,
{
priceFormat: { type: 'volume' },
priceScaleId: 'volume'
},
1
);
const processedChartData = chartData.map((candle) => {
if (candle.open === candle.close) {
@ -224,7 +228,9 @@
});
function formatPrice(price: number): string {
if (price < 0.01) {
if (price < 0.000001) {
return price.toFixed(8);
} else if (price < 0.01) {
return price.toFixed(6);
} else if (price < 1) {
return price.toFixed(4);
@ -234,17 +240,21 @@
}
function formatMarketCap(value: number): string {
if (value >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;
if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
if (value >= 1e3) return `$${(value / 1e3).toFixed(2)}K`;
return `$${value.toFixed(2)}`;
const num = Number(value);
if (isNaN(num)) return '$0.00';
if (num >= 1e9) return `$${(num / 1e9).toFixed(2)}B`;
if (num >= 1e6) return `$${(num / 1e6).toFixed(2)}M`;
if (num >= 1e3) return `$${(num / 1e3).toFixed(2)}K`;
return `$${num.toFixed(2)}`;
}
function formatSupply(value: number): string {
if (value >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
if (value >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
if (value >= 1e3) return `${(value / 1e3).toFixed(2)}K`;
return value.toLocaleString();
const num = Number(value);
if (isNaN(num)) return '0';
if (num >= 1e9) return `${(num / 1e9).toFixed(2)}B`;
if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
return num.toLocaleString();
}
</script>
@ -314,7 +324,7 @@
<TrendingDown class="h-4 w-4 text-red-500" />
{/if}
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'}>
{coin.change24h >= 0 ? '+' : ''}{coin.change24h.toFixed(2)}%
{coin.change24h >= 0 ? '+' : ''}{Number(coin.change24h).toFixed(2)}%
</Badge>
</div>
</div>
@ -327,7 +337,7 @@
<HoverCard.Root>
<HoverCard.Trigger
class="flex cursor-pointer items-center gap-2 rounded-sm underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-8"
class="flex cursor-pointer items-center gap-1 rounded-sm underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-8"
onclick={() => goto(`/user/${coin.creatorId}`)}
>
<Avatar.Root class="h-4 w-4">
@ -411,7 +421,7 @@
<Card.Title>Trade {coin.symbol}</Card.Title>
{#if userHolding > 0}
<p class="text-muted-foreground text-sm">
You own: {userHolding.toFixed(2)}
You own: {formatSupply(userHolding)}
{coin.symbol}
</p>
{/if}
@ -465,9 +475,9 @@
</div>
<div class="flex justify-between">
<span class="text-muted-foreground text-sm">Base Currency:</span>
<span class="font-mono text-sm"
>${coin.poolBaseCurrencyAmount.toLocaleString()}</span
>
<span class="font-mono text-sm">
${Number(coin.poolBaseCurrencyAmount).toLocaleString()}
</span>
</div>
</div>
</div>
@ -476,13 +486,13 @@
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-muted-foreground text-sm">Total Liquidity:</span>
<span class="font-mono text-sm"
>${(coin.poolBaseCurrencyAmount * 2).toLocaleString()}</span
>
<span class="font-mono text-sm">
${(Number(coin.poolBaseCurrencyAmount) * 2).toLocaleString()}
</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground text-sm">Price Impact:</span>
<Badge variant="success" class="text-xs">Low</Badge>
<span class="text-muted-foreground text-sm">Current Price:</span>
<span class="font-mono text-sm">${formatPrice(coin.currentPrice)}</span>
</div>
</div>
</div>
@ -549,7 +559,7 @@
<TrendingDown class="h-4 w-4 text-red-500" />
{/if}
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="text-sm">
{coin.change24h >= 0 ? '+' : ''}{coin.change24h.toFixed(2)}%
{coin.change24h >= 0 ? '+' : ''}{Number(coin.change24h).toFixed(2)}%
</Badge>
</div>
</Card.Content>