Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions src/app/api/rpc/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
);
}
}
108 changes: 104 additions & 4 deletions src/components/escrow/EscrowDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand All @@ -45,10 +46,22 @@ const EscrowDetailsClient: React.FC<EscrowDetailsClientProps> = ({
initialEscrowId,
}) => {
const router = useRouter();
const { currentNetwork } = useNetwork();
const { currentNetwork, setNetwork } = useNetwork();

// Input / responsive state
const [contractId, setContractId] = useState<string>(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<boolean>(false);

Expand All @@ -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
Expand Down Expand Up @@ -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]);
Expand All @@ -110,7 +129,7 @@ const isMobile = useIsMobile();
setTransactionLoading(false);
}
},
[]
[currentNetwork]
);

// Initial + network-change fetch (escrow + txs)
Expand Down Expand Up @@ -292,7 +311,11 @@ useEffect(() => {
)}

{/* Error Display */}
<ErrorDisplay error={error} />
<ErrorDisplay
error={error}
onSwitchNetwork={handleSwitchNetwork}
onRetry={refresh}
/>

{/* Content Section (hidden when showing transactions as a page) */}
{!showOnlyTransactions && (
Expand Down Expand Up @@ -356,6 +379,82 @@ useEffect(() => {
</div>
</motion.div>
</div>

{/* Recent Contract Events Section */}
<motion.div
className="mt-8"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.4 }}
>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl md:text-3xl font-bold">Recent Contract Events</h2>
</div>

<div className="mb-4">
<p className="text-sm text-gray-600 dark:text-[#6fbfe6]">Last 7 days (RPC-limited)</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
This view shows only events available via Stellar RPC (last ~7 days).
Historical events will be available in future versions.
</p>
</div>

<div>
<motion.div
className="relative"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.05, duration: 0.4 }}
>
<motion.div
className="absolute inset-0 bg-gradient-to-br from-blue-50/30 via-transparent to-purple-50/30 rounded-3xl -z-10"
animate={{ backgroundPosition: ["0% 0%", "100% 100%", "0% 0%"] }}
transition={{ duration: 25, repeat: Infinity, ease: "linear" }}
/>
<div className="relative bg-white/95 dark:bg-[#070708] backdrop-blur-sm rounded-3xl shadow-2xl border border-gray-200/60 dark:border-[rgba(255,255,255,0.06)] dark:text-[#BFEFFD] overflow-hidden hover:shadow-3xl transition-all duration-700 p-6">
{eventsLoading ? (
<div className="text-center py-8">
<p className="text-gray-500">Loading recent events...</p>
</div>
) : eventsError ? (
<div className="text-center py-8">
<p className="text-red-500">Unable to fetch recent events from Stellar RPC.</p>
</div>
) : events.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-500">No contract events found in the last 7 days.</p>
</div>
) : (
<div className="space-y-4">
{events.map((event) => (
<div key={event.id} className="border-b border-gray-200 dark:border-gray-700 pb-4 last:border-b-0">
<div className="flex justify-between items-start mb-2">
<span className="font-semibold text-sm">Event Type: {event.type}</span>
<span className="text-xs text-gray-500">Ledger: {event.ledger}</span>
</div>
<div className="text-xs text-gray-600 dark:text-gray-400 mb-2">
ID: {event.id}
</div>
<div className="mb-2">
<span className="font-medium text-xs">Topics:</span>
<div className="text-xs font-mono bg-gray-100 dark:bg-gray-800 p-2 rounded mt-1">
{event.topics.join(', ')}
</div>
</div>
<div>
<span className="font-medium text-xs">Value:</span>
<div className="text-xs font-mono bg-gray-100 dark:bg-gray-800 p-2 rounded mt-1 break-all">
{event.value}
</div>
</div>
</div>
))}
</div>
)}
</div>
</motion.div>
</div>
</motion.div>
</motion.div>
)}

Expand All @@ -365,6 +464,7 @@ useEffect(() => {
isOpen={isModalOpen}
onClose={handleModalClose}
isMobile={isMobile}
network={currentNetwork}
/>
</main>
</div>
Expand Down
7 changes: 5 additions & 2 deletions src/components/escrow/TransactionDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,22 @@ 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<TransactionDetailModalProps> = ({
txHash,
isOpen,
onClose,
isMobile,
network,
}) => {
const [details, setDetails] = useState<TransactionDetails | null>(null);
const [loading, setLoading] = useState(false);
Expand All @@ -54,15 +57,15 @@ export const TransactionDetailModal: React.FC<TransactionDetailModalProps> = ({
setError(null);

try {
const transactionDetails = await fetchTransactionDetails(txHash);
const transactionDetails = await fetchTransactionDetails(txHash, network);
setDetails(transactionDetails);
} catch (err) {
setError("Failed to fetch transaction details");
console.error("Error fetching transaction details:", err);
} finally {
setLoading(false);
}
}, [txHash]);
}, [txHash, network]);

const copyToClipboard = async (text: string) => {
try {
Expand Down
81 changes: 68 additions & 13 deletions src/components/escrow/error-display.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<AnimatePresence>
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="max-w-2xl mx-auto mb-6 p-4 bg-red-50 text-red-800 rounded-md flex items-center gap-2 shadow-sm border border-red-100"
>
<AlertCircle size={18} />
<p>{error}</p>
</motion.div>
)}
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="max-w-2xl mx-auto mb-6 p-4 bg-red-50 text-red-800 rounded-md shadow-sm border border-red-100"
>
<div className="flex items-start gap-2">
<AlertCircle size={18} className="mt-0.5 flex-shrink-0" />
<div className="flex-1">
{parts.length > 1 ? (
<div>
<p className="font-medium mb-2">{parts[0]}</p>
<ul className="list-disc list-inside space-y-1 text-sm">
{parts.slice(1).map((line, index) => (
<li key={index}>{line}</li>
))}
</ul>
</div>
) : (
<p>{error}</p>
)}
{/* Action buttons */}
<div className="flex gap-2 mt-3">
{targetNetwork && onSwitchNetwork && (
<Button
onClick={() => onSwitchNetwork(targetNetwork!)}
size="sm"
variant="outline"
className="bg-white hover:bg-gray-50 border-red-200 text-red-700"
>
Switch to {targetNetwork === 'testnet' ? 'Testnet' : 'Mainnet'}
</Button>
)}
{onRetry && (
<Button
onClick={onRetry}
size="sm"
variant="outline"
className="bg-white hover:bg-gray-50 border-red-200 text-red-700"
>
<RefreshCw size={14} className="mr-1" />
Retry
</Button>
)}
</div>
</div>
</div>
</motion.div>
</AnimatePresence>
)
}
5 changes: 2 additions & 3 deletions src/components/escrow/escrow-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -70,7 +69,7 @@ export const EscrowContent = ({
</AnimatePresence>

{/* No data state */}
<WelcomeState showWelcome={!organized && !loading && !error} />
<WelcomeState showWelcome={!organized && !loading} network={currentNetwork} />
</div>
)
}
Loading