This repository was archived by the owner on Dec 2, 2025. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 52
This repository was archived by the owner on Dec 2, 2025. It is now read-only.
Wallet Frontend Interface Implementation #263
Copy link
Copy link
Open
Labels
External contributorsgood first issueGood for newcomersGood for newcomersonlydust-waveContribute to awesome OSS repos during OnlyDust's open source weekContribute to awesome OSS repos during OnlyDust's open source week
Description
Description
Create a comprehensive wallet frontend interface that displays all wallet information including balances, transaction history, account details, and wallet management features. This will provide users with complete visibility and control over their automatically-created Stellar wallet.
Reference Implementation: Stellar Smart Wallet Demo
What to Implement
- Dedicated wallet section in the dashboard
- Real-time balance display for all assets
- Transaction history with filtering and search
- Account information and settings
- Send/receive functionality
- Wallet security and backup options
Acceptance Criteria
- Complete wallet dashboard with balance overview
- Real-time balance updates for all Stellar assets
- Transaction history with detailed information
- Send/receive functionality for Stellar assets
- Account information display (sequence, signers, etc.)
- Wallet management and security settings
- Responsive design for mobile and desktop
Technical Requirements
Files to Create
-
Wallet Components Directory
- Path:
src/components/modules/wallet/ - Contents: All wallet-related components
- Path:
-
Wallet Page
- Path:
src/app/dashboard/wallet/page.tsx - Purpose: Main wallet interface page
- Path:
-
Wallet Hook
- Path:
src/hooks/useWallet.ts - Purpose: Wallet data and operations management
- Path:
-
Transaction History Service
- Path:
src/services/transaction-history.service.ts - Purpose: Fetch and format transaction data
- Path:
-
Asset Management
- Path:
src/lib/stellar-assets.ts - Purpose: Asset information and formatting
- Path:
Wallet Components Structure
src/components/modules/wallet/
├── ui/
│ ├── pages/
│ │ └── WalletPage.tsx
│ ├── components/
│ │ ├── BalanceOverview.tsx
│ │ ├── AssetBalance.tsx
│ │ ├── TransactionHistory.tsx
│ │ ├── TransactionItem.tsx
│ │ ├── SendAssetModal.tsx
│ │ ├── ReceiveAssetModal.tsx
│ │ ├── AccountInfo.tsx
│ │ └── WalletSettings.tsx
│ └── charts/
│ └── BalanceChart.tsx
└── hooks/
├── useWalletBalance.ts
├── useTransactionHistory.ts
└── useSendAsset.ts
Implementation Details
Main Wallet Page
// src/app/dashboard/wallet/page.tsx
"use client";
import { useAuth } from "@/providers/wallet.provider";
import { WalletPage } from "@/components/modules/wallet/ui/pages/WalletPage";
import { Card } from "@/components/ui/card";
import { AlertCircle } from "lucide-react";
export default function WalletDashboard() {
const { isAuthenticated, stellarAccount } = useAuth();
if (!isAuthenticated || !stellarAccount) {
return (
<div className="container mx-auto px-4 pt-24 pb-16">
<Card className="p-8 text-center">
<AlertCircle className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<h2 className="text-xl font-semibold mb-2">Wallet Access Denied</h2>
<p className="text-muted-foreground">
Please authenticate to access your wallet.
</p>
</Card>
</div>
);
}
return <WalletPage />;
}Wallet Overview Component
// src/components/modules/wallet/ui/pages/WalletPage.tsx
"use client";
import { useState } from "react";
import { useWallet } from "../../hooks/useWallet";
import { BalanceOverview } from "../components/BalanceOverview";
import { TransactionHistory } from "../components/TransactionHistory";
import { AccountInfo } from "../components/AccountInfo";
import { WalletSettings } from "../components/WalletSettings";
import { SendAssetModal } from "../components/SendAssetModal";
import { ReceiveAssetModal } from "../components/ReceiveAssetModal";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Send, Receive, Settings, Info } from "lucide-react";
export function WalletPage() {
const {
balances,
accountInfo,
transactions,
isLoading,
error,
refreshWalletData
} = useWallet();
const [showSendModal, setShowSendModal] = useState(false);
const [showReceiveModal, setShowReceiveModal] = useState(false);
const [selectedAsset, setSelectedAsset] = useState<string>('');
const handleSendAsset = (assetCode: string) => {
setSelectedAsset(assetCode);
setShowSendModal(true);
};
const handleReceiveAsset = (assetCode: string) => {
setSelectedAsset(assetCode);
setShowReceiveModal(true);
};
if (error) {
return (
<div className="container mx-auto px-4 pt-24 pb-16">
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-6">
<h2 className="text-lg font-semibold text-destructive mb-2">
Wallet Error
</h2>
<p className="text-destructive/80">{error}</p>
<Button
onClick={refreshWalletData}
variant="outline"
className="mt-4"
>
Retry
</Button>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 pt-24 pb-16 max-w-6xl">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold">Wallet</h1>
<p className="text-muted-foreground">
Manage your Stellar assets and transactions
</p>
</div>
<div className="flex gap-2">
<Button
onClick={() => handleSendAsset('')}
className="flex items-center gap-2"
>
<Send className="h-4 w-4" />
Send
</Button>
<Button
onClick={() => handleReceiveAsset('')}
variant="outline"
className="flex items-center gap-2"
>
<Receive className="h-4 w-4" />
Receive
</Button>
</div>
</div>
{/* Balance Overview */}
<BalanceOverview
balances={balances}
isLoading={isLoading}
onSendAsset={handleSendAsset}
onReceiveAsset={handleReceiveAsset}
/>
{/* Tabs */}
<Tabs defaultValue="transactions" className="mt-8">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="transactions">
<span className="flex items-center gap-2">
<span>Transactions</span>
</span>
</TabsTrigger>
<TabsTrigger value="account">
<Info className="h-4 w-4" />
Account Info
</TabsTrigger>
<TabsTrigger value="settings">
<Settings className="h-4 w-4" />
Settings
</TabsTrigger>
</TabsList>
<TabsContent value="transactions" className="mt-6">
<TransactionHistory
transactions={transactions}
isLoading={isLoading}
accountId={accountInfo?.accountId}
/>
</TabsContent>
<TabsContent value="account" className="mt-6">
<AccountInfo
accountInfo={accountInfo}
isLoading={isLoading}
/>
</TabsContent>
<TabsContent value="settings" className="mt-6">
<WalletSettings
accountInfo={accountInfo}
/>
</TabsContent>
</Tabs>
{/* Modals */}
<SendAssetModal
isOpen={showSendModal}
onClose={() => setShowSendModal(false)}
initialAsset={selectedAsset}
availableBalances={balances}
/>
<ReceiveAssetModal
isOpen={showReceiveModal}
onClose={() => setShowReceiveModal(false)}
assetCode={selectedAsset}
accountId={accountInfo?.accountId}
/>
</div>
);
}Balance Overview Component
// src/components/modules/wallet/ui/components/BalanceOverview.tsx
"use client";
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { AssetBalance } from "./AssetBalance";
import { BalanceChart } from "../charts/BalanceChart";
import { formatCurrency } from "@/lib/utils";
import { TrendingUp, TrendingDown, Minus, RefreshCw } from "lucide-react";
interface Balance {
assetCode: string;
assetType: string;
balance: string;
limit?: string;
issuer?: string;
usdValue?: number;
change24h?: number;
}
interface BalanceOverviewProps {
balances: Balance[];
isLoading: boolean;
onSendAsset: (assetCode: string) => void;
onReceiveAsset: (assetCode: string) => void;
}
export function BalanceOverview({
balances,
isLoading,
onSendAsset,
onReceiveAsset
}: BalanceOverviewProps) {
const [showChart, setShowChart] = useState(false);
const totalUsdValue = balances.reduce(
(sum, balance) => sum + (balance.usdValue || 0),
0
);
const totalChange24h = balances.reduce(
(sum, balance) => sum + (balance.change24h || 0),
0
);
const changeIcon = totalChange24h > 0
? <TrendingUp className="h-4 w-4 text-green-500" />
: totalChange24h < 0
? <TrendingDown className="h-4 w-4 text-red-500" />
: <Minus className="h-4 w-4 text-muted-foreground" />;
if (isLoading) {
return (
<div className="space-y-6">
{/* Total Balance Card Skeleton */}
<Card>
<CardHeader>
<CardTitle>Total Balance</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-32" />
</CardContent>
</Card>
{/* Assets Grid Skeleton */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<Card key={i}>
<CardContent className="p-6">
<div className="space-y-3">
<Skeleton className="h-6 w-16" />
<Skeleton className="h-8 w-24" />
<Skeleton className="h-4 w-20" />
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Total Balance Card */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Total Portfolio Value</CardTitle>
<Button
variant="ghost"
size="sm"
onClick={() => setShowChart(!showChart)}
>
<RefreshCw className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="text-3xl font-bold">
{formatCurrency(totalUsdValue)}
</div>
<div className="flex items-center gap-2 text-sm">
{changeIcon}
<span className={
totalChange24h > 0
? "text-green-500"
: totalChange24h < 0
? "text-red-500"
: "text-muted-foreground"
}>
{totalChange24h > 0 ? '+' : ''}{totalChange24h.toFixed(2)}%
</span>
<span className="text-muted-foreground">24h</span>
</div>
</div>
{showChart && (
<div className="mt-6">
<BalanceChart balances={balances} />
</div>
)}
</CardContent>
</Card>
{/* Assets Grid */}
<div>
<h3 className="text-lg font-semibold mb-4">Your Assets</h3>
{balances.length === 0 ? (
<Card>
<CardContent className="p-8 text-center">
<div className="text-muted-foreground mb-4">
<div className="text-4xl mb-2">💰</div>
<p>No assets found</p>
<p className="text-sm mt-1">
Your account is active but doesn't have any assets yet.
</p>
</div>
<Button onClick={() => onReceiveAsset('')}>
Receive Assets
</Button>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{balances.map((balance) => (
<AssetBalance
key={`${balance.assetCode}-${balance.issuer || 'native'}`}
balance={balance}
onSend={() => onSendAsset(balance.assetCode)}
onReceive={() => onReceiveAsset(balance.assetCode)}
/>
))}
</div>
)}
</div>
</div>
);
}Asset Balance Card Component
// src/components/modules/wallet/ui/components/AssetBalance.tsx
"use client";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { formatCurrency, formatAssetAmount } from "@/lib/utils";
import { Send, Receive, TrendingUp, TrendingDown } from "lucide-react";
interface AssetBalanceProps {
balance: {
assetCode: string;
assetType: string;
balance: string;
limit?: string;
issuer?: string;
usdValue?: number;
change24h?: number;
};
onSend: () => void;
onReceive: () => void;
}
export function AssetBalance({ balance, onSend, onReceive }: AssetBalanceProps) {
const isNative = balance.assetType === 'native';
const displayCode = isNative ? 'XLM' : balance.assetCode;
const numericBalance = parseFloat(balance.balance);
const getAssetIcon = (assetCode: string) => {
const icons: Record<string, string> = {
'XLM': '/img/tokens/xlm.png',
'USDC': '/img/tokens/usdc.png',
'TBRG': '/img/tokens/tbt.png',
};
return icons[assetCode] || '/img/tokens/default.png';
};
const getAssetName = (assetCode: string) => {
const names: Record<string, string> = {
'XLM': 'Stellar Lumens',
'USDC': 'USD Coin',
'TBRG': 'TrustBridge Token',
};
return names[assetCode] || assetCode;
};
return (
<Card className="hover:shadow-md transition-shadow">
<CardContent className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<img
src={getAssetIcon(displayCode)}
alt={`${displayCode} icon`}
className="w-10 h-10 rounded-full"
onError={(e) => {
(e.target as HTMLImageElement).src = '/img/tokens/default.png';
}}
/>
<div>
<div className="font-semibold">{displayCode}</div>
<div className="text-sm text-muted-foreground">
{getAssetName(displayCode)}
</div>
</div>
</div>
{!isNative && balance.issuer && (
<Badge variant="outline" className="text-xs">
Asset
</Badge>
)}
</div>
<div className="space-y-2 mb-4">
<div className="text-2xl font-bold">
{formatAssetAmount(numericBalance, displayCode)}
</div>
{balance.usdValue !== undefined && (
<div className="text-sm text-muted-foreground">
{formatCurrency(balance.usdValue)}
</div>
)}
{balance.change24h !== undefined && (
<div className={`flex items-center gap-1 text-sm ${
balance.change24h > 0
? 'text-green-500'
: balance.change24h < 0
? 'text-red-500'
: 'text-muted-foreground'
}`}>
{balance.change24h > 0 ? (
<TrendingUp className="h-3 w-3" />
) : balance.change24h < 0 ? (
<TrendingDown className="h-3 w-3" />
) : null}
{balance.change24h > 0 ? '+' : ''}{balance.change24h.toFixed(2)}%
</div>
)}
</div>
{balance.limit && (
<div className="text-xs text-muted-foreground mb-4">
Limit: {formatAssetAmount(parseFloat(balance.limit), displayCode)}
</div>
)}
<div className="flex gap-2">
<Button
size="sm"
onClick={onSend}
disabled={numericBalance === 0}
className="flex-1"
>
<Send className="h-3 w-3 mr-1" />
Send
</Button>
<Button
size="sm"
variant="outline"
onClick={onReceive}
className="flex-1"
>
<Receive className="h-3 w-3 mr-1" />
Receive
</Button>
</div>
</CardContent>
</Card>
);
}Wallet Data Hook
// src/hooks/useWallet.ts
"use client";
import { useState, useEffect, useCallback } from "react";
import { useAuth } from "@/providers/wallet.provider";
import { stellarWalletService } from "@/services/stellar-wallet.service";
import { transactionHistoryService } from "@/services/transaction-history.service";
export function useWallet() {
const { stellarAccount, isAuthenticated } = useAuth();
const [balances, setBalances] = useState<any[]>([]);
const [accountInfo, setAccountInfo] = useState<any>(null);
const [transactions, setTransactions] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const refreshWalletData = useCallback(async () => {
if (!stellarAccount || !isAuthenticated) return;
setIsLoading(true);
setError(null);
try {
// Fetch account info and balances
const [accountData, balanceData, transactionData] = await Promise.all([
stellarWalletService.getAccountInfo(stellarAccount),
stellarWalletService.getAccountBalance(stellarAccount),
transactionHistoryService.getTransactionHistory(stellarAccount)
]);
setAccountInfo(accountData);
setBalances(balanceData || []);
setTransactions(transactionData || []);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load wallet data');
} finally {
setIsLoading(false);
}
}, [stellarAccount, isAuthenticated]);
// Initial data load
useEffect(() => {
refreshWalletData();
}, [refreshWalletData]);
// Periodic refresh every 30 seconds
useEffect(() => {
if (!stellarAccount) return;
const interval = setInterval(refreshWalletData, 30000);
return () => clearInterval(interval);
}, [stellarAccount, refreshWalletData]);
return {
balances,
accountInfo,
transactions,
isLoading,
error,
refreshWalletData,
};
}Send Asset Modal
// src/components/modules/wallet/ui/components/SendAssetModal.tsx
"use client";
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { useSendAsset } from "../../hooks/useSendAsset";
import { formatAssetAmount } from "@/lib/utils";
import { Loader, AlertCircle } from "lucide-react";
interface SendAssetModalProps {
isOpen: boolean;
onClose: () => void;
initialAsset?: string;
availableBalances: any[];
}
export function SendAssetModal({
isOpen,
onClose,
initialAsset = '',
availableBalances
}: SendAssetModalProps) {
const [selectedAsset, setSelectedAsset] = useState(initialAsset);
const [destination, setDestination] = useState('');
const [amount, setAmount] = useState('');
const [memo, setMemo] = useState('');
const { sendAsset, isLoading, error } = useSendAsset();
const selectedBalance = availableBalances.find(
balance => balance.assetCode === selectedAsset ||
(selectedAsset === 'XLM' && balance.assetType === 'native')
);
const maxAmount = selectedBalance ? parseFloat(selectedBalance.balance) : 0;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedAsset || !destination || !amount) return;
try {
await sendAsset({
assetCode: selectedAsset,
destination,
amount: parseFloat(amount),
memo: memo || undefined
});
// Reset form and close modal
setSelectedAsset('');
setDestination('');
setAmount('');
setMemo('');
onClose();
} catch (err) {
// Error is handled by the hook
}
};
const handleMaxClick = () => {
if (selectedBalance) {
// Reserve some XLM for fees
const reserveAmount = selectedAsset === 'XLM' ? 0.5 : 0;
const availableAmount = Math.max(0, maxAmount - reserveAmount);
setAmount(availableAmount.toString());
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Send Asset</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Asset Selection */}
<div>
<Label htmlFor="asset">Asset</Label>
<Select value={selectedAsset} onValueChange={setSelectedAsset}>
<SelectTrigger>
<SelectValue placeholder="Select asset to send" />
</SelectTrigger>
<SelectContent>
{availableBalances.map((balance) => {
const assetCode = balance.assetType === 'native' ? 'XLM' : balance.assetCode;
const balanceAmount = parseFloat(balance.balance);
return (
<SelectItem
key={`${assetCode}-${balance.issuer || 'native'}`}
value={assetCode}
disabled={balanceAmount === 0}
>
<div className="flex justify-between w-full">
<span>{assetCode}</span>
<span className="text-muted-foreground">
{formatAssetAmount(balanceAmount, assetCode)}
</span>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
{/* Destination */}
<div>
<Label htmlFor="destination">Destination Address</Label>
<Input
id="destination"
value={destination}
onChange={(e) => setDestination(e.target.value)}
placeholder="GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
className="font-mono text-sm"
/>
</div>
{/* Amount */}
<div>
<Label htmlFor="amount">Amount</Label>
<div className="relative">
<Input
id="amount"
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.00"
min="0"
max={maxAmount}
step="0.0000001"
className="pr-16"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 px-2"
onClick={handleMaxClick}
disabled={!selectedBalance}
>
Max
</Button>
</div>
{selectedBalance && (
<p className="text-sm text-muted-foreground mt-1">
Available: {formatAssetAmount(maxAmount, selectedAsset)}
</p>
)}
</div>
{/* Memo */}
<div>
<Label htmlFor="memo">Memo (Optional)</Label>
<Input
id="memo"
value={memo}
onChange={(e) => setMemo(e.target.value)}
placeholder="Transaction memo"
maxLength={28}
/>
</div>
{/* Error Alert */}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Submit Buttons */}
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={onClose}
disabled={isLoading}
className="flex-1"
>
Cancel
</Button>
<Button
type="submit"
disabled={!selectedAsset || !destination || !amount || isLoading}
className="flex-1"
>
{isLoading ? (
<>
<Loader className="mr-2 h-4 w-4 animate-spin" />
Sending...
</>
) : (
'Send'
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}Receive Asset Modal
// src/components/modules/wallet/ui/components/ReceiveAssetModal.tsx
"use client";
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { QrCode, Copy, Check } from "lucide-react";
import { toast } from "sonner";
interface ReceiveAssetModalProps {
isOpen: boolean;
onClose: () => void;
assetCode?: string;
accountId?: string;
}
export function ReceiveAssetModal({
isOpen,
onClose,
assetCode = '',
accountId = ''
}: ReceiveAssetModalProps) {
const [copied, setCopied] = useState(false);
const [memo, setMemo] = useState('');
const handleCopyAddress = async () => {
if (!accountId) return;
try {
await navigator.clipboard.writeText(accountId);
setCopied(true);
toast.success('Address copied to clipboard');
setTimeout(() => setCopied(false), 2000);
} catch (err) {
toast.error('Failed to copy address');
}
};
const generateQRCodeUrl = () => {
if (!accountId) return '';
const baseUrl = 'https://api.qrserver.com/v1/create-qr-code/';
const params = new URLSearchParams({
size: '200x200',
data: accountId,
bgcolor: 'ffffff',
color: '000000'
});
return `${baseUrl}?${params.toString()}`;
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>
Receive {assetCode || 'Assets'}
</DialogTitle>
</DialogHeader>
<div className="space-y-6">
{/* QR Code */}
<div className="flex justify-center">
<div className="p-4 bg-white rounded-lg border">
{accountId ? (
<img
src={generateQRCodeUrl()}
alt="Account QR Code"
className="w-48 h-48"
/>
) : (
<div className="w-48 h-48 bg-muted rounded flex items-center justify-center">
<QrCode className="h-12 w-12 text-muted-foreground" />
</div>
)}
</div>
</div>
{/* Account Address */}
<div>
<Label htmlFor="address">Your Stellar Address</Label>
<div className="flex gap-2 mt-2">
<Input
id="address"
value={accountId}
readOnly
className="font-mono text-sm"
/>
<Button
variant="outline"
size="sm"
onClick={handleCopyAddress}
disabled={!accountId}
>
{copied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
{/* Optional Memo */}
<div>
<Label htmlFor="memo">Memo (Optional)</Label>
<Input
id="memo"
value={memo}
onChange={(e) => setMemo(e.target.value)}
placeholder="Add a memo for the sender"
maxLength={28}
/>
<p className="text-sm text-muted-foreground mt-1">
Share this memo with the sender if you want to identify the transaction
</p>
</div>
{/* Instructions */}
<div className="bg-muted/50 rounded-lg p-4">
<h4 className="font-medium mb-2">Instructions</h4>
<ul className="text-sm text-muted-foreground space-y-1">
<li>• Share your address with the sender</li>
<li>• Make sure they're sending on the Stellar network</li>
<li>• Include the memo if you provided one</li>
<li>• Transactions typically confirm within 5 seconds</li>
</ul>
</div>
<Button onClick={onClose} className="w-full">
Done
</Button>
</div>
</DialogContent>
</Dialog>
);
}Navigation Integration
Add Wallet Route to Sidebar
// Update src/components/layouts/sidebar/Sidebar.tsx
const navItems = [
{
href: '/dashboard',
label: 'Dashboard',
icon: Home,
},
{
href: '/dashboard/marketplace',
label: 'Marketplace',
icon: Store,
},
{
href: '/dashboard/wallet', // Add this
label: 'Wallet',
icon: Wallet,
},
{
href: '/dashboard/profile',
label: 'Profile',
icon: User,
},
];Utility Functions
Asset Formatting
// src/lib/stellar-assets.ts
export function formatAssetAmount(amount: number, assetCode: string): string {
const decimals = assetCode === 'XLM' ? 7 : 2;
if (amount === 0) return '0';
if (amount < 0.0001) return '< 0.0001';
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: decimals,
}).format(amount);
}
export function getAssetDisplayName(assetCode: string, issuer?: string): string {
const wellKnownAssets: Record<string, string> = {
'XLM': 'Stellar Lumens',
'USDC': 'USD Coin',
'TBRG': 'TrustBridge Token',
};
return wellKnownAssets[assetCode] || assetCode;
}
export function getAssetIcon(assetCode: string): string {
const icons: Record<string, string> = {
'XLM': '/img/tokens/xlm.png',
'USDC': '/img/tokens/usdc.png',
'TBRG': '/img/tokens/tbt.png',
};
return icons[assetCode] || '/img/tokens/default.png';
}Performance Considerations
- Real-time balance updates with WebSocket connections
- Efficient data caching to reduce API calls
- Lazy loading for transaction history
- Optimized re-rendering with React.memo
Security Features
- Address validation before sending
- Transaction confirmation modals
- Clear display of transaction fees
- Memo field for transaction identification
Dependencies
UI Components
- Existing shadcn/ui components
- Chart library for balance visualization
- QR code generation service
Stellar Integration
- Stellar SDK for operations
- Horizon API for data fetching
- Real-time transaction monitoring
Definition of Done
- Complete wallet interface with all sections working
- Real-time balance display and updates
- Send/receive functionality working correctly
- Transaction history displaying properly
- Account information showing all details
- Responsive design working on all devices
- Error handling comprehensive and user-friendly
- Security measures properly implemented
- Performance optimized for smooth experience
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
External contributorsgood first issueGood for newcomersGood for newcomersonlydust-waveContribute to awesome OSS repos during OnlyDust's open source weekContribute to awesome OSS repos during OnlyDust's open source week