From 9608fe9df367095ec559915774f72b8b3d16f7a4 Mon Sep 17 00:00:00 2001
From: Muhammad Zayyad Mukhtar
<95658387+El-swaggerito@users.noreply.github.com>
Date: Wed, 29 Apr 2026 11:45:51 +0100
Subject: [PATCH 1/2] refactor: enhance code documentation and component
consistency
- Add comprehensive JSDoc comments to all major components and utilities
- Improve UI component variants and accessibility attributes
- Standardize error handling and logging across API routes
- Enhance type safety in Stellar integration modules
- Update formatting utilities with additional helper functions
---
src/app/api/pnl/route.ts | 26 ++++-
src/app/api/upload/route.ts | 24 +++-
src/app/page.tsx | 68 +++++++++--
src/components/SwapInterface.tsx | 110 +++++++++++++-----
src/components/ui/Button.tsx | 49 +++++++-
src/components/ui/NetworkFeeIndicator.tsx | 77 ++++++++++---
src/lib/format.ts | 78 +++++++++----
src/lib/parser.ts | 75 ++++++++-----
src/lib/stellar.ts | 131 ++++++++++++++++------
src/stores/tokenStore.ts | 79 +++++++++++--
src/stores/useWeb3Store.ts | 66 +++++++++--
11 files changed, 617 insertions(+), 166 deletions(-)
diff --git a/src/app/api/pnl/route.ts b/src/app/api/pnl/route.ts
index f57e640..e89b3a7 100644
--- a/src/app/api/pnl/route.ts
+++ b/src/app/api/pnl/route.ts
@@ -1,21 +1,39 @@
+/**
+ * Profit and Loss (PnL) API Route.
+ * Generates synthetic historical performance data for the dashboard charts.
+ * This is used for demonstrating portfolio tracking features.
+ */
+
import { NextResponse } from 'next/server';
+/**
+ * Historical data point for the PnL chart.
+ */
interface PnLData {
+ /** Localized date string (e.g., "Jan 12") */
date: string;
+ /** The portfolio value at that specific point in time */
value: number;
}
+/**
+ * GET handler for the PnL endpoint.
+ * Returns a 30-day series of simulated portfolio values.
+ */
export async function GET() {
- // Generate dummy PnL data for the last 30 days
+ // Generate mock PnL data for the last 30 days
const data: PnLData[] = [];
const today = new Date();
- let currentValue = 10000; // Starting value
+
+ // Starting seed value for the simulation
+ let currentValue = 10000;
for (let i = 29; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
- // Random walk with slight upward trend
+ // Simulate a random walk with a slight positive bias (0.45 instead of 0.50)
+ // and a volatility factor of 200
const change = (Math.random() - 0.45) * 200;
currentValue += change;
@@ -25,5 +43,7 @@ export async function GET() {
});
}
+ // Return the series as a JSON response
return NextResponse.json(data);
}
+
diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts
index 94c21e9..4380ead 100644
--- a/src/app/api/upload/route.ts
+++ b/src/app/api/upload/route.ts
@@ -1,8 +1,30 @@
+/**
+ * Document Upload API Route.
+ * Handles the secure uploading of invoice documents to IPFS/Pinata.
+ * Currently disabled for maintenance or pending further security implementation.
+ */
+
import { NextRequest, NextResponse } from 'next/server';
+/**
+ * POST handler for the upload endpoint.
+ * Currently returns a 503 Service Unavailable error as the feature is locked.
+ *
+ * @param {NextRequest} request - The incoming upload request.
+ */
export async function POST(request: NextRequest) {
+ // 1. Log the attempt for security auditing
+ const clientIp = request.headers.get('x-forwarded-for') || 'unknown';
+ console.log(`[UploadAPI] Blocked upload attempt from ${clientIp}`);
+
+ // 2. Return a consistent error response
return NextResponse.json(
- { error: 'Upload temporarily disabled' },
+ {
+ error: 'Upload service temporarily disabled',
+ reason: 'Undergoing maintenance',
+ retryAfter: 3600
+ },
{ status: 503 }
);
}
+
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 16537de..02b443e 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,3 +1,9 @@
+/**
+ * TradeFlow Main Dashboard Page.
+ * This is the primary entry point for the application, providing users with
+ * a high-level overview of their assets, protocol status, and the RWA pipeline.
+ */
+
"use client";
import React, { useState, useEffect } from "react";
@@ -19,63 +25,103 @@ import TabNavigation from "../components/TabNavigation";
import { useWatchlist } from "../hooks/useWatchlist";
import StarIcon from "../components/StarIcon";
+/**
+ * The root component for the TradeFlow dashboard.
+ * Manages high-level state for wallet connection, active tabs, and invoice data.
+ */
export default function Page() {
+ // --- Component State ---
+ /** The public Stellar address of the connected user */
const [address, setAddress] = useState("");
+ /** List of invoices fetched from the backend pipeline */
const [invoices, setInvoices] = useState([]);
+ /** Loading state for initial data fetch */
const [loading, setLoading] = useState(false);
+ /** Controls visibility of the Invoice Minting modal */
const [showMintForm, setShowMintForm] = useState(false);
+ /** Controls visibility of the Wallet Selection modal */
const [isModalOpen, setIsModalOpen] = useState(false);
+ /** Currently active navigation tab (dashboard or watchlist) */
const [activeTab, setActiveTab] = useState("dashboard");
+
+ /** Watchlist management hook */
const { toggleWatchlist, isInWatchlist } = useWatchlist();
- // 1. Connect Stellar Wallet (supports Freighter, Albedo, xBull)
+ // --- Handlers ---
+
+ /**
+ * Triggers the Stellar wallet connection flow.
+ * Supports multiple providers via the stellar-wallets-kit.
+ *
+ * @param {WalletType} walletType - The ID of the wallet provider (e.g., Freighter).
+ */
const handleConnectWallet = async (walletType: WalletType) => {
try {
const userInfo = await connectWallet(walletType);
if (userInfo && userInfo.publicKey) {
setAddress(userInfo.publicKey);
- console.log("Wallet connected:", userInfo.publicKey, "Type:", userInfo.walletType);
+ console.log("[Dashboard] Wallet connected:", userInfo.publicKey, "Provider:", userInfo.walletType);
}
} catch (e: any) {
- console.error("Connection failed:", e.message);
+ console.error("[Dashboard] Connection failed:", e.message);
+ // In production, this would be a user-friendly toast notification
alert(e.message || "Failed to connect to wallet.");
}
};
- // 2. Fetch Invoices from your Repo 2 API
+ /**
+ * Fetches the latest verified assets from the RWA pipeline.
+ * Currently points to a local mock API for development.
+ */
const fetchInvoices = async () => {
setLoading(true);
try {
+ // TODO: Replace with environment-aware API base URL
const res = await fetch("http://localhost:3000/invoices");
const data = await res.json();
setInvoices(data);
} catch (e) {
- console.error("API not running");
+ console.warn("[Dashboard] Asset pipeline API not reachable. Check if local server is running.");
} finally {
setLoading(false);
}
};
+ // --- Lifecycle Hooks ---
+
useEffect(() => {
fetchInvoices();
}, []);
+
+ /**
+ * Debugging utility for testing transaction status notifications.
+ */
const handleTestToast = () => {
- useTransactionToast().loading();
- useTransactionToast().success();
- useTransactionToast().error();
+ const toast = useTransactionToast();
+ toast.loading();
+ setTimeout(() => toast.success(), 2000);
};
+ /**
+ * Callback triggered when a new invoice is successfully submitted for minting.
+ *
+ * @param {any} data - The validated invoice metadata.
+ */
const handleInvoiceMint = (data: any) => {
- console.log("Invoice data received:", data);
+ console.log("[Dashboard] Mint request received:", data);
setShowMintForm(false);
- // TODO: Chain integration will be handled separately
+ // TODO: Initiate Soroban contract call for minting the NFT
};
+ // --- Configuration ---
+
+ /** Tab definitions for the main navigation */
const tabs = [
{ id: "dashboard", label: "Dashboard" },
- { id: "watchlist", label: "Watchlist", icon: },
+ { id: "watchlist", label: "Watchlist", icon: },
];
+
return (
{/* News Banner */}
diff --git a/src/components/SwapInterface.tsx b/src/components/SwapInterface.tsx
index dd1dc35..5262131 100644
--- a/src/components/SwapInterface.tsx
+++ b/src/components/SwapInterface.tsx
@@ -1,3 +1,10 @@
+/**
+ * Swap Interface Component.
+ * Provides a decentralized exchange (DEX) style interface for swapping
+ * Stellar assets. Includes slippage control, price impact estimation,
+ * and transaction status monitoring.
+ */
+
"use client";
import React, { useState, useEffect } from "react";
@@ -17,27 +24,44 @@ import LivePriceChart from "./LivePriceChart";
/* ISSUE #87: Import the new Success/Share modal */
import TradeSuccessModal from "./TradeSuccessModal";
+/**
+ * Main component for the token swap functionality.
+ */
export default function SwapInterface() {
+ // --- Token Selection State ---
+ /** The asset code of the token being sold */
const [fromToken, setFromToken] = useState("XLM");
+ /** The asset code of the token being bought */
const [toToken, setToToken] = useState("USDC");
+
+ // --- UI Visibility State ---
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isHighSlippageWarningOpen, setIsHighSlippageWarningOpen] = useState(false);
- const [fromAmount, setFromAmount] = useState("");
- const [toAmount, setToAmount] = useState("");
- const [priceImpact, setPriceImpact] = useState(0);
- const [isSubmitting, setIsSubmitting] = useState(false);
const [isTradeReviewOpen, setIsTradeReviewOpen] = useState(false);
const [isTransactionSignatureOpen, setIsTransactionSignatureOpen] = useState(false);
-
- /* ISSUE #87: State to manage the visibility of the growth/share modal */
+ /** Visibility for the post-trade growth/share modal */
const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false);
+ // --- Trade Value State ---
+ const [fromAmount, setFromAmount] = useState("");
+ const [toAmount, setToAmount] = useState("");
+ /** Estimated price impact as a percentage */
+ const [priceImpact, setPriceImpact] = useState(0);
+ /** Mock balance for the selected source token */
+ const [fromBalance] = useState("1240.50");
+
+ // --- Submission & Timing State ---
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [submissionStartTime, setSubmissionStartTime] = useState(null);
const [timeLeft, setTimeLeft] = useState("");
- const [fromBalance] = useState("1240.50");
+
+ // --- Context Hooks ---
const { slippageTolerance, transactionDeadline } = useSlippage();
- // Countdown timer effect
+ /**
+ * Countdown timer effect for transaction expiry.
+ * Runs when a transaction is pending submission.
+ */
useEffect(() => {
let interval: any;
if (isSubmitting && submissionStartTime) {
@@ -64,7 +88,9 @@ export default function SwapInterface() {
return () => clearInterval(interval);
}, [isSubmitting, submissionStartTime, transactionDeadline]);
- // Load saved token selections on mount
+ /**
+ * Persists token selections across page reloads.
+ */
useEffect(() => {
const savedFromToken = localStorage.getItem('tradeflow-fromToken');
const savedToToken = localStorage.getItem('tradeflow-toToken');
@@ -73,7 +99,6 @@ export default function SwapInterface() {
if (savedToToken) setToToken(savedToToken);
}, []);
- // Save token selections to localStorage
useEffect(() => {
localStorage.setItem('tradeflow-fromToken', fromToken);
}, [fromToken]);
@@ -82,7 +107,13 @@ export default function SwapInterface() {
localStorage.setItem('tradeflow-toToken', toToken);
}, [toToken]);
- // Calculate price impact
+ /**
+ * Estimates the price impact based on the input amount.
+ * This is a simplified mock for development purposes.
+ *
+ * @param {string} amount - The numeric amount string.
+ * @returns {number} The calculated percentage impact.
+ */
const calculatePriceImpact = (amount: string) => {
if (!amount || parseFloat(amount) <= 0) return 0;
const baseImpact = Math.min(parseFloat(amount) * 0.01, 15);
@@ -90,6 +121,9 @@ export default function SwapInterface() {
return baseImpact * tokenMultiplier;
};
+ /**
+ * Swaps the 'from' and 'to' tokens and their amounts.
+ */
const handleSwap = () => {
const temp = fromToken;
setFromToken(toToken);
@@ -98,12 +132,18 @@ export default function SwapInterface() {
setToAmount(fromAmount);
};
+ /**
+ * Updates the source amount and recalculates the destination amount and price impact.
+ *
+ * @param {string} value - The new input amount.
+ */
const handleFromAmountChange = (value: string) => {
setFromAmount(value);
const impact = calculatePriceImpact(value);
setPriceImpact(impact);
if (value && parseFloat(value) > 0) {
+ // Mock exchange rate logic
const mockRate = fromToken === "XLM" ? 0.15 : 6.67;
setToAmount((parseFloat(value) * mockRate * (1 - impact / 100)).toFixed(6));
} else {
@@ -111,59 +151,67 @@ export default function SwapInterface() {
}
};
+ /**
+ * Initiates the swap flow, validating inputs and checking for high slippage.
+ */
const handleSwapClick = async () => {
if (!fromAmount || parseFloat(fromAmount) <= 0) {
toast.error("Please enter an amount to swap");
return;
}
- const loadingToast = toast.loading("Processing swap...");
+ const loadingToast = toast.loading("Processing swap calculation...");
try {
+ // Threshold check for high slippage warning
if (priceImpact > 5) {
setIsHighSlippageWarningOpen(true);
toast.dismiss(loadingToast);
return;
}
- await new Promise((resolve) => setTimeout(resolve, 1800));
+ // Simulate network delay
+ await new Promise((resolve) => setTimeout(resolve, 1500));
- toast.success(`Swapped ${fromAmount} ${fromToken} → ${toAmount} ${toToken}`, {
+ toast.success(`Trade calculated: ${fromAmount} ${fromToken} → ${toAmount} ${toToken}`, {
id: loadingToast,
});
- if (priceImpact > 5) {
- setIsHighSlippageWarningOpen(true);
- } else {
- setIsTradeReviewOpen(true);
- }
+ setIsTradeReviewOpen(true);
} catch (error) {
- toast.error("Failed to process swap", {
+ toast.error("Failed to calculate trade parameters", {
id: loadingToast,
});
}
};
+ /**
+ * Confirms the trade and prepares the transaction for signing.
+ */
const handleTradeConfirm = async () => {
setIsTradeReviewOpen(false);
setIsSubmitting(true);
setSubmissionStartTime(Date.now());
try {
+ // Simulate transaction building time
await new Promise(resolve => setTimeout(resolve, 2000));
- // Generate mock transaction XDR
+ // Mock transaction XDR for demonstration
const mockTransactionXDR = "AAAAAK/eFzA7Jf5Xf3Xf3Xf3Xf3Xf3Xf3Xf3Xf3Xf3Xf3Xf3Xf3Xf3Xf3Xf3Xf3XAAAABQAAAAAAAAAAA==";
- console.log("Mock XDR generated:", mockTransactionXDR);
+ console.log("[SwapInterface] Mock XDR generated:", mockTransactionXDR);
setIsTransactionSignatureOpen(true);
} catch (error) {
- toast.error("Failed to submit trade");
+ toast.error("Failed to prepare transaction");
setIsSubmitting(false);
setSubmissionStartTime(null);
}
};
+ /**
+ * Handles confirmation from the high slippage warning modal.
+ */
const handleHighSlippageConfirm = async () => {
const loadingToast = toast.loading("Processing high slippage swap...");
@@ -180,21 +228,26 @@ export default function SwapInterface() {
}
};
- /* ISSUE #87: Trigger the success modal when the transaction is signed */
+ /**
+ * Callback for when the user successfully signs the transaction.
+ *
+ * @param {string} signedXDR - The base64 signed transaction XDR.
+ */
const handleTransactionSuccess = (signedXDR: string) => {
- console.log("Transaction signed:", signedXDR);
+ console.log("[SwapInterface] Transaction signed:", signedXDR);
- toast.success("Transaction signed successfully!", {
- icon: "✅",
+ toast.success("Trade executed successfully!", {
+ icon: "🚀",
});
setIsTransactionSignatureOpen(false);
setIsSubmitting(false);
setSubmissionStartTime(null);
- // Show the Growth/Share modal
+ // Show the post-trade share/growth modal
setIsSuccessModalOpen(true);
+ // Reset form after a short delay
setTimeout(() => {
setFromAmount("");
setToAmount("");
@@ -202,6 +255,7 @@ export default function SwapInterface() {
}, 1500);
};
+
const isAnyModalOpen = isSettingsOpen || isHighSlippageWarningOpen || isTradeReviewOpen || isSuccessModalOpen;
const isSwapValid = fromAmount && parseFloat(fromAmount) > 0 && !isSubmitting;
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
index 1166783..1703458 100644
--- a/src/components/ui/Button.tsx
+++ b/src/components/ui/Button.tsx
@@ -1,32 +1,69 @@
+/**
+ * Shared Button component for the TradeFlow UI.
+ * Provides consistent styling, variants, and built-in accessibility.
+ */
+
import React from "react";
+/**
+ * Props for the Button component.
+ * Extends standard HTML button attributes for full compatibility.
+ */
interface ButtonProps extends React.ButtonHTMLAttributes {
+ /** The content to be displayed inside the button */
children: React.ReactNode;
- variant?: "primary" | "secondary";
+ /** Visual style variant of the button */
+ variant?: "primary" | "secondary" | "outline" | "ghost";
+ /** Additional CSS classes for custom styling */
className?: string;
+ /** Optional loading state to disable button and show feedback */
+ isLoading?: boolean;
}
+/**
+ * A versatile button component that supports different visual styles and states.
+ */
export default function Button({
children,
variant = "primary",
className = "",
+ isLoading = false,
+ disabled,
...props
}: ButtonProps) {
- const baseStyles = "font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-800";
+ // Core styling for all button types
+ const baseStyles = "px-4 py-2 font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-tradeflow-dark disabled:opacity-50 disabled:cursor-not-allowed active:scale-[0.98]";
+ // Style definitions for each variant
const variantStyles = {
- primary: "bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500",
- secondary: "bg-slate-700 hover:bg-slate-600 text-white focus:ring-slate-500"
+ primary: "bg-tradeflow-accent hover:bg-tradeflow-accent/90 text-white focus:ring-tradeflow-accent",
+ secondary: "bg-slate-700 hover:bg-slate-600 text-white focus:ring-slate-500",
+ outline: "border-2 border-tradeflow-muted hover:border-tradeflow-accent hover:text-tradeflow-accent text-white",
+ ghost: "hover:bg-white/5 text-tradeflow-muted hover:text-white"
};
- const combinedClassName = `${baseStyles} ${variantStyles[variant]} ${className}`;
+ // Merge all styles into a single string
+ const combinedClassName = `${baseStyles} ${variantStyles[variant as keyof typeof variantStyles]} ${className}`;
return (
);
}
+
diff --git a/src/components/ui/NetworkFeeIndicator.tsx b/src/components/ui/NetworkFeeIndicator.tsx
index f0cc80c..ccbec33 100644
--- a/src/components/ui/NetworkFeeIndicator.tsx
+++ b/src/components/ui/NetworkFeeIndicator.tsx
@@ -1,21 +1,40 @@
+/**
+ * Network Fee Indicator Component.
+ * Fetches and displays the current Stellar network base fee (in stroops)
+ * with visual cues for congestion levels.
+ */
+
"use client";
import React, { useState, useEffect } from 'react';
import { Fuel } from 'lucide-react';
+/**
+ * Data structure for the network fee API response.
+ */
interface FeeData {
+ /** The base fee in stroops (1 XLM = 10,000,000 stroops) */
baseFee: number;
+ /** ISO timestamp of when the fee was last calculated */
lastUpdated: string;
}
+/**
+ * A component that monitors and displays real-time Stellar network fees.
+ * It polls the internal API every 15 seconds to stay up-to-date.
+ */
export default function NetworkFeeIndicator() {
const [feeData, setFeeData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
+ /**
+ * Fetches the latest network fee data from the internal API.
+ */
const fetchNetworkFee = async () => {
try {
setError(null);
+ // Fetch from the Next.js API route
const res = await fetch('/api/v1/network/fees');
if (!res.ok) {
@@ -33,61 +52,89 @@ export default function NetworkFeeIndicator() {
};
useEffect(() => {
+ // Initial fetch on mount
fetchNetworkFee();
- // Poll every 15 seconds
+ // Setup periodic polling (15 seconds is standard for network changes)
const interval = setInterval(fetchNetworkFee, 15000);
+ // Cleanup interval on component unmount
return () => clearInterval(interval);
}, []);
- // Determine color based on fee level (in stroops)
+ /**
+ * Returns a Tailwind color class based on the fee level.
+ *
+ * @param {number} baseFee - The fee in stroops.
+ * @returns {string} The CSS class for the text color.
+ */
const getFeeColor = (baseFee: number) => {
- if (baseFee < 150) return 'text-green-400'; // Cheap
- if (baseFee < 300) return 'text-yellow-400'; // Moderate
- return 'text-red-500'; // Expensive
+ if (baseFee < 150) return 'text-emerald-400'; // Cheap/Low congestion
+ if (baseFee < 300) return 'text-yellow-400'; // Moderate congestion
+ return 'text-red-500'; // High congestion
};
+ /**
+ * Returns a human-readable label for the congestion level.
+ *
+ * @param {number} baseFee - The fee in stroops.
+ * @returns {string} The status label.
+ */
const getFeeLabel = (baseFee: number) => {
- if (baseFee < 150) return 'Cheap';
+ if (baseFee < 150) return 'Optimized';
if (baseFee < 300) return 'Moderate';
- return 'Expensive';
+ return 'Congested';
};
+ // 1. Loading State UI
if (loading && !feeData) {
return (
-
+
Loading fee...
);
}
+ // 2. Error/Fallback State UI
if (error || !feeData) {
return (
-
+
Fee unavailable
);
}
+ // 3. Main Data Display
return (
-
+
-
+
{feeData.baseFee}
- stroops
+ stroops
-
+
{getFeeLabel(feeData.baseFee)}
);
-}
\ No newline at end of file
+}
diff --git a/src/lib/format.ts b/src/lib/format.ts
index 99f104a..a01b1f9 100644
--- a/src/lib/format.ts
+++ b/src/lib/format.ts
@@ -1,18 +1,28 @@
/**
- * Format currency values from blockchain (USDC with 7 decimals)
- * @param amount - Raw amount from blockchain (e.g., 10000000 for 1 USDC)
- * @returns Formatted USD string (e.g., "$1.00")
+ * Data formatting utilities for the TradeFlow application.
+ * Provides consistent formatting for currency, dates, addresses, and percentages.
+ */
+
+/**
+ * Formats raw blockchain currency values into a human-readable USD string.
+ * Specifically handles Stellar/Soroban assets with 7 decimal places (standard for USDC).
+ *
+ * @param {number | string} amount - The raw amount from the blockchain (e.g., 10,000,000 for 1.00 USDC).
+ * @returns {string} A localized currency string (e.g., "$1.00").
*/
export const formatCurrency = (amount: number | string): string => {
+ // 1. Convert string input to number if necessary
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
if (isNaN(numAmount)) {
return '$0.00';
}
- // USDC has 7 decimals, so divide by 10^7
+ // 2. Stellar assets like USDC typically use 7 decimal places
+ // We divide by 10^7 to get the actual unit value
const usdcAmount = numAmount / 10000000;
+ // 3. Format using standard US locale settings
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
@@ -22,28 +32,52 @@ export const formatCurrency = (amount: number | string): string => {
};
/**
- * Format Unix timestamp to human-readable date
- * @param timestamp - Unix timestamp (seconds or milliseconds)
- * @returns Formatted date string (e.g., "Oct 24, 2024")
+ * Formats a generic token amount with a specified number of decimals.
+ *
+ * @param {number | string} amount - The raw token amount.
+ * @param {number} [decimals=7] - Number of decimal places for the asset.
+ * @returns {string} The formatted number string.
+ */
+export const formatTokenAmount = (amount: number | string, decimals: number = 7): string => {
+ const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
+ if (isNaN(numAmount)) return "0";
+
+ const value = numAmount / Math.pow(10, decimals);
+ return value.toLocaleString('en-US', {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: decimals,
+ });
+};
+
+/**
+ * Formats a Unix timestamp into a human-readable date string.
+ * Supports both seconds (10 digits) and milliseconds (13 digits) formats.
+ *
+ * @param {number | string} timestamp - The Unix timestamp to format.
+ * @returns {string} Formatted date (e.g., "Oct 24, 2024") or "Invalid Date".
*/
export const formatDate = (timestamp: number | string): string => {
+ // 1. Ensure we have a numeric value
const numTimestamp = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
if (isNaN(numTimestamp)) {
return 'Invalid Date';
}
- // Handle both seconds and milliseconds timestamps
+ // 2. Detect format based on digit count
+ // Unix seconds = 10 digits, Milliseconds = 13 digits
const date = new Date(
numTimestamp.toString().length === 10
? numTimestamp * 1000 // Convert seconds to milliseconds
: numTimestamp // Already in milliseconds
);
+ // 3. Check for invalid date objects
if (isNaN(date.getTime())) {
return 'Invalid Date';
}
+ // 4. Localized short date format
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
@@ -52,9 +86,11 @@ export const formatDate = (timestamp: number | string): string => {
};
/**
- * Format full date with time
- * @param timestamp - Unix timestamp (seconds or milliseconds)
- * @returns Formatted date-time string (e.g., "Oct 24, 2024, 2:30 PM")
+ * Formats a Unix timestamp into a detailed date and time string.
+ * Useful for transaction history or specific event logging.
+ *
+ * @param {number | string} timestamp - The Unix timestamp to format.
+ * @returns {string} Formatted date-time (e.g., "Oct 24, 2024, 2:30 PM").
*/
export const formatDateTime = (timestamp: number | string): string => {
const numTimestamp = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
@@ -84,12 +120,14 @@ export const formatDateTime = (timestamp: number | string): string => {
};
/**
- * Format wallet address for display
- * @param address - Full wallet address
- * @returns Shortened address (e.g., "0x1234...5678")
+ * Shortens a Stellar or Ethereum wallet address for UI display.
+ * Shows the first 6 and last 4 characters by default.
+ *
+ * @param {string} address - The full wallet address string.
+ * @returns {string} The truncated address (e.g., "GBBD67...OC6S").
*/
export const formatAddress = (address: string): string => {
- if (!address || address.length < 8) {
+ if (!address || address.length < 12) {
return address;
}
@@ -97,10 +135,11 @@ export const formatAddress = (address: string): string => {
};
/**
- * Format percentage with proper decimal places
- * @param value - Percentage value (0-100)
- * @param decimals - Number of decimal places (default: 1)
- * @returns Formatted percentage string
+ * Formats a numeric value as a percentage string.
+ *
+ * @param {number | string} value - The raw percentage value (e.g., 5.5 for 5.5%).
+ * @param {number} [decimals=1] - Precision of the output.
+ * @returns {string} Formatted percentage (e.g., "5.5%").
*/
export const formatPercentage = (value: number | string, decimals: number = 1): string => {
const numValue = typeof value === 'string' ? parseFloat(value) : value;
@@ -111,3 +150,4 @@ export const formatPercentage = (value: number | string, decimals: number = 1):
return `${numValue.toFixed(decimals)}%`;
};
+
diff --git a/src/lib/parser.ts b/src/lib/parser.ts
index a1b667e..ae53af0 100644
--- a/src/lib/parser.ts
+++ b/src/lib/parser.ts
@@ -1,47 +1,53 @@
+/**
+ * XDR Parsing and Serialization Utilities.
+ * Handles conversion between Stellar's External Data Representation (XDR)
+ * and native JavaScript objects for the TradeFlow smart contracts.
+ */
+
import { xdr, scValToNative } from "soroban-client";
+/**
+ * Structured data representation of a TradeFlow Invoice.
+ */
export interface Invoice {
+ /** Unique numeric identifier for the invoice */
id: number;
+ /** Public Stellar address of the invoice owner/creator */
owner: string;
+ /** The total amount of the invoice (raw units) */
amount: number;
}
/**
- * Parses a Base64-encoded XDR string returned by a Soroban smart contract call (e.g., get_invoice)
- * and converts it into a structured Invoice object.
+ * Parses a Base64-encoded XDR string returned by a Soroban smart contract call
+ * (specifically the 'get_invoice' method) and converts it into a structured Invoice object.
*
- * @param xdrBase64 - The Base64-encoded XDR string.
- * @returns The parsed Invoice object.
- * @throws Error if the XDR is invalid or the data structure does not match the expected Invoice schema.
+ * @param {string} xdrBase64 - The Base64-encoded ScVal XDR string from the network.
+ * @returns {Invoice} The parsed and validated Invoice object.
+ * @throws {Error} If the XDR is malformed or the data structure is invalid.
*/
export function parseInvoiceFromXdr(xdrBase64: string): Invoice {
+ // 1. Basic validation of input string
if (!xdrBase64 || typeof xdrBase64 !== 'string') {
throw new Error("Invalid input: xdrBase64 must be a non-empty string.");
}
try {
- // 1. Decode the XDR string to an ScVal
- // Note: We use 'base64' encoding as standard for Soroban return values
+ // 2. Decode the XDR string to a Soroban ScVal (Smart Contract Value)
const value = xdr.ScVal.fromXDR(xdrBase64, 'base64');
- // 2. Convert ScVal to a native JavaScript object
- // scValToNative handles BigInt conversion automatically (returns bigint for u64/i64/u128/i128)
+ // 3. Convert ScVal to a native JavaScript object
+ // scValToNative handles basic types and recursively converts complex types (Maps, Structs, Vecs)
const nativeValue = scValToNative(value);
if (!nativeValue || typeof nativeValue !== 'object') {
throw new Error(`Parsed XDR result is not an object or Map. Got: ${typeof nativeValue}`);
}
- // 3. Normalize the native value to a plain object (handle Map vs Object)
+ // 4. Normalize the native value (handle different SDK return formats)
let invoiceData: Record = {};
- // Check if it's a Map (common in newer SDKs for ScMap)
- // We check for .get method to be safe, or just check instanceof Map if environment supports it
- // But usually scValToNative returns plain objects for Structs in some versions, or objects with properties.
- // However, if it returns an array of entries or a Map, we need to handle it.
- // Let's assume it returns an object with keys matching struct fields.
- // If it is a Map, we convert it.
- /* eslint-disable @typescript-eslint/no-explicit-any */
+ // In some SDK versions, ScMap/Struct returns a native Map
if (nativeValue instanceof Map) {
nativeValue.forEach((val: any, key: any) => {
invoiceData[String(key)] = val;
@@ -50,26 +56,28 @@ export function parseInvoiceFromXdr(xdrBase64: string): Invoice {
invoiceData = nativeValue as Record;
}
- // 4. Validate and transform into strictly-typed Invoice
+ // 5. Transform and strictly validate fields
const result: Invoice = {
id: 0,
owner: '',
amount: 0
};
- // Helper to safely convert to Number
+ /**
+ * Safely converts various numeric types (number, bigint, string) to a standard number.
+ */
const safelyConvertToNumber = (val: any, fieldName: string): number => {
if (typeof val === 'number') {
return val;
}
if (typeof val === 'bigint') {
+ // Log warning if bigint exceeds JavaScript's safe integer range
if (val > BigInt(Number.MAX_SAFE_INTEGER) || val < BigInt(Number.MIN_SAFE_INTEGER)) {
- console.warn(`[TradeFlow] Warning: Value for '${fieldName}' (${val}) exceeds Number.MAX_SAFE_INTEGER. Precision lost.`);
+ console.warn(`[TradeFlow] Precision warning: Value for '${fieldName}' (${val}) exceeds Number.MAX_SAFE_INTEGER.`);
}
return Number(val);
}
if (typeof val === 'string') {
- // Try parsing string to number
const num = Number(val);
if (isNaN(num)) {
throw new Error(`Invalid number format for field '${fieldName}': ${val}`);
@@ -79,23 +87,26 @@ export function parseInvoiceFromXdr(xdrBase64: string): Invoice {
throw new Error(`Invalid type for field '${fieldName}': expected number or bigint, got ${typeof val}`);
};
- // Helper to safely convert to String
+ /**
+ * Ensures a value is treated as a string, handling Address objects if necessary.
+ */
const safelyConvertToString = (val: any, fieldName: string): string => {
if (typeof val === 'string') {
return val;
}
- // Handle Address object if scValToNative returns an Address class
+ // Soroban Address types might return an object with a toString() method
if (val && typeof val.toString === 'function') {
return val.toString();
}
throw new Error(`Invalid type for field '${fieldName}': expected string, got ${typeof val}`);
};
- // Validate fields
+ // Verify presence of all required struct fields
if (!('id' in invoiceData)) throw new Error("Missing required field: 'id'");
if (!('owner' in invoiceData)) throw new Error("Missing required field: 'owner'");
if (!('amount' in invoiceData)) throw new Error("Missing required field: 'amount'");
+ // Perform final assignments with type safety
result.id = safelyConvertToNumber(invoiceData.id, 'id');
result.owner = safelyConvertToString(invoiceData.owner, 'owner');
result.amount = safelyConvertToNumber(invoiceData.amount, 'amount');
@@ -103,18 +114,21 @@ export function parseInvoiceFromXdr(xdrBase64: string): Invoice {
return result;
} catch (error: any) {
- // Wrap error with context
- // Check if it's our own error or from library
- if (error.message && error.message.startsWith('Invalid input')) throw error;
- if (error.message && error.message.startsWith('Missing required')) throw error;
+ // Propagate validation errors directly, wrap others with context
+ if (error.message && (error.message.startsWith('Invalid input') || error.message.startsWith('Missing required'))) {
+ throw error;
+ }
throw new Error(`Failed to parse Invoice from XDR: ${error.message}`);
}
}
/**
- * Helper to safely stringify objects containing BigInts for logging or API responses.
- * JSON.stringify throws on BigInt by default.
+ * A safe alternative to JSON.stringify that handles BigInt values.
+ * BigInt is commonly used in Stellar SDKs for large numeric values.
+ *
+ * @param {any} obj - The object to stringify.
+ * @returns {string} The JSON string representation.
*/
export function safeJsonStringify(obj: any): string {
return JSON.stringify(obj, (key, value) =>
@@ -123,3 +137,4 @@ export function safeJsonStringify(obj: any): string {
: value
);
}
+
diff --git a/src/lib/stellar.ts b/src/lib/stellar.ts
index 96d3717..738c338 100644
--- a/src/lib/stellar.ts
+++ b/src/lib/stellar.ts
@@ -1,3 +1,8 @@
+/**
+ * Stellar integration utilities for TradeFlow.
+ * This module handles wallet connections, transaction building, and network interactions.
+ */
+
import {
walletKit,
FREIGHTER_ID,
@@ -13,44 +18,66 @@ import {
Networks
} from "soroban-client";
-// Default to Testnet for development
+/**
+ * The RPC endpoint for the Stellar network.
+ * Currently defaults to the public Soroban Testnet.
+ */
const RPC_URL = "https://soroban-testnet.stellar.org";
+
+/**
+ * Internal server instance for interacting with the Stellar network.
+ */
const server = new Server(RPC_URL);
+/**
+ * The network passphrase used for transaction signing.
+ * Must match the network the transaction is being submitted to.
+ */
const NETWORK_PASSPHRASE = Networks.TESTNET;
-// Initialize wallet kit
+/**
+ * Initialize the wallet kit with a default wallet (Freighter).
+ */
walletKit.setWallet(FREIGHTER_ID);
+/**
+ * Represents the information returned after a successful wallet connection.
+ */
export interface WalletInfo {
+ /** The public G-address of the connected Stellar account */
publicKey: string;
+ /** The type of wallet used for connection (e.g., freighter, xbull) */
walletType: WalletType;
}
/**
* Connects to a Stellar wallet using the wallet kit.
- * Supports Freighter, Albedo, and xBull wallets.
+ * Supports multiple providers including Freighter, Albedo, and xBull.
+ *
+ * @param {WalletType} walletType - The ID of the wallet provider to use.
+ * @returns {Promise} A promise that resolves to the wallet information.
+ * @throws {Error} If the wallet is not installed or the wrong network is selected.
*/
export async function connectWallet(walletType: WalletType = FREIGHTER_ID): Promise {
try {
- // Set the wallet type
+ // 1. Configure the kit to use the requested wallet provider
walletKit.setWallet(walletType);
- // Check if wallet is available
+ // 2. Ensure the extension or provider is available in the browser
const isWalletAvailable = await walletKit.isWalletAvailable();
if (!isWalletAvailable) {
const walletName = getWalletName(walletType);
throw new Error(`${walletName} is not available. Please install it to continue.`);
}
- // Get public key
+ // 3. Request the public key from the user
const publicKey = await walletKit.getPublicKey();
if (!publicKey) {
throw new Error("Unable to retrieve public key.");
}
- // Verify correct network (Testnet)
+ // 4. Validate that the user is on the expected network (Testnet)
const network = await walletKit.getNetwork();
if (network !== "TESTNET") {
const walletName = getWalletName(walletType);
@@ -59,13 +86,16 @@ export async function connectWallet(walletType: WalletType = FREIGHTER_ID): Prom
return { publicKey, walletType };
} catch (error: any) {
+ // Log error for debugging but propagate to the caller
console.error("Wallet connection error:", error);
throw error;
}
}
/**
- * Gets the current connected wallet info
+ * Retrieves information about the currently connected wallet if available.
+ *
+ * @returns {Promise} The wallet info or null if disconnected.
*/
export async function getConnectedWallet(): Promise {
try {
@@ -75,12 +105,15 @@ export async function getConnectedWallet(): Promise {
const currentWallet = walletKit.getWallet();
return { publicKey, walletType: currentWallet };
} catch (error) {
+ // Silently return null on error as it usually means no wallet is active
return null;
}
}
/**
- * Disconnects the current wallet
+ * Terminates the current wallet session.
+ *
+ * @returns {Promise}
*/
export async function disconnectWallet(): Promise {
try {
@@ -91,7 +124,10 @@ export async function disconnectWallet(): Promise {
}
/**
- * Gets the display name for a wallet type
+ * Maps internal wallet IDs to human-readable display names.
+ *
+ * @param {WalletType} walletType - The internal ID of the wallet.
+ * @returns {string} The display name (e.g., "Freighter").
*/
function getWalletName(walletType: WalletType): string {
switch (walletType) {
@@ -108,21 +144,22 @@ function getWalletName(walletType: WalletType): string {
/**
* Monitors the status of a Stellar transaction until it succeeds, fails, or times out.
- * Polls the network every 2 seconds.
+ * This function uses a polling mechanism to check the Horizon server.
*
- * @param hash - The transaction hash to monitor
- * @returns Promise that resolves to "SUCCESS" if successful
+ * @param {string} hash - The transaction hash to monitor.
+ * @returns {Promise} Promise that resolves to "SUCCESS" if successful.
+ * @throws {Error} If the transaction fails or the polling times out.
*/
export async function waitForTransaction(hash: string): Promise {
- const TIMEOUT_MS = 30000;
- const POLLING_INTERVAL_MS = 2000;
+ const TIMEOUT_MS = 30000; // 30 seconds timeout
+ const POLLING_INTERVAL_MS = 2000; // Poll every 2 seconds
const startTime = Date.now();
console.log(`[waitForTransaction] Starting monitoring for transaction: ${hash}`);
while (Date.now() - startTime < TIMEOUT_MS) {
try {
- // Attempt to fetch transaction status
+ // Fetch transaction status from the Horizon server
const tx = await server.getTransaction(hash);
console.log(`[waitForTransaction] Polling ${hash}: Status = ${tx.status}`);
@@ -137,57 +174,61 @@ export async function waitForTransaction(hash: string): Promise {
// If status is NOT_FOUND or other pending states, continue polling
} catch (error: any) {
- // Log error but continue polling (common for 404 Not Found initially)
+ // Log warning but continue polling (common for 404 Not Found initially)
console.warn(`[waitForTransaction] Polling attempt failed (retrying): ${error.message}`);
}
- // Wait before next poll
+ // Wait before next poll attempt
await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVAL_MS));
}
- // Timeout reached
+ // Timeout reached if we exit the loop
const errorMsg = `Transaction monitoring timed out after ${TIMEOUT_MS / 1000}s for hash: ${hash}`;
console.error(`[waitForTransaction] ${errorMsg}`);
throw new Error(errorMsg);
}
/**
- * Adds a trustline for a Stellar asset (ChangeTrust operation).
- * @param assetCode - Code of the asset (e.g., "USDC")
- * @param assetIssuer - Issuer address of the asset
- * @param walletType - Optional wallet type override
+ * Establishes a trustline for a specific Stellar asset.
+ * This is required before an account can hold or receive a non-native asset.
+ *
+ * @param {string} assetCode - The code of the asset (e.g., "USDC").
+ * @param {string} assetIssuer - The public G-address of the asset issuer.
+ * @param {WalletType} [walletType] - Optional wallet provider override.
+ * @returns {Promise} The status of the transaction.
*/
export async function addTrustline(assetCode: string, assetIssuer: string, walletType?: WalletType) {
- // If walletType is provided, set it temporarily
+ // Update wallet provider if specified
if (walletType) {
walletKit.setWallet(walletType);
}
const publicKey = await walletKit.getPublicKey();
- // Fetch account details to get the current sequence number
+ // 1. Fetch current account state for sequence number
const account = await server.getAccount(publicKey);
const asset = new Asset(assetCode, assetIssuer);
- // Construct the transaction
+ // 2. Build the ChangeTrust transaction
const transaction = new TransactionBuilder(account, {
- fee: "1000", // Standard fee in stroops
+ fee: "1000", // Fixed fee of 1000 stroops for demo
networkPassphrase: NETWORK_PASSPHRASE,
})
.addOperation(Operation.changeTrust({ asset }))
- .setTimeout(60)
+ .setTimeout(60) // 60 seconds transaction validity
.build();
- // Request signature from the current wallet
+ // 3. Request user signature via the selected wallet
const xdr = transaction.toXDR();
const signedXdr = await walletKit.signTransaction(xdr, {
network: "TESTNET",
});
- // Submit to the network
+ // 4. Submit the signed transaction to the network
const response = await server.sendTransaction(transaction);
if (response.hash) {
+ // 5. Wait for ledger confirmation
return await waitForTransaction(response.hash);
}
@@ -195,7 +236,11 @@ export async function addTrustline(assetCode: string, assetIssuer: string, walle
}
/**
- * Signs a transaction using the currently connected wallet
+ * Signs a raw XDR transaction using the active wallet.
+ *
+ * @param {string} xdr - The base64 encoded transaction XDR.
+ * @param {any} [options] - Additional signing options.
+ * @returns {Promise} The signed transaction XDR.
*/
export async function signTransaction(xdr: string, options?: any): Promise {
return await walletKit.signTransaction(xdr, {
@@ -205,14 +250,18 @@ export async function signTransaction(xdr: string, options?: any): Promise} The network identifier (e.g., "TESTNET").
*/
export async function getNetwork(): Promise {
return await walletKit.getNetwork();
}
/**
- * Checks if a wallet is connected
+ * Quick check to see if a wallet is currently active and reachable.
+ *
+ * @returns {Promise} True if a public key can be retrieved.
*/
export async function isWalletConnected(): Promise {
try {
@@ -223,7 +272,20 @@ export async function isWalletConnected(): Promise {
}
}
-// Export wallet types and kit for use in other modules
+/**
+ * Utility to shorten a Stellar address for UI display.
+ *
+ * @param {string} address - The full Stellar address.
+ * @param {number} [chars=4] - Number of characters to show at start and end.
+ * @returns {string} The shortened address (e.g., GABC...XYZ).
+ */
+export function shortenAddress(address: string, chars = 4): string {
+ if (!address) return "";
+ if (address.length <= chars * 2) return address;
+ return `${address.substring(0, chars)}...${address.substring(address.length - chars)}`;
+}
+
+// Re-export constants and types for downstream usage
export {
walletKit,
FREIGHTER_ID,
@@ -231,3 +293,4 @@ export {
ALBEDO_ID,
WalletType
};
+
diff --git a/src/stores/tokenStore.ts b/src/stores/tokenStore.ts
index 2b3f39a..8ab6664 100644
--- a/src/stores/tokenStore.ts
+++ b/src/stores/tokenStore.ts
@@ -1,53 +1,100 @@
+/**
+ * Token Management Store.
+ * Handles tracking of the native TradeFlow (TF) utility token,
+ * including balances, pro-mode access levels, and connection status.
+ */
+
import { create } from 'zustand';
import { Server, Asset } from 'soroban-client';
-// TF Token configuration (would be actual token details in production)
+/**
+ * TradeFlow Token (TF) Configuration.
+ * These constants define the utility token used for premium features.
+ */
const TF_TOKEN_CODE = 'TF';
-const TF_TOKEN_ISSUER = 'GBBHPLX4LBHS5JPC4FBDHD4YDZSZJZG7VQMIY6RDZT6HRJ5QJ5N6KFGH'; // Example issuer
-const PRO_MODE_THRESHOLD = 1000;
+const TF_TOKEN_ISSUER = 'GBBHPLX4LBHS5JPC4FBDHD4YDZSZJZG7VQMIY6RDZT6HRJ5QJ5N6KFGH'; // Example issuer address
+const PRO_MODE_THRESHOLD = 1000; // Minimum TF tokens required for Pro Mode
+/**
+ * Representation of a Stellar asset balance.
+ */
interface TokenBalance {
+ /** The 1-12 character asset code */
code: string;
+ /** The public address of the asset issuer */
issuer: string;
+ /** The current balance as a string (to maintain precision) */
balance: string;
}
+/**
+ * State and actions for the Token Store.
+ */
interface TokenStore {
+ /** Current balance of TF tokens for the connected user */
tfTokenBalance: number;
+ /** Whether a wallet is currently connected to the store */
isConnected: boolean;
+ /** The public address of the connected wallet */
publicKey: string | null;
+ /** Loading state for asynchronous balance fetching */
isLoading: boolean;
+ /** Error message if a balance fetch fails */
error: string | null;
+
+ /**
+ * Fetches the TF token balance for a specific public key from the network.
+ * @param {string} publicKey - The Stellar address to query.
+ */
fetchTokenBalance: (publicKey: string) => Promise;
+
+ /**
+ * Updates the connection status and triggers a balance refresh if connecting.
+ * @param {boolean} connected - The new connection state.
+ * @param {string} [publicKey] - The address of the connecting wallet.
+ */
setConnected: (connected: boolean, publicKey?: string) => void;
+
+ /**
+ * Evaluates if the current user has enough TF tokens to access Pro Mode.
+ * @returns {boolean} True if the threshold is met.
+ */
hasProModeAccess: () => boolean;
}
+/**
+ * Zustand store for managing TradeFlow token state.
+ */
export const useTokenStore = create((set, get) => ({
+ // Initial state values
tfTokenBalance: 0,
isConnected: false,
publicKey: null,
isLoading: false,
error: null,
+ /**
+ * Asynchronously retrieves the TF token balance from the Horizon server.
+ */
fetchTokenBalance: async (publicKey: string) => {
set({ isLoading: true, error: null });
try {
- // In production, this would fetch from Stellar network
- // For now, we'll simulate with a mock balance
+ // Connect to the Soroban Testnet Horizon server
const server = new Server('https://soroban-testnet.stellar.org');
try {
+ // 1. Retrieve the account details
const account = await server.getAccount(publicKey);
const tfAsset = new Asset(TF_TOKEN_CODE, TF_TOKEN_ISSUER);
- // Look for TF token balance in account balances
+ // 2. Locate the TF token in the account's balances array
const tfBalance = account.balances.find((balance: any) =>
balance.asset_code === TF_TOKEN_CODE &&
balance.asset_issuer === TF_TOKEN_ISSUER
);
+ // 3. Parse and update the balance state
const balance = tfBalance ? parseFloat(tfBalance.balance) : 0;
set({
@@ -55,7 +102,7 @@ export const useTokenStore = create((set, get) => ({
isLoading: false
});
} catch (error) {
- // Account not found or other error - set balance to 0
+ // Fallback: If account is not found or has no trustline, balance is 0
set({
tfTokenBalance: 0,
isLoading: false,
@@ -63,15 +110,18 @@ export const useTokenStore = create((set, get) => ({
});
}
} catch (error) {
- console.error('Error fetching token balance:', error);
+ console.error('[TokenStore] Critical error fetching balance:', error);
set({
tfTokenBalance: 0,
isLoading: false,
- error: 'Failed to fetch token balance'
+ error: 'Failed to connect to network'
});
}
},
+ /**
+ * Updates the global connection state and manages balance refresh.
+ */
setConnected: (connected: boolean, publicKey?: string) => {
set({
isConnected: connected,
@@ -79,24 +129,33 @@ export const useTokenStore = create((set, get) => ({
error: null
});
+ // Auto-fetch balance on successful connection
if (connected && publicKey) {
get().fetchTokenBalance(publicKey);
} else {
+ // Clear balance on disconnect
set({ tfTokenBalance: 0 });
}
},
+ /**
+ * Logic for determining premium access.
+ */
hasProModeAccess: () => {
const { tfTokenBalance, isConnected } = get();
return isConnected && tfTokenBalance >= PRO_MODE_THRESHOLD;
}
}));
-// Helper constants for components
+/**
+ * Re-export constants for use in UI components.
+ */
export const PRO_MODE_THRESHOLD_AMOUNT = PRO_MODE_THRESHOLD;
export const TF_TOKEN_INFO = {
code: TF_TOKEN_CODE,
issuer: TF_TOKEN_ISSUER,
name: 'TradeFlow Token',
+ description: 'Utility token for the TradeFlow ecosystem.',
symbol: 'TF'
};
+
diff --git a/src/stores/useWeb3Store.ts b/src/stores/useWeb3Store.ts
index a0162a7..80d871d 100644
--- a/src/stores/useWeb3Store.ts
+++ b/src/stores/useWeb3Store.ts
@@ -1,13 +1,24 @@
+/**
+ * Web3 State Management Store.
+ * Centralizes wallet connection, network status, and account balances
+ * using Zustand for reactive state updates across the application.
+ */
+
import { create } from 'zustand';
import { Server, Asset } from 'soroban-client';
import { walletKit, FREIGHTER_ID, WalletType } from '../lib/stellar';
-// Network configuration
+/**
+ * Supported Stellar Networks.
+ */
export const NETWORKS = {
+ /** Public Stellar Testnet (Used for development and QA) */
TESTNET: 'Testnet',
+ /** Public Stellar Mainnet (Used for production assets) */
MAINNET: 'Mainnet'
} as const;
+
export type NetworkType = typeof NETWORKS[keyof typeof NETWORKS];
// Stellar network endpoints
@@ -16,41 +27,78 @@ const NETWORK_ENDPOINTS = {
[NETWORKS.MAINNET]: 'https://horizon.stellar.org'
};
+/**
+ * Internal state for the Web3 store.
+ */
interface Web3State {
- // Wallet connection state
+ /** The public address of the connected wallet, or null if disconnected */
walletAddress: string | null;
+ /** The ID of the connected wallet provider (e.g., freighter) */
walletType: WalletType | null;
+ /** True if a wallet is successfully connected and reachable */
isConnected: boolean;
+ /** True during the asynchronous wallet connection process */
isConnecting: boolean;
- // Network state
+ /** The currently selected network (defaults to TESTNET) */
network: NetworkType;
- // Token balances
+ /** Dictionary of asset balances, keyed by asset code (e.g., { "XLM": 50.5 }) */
balances: Record;
- // Loading and error states
+ /** Global loading state for network-bound operations */
isLoading: boolean;
+ /** Stores the last encountered error message, if any */
error: string | null;
}
+
+/**
+ * Available actions for interacting with the Web3 store.
+ */
interface Web3Actions {
- // Wallet actions
+ /**
+ * Initiates a connection request to a Stellar wallet.
+ * @param {WalletType} [walletType] - The specific wallet provider to use.
+ */
connectWallet: (walletType?: WalletType) => Promise;
+
+ /**
+ * Clears the current wallet session and resets relevant state.
+ */
disconnectWallet: () => void;
- // Network actions
+ /**
+ * Updates the store to point to a different Stellar network.
+ * @param {NetworkType} network - The target network to switch to.
+ */
switchNetwork: (network: NetworkType) => Promise;
- // Balance actions
+ /**
+ * Fetches the latest balances for all assets held by the connected account.
+ */
updateBalances: () => Promise;
+
+ /**
+ * Manually updates the balance for a specific token in the store.
+ * @param {string} tokenCode - The code of the asset.
+ * @param {number} balance - The new numeric balance.
+ */
updateTokenBalance: (tokenCode: string, balance: number) => void;
- // Utility actions
+ /**
+ * Resets the error state in the store.
+ */
clearError: () => void;
+
+ /**
+ * Manually toggles the global loading state.
+ * @param {boolean} loading - The new loading state.
+ */
setLoading: (loading: boolean) => void;
}
+
type Web3Store = Web3State & Web3Actions;
export const useWeb3Store = create((set, get) => ({
From f42b5cdeb512435afc977453107bf467cf544600 Mon Sep 17 00:00:00 2001
From: Muhammad Zayyad Mukhtar
<95658387+El-swaggerito@users.noreply.github.com>
Date: Wed, 29 Apr 2026 11:52:09 +0100
Subject: [PATCH 2/2] refactor(components): enhance UI components with improved
styling and accessibility
- Add comprehensive JSDoc comments and TypeScript interfaces for better maintainability
- Implement consistent design system with updated colors, spacing, and interactive states
- Improve accessibility with ARIA labels, keyboard navigation, and semantic HTML
- Add hover effects, transitions, and responsive design patterns
- Enhance error handling and user feedback mechanisms
- Update form validation with better user experience
---
src/components/Card.tsx | 45 ++++++++-
src/components/InvoiceMintForm.tsx | 157 +++++++++++++++++++----------
src/components/LoanTable.tsx | 152 ++++++++++++++++++----------
src/components/Navbar.tsx | 119 +++++++++++++++-------
src/components/layout/Footer.tsx | 139 ++++++++++++++++++-------
src/hooks/useWatchlist.ts | 60 +++++++++--
6 files changed, 475 insertions(+), 197 deletions(-)
diff --git a/src/components/Card.tsx b/src/components/Card.tsx
index 0e5614e..9990d6e 100644
--- a/src/components/Card.tsx
+++ b/src/components/Card.tsx
@@ -1,14 +1,53 @@
+/**
+ * Shared Card Component.
+ * Provides a consistent container for UI sections with standard
+ * TradeFlow styling (borders, padding, and background).
+ */
+
import React from "react";
+/**
+ * Props for the Card component.
+ */
interface CardProps {
+ /** The content to be wrapped by the card */
children: React.ReactNode;
+ /** Additional CSS classes for custom styling overrides */
className?: string;
+ /** Optional click handler for interactive cards */
+ onClick?: () => void;
+ /** Optional hover effect toggle */
+ hoverable?: boolean;
}
-export default function Card({ children, className = "" }: CardProps) {
+/**
+ * A versatile layout component for grouping related content.
+ */
+export default function Card({
+ children,
+ className = "",
+ onClick,
+ hoverable = false
+}: CardProps) {
+ // --- Styling ---
+ const baseStyles = "bg-slate-800 border border-slate-700 rounded-3xl p-6 transition-all duration-300 shadow-xl shadow-black/5";
+ const hoverStyles = hoverable ? "hover:border-slate-600 hover:bg-slate-800/80 hover:translate-y-[-2px]" : "";
+ const interactiveStyles = onClick ? "cursor-pointer active:scale-[0.99]" : "";
+
return (
-