diff --git a/src/app/history/page.tsx b/src/app/history/page.tsx new file mode 100644 index 0000000..beeae07 --- /dev/null +++ b/src/app/history/page.tsx @@ -0,0 +1,223 @@ +"use client"; + +import { Navbar } from "@/components/Navbar"; +import { useWallet } from "@/app/providers"; +import { useState, useEffect } from "react"; + +// Transaction types +interface Transaction { + hash: string; + type: "contribution" | "payout" | "group_join"; + amount: string; + timestamp: number; + memo?: string; + explorerUrl: string; +} + +export default function HistoryPage() { + const { isConnected, address } = useWallet(); + const [transactions, setTransactions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!isConnected || !address) { + setLoading(false); + return; + } + + const fetchTransactions = async () => { + try { + // Fetch transactions from Stellar testnet + const resp = await fetch( + `https://soroban-testnet.stellar.org/events?address=${address}&types=operation&cursor=&limit=50` + ); + + if (!resp.ok) { + throw new Error("Failed to fetch transactions"); + } + + const data = await resp.json(); + + // Transform Soroban events into our Transaction type + const txns: Transaction[] = data.items + .filter((item: any) => item.type === "operation") + .slice(0, 30) + .map((item: any) => ({ + hash: item.id || "unknown", + type: mapTransactionType(item), + amount: formatAmount(item), + timestamp: extractTimestamp(item), + memo: item.body?.value || "", + explorerUrl: getExplorerUrl(item), + })); + + setTransactions(txns); + } catch (err) { + setError( + err instanceof Error ? err.message : "Unknown error occurred" + ); + } finally { + setLoading(false); + } + }; + + fetchTransactions(); + }, [isConnected, address]); + + if (!isConnected || !address) { + return ( +
+ +
+
+

Connect Wallet to View History

+

+ You need to connect your Stellar wallet to view your transaction history. +

+
+
+
+ ); + } + + if (loading) { + return ( +
+ +
+
+ Loading transactions... +
+
+ ); + } + + if (error) { + return ( +
+ +
+
+

Error loading transactions: {error}

+
+
+
+ ); + } + + return ( +
+ +
+

Transaction History

+

+ Wallet: {truncateAddress(address)} +

+ + {transactions.length === 0 ? ( +
+ No transactions found for this wallet. +
+ ) : ( +
+ {transactions.map((tx) => ( + + ))} +
+ )} +
+
+ ); +} + +// Helper: map Soroban event type to our transaction type +function mapTransactionType(item: any): Transaction["type"] { + const body = item.body?.value || ""; + if (typeof body === "string" && body.includes("contribute")) return "contribution"; + if (typeof body === "string" && body.includes("payout")) return "payout"; + return "group_join"; +} + +// Helper: format amount +function formatAmount(item: any): string { + const body = item.body?.value || ""; + if (typeof body === "string") { + const match = body.match(/\d+/); + if (match) { + const amount = parseInt(match[0]); + return `${(amount / 10000000).toFixed(7)} XLM`; + } + } + return "0 XLM"; +} + +// Helper: extract timestamp +function extractTimestamp(item: any): number { + return Math.floor((item.id || "").match(/(\d+)/)?.[0] || Date.now() / 1000); +} + +// Helper: get Stellar explorer URL +function getExplorerUrl(item: any): string { + return `https://stellar.expert/explorer/testnet/tx/${item.id || ""}`; +} + +// Helper: truncate address +function truncateAddress(address: string): string { + return `${address.slice(0, 8)}...${address.slice(-8)}`; +} + +// Transaction card component +function TransactionCard({ tx }: { tx: Transaction }) { + const icon = getTransactionIcon(tx.type); + const timeAgo = getTimeAgo(tx.timestamp); + + return ( +
+
+
{icon}
+
+
{tx.type.replace("_", " ")}
+
{truncateAddress(tx.hash)}
+
{timeAgo}
+
+
+
+
{tx.amount}
+ + View on Explorer → + +
+
+ ); +} + +function getTransactionIcon(type: Transaction["type"]): string { + switch (type) { + case "contribution": + return "📥"; + case "payout": + return "📤"; + case "group_join": + return "👥"; + default: + return "🔄"; + } +} + +function getTimeAgo(timestamp: number): string { + if (!timestamp || timestamp === 0) return "Recently"; + const seconds = Math.floor((Date.now() / 1000 - timestamp) / 1000); + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +}