From 7e0508b7a7026957e7dfb4399a847aba60b8a1c7 Mon Sep 17 00:00:00 2001
From: bitstarkbridge
Date: Thu, 22 Jan 2026 15:45:22 +0000
Subject: [PATCH 1/4] Add Recent Contract Events section
- Implement useRecentEvents hook to fetch Soroban contract events via Stellar RPC
- Add Recent Contract Events section in Transaction History view
- Display events with type, ledger, ID, topics, and value (base64)
- Handle loading, error, and empty states
- Include disclosure about 7-day RPC limitation
- Events ordered most recent first
---
package-lock.json | 41 ++++++------
src/components/escrow/EscrowDetails.tsx | 83 +++++++++++++++++++++++
src/hooks/useRecentEvents.ts | 89 +++++++++++++++++++++++++
3 files changed, 192 insertions(+), 21 deletions(-)
create mode 100644 src/hooks/useRecentEvents.ts
diff --git a/package-lock.json b/package-lock.json
index ff4af88..37ce709 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,7 @@
"clsx": "^2.1.1",
"framer-motion": "^12.5.0",
"lucide-react": "^0.482.0",
- "next": "15.3.6",
+ "next": "15.3.8",
"next-themes": "^0.4.6",
"react": "^19.1.2",
"react-dom": "^19.1.2",
@@ -808,10 +808,9 @@
}
},
"node_modules/@next/env": {
- "version": "15.3.6",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.6.tgz",
- "integrity": "sha512-/cK+QPcfRbDZxmI/uckT4lu9pHCfRIPBLqy88MhE+7Vg5hKrEYc333Ae76dn/cw2FBP2bR/GoK/4DU+U7by/Nw==",
- "license": "MIT"
+ "version": "15.3.8",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.8.tgz",
+ "integrity": "sha512-SAfHg0g91MQVMPioeFeDjE+8UPF3j3BvHjs8ZKJAUz1BG7eMPvfCKOAgNWJ6s1MLNeP6O2InKQRTNblxPWuq+Q=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "15.3.6",
@@ -3375,6 +3374,7 @@
"integrity": "sha512-vrdxRZfo9ALXth6yPfV16PYTLZwsUWhVjjC+DkfE5t1suNSbBrWC9YqSuuxJZ8Ps6z1o2ycRpIqzZJIgklq4Tw==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -3385,6 +3385,7 @@
"integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -3425,6 +3426,7 @@
"integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.26.1",
"@typescript-eslint/types": "8.26.1",
@@ -3788,6 +3790,7 @@
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4166,16 +4169,6 @@
"license": "Apache-2.0",
"optional": true
},
- "node_modules/bare-url": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.1.3.tgz",
- "integrity": "sha512-c02+eKvn/4esh5E2lSYQFwHL1WoTIL3u3NeFqb9e7ahBVENXw13MWx4/4/wdPyI557GqqB2Cm0bBbOXD0I0qgA==",
- "license": "Apache-2.0",
- "optional": true,
- "dependencies": {
- "bare-path": "^3.0.0"
- }
- },
"node_modules/base32.js": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz",
@@ -4841,6 +4834,7 @@
"integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5015,6 +5009,7 @@
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
@@ -6802,12 +6797,11 @@
"license": "MIT"
},
"node_modules/next": {
- "version": "15.3.6",
- "resolved": "https://registry.npmjs.org/next/-/next-15.3.6.tgz",
- "integrity": "sha512-oI6D1zbbsh6JzzZFDCSHnnx6Qpvd1fSkVJu/5d8uluqnxzuoqtodVZjYvNovooznUq8udSAiKp7MbwlfZ8Gm6w==",
- "license": "MIT",
+ "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==",
"dependencies": {
- "@next/env": "15.3.6",
+ "@next/env": "15.3.8",
"@swc/counter": "0.1.3",
"@swc/helpers": "0.5.15",
"busboy": "1.6.0",
@@ -7254,6 +7248,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7263,6 +7258,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -8045,7 +8041,8 @@
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.14.tgz",
"integrity": "sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -8104,6 +8101,7 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -8265,6 +8263,7 @@
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/src/components/escrow/EscrowDetails.tsx b/src/components/escrow/EscrowDetails.tsx
index 776fe90..d6d4bfa 100644
--- a/src/components/escrow/EscrowDetails.tsx
+++ b/src/components/escrow/EscrowDetails.tsx
@@ -29,6 +29,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)
@@ -63,6 +64,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
@@ -318,6 +325,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}
+
+
+
+ ))}
+
+ )}
+
+
+
+
)}
diff --git a/src/hooks/useRecentEvents.ts b/src/hooks/useRecentEvents.ts
new file mode 100644
index 0000000..645bede
--- /dev/null
+++ b/src/hooks/useRecentEvents.ts
@@ -0,0 +1,89 @@
+import { useState, useEffect, useCallback } from 'react';
+import { SorobanRpc } from '@stellar/stellar-sdk';
+import { getNetworkConfig, type NetworkType } from '@/lib/network-config';
+
+export interface ContractEvent {
+ id: string;
+ type: string;
+ ledger: number;
+ contractId: string;
+ topics: string[]; // base64 or decoded
+ value: string; // base64 or decoded
+}
+
+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 {
+ const config = getNetworkConfig(network);
+ const server = new SorobanRpc.Server(config.rpcUrl);
+
+ // Get latest ledger
+ const latestLedger = await server.getLatestLedger();
+ const endLedger = latestLedger.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);
+
+ const response = await server.getEvents({
+ startLedger,
+ filters: [
+ {
+ contractIds: [contractId],
+ type: 'contract',
+ },
+ ],
+ limit: 100, // reasonable limit
+ });
+
+ // Map to our interface
+ const mappedEvents: ContractEvent[] = response.events.map((event) => ({
+ id: event.id,
+ type: event.type,
+ ledger: event.ledger,
+ contractId: event.contractId,
+ topics: event.topic.map((topic) => topic.toString('base64')), // keep as base64 for now
+ value: event.value.toString('base64'), // keep as base64
+ }));
+
+ // 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
From a2c00060b86b5fa17398de48f9bf2b92a56bc030 Mon Sep 17 00:00:00 2001
From: bitstarkbridge
Date: Thu, 22 Jan 2026 16:30:52 +0000
Subject: [PATCH 2/4] Force push frontend code
---
src/components/escrow/error-display.tsx | 39 +++++++++++++++++-------
src/components/escrow/escrow-content.tsx | 5 ++-
src/components/escrow/welcome-state.tsx | 12 +++++---
src/components/ui/theme-toggle.tsx | 25 +++++++++------
src/hooks/useEscrowData.ts | 35 +++++++++++++++++++--
src/hooks/useRecentEvents.ts | 16 +++++-----
src/lib/escrow-constants.ts | 2 +-
src/lib/network-config.ts | 2 +-
src/utils/ledgerkeycontract.ts | 2 +-
9 files changed, 98 insertions(+), 40 deletions(-)
diff --git a/src/components/escrow/error-display.tsx b/src/components/escrow/error-display.tsx
index 2a4e452..b00ce76 100644
--- a/src/components/escrow/error-display.tsx
+++ b/src/components/escrow/error-display.tsx
@@ -6,19 +6,36 @@ interface ErrorDisplayProps {
}
export const ErrorDisplay = ({ error }: ErrorDisplayProps) => {
+ if (!error) return null;
+
+ const parts = error.split('•').map(part => part.trim()).filter(part => part);
+
return (
- {error && (
-
-
- {error}
-
- )}
+
+
+
+
+ {parts.length > 1 ? (
+
+
{parts[0]}
+
+ {parts.slice(1).map((line, index) => (
+ - {line}
+ ))}
+
+
+ ) : (
+
{error}
+ )}
+
+
+
)
}
\ 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 cf07b53..b8432fc 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-blue-600 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 4f255a8..29dbafa 100644
--- a/src/components/ui/theme-toggle.tsx
+++ b/src/components/ui/theme-toggle.tsx
@@ -3,27 +3,34 @@
import { useEffect, useState } from "react";
export function ThemeToggle() {
- const [isDark, setIsDark] = useState(() => {
+ const [isDark, setIsDark] = useState(false);
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
try {
- if (typeof window === "undefined") return false;
const stored = localStorage.getItem("theme");
- if (stored) return stored === "dark";
- return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
- } catch {
- return false;
+ if (stored) {
+ setIsDark(stored === "dark");
+ } else {
+ setIsDark(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
+ }
+ } catch {
+ // ignore
}
- });
+ }, []);
useEffect(() => {
+ if (!mounted) return;
const root = document.documentElement;
if (isDark) root.classList.add("dark");
else root.classList.remove("dark");
try {
localStorage.setItem("theme", isDark ? "dark" : "light");
- } catch {
+ } catch {
// ignore
}
- }, [isDark]);
+ }, [isDark, mounted]);
const toggle = () => setIsDark((v) => !v);
diff --git a/src/hooks/useEscrowData.ts b/src/hooks/useEscrowData.ts
index 5a48e3b..3f862b0 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,10 +30,30 @@ 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}•Try switching to ${network === 'testnet' ? 'mainnet' : 'testnet'} or 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);
diff --git a/src/hooks/useRecentEvents.ts b/src/hooks/useRecentEvents.ts
index 645bede..a85f500 100644
--- a/src/hooks/useRecentEvents.ts
+++ b/src/hooks/useRecentEvents.ts
@@ -1,14 +1,14 @@
import { useState, useEffect, useCallback } from 'react';
-import { SorobanRpc } from '@stellar/stellar-sdk';
+import { rpc } from '@stellar/stellar-sdk';
import { getNetworkConfig, type NetworkType } from '@/lib/network-config';
export interface ContractEvent {
id: string;
type: string;
ledger: number;
- contractId: string;
- topics: string[]; // base64 or decoded
- value: string; // base64 or decoded
+ contractId?: string;
+ topics: string[]; // base64
+ value: string; // base64
}
export interface UseRecentEventsResult {
@@ -34,7 +34,7 @@ export function useRecentEvents(
try {
const config = getNetworkConfig(network);
- const server = new SorobanRpc.Server(config.rpcUrl);
+ const server = new rpc.Server(config.rpcUrl);
// Get latest ledger
const latestLedger = await server.getLatestLedger();
@@ -60,9 +60,9 @@ export function useRecentEvents(
id: event.id,
type: event.type,
ledger: event.ledger,
- contractId: event.contractId,
- topics: event.topic.map((topic) => topic.toString('base64')), // keep as base64 for now
- value: event.value.toString('base64'), // keep as base64
+ contractId: event.contractId?.toString(),
+ topics: event.topic.map((topic) => topic.toXDR('base64')),
+ value: event.value.toXDR('base64'),
}));
// Sort by ledger descending (most recent first)
diff --git a/src/lib/escrow-constants.ts b/src/lib/escrow-constants.ts
index 64a1c65..928b535 100644
--- a/src/lib/escrow-constants.ts
+++ b/src/lib/escrow-constants.ts
@@ -86,4 +86,4 @@ export const EXAMPLE_CONTRACT_IDS = {
mainnet: "CANVLF5SPV7LF6YOA2PFFPJQAFEUXEEE7SLKXRHUAMAN65EXFHBDLARP"
};
-export const EXAMPLE_CONTRACT_ID = EXAMPLE_CONTRACT_IDS.testnet;
+export const EXAMPLE_CONTRACT_ID = EXAMPLE_CONTRACT_IDS.mainnet;
diff --git a/src/lib/network-config.ts b/src/lib/network-config.ts
index d820a40..d567334 100644
--- a/src/lib/network-config.ts
+++ b/src/lib/network-config.ts
@@ -27,5 +27,5 @@ export function getNetworkConfig(network: NetworkType): NetworkConfig {
}
export function getDefaultNetwork(): NetworkType {
- return 'testnet';
+ return 'mainnet';
}
\ No newline at end of file
diff --git a/src/utils/ledgerkeycontract.ts b/src/utils/ledgerkeycontract.ts
index bf990db..cbbf559 100644
--- a/src/utils/ledgerkeycontract.ts
+++ b/src/utils/ledgerkeycontract.ts
@@ -67,7 +67,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;
From d64cb4c7e49a745175685a12a03cac9b35c86e8b Mon Sep 17 00:00:00 2001
From: bitstarkbridge
Date: Thu, 22 Jan 2026 16:38:49 +0000
Subject: [PATCH 3/4] Force push frontend codes
---
src/lib/escrow-constants.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/lib/escrow-constants.ts b/src/lib/escrow-constants.ts
index 928b535..64a1c65 100644
--- a/src/lib/escrow-constants.ts
+++ b/src/lib/escrow-constants.ts
@@ -86,4 +86,4 @@ export const EXAMPLE_CONTRACT_IDS = {
mainnet: "CANVLF5SPV7LF6YOA2PFFPJQAFEUXEEE7SLKXRHUAMAN65EXFHBDLARP"
};
-export const EXAMPLE_CONTRACT_ID = EXAMPLE_CONTRACT_IDS.mainnet;
+export const EXAMPLE_CONTRACT_ID = EXAMPLE_CONTRACT_IDS.testnet;
From 338b2de9a1fc5ddf25d56516b6a4dd6f9b74eb2f Mon Sep 17 00:00:00 2001
From: bitstarkbridge
Date: Thu, 22 Jan 2026 22:49:16 +0000
Subject: [PATCH 4/4] Force push backend code
---
src/app/api/rpc/route.ts | 38 +++++++++
src/components/escrow/EscrowDetails.tsx | 25 +++++-
.../escrow/TransactionDetailModal.tsx | 7 +-
src/components/escrow/error-display.tsx | 44 ++++++++++-
src/hooks/useEscrowData.ts | 8 +-
src/hooks/useRecentEvents.ts | 77 ++++++++++++++-----
src/lib/network-config.ts | 2 +-
src/utils/ledgerkeycontract.ts | 7 +-
src/utils/transactionFetcher.ts | 14 ++--
9 files changed, 180 insertions(+), 42 deletions(-)
create mode 100644 src/app/api/rpc/route.ts
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 d6d4bfa..77ae222 100644
--- a/src/components/escrow/EscrowDetails.tsx
+++ b/src/components/escrow/EscrowDetails.tsx
@@ -43,10 +43,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);
@@ -101,7 +113,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]);
@@ -114,7 +126,7 @@ const isMobile = useIsMobile();
setTransactionLoading(false);
}
},
- []
+ [currentNetwork]
);
// Initial + network-change fetch (escrow + txs)
@@ -261,7 +273,11 @@ useEffect(() => {
)}
{/* Error Display */}
-
+
{/* Content Section (hidden when showing transactions as a page) */}
{!showOnlyTransactions && (
@@ -410,6 +426,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 34cd8d2..e42547d 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 b00ce76..34136ac 100644
--- a/src/components/escrow/error-display.tsx
+++ b/src/components/escrow/error-display.tsx
@@ -1,15 +1,29 @@
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 (
{
>
-
+
{parts.length > 1 ? (
{parts[0]}
@@ -33,6 +47,30 @@ export const ErrorDisplay = ({ error }: ErrorDisplayProps) => {
) : (
{error}
)}
+ {/* Action buttons */}
+
+ {targetNetwork && onSwitchNetwork && (
+
+ )}
+ {onRetry && (
+
+ )}
+
diff --git a/src/hooks/useEscrowData.ts b/src/hooks/useEscrowData.ts
index 3f862b0..30b2cc3 100644
--- a/src/hooks/useEscrowData.ts
+++ b/src/hooks/useEscrowData.ts
@@ -47,7 +47,7 @@ export function useEscrowData(contractId: string, network: NetworkType, isMobile
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}•Try switching to ${network === 'testnet' ? 'mainnet' : 'testnet'} or use a different contract ID•The contract was recently deployed and not yet indexed`
+ ? `•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 {
@@ -57,7 +57,11 @@ export function useEscrowData(contractId: string, network: NetworkType, 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
index a85f500..adc25fc 100644
--- a/src/hooks/useRecentEvents.ts
+++ b/src/hooks/useRecentEvents.ts
@@ -1,6 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
-import { rpc } from '@stellar/stellar-sdk';
-import { getNetworkConfig, type NetworkType } from '@/lib/network-config';
+import { type NetworkType } from '@/lib/network-config';
export interface ContractEvent {
id: string;
@@ -33,36 +32,76 @@ export function useRecentEvents(
setError(null);
try {
- const config = getNetworkConfig(network);
- const server = new rpc.Server(config.rpcUrl);
-
// Get latest ledger
- const latestLedger = await server.getLatestLedger();
- const endLedger = latestLedger.sequence;
+ 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);
- const response = await server.getEvents({
- startLedger,
- filters: [
- {
- contractIds: [contractId],
- type: 'contract',
+ // 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,
},
- ],
- limit: 100, // reasonable limit
+ }),
});
+ 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[] = response.events.map((event) => ({
+ 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?.toString(),
- topics: event.topic.map((topic) => topic.toXDR('base64')),
- value: event.value.toXDR('base64'),
+ contractId: event.contractId,
+ topics: event.topic,
+ value: event.value,
}));
// Sort by ledger descending (most recent first)
diff --git a/src/lib/network-config.ts b/src/lib/network-config.ts
index d567334..964dbe4 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'
}
diff --git a/src/utils/ledgerkeycontract.ts b/src/utils/ledgerkeycontract.ts
index cbbf559..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) {
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