From 2d1a2cf3bab682cf4fdf38e1da18322b573b602b Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Tue, 28 Oct 2025 00:28:20 +0300 Subject: [PATCH] feat: Add wallet dashboard with balance and transfer functionality New Components: - AccountBalance: Real-time balance display with free/reserved breakdown - TransferModal: Token transfer interface with transaction signing - WalletDashboard: Complete wallet management page Features: - Live balance fetching from blockchain - Balance subscription for real-time updates - Transfer modal with recipient and amount input - Transaction signing via Polkadot.js extension - Transaction status tracking (signing, pending, success, error) - Account switching support - Responsive dashboard layout - Quick action buttons (Send, Receive, History) Technical: - Integration with PolkadotContext - web3FromAddress for transaction signing - signAndSend for blockchain transactions - Balance conversion (plancks to tokens) - Error handling and user feedback - Toast notifications for transaction status Navigation: - Added /wallet route with ProtectedRoute - Added Wallet link to navigation menu --- src/App.tsx | 7 +- src/components/AccountBalance.tsx | 179 ++++++++++++++++++++++ src/components/AppLayout.tsx | 7 + src/components/TransferModal.tsx | 243 ++++++++++++++++++++++++++++++ src/pages/WalletDashboard.tsx | 91 +++++++++++ 5 files changed, 526 insertions(+), 1 deletion(-) create mode 100644 src/components/AccountBalance.tsx create mode 100644 src/components/TransferModal.tsx create mode 100644 src/pages/WalletDashboard.tsx diff --git a/src/App.tsx b/src/App.tsx index cc5d9322..86e69395 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import EmailVerification from '@/pages/EmailVerification'; import PasswordReset from '@/pages/PasswordReset'; import ProfileSettings from '@/pages/ProfileSettings'; import AdminPanel from '@/pages/AdminPanel'; - +import WalletDashboard from './pages/WalletDashboard'; import { AppProvider } from '@/contexts/AppContext'; import { WalletProvider } from '@/contexts/WalletContext'; import { WebSocketProvider } from '@/contexts/WebSocketContext'; @@ -49,6 +49,11 @@ function App() { } /> + + + + } /> } /> diff --git a/src/components/AccountBalance.tsx b/src/components/AccountBalance.tsx new file mode 100644 index 00000000..b81405ad --- /dev/null +++ b/src/components/AccountBalance.tsx @@ -0,0 +1,179 @@ +import React, { useEffect, useState } from 'react'; +import { usePolkadot } from '@/contexts/PolkadotContext'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Wallet, TrendingUp, ArrowUpRight, ArrowDownRight, RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +export const AccountBalance: React.FC = () => { + const { api, isApiReady, selectedAccount } = usePolkadot(); + const [balance, setBalance] = useState<{ + free: string; + reserved: string; + total: string; + }>({ + free: '0', + reserved: '0', + total: '0', + }); + const [isLoading, setIsLoading] = useState(false); + + const fetchBalance = async () => { + if (!api || !isApiReady || !selectedAccount) return; + + setIsLoading(true); + try { + const { data: balanceData } = await api.query.system.account(selectedAccount.address); + + const free = balanceData.free.toString(); + const reserved = balanceData.reserved.toString(); + + // Convert from plancks to tokens (assuming 12 decimals like DOT) + const decimals = 12; + const divisor = Math.pow(10, decimals); + + const freeTokens = (parseInt(free) / divisor).toFixed(4); + const reservedTokens = (parseInt(reserved) / divisor).toFixed(4); + const totalTokens = ((parseInt(free) + parseInt(reserved)) / divisor).toFixed(4); + + setBalance({ + free: freeTokens, + reserved: reservedTokens, + total: totalTokens, + }); + } catch (error) { + console.error('Failed to fetch balance:', error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchBalance(); + + // Subscribe to balance updates + let unsubscribe: () => void; + + const subscribeBalance = async () => { + if (!api || !isApiReady || !selectedAccount) return; + + unsubscribe = await api.query.system.account( + selectedAccount.address, + ({ data: balanceData }) => { + const free = balanceData.free.toString(); + const reserved = balanceData.reserved.toString(); + + const decimals = 12; + const divisor = Math.pow(10, decimals); + + const freeTokens = (parseInt(free) / divisor).toFixed(4); + const reservedTokens = (parseInt(reserved) / divisor).toFixed(4); + const totalTokens = ((parseInt(free) + parseInt(reserved)) / divisor).toFixed(4); + + setBalance({ + free: freeTokens, + reserved: reservedTokens, + total: totalTokens, + }); + } + ); + }; + + subscribeBalance(); + + return () => { + if (unsubscribe) unsubscribe(); + }; + }, [api, isApiReady, selectedAccount]); + + if (!selectedAccount) { + return ( + + +
+ +

Connect your wallet to view balance

+
+
+
+ ); + } + + return ( +
+ {/* Total Balance Card */} + + +
+ + Total Balance + + +
+
+ +
+
+
+ {isLoading ? '...' : balance.total} + HEZ +
+
+ ≈ ${(parseFloat(balance.total) * 0.5).toFixed(2)} USD +
+
+ +
+
+
+ + Transferable +
+
+ {balance.free} HEZ +
+
+ +
+
+ + Reserved +
+
+ {balance.reserved} HEZ +
+
+
+
+
+
+ + {/* Account Info */} + + +
+
+ Account + + {selectedAccount.meta.name || 'Unnamed'} + +
+
+ Address + + {selectedAccount.address.slice(0, 8)}...{selectedAccount.address.slice(-8)} + +
+
+
+
+
+ ); +}; diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index a7bc7181..4a75ab24 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -90,6 +90,13 @@ const AppLayout: React.FC = () => { {t('nav.dashboard', 'Dashboard')} + + + ) : ( +
+
+ + setRecipient(e.target.value)} + placeholder="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + className="bg-gray-800 border-gray-700 text-white mt-2" + disabled={isTransferring} + /> +
+ +
+ + setAmount(e.target.value)} + placeholder="0.0000" + className="bg-gray-800 border-gray-700 text-white mt-2" + disabled={isTransferring} + /> +
+ + {txStatus === 'signing' && ( +
+

+ Please sign the transaction in your Polkadot.js extension +

+
+ )} + + {txStatus === 'pending' && ( +
+

+ + Transaction pending... Waiting for finalization +

+
+ )} + + +
+ )} + + + ); +}; diff --git a/src/pages/WalletDashboard.tsx b/src/pages/WalletDashboard.tsx new file mode 100644 index 00000000..b3afec74 --- /dev/null +++ b/src/pages/WalletDashboard.tsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { usePolkadot } from '@/contexts/PolkadotContext'; +import { AccountBalance } from '@/components/AccountBalance'; +import { TransferModal } from '@/components/TransferModal'; +import { Button } from '@/components/ui/button'; +import { ArrowUpRight, ArrowDownRight, History } from 'lucide-react'; + +const WalletDashboard: React.FC = () => { + const { selectedAccount } = usePolkadot(); + const [isTransferModalOpen, setIsTransferModalOpen] = useState(false); + + if (!selectedAccount) { + return ( +
+
+

Wallet Not Connected

+

Please connect your wallet to view your dashboard

+
+
+ ); + } + + return ( +
+
+
+

Wallet Dashboard

+

Manage your HEZ and PEZ tokens

+
+ +
+ {/* Left Column - Balance */} +
+ +
+ + {/* Right Column - Actions */} +
+ {/* Quick Actions */} +
+ + + + + +
+ + {/* Recent Activity Placeholder */} +
+

Recent Activity

+
+ +

No recent transactions

+

+ Your transaction history will appear here +

+
+
+
+
+
+ + setIsTransferModalOpen(false)} + /> +
+ ); +}; + +export default WalletDashboard;