diff --git a/src/app/transactions/page.tsx b/src/app/transactions/page.tsx
new file mode 100644
index 0000000..f2c439c
--- /dev/null
+++ b/src/app/transactions/page.tsx
@@ -0,0 +1,333 @@
+"use client";
+
+import { useEffect, useState, useCallback } from "react";
+import { Navbar } from "@/components/Navbar";
+import {
+ fetchTransactions,
+ ParsedTransaction,
+ TransactionPage,
+} from "@/lib/horizon";
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function truncateAddress(addr: string, chars = 6): string {
+ if (addr.length <= chars * 2 + 3) return addr;
+ return `${addr.slice(0, chars)}...${addr.slice(-chars)}`;
+}
+
+function formatDate(iso: string): string {
+ return new Date(iso).toLocaleString();
+}
+
+function getOperationLabel(type: string): string {
+ switch (type) {
+ case "payment":
+ return "Payment";
+ case "create_account":
+ return "Create Account";
+ case "invoke_host_function":
+ return "Contract Invoke";
+ case "manage_sell_offer":
+ return "Manage Offer";
+ case "change_trust":
+ return "Change Trust";
+ default:
+ return type.replace(/_/g, " ");
+ }
+}
+
+function getOperationColor(type: string): string {
+ switch (type) {
+ case "payment":
+ return "bg-green-100 text-green-800";
+ case "invoke_host_function":
+ return "bg-purple-100 text-purple-800";
+ case "create_account":
+ return "bg-blue-100 text-blue-800";
+ default:
+ return "bg-gray-100 text-gray-800";
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Components
+// ---------------------------------------------------------------------------
+
+function OperationBadge({ type }: { type: string }) {
+ return (
+
+ {getOperationLabel(type)}
+
+ );
+}
+
+function TransactionRow({ tx }: { tx: ParsedTransaction }) {
+ return (
+
+
+ {/* Top row */}
+
+
+
+
+ {truncateAddress(tx.hash, 8)}
+
+
+
{formatDate(tx.createdAt)}
+
+
+ {/* Meta */}
+
+
+ Source:{" "}
+ {truncateAddress(tx.sourceAccount)}
+
+ Ledger #{tx.ledger}
+ {tx.memo && Memo: {tx.memo}}
+
+
+ {/* Operations */}
+ {tx.operations.length > 0 && (
+
+ {tx.operations.map((op) => (
+
+
+ {op.type === "payment" && (
+
+ {op.amount}{" "}
+ {op.asset_type === "native" ? "XLM" : op.asset_code}
+ {" → "}
+
+ {truncateAddress(op.to ?? "", 4)}
+
+
+ )}
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Page
+// ---------------------------------------------------------------------------
+
+export default function TransactionsPage() {
+ const [page, setPage] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [cursor, setCursor] = useState(null);
+ const [cursorStack, setCursorStack] = useState<(string | null)[]>([null]);
+
+ const loadPage = useCallback(async (cursorVal: string | null) => {
+ setLoading(true);
+ setError(null);
+ try {
+ const data = await fetchTransactions(cursorVal, 10, "desc");
+ setPage(data);
+ setCursor(cursorVal);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to fetch transactions");
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ loadPage(null);
+ }, [loadPage]);
+
+ const handleNext = () => {
+ if (page?.nextCursor) {
+ setCursorStack((prev) => [...prev, page.nextCursor]);
+ loadPage(page.nextCursor);
+ }
+ };
+
+ const handlePrev = () => {
+ if (cursorStack.length > 1) {
+ const stack = [...cursorStack];
+ stack.pop();
+ const prevCursor = stack[stack.length - 1];
+ setCursorStack(stack);
+ loadPage(prevCursor);
+ }
+ };
+
+ const handleRefresh = () => {
+ setCursorStack([null]);
+ loadPage(null);
+ };
+
+ return (
+ <>
+
+
+ {/* Header */}
+
+
+
+ Transaction History
+
+
+ On-chain transactions from Stellar Horizon
+
+
+
+
+
+ {/* Error */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Loading skeleton */}
+ {loading && !page && (
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+ )}
+
+ {/* Transaction list */}
+ {page && page.transactions.length > 0 && (
+
+ {page.transactions.map((tx) => (
+
+ ))}
+
+ )}
+
+ {/* Empty state */}
+ {page && page.transactions.length === 0 && (
+
+
+
+ No transactions found
+
+
+ There are no transactions for this account yet.
+
+
+ )}
+
+ {/* Pagination */}
+ {page && page.transactions.length > 0 && (
+
+
+
+
+ Page {cursorStack.length}
+
+
+
+
+ )}
+
+ >
+ );
+}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 2d673aa..9d6d67c 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -25,6 +25,12 @@ export function Navbar() {
>
Create Group
+
+ Transactions
+
diff --git a/src/lib/horizon.ts b/src/lib/horizon.ts
new file mode 100644
index 0000000..c3a747c
--- /dev/null
+++ b/src/lib/horizon.ts
@@ -0,0 +1,217 @@
+/**
+ * Stellar Horizon API client for fetching transaction history.
+ * Provides caching and pagination support.
+ */
+
+const HORIZON_URL =
+ process.env.NEXT_PUBLIC_HORIZON_URL ||
+ "https://horizon-testnet.stellar.org";
+
+const CONTRACT_ID = process.env.NEXT_PUBLIC_CONTRACT_ID || "";
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+export interface HorizonTransaction {
+ id: string;
+ hash: string;
+ ledger: number;
+ created_at: string;
+ source_account: string;
+ operation_count: number;
+ memo_type: string;
+ memo?: string;
+ successful: boolean;
+ paging_token: string;
+}
+
+export interface TransactionOperation {
+ id: string;
+ type: string;
+ type_i: number;
+ source_account: string;
+ transaction_hash: string;
+ created_at: string;
+ from?: string;
+ to?: string;
+ amount?: string;
+ asset_type?: string;
+ asset_code?: string;
+ asset_issuer?: string;
+ contract_id?: string;
+ function?: string;
+}
+
+export interface ParsedTransaction {
+ id: string;
+ hash: string;
+ ledger: number;
+ createdAt: string;
+ sourceAccount: string;
+ successful: boolean;
+ operationCount: number;
+ operations: TransactionOperation[];
+ memo?: string;
+}
+
+export interface TransactionPage {
+ transactions: ParsedTransaction[];
+ nextCursor: string | null;
+ prevCursor: string | null;
+}
+
+// ---------------------------------------------------------------------------
+// Simple in-memory cache
+// ---------------------------------------------------------------------------
+
+interface CacheEntry {
+ data: T;
+ timestamp: number;
+}
+
+const cache = new Map>();
+const CACHE_TTL_MS = 30_000;
+
+function getCached(key: string): T | null {
+ const entry = cache.get(key);
+ if (!entry) return null;
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
+ cache.delete(key);
+ return null;
+ }
+ return entry.data as T;
+}
+
+function setCache(key: string, data: T): void {
+ cache.set(key, { data, timestamp: Date.now() });
+}
+
+export function clearHorizonCache(): void {
+ cache.clear();
+}
+
+// ---------------------------------------------------------------------------
+// Horizon API helpers
+// ---------------------------------------------------------------------------
+
+async function fetchHorizon(path: string): Promise {
+ const url = `${HORIZON_URL}${path}`;
+ const res = await fetch(url);
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`Horizon error ${res.status}: ${text}`);
+ }
+ return res.json() as Promise;
+}
+
+interface HorizonTransactionResponse {
+ _embedded: {
+ records: HorizonTransaction[];
+ };
+ _links: {
+ next: { href: string };
+ prev: { href: string };
+ };
+}
+
+interface HorizonOperationsResponse {
+ _embedded: {
+ records: TransactionOperation[];
+ };
+}
+
+async function fetchTransactionOperations(
+ txHash: string
+): Promise {
+ const data = await fetchHorizon(
+ `/transactions/${txHash}/operations?limit=50`
+ );
+ return data._embedded.records;
+}
+
+async function enrichTransactions(
+ records: HorizonTransaction[]
+): Promise {
+ const parsed = await Promise.all(
+ records.map(async (tx) => {
+ let operations: TransactionOperation[] = [];
+ try {
+ operations = await fetchTransactionOperations(tx.hash);
+ } catch {
+ // If operations fetch fails, continue without them
+ }
+ return {
+ id: tx.id,
+ hash: tx.hash,
+ ledger: tx.ledger,
+ createdAt: tx.created_at,
+ sourceAccount: tx.source_account,
+ successful: tx.successful,
+ operationCount: tx.operation_count,
+ operations,
+ memo: tx.memo,
+ };
+ })
+ );
+ return parsed;
+}
+
+// ---------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------
+
+export async function fetchTransactions(
+ cursor: string | null = null,
+ limit = 10,
+ order: "asc" | "desc" = "desc"
+): Promise {
+ if (!CONTRACT_ID) {
+ throw new Error(
+ "NEXT_PUBLIC_CONTRACT_ID is not configured. Set it in your .env file."
+ );
+ }
+
+ const cacheKey = `tx:${CONTRACT_ID}:${cursor ?? "latest"}:${limit}:${order}`;
+ const cached = getCached(cacheKey);
+ if (cached) return cached;
+
+ const params = new URLSearchParams({
+ limit: String(Math.min(limit, 200)),
+ order,
+ include_failed: "true",
+ });
+ if (cursor) {
+ params.set("cursor", cursor);
+ }
+
+ const data = await fetchHorizon(
+ `/accounts/${CONTRACT_ID}/transactions?${params}`
+ );
+
+ const transactions = await enrichTransactions(data._embedded.records);
+
+ const nextHref = data._links.next?.href ?? null;
+ const prevHref = data._links.prev?.href ?? null;
+ const nextCursor = nextHref
+ ? new URL(nextHref, HORIZON_URL).searchParams.get("cursor")
+ : null;
+ const prevCursor = prevHref
+ ? new URL(prevHref, HORIZON_URL).searchParams.get("cursor")
+ : null;
+
+ const page: TransactionPage = {
+ transactions,
+ nextCursor,
+ prevCursor,
+ };
+
+ setCache(cacheKey, page);
+ return page;
+}
+
+export async function fetchRecentTransactions(
+ limit = 10
+): Promise {
+ return fetchTransactions(null, limit, "desc");
+}