2025-05-23 19:48:23 +03:00
< script lang = "ts" >
import * as Dialog from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Badge } from '$lib/components/ui/badge';
import { TrendingUp , TrendingDown , Loader2 } from 'lucide-svelte';
2025-05-27 14:54:19 +03:00
import { PORTFOLIO_DATA } from '$lib/stores/portfolio-data';
2025-05-23 19:48:23 +03:00
import { toast } from 'svelte-sonner';
2025-05-23 21:45:41 +03:00
let {
2025-05-23 19:48:23 +03:00
open = $bindable(false),
type,
coin,
userHolding = 0,
onSuccess
} = $props< {
open?: boolean;
type: 'BUY' | 'SELL';
coin: any;
userHolding?: number;
onSuccess?: () => void;
}>();
let amount = $state('');
let loading = $state(false);
let numericAmount = $derived(parseFloat(amount) || 0);
2025-05-23 21:45:41 +03:00
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));
2025-05-23 19:48:23 +03:00
let hasValidAmount = $derived(numericAmount > 0);
2025-05-27 14:54:19 +03:00
let userBalance = $derived($PORTFOLIO_DATA ? $PORTFOLIO_DATA.baseCurrencyBalance : 0);
2025-05-23 19:48:23 +03:00
let hasEnoughFunds = $derived(
2025-05-23 21:45:41 +03:00
type === 'BUY' ? numericAmount < = userBalance : numericAmount < = userHolding
2025-05-23 19:48:23 +03:00
);
let canTrade = $derived(hasValidAmount & & hasEnoughFunds & & !loading);
2025-05-23 21:45:41 +03:00
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 } ;
}
}
2025-05-23 19:48:23 +03:00
function handleClose() {
open = false;
amount = '';
loading = false;
}
async function handleTrade() {
if (!canTrade) return;
loading = true;
try {
const response = await fetch(`/api/coin/${ coin . symbol } /trade`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
type,
amount: numericAmount
})
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || 'Trade failed');
}
toast.success(`${ type === 'BUY' ? 'Bought' : 'Sold' } successfully!`, {
2025-05-23 21:45:41 +03:00
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 )} `
2025-05-23 19:48:23 +03:00
});
onSuccess?.();
handleClose();
} catch (e) {
toast.error('Trade failed', {
description: (e as Error).message
});
} finally {
loading = false;
}
}
function setMaxAmount() {
if (type === 'SELL') {
2025-05-23 21:45:41 +03:00
amount = maxSellableAmount.toString();
2025-05-27 14:54:19 +03:00
} else if ($PORTFOLIO_DATA) {
2025-05-23 21:45:41 +03:00
// For BUY, max is user's balance
amount = userBalance.toString();
2025-05-23 19:48:23 +03:00
}
}
< / script >
< Dialog.Root bind:open >
< Dialog.Content class = "sm:max-w-md" >
< Dialog.Header >
< Dialog.Title class = "flex items-center gap-2" >
{ #if type === 'BUY' }
< TrendingUp class = "h-5 w-5 text-green-500" / >
Buy { coin . symbol }
{ : else }
< TrendingDown class = "h-5 w-5 text-red-500" / >
Sell { coin . symbol }
{ /if }
< / Dialog.Title >
< Dialog.Description >
Current price: ${ coin . currentPrice . toFixed ( 6 )} per { coin . symbol }
< / Dialog.Description >
< / Dialog.Header >
< div class = "space-y-4" >
<!-- Amount Input -->
< div class = "space-y-2" >
2025-05-23 21:45:41 +03:00
< Label for = "amount" >
{ type === 'BUY' ? 'Amount to spend ($)' : `Amount ($ { coin . symbol }) ` }
< / Label >
2025-05-23 19:48:23 +03:00
< div class = "flex gap-2" >
< Input
id="amount"
type="number"
2025-05-23 21:45:41 +03:00
step={ type === 'BUY' ? '0.01' : '1' }
2025-05-23 19:48:23 +03:00
min="0"
bind:value={ amount }
placeholder="0.00"
class="flex-1"
/>
2025-05-23 21:45:41 +03:00
< Button variant = "outline" size = "sm" onclick = { setMaxAmount } > Max</Button >
2025-05-23 19:48:23 +03:00
< / div >
{ #if type === 'SELL' }
< p class = "text-muted-foreground text-xs" >
2025-05-23 21:45:41 +03:00
Available: { userHolding . toFixed ( 6 )}
{ coin . symbol }
{ #if maxSellableAmount < userHolding }
< br /> Max sellable: { maxSellableAmount . toFixed ( 0 )} { coin . symbol } (pool limit)
{ /if }
2025-05-23 19:48:23 +03:00
< / p >
2025-05-27 14:54:19 +03:00
{ :else if $PORTFOLIO_DATA }
2025-05-23 19:48:23 +03:00
< p class = "text-muted-foreground text-xs" >
2025-05-23 21:45:41 +03:00
Balance: ${ userBalance . toFixed ( 6 )}
2025-05-23 19:48:23 +03:00
< / p >
{ /if }
< / div >
2025-05-23 21:45:41 +03:00
<!-- Estimated Cost/Return with explicit fees -->
2025-05-23 19:48:23 +03:00
{ #if hasValidAmount }
< div class = "bg-muted/50 rounded-lg p-3" >
2025-05-23 21:45:41 +03:00
< div class = "flex items-center justify-between" >
2025-05-23 19:48:23 +03:00
< span class = "text-sm font-medium" >
2025-05-23 21:45:41 +03:00
{ type === 'BUY' ? `$ { coin . symbol } you 'll get:` : "You' ll receive : " }
2025-05-23 19:48:23 +03:00
< / span >
< span class = "font-bold" >
2025-05-23 21:45:41 +03:00
{ type === 'BUY'
? `~${ estimatedResult . result . toFixed ( 6 )} ${ coin . symbol } `
: `~$${ estimatedResult . result . toFixed ( 6 )} `}
2025-05-23 19:48:23 +03:00
< / span >
< / div >
2025-05-23 21:45:41 +03:00
< p class = "text-muted-foreground mt-1 text-xs" >
AMM estimation - includes slippage from pool impact
< / p >
2025-05-23 19:48:23 +03:00
< / div >
{ /if }
2025-05-23 21:45:41 +03:00
{ #if ! hasEnoughFunds && hasValidAmount }
< Badge variant = "destructive" class = "text-xs" >
{ type === 'BUY' ? 'Insufficient funds' : 'Insufficient coins' }
< / Badge >
{ /if }
2025-05-23 19:48:23 +03:00
< / div >
< Dialog.Footer class = "flex gap-2" >
2025-05-23 21:45:41 +03:00
< Button variant = "outline" onclick = { handleClose } disabled= { loading } > Cancel</ Button >
< Button
onclick={ handleTrade }
2025-05-23 19:48:23 +03:00
disabled={ ! canTrade }
variant={ type === 'BUY' ? 'default' : 'destructive' }
>
{ #if loading }
< Loader2 class = "h-4 w-4 animate-spin" / >
Processing...
{ : else }
{ type === 'BUY' ? 'Buy' : 'Sell' } { coin . symbol }
{ /if }
< / Button >
< / Dialog.Footer >
< / Dialog.Content >
< / Dialog.Root >