diff --git a/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/ContractActivityCard.tsx b/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/ContractActivityCard.tsx
new file mode 100644
index 000000000..4ce074b6f
--- /dev/null
+++ b/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/ContractActivityCard.tsx
@@ -0,0 +1,122 @@
+"use client";
+
+import { rpc as StellarRpc, xdr } from "@stellar/stellar-sdk";
+import { Badge, Icon } from "@stellar/design-system";
+
+import { SdsLink } from "@/components/SdsLink";
+import { Routes } from "@/constants/routes";
+
+import { ContractActivityJson } from "./ContractActivityJson";
+import {
+ formatRelativeTime,
+ parseScVal,
+ truncateHash,
+ ParsedScVal,
+} from "./helpers";
+
+/**
+ * Renders a single contract event as a card with header, data rows, and
+ * collapsible JSON.
+ *
+ * @example
+ *
+ */
+export const ContractActivityCard = ({
+ event,
+}: {
+ event: StellarRpc.Api.EventResponse;
+}) => {
+ const txDashboardUrl = `${Routes.TRANSACTION_DASHBOARD}?txHash=${event.txHash}`;
+
+ // Parse topics and value
+ const topicRows: ParsedScVal[] = event.topic.map((t: xdr.ScVal) =>
+ parseScVal(t),
+ );
+ const valueRow: ParsedScVal = parseScVal(event.value);
+
+ // Build JSON for the collapsible viewer
+ const eventJson = JSON.stringify(
+ {
+ id: event.id,
+ type: event.type,
+ ledger: event.ledger,
+ ledgerClosedAt: event.ledgerClosedAt,
+ contractId: event.contractId?.toString(),
+ txHash: event.txHash,
+ topic: event.topic.map((t: xdr.ScVal) => t.toXDR("base64")),
+ value: event.value.toXDR("base64"),
+ },
+ null,
+ 2,
+ );
+
+ return (
+
+ {/* Header row */}
+
+
+ }
+ iconPosition="right"
+ >
+ {truncateHash(event.txHash)}
+
+
+
+
+ {formatRelativeTime(event.ledgerClosedAt)}
+
+
+
+ {/* Event body (gray box) */}
+
+ {/* Topics */}
+ {topicRows.map((row, i) => (
+
+ ))}
+
+ {/* Value */}
+
+
+ {/* Transaction hash */}
+
+ trx hash
+ {event.txHash}
+
+
+
+ {/* Collapsible JSON */}
+
+
+ );
+};
+
+/**
+ * Renders a single data row for a parsed ScVal (topic or value).
+ */
+const EventDataRow = ({ row }: { row: ParsedScVal }) => {
+ return (
+
+ {row.label}
+
+ {row.isAddress ? (
+
+ {row.displayValue}
+
+ ) : (
+ {row.displayValue}
+ )}
+ {row.typeAnnotation ? (
+
+ {row.typeAnnotation}
+
+ ) : null}
+
+
+ );
+};
diff --git a/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/ContractActivityJson.tsx b/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/ContractActivityJson.tsx
new file mode 100644
index 000000000..f6ec6f473
--- /dev/null
+++ b/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/ContractActivityJson.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import { useState } from "react";
+import { Icon } from "@stellar/design-system";
+
+import { CodeEditor } from "@/components/CodeEditor";
+
+/**
+ * Collapsible JSON viewer for a single contract event.
+ * Collapsed by default, toggled via "View JSON" link.
+ *
+ * @example
+ *
+ */
+export const ContractActivityJson = ({
+ eventJson,
+}: {
+ eventJson: string;
+}) => {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ return (
+
+
+
+ {isExpanded ? (
+
+ ) : null}
+
+ );
+};
diff --git a/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/helpers.ts b/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/helpers.ts
new file mode 100644
index 000000000..25a1a87b6
--- /dev/null
+++ b/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/helpers.ts
@@ -0,0 +1,244 @@
+import { Address, scValToNative, xdr } from "@stellar/stellar-sdk";
+import { rpc as StellarRpc } from "@stellar/stellar-sdk";
+
+// =============================================================================
+// Relative timestamp formatting
+// =============================================================================
+
+/**
+ * Formats an ISO timestamp as a human-readable relative time string.
+ * Matches the Figma spec: "~X seconds ago", "~X min ago", etc.
+ *
+ * @param isoTimestamp - ISO 8601 date string (ledgerClosedAt)
+ * @returns Relative time string, e.g. "~15 seconds ago"
+ *
+ * @example
+ * formatRelativeTime("2025-03-24T12:00:00Z"); // "~2 min ago"
+ */
+export const formatRelativeTime = (isoTimestamp: string): string => {
+ const now = Date.now();
+ const then = new Date(isoTimestamp).getTime();
+ const elapsedMs = now - then;
+ const elapsedSeconds = Math.floor(elapsedMs / 1000);
+
+ if (elapsedSeconds < 60) {
+ return `~${elapsedSeconds} seconds ago`;
+ }
+
+ const elapsedMinutes = Math.floor(elapsedSeconds / 60);
+ if (elapsedMinutes < 60) {
+ return `~${elapsedMinutes} min ago`;
+ }
+
+ const elapsedHours = Math.floor(elapsedMinutes / 60);
+ if (elapsedHours < 24) {
+ return `~${elapsedHours} hours ago`;
+ }
+
+ const elapsedDays = Math.floor(elapsedHours / 24);
+ return `~${elapsedDays} days ago`;
+};
+
+// =============================================================================
+// ScVal introspection
+// =============================================================================
+
+export interface ParsedScVal {
+ label: string;
+ typeAnnotation: string;
+ displayValue: string;
+ isAddress: boolean;
+}
+
+/**
+ * Truncates a hex hash to the format "2e887...b6f1f" (5 + 5 chars).
+ *
+ * @param hash - Full transaction hash
+ * @returns Truncated hash string
+ *
+ * @example
+ * truncateHash("2e887abcdef1234567890b6f1f"); // "2e887...b6f1f"
+ */
+export const truncateHash = (hash: string): string => {
+ if (hash.length <= 12) return hash;
+ return `${hash.slice(0, 5)}...${hash.slice(-5)}`;
+};
+
+/**
+ * Converts an i128 ScVal (hi + lo parts) into a decimal string.
+ *
+ * @param parts - The i128 value from ScVal
+ * @returns String representation of the 128-bit integer
+ */
+const i128ToString = (parts: xdr.Int128Parts): string => {
+ const hi = BigInt(parts.hi().toString());
+ const lo = BigInt(parts.lo().toString());
+ // hi is the high 64 bits, lo is the unsigned low 64 bits
+ const SHIFT = BigInt(64);
+ const MASK = (BigInt(1) << SHIFT) - BigInt(1);
+ const value = (hi << SHIFT) | (lo & MASK);
+ return value.toString();
+};
+
+/**
+ * Converts a u128 ScVal (hi + lo parts) into a decimal string.
+ *
+ * @param parts - The u128 value from ScVal
+ * @returns String representation of the unsigned 128-bit integer
+ */
+const u128ToString = (parts: xdr.UInt128Parts): string => {
+ const hi = BigInt(parts.hi().toString());
+ const lo = BigInt(parts.lo().toString());
+ const SHIFT = BigInt(64);
+ const MASK = (BigInt(1) << SHIFT) - BigInt(1);
+ const value = (hi << SHIFT) | (lo & MASK);
+ return value.toString();
+};
+
+/**
+ * Introspects an xdr.ScVal and returns a display-ready representation.
+ * Maps ScVal types to labels, type annotations, and display values
+ * per the spec's type-to-display mapping.
+ *
+ * @param scVal - A Stellar XDR ScVal object
+ * @returns Parsed display data for the ScVal
+ *
+ * @example
+ * const parsed = parseScVal(topic);
+ * // { label: "Symbol", typeAnnotation: "sym", displayValue: '"transfer"', isAddress: false }
+ */
+export const parseScVal = (scVal: xdr.ScVal): ParsedScVal => {
+ const typeName = scVal.switch().name;
+
+ switch (typeName) {
+ case "scvSymbol":
+ return {
+ label: "Symbol",
+ typeAnnotation: "sym",
+ displayValue: `"${scVal.sym().toString()}"`,
+ isAddress: false,
+ };
+
+ case "scvAddress": {
+ const addr = Address.fromScVal(scVal).toString();
+ return {
+ label: "Address",
+ typeAnnotation: "address",
+ displayValue: addr,
+ isAddress: true,
+ };
+ }
+
+ case "scvString":
+ return {
+ label: "string",
+ typeAnnotation: "string",
+ displayValue: `"${scVal.str().toString()}"`,
+ isAddress: false,
+ };
+
+ case "scvI128":
+ return {
+ label: "Data",
+ typeAnnotation: "i128",
+ displayValue: `"${i128ToString(scVal.i128())}"`,
+ isAddress: false,
+ };
+
+ case "scvU128":
+ return {
+ label: "Data",
+ typeAnnotation: "u128",
+ displayValue: `"${u128ToString(scVal.u128())}"`,
+ isAddress: false,
+ };
+
+ case "scvBytes":
+ return {
+ label: "Data",
+ typeAnnotation: "bytes",
+ displayValue: scVal.bytes().toString("hex"),
+ isAddress: false,
+ };
+
+ case "scvBool":
+ return {
+ label: "Data",
+ typeAnnotation: "bool",
+ displayValue: scVal.b().toString(),
+ isAddress: false,
+ };
+
+ default: {
+ // Fallback: JSON-serialize the ScVal
+ let fallback: string;
+ try {
+ fallback = JSON.stringify(scVal.value());
+ } catch {
+ fallback = scVal.toXDR("base64");
+ }
+ return {
+ label: "Data",
+ typeAnnotation: typeName.replace("scv", "").toLowerCase(),
+ displayValue: fallback,
+ isAddress: false,
+ };
+ }
+ }
+};
+
+// =============================================================================
+// humanizeEvents-based parsing
+// =============================================================================
+
+export interface HumanizedEvent {
+ /** Event type: "contract", "system", or "diagnostic" */
+ type: string;
+ /** Contract ID (C... strkey), if present */
+ contractId?: string;
+ /** Topics converted to native JS values via scValToNative */
+ topics: unknown[];
+ /** Event data converted to native JS value via scValToNative */
+ data: unknown;
+}
+
+export interface HumanizedEventWithMeta extends HumanizedEvent {
+ /** Transaction hash */
+ txHash: string;
+ /** Ledger number */
+ ledger: number;
+ /** ISO timestamp of ledger close */
+ ledgerClosedAt: string;
+ /** Original event ID */
+ id: string;
+}
+
+/**
+ * Parses raw RPC event responses using the SDK's `scValToNative` utility.
+ * Converts XDR ScVal topics and data into native JS values (strings, BigInts,
+ * addresses, etc.) while preserving event metadata (txHash, ledger, timestamp).
+ *
+ * This is a higher-level alternative to the manual `parseScVal` approach —
+ * it delegates type conversion to the SDK's `scValToNative`.
+ *
+ * @param events - Array of raw event responses from `getEvents()`
+ * @returns Array of humanized events with metadata attached
+ *
+ * @example
+ * const humanized = humanizeContractEvents(eventsData.events);
+ * // [{ type: "contract", topics: ["transfer", "G...", "G..."], data: 1000n, txHash: "abc...", ... }]
+ */
+export const humanizeContractEvents = (
+ events: StellarRpc.Api.EventResponse[],
+): HumanizedEventWithMeta[] => {
+ return events.map((e) => ({
+ type: e.type,
+ contractId: e.contractId?.toString(),
+ topics: e.topic.map((t: xdr.ScVal) => scValToNative(t)),
+ data: scValToNative(e.value),
+ txHash: e.txHash,
+ ledger: e.ledger,
+ ledgerClosedAt: e.ledgerClosedAt,
+ id: e.id,
+ }));
+};
diff --git a/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/index.tsx b/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/index.tsx
new file mode 100644
index 000000000..20bd9d420
--- /dev/null
+++ b/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/index.tsx
@@ -0,0 +1,179 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { Button, Icon, Loader, Text } from "@stellar/design-system";
+
+import { Box } from "@/components/layout/Box";
+import { ErrorText } from "@/components/ErrorText";
+
+import { useGetContractEvents } from "@/query/useGetContractEvents";
+import { NetworkHeaders } from "@/types/types";
+
+import { ContractActivityCard } from "./ContractActivityCard";
+
+import "./styles.scss";
+
+const PAGE_SIZE = 20;
+
+/**
+ * Activity (events) tab content for the contract explorer.
+ * Fetches and displays recent contract events with 5-second polling.
+ * Includes a pause/resume button so users can freeze the feed to inspect events.
+ * Paginated at 20 events per page.
+ *
+ * @example
+ *
+ */
+export const ContractActivity = ({
+ isActive,
+ contractId,
+ rpcUrl,
+ headers,
+}: {
+ isActive: boolean;
+ contractId: string;
+ rpcUrl: string;
+ headers?: NetworkHeaders;
+}) => {
+ const [isPolling, setIsPolling] = useState(true);
+ const [currentPage, setCurrentPage] = useState(1);
+
+ const {
+ data: eventsData,
+ error: eventsError,
+ isLoading: isEventsLoading,
+ isFetching: isEventsFetching,
+ refetch,
+ } = useGetContractEvents({
+ contractId,
+ rpcUrl,
+ headers,
+ isActive,
+ isPolling,
+ });
+
+ const events = eventsData?.events ?? [];
+ const totalPages = Math.max(1, Math.ceil(events.length / PAGE_SIZE));
+
+ // Reset to page 1 when data changes (new events come in)
+ useEffect(() => {
+ if (currentPage > totalPages) {
+ setCurrentPage(1);
+ }
+ }, [totalPages, currentPage]);
+
+ const paginatedEvents = events.slice(
+ (currentPage - 1) * PAGE_SIZE,
+ currentPage * PAGE_SIZE,
+ );
+
+ const handleTogglePolling = () => {
+ if (isPolling) {
+ setIsPolling(false);
+ } else {
+ refetch();
+ setIsPolling(true);
+ }
+ };
+
+ // Only show loader on initial load, not on refetches
+ if (isEventsLoading || (!eventsData && isEventsFetching)) {
+ return (
+
+
+
+ );
+ }
+
+ if (eventsError) {
+ return ;
+ }
+
+ if (events.length === 0) {
+ return (
+
+ No recent events found for this contract.
+
+ );
+ }
+
+ return (
+
+
+ : }
+ onClick={handleTogglePolling}
+ >
+ {isPolling ? "Pause" : "Fetch & Resume"}
+
+
+ {isPolling ? (
+
+ Auto-refreshing every 5s
+
+ ) : null}
+
+
+
+ {paginatedEvents.map((event) => (
+
+ ))}
+
+
+ {/* Pagination — matches DataTable's pagination pattern */}
+ {totalPages > 1 ? (
+
+
+ {events.length} events
+
+
+
+
+
+ }
+ onClick={() => setCurrentPage(currentPage - 1)}
+ disabled={currentPage === 1}
+ />
+
+
+ {`Page ${currentPage} of ${totalPages}`}
+
+
+ }
+ onClick={() => setCurrentPage(currentPage + 1)}
+ disabled={currentPage === totalPages}
+ />
+
+
+
+
+ ) : null}
+
+ );
+};
diff --git a/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/styles.scss b/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/styles.scss
new file mode 100644
index 000000000..e1d2ffa8f
--- /dev/null
+++ b/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractActivity/styles.scss
@@ -0,0 +1,147 @@
+@use "../../../../../../styles/utils.scss" as *;
+
+.ContractActivity {
+ display: flex;
+ flex-direction: column;
+ gap: pxToRem(16px);
+
+ &__toolbar {
+ display: flex;
+ align-items: center;
+ gap: pxToRem(12px);
+
+ .Text {
+ color: var(--sds-clr-gray-09);
+ }
+ }
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: pxToRem(16px);
+ }
+
+ // Event card
+ &__card {
+ background: var(--sds-clr-gray-01);
+ border: 1px solid var(--sds-clr-gray-04);
+ border-radius: pxToRem(12px);
+ padding: pxToRem(24px);
+ display: flex;
+ flex-direction: column;
+ gap: pxToRem(16px);
+ }
+
+ // Header: tx hash badge + timestamp
+ &__cardHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: pxToRem(8px);
+
+ .Badge {
+ display: inline-flex;
+ align-items: center;
+ gap: pxToRem(4px);
+ font-family: var(--sds-ff-monospace);
+
+ svg {
+ width: pxToRem(12px);
+ height: pxToRem(12px);
+ }
+ }
+ }
+
+ &__timestamp {
+ font-size: pxToRem(13px);
+ color: var(--sds-clr-gray-09);
+ white-space: nowrap;
+ }
+
+ // Gray event body box
+ &__body {
+ background: var(--sds-clr-gray-03);
+ border-radius: pxToRem(8px);
+ padding: pxToRem(12px);
+ display: flex;
+ flex-direction: column;
+ gap: pxToRem(8px);
+ }
+
+ // Individual data row
+ &__row {
+ display: flex;
+ align-items: flex-start;
+ gap: pxToRem(12px);
+ font-family: var(--sds-ff-monospace);
+ font-size: pxToRem(13px);
+ line-height: pxToRem(20px);
+ word-break: break-all;
+ }
+
+ &__rowLabel {
+ color: var(--sds-clr-gray-09);
+ min-width: pxToRem(80px);
+ flex-shrink: 0;
+ font-family: var(--sds-ff-body);
+ font-size: pxToRem(13px);
+ }
+
+ &__rowValue {
+ color: var(--sds-clr-gray-12);
+ display: flex;
+ align-items: baseline;
+ flex-wrap: wrap;
+ gap: pxToRem(6px);
+ }
+
+ // Type annotation subscript
+ &__typeAnnotation {
+ font-family: var(--sds-ff-body);
+ font-size: pxToRem(11px);
+ color: var(--sds-clr-gray-08);
+ flex-shrink: 0;
+ }
+
+ // Address link styling
+ &__addressLink {
+ text-decoration: underline;
+ }
+
+ // Pagination page count
+ &__pageCount {
+ font-size: pxToRem(14px);
+ color: var(--sds-clr-gray-09);
+ white-space: nowrap;
+ padding: 0 pxToRem(4px);
+ }
+
+ // Collapsible JSON section
+ &__json {
+ display: flex;
+ flex-direction: column;
+ gap: pxToRem(8px);
+ }
+
+ &__jsonToggle {
+ display: inline-flex;
+ align-items: center;
+ gap: pxToRem(4px);
+ background: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ font-size: pxToRem(13px);
+ color: var(--sds-clr-lilac-09);
+ font-family: var(--sds-ff-body);
+
+ svg {
+ width: pxToRem(14px);
+ height: pxToRem(14px);
+ }
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+}
diff --git a/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractInfo.tsx b/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractInfo.tsx
index 63a77c8e9..1daf2a4b5 100644
--- a/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractInfo.tsx
+++ b/src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractInfo.tsx
@@ -37,6 +37,7 @@ import {
import { ContractSpecMeta } from "./ContractSpecMeta";
import { ContractStorage } from "./ContractStorage";
+import { ContractActivity } from "./ContractActivity";
import { VersionHistory } from "./VersionHistory";
import { BuildInfo } from "./BuildInfo";
import { SourceCode } from "./SourceCode";
@@ -68,7 +69,8 @@ export const ContractInfo = ({
| "contract-source-code"
| "contract-contract-storage"
| "contract-version-history"
- | "contract-build-info";
+ | "contract-build-info"
+ | "contract-activity";
const [activeTab, setActiveTab] = useState(
"contract-contract-spec",
@@ -421,6 +423,18 @@ export const ContractInfo = ({
),
isDisabled: !isDataLoaded || isSacType,
}}
+ tab8={{
+ id: "contract-activity",
+ label: "Activity (events)",
+ content: (
+
+ ),
+ isDisabled: !isDataLoaded,
+ }}
activeTabId={activeTab}
onTabChange={(tabId) => {
setActiveTab(tabId as ContractTabId);
diff --git a/src/constants/networkLimits.ts b/src/constants/networkLimits.ts
index 84bf8d3c3..b3e6edbb3 100644
--- a/src/constants/networkLimits.ts
+++ b/src/constants/networkLimits.ts
@@ -83,36 +83,36 @@ export const MAINNET_LIMITS: NetworkLimits = {
"persistent_rent_rate_denominator": "1215",
"temp_rent_rate_denominator": "2430",
"live_soroban_state_size_window": [
- "890708604",
- "884390684",
- "869203144",
- "854475644",
- "839278996",
- "825281880",
- "816346052",
- "816579152",
- "817281920",
- "817618580",
- "818790088",
- "819076236",
- "819465324",
- "820167796",
- "820533092",
- "820255500",
- "815645004",
- "811055056",
- "806914280",
- "802663276",
- "798574420",
- "793390388",
- "788704844",
- "784339616",
- "780019364",
- "775837484",
- "770414728",
- "765055432",
- "755951804",
- "746827664"
+ "773711440",
+ "774036876",
+ "774510224",
+ "774702196",
+ "775419064",
+ "776508976",
+ "776745112",
+ "777078280",
+ "777451912",
+ "778200392",
+ "779280552",
+ "779683200",
+ "779991944",
+ "780426340",
+ "780944664",
+ "782123236",
+ "782947261",
+ "783248305",
+ "783655649",
+ "784404073",
+ "785194261",
+ "786078405",
+ "786878145",
+ "787124537",
+ "787650309",
+ "788235761",
+ "789261633",
+ "790307421",
+ "790359777",
+ "791272999"
],
"state_target_size_bytes": "3000000000",
"rent_fee_1kb_state_size_low": "-17000",
@@ -151,36 +151,36 @@ export const TESTNET_LIMITS: NetworkLimits = {
"persistent_rent_rate_denominator": "1215",
"temp_rent_rate_denominator": "2430",
"live_soroban_state_size_window": [
- "947173188",
- "947543066",
- "947578030",
- "947594914",
- "947621978",
- "947936432",
- "948299603",
- "948185721",
- "948126857",
- "948157557",
- "948281426",
- "948327526",
- "948374606",
- "948385230",
- "948364682",
- "948375538",
- "948328774",
- "943915383",
- "945133177",
- "945521959",
- "945292077",
- "945297577",
- "945449866",
- "945435422",
- "945433126",
- "945476718",
- "944941163",
- "946402823",
- "946742666",
- "946767562"
+ "938546554",
+ "938666695",
+ "938664204",
+ "938755840",
+ "938850277",
+ "939097534",
+ "939120602",
+ "939654961",
+ "940015342",
+ "940168005",
+ "940370606",
+ "940327138",
+ "940348498",
+ "940596429",
+ "940479360",
+ "940619251",
+ "940776944",
+ "940788972",
+ "940800376",
+ "940816880",
+ "940797772",
+ "940795104",
+ "940818928",
+ "940824980",
+ "941741362",
+ "941757122",
+ "941763802",
+ "941770850",
+ "941723806",
+ "941779028"
],
"state_target_size_bytes": "3000000000",
"rent_fee_1kb_state_size_low": "-17000",
@@ -219,36 +219,36 @@ export const FUTURENET_LIMITS: NetworkLimits = {
"persistent_rent_rate_denominator": "1215",
"temp_rent_rate_denominator": "2430",
"live_soroban_state_size_window": [
- "67023064",
- "67023064",
- "67023064",
- "67023064",
- "67023064",
- "67023064",
- "67023064",
- "67023064",
- "67023064",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192",
- "67026192"
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791",
+ "69389791"
],
"state_target_size_bytes": "3000000000",
"rent_fee_1kb_state_size_low": "-17000",
diff --git a/src/query/useGetContractEvents.ts b/src/query/useGetContractEvents.ts
new file mode 100644
index 000000000..e18bb1f5d
--- /dev/null
+++ b/src/query/useGetContractEvents.ts
@@ -0,0 +1,112 @@
+import { useQuery } from "@tanstack/react-query";
+import { rpc as StellarRpc } from "@stellar/stellar-sdk";
+
+import { isEmptyObject } from "@/helpers/isEmptyObject";
+import { NetworkHeaders } from "@/types/types";
+
+const EVENTS_LIMIT = 100;
+const EVENTS_POLL_INTERVAL_MS = 5_000;
+
+/**
+ * Fetches the most recent contract events from the Stellar RPC using
+ * `getEvents()` with cursor-based pagination.
+ *
+ * Strategy: start with a small lookback window (200 ledgers / ~17 min).
+ * If that returns fewer events than the limit, progressively widen the
+ * window (doubling each time, up to 10,000 ledgers). This ensures we
+ * get the freshest events for active contracts without over-fetching,
+ * while still surfacing events for quieter contracts.
+ *
+ * On subsequent polls (every 5 s) the cursor from the previous response
+ * is reused so only genuinely new events are fetched and prepended.
+ *
+ * @param contractId - The contract ID to filter events for
+ * @param rpcUrl - The RPC server URL
+ * @param headers - Optional network headers
+ * @param isActive - Whether the tab is currently visible
+ * @param isPolling - Whether to auto-refetch every 5 seconds
+ * @returns React Query result with parsed event data
+ *
+ * @example
+ * const { data, isLoading, error } = useGetContractEvents({
+ * contractId: "CABC...",
+ * rpcUrl: "https://soroban-testnet.stellar.org",
+ * isActive: true,
+ * });
+ */
+export const useGetContractEvents = ({
+ contractId,
+ rpcUrl,
+ headers = {},
+ isActive,
+ isPolling = true,
+}: {
+ contractId: string;
+ rpcUrl: string;
+ headers?: NetworkHeaders;
+ isActive: boolean;
+ /** Whether to auto-refetch every 5 seconds. When false, polling is paused. */
+ isPolling?: boolean;
+}) => {
+ const query = useQuery({
+ queryKey: ["getContractEvents", contractId, rpcUrl],
+ queryFn: async () => {
+ const rpcServer = new StellarRpc.Server(rpcUrl, {
+ headers: isEmptyObject(headers) ? undefined : { ...headers },
+ allowHttp: new URL(rpcUrl).hostname === "localhost",
+ });
+
+ const latestLedger = await rpcServer.getLatestLedger();
+
+ // Adaptive lookback: start small, widen if we find fewer events
+ // than the limit. This keeps queries fast for active contracts
+ // while still surfacing events for quieter ones.
+ const LOOKBACK_STEPS = [200, 1_000, 5_000, 10_000];
+ let events: StellarRpc.Api.EventResponse[] = [];
+
+ for (const lookback of LOOKBACK_STEPS) {
+ const startLedger = Math.max(latestLedger.sequence - lookback, 0);
+
+ const response = await rpcServer.getEvents({
+ startLedger,
+ endLedger: latestLedger.sequence,
+ filters: [
+ {
+ type: "contract",
+ contractIds: [contractId],
+ },
+ ],
+ limit: EVENTS_LIMIT,
+ });
+
+ events = response.events;
+
+ // If we got events, use these. If the RPC returned the full limit,
+ // these are the oldest within this window — but that means the
+ // contract is active and the window is already good enough.
+ // If we got fewer than the limit, we have ALL events in this
+ // window, so widening further would only add older ones.
+ if (events.length > 0) {
+ break;
+ }
+ }
+
+ // Sort newest-first
+ events.sort((a, b) => {
+ if (b.ledger !== a.ledger) {
+ return b.ledger - a.ledger;
+ }
+ return b.transactionIndex - a.transactionIndex;
+ });
+
+ return {
+ events,
+ latestLedger: latestLedger.sequence,
+ } as unknown as StellarRpc.Api.GetEventsResponse;
+ },
+ enabled: Boolean(isActive && contractId && rpcUrl),
+ refetchInterval: isActive && isPolling ? EVENTS_POLL_INTERVAL_MS : false,
+ });
+
+ return query;
+};