diff --git a/apps/desktop-wallet/src/components/Layout.tsx b/apps/desktop-wallet/src/components/Layout.tsx
index a4ad9bc..b7cf915 100644
--- a/apps/desktop-wallet/src/components/Layout.tsx
+++ b/apps/desktop-wallet/src/components/Layout.tsx
@@ -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 },
diff --git a/apps/desktop-wallet/src/pages/Alerts/AlertsDashboard.tsx b/apps/desktop-wallet/src/pages/Alerts/AlertsDashboard.tsx
index 3140bb4..bee9738 100644
--- a/apps/desktop-wallet/src/pages/Alerts/AlertsDashboard.tsx
+++ b/apps/desktop-wallet/src/pages/Alerts/AlertsDashboard.tsx
@@ -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 (
diff --git a/apps/desktop-wallet/src/pages/CLI/CliDashboard.tsx b/apps/desktop-wallet/src/pages/CLI/CliDashboard.tsx
index 8a790f7..4fd79a0 100644
--- a/apps/desktop-wallet/src/pages/CLI/CliDashboard.tsx
+++ b/apps/desktop-wallet/src/pages/CLI/CliDashboard.tsx
@@ -1,68 +1,103 @@
-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([
- '> Welcome to Synor CLI Mode',
- '> Type "help" for available commands',
- '>',
- ]);
+ const outputRef = useRef(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 (
-
-
-
- CLI Mode
-
-
Terminal interface for power users
+
+
+
+
+ CLI Mode
+
+
Terminal interface for power users
+
+
+ Clear
+
-
- {history.map((line, i) => (
-
') ? 'text-synor-400' : 'text-gray-300'}>
- {line}
+
+ {/* Welcome message if no history */}
+ {history.length === 0 && (
+
+
{'>'} Welcome to Synor CLI Mode
+
{'>'} Type "help" for available commands
+
{'>'}
+
+ )}
+
+ {/* Command history */}
+ {history.map((result, i) => (
+
+
+ {'>'} {result.command}
+
+
+ {result.output}
+
))}
+
+ {/* Loading indicator */}
+ {isExecuting && (
+
+
+ Executing...
+
+ )}
@@ -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}
/>
-
+ {isExecuting ? : }
+ {/* Quick Commands */}
+
+
Quick Commands
+
+ {['help', 'balance', 'address', 'status', 'utxos', 'peers'].map((cmd) => (
+ { setInput(cmd); }}
+ className="px-3 py-1 bg-gray-800 rounded font-mono text-sm hover:bg-gray-700"
+ >
+ {cmd}
+
+ ))}
+
+
+
+ {/* Command Reference */}
+
+
Command Reference
+
+
help
+
Show all available commands
+
+
balance
+
Show wallet balance
+
+
address
+
Show wallet addresses
+
+
send <addr> <amount>
+
Send SYN to address
+
+
utxos
+
List unspent outputs
+
+
history
+
Transaction history
+
+
status
+
Network status
+
+
peers
+
Connected peers
+
+
clear
+
Clear output
+
+
+
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.
diff --git a/apps/desktop-wallet/src/pages/LimitOrders/LimitOrdersDashboard.tsx b/apps/desktop-wallet/src/pages/LimitOrders/LimitOrdersDashboard.tsx
index 16938a8..445062c 100644
--- a/apps/desktop-wallet/src/pages/LimitOrders/LimitOrdersDashboard.tsx
+++ b/apps/desktop-wallet/src/pages/LimitOrders/LimitOrdersDashboard.tsx
@@ -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 (
@@ -11,34 +56,218 @@ export default function LimitOrdersDashboard() {
Set buy/sell orders at specific prices
-
-
- New Order
-
-
-
-
-
-
-
Coming Soon
-
- Limit orders let you automate trades - set your target price and the order
- executes automatically when the market reaches it.
-
+
+
{ listOrders(); getOrderBook('SYN/USDT'); }}
+ className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
+ disabled={isLoading}
+ >
+ {isLoading ? : }
+
+
setShowCreateModal(true)}
+ className="px-4 py-2 bg-synor-600 rounded-lg flex items-center gap-2 hover:bg-synor-700"
+ >
+
+ New Order
+
-
-
-
Buy Orders
-
No active buy orders
+ {error && (
+
-
-
Sell Orders
-
No active sell orders
+ )}
+
+ {/* Order Book */}
+ {orderBook && (
+
+
+
+
+ Buy Orders ({orderBook.bids.length})
+
+ {orderBook.bids.length === 0 ? (
+
No buy orders
+ ) : (
+
+ {orderBook.bids.slice(0, 5).map((bid, i) => (
+
+ ${bid.price.toFixed(4)}
+ {formatAmount(bid.amount)}
+
+ ))}
+
+ )}
+
+
+
+
+ Sell Orders ({orderBook.asks.length})
+
+ {orderBook.asks.length === 0 ? (
+
No sell orders
+ ) : (
+
+ {orderBook.asks.slice(0, 5).map((ask, i) => (
+
+ ${ask.price.toFixed(4)}
+ {formatAmount(ask.amount)}
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Active Orders */}
+
+
Active Orders ({activeOrders.length})
+ {activeOrders.length === 0 ? (
+
No active orders
+ ) : (
+
+ {activeOrders.map((order) => (
+
+
+
+ {order.orderType.toUpperCase()}
+
+
+
{order.pair}
+
+ {formatAmount(order.amount)} @ ${order.price.toFixed(4)}
+
+
+
+
+
+ {((order.filledAmount / order.amount) * 100).toFixed(0)}% filled
+
+ cancelOrder(order.id)}
+ className="p-1 text-red-400 hover:bg-red-500/20 rounded"
+ >
+
+
+
+
+ ))}
+
+ )}
+ {/* Order History */}
+
+
Filled Orders ({filledOrders.length})
+ {filledOrders.length === 0 ? (
+
No filled orders
+ ) : (
+
+ {filledOrders.slice(0, 10).map((order) => (
+
+
+
+ {order.orderType.toUpperCase()}
+
+
+
{order.pair}
+
{formatAmount(order.amount)}
+
+
+
Filled
+
+ ))}
+
+ )}
+
+
+ {/* Create Order Modal */}
+ {showCreateModal && (
+
+
+
+
Create Limit Order
+ setShowCreateModal(false)} className="text-gray-400 hover:text-white">
+
+
+
+
+
+ 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
+
+ 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
+
+
+
+ Trading Pair
+ setTradingPair(e.target.value)}
+ className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
+ >
+ SYN/USDT
+ SYN/BTC
+
+
+
+ Amount (SYN)
+ setAmount(e.target.value)}
+ placeholder="0.00"
+ className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
+ />
+
+
+ Price (USD)
+ 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"
+ />
+
+
+ {isLoading ? : `Place ${orderType.toUpperCase()} Order`}
+
+
+
+
+ )}
+
diff --git a/apps/desktop-wallet/src/pages/Mixer/MixerDashboard.tsx b/apps/desktop-wallet/src/pages/Mixer/MixerDashboard.tsx
index 6c9183f..60a99a6 100644
--- a/apps/desktop-wallet/src/pages/Mixer/MixerDashboard.tsx
+++ b/apps/desktop-wallet/src/pages/Mixer/MixerDashboard.tsx
@@ -1,45 +1,250 @@
-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(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 (
-
-
-
- Transaction Mixer
-
-
Enhanced privacy through coin mixing
+
+
+
+
+ Transaction Mixer
+
+
Enhanced privacy through coin mixing
+
+
{ loadDenominations(); listRequests(); }}
+ className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
+ disabled={isLoading}
+ >
+ {isLoading ? : }
+
-
-
-
-
Coming Soon
-
- Transaction mixing will allow you to break the link between source and destination
- addresses, enhancing privacy for your transactions.
-
+ {error && (
+
+ )}
+
+ {/* Denomination Selection */}
+
+
Select Mixing Denomination
+
+ {denominations.length === 0 ? (
+
Loading denominations...
+ ) : (
+ denominations.map((denom) => (
+
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)}
+
+ ))
+ )}
+ {/* Pool Status */}
+ {currentPool && (
+
+
+
Denomination
+
{formatDenomination(currentPool.denomination)}
+
+
+
Participants
+
{currentPool.participants}/{currentPool.requiredParticipants}
+
+
+
Status
+
+ {currentPool.status.charAt(0).toUpperCase() + currentPool.status.slice(1)}
+
+
+
+
Est. Time
+
+ {currentPool.estimatedTimeSecs ? `${Math.ceil(currentPool.estimatedTimeSecs / 60)}m` : '--'}
+
+
+
+ )}
+
+ {/* Create Mix Request */}
+
+
Join Mixing Pool
+
+
+
Selected Amount
+
+ {selectedDenom ? formatDenomination(selectedDenom) : 'Select a denomination above'}
+
+
+
+
Output Address
+
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"
+ />
+
Use a new address that hasn't received funds before
+
+
+ {isLoading ? : }
+ Join Pool
+
+
+
+
+ {/* Pending Requests */}
+
+
Pending Mixes ({pendingRequests.length})
+ {pendingRequests.length === 0 ? (
+
No pending mix requests
+ ) : (
+
+ {pendingRequests.map((req) => (
+
+
+
{formatAmount(req.amount)}
+
Status: {req.status}
+
+
+
+ {req.status}
+
+ {req.status === 'pending' && (
+ cancelRequest(req.id)}
+ className="p-1 text-red-400 hover:bg-red-500/20 rounded"
+ >
+
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ {/* Completed Requests */}
+
+
Completed Mixes ({completedRequests.length})
+ {completedRequests.length === 0 ? (
+
No completed mixes
+ ) : (
+
+ {completedRequests.map((req) => (
+
+
+
{formatAmount(req.amount)}
+
→ {req.outputAddress}
+ {req.txId && (
+
TX: {req.txId}
+ )}
+
+
+ Complete
+
+
+ ))}
+
+ )}
+
+
+ {/* How It Works */}
How Mixing Works
- 1
- Deposit funds into the mixing pool
+ 1
+ Select a denomination and provide an output address
- 2
- Your funds are combined with others
+ 2
+ Wait for enough participants to join the pool
- 3
- After a delay, withdraw to a fresh address
+ 3
+ All participants' coins are mixed together cryptographically
- 4
- Transaction history is broken
+ 4
+ Receive coins at your output address with broken transaction history
@@ -48,7 +253,7 @@ export default function MixerDashboard() {
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.
diff --git a/apps/desktop-wallet/src/pages/Portfolio/PortfolioDashboard.tsx b/apps/desktop-wallet/src/pages/Portfolio/PortfolioDashboard.tsx
index bf2446a..c6528e4 100644
--- a/apps/desktop-wallet/src/pages/Portfolio/PortfolioDashboard.tsx
+++ b/apps/desktop-wallet/src/pages/Portfolio/PortfolioDashboard.tsx
@@ -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 (
@@ -11,62 +53,184 @@ export default function PortfolioDashboard() {
P&L tracking and tax reports
-
-
- Export Report
-
-
-
-
-
-
-
Coming Soon
-
- Track your profit & loss, view portfolio allocation, and generate tax reports
- for your crypto holdings.
-
+
+ { loadSummary(); loadHoldings(); }}
+ className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
+ disabled={isLoading}
+ >
+ {isLoading ? : }
+
+
+
+ Export CSV
+
-
-
-
Total Value
-
$0.00
+ {error && (
+
-
-
-
Unrealized P&L
-
$0.00
-
-
-
+ )}
-
-
-
Asset Allocation
-
- Chart coming soon
+ {/* Summary Stats */}
+ {summary && (
+
+
+
Total Value
+
{formatUSD(summary.totalValueUsd)}
+
+
+
24h Change
+
= 0 ? 'text-green-400' : 'text-red-400'
+ }`}>
+ {summary.dayChangePercent >= 0 ? : }
+ {summary.dayChangePercent >= 0 ? '+' : ''}{summary.dayChangePercent.toFixed(2)}%
+
+
= 0 ? 'text-green-400/70' : 'text-red-400/70'}`}>
+ {formatUSD(summary.dayChangeUsd)}
+
+
+
+
Total P&L
+
= 0 ? 'text-green-400' : 'text-red-400'
+ }`}>
+ {summary.totalPnlUsd >= 0 ? '+' : ''}{formatUSD(summary.totalPnlUsd)}
+
+
= 0 ? 'text-green-400/70' : 'text-red-400/70'}`}>
+ {summary.totalPnlPercent >= 0 ? '+' : ''}{summary.totalPnlPercent.toFixed(2)}%
+
+
+
+
Cost Basis
+
{formatUSD(summary.totalCostBasisUsd)}
-
-
Performance History
-
- Chart coming soon
+ )}
+
+ {/* Holdings */}
+
+
Asset Holdings ({holdings.length})
+ {holdings.length === 0 ? (
+
No holdings found
+ ) : (
+
+ {holdings.map((holding) => (
+
+
+
+ {holding.symbol.substring(0, 2)}
+
+
+
{holding.asset}
+
{holding.balanceFormatted}
+
+
+
+
{formatUSD(holding.valueUsd)}
+
= 0 ? 'text-green-400' : 'text-red-400'}`}>
+ {holding.pnlPercent >= 0 ? '+' : ''}{holding.pnlPercent.toFixed(2)}%
+
+
+
+
Allocation
+
+
+
{holding.allocationPercent.toFixed(1)}%
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Tax Report Section */}
+
+
+
Tax Report
+
+ 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 => (
+ {year}
+ ))}
+
+
+ {isLoading ? : 'Load'}
+
+ {taxReport.length === 0 ? (
+
+
Select a year and click Load to generate your tax report
+
+ ) : (
+
+
+ Date
+ Type
+ Asset
+ Amount
+ Total
+ Gain/Loss
+
+ {taxReport.slice(0, 10).map((tx) => (
+
+ {new Date(tx.timestamp * 1000).toLocaleDateString()}
+
+ {tx.txType.toUpperCase()}
+
+ {tx.asset}
+ {formatAmount(tx.amount)}
+ {formatUSD(tx.totalUsd)}
+ = 0 ? 'text-green-400' : 'text-red-400'}>
+ {tx.gainLossUsd !== undefined ? formatUSD(tx.gainLossUsd) : '--'}
+ {tx.isLongTerm && (LT) }
+
+
+ ))}
+ {taxReport.length > 10 && (
+
+ Showing 10 of {taxReport.length} transactions. Export to see all.
+
+ )}
+
+ )}
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.
diff --git a/apps/desktop-wallet/src/pages/Vaults/VaultsDashboard.tsx b/apps/desktop-wallet/src/pages/Vaults/VaultsDashboard.tsx
index df62a87..3115333 100644
--- a/apps/desktop-wallet/src/pages/Vaults/VaultsDashboard.tsx
+++ b/apps/desktop-wallet/src/pages/Vaults/VaultsDashboard.tsx
@@ -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,
diff --git a/apps/desktop-wallet/src/pages/Yield/YieldDashboard.tsx b/apps/desktop-wallet/src/pages/Yield/YieldDashboard.tsx
index 8773b8f..5529dec 100644
--- a/apps/desktop-wallet/src/pages/Yield/YieldDashboard.tsx
+++ b/apps/desktop-wallet/src/pages/Yield/YieldDashboard.tsx
@@ -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
(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 (
@@ -11,44 +56,194 @@ export default function YieldDashboard() {
Auto-compound and find the best APY
-
-
+ { loadOpportunities(); listPositions(); }}
+ className="p-2 bg-gray-800 rounded-lg hover:bg-gray-700"
+ disabled={isLoading}
+ >
+ {isLoading ? : }
-
-
-
-
Coming Soon
-
- The yield aggregator automatically finds the best yields across DeFi protocols
- and compounds your earnings.
-
+ {error && (
+
-
+ )}
+ {/* Summary Stats */}
Total Deposited
-
0 SYN
+
{formatAmount(totalDeposited)}
Total Earned
-
0 SYN
+
{formatAmount(totalEarned)}
Best APY
-
--%
+
{bestApy.toFixed(2)}%
+ {/* Active Positions */}
-
Available Strategies
-
- No yield strategies available yet
-
+
Your Positions ({positions.length})
+ {positions.length === 0 ? (
+
No active positions
+ ) : (
+
+ {positions.map((position) => {
+ const opp = getOpportunity(position.opportunityId);
+ return (
+
+
+
+
{opp?.protocol || 'Unknown Protocol'}
+
{opp?.asset || 'Unknown Asset'}
+
+
+ {position.autoCompound && (
+
Auto
+ )}
+
+
+ {opp?.apy.toFixed(2) || '--'}% APY
+
+
+
+
+
+
Deposited
+
{formatAmount(position.depositedAmount)}
+
+
+
Current Value
+
{formatAmount(position.currentValue)}
+
+
+
Earned
+
+{formatAmount(position.rewardsEarned)}
+
+
+
withdraw(position.id)}
+ className="mt-3 w-full py-2 bg-gray-700 rounded text-sm hover:bg-gray-600"
+ >
+ Withdraw All
+
+
+ );
+ })}
+
+ )}
+ {/* Available Strategies */}
+
+
Available Strategies ({opportunities.length})
+ {opportunities.length === 0 ? (
+
No yield strategies available yet
+ ) : (
+
+ {opportunities.map((opp) => (
+
+
+
+
{opp.protocol}
+
+ {opp.riskLevel} risk
+
+
+
{opp.asset}
+
+ TVL: ${(opp.tvl / 1_000_000).toFixed(2)}M
+ {opp.lockupPeriodDays > 0 && (
+ • {opp.lockupPeriodDays}d lockup
+ )}
+ • Min: {formatAmount(opp.minDeposit)}
+
+
+
+
{opp.apy.toFixed(2)}%
+
APY
+
{ 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"
+ >
+
+ Deposit
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Deposit Modal */}
+ {showDepositModal && (
+
+
+
+
Deposit to Yield Strategy
+ setShowDepositModal(false)} className="text-gray-400 hover:text-white">
+
+
+
+
+
+ Amount (SYN)
+ setDepositAmount(e.target.value)}
+ placeholder="0.00"
+ className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
+ />
+
+
+ setAutoCompound(e.target.checked)}
+ className="w-4 h-4 rounded"
+ />
+
+ Enable auto-compounding
+
+
+
+ setShowDepositModal(false)}
+ className="flex-1 py-2 bg-gray-700 rounded-lg"
+ >
+ Cancel
+
+
+ {isLoading ? : 'Deposit'}
+
+
+
+
+
+ )}
+
diff --git a/apps/desktop-wallet/src/store/decoy.ts b/apps/desktop-wallet/src/store/decoy.ts
index 6a6dfb4..48768ac 100644
--- a/apps/desktop-wallet/src/store/decoy.ts
+++ b/apps/desktop-wallet/src/store/decoy.ts
@@ -49,7 +49,7 @@ function transformDecoy(data: any): DecoyWallet {
};
}
-export const useDecoyStore = create()((set, get) => ({
+export const useDecoyStore = create()((set) => ({
isEnabled: false,
decoys: [],
isLoading: false,
diff --git a/apps/desktop-wallet/src/store/limitOrders.ts b/apps/desktop-wallet/src/store/limitOrders.ts
index f5706c1..723afd5 100644
--- a/apps/desktop-wallet/src/store/limitOrders.ts
+++ b/apps/desktop-wallet/src/store/limitOrders.ts
@@ -130,3 +130,9 @@ export const useLimitOrdersStore = create
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`;
+};
diff --git a/apps/desktop-wallet/src/store/mixer.ts b/apps/desktop-wallet/src/store/mixer.ts
index 105789d..4437f60 100644
--- a/apps/desktop-wallet/src/store/mixer.ts
+++ b/apps/desktop-wallet/src/store/mixer.ts
@@ -61,7 +61,7 @@ const transformPoolStatus = (data: Record): MixPoolStatus => ({
estimatedTimeSecs: data.estimated_time_secs as number | undefined,
});
-export const useMixerStore = create((set, get) => ({
+export const useMixerStore = create((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`;
+};
diff --git a/apps/desktop-wallet/src/store/portfolio.ts b/apps/desktop-wallet/src/store/portfolio.ts
index c0d593e..649f937 100644
--- a/apps/desktop-wallet/src/store/portfolio.ts
+++ b/apps/desktop-wallet/src/store/portfolio.ts
@@ -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`;
+};
diff --git a/apps/desktop-wallet/src/store/recovery.ts b/apps/desktop-wallet/src/store/recovery.ts
index 786d257..8414d74 100644
--- a/apps/desktop-wallet/src/store/recovery.ts
+++ b/apps/desktop-wallet/src/store/recovery.ts
@@ -102,7 +102,7 @@ function transformRequest(data: any): RecoveryRequest {
};
}
-export const useRecoveryStore = create()((set, get) => ({
+export const useRecoveryStore = create()((set) => ({
// Initial state
config: null,
requests: [],
diff --git a/apps/desktop-wallet/src/store/rpcProfiles.ts b/apps/desktop-wallet/src/store/rpcProfiles.ts
index fbe73fd..a0c8077 100644
--- a/apps/desktop-wallet/src/store/rpcProfiles.ts
+++ b/apps/desktop-wallet/src/store/rpcProfiles.ts
@@ -44,7 +44,7 @@ const transformProfile = (data: Record): RpcProfile => ({
isHealthy: data.is_healthy as boolean,
});
-export const useRpcProfilesStore = create((set, get) => ({
+export const useRpcProfilesStore = create((set) => ({
profiles: [],
activeProfile: null,
isLoading: false,
diff --git a/apps/desktop-wallet/src/store/yield.ts b/apps/desktop-wallet/src/store/yield.ts
index 0ea2da4..6bbe31d 100644
--- a/apps/desktop-wallet/src/store/yield.ts
+++ b/apps/desktop-wallet/src/store/yield.ts
@@ -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`;
+};