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 (
@@ -11,31 +48,125 @@ export default function AlertsDashboard() {

Get notified when tokens hit your targets

- -
- -
- -
-

Coming Soon

-

- Set price targets and receive desktop notifications when your tokens - reach those prices. -

+
+ +
+ {error && ( +
+ +
+

Error

+

{error}

+
+ +
+ )} + + {/* Active Alerts */}
-

Active Alerts

-
- -

No price alerts set

-
+

Active Alerts ({activeAlerts.length})

+ {activeAlerts.length === 0 ? ( +
+ +

No active price alerts

+ +
+ ) : ( +
+ {activeAlerts.map((alert) => ( +
+
+
+ + {alert.condition === 'above' ? '↑' : '↓'} + +
+
+

{alert.asset}

+

+ {alert.condition === 'above' ? 'Above' : 'Below'} ${alert.targetPrice.toFixed(4)} +

+

+ Current: ${alert.currentPrice.toFixed(4)} +

+
+
+
+ + +
+
+ ))} +
+ )}
+ {/* Triggered Alerts */} + {triggeredAlerts.length > 0 && ( +
+

Triggered Alerts ({triggeredAlerts.length})

+
+ {triggeredAlerts.map((alert) => ( +
+
+ +
+

{alert.asset}

+

+ Reached ${alert.targetPrice.toFixed(4)} on{' '} + {alert.triggeredAt ? new Date(alert.triggeredAt * 1000).toLocaleString() : 'Unknown'} +

+
+
+ +
+ ))} +
+
+ )} + + {/* Alert Types Info */}

Alert Types

@@ -48,12 +179,91 @@ export default function AlertsDashboard() {

Alert when price drops below target

-

% Change

-

Alert on percentage movement

+

Notification Methods

+

Push, Email, or Both

+ {/* Create Alert Modal */} + {showCreateModal && ( +
+
+
+

Create Price Alert

+ +
+
+
+ + +
+
+ +
+ + +
+
+
+ + 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" + /> +
+
+ + +
+ +
+
+
+ )} +

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

+
+
-
- {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} />
+ {/* Quick Commands */} +
+

Quick Commands

+
+ {['help', 'balance', 'address', 'status', 'utxos', 'peers'].map((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

- -
- -
- -
-

Coming Soon

-

- Limit orders let you automate trades - set your target price and the order - executes automatically when the market reaches it. -

+
+ +
-
-
-

Buy Orders

-

No active buy orders

+ {error && ( +
+ +
+

Error

+

{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 + + +
+
+ ))} +
+ )}
+ {/* 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

+ +
+
+
+ + +
+
+ + +
+
+ + setAmount(e.target.value)} + placeholder="0.00" + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg" + /> +
+
+ + 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" + /> +
+ +
+
+
+ )} +

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

+
+
-
- -
-

Coming Soon

-

- Transaction mixing will allow you to break the link between source and destination - addresses, enhancing privacy for your transactions. -

+ {error && ( +
+ +
+

Error

+

{error}

+
+ +
+ )} + + {/* Denomination Selection */} +
+

Select Mixing Denomination

+
+ {denominations.length === 0 ? ( +

Loading denominations...

+ ) : ( + denominations.map((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

+
+
+ +
+ {selectedDenom ? formatDenomination(selectedDenom) : 'Select a denomination above'} +
+
+
+ + 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

+
+ +
+
+ + {/* 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' && ( + + )} +
+
+ ))} +
+ )} +
+ + {/* 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. - 1 - Deposit funds into the mixing pool + 1 + Select a denomination and provide an output address
  2. - 2 - Your funds are combined with others + 2 + Wait for enough participants to join the pool
  3. - 3 - After a delay, withdraw to a fresh address + 3 + All participants' coins are mixed together cryptographically
  4. - 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

- -
- -
- -
-

Coming Soon

-

- Track your profit & loss, view portfolio allocation, and generate tax reports - for your crypto holdings. -

+
+ +
-
-
-

Total Value

-

$0.00

+ {error && ( +
+ +
+

Error

+

{error}

+
+
-
-

24h Change

-

0%

-
-
-

Unrealized P&L

-

$0.00

-
-
-

Realized 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

+
+ +
+ {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

-
-
- -
-

Coming Soon

-

- The yield aggregator automatically finds the best yields across DeFi protocols - and compounds your earnings. -

+ {error && ( +
+ +
+

Error

+

{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)}

+
+
+ +
+ ); + })} +
+ )}
+ {/* 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

+ +
+
+ ))} +
+ )} +
+ + {/* Deposit Modal */} + {showDepositModal && ( +
+
+
+

Deposit to Yield Strategy

+ +
+
+
+ + 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" + /> + +
+
+ + +
+
+
+
+ )} +

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`; +};