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

@ -8,7 +8,7 @@
import { USER_DATA } from '$lib/stores/user-data';
import { toast } from 'svelte-sonner';
let {
let {
open = $bindable(false),
type,
coin,
@ -26,16 +26,45 @@
let loading = $state(false);
let numericAmount = $derived(parseFloat(amount) || 0);
let estimatedCost = $derived(numericAmount * coin.currentPrice);
let currentPrice = $derived(coin.currentPrice || 0);
let maxSellableAmount = $derived(
type === 'SELL' && coin
? Math.min(userHolding, Math.floor(Number(coin.poolCoinAmount) * 0.995))
: userHolding
);
let estimatedResult = $derived(calculateEstimate(numericAmount, type, currentPrice));
let hasValidAmount = $derived(numericAmount > 0);
let userBalance = $derived($USER_DATA ? Number($USER_DATA.baseCurrencyBalance) : 0);
let hasEnoughFunds = $derived(
type === 'BUY'
? estimatedCost <= userBalance
: numericAmount <= userHolding
type === 'BUY' ? numericAmount <= userBalance : numericAmount <= userHolding
);
let canTrade = $derived(hasValidAmount && hasEnoughFunds && !loading);
function calculateEstimate(amount: number, tradeType: 'BUY' | 'SELL', price: number) {
if (!amount || !price || !coin) return { result: 0 };
const poolCoin = Number(coin.poolCoinAmount);
const poolBase = Number(coin.poolBaseCurrencyAmount);
if (poolCoin <= 0 || poolBase <= 0) return { result: 0 };
const k = poolCoin * poolBase;
if (tradeType === 'BUY') {
// AMM formula: how many coins for spending 'amount' dollars
const newPoolBase = poolBase + amount;
const newPoolCoin = k / newPoolBase;
return { result: poolCoin - newPoolCoin };
} else {
// AMM formula: how many dollars for selling 'amount' coins
const newPoolCoin = poolCoin + amount;
const newPoolBase = k / newPoolCoin;
return { result: poolBase - newPoolBase };
}
}
function handleClose() {
open = false;
amount = '';
@ -65,9 +94,10 @@
}
toast.success(`${type === 'BUY' ? 'Bought' : 'Sold'} successfully!`, {
description: type === 'BUY'
? `Purchased ${result.coinsBought.toFixed(2)} ${coin.symbol} for $${result.totalCost.toFixed(2)}`
: `Sold ${result.coinsSold.toFixed(2)} ${coin.symbol} for $${result.totalReceived.toFixed(2)}`
description:
type === 'BUY'
? `Purchased ${result.coinsBought.toFixed(6)} ${coin.symbol} for $${result.totalCost.toFixed(6)}`
: `Sold ${result.coinsSold.toFixed(6)} ${coin.symbol} for $${result.totalReceived.toFixed(6)}`
});
onSuccess?.();
@ -83,10 +113,10 @@
function setMaxAmount() {
if (type === 'SELL') {
amount = userHolding.toString();
amount = maxSellableAmount.toString();
} else if ($USER_DATA) {
const maxCoins = Math.floor(userBalance / coin.currentPrice * 100) / 100;
amount = maxCoins.toString();
// For BUY, max is user's balance
amount = userBalance.toString();
}
}
</script>
@ -111,58 +141,66 @@
<div class="space-y-4">
<!-- Amount Input -->
<div class="space-y-2">
<Label for="amount">Amount ({coin.symbol})</Label>
<Label for="amount">
{type === 'BUY' ? 'Amount to spend ($)' : `Amount (${coin.symbol})`}
</Label>
<div class="flex gap-2">
<Input
id="amount"
type="number"
step="0.01"
step={type === 'BUY' ? '0.01' : '1'}
min="0"
bind:value={amount}
placeholder="0.00"
class="flex-1"
/>
<Button variant="outline" size="sm" onclick={setMaxAmount}>
Max
</Button>
<Button variant="outline" size="sm" onclick={setMaxAmount}>Max</Button>
</div>
{#if type === 'SELL'}
<p class="text-muted-foreground text-xs">
Available: {userHolding.toFixed(2)} {coin.symbol}
Available: {userHolding.toFixed(6)}
{coin.symbol}
{#if maxSellableAmount < userHolding}
<br />Max sellable: {maxSellableAmount.toFixed(0)} {coin.symbol} (pool limit)
{/if}
</p>
{:else if $USER_DATA}
<p class="text-muted-foreground text-xs">
Balance: ${userBalance.toFixed(2)}
Balance: ${userBalance.toFixed(6)}
</p>
{/if}
</div>
<!-- Estimated Cost/Return -->
<!-- Estimated Cost/Return with explicit fees -->
{#if hasValidAmount}
<div class="bg-muted/50 rounded-lg p-3">
<div class="flex justify-between items-center">
<div class="flex items-center justify-between">
<span class="text-sm font-medium">
{type === 'BUY' ? 'Total Cost:' : 'You\'ll Receive:'}
{type === 'BUY' ? `${coin.symbol} you'll get:` : "You'll receive:"}
</span>
<span class="font-bold">
${estimatedCost.toFixed(2)}
{type === 'BUY'
? `~${estimatedResult.result.toFixed(6)} ${coin.symbol}`
: `~$${estimatedResult.result.toFixed(6)}`}
</span>
</div>
{#if !hasEnoughFunds}
<Badge variant="destructive" class="mt-2 text-xs">
{type === 'BUY' ? 'Insufficient funds' : 'Insufficient coins'}
</Badge>
{/if}
<p class="text-muted-foreground mt-1 text-xs">
AMM estimation - includes slippage from pool impact
</p>
</div>
{/if}
{#if !hasEnoughFunds && hasValidAmount}
<Badge variant="destructive" class="text-xs">
{type === 'BUY' ? 'Insufficient funds' : 'Insufficient coins'}
</Badge>
{/if}
</div>
<Dialog.Footer class="flex gap-2">
<Button variant="outline" onclick={handleClose} disabled={loading}>
Cancel
</Button>
<Button
onclick={handleTrade}
<Button variant="outline" onclick={handleClose} disabled={loading}>Cancel</Button>
<Button
onclick={handleTrade}
disabled={!canTrade}
variant={type === 'BUY' ? 'default' : 'destructive'}
>

View file

@ -14,9 +14,9 @@ export const user = pgTable("user", {
isBanned: boolean("is_banned").default(false),
banReason: text("ban_reason"),
baseCurrencyBalance: decimal("base_currency_balance", {
precision: 19,
scale: 4,
}).notNull().default("10000.0000"), // 10,000 *BUSS
precision: 20,
scale: 8,
}).notNull().default("10000.00000000"), // 10,000 *BUSS
bio: varchar("bio", { length: 160 }).default("Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”"),
username: varchar("username", { length: 30 }).notNull().unique(),
});
@ -63,14 +63,14 @@ export const coin = pgTable("coin", {
symbol: varchar("symbol", { length: 10 }).notNull().unique(),
icon: text("icon"), // New field for coin icon
creatorId: integer("creator_id").references(() => user.id, { onDelete: "set null", }), // Coin can exist even if creator is deleted
initialSupply: decimal("initial_supply", { precision: 28, scale: 8 }).notNull(),
circulatingSupply: decimal("circulating_supply", { precision: 28, scale: 8 }).notNull(),
currentPrice: decimal("current_price", { precision: 19, scale: 8 }).notNull(), // Price in base currency
marketCap: decimal("market_cap", { precision: 28, scale: 4 }).notNull(),
volume24h: decimal("volume_24h", { precision: 28, scale: 4 }).default("0.0000"),
change24h: decimal("change_24h", { precision: 8, scale: 4 }).default("0.0000"), // Percentage
poolCoinAmount: decimal("pool_coin_amount", { precision: 28, scale: 8 }).notNull().default("0.00000000"),
poolBaseCurrencyAmount: decimal("pool_base_currency_amount", { precision: 28, scale: 4, }).notNull().default("0.0000"),
initialSupply: decimal("initial_supply", { precision: 30, scale: 8 }).notNull(),
circulatingSupply: decimal("circulating_supply", { precision: 30, scale: 8 }).notNull(),
currentPrice: decimal("current_price", { precision: 20, scale: 8 }).notNull(), // Price in base currency
marketCap: decimal("market_cap", { precision: 30, scale: 2 }).notNull(),
volume24h: decimal("volume_24h", { precision: 30, scale: 2 }).default("0.00"),
change24h: decimal("change_24h", { precision: 10, scale: 4 }).default("0.0000"), // Percentage
poolCoinAmount: decimal("pool_coin_amount", { precision: 30, scale: 8 }).notNull().default("0.00000000"),
poolBaseCurrencyAmount: decimal("pool_base_currency_amount", { precision: 30, scale: 8, }).notNull().default("0.00000000"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
isListed: boolean("is_listed").default(true).notNull(),
@ -79,7 +79,7 @@ export const coin = pgTable("coin", {
export const userPortfolio = pgTable("user_portfolio", {
userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
quantity: decimal("quantity", { precision: 28, scale: 8 }).notNull(),
quantity: decimal("quantity", { precision: 30, scale: 8 }).notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => {
@ -94,15 +94,15 @@ export const transaction = pgTable("transaction", {
userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
type: transactionTypeEnum("type").notNull(),
quantity: decimal("quantity", { precision: 28, scale: 8 }).notNull(),
pricePerCoin: decimal("price_per_coin", { precision: 19, scale: 8 }).notNull(),
totalBaseCurrencyAmount: decimal("total_base_currency_amount", { precision: 28, scale: 4 }).notNull(),
quantity: decimal("quantity", { precision: 30, scale: 8 }).notNull(),
pricePerCoin: decimal("price_per_coin", { precision: 20, scale: 8 }).notNull(),
totalBaseCurrencyAmount: decimal("total_base_currency_amount", { precision: 30, scale: 8 }).notNull(),
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow(),
});
export const priceHistory = pgTable("price_history", {
id: serial("id").primaryKey(),
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
price: decimal("price", { precision: 19, scale: 8 }).notNull(),
price: decimal("price", { precision: 20, scale: 8 }).notNull(),
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow(),
});