Skip to content
This repository was archived by the owner on Dec 2, 2025. It is now read-only.
This repository was archived by the owner on Dec 2, 2025. It is now read-only.

Wallet Frontend Interface Implementation #263

@JosueBrenes

Description

@JosueBrenes

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

  1. Wallet Components Directory

    • Path: src/components/modules/wallet/
    • Contents: All wallet-related components
  2. Wallet Page

    • Path: src/app/dashboard/wallet/page.tsx
    • Purpose: Main wallet interface page
  3. Wallet Hook

    • Path: src/hooks/useWallet.ts
    • Purpose: Wallet data and operations management
  4. Transaction History Service

    • Path: src/services/transaction-history.service.ts
    • Purpose: Fetch and format transaction data
  5. Asset Management

    • Path: src/lib/stellar-assets.ts
    • Purpose: Asset information and formatting

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions