diff --git a/src/app/api/pnl/route.ts b/src/app/api/pnl/route.ts index f1cd551..d2a84d9 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,6 +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 bd3c54c..e32a022 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -1,8 +1,29 @@ +/** + * 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 f0864e5..db906f5 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, useRef } from "react"; @@ -29,15 +35,24 @@ import { useWalletConnection } from "../stores/useWeb3Store"; import { showError, showSuccess } from "../lib/toast"; import Icon from "../components/ui/Icon"; +/** + * The root component for the TradeFlow dashboard. + * Manages high-level state for wallet connection, active tabs, and invoice data. + */ export default function Page() { const router = useRouter(); const searchParams = useSearchParams(); const { isConnected, walletAddress, isConnecting } = useWalletConnection(); const [invoices, setInvoices] = useState([]); 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(); const riskSocketRef = useRef(null); @@ -77,6 +92,8 @@ export default function Page() { } }; + // --- Lifecycle Hooks --- + useEffect(() => { const controller = new AbortController(); @@ -147,14 +164,18 @@ export default function Page() { const handleInvoiceMint = (data: Record) => { console.log("Invoice data 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: }, ]; + return (
{/* Header */} diff --git a/src/components/SwapInterface.tsx b/src/components/SwapInterface.tsx index de6d9ec..4769d92 100644 --- a/src/components/SwapInterface.tsx +++ b/src/components/SwapInterface.tsx @@ -7,14 +7,25 @@ import { dismissToast, showError, showLoading, showSuccess } from "../lib/toast" import { useSigningActions } from "../stores/signatureStore"; import Icon from "./ui/Icon"; +/** + * 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 [isProMode, setIsProMode] = useState(false); const { deadline } = useSettings(); + /** + * Swaps the 'from' and 'to' tokens and their amounts. + */ const handleSwap = () => { const temp = fromToken; setFromToken(toToken); @@ -23,12 +34,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 { @@ -36,6 +53,9 @@ export default function SwapInterface() { } }; + /** + * Initiates the swap flow, validating inputs and checking for high slippage. + */ const handleSwapClick = async () => { if (!fromAmount || parseFloat(fromAmount) <= 0) { showError("Please enter an amount to swap"); @@ -45,23 +65,21 @@ export default function SwapInterface() { const loadingToast = showLoading("Processing swap..."); try { + // Threshold check for high slippage warning if (priceImpact > 5) { setIsHighSlippageWarningOpen(true); dismissToast(loadingToast); return; } - await new Promise((resolve) => setTimeout(resolve, 1800)); + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 1500)); showSuccess(`Swapped ${fromAmount} ${fromToken} → ${toAmount} ${toToken}`, { id: loadingToast, }); - if (priceImpact > 5) { - setIsHighSlippageWarningOpen(true); - } else { - setIsTradeReviewOpen(true); - } + setIsTradeReviewOpen(true); } catch (error) { showError("Failed to process swap", { id: loadingToast, @@ -69,17 +87,21 @@ export default function SwapInterface() { } }; + /** + * 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) { @@ -89,6 +111,9 @@ export default function SwapInterface() { } }; + /** + * Handles confirmation from the high slippage warning modal. + */ const handleHighSlippageConfirm = async () => { const loadingToast = showLoading("Processing high slippage swap..."); @@ -105,9 +130,13 @@ 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); showSuccess("Transaction signed successfully!", { icon: "✅", @@ -117,9 +146,10 @@ export default function SwapInterface() { 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(""); @@ -127,6 +157,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/NetworkFeeIndicator.tsx b/src/components/ui/NetworkFeeIndicator.tsx index 730fa3d..e90cead 100644 --- a/src/components/ui/NetworkFeeIndicator.tsx +++ b/src/components/ui/NetworkFeeIndicator.tsx @@ -1,22 +1,41 @@ +/** + * 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'; import Icon from './Icon'; +/** + * 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) { @@ -34,27 +53,41 @@ 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 (
@@ -64,6 +97,7 @@ export default function NetworkFeeIndicator() { ); } + // 2. Error/Fallback State UI if (error || !feeData) { return (
@@ -73,6 +107,7 @@ export default function NetworkFeeIndicator() { ); } + // 3. Main Data Display return (
- + {feeData.baseFee} - stroops + stroops
-
+
{getFeeLabel(feeData.baseFee)}
diff --git a/src/lib/format.ts b/src/lib/format.ts index d15b1ca..42b2d43 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -15,6 +15,7 @@ export const formatCurrency = (amount: number | string, isRaw: boolean = true): // Otherwise, use the number as is (pre-calculated USD) const usdcAmount = isRaw ? numAmount / 10000000 : numAmount; + // 3. Format using standard US locale settings return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', @@ -24,28 +25,52 @@ export const formatCurrency = (amount: number | string, isRaw: boolean = true): }; /** - * 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', @@ -54,9 +79,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; @@ -86,12 +113,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; } @@ -99,10 +128,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; @@ -113,3 +143,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 9d6f6d3..24f606b 100644 --- a/src/lib/stellar.ts +++ b/src/lib/stellar.ts @@ -18,8 +18,15 @@ import { // Re-export WalletType for backward compatibility export { WalletType }; -// 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); const NETWORK_PASSPHRASE = Networks.TESTNET; @@ -37,13 +44,16 @@ export async function connectWallet(walletType: WalletType = FREIGHTER_ID): Prom currentWalletConnector = connector; return walletInfo; } 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 { if (!currentWalletConnector) return null; @@ -55,12 +65,15 @@ export async function getConnectedWallet(): Promise { const publicKey = await currentWalletConnector.getPublicKey(); return { publicKey, walletType: currentWalletConnector.getWalletType() }; } 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 { if (!currentWalletConnector) return; @@ -82,8 +95,8 @@ export async function disconnectWallet(): Promise { * @returns Promise that resolves to "SUCCESS" if successful */ 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( @@ -92,7 +105,7 @@ export async function waitForTransaction(hash: string): Promise { 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( @@ -117,21 +130,24 @@ export async function waitForTransaction(hash: string): Promise { ); } - // 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) { const connector = walletType ? createWalletConnector(walletType) : currentWalletConnector; @@ -139,7 +155,7 @@ export async function addTrustline(assetCode: string, assetIssuer: string, walle const publicKey = await connector.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); @@ -148,7 +164,7 @@ export async function addTrustline(assetCode: string, assetIssuer: string, walle networkPassphrase: NETWORK_PASSPHRASE, }) .addOperation(Operation.changeTrust({ asset })) - .setTimeout(60) + .setTimeout(60) // 60 seconds transaction validity .build(); const xdr = transaction.toXDR(); @@ -159,6 +175,7 @@ export async function addTrustline(assetCode: string, assetIssuer: string, walle const response = await server.sendTransaction(transaction); if (response.hash) { + // 5. Wait for ledger confirmation return await waitForTransaction(response.hash); } @@ -166,7 +183,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, 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 08361c2..55bc1d4 100644 --- a/src/stores/useWeb3Store.ts +++ b/src/stores/useWeb3Store.ts @@ -1,15 +1,26 @@ +/** + * 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 { FREIGHTER_ID, WalletType } from '../lib/stellar'; import { createWalletConnector, getWalletDisplayName } from '../lib/walletConnector'; import { getEffectiveNetwork, getNetworkConfig } from '../lib/networkConfig'; -// 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 @@ -18,41 +29,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) => ({