diff --git a/src/app/history/page.tsx b/src/app/history/page.tsx new file mode 100644 index 0000000..decc75a --- /dev/null +++ b/src/app/history/page.tsx @@ -0,0 +1,269 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Navbar } from "@/components/Navbar"; +import { useWallet } from "@/app/providers"; + +type HorizonOperation = { + id: string; + type: string; + created_at: string; + transaction_hash: string; + from?: string; + to?: string; + funder?: string; + account?: string; + amount?: string; + asset_type?: string; + asset_code?: string; +}; + +type HorizonOperationsResponse = { + _embedded?: { + records?: HorizonOperation[]; + }; +}; + +type HistoryItem = { + id: string; + kind: "Contribution" | "Payout" | "Group Join"; + timestamp: string; + hash: string; + amount: string; + asset: string; +}; + +const HORIZON_URL = + process.env.NEXT_PUBLIC_HORIZON_URL ?? "https://horizon-testnet.stellar.org"; +const STELLAR_EXPERT_NETWORK = + process.env.NEXT_PUBLIC_STELLAR_EXPERT_NETWORK ?? "testnet"; + +function getExplorerUrl(hash: string) { + return `https://stellar.expert/explorer/${STELLAR_EXPERT_NETWORK}/tx/${hash}`; +} + +function getHistoryKind(operation: HorizonOperation, address: string) { + if (operation.from === address || operation.funder === address) { + return "Contribution" as const; + } + + if (operation.to === address) { + return "Payout" as const; + } + + return "Group Join" as const; +} + +function formatAmount(operation: HorizonOperation) { + if (!operation.amount) return "N/A"; + return operation.amount; +} + +function formatAsset(operation: HorizonOperation) { + if (!operation.amount) return "N/A"; + if (operation.asset_type === "native") return "XLM"; + return operation.asset_code ?? "Token"; +} + +function mapOperationToHistoryItem( + operation: HorizonOperation, + address: string +): HistoryItem { + return { + id: operation.id, + kind: getHistoryKind(operation, address), + timestamp: operation.created_at, + hash: operation.transaction_hash, + amount: formatAmount(operation), + asset: formatAsset(operation), + }; +} + +function formatTimestamp(timestamp: string) { + return new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "short", + }).format(new Date(timestamp)); +} + +function shortenHash(hash: string) { + return `${hash.slice(0, 8)}...${hash.slice(-8)}`; +} + +export default function HistoryPage() { + const { address, isConnected, connect, isFreighterAvailable } = useWallet(); + const [items, setItems] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const historyUrl = useMemo(() => { + if (!address) return null; + + const url = new URL(`${HORIZON_URL}/accounts/${address}/operations`); + url.searchParams.set("order", "desc"); + url.searchParams.set("limit", "50"); + url.searchParams.set("include_failed", "false"); + return url.toString(); + }, [address]); + + const loadHistory = useCallback(async () => { + if (!historyUrl || !address) return; + + setIsLoading(true); + setError(null); + + try { + const response = await fetch(historyUrl); + + if (response.status === 404) { + setItems([]); + return; + } + + if (!response.ok) { + throw new Error("Unable to load transaction history."); + } + + const data = (await response.json()) as HorizonOperationsResponse; + const records = data._embedded?.records ?? []; + + setItems( + records.map((record) => mapOperationToHistoryItem(record, address)) + ); + } catch (err) { + setError( + err instanceof Error ? err.message : "Unable to load transaction history." + ); + } finally { + setIsLoading(false); + } + }, [address, historyUrl]); + + useEffect(() => { + loadHistory(); + }, [loadHistory]); + + return ( + <> + +
+
+
+

+ Transaction History +

+

+ Contributions, payouts, and group join activity from your Stellar + wallet. +

+
+ + {isConnected && ( + + )} +
+ + {!isConnected ? ( +
+

+ Connect your wallet +

+

+ Connect Freighter to load your recent SoroSave-related Stellar + activity. +

+ {isFreighterAvailable ? ( + + ) : ( + + Install Freighter + + )} +
+ ) : ( +
+ {error && ( +
+ {error} +
+ )} + + {isLoading && items.length === 0 ? ( +
+ Loading transaction history... +
+ ) : items.length === 0 ? ( +
+ No transactions found for this wallet. +
+ ) : ( +
+ + + + + + + + + + + {items.map((item) => ( + + + + + + + ))} + +
+ Type + + Timestamp + + Amount + + Transaction Hash +
+ + {item.kind} + + + {formatTimestamp(item.timestamp)} + + {item.amount} {item.asset} + + + {shortenHash(item.hash)} + +
+
+ )} +
+ )} +
+ + ); +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 2d673aa..9b1e5b3 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -25,6 +25,12 @@ export function Navbar() { > Create Group + + History +