This commit is contained in:
Gulshan Yadav 2026-02-02 15:00:13 +05:30
parent c32622f34f
commit f08eb965c2
15 changed files with 1306 additions and 189 deletions

View file

@ -33,7 +33,7 @@ import {
Eye,
ListPlus,
Activity,
Vault,
Timer,
ShieldCheck,
// Phase 7-16 icons
UserX,
@ -97,7 +97,7 @@ const governanceNavItems = [
const toolsNavItems = [
{ to: '/watch-only', label: 'Watch-Only', icon: Eye },
{ to: '/fee-analytics', label: 'Fee Analytics', icon: Activity },
{ to: '/vaults', label: 'Time Vaults', icon: Vault },
{ to: '/vaults', label: 'Time Vaults', icon: Timer },
{ to: '/recovery', label: 'Recovery', icon: ShieldCheck },
{ to: '/decoy', label: 'Decoy Wallets', icon: UserX },
{ to: '/mixer', label: 'Mixer', icon: Shuffle },

View file

@ -1,6 +1,43 @@
import { Bell, Info, AlertTriangle, Plus } from 'lucide-react';
import { useEffect, useState } from 'react';
import { Bell, Info, AlertCircle, Plus, RefreshCw, Loader2, X, Trash2, ToggleLeft, ToggleRight } from 'lucide-react';
import { useAlertsStore } from '../../store/alerts';
export default function AlertsDashboard() {
const {
alerts,
isLoading,
error,
listAlerts,
createAlert,
deleteAlert,
toggleAlert,
clearError,
} = useAlertsStore();
const [showCreateModal, setShowCreateModal] = useState(false);
const [asset, setAsset] = useState('SYN');
const [condition, setCondition] = useState<'above' | 'below'>('above');
const [targetPrice, setTargetPrice] = useState('');
const [notificationMethod, setNotificationMethod] = useState<'push' | 'email' | 'both'>('push');
useEffect(() => {
listAlerts();
}, [listAlerts]);
const handleCreateAlert = async () => {
if (!targetPrice) return;
try {
await createAlert(asset, condition, parseFloat(targetPrice), notificationMethod);
setShowCreateModal(false);
setTargetPrice('');
} catch {
// Error handled by store
}
};
const activeAlerts = alerts.filter(a => a.isEnabled && !a.isTriggered);
const triggeredAlerts = alerts.filter(a => a.isTriggered);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
@ -11,31 +48,125 @@ export default function AlertsDashboard() {
</h1>
<p className="text-gray-400 mt-1">Get notified when tokens hit your targets</p>
</div>
<button className="px-4 py-2 bg-synor-600 rounded-lg flex items-center gap-2 opacity-50 cursor-not-allowed">
<div className="flex gap-2">
<button
onClick={listAlerts}
className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
disabled={isLoading}
>
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <RefreshCw size={18} />}
</button>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-synor-600 rounded-lg flex items-center gap-2 hover:bg-synor-700"
>
<Plus size={18} />
New Alert
</button>
</div>
</div>
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3">
<AlertTriangle className="text-yellow-400 mt-0.5" />
{error && (
<div className="bg-red-500/20 border border-red-500/30 rounded-xl p-4 flex items-start gap-3">
<AlertCircle className="text-red-400 mt-0.5" />
<div className="flex-1">
<p className="font-medium text-red-200">Error</p>
<p className="text-sm text-red-200/70">{error}</p>
</div>
<button onClick={clearError} className="text-red-400 hover:text-red-300">×</button>
</div>
)}
{/* Active Alerts */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Active Alerts ({activeAlerts.length})</h3>
{activeAlerts.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<Bell size={32} className="mx-auto mb-2 opacity-50" />
<p>No active price alerts</p>
<button
onClick={() => setShowCreateModal(true)}
className="mt-2 text-synor-400 hover:text-synor-300 text-sm"
>
Create your first alert
</button>
</div>
) : (
<div className="space-y-2">
{activeAlerts.map((alert) => (
<div key={alert.id} className="p-4 bg-gray-800 rounded-lg flex justify-between items-center">
<div className="flex items-center gap-4">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
alert.condition === 'above' ? 'bg-green-500/20' : 'bg-red-500/20'
}`}>
<span className={alert.condition === 'above' ? 'text-green-400' : 'text-red-400'}>
{alert.condition === 'above' ? '↑' : '↓'}
</span>
</div>
<div>
<p className="font-medium text-yellow-200">Coming Soon</p>
<p className="text-sm text-yellow-200/70">
Set price targets and receive desktop notifications when your tokens
reach those prices.
<p className="font-medium">{alert.asset}</p>
<p className="text-sm text-gray-500">
{alert.condition === 'above' ? 'Above' : 'Below'} ${alert.targetPrice.toFixed(4)}
</p>
<p className="text-xs text-gray-600">
Current: ${alert.currentPrice.toFixed(4)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => toggleAlert(alert.id, !alert.isEnabled)}
className="p-2 hover:bg-gray-700 rounded"
>
{alert.isEnabled ? (
<ToggleRight size={20} className="text-synor-400" />
) : (
<ToggleLeft size={20} className="text-gray-500" />
)}
</button>
<button
onClick={() => deleteAlert(alert.id)}
className="p-2 text-red-400 hover:bg-red-500/20 rounded"
>
<Trash2 size={16} />
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Triggered Alerts */}
{triggeredAlerts.length > 0 && (
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Active Alerts</h3>
<div className="text-center py-8 text-gray-500">
<Bell size={32} className="mx-auto mb-2 opacity-50" />
<p>No price alerts set</p>
<h3 className="font-medium mb-4 text-yellow-400">Triggered Alerts ({triggeredAlerts.length})</h3>
<div className="space-y-2">
{triggeredAlerts.map((alert) => (
<div key={alert.id} className="p-4 bg-yellow-500/10 rounded-lg flex justify-between items-center">
<div className="flex items-center gap-4">
<Bell className="text-yellow-400" />
<div>
<p className="font-medium">{alert.asset}</p>
<p className="text-sm text-gray-400">
Reached ${alert.targetPrice.toFixed(4)} on{' '}
{alert.triggeredAt ? new Date(alert.triggeredAt * 1000).toLocaleString() : 'Unknown'}
</p>
</div>
</div>
<button
onClick={() => deleteAlert(alert.id)}
className="p-2 text-gray-400 hover:bg-gray-700 rounded"
>
<X size={16} />
</button>
</div>
))}
</div>
</div>
)}
{/* Alert Types Info */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Alert Types</h3>
<div className="grid grid-cols-3 gap-4">
@ -48,12 +179,91 @@ export default function AlertsDashboard() {
<p className="text-xs text-gray-500">Alert when price drops below target</p>
</div>
<div className="p-3 bg-gray-800 rounded-lg">
<p className="font-medium text-yellow-400">% Change</p>
<p className="text-xs text-gray-500">Alert on percentage movement</p>
<p className="font-medium text-yellow-400">Notification Methods</p>
<p className="text-xs text-gray-500">Push, Email, or Both</p>
</div>
</div>
</div>
{/* Create Alert Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-gray-900 rounded-xl p-6 w-full max-w-md border border-gray-700">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium">Create Price Alert</h3>
<button onClick={() => setShowCreateModal(false)} className="text-gray-400 hover:text-white">
<X size={20} />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-2">Asset</label>
<select
value={asset}
onChange={(e) => setAsset(e.target.value)}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
>
<option value="SYN">SYN</option>
<option value="BTC">BTC</option>
<option value="ETH">ETH</option>
</select>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Condition</label>
<div className="flex gap-2">
<button
onClick={() => setCondition('above')}
className={`flex-1 py-2 rounded-lg font-medium ${
condition === 'above' ? 'bg-green-600 text-white' : 'bg-gray-800 text-gray-400'
}`}
>
Above Price
</button>
<button
onClick={() => setCondition('below')}
className={`flex-1 py-2 rounded-lg font-medium ${
condition === 'below' ? 'bg-red-600 text-white' : 'bg-gray-800 text-gray-400'
}`}
>
Below Price
</button>
</div>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Target Price (USD)</label>
<input
type="number"
value={targetPrice}
onChange={(e) => setTargetPrice(e.target.value)}
placeholder="0.0000"
step="0.0001"
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Notification Method</label>
<select
value={notificationMethod}
onChange={(e) => setNotificationMethod(e.target.value as 'push' | 'email' | 'both')}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
>
<option value="push">Desktop Push</option>
<option value="email">Email</option>
<option value="both">Both</option>
</select>
</div>
<button
onClick={handleCreateAlert}
disabled={isLoading || !targetPrice}
className="w-full py-3 bg-synor-600 rounded-lg font-medium hover:bg-synor-700 disabled:opacity-50"
>
{isLoading ? <Loader2 className="animate-spin mx-auto" /> : 'Create Alert'}
</button>
</div>
</div>
</div>
)}
<div className="bg-gray-900/50 rounded-lg p-4 border border-gray-800 flex items-start gap-3">
<Info className="text-gray-500 mt-0.5" size={18} />
<p className="text-sm text-gray-400">

View file

@ -1,53 +1,55 @@
import { useState } from 'react';
import { Terminal, Info, Send } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { Terminal, Info, Send, Loader2 } from 'lucide-react';
import { useCliStore } from '../../store/cli';
export default function CliDashboard() {
const {
history,
isExecuting,
execute,
loadHistory,
clearOutput,
navigateHistory,
} = useCliStore();
const [input, setInput] = useState('');
const [history, setHistory] = useState<string[]>([
'> Welcome to Synor CLI Mode',
'> Type "help" for available commands',
'>',
]);
const outputRef = useRef<HTMLDivElement>(null);
const handleCommand = () => {
if (!input.trim()) return;
useEffect(() => {
loadHistory();
}, [loadHistory]);
const cmd = input.trim().toLowerCase();
let output = '';
switch (cmd) {
case 'help':
output = `Available commands:
help - Show this help
balance - Show wallet balance
address - Show wallet address
send - Send transaction (coming soon)
status - Network status
clear - Clear history`;
break;
case 'balance':
output = 'Balance: 0 SYN (feature coming soon)';
break;
case 'address':
output = 'Primary address: synor1... (feature coming soon)';
break;
case 'status':
output = 'Network: Testnet\nConnected: No\nBlock Height: -';
break;
case 'clear':
setHistory(['> Cleared', '>']);
setInput('');
return;
default:
output = `Unknown command: ${cmd}. Type "help" for available commands.`;
// Auto-scroll to bottom when history changes
useEffect(() => {
if (outputRef.current) {
outputRef.current.scrollTop = outputRef.current.scrollHeight;
}
}, [history]);
setHistory([...history, `> ${input}`, output, '>']);
const handleCommand = async () => {
if (!input.trim() || isExecuting) return;
const cmd = input.trim();
setInput('');
await execute(cmd);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleCommand();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const prev = navigateHistory('up');
if (prev !== null) setInput(prev);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
const next = navigateHistory('down');
if (next !== null) setInput(next);
}
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold flex items-center gap-3">
<Terminal className="text-synor-400" />
@ -55,14 +57,47 @@ export default function CliDashboard() {
</h1>
<p className="text-gray-400 mt-1">Terminal interface for power users</p>
</div>
<button
onClick={clearOutput}
className="px-3 py-1 bg-gray-800 rounded text-sm hover:bg-gray-700"
>
Clear
</button>
</div>
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
<div className="p-4 font-mono text-sm bg-black/50 h-96 overflow-y-auto">
{history.map((line, i) => (
<div key={i} className={line.startsWith('>') ? 'text-synor-400' : 'text-gray-300'}>
{line}
<div
ref={outputRef}
className="p-4 font-mono text-sm bg-black/50 h-96 overflow-y-auto"
>
{/* Welcome message if no history */}
{history.length === 0 && (
<div className="text-gray-500">
<p className="text-synor-400">{'>'} Welcome to Synor CLI Mode</p>
<p className="text-synor-400">{'>'} Type "help" for available commands</p>
<p className="text-synor-400">{'>'}</p>
</div>
)}
{/* Command history */}
{history.map((result, i) => (
<div key={i} className="mb-2">
<div className="text-synor-400">
{'>'} {result.command}
</div>
<div className={`whitespace-pre-wrap ${result.isError ? 'text-red-400' : 'text-gray-300'}`}>
{result.output}
</div>
</div>
))}
{/* Loading indicator */}
{isExecuting && (
<div className="flex items-center gap-2 text-gray-500">
<Loader2 size={14} className="animate-spin" />
<span>Executing...</span>
</div>
)}
</div>
<div className="border-t border-gray-800 p-3 flex gap-2">
@ -71,25 +106,76 @@ export default function CliDashboard() {
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleCommand()}
onKeyDown={handleKeyDown}
placeholder="Enter command..."
className="flex-1 bg-transparent text-white font-mono outline-none"
autoFocus
disabled={isExecuting}
/>
<button
onClick={handleCommand}
className="p-2 bg-synor-600 rounded-lg hover:bg-synor-700"
disabled={isExecuting || !input.trim()}
className="p-2 bg-synor-600 rounded-lg hover:bg-synor-700 disabled:opacity-50"
>
<Send size={16} />
{isExecuting ? <Loader2 size={16} className="animate-spin" /> : <Send size={16} />}
</button>
</div>
</div>
{/* Quick Commands */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-3">Quick Commands</h3>
<div className="flex flex-wrap gap-2">
{['help', 'balance', 'address', 'status', 'utxos', 'peers'].map((cmd) => (
<button
key={cmd}
onClick={() => { setInput(cmd); }}
className="px-3 py-1 bg-gray-800 rounded font-mono text-sm hover:bg-gray-700"
>
{cmd}
</button>
))}
</div>
</div>
{/* Command Reference */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-3">Command Reference</h3>
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="font-mono text-synor-400">help</div>
<div className="text-gray-400">Show all available commands</div>
<div className="font-mono text-synor-400">balance</div>
<div className="text-gray-400">Show wallet balance</div>
<div className="font-mono text-synor-400">address</div>
<div className="text-gray-400">Show wallet addresses</div>
<div className="font-mono text-synor-400">send &lt;addr&gt; &lt;amount&gt;</div>
<div className="text-gray-400">Send SYN to address</div>
<div className="font-mono text-synor-400">utxos</div>
<div className="text-gray-400">List unspent outputs</div>
<div className="font-mono text-synor-400">history</div>
<div className="text-gray-400">Transaction history</div>
<div className="font-mono text-synor-400">status</div>
<div className="text-gray-400">Network status</div>
<div className="font-mono text-synor-400">peers</div>
<div className="text-gray-400">Connected peers</div>
<div className="font-mono text-synor-400">clear</div>
<div className="text-gray-400">Clear output</div>
</div>
</div>
<div className="bg-gray-900/50 rounded-lg p-4 border border-gray-800 flex items-start gap-3">
<Info className="text-gray-500 mt-0.5" size={18} />
<p className="text-sm text-gray-400">
CLI mode provides a terminal interface for advanced users who prefer keyboard-driven
interaction. All wallet operations are available through commands.
interaction. Use / arrows to navigate command history.
</p>
</div>
</div>

View file

@ -1,6 +1,51 @@
import { ArrowUpDown, Info, AlertTriangle, Plus } from 'lucide-react';
import { useEffect, useState } from 'react';
import { ArrowUpDown, Info, AlertCircle, Plus, RefreshCw, Loader2, X, TrendingUp, TrendingDown } from 'lucide-react';
import { useLimitOrdersStore, formatAmount } from '../../store/limitOrders';
export default function LimitOrdersDashboard() {
const {
orders,
orderBook,
isLoading,
error,
listOrders,
getOrderBook,
createOrder,
cancelOrder,
clearError,
} = useLimitOrdersStore();
const [showCreateModal, setShowCreateModal] = useState(false);
const [orderType, setOrderType] = useState<'buy' | 'sell'>('buy');
const [tradingPair, setTradingPair] = useState('SYN/USDT');
const [amount, setAmount] = useState('');
const [price, setPrice] = useState('');
useEffect(() => {
listOrders();
getOrderBook('SYN/USDT');
}, [listOrders, getOrderBook]);
const handleCreateOrder = async () => {
if (!amount || !price) return;
try {
await createOrder(
orderType,
tradingPair,
parseFloat(amount) * 100_000_000,
parseFloat(price)
);
setShowCreateModal(false);
setAmount('');
setPrice('');
} catch {
// Error handled by store
}
};
const activeOrders = orders.filter(o => o.status === 'open');
const filledOrders = orders.filter(o => o.status === 'filled');
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
@ -11,33 +56,217 @@ export default function LimitOrdersDashboard() {
</h1>
<p className="text-gray-400 mt-1">Set buy/sell orders at specific prices</p>
</div>
<button className="px-4 py-2 bg-synor-600 rounded-lg flex items-center gap-2 opacity-50 cursor-not-allowed">
<div className="flex gap-2">
<button
onClick={() => { listOrders(); getOrderBook('SYN/USDT'); }}
className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
disabled={isLoading}
>
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <RefreshCw size={18} />}
</button>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-synor-600 rounded-lg flex items-center gap-2 hover:bg-synor-700"
>
<Plus size={18} />
New Order
</button>
</div>
</div>
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3">
<AlertTriangle className="text-yellow-400 mt-0.5" />
{error && (
<div className="bg-red-500/20 border border-red-500/30 rounded-xl p-4 flex items-start gap-3">
<AlertCircle className="text-red-400 mt-0.5" />
<div className="flex-1">
<p className="font-medium text-red-200">Error</p>
<p className="text-sm text-red-200/70">{error}</p>
</div>
<button onClick={clearError} className="text-red-400 hover:text-red-300">×</button>
</div>
)}
{/* Order Book */}
{orderBook && (
<div className="grid grid-cols-2 gap-4">
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-3 text-green-400 flex items-center gap-2">
<TrendingUp size={16} />
Buy Orders ({orderBook.bids.length})
</h3>
{orderBook.bids.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-4">No buy orders</p>
) : (
<div className="space-y-1">
{orderBook.bids.slice(0, 5).map((bid, i) => (
<div key={i} className="flex justify-between text-sm p-2 bg-green-500/10 rounded">
<span className="text-green-400">${bid.price.toFixed(4)}</span>
<span className="text-gray-400">{formatAmount(bid.amount)}</span>
</div>
))}
</div>
)}
</div>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-3 text-red-400 flex items-center gap-2">
<TrendingDown size={16} />
Sell Orders ({orderBook.asks.length})
</h3>
{orderBook.asks.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-4">No sell orders</p>
) : (
<div className="space-y-1">
{orderBook.asks.slice(0, 5).map((ask, i) => (
<div key={i} className="flex justify-between text-sm p-2 bg-red-500/10 rounded">
<span className="text-red-400">${ask.price.toFixed(4)}</span>
<span className="text-gray-400">{formatAmount(ask.amount)}</span>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* Active Orders */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Active Orders ({activeOrders.length})</h3>
{activeOrders.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-4">No active orders</p>
) : (
<div className="space-y-2">
{activeOrders.map((order) => (
<div key={order.id} className="p-3 bg-gray-800 rounded-lg flex justify-between items-center">
<div className="flex items-center gap-3">
<span className={`px-2 py-1 rounded text-xs ${
order.orderType === 'buy' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
}`}>
{order.orderType.toUpperCase()}
</span>
<div>
<p className="font-medium text-yellow-200">Coming Soon</p>
<p className="text-sm text-yellow-200/70">
Limit orders let you automate trades - set your target price and the order
executes automatically when the market reaches it.
<p className="font-medium">{order.pair}</p>
<p className="text-xs text-gray-500">
{formatAmount(order.amount)} @ ${order.price.toFixed(4)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-400">
{((order.filledAmount / order.amount) * 100).toFixed(0)}% filled
</span>
<button
onClick={() => cancelOrder(order.id)}
className="p-1 text-red-400 hover:bg-red-500/20 rounded"
>
<X size={16} />
</button>
</div>
</div>
))}
</div>
)}
</div>
<div className="grid grid-cols-2 gap-4">
{/* Order History */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-2 text-green-400">Buy Orders</h3>
<p className="text-sm text-gray-500">No active buy orders</p>
</div>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-2 text-red-400">Sell Orders</h3>
<p className="text-sm text-gray-500">No active sell orders</p>
<h3 className="font-medium mb-4">Filled Orders ({filledOrders.length})</h3>
{filledOrders.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-4">No filled orders</p>
) : (
<div className="space-y-2">
{filledOrders.slice(0, 10).map((order) => (
<div key={order.id} className="p-3 bg-gray-800 rounded-lg flex justify-between items-center">
<div className="flex items-center gap-3">
<span className={`px-2 py-1 rounded text-xs ${
order.orderType === 'buy' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
}`}>
{order.orderType.toUpperCase()}
</span>
<div>
<p className="font-medium">{order.pair}</p>
<p className="text-xs text-gray-500">{formatAmount(order.amount)}</p>
</div>
</div>
<span className="text-green-400 text-sm">Filled</span>
</div>
))}
</div>
)}
</div>
{/* Create Order Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-gray-900 rounded-xl p-6 w-full max-w-md border border-gray-700">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium">Create Limit Order</h3>
<button onClick={() => setShowCreateModal(false)} className="text-gray-400 hover:text-white">
<X size={20} />
</button>
</div>
<div className="space-y-4">
<div className="flex gap-2">
<button
onClick={() => setOrderType('buy')}
className={`flex-1 py-2 rounded-lg font-medium ${
orderType === 'buy' ? 'bg-green-600 text-white' : 'bg-gray-800 text-gray-400'
}`}
>
Buy
</button>
<button
onClick={() => setOrderType('sell')}
className={`flex-1 py-2 rounded-lg font-medium ${
orderType === 'sell' ? 'bg-red-600 text-white' : 'bg-gray-800 text-gray-400'
}`}
>
Sell
</button>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Trading Pair</label>
<select
value={tradingPair}
onChange={(e) => setTradingPair(e.target.value)}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
>
<option value="SYN/USDT">SYN/USDT</option>
<option value="SYN/BTC">SYN/BTC</option>
</select>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Amount (SYN)</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.00"
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Price (USD)</label>
<input
type="number"
value={price}
onChange={(e) => setPrice(e.target.value)}
placeholder="0.0000"
step="0.0001"
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
/>
</div>
<button
onClick={handleCreateOrder}
disabled={isLoading || !amount || !price}
className={`w-full py-3 rounded-lg font-medium disabled:opacity-50 ${
orderType === 'buy' ? 'bg-green-600 hover:bg-green-700' : 'bg-red-600 hover:bg-red-700'
}`}
>
{isLoading ? <Loader2 className="animate-spin mx-auto" /> : `Place ${orderType.toUpperCase()} Order`}
</button>
</div>
</div>
</div>
)}
<div className="bg-gray-900/50 rounded-lg p-4 border border-gray-800 flex items-start gap-3">
<Info className="text-gray-500 mt-0.5" size={18} />

View file

@ -1,8 +1,60 @@
import { Shuffle, Info, AlertTriangle } from 'lucide-react';
import { useEffect, useState } from 'react';
import { Shuffle, Info, AlertCircle, RefreshCw, Loader2, X } from 'lucide-react';
import { useMixerStore, formatAmount, formatDenomination } from '../../store/mixer';
export default function MixerDashboard() {
const {
denominations,
poolStatus,
requests,
isLoading,
error,
loadDenominations,
getPoolStatus,
createRequest,
listRequests,
cancelRequest,
clearError,
} = useMixerStore();
const [selectedDenom, setSelectedDenom] = useState<number | null>(null);
const [outputAddress, setOutputAddress] = useState('');
useEffect(() => {
loadDenominations();
listRequests();
}, [loadDenominations, listRequests]);
useEffect(() => {
// Load pool status for first denomination when available
if (denominations.length > 0 && !selectedDenom) {
setSelectedDenom(denominations[0]);
getPoolStatus(denominations[0]);
}
}, [denominations, selectedDenom, getPoolStatus]);
const handleSelectDenom = (denom: number) => {
setSelectedDenom(denom);
getPoolStatus(denom);
};
const handleCreateRequest = async () => {
if (!selectedDenom || !outputAddress) return;
try {
await createRequest(selectedDenom, selectedDenom, outputAddress);
setOutputAddress('');
} catch {
// Error handled by store
}
};
const pendingRequests = requests.filter(r => r.status === 'pending' || r.status === 'mixing');
const completedRequests = requests.filter(r => r.status === 'completed');
const currentPool = selectedDenom ? poolStatus[selectedDenom] : null;
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold flex items-center gap-3">
<Shuffle className="text-synor-400" />
@ -10,36 +62,189 @@ export default function MixerDashboard() {
</h1>
<p className="text-gray-400 mt-1">Enhanced privacy through coin mixing</p>
</div>
<button
onClick={() => { loadDenominations(); listRequests(); }}
className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
disabled={isLoading}
>
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <RefreshCw size={18} />}
</button>
</div>
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3">
<AlertTriangle className="text-yellow-400 mt-0.5" />
<div>
<p className="font-medium text-yellow-200">Coming Soon</p>
<p className="text-sm text-yellow-200/70">
Transaction mixing will allow you to break the link between source and destination
addresses, enhancing privacy for your transactions.
{error && (
<div className="bg-red-500/20 border border-red-500/30 rounded-xl p-4 flex items-start gap-3">
<AlertCircle className="text-red-400 mt-0.5" />
<div className="flex-1">
<p className="font-medium text-red-200">Error</p>
<p className="text-sm text-red-200/70">{error}</p>
</div>
<button onClick={clearError} className="text-red-400 hover:text-red-300">×</button>
</div>
)}
{/* Denomination Selection */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-3">Select Mixing Denomination</h3>
<div className="flex flex-wrap gap-2">
{denominations.length === 0 ? (
<p className="text-sm text-gray-500">Loading denominations...</p>
) : (
denominations.map((denom) => (
<button
key={denom}
onClick={() => handleSelectDenom(denom)}
className={`px-4 py-2 rounded-lg font-medium transition ${
selectedDenom === denom
? 'bg-synor-600 text-white'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
}`}
>
{formatDenomination(denom)}
</button>
))
)}
</div>
</div>
{/* Pool Status */}
{currentPool && (
<div className="grid grid-cols-4 gap-4">
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<p className="text-sm text-gray-400 mb-1">Denomination</p>
<p className="text-xl font-bold">{formatDenomination(currentPool.denomination)}</p>
</div>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<p className="text-sm text-gray-400 mb-1">Participants</p>
<p className="text-xl font-bold">{currentPool.participants}/{currentPool.requiredParticipants}</p>
</div>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<p className="text-sm text-gray-400 mb-1">Status</p>
<p className={`text-xl font-bold ${
currentPool.status === 'mixing' ? 'text-yellow-400' :
currentPool.status === 'completed' ? 'text-green-400' : 'text-gray-400'
}`}>
{currentPool.status.charAt(0).toUpperCase() + currentPool.status.slice(1)}
</p>
</div>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<p className="text-sm text-gray-400 mb-1">Est. Time</p>
<p className="text-xl font-bold text-synor-400">
{currentPool.estimatedTimeSecs ? `${Math.ceil(currentPool.estimatedTimeSecs / 60)}m` : '--'}
</p>
</div>
</div>
)}
{/* Create Mix Request */}
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<h3 className="font-medium mb-4">Join Mixing Pool</h3>
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-2">Selected Amount</label>
<div className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-lg font-bold">
{selectedDenom ? formatDenomination(selectedDenom) : 'Select a denomination above'}
</div>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Output Address</label>
<input
type="text"
value={outputAddress}
onChange={(e) => setOutputAddress(e.target.value)}
placeholder="synor1... (fresh address for privacy)"
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:border-synor-500 outline-none font-mono text-sm"
/>
<p className="text-xs text-gray-500 mt-1">Use a new address that hasn't received funds before</p>
</div>
<button
onClick={handleCreateRequest}
disabled={isLoading || !selectedDenom || !outputAddress}
className="w-full py-3 bg-synor-600 rounded-lg font-medium hover:bg-synor-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <Shuffle size={18} />}
Join Pool
</button>
</div>
</div>
{/* Pending Requests */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4 text-yellow-400">Pending Mixes ({pendingRequests.length})</h3>
{pendingRequests.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-4">No pending mix requests</p>
) : (
<div className="space-y-2">
{pendingRequests.map((req) => (
<div key={req.id} className="p-3 bg-gray-800 rounded-lg flex justify-between items-center">
<div>
<p className="font-mono text-sm">{formatAmount(req.amount)}</p>
<p className="text-xs text-gray-500">Status: {req.status}</p>
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs ${
req.status === 'mixing' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-gray-700 text-gray-400'
}`}>
{req.status}
</span>
{req.status === 'pending' && (
<button
onClick={() => cancelRequest(req.id)}
className="p-1 text-red-400 hover:bg-red-500/20 rounded"
>
<X size={16} />
</button>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* Completed Requests */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4 text-green-400">Completed Mixes ({completedRequests.length})</h3>
{completedRequests.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-4">No completed mixes</p>
) : (
<div className="space-y-2">
{completedRequests.map((req) => (
<div key={req.id} className="p-3 bg-gray-800 rounded-lg flex justify-between items-center">
<div>
<p className="font-mono text-sm">{formatAmount(req.amount)}</p>
<p className="text-xs text-gray-500 font-mono truncate max-w-xs"> {req.outputAddress}</p>
{req.txId && (
<p className="text-xs text-synor-400 font-mono truncate max-w-xs">TX: {req.txId}</p>
)}
</div>
<span className="px-2 py-1 bg-green-500/20 text-green-400 rounded text-xs">
Complete
</span>
</div>
))}
</div>
)}
</div>
{/* How It Works */}
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<h3 className="font-medium mb-4">How Mixing Works</h3>
<ol className="space-y-3 text-sm text-gray-400">
<li className="flex gap-3">
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs">1</span>
<span>Deposit funds into the mixing pool</span>
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs flex-shrink-0">1</span>
<span>Select a denomination and provide an output address</span>
</li>
<li className="flex gap-3">
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs">2</span>
<span>Your funds are combined with others</span>
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs flex-shrink-0">2</span>
<span>Wait for enough participants to join the pool</span>
</li>
<li className="flex gap-3">
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs">3</span>
<span>After a delay, withdraw to a fresh address</span>
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs flex-shrink-0">3</span>
<span>All participants' coins are mixed together cryptographically</span>
</li>
<li className="flex gap-3">
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs">4</span>
<span>Transaction history is broken</span>
<span className="w-6 h-6 bg-synor-600 rounded-full flex items-center justify-center text-white text-xs flex-shrink-0">4</span>
<span>Receive coins at your output address with broken transaction history</span>
</li>
</ol>
</div>
@ -48,7 +253,7 @@ export default function MixerDashboard() {
<Info className="text-gray-500 mt-0.5" size={18} />
<p className="text-sm text-gray-400">
Mixing uses cryptographic techniques to make transactions untraceable while remaining
fully on-chain and trustless.
fully on-chain and trustless. Fixed denominations ensure all outputs look identical.
</p>
</div>
</div>

View file

@ -1,6 +1,48 @@
import { PieChart, Info, AlertTriangle, Download } from 'lucide-react';
import { useEffect, useState } from 'react';
import { PieChart, Info, AlertCircle, Download, RefreshCw, Loader2, TrendingUp, TrendingDown } from 'lucide-react';
import { usePortfolioStore, formatUSD, formatAmount } from '../../store/portfolio';
export default function PortfolioDashboard() {
const {
summary,
holdings,
taxReport,
isLoading,
error,
loadSummary,
loadHoldings,
loadTaxReport,
exportTaxReport,
clearError,
} = usePortfolioStore();
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
useEffect(() => {
loadSummary();
loadHoldings();
}, [loadSummary, loadHoldings]);
const handleLoadTaxReport = async () => {
await loadTaxReport(selectedYear);
};
const handleExportTaxReport = async () => {
try {
const data = await exportTaxReport(selectedYear, 'csv');
// Create a download link
const blob = new Blob([data], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `tax-report-${selectedYear}.csv`;
a.click();
window.URL.revokeObjectURL(url);
} catch {
// Error handled by store
}
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
@ -11,62 +53,184 @@ export default function PortfolioDashboard() {
</h1>
<p className="text-gray-400 mt-1">P&L tracking and tax reports</p>
</div>
<button className="px-4 py-2 bg-gray-800 rounded-lg flex items-center gap-2 opacity-50 cursor-not-allowed">
<div className="flex gap-2">
<button
onClick={() => { loadSummary(); loadHoldings(); }}
className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
disabled={isLoading}
>
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <RefreshCw size={18} />}
</button>
<button
onClick={handleExportTaxReport}
className="px-4 py-2 bg-gray-800 rounded-lg flex items-center gap-2 hover:bg-gray-700"
>
<Download size={18} />
Export Report
Export CSV
</button>
</div>
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3">
<AlertTriangle className="text-yellow-400 mt-0.5" />
<div>
<p className="font-medium text-yellow-200">Coming Soon</p>
<p className="text-sm text-yellow-200/70">
Track your profit & loss, view portfolio allocation, and generate tax reports
for your crypto holdings.
</p>
</div>
</div>
{error && (
<div className="bg-red-500/20 border border-red-500/30 rounded-xl p-4 flex items-start gap-3">
<AlertCircle className="text-red-400 mt-0.5" />
<div className="flex-1">
<p className="font-medium text-red-200">Error</p>
<p className="text-sm text-red-200/70">{error}</p>
</div>
<button onClick={clearError} className="text-red-400 hover:text-red-300">×</button>
</div>
)}
{/* Summary Stats */}
{summary && (
<div className="grid grid-cols-4 gap-4">
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<p className="text-sm text-gray-400 mb-1">Total Value</p>
<p className="text-2xl font-bold">$0.00</p>
<p className="text-2xl font-bold">{formatUSD(summary.totalValueUsd)}</p>
</div>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<p className="text-sm text-gray-400 mb-1">24h Change</p>
<p className="text-2xl font-bold text-gray-500">0%</p>
<p className={`text-2xl font-bold flex items-center gap-1 ${
summary.dayChangePercent >= 0 ? 'text-green-400' : 'text-red-400'
}`}>
{summary.dayChangePercent >= 0 ? <TrendingUp size={20} /> : <TrendingDown size={20} />}
{summary.dayChangePercent >= 0 ? '+' : ''}{summary.dayChangePercent.toFixed(2)}%
</p>
<p className={`text-xs ${summary.dayChangeUsd >= 0 ? 'text-green-400/70' : 'text-red-400/70'}`}>
{formatUSD(summary.dayChangeUsd)}
</p>
</div>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<p className="text-sm text-gray-400 mb-1">Unrealized P&L</p>
<p className="text-2xl font-bold text-gray-500">$0.00</p>
<p className="text-sm text-gray-400 mb-1">Total P&L</p>
<p className={`text-2xl font-bold ${
summary.totalPnlUsd >= 0 ? 'text-green-400' : 'text-red-400'
}`}>
{summary.totalPnlUsd >= 0 ? '+' : ''}{formatUSD(summary.totalPnlUsd)}
</p>
<p className={`text-xs ${summary.totalPnlPercent >= 0 ? 'text-green-400/70' : 'text-red-400/70'}`}>
{summary.totalPnlPercent >= 0 ? '+' : ''}{summary.totalPnlPercent.toFixed(2)}%
</p>
</div>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<p className="text-sm text-gray-400 mb-1">Realized P&L</p>
<p className="text-2xl font-bold text-gray-500">$0.00</p>
<p className="text-sm text-gray-400 mb-1">Cost Basis</p>
<p className="text-2xl font-bold">{formatUSD(summary.totalCostBasisUsd)}</p>
</div>
</div>
)}
{/* Holdings */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Asset Holdings ({holdings.length})</h3>
{holdings.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-4">No holdings found</p>
) : (
<div className="space-y-2">
{holdings.map((holding) => (
<div key={holding.asset} className="p-4 bg-gray-800 rounded-lg flex justify-between items-center">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-synor-600 rounded-full flex items-center justify-center font-bold">
{holding.symbol.substring(0, 2)}
</div>
<div>
<p className="font-medium">{holding.asset}</p>
<p className="text-sm text-gray-500">{holding.balanceFormatted}</p>
</div>
</div>
<div className="text-right">
<p className="font-medium">{formatUSD(holding.valueUsd)}</p>
<p className={`text-sm ${holding.pnlPercent >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{holding.pnlPercent >= 0 ? '+' : ''}{holding.pnlPercent.toFixed(2)}%
</p>
</div>
<div className="text-right">
<p className="text-sm text-gray-400">Allocation</p>
<div className="flex items-center gap-2">
<div className="w-20 h-2 bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full bg-synor-500 rounded-full"
style={{ width: `${holding.allocationPercent}%` }}
/>
</div>
<span className="text-sm">{holding.allocationPercent.toFixed(1)}%</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
<div className="grid grid-cols-2 gap-4">
{/* Tax Report Section */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Asset Allocation</h3>
<div className="h-48 flex items-center justify-center text-gray-500">
Chart coming soon
<div className="flex justify-between items-center mb-4">
<h3 className="font-medium">Tax Report</h3>
<div className="flex items-center gap-2">
<select
value={selectedYear}
onChange={(e) => setSelectedYear(parseInt(e.target.value))}
className="px-3 py-1 bg-gray-800 border border-gray-700 rounded-lg text-sm"
>
{[2026, 2025, 2024, 2023].map(year => (
<option key={year} value={year}>{year}</option>
))}
</select>
<button
onClick={handleLoadTaxReport}
className="px-3 py-1 bg-synor-600 rounded-lg text-sm hover:bg-synor-700"
disabled={isLoading}
>
{isLoading ? <Loader2 className="animate-spin" size={14} /> : 'Load'}
</button>
</div>
</div>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Performance History</h3>
<div className="h-48 flex items-center justify-center text-gray-500">
Chart coming soon
{taxReport.length === 0 ? (
<div className="text-center py-4">
<p className="text-sm text-gray-500 mb-2">Select a year and click Load to generate your tax report</p>
</div>
) : (
<div className="space-y-2">
<div className="grid grid-cols-6 text-xs text-gray-500 p-2">
<span>Date</span>
<span>Type</span>
<span>Asset</span>
<span>Amount</span>
<span>Total</span>
<span>Gain/Loss</span>
</div>
{taxReport.slice(0, 10).map((tx) => (
<div key={tx.id} className="grid grid-cols-6 text-sm p-2 bg-gray-800 rounded items-center">
<span>{new Date(tx.timestamp * 1000).toLocaleDateString()}</span>
<span className={
tx.txType === 'sell' ? 'text-red-400' :
tx.txType === 'buy' ? 'text-green-400' :
'text-yellow-400'
}>
{tx.txType.toUpperCase()}
</span>
<span>{tx.asset}</span>
<span>{formatAmount(tx.amount)}</span>
<span>{formatUSD(tx.totalUsd)}</span>
<span className={tx.gainLossUsd && tx.gainLossUsd >= 0 ? 'text-green-400' : 'text-red-400'}>
{tx.gainLossUsd !== undefined ? formatUSD(tx.gainLossUsd) : '--'}
{tx.isLongTerm && <span className="text-xs ml-1">(LT)</span>}
</span>
</div>
))}
{taxReport.length > 10 && (
<p className="text-center text-sm text-gray-500 pt-2">
Showing 10 of {taxReport.length} transactions. Export to see all.
</p>
)}
</div>
)}
</div>
<div className="bg-gray-900/50 rounded-lg p-4 border border-gray-800 flex items-start gap-3">
<Info className="text-gray-500 mt-0.5" size={18} />
<p className="text-sm text-gray-400">
Portfolio analytics helps you understand your investment performance and simplifies
tax reporting with exportable transaction history.
tax reporting with exportable transaction history. LT = Long-term capital gains.
</p>
</div>
</div>

View file

@ -1,8 +1,6 @@
import { useEffect, useState } from 'react';
import {
Vault as VaultIcon,
Plus,
Clock,
Lock,
Unlock,
Trash2,
@ -13,6 +11,9 @@ import {
Timer,
Info,
} from 'lucide-react';
// Using Timer as VaultIcon since lucide-react doesn't export Vault
const VaultIcon = Timer;
import {
useVaultsStore,
Vault,

View file

@ -1,6 +1,51 @@
import { TrendingUp, Info, AlertTriangle, RefreshCw } from 'lucide-react';
import { useEffect, useState } from 'react';
import { TrendingUp, Info, AlertCircle, RefreshCw, Loader2, Plus, Percent, X } from 'lucide-react';
import { useYieldStore, formatAmount } from '../../store/yield';
export default function YieldDashboard() {
const {
opportunities,
positions,
isLoading,
error,
loadOpportunities,
listPositions,
deposit,
withdraw,
clearError,
} = useYieldStore();
const [selectedOpportunity, setSelectedOpportunity] = useState<string | null>(null);
const [depositAmount, setDepositAmount] = useState('');
const [autoCompound, setAutoCompound] = useState(true);
const [showDepositModal, setShowDepositModal] = useState(false);
useEffect(() => {
loadOpportunities();
listPositions();
}, [loadOpportunities, listPositions]);
const handleDeposit = async () => {
if (!selectedOpportunity || !depositAmount) return;
try {
await deposit(selectedOpportunity, parseFloat(depositAmount) * 100_000_000, autoCompound);
setShowDepositModal(false);
setDepositAmount('');
setSelectedOpportunity(null);
} catch {
// Error handled by store
}
};
const totalDeposited = positions.reduce((sum, p) => sum + p.depositedAmount, 0);
const totalEarned = positions.reduce((sum, p) => sum + p.rewardsEarned, 0);
const bestApy = opportunities.length > 0
? Math.max(...opportunities.map(o => o.apy))
: 0;
// Helper to get opportunity details for a position
const getOpportunity = (oppId: string) => opportunities.find(o => o.id === oppId);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
@ -11,43 +56,193 @@ export default function YieldDashboard() {
</h1>
<p className="text-gray-400 mt-1">Auto-compound and find the best APY</p>
</div>
<button className="p-2 bg-gray-800 rounded-lg">
<RefreshCw size={18} />
<button
onClick={() => { loadOpportunities(); listPositions(); }}
className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
disabled={isLoading}
>
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <RefreshCw size={18} />}
</button>
</div>
<div className="bg-yellow-500/20 border border-yellow-500/30 rounded-xl p-4 flex items-start gap-3">
<AlertTriangle className="text-yellow-400 mt-0.5" />
<div>
<p className="font-medium text-yellow-200">Coming Soon</p>
<p className="text-sm text-yellow-200/70">
The yield aggregator automatically finds the best yields across DeFi protocols
and compounds your earnings.
</p>
{error && (
<div className="bg-red-500/20 border border-red-500/30 rounded-xl p-4 flex items-start gap-3">
<AlertCircle className="text-red-400 mt-0.5" />
<div className="flex-1">
<p className="font-medium text-red-200">Error</p>
<p className="text-sm text-red-200/70">{error}</p>
</div>
<button onClick={clearError} className="text-red-400 hover:text-red-300">×</button>
</div>
)}
{/* Summary Stats */}
<div className="grid grid-cols-3 gap-4">
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800 text-center">
<p className="text-sm text-gray-400 mb-1">Total Deposited</p>
<p className="text-2xl font-bold">0 SYN</p>
<p className="text-2xl font-bold">{formatAmount(totalDeposited)}</p>
</div>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800 text-center">
<p className="text-sm text-gray-400 mb-1">Total Earned</p>
<p className="text-2xl font-bold text-green-400">0 SYN</p>
<p className="text-2xl font-bold text-green-400">{formatAmount(totalEarned)}</p>
</div>
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800 text-center">
<p className="text-sm text-gray-400 mb-1">Best APY</p>
<p className="text-2xl font-bold text-synor-400">--%</p>
<p className="text-2xl font-bold text-synor-400">{bestApy.toFixed(2)}%</p>
</div>
</div>
{/* Active Positions */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Available Strategies</h3>
<p className="text-sm text-gray-500 text-center py-4">
No yield strategies available yet
</p>
<h3 className="font-medium mb-4">Your Positions ({positions.length})</h3>
{positions.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-4">No active positions</p>
) : (
<div className="space-y-3">
{positions.map((position) => {
const opp = getOpportunity(position.opportunityId);
return (
<div key={position.id} className="p-4 bg-gray-800 rounded-lg">
<div className="flex justify-between items-start mb-2">
<div>
<p className="font-medium">{opp?.protocol || 'Unknown Protocol'}</p>
<p className="text-xs text-gray-500">{opp?.asset || 'Unknown Asset'}</p>
</div>
<div className="flex items-center gap-2">
{position.autoCompound && (
<span className="px-1.5 py-0.5 bg-blue-500/20 text-blue-400 rounded text-xs">Auto</span>
)}
<span className="px-2 py-1 bg-green-500/20 text-green-400 rounded text-xs flex items-center gap-1">
<Percent size={12} />
{opp?.apy.toFixed(2) || '--'}% APY
</span>
</div>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<p className="text-gray-500">Deposited</p>
<p className="font-mono">{formatAmount(position.depositedAmount)}</p>
</div>
<div>
<p className="text-gray-500">Current Value</p>
<p className="font-mono">{formatAmount(position.currentValue)}</p>
</div>
<div>
<p className="text-gray-500">Earned</p>
<p className="font-mono text-green-400">+{formatAmount(position.rewardsEarned)}</p>
</div>
</div>
<button
onClick={() => withdraw(position.id)}
className="mt-3 w-full py-2 bg-gray-700 rounded text-sm hover:bg-gray-600"
>
Withdraw All
</button>
</div>
);
})}
</div>
)}
</div>
{/* Available Strategies */}
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<h3 className="font-medium mb-4">Available Strategies ({opportunities.length})</h3>
{opportunities.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-4">No yield strategies available yet</p>
) : (
<div className="space-y-3">
{opportunities.map((opp) => (
<div key={opp.id} className="p-4 bg-gray-800 rounded-lg flex justify-between items-center">
<div>
<div className="flex items-center gap-2">
<p className="font-medium">{opp.protocol}</p>
<span className={`px-1.5 py-0.5 rounded text-xs ${
opp.riskLevel === 'low' ? 'bg-green-500/20 text-green-400' :
opp.riskLevel === 'medium' ? 'bg-yellow-500/20 text-yellow-400' :
'bg-red-500/20 text-red-400'
}`}>
{opp.riskLevel} risk
</span>
</div>
<p className="text-sm text-gray-500">{opp.asset}</p>
<div className="text-xs text-gray-600 mt-1">
<span>TVL: ${(opp.tvl / 1_000_000).toFixed(2)}M</span>
{opp.lockupPeriodDays > 0 && (
<span className="ml-2"> {opp.lockupPeriodDays}d lockup</span>
)}
<span className="ml-2"> Min: {formatAmount(opp.minDeposit)}</span>
</div>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-green-400">{opp.apy.toFixed(2)}%</p>
<p className="text-xs text-gray-500">APY</p>
<button
onClick={() => { setSelectedOpportunity(opp.id); setShowDepositModal(true); }}
className="mt-2 px-4 py-1 bg-synor-600 rounded text-sm hover:bg-synor-700 flex items-center gap-1"
>
<Plus size={14} />
Deposit
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Deposit Modal */}
{showDepositModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-gray-900 rounded-xl p-6 w-full max-w-md border border-gray-700">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium">Deposit to Yield Strategy</h3>
<button onClick={() => setShowDepositModal(false)} className="text-gray-400 hover:text-white">
<X size={20} />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-2">Amount (SYN)</label>
<input
type="number"
value={depositAmount}
onChange={(e) => setDepositAmount(e.target.value)}
placeholder="0.00"
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
/>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="autoCompound"
checked={autoCompound}
onChange={(e) => setAutoCompound(e.target.checked)}
className="w-4 h-4 rounded"
/>
<label htmlFor="autoCompound" className="text-sm text-gray-400">
Enable auto-compounding
</label>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowDepositModal(false)}
className="flex-1 py-2 bg-gray-700 rounded-lg"
>
Cancel
</button>
<button
onClick={handleDeposit}
disabled={isLoading || !depositAmount}
className="flex-1 py-2 bg-synor-600 rounded-lg disabled:opacity-50"
>
{isLoading ? <Loader2 className="animate-spin mx-auto" /> : 'Deposit'}
</button>
</div>
</div>
</div>
</div>
)}
<div className="bg-gray-900/50 rounded-lg p-4 border border-gray-800 flex items-start gap-3">
<Info className="text-gray-500 mt-0.5" size={18} />

View file

@ -49,7 +49,7 @@ function transformDecoy(data: any): DecoyWallet {
};
}
export const useDecoyStore = create<DecoyState>()((set, get) => ({
export const useDecoyStore = create<DecoyState>()((set) => ({
isEnabled: false,
decoys: [],
isLoading: false,

View file

@ -130,3 +130,9 @@ export const useLimitOrdersStore = create<LimitOrdersState & LimitOrdersActions>
clearError: () => set({ error: null }),
}));
// Helper to format sompi to SYN
export const formatAmount = (sompi: number): string => {
const syn = sompi / 100_000_000;
return `${syn.toFixed(8)} SYN`;
};

View file

@ -61,7 +61,7 @@ const transformPoolStatus = (data: Record<string, unknown>): MixPoolStatus => ({
estimatedTimeSecs: data.estimated_time_secs as number | undefined,
});
export const useMixerStore = create<MixerState & MixerActions>((set, get) => ({
export const useMixerStore = create<MixerState & MixerActions>((set) => ({
denominations: [],
poolStatus: {},
requests: [],
@ -155,3 +155,9 @@ export const formatDenomination = (sompi: number): string => {
const syn = sompi / 100_000_000;
return `${syn} SYN`;
};
// Helper to format sompi to SYN
export const formatAmount = (sompi: number): string => {
const syn = sompi / 100_000_000;
return `${syn.toFixed(8)} SYN`;
};

View file

@ -178,3 +178,12 @@ export const formatPercent = (value: number): string => {
const sign = value >= 0 ? '+' : '';
return `${sign}${value.toFixed(2)}%`;
};
// Alias for formatting USD
export const formatCurrency = formatUSD;
// Helper to format sompi to SYN
export const formatAmount = (sompi: number): string => {
const syn = sompi / 100_000_000;
return `${syn.toFixed(8)} SYN`;
};

View file

@ -102,7 +102,7 @@ function transformRequest(data: any): RecoveryRequest {
};
}
export const useRecoveryStore = create<RecoveryState>()((set, get) => ({
export const useRecoveryStore = create<RecoveryState>()((set) => ({
// Initial state
config: null,
requests: [],

View file

@ -44,7 +44,7 @@ const transformProfile = (data: Record<string, unknown>): RpcProfile => ({
isHealthy: data.is_healthy as boolean,
});
export const useRpcProfilesStore = create<RpcProfilesState & RpcProfilesActions>((set, get) => ({
export const useRpcProfilesStore = create<RpcProfilesState & RpcProfilesActions>((set) => ({
profiles: [],
activeProfile: null,
isLoading: false,

View file

@ -158,3 +158,9 @@ export const formatTVL = (sompi: number): string => {
if (syn >= 1_000) return `${(syn / 1_000).toFixed(2)}K SYN`;
return `${syn.toFixed(2)} SYN`;
};
// Helper to format sompi to SYN
export const formatAmount = (sompi: number): string => {
const syn = sompi / 100_000_000;
return `${syn.toFixed(8)} SYN`;
};