diff --git a/Desktop/vault/YieldVault-RWA/frontend/src/components/VaultDashboard.tsx b/Desktop/vault/YieldVault-RWA/frontend/src/components/VaultDashboard.tsx
new file mode 100644
index 00000000..28e12483
--- /dev/null
+++ b/Desktop/vault/YieldVault-RWA/frontend/src/components/VaultDashboard.tsx
@@ -0,0 +1,1197 @@
+import React, { useEffect, useState } from "react";
+import {
+ Activity,
+ AlertCircle,
+ AlertTriangle,
+ Check,
+ Loader2,
+ Share2,
+ ShieldCheck,
+ TrendingUp,
+ Wallet as WalletIcon,
+} from "./icons";
+import Skeleton, { DashboardCardSkeleton, SkeletonText, SkeletonCircle } from "./Skeleton";
+import { useDelayedLoading } from "../hooks/useDelayedLoading";
+import { useVault } from "../context/VaultContext";
+import ApiStatusBanner from "./ApiStatusBanner";
+import SharePriceDisplay from "./SharePriceDisplay";
+import VaultPerformanceChart from "./VaultPerformanceChart";
+import { useToast } from "../context/ToastContext";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs";
+import { FormField } from "../forms";
+import { isValidationError } from "../lib/api";
+import { useForm } from "../forms/useForm";
+import type { ValidationSchema } from "../forms/validate";
+import { useDepositMutation, useWithdrawMutation } from "../hooks/useVaultMutations";
+import { useTokenAllowance } from "../hooks/useTokenAllowance";
+import { createDepositFormSchema, MIN_DEPOSIT_AMOUNT } from "../forms/schemas/depositFormSchema";
+import { createWithdrawFormSchema } from "../forms/schemas/withdrawFormSchema";
+import { mapServerError } from "../lib/errorMappers";
+import CopyButton from "./CopyButton";
+import { copyTextToClipboard } from "../lib/clipboard";
+import { useFeeEstimate } from "../hooks/useFeeEstimate";
+import { useSlippage } from "../hooks/useSlippage";
+import HelpIcon from "./ui/HelpIcon";
+import EmptyState from "./ui/EmptyState";
+import { useTranslation } from "../i18n";
+import { networkConfig } from "../config/network";
+import { useDashboardUrlState, type TransactionTab, type TransactionStep } from "../hooks/useDashboardUrlState";
+import RefreshControl from "./RefreshControl";
+import { usePolling } from "../hooks/usePolling";
+import { useStaleIndicator } from "../hooks/useStaleIndicator";
+import { useNetworkStatus } from "../hooks/useNetworkStatus";
+import { useTransactionConfirmation } from "../hooks/useTransactionConfirmation";
+import { buildDepositSummary, buildWithdrawalSummary } from "../lib/transactionConfirmationBuilder";
+
+/**
+ * Visual indicator for the 3-step transaction wizard.
+ * Shows progress through Amount, Review, and Result stages.
+ */
+const StepIndicator: React.FC<{ currentStep: TransactionStep }> = ({ currentStep }) => {
+ const steps: Array<{ id: TransactionStep; label: string }> = [
+ { id: "amount", label: "Amount" },
+ { id: "review", label: "Review" },
+ { id: "result", label: "Result" },
+ ];
+ const stepOrder: TransactionStep[] = ["amount", "review", "result"];
+ const currentIndex = stepOrder.indexOf(currentStep);
+
+ return (
+
+ {steps.map((step, index) => {
+ const status =
+ index < currentIndex
+ ? "completed"
+ : index === currentIndex
+ ? "active"
+ : "pending";
+
+ return (
+
+
+
+ {status === "completed" ? : index + 1}
+
+
{step.label}
+
+ {index < steps.length - 1 && (
+
+ )}
+
+ );
+ })}
+
+ );
+};
+
+interface VaultDashboardProps {
+ walletAddress: string | null;
+ usdcBalance?: number;
+ xlmBalance?: number;
+}
+
+const MIN_DEPOSIT_AMOUNT = 1;
+
+const VaultCapWarning: React.FC<{ utilization: number; isReached: boolean }> = ({
+ utilization,
+ isReached,
+}) => {
+ const percent = (utilization * 100).toFixed(1);
+
+ return (
+
+ {isReached ? (
+
+ ) : (
+
+ )}
+
+
+ {isReached ? "Vault Capacity Reached" : "Vault Near Capacity"}
+
+
+ {isReached
+ ? `This vault has reached its maximum deposit cap of ${percent}%. Deposits are temporarily disabled.`
+ : `This vault is at ${percent}% capacity. New deposits may be restricted soon.`}
+
+
+
+ );
+};
+
+const VaultDashboard: React.FC = ({
+ walletAddress,
+ usdcBalance = 0,
+ xlmBalance = 0,
+}) => {
+ const dashboardUrl = useDashboardUrlState();
+ const {
+ formattedTvl,
+ formattedApy,
+ summary,
+ error,
+ isLoading,
+ utilization,
+ isCapWarning,
+ isCapReached,
+ lastUpdate,
+ refresh,
+ } = useVault();
+ const toast = useToast();
+ const delayedLoading = useDelayedLoading(isLoading);
+
+ const statsPolling = usePolling(refresh, {
+ interval: 30000,
+ pauseOnHidden: true,
+ pauseOnOffline: true,
+ });
+ const { isStale: statsIsStale, ageText: statsAgeText } = useStaleIndicator(lastUpdate);
+
+ const availableBalance = walletAddress ? usdcBalance : 0;
+
+ // Wizard state
+ const [transactionResult, setTransactionResult] = useState<{
+ success: boolean;
+ message: string;
+ txHash?: string
+ } | null>(null);
+
+ const { isOffline, countdown } = useOfflineRetryCountdown();
+
+ const depositMutation = useDepositMutation();
+ const withdrawMutation = useWithdrawMutation();
+ const { approvalStatus, needsApproval, approve, resetApproval } =
+ useTokenAllowance(walletAddress);
+
+ // Transaction confirmation modal
+ const confirmation = useTransactionConfirmation();
+
+ const { isOnline } = useNetworkStatus();
+ const { feeXlm, isEstimating, isHighFee } = useFeeEstimate(
+ walletAddress,
+ "",
+ dashboardUrl.state.tab,
+ isOnline
+ );
+
+ const { slippage, setSlippage, presets, isHighSlippage, minReceived } = useSlippage();
+ const [customSlippage, setCustomSlippage] = useState("");
+
+ // Create validation schema based on transaction type and current state
+ const transactionSchema = React.useMemo>(() => {
+ if (dashboardUrl.state.tab === "deposit") {
+ return createDepositFormSchema(availableBalance, isCapReached, xlmBalance, feeXlm);
+ } else {
+ return createWithdrawFormSchema(availableBalance);
+ }
+ }, [dashboardUrl.state.tab, availableBalance, isCapReached, xlmBalance, feeXlm]);
+
+ const {
+ values,
+ errors,
+ touched,
+ handleChange,
+ handleBlur,
+ setValues,
+ setFieldError
+ } = useForm({ amount: dashboardUrl.state.amount }, transactionSchema);
+
+ const amount = values.amount;
+
+ // Handle deep link parameters
+ useEffect(() => {
+ const action = dashboardUrl.state.tab;
+ const amountParam = dashboardUrl.state.amount;
+
+ if (action !== "deposit") {
+ return;
+ }
+
+ const parsedAmount = amountParam === "" ? Number.NaN : Number(amountParam);
+ if (Number.isFinite(parsedAmount) && parsedAmount > 0) {
+ setValues({ amount: parsedAmount.toString() });
+ }
+ }, [dashboardUrl.state.tab, dashboardUrl.state.amount, setValues]);
+
+ // Reset approval when deposit amount changes
+ useEffect(() => {
+ resetApproval();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [amount]);
+
+ const resetWizard = () => {
+ setValues({ amount: "" });
+ dashboardUrl.setStep("amount");
+ dashboardUrl.setAmount("");
+ setTransactionResult(null);
+ };
+
+ const goToReview = () => {
+ if (Object.keys(errors).length > 0) {
+ toast.warning({
+ title: "Please fix validation errors",
+ description: errors.amount || "Please enter a valid amount",
+ });
+ return;
+ }
+
+ dashboardUrl.setStep("review");
+ };
+
+ useEffect(() => {
+ const handleDeposit = () => {
+ dashboardUrl.setTab("deposit");
+ setTimeout(() => {
+ const input = document.querySelector(".input-field") as HTMLInputElement | null;
+ if (input) input.focus();
+ }, 0);
+ };
+ const handleWithdraw = () => {
+ dashboardUrl.setTab("withdraw");
+ setTimeout(() => {
+ const input = document.querySelector(".input-field") as HTMLInputElement | null;
+ if (input) input.focus();
+ }, 0);
+ };
+ window.addEventListener("TRIGGER_DEPOSIT", handleDeposit);
+ window.addEventListener("TRIGGER_WITHDRAW", handleWithdraw);
+ return () => {
+ window.removeEventListener("TRIGGER_DEPOSIT", handleDeposit);
+ window.removeEventListener("TRIGGER_WITHDRAW", handleWithdraw);
+ };
+ }, [dashboardUrl]);
+
+ const isProcessing = depositMutation.isPending
+ ? "deposit"
+ : withdrawMutation.isPending
+ ? "withdraw"
+ : null;
+ const isBusy = isProcessing !== null;
+
+ const strategy = summary.strategy;
+ const enteredAmount = Number(amount);
+ const activeAmountError = errors.amount;
+ const isValidAmount = !activeAmountError;
+ const showInlineError = touched.amount && Boolean(activeAmountError);
+ const managementFeeBps = 35;
+ const estimatedFee = isValidAmount
+ ? (enteredAmount * managementFeeBps) / 10_000
+ : 0;
+ const estimatedNetAmount = isValidAmount
+ ? Math.max(enteredAmount - estimatedFee, 0)
+ : 0;
+ const isSubmitDisabled =
+ !walletAddress ||
+ isBusy ||
+ Boolean(activeAmountError) ||
+ !amount ||
+ (dashboardUrl.state.tab === "deposit" && isCapReached);
+
+
+ const handleTransaction = async (actionType: TransactionTab) => {
+ const value = Number(amount);
+
+ if (!walletAddress) {
+ toast.warning({
+ title: "Wallet required",
+ description: "Connect your wallet before submitting a transaction.",
+ });
+ return;
+ }
+
+ try {
+ // Build transaction summary and request user confirmation before signing
+ const contractAddress = networkConfig.contractId;
+ let summary;
+
+ if (actionType === "deposit") {
+ summary = buildDepositSummary({
+ amount: value,
+ feeXlm,
+ contractAddress,
+ });
+ } else {
+ summary = buildWithdrawalSummary({
+ amount: value,
+ feeXlm,
+ contractAddress,
+ });
+ }
+
+ // Show confirmation modal and wait for user response
+ const confirmed = await confirmation.requestConfirmation(summary);
+ if (!confirmed) {
+ // User cancelled the confirmation
+ return;
+ }
+
+ // Proceed with the transaction after user confirmed
+ if (actionType === "deposit") {
+ await depositMutation.mutateAsync({ walletAddress, amount: value });
+
+ try {
+ const depositKey = `has_deposited_${walletAddress}`;
+ const alreadyDeposited = localStorage.getItem(depositKey);
+ const isTest = typeof process !== "undefined" && process.env?.NODE_ENV === "test";
+ if (!alreadyDeposited && !isTest) {
+ confetti({
+ particleCount: 150,
+ spread: 80,
+ origin: { y: 0.6 },
+ colors: ["#00f0ff", "#a855f7", "#ffffff", "#3b82f6"]
+ });
+ localStorage.setItem(depositKey, "true");
+ }
+ } catch (storageErr) {
+ console.warn("Storage access failed, triggering confetti anyway", storageErr);
+ const isTest = typeof process !== "undefined" && process.env?.NODE_ENV === "test";
+ if (!isTest) {
+ confetti({
+ particleCount: 150,
+ spread: 80,
+ origin: { y: 0.6 },
+ colors: ["#00f0ff", "#a855f7", "#ffffff", "#3b82f6"]
+ });
+ }
+ }
+ } else {
+ await withdrawMutation.mutateAsync({ walletAddress, amount: value });
+ }
+
+ setTransactionResult({
+ success: true,
+ message: actionType === "deposit"
+ ? `${value.toFixed(2)} USDC has been deposited into the vault.`
+ : `${value.toFixed(2)} USDC has been withdrawn from the vault.`,
+ });
+ dashboardUrl.setStep("result");
+
+ toast.success({
+ title: actionType === "deposit" ? "Deposit Successful" : "Withdrawal Successful",
+ description:
+ actionType === "deposit"
+ ? `${value.toFixed(2)} USDC has been deposited into the vault.`
+ : `${value.toFixed(2)} USDC has been withdrawn from the vault.`,
+ });
+ } catch (err: unknown) {
+ // Map server errors to form field errors
+ const mappedError = mapServerError(err);
+
+ if (mappedError.fieldErrors.length > 0) {
+ // Set field-level errors
+ mappedError.fieldErrors.forEach(({ fieldName, message }) => {
+ setFieldError(fieldName as keyof { amount: string }, message);
+ });
+ dashboardUrl.setStep("amount");
+ }
+
+ // Get error message for display
+ let errorMessage = "An error occurred during the transaction.";
+
+ if (isValidationError(err)) {
+ errorMessage = err.details?.[0]?.message || errorMessage;
+ } else if (err instanceof Error) {
+ errorMessage = err.message;
+ } else if (mappedError.generalError) {
+ errorMessage = mappedError.generalError;
+ }
+
+ setTransactionResult({
+ success: false,
+ message: errorMessage,
+ });
+ dashboardUrl.setStep("result");
+
+ toast.error({
+ title: "Transaction Failed",
+ description: errorMessage,
+ });
+ }
+ };
+
+ return (
+
+ {/* Transaction Confirmation Modal - shown for all sensitive actions */}
+ {confirmation.modal}
+
+
+
+ {error && (
+
+ )}
+
+
+
+ {delayedLoading ? : "Global RWA Yield Fund"}
+
+
+ {delayedLoading ? (
+
+ ) : (
+ <>
+
+ Tokens: USDC
+
+
+ >
+ )}
+
+
+
+
+ Current APY
+
+
+
+ {delayedLoading ? : formattedApy}
+
+
+
+
+
+
+ {/* Per-widget refresh control + stale indicator for stats panel */}
+
+
+ {statsIsStale && statsAgeText && (
+
+
+ Data may be stale · {statsAgeText}
+
+ )}
+
+
+
+
+
+ Total Value Locked
+
+ {!isOffline && }
+ {isOffline ? `Retrying in ${countdown}s...` : isLoading ? "Syncing" : "Live"}
+
+
+
+ {delayedLoading ? : formattedTvl}
+
+
+
+
+ Underlying Asset
+
+
+ {delayedLoading ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+ {summary.assetLabel}
+ >
+ )}
+
+
+
+
+
+ {delayedLoading ? (
+
+ ) : (
+ <>
+
+
+ Strategy Overview
+
+
+ BENJI Strategy
+
+
+ This vault pools USDC and deploys it into verified tokenized sovereign bonds available on
+ the Stellar network.
+
+
+
+
+ Target Allocation
+
+
70% Treasuries
+
30% Cash Reserve
+
+
+
+ Yield Distribution
+
+
Daily Compounding
+
+ Reflected in yvUSDC NAV
+
+
+
+
+ Risk Controls
+
+
Issuer + Duration Caps
+
+ Rebalanced every epoch
+
+
+
+
+ Strategy: {strategy.name} ({strategy.issuer})
+
+
+ Strategy ID:
+ {strategy.id}
+
+
+
+ Contract:
+
+ {networkConfig.contractId ? `${networkConfig.contractId.slice(0, 6)}...${networkConfig.contractId.slice(-4)}` : "Not Configured"}
+
+ {networkConfig.contractId && (
+
+ )}
+
+ >
+ )}
+
+
+ {/* Empty state: wallet connected, loading done, no USDC balance */}
+ {!isLoading && walletAddress && usdcBalance === 0 && (
+
}
+ actionLabel="Deposit Now"
+ onAction={() => {
+ window.dispatchEvent(new Event("TRIGGER_DEPOSIT"));
+ }}
+ />
+ )}
+
+
+
+
+
+
+
+
+
+ {!walletAddress && (
+
+
+
Wallet Not Connected
+
+ Please connect your Freighter wallet to interact with the vault.
+
+
+ )}
+
+
{
+ dashboardUrl.setTab(value as TransactionTab);
+ setValues({ amount: "" });
+ dashboardUrl.setAmount("");
+ }}
+ >
+ {dashboardUrl.state.step === "amount" && (
+
+ Deposit
+ Withdraw
+
+ )}
+
+
+
+ {(["deposit", "withdraw"] as const).map((tab) => (
+
+ {(isCapReached || isCapWarning) && tab === "deposit" && (
+
+ )}
+
+
+ {dashboardUrl.state.step === "amount" && (
+
+
+
+
+ {tab === "deposit" ? "Amount to deposit" : "Amount to withdraw"}
+
+
+ Balance: {availableBalance.toFixed(2)}
+
+
+
+
+
+
+
+
Asset: USDC
+ {tab === "deposit" && (
+ <>
+
+
{
+ const baseUrl = window.location.origin + window.location.pathname;
+ const shareUrl = amount && !isNaN(Number(amount)) && Number(amount) > 0
+ ? `${baseUrl}?action=deposit&amount=${amount}`
+ : baseUrl;
+
+ try {
+ await copyTextToClipboard(shareUrl);
+ toast.success({
+ title: "Link copied",
+ description: "Shareable vault link is ready to paste."
+ });
+ } catch {
+ toast.error({
+ title: "Copy failed",
+ description: "Could not copy link to clipboard."
+ });
+ }
+ }}
+ >
+
+ Share Link
+
+ >
+ )}
+
+
{
+ setValues({ amount: availableBalance.toFixed(2) });
+ }}
+ disabled={
+ !walletAddress ||
+ availableBalance <= 0 ||
+ isBusy ||
+ (tab === "deposit" && isCapReached)
+ }
+ >
+ MAX
+
+
+
+
+
+
+
+ Estimated protocol fee
+
+
+
+ {isValidAmount ? `${estimatedFee.toFixed(4)} USDC` : "0.0000 USDC"}
+
+
+
+
+ {tab === "deposit" ? "Estimated net deposit" : "Estimated net withdrawal"}
+
+
+ {isValidAmount ? `${estimatedNetAmount.toFixed(4)} USDC` : "0.0000 USDC"}
+
+
+
+
+
+ Review Transaction
+
+
+ )}
+
+ {dashboardUrl.state.step === "review" && (
+
+
+
+
+ Confirm Transaction
+
+
+
+
+
+ Action
+ {tab}
+
+
+ Amount
+ {enteredAmount.toFixed(2)} USDC
+
+
+
+ Protocol Fee (0.35%)
+ {estimatedFee.toFixed(4)} USDC
+
+
+ Network Fee
+
+ {isEstimating ? : `${feeXlm.toFixed(4)} XLM`}
+
+
+
+
+ Total To {tab === "deposit" ? "Vault" : "Wallet"}
+
+ {estimatedNetAmount.toFixed(4)} USDC
+
+
+
+
+
+ {tab === "withdraw" && isValidAmount && (
+
+
+ Slippage Tolerance
+
+
+ {presets.map((p) => (
+ { setSlippage(p); setCustomSlippage(""); }}
+ style={{
+ padding: "5px 12px",
+ borderRadius: "6px",
+ border: slippage === p && customSlippage === "" ? "1px solid var(--accent-cyan)" : "1px solid var(--border-glass)",
+ background: slippage === p && customSlippage === "" ? "rgba(0,240,255,0.1)" : "transparent",
+ color: slippage === p && customSlippage === "" ? "var(--accent-cyan)" : "var(--text-secondary)",
+ fontSize: "0.82rem",
+ cursor: "pointer",
+ fontWeight: 600,
+ }}
+ >
+ {p}%
+
+ ))}
+ {
+ const v = e.target.value;
+ setCustomSlippage(v);
+ const n = parseFloat(v);
+ if (isFinite(n) && n >= 0) setSlippage(n);
+ }}
+ style={{
+ width: "80px",
+ padding: "5px 8px",
+ borderRadius: "6px",
+ border: customSlippage !== "" ? "1px solid var(--accent-cyan)" : "1px solid var(--border-glass)",
+ background: "transparent",
+ color: "var(--text-primary)",
+ fontSize: "0.82rem",
+ outline: "none",
+ }}
+ aria-label="Custom slippage percentage"
+ />
+ %
+
+ {isHighSlippage && (
+
+
+
+ High slippage — you may receive significantly less than expected.
+
+
+ )}
+
+ Minimum received
+
+ {minReceived(estimatedNetAmount).toFixed(4)} USDC
+
+
+
+ )}
+
+ {isHighFee && (
+
+
+ High network fee
+ The estimated network fee exceeds 1% of your transaction value.
+
+
+ )}
+
+ {tab === "deposit" && xlmBalance < feeXlm && (
+
+
+
+ Insufficient XLM balance
+ You do not have enough XLM to cover the estimated network fee.
+
+
+ )}
+
+ {tab === "deposit" && isValidAmount && needsApproval(enteredAmount) && (
+
+
+
+
+ {approvalStatus === "confirmed" ? : "1"}
+
+ Approve USDC
+
+
+
+
+ {approvalStatus !== "confirmed" && (
+
{
+ try {
+ await approve(enteredAmount);
+ toast.success({ title: "USDC Approved" });
+ } catch {
+ toast.error({ title: "Approval Failed" });
+ }
+ }}
+ >
+ {approvalStatus === "pending" ? "Approving..." : "Approve USDC"}
+
+ )}
+
+ )}
+
+
+
+ dashboardUrl.setStep("amount")}
+ disabled={isBusy}
+ >
+ Back
+
+ void handleTransaction(tab)}
+ disabled={
+ isBusy ||
+ (tab === "deposit" && needsApproval(enteredAmount) && approvalStatus !== "confirmed") ||
+ (tab === "deposit" && xlmBalance < feeXlm)
+ }
+ >
+ {isBusy ? (
+ <>
+
+ Processing...
+ >
+ ) : (
+ `Confirm ${tab}`
+ )}
+
+
+
+ )}
+
+ {dashboardUrl.state.step === "result" && transactionResult && (
+
+
+ {transactionResult.success ?
:
}
+
+
+ {transactionResult.success ? "Transaction Successful" : "Transaction Failed"}
+
+
+ {transactionResult.message}
+
+
+
+ {transactionResult.success ? "Done" : "Try Again"}
+
+
+ )}
+
+
+ ))}
+
+
+
+
+ );
+};
+
+export default VaultDashboard;
diff --git a/Desktop/vault/YieldVault-RWA/frontend/src/pages/TransactionHistory.tsx b/Desktop/vault/YieldVault-RWA/frontend/src/pages/TransactionHistory.tsx
new file mode 100644
index 00000000..493dd151
--- /dev/null
+++ b/Desktop/vault/YieldVault-RWA/frontend/src/pages/TransactionHistory.tsx
@@ -0,0 +1,735 @@
+import React, { useState, useEffect, useCallback, useRef } from "react";
+import { useNavigate, useSearchParams } from "react-router-dom";
+import ApiStatusBanner from "../components/ApiStatusBanner";
+import Badge from "../components/Badge";
+import { DataTable, type DataTableColumn } from "../components/DataTable";
+import PageHeader from "../components/PageHeader";
+import TransactionFilterPanel from "../components/TransactionFilterPanel";
+import TransactionTimeline from "../components/TransactionTimeline";
+import EmptyState from "../components/ui/EmptyState";
+import { Activity, Loader2 } from "../components/icons";
+import { useTransactionTimeline } from "../hooks/useTransactionTimeline";
+import {
+ normalizeApiError,
+ isValidationError,
+ type ApiError,
+ type ValidationError,
+} from "../lib/api";
+import {
+ formatAmount,
+ formatTimestamp,
+ truncateHash,
+ getTransactions,
+ type Transaction,
+} from "../lib/transactionApi";
+import { useClientDataTable } from "../hooks/useClientDataTable";
+import { useDataTableState } from "../hooks/useDataTableState";
+import { useInfiniteScroll } from "../hooks/useInfiniteScroll";
+import { useTransactionFilters } from "../hooks/useTransactionFilters";
+import { useTransactionHistory } from "../hooks/useTransactionData";
+import { getStellarExplorerUrl } from "../lib/security";
+import { networkConfig } from "../config/network";
+import RefreshControl from "../components/RefreshControl";
+import CopyButton from "../components/CopyButton";
+
+import { useDelayedLoading } from "../hooks/useDelayedLoading";
+
+interface TransactionHistoryProps {
+ walletAddress: string | null;
+}
+
+type TxTypeFilter = "all" | "deposit" | "withdrawal";
+type ViewMode = "paginated" | "infinite";
+const DEFAULT_PAGE_SIZE = 10;
+const INFINITE_SCROLL_BATCH_SIZE = 20;
+const PAGE_SIZE_OPTIONS = [10, 25, 50] as const;
+
+function getPageSizeStorageKey(walletAddress: string | null): string {
+ return `yieldvault:transactions:page-size:${walletAddress ?? "guest"}`;
+}
+
+function getViewModeStorageKey(walletAddress: string | null): string {
+ return `yieldvault:transactions:view-mode:${walletAddress ?? "guest"}`;
+}
+
+function loadPreferredPageSize(walletAddress: string | null): number {
+ try {
+ const raw = localStorage.getItem(getPageSizeStorageKey(walletAddress));
+ const parsed = raw ? Number(raw) : Number.NaN;
+ if (PAGE_SIZE_OPTIONS.includes(parsed as (typeof PAGE_SIZE_OPTIONS)[number])) {
+ return parsed;
+ }
+ } catch {
+ // localStorage unavailable; fall back to defaults
+ }
+ return DEFAULT_PAGE_SIZE;
+}
+
+function persistPreferredPageSize(walletAddress: string | null, pageSize: number): void {
+ try {
+ localStorage.setItem(getPageSizeStorageKey(walletAddress), String(pageSize));
+ } catch {
+ // localStorage unavailable; silently ignore
+ }
+}
+
+function loadViewMode(walletAddress: string | null): ViewMode {
+ try {
+ const raw = localStorage.getItem(getViewModeStorageKey(walletAddress));
+ if (raw === "paginated" || raw === "infinite") {
+ return raw;
+ }
+ } catch {
+ // localStorage unavailable
+ }
+ return "paginated";
+}
+
+function persistViewMode(walletAddress: string | null, mode: ViewMode): void {
+ try {
+ localStorage.setItem(getViewModeStorageKey(walletAddress), mode);
+ } catch {
+ // localStorage unavailable
+ }
+}
+const STATUS_COLOR_MAP: Record = {
+ completed: "success",
+ pending: "warning",
+ failed: "error",
+};
+
+/** Inline panel shown below a pending transaction row to track its live state. */
+const PendingTimelinePanel: React.FC<{ txHash: string; onDismiss: () => void }> = ({
+ txHash,
+ onDismiss,
+}) => {
+ const { status, elapsedSeconds, errorMessage } = useTransactionTimeline({ txHash });
+
+ return (
+
+
+
+ Live Status
+
+
+ ✕
+
+
+
+
+ );
+};
+
+const TransactionHistory: React.FC = ({
+ walletAddress,
+}) => {
+ const navigate = useNavigate();
+ const { data: queryTransactions, isLoading, error: queryError } = useTransactionHistory(walletAddress);
+ const delayedLoading = useDelayedLoading(isLoading);
+ const transactions = queryTransactions ?? [];
+
+ const [selectedPendingHash, setSelectedPendingHash] = useState(null);
+
+ const columns: DataTableColumn[] = React.useMemo(() => [
+ {
+ id: "type",
+ header: "Type",
+ sortable: true,
+ cell: (row) => (
+
+ {row.type}
+
+ ),
+ },
+ {
+ id: "status",
+ header: "Status",
+ sortable: true,
+ cell: (row) => (
+ setSelectedPendingHash(
+ selectedPendingHash === row.transactionHash ? null : row.transactionHash
+ ) : undefined}
+ style={{
+ background: "none",
+ border: "none",
+ padding: 0,
+ cursor: row.status === "pending" ? "pointer" : "default",
+ }}
+ title={row.status === "pending" ? "Click to track live status" : undefined}
+ aria-expanded={row.status === "pending" ? selectedPendingHash === row.transactionHash : undefined}
+ >
+ : undefined}
+ >
+ {row.status}
+
+
+ ),
+ },
+ {
+ id: "amount",
+ header: "Amount",
+ sortable: true,
+ cell: (row) => {formatAmount(row.amount, row.asset)} ,
+ },
+ {
+ id: "asset",
+ header: "Asset",
+ sortable: false,
+ cell: (row) => {row.asset ?? "—"} ,
+ },
+ {
+ id: "date",
+ header: "Date",
+ sortable: true,
+ cell: (row) => {formatTimestamp(row.timestamp)} ,
+ },
+ {
+ id: "hash",
+ header: "Transaction Hash",
+ sortable: false,
+ cell: (row) => (
+
+ ),
+ },
+ ], [selectedPendingHash]);
+
+ const error = queryError
+ ? (isValidationError(queryError) ? queryError : normalizeApiError(queryError))
+ : null;
+
+ const preferredPageSize = React.useMemo(
+ () => loadPreferredPageSize(walletAddress),
+ [walletAddress],
+ );
+
+ // View mode state
+ const [viewMode, setViewMode] = useState(() => loadViewMode(walletAddress));
+
+ // Infinite scroll state
+ const [visibleCount, setVisibleCount] = useState(INFINITE_SCROLL_BATCH_SIZE);
+ const [hasMoreItems, setHasMoreItems] = useState(true);
+ const loadMoreLockRef = useRef(false);
+
+ // ── Sort / pagination state (URL-synced via useDataTableState) ──────────
+ const { state, setSearch, setSort, setPage, setPageSize } = useDataTableState(
+ {
+ defaultSortBy: "date",
+ defaultSortDirection: "desc",
+ defaultPageSize: preferredPageSize,
+ },
+ );
+
+ // ── Multi-filter state (URL-synced via useTransactionFilters) ───────────
+ const {
+ filters,
+ hasActiveFilters,
+ setSearch: setFilterSearch,
+ setTypes,
+ setStatuses,
+ setDateFrom,
+ setDateTo,
+ setAmountMin,
+ setAmountMax,
+ clearAll,
+ setAsset,
+ } = useTransactionFilters();
+
+ // Keep useDataTableState's search in sync with the filter panel's search
+ // so that useClientDataTable's text-search logic still runs correctly.
+ const [searchParams] = useSearchParams();
+ useEffect(() => {
+ const urlSearch = searchParams.get("search") ?? "";
+ if (urlSearch !== state.search) {
+ setSearch(urlSearch);
+ }
+ }, [searchParams, state.search, setSearch]);
+
+ // Client-side filtering is handled by useClientDataTable.
+
+ // ── Client-side filtering ───────────────────────────────────────────────
+ const { rows, sortedRows, page, totalItems, totalPages } = useClientDataTable(
+ {
+ rows: transactions,
+ state,
+ getSearchValue: (row) =>
+ `${row.type} ${row.asset ?? ""} ${row.transactionHash}`,
+ getSortValue: (row, columnId) => {
+ switch (columnId) {
+ case "type":
+ return row.type;
+ case "status":
+ return row.status;
+ case "amount":
+ return row.amount !== null ? parseFloat(row.amount) : 0;
+ case "date":
+ return row.timestamp;
+ default:
+ return row.timestamp;
+ }
+ },
+ filterRow: (row) => {
+ // Multi-type filter (client-side)
+ if (filters.types.length > 0 && !filters.types.includes(row.type)) {
+ return false;
+ }
+
+ // Status filter (client-side)
+ if (
+ filters.statuses.length > 0 &&
+ !filters.statuses.includes(row.status)
+ ) {
+ return false;
+ }
+
+ // Date range
+ if (filters.dateFrom) {
+ const from = new Date(filters.dateFrom);
+ from.setHours(0, 0, 0, 0);
+ if (new Date(row.timestamp) < from) return false;
+ }
+ if (filters.dateTo) {
+ const to = new Date(filters.dateTo);
+ to.setHours(23, 59, 59, 999);
+ if (new Date(row.timestamp) > to) return false;
+ }
+
+ // Amount range (numeric)
+ if (filters.amountMin !== "" && row.amount !== null) {
+ const min = parseFloat(filters.amountMin);
+ const amt = parseFloat(row.amount);
+ if (!isNaN(min) && !isNaN(amt) && amt < min) return false;
+ }
+ if (filters.amountMax !== "" && row.amount !== null) {
+ const max = parseFloat(filters.amountMax);
+ const amt = parseFloat(row.amount);
+ if (!isNaN(max) && !isNaN(amt) && amt > max) return false;
+ }
+
+ return true;
+ },
+ },
+ );
+
+ // Available assets for the asset filter (unique, non-empty)
+ const assetOptions = React.useMemo(() => {
+ const set = new Set();
+ for (const t of transactions) {
+ if (t.asset) set.add(t.asset);
+ }
+ return Array.from(set).sort();
+ }, [transactions]);
+
+ // Infinite scroll: compute visible rows from sorted/filtered set
+ const infiniteScrollRows = React.useMemo(() => {
+ return sortedRows.slice(0, visibleCount);
+ }, [sortedRows, visibleCount]);
+
+ // Update hasMoreItems when the data or visibleCount changes
+ useEffect(() => {
+ setHasMoreItems(visibleCount < sortedRows.length);
+ }, [visibleCount, sortedRows.length]);
+
+ // Reset visible count when filters/search/sort change
+ useEffect(() => {
+ setVisibleCount(INFINITE_SCROLL_BATCH_SIZE);
+ }, [
+ state.search,
+ state.sortBy,
+ state.sortDirection,
+ filters.types,
+ filters.dateFrom,
+ filters.dateTo,
+ ]);
+
+ // Handle loading more items for infinite scroll
+ const handleLoadMore = useCallback(() => {
+ if (loadMoreLockRef.current || !hasMoreItems) return;
+ loadMoreLockRef.current = true;
+
+ setVisibleCount((prev) => {
+ const next = Math.min(prev + INFINITE_SCROLL_BATCH_SIZE, sortedRows.length);
+ return next;
+ });
+
+ // Release lock after a small delay to prevent rapid-fire calls
+ setTimeout(() => {
+ loadMoreLockRef.current = false;
+ }, 100);
+ }, [hasMoreItems, sortedRows.length]);
+
+ const { sentinelRef, isLoadingMore } = useInfiniteScroll(handleLoadMore, {
+ enabled: viewMode === "infinite" && hasMoreItems && !isLoading,
+ threshold: 200,
+ });
+
+ // View mode toggle handler
+ const handleViewModeChange = (mode: ViewMode) => {
+ setViewMode(mode);
+ persistViewMode(walletAddress, mode);
+ if (mode === "infinite") {
+ setVisibleCount(INFINITE_SCROLL_BATCH_SIZE);
+ }
+ };
+
+ // ── CSV export ──────────────────────────────────────────────────────────
+ const buildCsvContent = (transactionsToExport: Transaction[]) => {
+ const headers = ["date", "type", "status", "amount", "share price", "fee", "tx hash"];
+
+ const escapeCsvValue = (value: string) => `"${value.replace(/"/g, '""')}"`;
+
+ const csvRows = transactionsToExport.map((transaction) => [
+ formatTimestamp(transaction.timestamp),
+ transaction.type,
+ transaction.status,
+ formatAmount(transaction.amount, transaction.asset),
+ "",
+ "",
+ transaction.transactionHash,
+ ]);
+
+ return [headers, ...csvRows]
+ .map((columns) => columns.map(escapeCsvValue).join(","))
+ .join("\r\n");
+ };
+
+ const handleExportCsv = () => {
+ const csvContent = buildCsvContent(sortedRows);
+ const fileName = `transactions_${new Date().toISOString().slice(0, 10)}.csv`;
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
+ const url =
+ typeof URL !== "undefined" && URL.createObjectURL
+ ? URL.createObjectURL(blob)
+ : `data:text/csv;charset=utf-8,${encodeURIComponent(csvContent)}`;
+
+ const link = document.createElement("a");
+ link.href = url;
+ link.setAttribute("download", fileName);
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ if (
+ typeof URL !== "undefined" &&
+ URL.revokeObjectURL &&
+ url.startsWith("blob:")
+ ) {
+ URL.revokeObjectURL(url);
+ }
+ };
+
+ // ── Empty state ─────────────────────────────────────────────────────────
+ const emptyMessage = (
+ }
+ {...(hasActiveFilters
+ ? { actionLabel: "Reset filters", onAction: clearAll }
+ : {
+ actionLabel: "Deposit Now",
+ onAction: () => navigate("/"),
+ })}
+ />
+ );
+
+ // Determine which rows to show based on view mode
+ const displayRows = viewMode === "infinite" ? infiniteScrollRows : rows;
+
+ return (
+
+
+ Transaction History
+ >
+ }
+ description="View all your past deposits and withdrawals."
+ breadcrumbs={[{ label: "Home", href: "/" }, { label: "Transactions" }]}
+ statusChips={
+ walletAddress
+ ? [
+ {
+ label: `${transactions.length} Total`,
+ variant: "cyan",
+ },
+ {
+ label: isLoading ? "Loading..." : "Up to date",
+ variant: isLoading ? "warning" : "success",
+ },
+ ]
+ : undefined
+ }
+ />
+
+ {!walletAddress ? (
+
+
+ Please connect your wallet to view your transaction history.
+
+
+ ) : (
+
+ {error &&
}
+
+ {/* ── Filter panel ──────────────────────────────────────── */}
+
+
+ {/* ── Data table ────────────────────────────────────────── */}
+
+
+
+
+ Transactions
+
+
+ Sort and filter your deposit and withdrawal history.
+
+
+
+
+
+ Rows
+
+ {
+ const nextSize = Number(e.target.value);
+ persistPreferredPageSize(walletAddress, nextSize);
+ setPageSize(nextSize);
+ }}
+ className="portfolio-select"
+ >
+ 10
+ 25
+ 50
+
+
+
+
+ {/* View Mode Toggle */}
+
+
View
+
+
handleViewModeChange("paginated")}
+ title="Paginated view"
+ >
+
+
+
+
+
+ Pages
+
+
handleViewModeChange("infinite")}
+ title="Infinite scroll view"
+ >
+
+
+
+
+
+
+
+ Scroll
+
+
+
+
+
+ Export CSV
+
+
+
+
+
+ {delayedLoading
+ ?
+ : viewMode === "infinite"
+ ? `Showing ${infiniteScrollRows.length} of ${sortedRows.length} transactions`
+ : `${totalItems} transactions found`}
+
+
+ {viewMode === "infinite" ? (
+ /* Infinite Scroll View */
+
+
row.id}
+ emptyMessage={emptyMessage}
+ isLoading={delayedLoading}
+ skeletonRows={state.pageSize}
+ sortBy={state.sortBy}
+ sortDirection={state.sortDirection}
+ onSortChange={setSort}
+ />
+
+ {/* Infinite scroll sentinel & status */}
+ {sortedRows.length > 0 && (
+
+ {hasMoreItems ? (
+ <>
+
+ {isLoadingMore && (
+
+
+
Loading more transactions...
+
+ )}
+ >
+ ) : (
+
+
+
All {sortedRows.length} transactions loaded
+
+
+ )}
+
+ {/* Progress indicator */}
+
+
+ )}
+
+ ) : (
+ /* Paginated View (original) */
+ row.id}
+ emptyMessage={emptyMessage}
+ isLoading={delayedLoading}
+ skeletonRows={state.pageSize}
+ sortBy={state.sortBy}
+ sortDirection={state.sortDirection}
+ onSortChange={setSort}
+ pagination={{
+ page,
+ pageSize: state.pageSize,
+ totalItems,
+ totalPages,
+ }}
+ onPageChange={setPage}
+ />
+ )}
+
+ {/* Live timeline for selected pending transaction */}
+ {selectedPendingHash && (
+ setSelectedPendingHash(null)}
+ />
+ )}
+
+
+ )}
+
+ );
+};
+
+export default TransactionHistory;
diff --git a/Desktop/vault/YieldVault-RWA/frontend/src/pages/TransactionReceipt.tsx b/Desktop/vault/YieldVault-RWA/frontend/src/pages/TransactionReceipt.tsx
new file mode 100644
index 00000000..b021e14a
--- /dev/null
+++ b/Desktop/vault/YieldVault-RWA/frontend/src/pages/TransactionReceipt.tsx
@@ -0,0 +1,198 @@
+import { useEffect, useState } from "react";
+import { useParams, Link } from "react-router-dom";
+import CopyButton from "../components/CopyButton";
+
+const HORIZON_BASE = "https://horizon-testnet.stellar.org";
+const EXPLORER_BASE = "https://stellar.expert/explorer/testnet/tx";
+
+interface TxDetails {
+ hash: string;
+ created_at: string;
+ fee_charged: string;
+ source_account: string;
+ operation_count: number;
+ memo?: string;
+ // Derived from first payment operation
+ type?: "deposit" | "withdrawal";
+ amount?: string;
+ asset?: string;
+}
+
+interface HorizonTx {
+ hash: string;
+ created_at: string;
+ fee_charged: string;
+ source_account: string;
+ operation_count: number;
+ memo?: string;
+}
+
+interface HorizonOp {
+ type: string;
+ amount?: string;
+ asset_type?: string;
+ asset_code?: string;
+ from?: string;
+ to?: string;
+}
+
+export default function TransactionReceipt() {
+ const { txHash } = useParams<{ txHash: string }>();
+ const [tx, setTx] = useState(null);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (!txHash) return;
+
+ async function fetchTx() {
+ try {
+ const [txRes, opsRes] = await Promise.all([
+ fetch(`${HORIZON_BASE}/transactions/${txHash}`),
+ fetch(`${HORIZON_BASE}/transactions/${txHash}/operations`),
+ ]);
+
+ if (!txRes.ok) throw new Error(`Transaction not found (${txRes.status})`);
+
+ const txData = (await txRes.json()) as HorizonTx;
+ const opsData = opsRes.ok
+ ? (await opsRes.json() as { _embedded: { records: HorizonOp[] } })
+ : null;
+
+ const paymentOp = opsData?._embedded?.records?.find(
+ (op) => op.type === "payment",
+ );
+
+ setTx({
+ hash: txData.hash,
+ created_at: txData.created_at,
+ fee_charged: txData.fee_charged,
+ source_account: txData.source_account,
+ operation_count: txData.operation_count,
+ memo: txData.memo,
+ type: paymentOp ? "deposit" : undefined,
+ amount: paymentOp?.amount,
+ asset:
+ paymentOp?.asset_type === "native"
+ ? "XLM"
+ : (paymentOp?.asset_code ?? undefined),
+ });
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to load transaction");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ void fetchTx();
+ }, [txHash]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error || !tx) {
+ return (
+
+
{error ?? "Transaction not found."}
+
← Back to app
+
+ );
+ }
+
+ const feeXlm = (parseInt(tx.fee_charged, 10) / 1e7).toFixed(7);
+ const date = new Date(tx.created_at).toLocaleString("en-US", {
+ dateStyle: "long",
+ timeStyle: "short",
+ });
+
+ return (
+
+
+
+
+
+
+
Date
+ {date}
+
+ {tx.type && (
+
+
Type
+
+ {tx.type.charAt(0).toUpperCase() + tx.type.slice(1)}
+
+
+ )}
+ {tx.amount && tx.asset && (
+
+
Amount
+
+ {parseFloat(tx.amount).toLocaleString("en-US", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 7,
+ })}{" "}
+ {tx.asset}
+
+
+ )}
+
+
Network Fee
+ {feeXlm} XLM
+
+
+
Wallet Address
+
+
+ {tx.source_account}
+
+
+
+
+
+ {tx.memo && (
+
+
Memo
+ {tx.memo}
+
+ )}
+
+
+
+ window.print()}
+ >
+ Print Receipt
+
+
+ View All Transactions
+
+
+
+
+ );
+}