diff --git a/package-lock.json b/package-lock.json index 3746bfc..35fc407 100644 --- a/package-lock.json +++ b/package-lock.json @@ -810,8 +810,7 @@ "node_modules/@next/env": { "version": "15.3.8", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.8.tgz", - "integrity": "sha512-SAfHg0g91MQVMPioeFeDjE+8UPF3j3BvHjs8ZKJAUz1BG7eMPvfCKOAgNWJ6s1MLNeP6O2InKQRTNblxPWuq+Q==", - "license": "MIT" + }, "node_modules/@next/eslint-plugin-next": { "version": "15.3.6", @@ -6801,7 +6800,6 @@ "version": "15.3.8", "resolved": "https://registry.npmjs.org/next/-/next-15.3.8.tgz", "integrity": "sha512-L+4c5Hlr84fuaNADZbB9+ceRX9/CzwxJ+obXIGHupboB/Q1OLbSUapFs4bO8hnS/E6zV/JDX7sG1QpKVR2bguA==", - "license": "MIT", "dependencies": { "@next/env": "15.3.8", "@swc/counter": "0.1.3", diff --git a/src/app/api/rpc/route.ts b/src/app/api/rpc/route.ts new file mode 100644 index 0000000..e250995 --- /dev/null +++ b/src/app/api/rpc/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getNetworkConfig } from '@/lib/network-config'; + +export async function POST(request: NextRequest) { + try { + const { network, ...body } = await request.json(); + + if (!network || !['testnet', 'mainnet'].includes(network)) { + return NextResponse.json({ error: 'Invalid network' }, { status: 400 }); + } + + const networkConfig = getNetworkConfig(network as 'testnet' | 'mainnet'); + + const response = await fetch(networkConfig.rpcUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + return NextResponse.json( + { error: `RPC error: ${response.status}` }, + { status: response.status } + ); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + console.error('RPC proxy error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/components/escrow/EscrowDetails.tsx b/src/components/escrow/EscrowDetails.tsx index c6d8a71..0efc326 100644 --- a/src/components/escrow/EscrowDetails.tsx +++ b/src/components/escrow/EscrowDetails.tsx @@ -32,6 +32,7 @@ import { useIsMobile } from "@/hooks/useIsMobile"; // ⬇️ New hooks import { useEscrowData } from "@/hooks/useEscrowData"; import { useTokenBalance } from "@/hooks/useTokenBalance"; +import { useRecentEvents } from "@/hooks/useRecentEvents"; // (useMemo is consolidated in the import above) @@ -45,10 +46,22 @@ const EscrowDetailsClient: React.FC = ({ initialEscrowId, }) => { const router = useRouter(); - const { currentNetwork } = useNetwork(); + const { currentNetwork, setNetwork } = useNetwork(); // Input / responsive state const [contractId, setContractId] = useState(initialEscrowId); + + // Handle network switch, updating contract ID for examples + const handleSwitchNetwork = useCallback((network: 'testnet' | 'mainnet') => { + setNetwork(network); + // If current contract is an example, switch to the example for the new network + if (contractId === EXAMPLE_CONTRACT_IDS.testnet || contractId === EXAMPLE_CONTRACT_IDS.mainnet) { + const newContractId = EXAMPLE_CONTRACT_IDS[network]; + setContractId(newContractId); + // Update URL + router.replace(`/${newContractId}`); + } + }, [contractId, setNetwork, router, setContractId]); const isMobile = useIsMobile(); const [isSearchFocused, setIsSearchFocused] = useState(false); @@ -66,6 +79,12 @@ const isMobile = useIsMobile(); currentNetwork ); + // Recent events hook + const { events, loading: eventsLoading, error: eventsError } = useRecentEvents( + contractId, + currentNetwork + ); + const organizedWithLive = useMemo(() => { if (!organized) return null; if (!ledgerBalance) return organized; // nothing to override @@ -97,7 +116,7 @@ const isMobile = useIsMobile(); setTransactionLoading(true); setTransactionError(null); try { - const response = await fetchTransactions(id, { cursor, limit: 20 }); + const response = await fetchTransactions(id, currentNetwork, { cursor, limit: 20 }); setTransactionResponse(response); if (cursor) { setTransactions((prev) => [...prev, ...response.transactions]); @@ -110,7 +129,7 @@ const isMobile = useIsMobile(); setTransactionLoading(false); } }, - [] + [currentNetwork] ); // Initial + network-change fetch (escrow + txs) @@ -292,7 +311,11 @@ useEffect(() => { )} {/* Error Display */} - + {/* Content Section (hidden when showing transactions as a page) */} {!showOnlyTransactions && ( @@ -356,6 +379,82 @@ useEffect(() => { + + {/* Recent Contract Events Section */} + +
+

Recent Contract Events

+
+ +
+

Last 7 days (RPC-limited)

+

+ This view shows only events available via Stellar RPC (last ~7 days). + Historical events will be available in future versions. +

+
+ +
+ + +
+ {eventsLoading ? ( +
+

Loading recent events...

+
+ ) : eventsError ? ( +
+

Unable to fetch recent events from Stellar RPC.

+
+ ) : events.length === 0 ? ( +
+

No contract events found in the last 7 days.

+
+ ) : ( +
+ {events.map((event) => ( +
+
+ Event Type: {event.type} + Ledger: {event.ledger} +
+
+ ID: {event.id} +
+
+ Topics: +
+ {event.topics.join(', ')} +
+
+
+ Value: +
+ {event.value} +
+
+
+ ))} +
+ )} +
+
+
+
)} @@ -365,6 +464,7 @@ useEffect(() => { isOpen={isModalOpen} onClose={handleModalClose} isMobile={isMobile} + network={currentNetwork} /> diff --git a/src/components/escrow/TransactionDetailModal.tsx b/src/components/escrow/TransactionDetailModal.tsx index 0dc6865..88f9eeb 100644 --- a/src/components/escrow/TransactionDetailModal.tsx +++ b/src/components/escrow/TransactionDetailModal.tsx @@ -29,12 +29,14 @@ import { formatTransactionTime, truncateHash, } from "@/utils/transactionFetcher"; +import { NetworkType } from "@/lib/network-config"; interface TransactionDetailModalProps { txHash: string | null; isOpen: boolean; onClose: () => void; isMobile: boolean; + network: NetworkType; } export const TransactionDetailModal: React.FC = ({ @@ -42,6 +44,7 @@ export const TransactionDetailModal: React.FC = ({ isOpen, onClose, isMobile, + network, }) => { const [details, setDetails] = useState(null); const [loading, setLoading] = useState(false); @@ -54,7 +57,7 @@ export const TransactionDetailModal: React.FC = ({ setError(null); try { - const transactionDetails = await fetchTransactionDetails(txHash); + const transactionDetails = await fetchTransactionDetails(txHash, network); setDetails(transactionDetails); } catch (err) { setError("Failed to fetch transaction details"); @@ -62,7 +65,7 @@ export const TransactionDetailModal: React.FC = ({ } finally { setLoading(false); } - }, [txHash]); + }, [txHash, network]); const copyToClipboard = async (text: string) => { try { diff --git a/src/components/escrow/error-display.tsx b/src/components/escrow/error-display.tsx index 2a4e452..34136ac 100644 --- a/src/components/escrow/error-display.tsx +++ b/src/components/escrow/error-display.tsx @@ -1,24 +1,79 @@ import { motion, AnimatePresence } from "framer-motion" -import { AlertCircle } from "lucide-react" +import { AlertCircle, RefreshCw } from "lucide-react" +import { Button } from "@/components/ui/button" interface ErrorDisplayProps { error: string | null + onSwitchNetwork?: (network: 'testnet' | 'mainnet') => void + onRetry?: () => void } -export const ErrorDisplay = ({ error }: ErrorDisplayProps) => { +export const ErrorDisplay = ({ error, onSwitchNetwork, onRetry }: ErrorDisplayProps) => { + if (!error) return null; + + const parts = error.split('•').map(part => part.trim()).filter(part => part); + + // Check if error suggests switching networks + const switchSuggestion = parts.find(part => part.includes('Try switching to')); + let targetNetwork: 'testnet' | 'mainnet' | null = null; + if (switchSuggestion) { + if (switchSuggestion.includes('mainnet')) { + targetNetwork = 'mainnet'; + } else if (switchSuggestion.includes('testnet')) { + targetNetwork = 'testnet'; + } + } + return ( - {error && ( - - -

{error}

-
- )} + +
+ +
+ {parts.length > 1 ? ( +
+

{parts[0]}

+
    + {parts.slice(1).map((line, index) => ( +
  • {line}
  • + ))} +
+
+ ) : ( +

{error}

+ )} + {/* Action buttons */} +
+ {targetNetwork && onSwitchNetwork && ( + + )} + {onRetry && ( + + )} +
+
+
+
) } \ No newline at end of file diff --git a/src/components/escrow/escrow-content.tsx b/src/components/escrow/escrow-content.tsx index 1ce543c..0c119e3 100644 --- a/src/components/escrow/escrow-content.tsx +++ b/src/components/escrow/escrow-content.tsx @@ -6,8 +6,7 @@ import { TitleCard } from "@/components/escrow/title-card" import { TabView } from "@/components/escrow/tab-view" import { DesktopView } from "@/components/escrow/desktop-view" import { WelcomeState } from "@/components/escrow/welcome-state" -import error from "next/error" -import { useNetwork } from "@/contexts/NetworkContext"; // Add this line +import { useNetwork } from "@/contexts/NetworkContext"; interface EscrowContentProps { @@ -70,7 +69,7 @@ export const EscrowContent = ({ {/* No data state */} - + ) } diff --git a/src/components/escrow/welcome-state.tsx b/src/components/escrow/welcome-state.tsx index 51bba43..650d970 100644 --- a/src/components/escrow/welcome-state.tsx +++ b/src/components/escrow/welcome-state.tsx @@ -1,17 +1,21 @@ import { motion, AnimatePresence } from "framer-motion" import { Button } from "@/components/ui/button" -import { EXAMPLE_CONTRACT_ID } from "@/lib/escrow-constants" +import { EXAMPLE_CONTRACT_IDS } from "@/lib/escrow-constants" +import type { NetworkType } from "@/lib/network-config" interface WelcomeStateProps { showWelcome: boolean; handleUseExample?: () => void; + network?: NetworkType; } -export const WelcomeState = ({ +export const WelcomeState = ({ showWelcome, - handleUseExample + handleUseExample, + network = 'mainnet' }: WelcomeStateProps) => { const useExample = handleUseExample || (() => {}); + const exampleId = EXAMPLE_CONTRACT_IDS[network]; return ( @@ -55,7 +59,7 @@ export const WelcomeState = ({ className="text-sm text-primary p-0 h-auto" onClick={useExample} > - {EXAMPLE_CONTRACT_ID} + {exampleId}

diff --git a/src/components/ui/theme-toggle.tsx b/src/components/ui/theme-toggle.tsx index 21a36b0..b46311c 100644 --- a/src/components/ui/theme-toggle.tsx +++ b/src/components/ui/theme-toggle.tsx @@ -4,17 +4,7 @@ import { useEffect, useState } from "react"; import { safeLocalStorage } from "@/utils/storage"; export function ThemeToggle() { - const [isDark, setIsDark] = useState(false); - const [mounted, setMounted] = useState(false); - // Only access localStorage after component mounts (client-side only) - useEffect(() => { - setMounted(true); - const stored = safeLocalStorage.getItem("theme"); - if (stored) { - setIsDark(stored === "dark"); - } else if (typeof window !== 'undefined' && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { - setIsDark(true); } }, []); @@ -22,12 +12,7 @@ export function ThemeToggle() { useEffect(() => { if (!mounted) return; const root = document.documentElement; - if (isDark) { - root.classList.add("dark"); - } else { - root.classList.remove("dark"); - } - safeLocalStorage.setItem("theme", isDark ? "dark" : "light"); + }, [isDark, mounted]); const toggle = () => setIsDark((v) => !v); diff --git a/src/hooks/useEscrowData.ts b/src/hooks/useEscrowData.ts index 5a48e3b..30b2cc3 100644 --- a/src/hooks/useEscrowData.ts +++ b/src/hooks/useEscrowData.ts @@ -3,6 +3,17 @@ import { useCallback, useEffect, useState } from "react"; import { getLedgerKeyContractCode, type EscrowMap } from "@/utils/ledgerkeycontract"; import { organizeEscrowData, type OrganizedEscrowData } from "@/mappers/escrow-mapper"; import type { NetworkType } from "@/lib/network-config"; +import { EXAMPLE_CONTRACT_IDS } from "@/lib/escrow-constants"; + +/** + * Basic validation for Stellar contract ID format + */ +function isValidContractId(contractId: string): boolean { + // Stellar contract IDs are 56 characters long and use base32 alphabet + if (contractId.length !== 56) return false; + const base32Regex = /^[A-Z2-7]+$/; + return base32Regex.test(contractId); +} /** * Loads raw escrow contract storage and maps it to OrganizedEscrowData for UI. @@ -19,14 +30,38 @@ export function useEscrowData(contractId: string, network: NetworkType, isMobile setLoading(true); setError(null); + // Validate contract ID format + if (!isValidContractId(contractId)) { + setRaw(null); + setOrganized(null); + setError(`Invalid contract ID format. Stellar contract IDs should be 56 characters long and contain only uppercase letters A-Z and digits 2-7.`); + setLoading(false); + return; + } + try { const data = await getLedgerKeyContractCode(contractId, network); - setRaw(data); - setOrganized(organizeEscrowData(data, contractId, isMobile)); + if (data.length === 0) { + setRaw(null); + setOrganized(null); + const isExampleContract = contractId === EXAMPLE_CONTRACT_IDS.testnet || contractId === EXAMPLE_CONTRACT_IDS.mainnet; + const baseMessage = `No ledger entry found for contract ID "${contractId}".`; + const suggestions = isExampleContract + ? `•This example contract may not be deployed on ${network}•Use a different contract ID•The contract was recently deployed and not yet indexed` + : `•The contract ID is invalid or doesn't exist on ${network}•The contract exists on a different network•The contract was recently deployed and not yet indexed`; + setError(`${baseMessage}${suggestions}`); + } else { + setRaw(data); + setOrganized(organizeEscrowData(data, contractId, isMobile)); + } } catch (e) { setRaw(null); setOrganized(null); - setError(e instanceof Error ? e.message : "Failed to fetch escrow data"); + const errorMessage = e instanceof Error ? e.message : "Failed to fetch escrow data"; + const userFriendlyMessage = errorMessage === "Failed to fetch" + ? "Network error: Unable to connect to Stellar RPC. Please check your internet connection or try again later." + : errorMessage; + setError(userFriendlyMessage); } finally { setLoading(false); } diff --git a/src/hooks/useRecentEvents.ts b/src/hooks/useRecentEvents.ts new file mode 100644 index 0000000..adc25fc --- /dev/null +++ b/src/hooks/useRecentEvents.ts @@ -0,0 +1,128 @@ +import { useState, useEffect, useCallback } from 'react'; +import { type NetworkType } from '@/lib/network-config'; + +export interface ContractEvent { + id: string; + type: string; + ledger: number; + contractId?: string; + topics: string[]; // base64 + value: string; // base64 +} + +export interface UseRecentEventsResult { + events: ContractEvent[]; + loading: boolean; + error: string | null; + refetch: () => void; +} + +export function useRecentEvents( + contractId: string, + network: NetworkType +): UseRecentEventsResult { + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchEvents = useCallback(async () => { + if (!contractId) return; + + setLoading(true); + setError(null); + + try { + // Get latest ledger + const latestLedgerResponse = await fetch('/api/rpc', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + network, + jsonrpc: '2.0', + id: 1, + method: 'getLatestLedger', + }), + }); + + if (!latestLedgerResponse.ok) { + throw new Error('Failed to get latest ledger'); + } + + const latestLedgerData = await latestLedgerResponse.json(); + const endLedger = latestLedgerData.result.sequence; + + // Approximate 7 days: ~5 seconds per ledger, 7*24*3600/5 ≈ 12096 + const sevenDaysLedgers = Math.floor((7 * 24 * 3600) / 5); + const startLedger = Math.max(1, endLedger - sevenDaysLedgers); + + // Get events + const eventsResponse = await fetch('/api/rpc', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + network, + jsonrpc: '2.0', + id: 2, + method: 'getEvents', + params: { + startLedger, + filters: [ + { + contractIds: [contractId], + type: 'contract', + }, + ], + limit: 100, + }, + }), + }); + + if (!eventsResponse.ok) { + throw new Error('Failed to get events'); + } + + const eventsData = await eventsResponse.json(); + + if (eventsData.error) { + throw new Error(eventsData.error.message || 'Failed to fetch events'); + } + + // Map to our interface + const mappedEvents: ContractEvent[] = (eventsData.result?.events || []).map((event: { + id: string; + type: string; + ledger: number; + contractId?: string; + topic: string[]; + value: string; + }) => ({ + id: event.id, + type: event.type, + ledger: event.ledger, + contractId: event.contractId, + topics: event.topic, + value: event.value, + })); + + // Sort by ledger descending (most recent first) + mappedEvents.sort((a, b) => b.ledger - a.ledger); + + setEvents(mappedEvents); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Failed to fetch recent events'); + } finally { + setLoading(false); + } + }, [contractId, network]); + + useEffect(() => { + fetchEvents(); + }, [fetchEvents]); + + return { + events, + loading, + error, + refetch: fetchEvents, + }; +} \ No newline at end of file diff --git a/src/lib/network-config.ts b/src/lib/network-config.ts index fc13b4b..692fb52 100644 --- a/src/lib/network-config.ts +++ b/src/lib/network-config.ts @@ -16,7 +16,7 @@ export const NETWORK_CONFIGS: Record = { }, mainnet: { name: 'Mainnet', - rpcUrl: 'https://stellar.api.onfinality.io/public', + rpcUrl: 'https://soroban-mainnet.stellar.org', horizonUrl: 'https://horizon.stellar.org', networkPassphrase: 'Public Global Stellar Network ; September 2015' } @@ -27,85 +27,5 @@ export function getNetworkConfig(network: NetworkType): NetworkConfig { } export function getDefaultNetwork(): NetworkType { - return 'testnet'; -} -/** - * Generates a Stellar Lab URL for the contract explorer - * - * Stellar Lab URL format: - * https://lab.stellar.org/smart-contracts/contract-explorer? - * $=network - * &id={network} - * &label={networkLabel} - * &horizonUrl={horizonUrl} - * &rpcUrl={rpcUrl} - * &passphrase={urlEncodedPassphrase} - * &smartContracts$explorer$contractId={contractId} - * - * Note: We manually construct the URL string instead of using URLSearchParams - * because Stellar Lab requires dollar signs ($) to remain unencoded in the - * parameter names (e.g., smartContracts$explorer$contractId), and the passphrase - * should only be encoded once, not double-encoded. - * - * @param network - The network type (testnet or mainnet) - * @param contractId - The contract ID to open in Stellar Lab - * @returns The complete Stellar Lab URL - */ -export function getStellarLabUrl(network: NetworkType, contractId: string): string { - // CONTRACT ID IS REQUIRED - throw error if not provided - if (!contractId || typeof contractId !== 'string' || contractId.trim() === '') { - throw new Error(`getStellarLabUrl: contractId is required but was: ${JSON.stringify(contractId)}`); - } - - const config = getNetworkConfig(network); - const trimmedContractId = contractId.trim(); - - // Base URL for Stellar Lab contract explorer - const baseUrl = 'https://lab.stellar.org/smart-contracts/contract-explorer'; - - // Stellar Lab uses specific RPC endpoints - // Mainnet uses Ankr RPC, testnet uses official Stellar endpoint - // Note: URLs use double slashes (https:////) as required by Stellar Lab - const labRpcUrl = network === 'testnet' - ? 'https:////soroban-testnet.stellar.org' - : 'https:////rpc.ankr.com//stellar_soroban'; - - // Horizon URLs also use double slashes - const labHorizonUrl = network === 'testnet' - ? 'https:////horizon-testnet.stellar.org' - : 'https:////horizon.stellar.org'; - - // URL encode individual values (but not the parameter names with $) - // The passphrase format includes /; before the date and trailing semicolon - // Note: The trailing semicolon in passphrase becomes %3B when encoded - let encodedPassphrase: string; - if (network === 'mainnet') { - // Mainnet passphrase: "Public Global Stellar Network /; September 2015;" - // When encoded: "Public%20Global%20Stellar%20Network%20/;%20September%202015;" - // The /; stays as /; (not encoded), the trailing ; becomes part of the encoded value - encodedPassphrase = encodeURIComponent('Public Global Stellar Network /; September 2015;'); - } else { - // Testnet passphrase: "Test SDF Network /; September 2015;" - encodedPassphrase = encodeURIComponent('Test SDF Network /; September 2015;'); - } - - const encodedLabel = encodeURIComponent(config.name); - - // Stellar Lab URL format uses $=network$id={network} (no & between $=network and id) - // Passphrase ends with ; (encoded as %3B), then &, then contract ID, then ;; at the very end - // CONTRACT ID IS REQUIRED - always include it - const params: string[] = [ - `$=network$id=${network}`, // Special format: $=network$id (no & separator) - `label=${encodedLabel}`, - `horizonUrl=${labHorizonUrl}`, // Double slashes preserved - `rpcUrl=${labRpcUrl}`, // Double slashes preserved - `passphrase=${encodedPassphrase}`, // Ends with ; (encoded as %3B) - `smartContracts$explorer$contractId=${trimmedContractId}`, // Dollar signs must remain unencoded - REQUIRED - ]; - - const queryString = params.join('&') + ';;'; // Trailing ;; at the very end - const finalUrl = `${baseUrl}?${queryString}`; - - return finalUrl; } \ No newline at end of file diff --git a/src/utils/ledgerkeycontract.ts b/src/utils/ledgerkeycontract.ts index bf990db..479557d 100644 --- a/src/utils/ledgerkeycontract.ts +++ b/src/utils/ledgerkeycontract.ts @@ -1,5 +1,5 @@ import { Contract } from "@stellar/stellar-sdk"; -import { NetworkType, getNetworkConfig } from "@/lib/network-config"; +import { NetworkType } from "@/lib/network-config"; // Define types for the escrow data map interface EscrowKey { @@ -46,13 +46,12 @@ export async function getLedgerKeyContractCode( }, }; - const networkConfig = getNetworkConfig(network); - const res = await fetch(networkConfig.rpcUrl, { + const res = await fetch('/api/rpc', { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify(requestBody), + body: JSON.stringify({ network, ...requestBody }), }); if (!res.ok) { @@ -67,7 +66,7 @@ export async function getLedgerKeyContractCode( const entry = json.result.entries[0]; if (!entry) { - throw new Error("No ledger entry found for this contract ID"); + return []; } const contractData = entry?.dataJson?.contract_data?.val?.contract_instance; diff --git a/src/utils/transactionFetcher.ts b/src/utils/transactionFetcher.ts index bb720a5..078ef7b 100644 --- a/src/utils/transactionFetcher.ts +++ b/src/utils/transactionFetcher.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Contract } from "@stellar/stellar-sdk"; +import { NetworkType } from "@/lib/network-config"; // Types for transaction data export interface TransactionMetadata { @@ -38,14 +39,13 @@ export interface TransactionResponse { retentionNotice?: string; } -const SOROBAN_RPC_URL = process.env.NEXT_PUBLIC_SOROBAN_RPC_URL || "https://soroban-testnet.stellar.org"; - /** * Fetches recent transactions for a contract using Soroban JSON-RPC * Gracefully handles retention-related errors */ export async function fetchTransactions( contractId: string, + network: NetworkType, options: FetchTransactionsOptions = {} ): Promise { try { @@ -72,12 +72,12 @@ export async function fetchTransactions( } }; - const response = await fetch(SOROBAN_RPC_URL, { + const response = await fetch('/api/rpc', { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify(requestBody), + body: JSON.stringify({ network, ...requestBody }), }); if (!response.ok) { @@ -138,7 +138,7 @@ export async function fetchTransactions( * Fetches detailed information for a specific transaction * Returns full details with XDR decoded as JSON */ -export async function fetchTransactionDetails(txHash: string): Promise { +export async function fetchTransactionDetails(txHash: string, network: NetworkType): Promise { try { const requestBody = { jsonrpc: "2.0", @@ -149,12 +149,12 @@ export async function fetchTransactionDetails(txHash: string): Promise