diff --git a/src/App.tsx b/src/App.tsx index fe4be363..3ca8aa50 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import PasswordReset from '@/pages/PasswordReset'; import ProfileSettings from '@/pages/ProfileSettings'; import AdminPanel from '@/pages/AdminPanel'; import WalletDashboard from './pages/WalletDashboard'; +import PoolDashboardPage from './pages/PoolDashboard'; import { AppProvider } from '@/contexts/AppContext'; import { PolkadotProvider } from '@/contexts/PolkadotContext'; import { WalletProvider } from '@/contexts/WalletContext'; @@ -56,6 +57,11 @@ function App() { } /> + + + + } /> } /> diff --git a/src/components/AddLiquidityModal.tsx b/src/components/AddLiquidityModal.tsx new file mode 100644 index 00000000..2bb2e043 --- /dev/null +++ b/src/components/AddLiquidityModal.tsx @@ -0,0 +1,321 @@ +import React, { useState, useEffect } from 'react'; +import { X, Plus, Info, AlertCircle } from 'lucide-react'; +import { web3FromAddress } from '@polkadot/extension-dapp'; +import { usePolkadot } from '@/contexts/PolkadotContext'; +import { useWallet } from '@/contexts/WalletContext'; +import { Button } from '@/components/ui/button'; +import { Alert, AlertDescription } from '@/components/ui/alert'; + +interface AddLiquidityModalProps { + isOpen: boolean; + onClose: () => void; +} + +export const AddLiquidityModal: React.FC = ({ isOpen, onClose }) => { + const { api, selectedAccount, isApiReady } = usePolkadot(); + const { balances, refreshBalances } = useWallet(); + + const [whezAmount, setWhezAmount] = useState(''); + const [pezAmount, setPezAmount] = useState(''); + const [currentPrice, setCurrentPrice] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + // Fetch current pool price + useEffect(() => { + if (!api || !isApiReady || !isOpen) return; + + const fetchPoolPrice = async () => { + try { + const poolId = [0, 1]; + const poolInfo = await api.query.assetConversion.pools(poolId); + + if (poolInfo.isSome) { + const lpTokenData = poolInfo.unwrap().toJSON() as any; + const lpTokenId = lpTokenData.lpToken; + + // Get pool account + const poolAccountData = await api.query.assetConversion.poolAccountIds?.(poolId); + let poolAccount = ''; + + if (poolAccountData && poolAccountData.isSome) { + poolAccount = poolAccountData.unwrap().toString(); + + // Get reserves + const whezBalanceData = await api.query.assets.account(0, poolAccount); + const pezBalanceData = await api.query.assets.account(1, poolAccount); + + if (whezBalanceData.isSome && pezBalanceData.isSome) { + const whezData = whezBalanceData.unwrap().toJSON() as any; + const pezData = pezBalanceData.unwrap().toJSON() as any; + + const reserve0 = Number(whezData.balance) / 1e12; + const reserve1 = Number(pezData.balance) / 1e12; + + setCurrentPrice(reserve1 / reserve0); + } + } + } + } catch (err) { + console.error('Error fetching pool price:', err); + } + }; + + fetchPoolPrice(); + }, [api, isApiReady, isOpen]); + + // Auto-calculate PEZ amount based on wHEZ input + useEffect(() => { + if (whezAmount && currentPrice) { + const calculatedPez = parseFloat(whezAmount) * currentPrice; + setPezAmount(calculatedPez.toFixed(4)); + } else if (!whezAmount) { + setPezAmount(''); + } + }, [whezAmount, currentPrice]); + + const handleAddLiquidity = async () => { + if (!api || !selectedAccount || !whezAmount || !pezAmount) return; + + setIsLoading(true); + setError(null); + + try { + // Validate amounts + if (parseFloat(whezAmount) <= 0 || parseFloat(pezAmount) <= 0) { + setError('Please enter valid amounts'); + setIsLoading(false); + return; + } + + if (parseFloat(whezAmount) > whezBalance) { + setError('Insufficient HEZ balance'); + setIsLoading(false); + return; + } + + if (parseFloat(pezAmount) > pezBalance) { + setError('Insufficient PEZ balance'); + setIsLoading(false); + return; + } + + // Get the signer from the extension + const injector = await web3FromAddress(selectedAccount.address); + + const whezAmountBN = BigInt(Math.floor(parseFloat(whezAmount) * 1e12)); + const pezAmountBN = BigInt(Math.floor(parseFloat(pezAmount) * 1e12)); + + // Min amounts (90% of desired to account for slippage - more tolerance for AMM) + const minWhezBN = (whezAmountBN * BigInt(90)) / BigInt(100); + const minPezBN = (pezAmountBN * BigInt(90)) / BigInt(100); + + // Need to wrap HEZ to wHEZ first + const wrapTx = api.tx.tokenWrapper.wrap(whezAmountBN.toString()); + + // Add liquidity transaction + const addLiquidityTx = api.tx.assetConversion.addLiquidity( + 0, // asset1 (wHEZ) + 1, // asset2 (PEZ) + whezAmountBN.toString(), + pezAmountBN.toString(), + minWhezBN.toString(), + minPezBN.toString(), + selectedAccount.address + ); + + // Batch transactions + const tx = api.tx.utility.batchAll([wrapTx, addLiquidityTx]); + + await tx.signAndSend( + selectedAccount.address, + { signer: injector.signer }, + ({ status, events, dispatchError }) => { + if (status.isInBlock) { + console.log('Transaction in block:', status.asInBlock.toHex()); + } else if (status.isFinalized) { + console.log('Transaction finalized:', status.asFinalized.toHex()); + + // Check for errors + const hasError = events.some(({ event }) => + api.events.system.ExtrinsicFailed.is(event) + ); + + if (hasError || dispatchError) { + let errorMessage = 'Transaction failed'; + + if (dispatchError) { + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + const { docs, name, section } = decoded; + errorMessage = `${section}.${name}: ${docs.join(' ')}`; + console.error('Dispatch error:', errorMessage); + } else { + errorMessage = dispatchError.toString(); + console.error('Dispatch error:', errorMessage); + } + } + + // Also check events for more details + events.forEach(({ event }) => { + if (api.events.system.ExtrinsicFailed.is(event)) { + console.error('ExtrinsicFailed event:', event.toHuman()); + } + }); + + setError(errorMessage); + setIsLoading(false); + } else { + console.log('Transaction successful'); + setSuccess(true); + setIsLoading(false); + setWhezAmount(''); + setPezAmount(''); + refreshBalances(); + + setTimeout(() => { + setSuccess(false); + onClose(); + }, 2000); + } + } + } + ); + } catch (err) { + console.error('Error adding liquidity:', err); + setError(err instanceof Error ? err.message : 'Failed to add liquidity'); + setIsLoading(false); + } + }; + + if (!isOpen) return null; + + const whezBalance = balances.HEZ || 0; + const pezBalance = balances.PEZ || 0; + + return ( +
+
+
+

Add Liquidity

+ +
+ + {error && ( + + + {error} + + )} + + {success && ( + + Liquidity added successfully! + + )} + + + + + Add liquidity to earn 3% fees from all swaps. Your HEZ will be automatically wrapped to wHEZ. + + + +
+ {/* HEZ Input */} +
+ +
+ setWhezAmount(e.target.value)} + placeholder="0.0" + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500" + disabled={isLoading} + /> +
+ HEZ +
+
+
+ Balance: {whezBalance.toLocaleString()} + +
+
+ +
+ +
+ + {/* PEZ Input */} +
+ +
+ +
+ PEZ +
+
+
+ Balance: {pezBalance.toLocaleString()} + + {currentPrice && `Rate: 1 HEZ = ${currentPrice.toFixed(4)} PEZ`} + +
+
+ + {/* Price Info */} + {whezAmount && pezAmount && ( +
+
+ Share of Pool + ~0.1% +
+
+ Slippage Tolerance + 10% +
+
+ )} + + +
+
+
+ ); +}; diff --git a/src/components/PoolDashboard.tsx b/src/components/PoolDashboard.tsx new file mode 100644 index 00000000..433557d3 --- /dev/null +++ b/src/components/PoolDashboard.tsx @@ -0,0 +1,486 @@ +import React, { useState, useEffect } from 'react'; +import { TrendingUp, Droplet, DollarSign, Percent, Info, AlertTriangle, BarChart3, Clock } from 'lucide-react'; +import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { usePolkadot } from '@/contexts/PolkadotContext'; +import { useWallet } from '@/contexts/WalletContext'; +import { ASSET_IDS } from '@/lib/wallet'; +import { AddLiquidityModal } from '@/components/AddLiquidityModal'; +import { RemoveLiquidityModal } from '@/components/RemoveLiquidityModal'; + +interface PoolData { + asset0: number; + asset1: number; + reserve0: number; + reserve1: number; + lpTokenId: number; + poolAccount: string; +} + +interface LPPosition { + lpTokenBalance: number; + share: number; // Percentage of pool + asset0Amount: number; + asset1Amount: number; +} + +const PoolDashboard = () => { + const { api, isApiReady, selectedAccount } = usePolkadot(); + const { balances } = useWallet(); + + const [poolData, setPoolData] = useState(null); + const [lpPosition, setLPPosition] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [isAddLiquidityModalOpen, setIsAddLiquidityModalOpen] = useState(false); + const [isRemoveLiquidityModalOpen, setIsRemoveLiquidityModalOpen] = useState(false); + + // Fetch pool data + useEffect(() => { + if (!api || !isApiReady) return; + + const fetchPoolData = async () => { + setIsLoading(true); + setError(null); + + try { + // Query wHEZ/PEZ pool + const poolId = [0, 1]; // wHEZ (asset 0) / PEZ (asset 1) + + const poolInfo = await api.query.assetConversion.pools(poolId); + + if (poolInfo.isSome) { + const lpTokenData = poolInfo.unwrap().toJSON() as any; + const lpTokenId = lpTokenData.lpToken; + + // Get pool account + const poolAccountData = await api.query.assetConversion.poolAccountIds?.(poolId); + let poolAccount = ''; + + if (poolAccountData && poolAccountData.isSome) { + poolAccount = poolAccountData.unwrap().toString(); + } + + // Get reserves + const whezBalanceData = await api.query.assets.account(0, poolAccount); + const pezBalanceData = await api.query.assets.account(1, poolAccount); + + let reserve0 = 0; + let reserve1 = 0; + + if (whezBalanceData.isSome) { + const whezData = whezBalanceData.unwrap().toJSON() as any; + reserve0 = Number(whezData.balance) / 1e12; + } + + if (pezBalanceData.isSome) { + const pezData = pezBalanceData.unwrap().toJSON() as any; + reserve1 = Number(pezData.balance) / 1e12; + } + + setPoolData({ + asset0: 0, + asset1: 1, + reserve0, + reserve1, + lpTokenId, + poolAccount, + }); + + // Get user's LP position if account connected + if (selectedAccount) { + await fetchLPPosition(lpTokenId, reserve0, reserve1); + } + } else { + setError('Pool not found'); + } + } catch (err) { + console.error('Error fetching pool data:', err); + setError(err instanceof Error ? err.message : 'Failed to fetch pool data'); + } finally { + setIsLoading(false); + } + }; + + const fetchLPPosition = async (lpTokenId: number, reserve0: number, reserve1: number) => { + if (!api || !selectedAccount) return; + + try { + // Query user's LP token balance + const lpBalance = await api.query.poolAssets.account(lpTokenId, selectedAccount.address); + + if (lpBalance.isSome) { + const lpData = lpBalance.unwrap().toJSON() as any; + const userLpBalance = Number(lpData.balance) / 1e12; + + // Query total LP supply + const lpAssetData = await api.query.poolAssets.asset(lpTokenId); + + if (lpAssetData.isSome) { + const assetInfo = lpAssetData.unwrap().toJSON() as any; + const totalSupply = Number(assetInfo.supply) / 1e12; + + // Calculate user's share + const sharePercentage = (userLpBalance / totalSupply) * 100; + + // Calculate user's actual token amounts + const asset0Amount = (sharePercentage / 100) * reserve0; + const asset1Amount = (sharePercentage / 100) * reserve1; + + setLPPosition({ + lpTokenBalance: userLpBalance, + share: sharePercentage, + asset0Amount, + asset1Amount, + }); + } + } + } catch (err) { + console.error('Error fetching LP position:', err); + } + }; + + fetchPoolData(); + + // Refresh every 30 seconds + const interval = setInterval(fetchPoolData, 30000); + + return () => clearInterval(interval); + }, [api, isApiReady, selectedAccount]); + + // Calculate metrics + const constantProduct = poolData ? poolData.reserve0 * poolData.reserve1 : 0; + const currentPrice = poolData ? poolData.reserve1 / poolData.reserve0 : 0; + const totalLiquidityUSD = poolData ? poolData.reserve0 * 2 : 0; // Simplified: assumes 1:1 USD peg + + // APR calculation (simplified - would need 24h volume data) + const estimateAPR = () => { + if (!poolData) return 0; + + // Estimate based on pool size and typical volume + // This is a simplified calculation + // Real APR = (24h fees × 365) / TVL + const dailyVolumeEstimate = totalLiquidityUSD * 0.1; // Assume 10% daily turnover + const dailyFees = dailyVolumeEstimate * 0.03; // 3% fee + const annualFees = dailyFees * 365; + const apr = (annualFees / totalLiquidityUSD) * 100; + + return apr; + }; + + // Impermanent loss calculator + const calculateImpermanentLoss = (priceChange: number) => { + // IL formula: 2 * sqrt(price_ratio) / (1 + price_ratio) - 1 + const priceRatio = 1 + priceChange / 100; + const il = ((2 * Math.sqrt(priceRatio)) / (1 + priceRatio) - 1) * 100; + return il; + }; + + if (isLoading && !poolData) { + return ( +
+
+
+

Loading pool data...

+
+
+ ); + } + + if (error) { + return ( + + + {error} + + ); + } + + if (!poolData) { + return ( + + + No pool data available + + ); + } + + return ( +
+
+
+

+ + wHEZ/PEZ Pool Dashboard +

+

Monitor liquidity pool metrics and your position

+
+ + + Live + +
+ + {/* Key Metrics Grid */} +
+ {/* Total Liquidity */} + +
+
+

Total Liquidity

+

+ ${totalLiquidityUSD.toLocaleString('en-US', { maximumFractionDigits: 0 })} +

+

+ {poolData.reserve0.toLocaleString()} wHEZ + {poolData.reserve1.toLocaleString()} PEZ +

+
+ +
+
+ + {/* Current Price */} + +
+
+

HEZ Price

+

+ {currentPrice.toFixed(4)} PEZ +

+

+ 1 PEZ = {(1 / currentPrice).toFixed(6)} HEZ +

+
+ +
+
+ + {/* APR */} + +
+
+

Estimated APR

+

+ {estimateAPR().toFixed(2)}% +

+

+ From swap fees +

+
+ +
+
+ + {/* Constant Product */} + +
+
+

Constant (k)

+

+ {(constantProduct / 1e9).toFixed(1)}B +

+

+ x × y = k +

+
+ +
+
+
+ + + + Reserves + Your Position + IL Calculator + + + {/* Reserves Tab */} + + +

Pool Reserves

+ +
+
+
+

wHEZ Reserve

+

{poolData.reserve0.toLocaleString('en-US', { maximumFractionDigits: 2 })}

+
+ Asset 0 +
+ +
+
+

PEZ Reserve

+

{poolData.reserve1.toLocaleString('en-US', { maximumFractionDigits: 2 })}

+
+ Asset 1 +
+
+ +
+
+ +
+

AMM Formula

+

Pool maintains constant product: x × y = k

+

+ {poolData.reserve0.toFixed(2)} × {poolData.reserve1.toFixed(2)} = {constantProduct.toLocaleString()} +

+
+
+
+
+
+ + {/* Your Position Tab */} + + +

Your Liquidity Position

+ + {!selectedAccount ? ( + + + Connect wallet to view your position + + ) : !lpPosition ? ( +
+ +

No liquidity position found

+ +
+ ) : ( +
+
+
+

LP Tokens

+

{lpPosition.lpTokenBalance.toFixed(4)}

+
+
+

Pool Share

+

{lpPosition.share.toFixed(4)}%

+
+
+ +
+

Your Position Value

+
+
+ wHEZ: + {lpPosition.asset0Amount.toFixed(4)} +
+
+ PEZ: + {lpPosition.asset1Amount.toFixed(4)} +
+
+
+ +
+

Estimated Earnings (APR {estimateAPR().toFixed(2)}%)

+
+
+ Daily: + ~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 365 / 100).toFixed(4)} HEZ +
+
+ Monthly: + ~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 12 / 100).toFixed(4)} HEZ +
+
+ Yearly: + ~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 100).toFixed(4)} HEZ +
+
+
+ +
+ + +
+
+ )} +
+
+ + {/* Impermanent Loss Calculator Tab */} + + +

Impermanent Loss Calculator

+ +
+
+

If HEZ price changes by:

+ +
+ {[10, 25, 50, 100, 200].map((change) => { + const il = calculateImpermanentLoss(change); + return ( +
+ +{change}% + + {il.toFixed(2)}% Loss + +
+ ); + })} +
+
+ + + + +

What is Impermanent Loss?

+

+ Impermanent loss occurs when the price ratio of tokens in the pool changes. + The larger the price change, the greater the loss compared to simply holding the tokens. + Fees earned from swaps can offset this loss over time. +

+
+
+
+
+
+
+ + {/* Modals */} + setIsAddLiquidityModalOpen(false)} + /> + + {lpPosition && poolData && ( + setIsRemoveLiquidityModalOpen(false)} + lpPosition={lpPosition} + lpTokenId={poolData.lpTokenId} + /> + )} +
+ ); +}; + +export default PoolDashboard; diff --git a/src/components/RemoveLiquidityModal.tsx b/src/components/RemoveLiquidityModal.tsx new file mode 100644 index 00000000..babb253f --- /dev/null +++ b/src/components/RemoveLiquidityModal.tsx @@ -0,0 +1,237 @@ +import React, { useState } from 'react'; +import { X, Minus, AlertCircle, Info } from 'lucide-react'; +import { web3FromAddress } from '@polkadot/extension-dapp'; +import { usePolkadot } from '@/contexts/PolkadotContext'; +import { useWallet } from '@/contexts/WalletContext'; +import { Button } from '@/components/ui/button'; +import { Alert, AlertDescription } from '@/components/ui/alert'; + +interface RemoveLiquidityModalProps { + isOpen: boolean; + onClose: () => void; + lpPosition: { + lpTokenBalance: number; + share: number; + asset0Amount: number; + asset1Amount: number; + }; + lpTokenId: number; +} + +export const RemoveLiquidityModal: React.FC = ({ + isOpen, + onClose, + lpPosition, + lpTokenId, +}) => { + const { api, selectedAccount } = usePolkadot(); + const { refreshBalances } = useWallet(); + + const [percentage, setPercentage] = useState(100); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + const handleRemoveLiquidity = async () => { + if (!api || !selectedAccount) return; + + setIsLoading(true); + setError(null); + + try { + // Get the signer from the extension + const injector = await web3FromAddress(selectedAccount.address); + + // Calculate LP tokens to remove + const lpToRemove = (lpPosition.lpTokenBalance * percentage) / 100; + const lpToRemoveBN = BigInt(Math.floor(lpToRemove * 1e12)); + + // Calculate expected token amounts (with 95% slippage tolerance) + const expectedWhezBN = BigInt(Math.floor((lpPosition.asset0Amount * percentage) / 100 * 1e12)); + const expectedPezBN = BigInt(Math.floor((lpPosition.asset1Amount * percentage) / 100 * 1e12)); + + const minWhezBN = (expectedWhezBN * BigInt(95)) / BigInt(100); + const minPezBN = (expectedPezBN * BigInt(95)) / BigInt(100); + + // Remove liquidity transaction + const removeLiquidityTx = api.tx.assetConversion.removeLiquidity( + 0, // asset1 (wHEZ) + 1, // asset2 (PEZ) + lpToRemoveBN.toString(), + minWhezBN.toString(), + minPezBN.toString(), + selectedAccount.address + ); + + // Unwrap wHEZ back to HEZ + const unwrapTx = api.tx.tokenWrapper.unwrap(minWhezBN.toString()); + + // Batch transactions + const tx = api.tx.utility.batchAll([removeLiquidityTx, unwrapTx]); + + await tx.signAndSend( + selectedAccount.address, + { signer: injector.signer }, + ({ status, events }) => { + if (status.isInBlock) { + console.log('Transaction in block'); + } else if (status.isFinalized) { + console.log('Transaction finalized'); + + // Check for errors + const hasError = events.some(({ event }) => + api.events.system.ExtrinsicFailed.is(event) + ); + + if (hasError) { + setError('Transaction failed'); + setIsLoading(false); + } else { + setSuccess(true); + setIsLoading(false); + refreshBalances(); + + setTimeout(() => { + setSuccess(false); + onClose(); + }, 2000); + } + } + } + ); + } catch (err) { + console.error('Error removing liquidity:', err); + setError(err instanceof Error ? err.message : 'Failed to remove liquidity'); + setIsLoading(false); + } + }; + + if (!isOpen) return null; + + const whezToReceive = (lpPosition.asset0Amount * percentage) / 100; + const pezToReceive = (lpPosition.asset1Amount * percentage) / 100; + + return ( +
+
+
+

Remove Liquidity

+ +
+ + {error && ( + + + {error} + + )} + + {success && ( + + Liquidity removed successfully! + + )} + + + + + Remove your liquidity to receive back your tokens. wHEZ will be automatically unwrapped to HEZ. + + + +
+ {/* Percentage Selector */} +
+
+ + {percentage}% +
+ + setPercentage(parseInt(e.target.value))} + className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-500" + disabled={isLoading} + /> + +
+ {[25, 50, 75, 100].map((p) => ( + + ))} +
+
+ + {/* You Will Receive */} +
+

You Will Receive

+ +
+
+

HEZ

+

+ {whezToReceive.toFixed(4)} +

+
+ +
+ +
+
+

PEZ

+

+ {pezToReceive.toFixed(4)} +

+
+ +
+
+ + {/* LP Token Info */} +
+
+ LP Tokens to Burn + {((lpPosition.lpTokenBalance * percentage) / 100).toFixed(4)} +
+
+ Remaining LP Tokens + + {((lpPosition.lpTokenBalance * (100 - percentage)) / 100).toFixed(4)} + +
+
+ Slippage Tolerance + 5% +
+
+ + +
+
+
+ ); +}; diff --git a/src/pages/PoolDashboard.tsx b/src/pages/PoolDashboard.tsx new file mode 100644 index 00000000..0a837547 --- /dev/null +++ b/src/pages/PoolDashboard.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ArrowLeft } from 'lucide-react'; +import PoolDashboard from '@/components/PoolDashboard'; + +const PoolDashboardPage = () => { + const navigate = useNavigate(); + + return ( +
+
+ + +
+
+ ); +}; + +export default PoolDashboardPage; diff --git a/src/pages/WalletDashboard.tsx b/src/pages/WalletDashboard.tsx index 4cefb218..8b62567a 100644 --- a/src/pages/WalletDashboard.tsx +++ b/src/pages/WalletDashboard.tsx @@ -6,7 +6,7 @@ import { TransferModal } from '@/components/TransferModal'; import { ReceiveModal } from '@/components/ReceiveModal'; import { TransactionHistory } from '@/components/TransactionHistory'; import { Button } from '@/components/ui/button'; -import { ArrowUpRight, ArrowDownRight, History, ArrowLeft } from 'lucide-react'; +import { ArrowUpRight, ArrowDownRight, History, ArrowLeft, Activity } from 'lucide-react'; const WalletDashboard: React.FC = () => { const navigate = useNavigate(); @@ -49,7 +49,7 @@ const WalletDashboard: React.FC = () => { {/* Right Column - Actions */}
{/* Quick Actions */} -
+
- + + +