diff --git a/packages/build-tools/explorer/client/src/App.tsx b/packages/build-tools/explorer/client/src/App.tsx
index ab79c75f7..a3f1c2c13 100644
--- a/packages/build-tools/explorer/client/src/App.tsx
+++ b/packages/build-tools/explorer/client/src/App.tsx
@@ -34,6 +34,9 @@ function App() {
prevStaticTablePage,
firstStaticTablePage,
setStaticTableLimit,
+ contractsData,
+ eventsData,
+ eventsPagination,
} = useTableData();
// Error handling for uncaught promises
@@ -80,6 +83,11 @@ function App() {
newBlockIndices={newBlockIndices}
/>
+
+
setPrimitiveLimit(name, limit)}
/>
+
+
${value}`;
}
diff --git a/packages/build-tools/explorer/client/src/config.ts b/packages/build-tools/explorer/client/src/config.ts
index acce55006..cfc513207 100644
--- a/packages/build-tools/explorer/client/src/config.ts
+++ b/packages/build-tools/explorer/client/src/config.ts
@@ -70,6 +70,9 @@ export const PRIMITIVES_SCHEMA_ENDPOINT =
`http://127.0.0.1:${ENV.PAIMA_API_PORT}/primitives-schema`;
export const TABLE_SCHEMA_ENDPOINT =
`http://127.0.0.1:${ENV.PAIMA_API_PORT}/table-schema`;
+export const EVENTS_ENDPOINT = `http://127.0.0.1:${ENV.PAIMA_API_PORT}/events`;
+export const EVENTS_SCHEMA_ENDPOINT =
+ `http://127.0.0.1:${ENV.PAIMA_API_PORT}/events-schema`;
export const BATCHER_ENDPOINT =
`http://localhost:${ENV.BATCHER_PORT}/send-input`;
export const BATCHER_OPENAPI_URL =
diff --git a/packages/build-tools/explorer/client/src/hooks/useTableData.ts b/packages/build-tools/explorer/client/src/hooks/useTableData.ts
index a5985c234..05e0bf6f6 100644
--- a/packages/build-tools/explorer/client/src/hooks/useTableData.ts
+++ b/packages/build-tools/explorer/client/src/hooks/useTableData.ts
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import {
CONFIG_ENDPOINT,
+ EVENTS_ENDPOINT,
PRIMITIVES_ENDPOINT,
PRIMITIVES_SCHEMA_ENDPOINT,
SCHEDULED_DATA_ENDPOINT,
@@ -60,12 +61,21 @@ export function useTableData() {
const [scheduledData, setScheduledData] = useState<
Record
>({});
+ const [contractsData, setContractsData] = useState<
+ Record
+ >({});
+ const [eventsData, setEventsData] = useState<
+ Record
+ >({});
const [primitivePagination, setPrimitivePagination] = useState<
Record
>({});
const [staticTablePagination, setStaticTablePagination] = useState<
Record
>({});
+ const [eventsPagination, setEventsPagination] = useState<
+ Record
+ >({});
const [primitiveSchemas, setPrimitiveSchemas] = useState<
Record
>({});
@@ -82,6 +92,7 @@ export function useTableData() {
const staticTableSchemasRef = useRef>({});
const primitivePaginationRef = useRef>({});
const staticTablePaginationRef = useRef>({});
+ const eventsPaginationRef = useRef>({});
// Update refs whenever state changes
useEffect(() => {
@@ -104,6 +115,10 @@ export function useTableData() {
staticTablePaginationRef.current = staticTablePagination;
}, [staticTablePagination]);
+ useEffect(() => {
+ eventsPaginationRef.current = eventsPagination;
+ }, [eventsPagination]);
+
// Convert schema columns to Field format
const convertSchemaToFields = useCallback(
(schema: SchemaColumn[]): Field[] => {
@@ -179,6 +194,59 @@ export function useTableData() {
[convertSchemaToFields],
);
+ // Convert contract data (from config) to TableData format
+ const convertContractsDataToTableFormat = useCallback(
+ (config: any[]): TableData | null => {
+ if (!config || !Array.isArray(config)) {
+ return null;
+ }
+
+ const rows: any[] = [];
+ config.forEach((syncProtocolConfig) => {
+ if (
+ !syncProtocolConfig.primitives ||
+ !Array.isArray(syncProtocolConfig.primitives)
+ ) {
+ return;
+ }
+ syncProtocolConfig.primitives.forEach((primitiveConfig: any) => {
+ if (!primitiveConfig.primitive || !primitiveConfig.primitive.name) {
+ console.warn(
+ "Primitive config is missing primitive or primitive.name",
+ primitiveConfig,
+ );
+ return;
+ }
+ rows.push({
+ network_type: syncProtocolConfig.networkType || "N/A",
+ primitive_name: primitiveConfig.primitive.name,
+ primitive_type: primitiveConfig.primitive.type,
+ contract_address: primitiveConfig.primitive.contractAddress ||
+ "N/A",
+ start_block: primitiveConfig.primitive.startBlockHeight ||
+ "N/A",
+ });
+ });
+ });
+
+ const fields: Field[] = [
+ { name: "network_type", dataTypeID: 25 },
+ { name: "primitive_name", dataTypeID: 25 },
+ { name: "primitive_type", dataTypeID: 25 },
+ { name: "contract_address", dataTypeID: 25 },
+ { name: "start_block", dataTypeID: 23 }, // number
+ ];
+
+ return {
+ command: "SELECT",
+ rowCount: rows.length,
+ rows: rows,
+ fields: fields,
+ };
+ },
+ [],
+ );
+
// Fetch schema for primitive
const fetchPrimitiveSchema = useCallback(
async (primitiveName: string): Promise => {
@@ -404,6 +472,82 @@ export function useTableData() {
[convertTableDataToTableFormat],
);
+ // Fetch events data
+ const fetchEventsData = useCallback(
+ async (pagination?: PaginationMeta) => {
+ try {
+ const current = pagination ?? {
+ limit: DEFAULT_LIMIT,
+ cursors: [undefined],
+ currentPage: 0,
+ hasMore: false,
+ };
+ const url = new URL(EVENTS_ENDPOINT);
+ url.searchParams.set("limit", String(current.limit));
+
+ const cursor = current.cursors[current.currentPage];
+ if (cursor) {
+ url.searchParams.set("after", cursor);
+ }
+
+ const response = await fetch(url.toString());
+ if (!response.ok) {
+ if (response.status === 404) {
+ console.log(`🚫 Events not found (404)`);
+ return null;
+ }
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const jsonResponse = await response.json();
+ console.log(`📊 Fetched events data:`, jsonResponse);
+ const data = jsonResponse.data ?? [];
+ const paginationMeta = jsonResponse.pagination;
+ if (paginationMeta) {
+ setEventsPagination((prev) => {
+ const newCursors = [...prev["events"].cursors];
+ if (
+ paginationMeta.nextCursor &&
+ newCursors.length === prev["events"].currentPage + 1
+ ) {
+ newCursors.push(paginationMeta.nextCursor);
+ }
+ return {
+ ...prev,
+ ["events"]: {
+ ...prev["events"],
+ hasMore: paginationMeta.hasMore,
+ cursors: newCursors,
+ },
+ };
+ });
+ }
+
+ // Manually define fields for events data
+ const fields: Field[] = [
+ { name: "id", dataTypeID: 23 },
+ { name: "event_name", dataTypeID: 25 },
+ { name: "topic", dataTypeID: 25 },
+ { name: "address", dataTypeID: 25 },
+ { name: "data", dataTypeID: 25 },
+ { name: "block_height", dataTypeID: 23 },
+ { name: "tx_index", dataTypeID: 23 },
+ { name: "log_index", dataTypeID: 23 },
+ ];
+
+ return {
+ command: "SELECT",
+ rowCount: data.length,
+ rows: data,
+ fields: fields,
+ };
+ } catch (error) {
+ console.error(`Error fetching events data:`, error);
+ return null;
+ }
+ },
+ [],
+ );
+
// Fetch scheduled data
const fetchScheduledData = useCallback(
async (): Promise => {
@@ -528,6 +672,23 @@ export function useTableData() {
}
}, [fetchScheduledData]);
+ // Refresh events data
+ const refreshEventsData = useCallback(async () => {
+ if (!isInitialLoadComplete.current) {
+ return;
+ }
+
+ try {
+ const pagination = eventsPaginationRef.current["events"];
+ const data = await fetchEventsData(pagination);
+ if (data !== null) {
+ setEventsData({ "events": data });
+ }
+ } catch (error) {
+ console.error("Error refreshing events data:", error);
+ }
+ }, [fetchEventsData]);
+
// Initialize primitive tables
const initializePrimitiveTables = useCallback(async () => {
console.log("📋 Initializing primitive tables...");
@@ -674,11 +835,59 @@ export function useTableData() {
}
}, [fetchScheduledData]);
+ const initializeEventsData = useCallback(async () => {
+ console.log("📋 Initializing events data...");
+
+ try {
+ setEventsPagination((prev) => ({
+ ...prev,
+ ["events"]: prev["events"] ?? {
+ limit: DEFAULT_LIMIT,
+ cursors: [undefined],
+ currentPage: 0,
+ hasMore: false,
+ },
+ }));
+
+ const pagination = eventsPaginationRef.current["events"] ?? {
+ limit: DEFAULT_LIMIT,
+ cursors: [undefined],
+ currentPage: 0,
+ hasMore: false,
+ };
+ const data = await fetchEventsData(pagination);
+ setEventsData({ "events": data });
+ console.log("✅ Events data initialized");
+ } catch (error) {
+ console.error("Error initializing events data:", error);
+ }
+ }, [fetchEventsData]);
+
+ // Initialize contracts data
+ const initializeContractsData = useCallback(async () => {
+ console.log("📋 Initializing contracts data...");
+
+ try {
+ const config = await fetchConfig();
+ if (!config) {
+ console.error("Failed to fetch config for contracts data");
+ return;
+ }
+
+ const tableData = convertContractsDataToTableFormat(config);
+ setContractsData({ "contracts": tableData });
+ console.log("✅ Contracts data initialized");
+ } catch (error) {
+ console.error("Error initializing contracts data:", error);
+ }
+ }, [fetchConfig, convertContractsDataToTableFormat]);
+
// Initialize and setup refresh intervals
useEffect(() => {
let primitiveRefreshInterval: number;
let staticTableRefreshInterval: number;
let scheduledDataRefreshInterval: number;
+ let eventsRefreshInterval: number;
const initialize = async () => {
// Initialize tables
@@ -686,6 +895,8 @@ export function useTableData() {
initializePrimitiveTables(),
initializeStaticTables(),
initializeScheduledData(),
+ initializeContractsData(),
+ initializeEventsData(),
]);
// Mark initial load as complete
@@ -712,6 +923,14 @@ export function useTableData() {
refreshScheduledData();
}, 5000);
}, 3000);
+
+ // Refresh events data after 4.5 seconds, then every 5 seconds
+ setTimeout(() => {
+ refreshEventsData();
+ eventsRefreshInterval = setInterval(() => {
+ refreshEventsData();
+ }, 5000);
+ }, 4500);
};
initialize();
@@ -726,6 +945,9 @@ export function useTableData() {
if (scheduledDataRefreshInterval) {
clearInterval(scheduledDataRefreshInterval);
}
+ if (eventsRefreshInterval) {
+ clearInterval(eventsRefreshInterval);
+ }
};
}, []); // Empty dependency array to prevent re-runs
@@ -733,11 +955,14 @@ export function useTableData() {
primitiveData,
staticTableData,
scheduledData,
+ contractsData,
+ eventsData,
refreshPrimitiveData,
refreshStaticTableData,
refreshScheduledData,
primitivePagination,
staticTablePagination,
+ eventsPagination,
// Pagination controls for primitives
setPrimitiveLimit: async (primitiveName: string, limit: number) => {
const newPagination = {
diff --git a/packages/node-sdk/db/src/sql/events.queries.ts b/packages/node-sdk/db/src/sql/events.queries.ts
index 02875af2d..0ae76b5d5 100644
--- a/packages/node-sdk/db/src/sql/events.queries.ts
+++ b/packages/node-sdk/db/src/sql/events.queries.ts
@@ -199,3 +199,55 @@ const getEventByTopicIR: any = {"usedParamSet":{"topic":true},"params":[{"name":
export const getEventByTopic = new PreparedQuery(getEventByTopicIR);
+/** 'GetAllEvents' parameters type */
+export interface IGetAllEventsParams {
+ after_id?: number | null | void;
+ limit?: number | null | void;
+}
+
+/** 'GetAllEvents' return type */
+export interface IGetAllEventsResult {
+ address: string;
+ block_height: number;
+ data: Json;
+ event_name: string;
+ id: number;
+ log_index: number;
+ topic: string;
+ tx_index: number;
+}
+
+/** 'GetAllEvents' query type */
+export interface IGetAllEventsQuery {
+ params: IGetAllEventsParams;
+ result: IGetAllEventsResult;
+}
+
+const getAllEventsIR: any = {"usedParamSet":{"after_id":true,"limit":true},"params":[{"name":"after_id","required":false,"transform":{"type":"scalar"},"locs":[{"a":232,"b":240},{"a":265,"b":273}]},{"name":"limit","required":false,"transform":{"type":"scalar"},"locs":[{"a":318,"b":323}]}],"statement":"SELECT\n e.id,\n re.name AS event_name,\n e.topic,\n e.address,\n e.data,\n e.block_height,\n e.tx_index,\n e.log_index\nFROM\n paima.event e\nLEFT JOIN\n paima.registered_event re ON e.topic = re.topic\nWHERE\n (:after_id::INT IS NULL OR e.id > :after_id::INT)\nORDER BY\n e.id ASC\nLIMIT COALESCE(:limit, 100)"};
+
+/**
+ * Query generated from SQL:
+ * ```
+ * SELECT
+ * e.id,
+ * re.name AS event_name,
+ * e.topic,
+ * e.address,
+ * e.data,
+ * e.block_height,
+ * e.tx_index,
+ * e.log_index
+ * FROM
+ * paima.event e
+ * LEFT JOIN
+ * paima.registered_event re ON e.topic = re.topic
+ * WHERE
+ * (:after_id::INT IS NULL OR e.id > :after_id::INT)
+ * ORDER BY
+ * e.id ASC
+ * LIMIT COALESCE(:limit, 100)
+ * ```
+ */
+export const getAllEvents = new PreparedQuery(getAllEventsIR);
+
+
diff --git a/packages/node-sdk/db/src/sql/events.sql b/packages/node-sdk/db/src/sql/events.sql
index 6c948fe6b..6176dc7e2 100644
--- a/packages/node-sdk/db/src/sql/events.sql
+++ b/packages/node-sdk/db/src/sql/events.sql
@@ -38,4 +38,24 @@ SELECT topic FROM paima.registered_event WHERE name = :name!;
SELECT name, topic FROM paima.registered_event;
/* @name getEventByTopic */
-SELECT name FROM paima.registered_event WHERE topic = :topic!;
\ No newline at end of file
+SELECT name FROM paima.registered_event WHERE topic = :topic!;
+
+/* @name getAllEvents */
+SELECT
+ e.id,
+ re.name AS event_name,
+ e.topic,
+ e.address,
+ e.data,
+ e.block_height,
+ e.tx_index,
+ e.log_index
+FROM
+ paima.event e
+LEFT JOIN
+ paima.registered_event re ON e.topic = re.topic
+WHERE
+ (:after_id::INT IS NULL OR e.id > :after_id::INT)
+ORDER BY
+ e.id ASC
+LIMIT COALESCE(:limit, 100);
\ No newline at end of file
diff --git a/packages/node-sdk/runtime/src/api/http-server.ts b/packages/node-sdk/runtime/src/api/http-server.ts
index e4f96581b..49f0549e9 100644
--- a/packages/node-sdk/runtime/src/api/http-server.ts
+++ b/packages/node-sdk/runtime/src/api/http-server.ts
@@ -6,6 +6,7 @@ import { run, until } from "effection";
import {
acquireDBMutex,
getAllAddresses,
+ getAllEvents,
getAllScheduledData,
getPrimaryKeyColumns,
getPrimitivePrefix,
@@ -209,6 +210,54 @@ export const startHttpServer = function* (
};
});
+ server.get("/events", {
+ schema: {
+ tags: ["status"],
+ querystring: PaginationQuerySchema,
+ response: {
+ 200: createPaginatedResponseSchema(Type.Object({
+ id: Type.Number(),
+ event_name: Type.Union([Type.String(), Type.Null()]),
+ topic: Type.String(),
+ address: Type.String(),
+ data: Type.String(),
+ block_height: Type.Number(),
+ tx_index: Type.Number(),
+ log_index: Type.Number(),
+ })),
+ },
+ },
+ }, async (request) => {
+ const { limit, after } = getPaginationParams(request);
+ let events: any[] = [];
+ try {
+ events = await runPreparedQuery(
+ getAllEvents.run(
+ {
+ limit,
+ after_id: after?.id ?? null,
+ },
+ dbConn,
+ ),
+ "events",
+ );
+ } catch (error) {
+ console.error("Error fetching events:", error);
+ throw error;
+ }
+
+ const pagination = createPaginationMeta(
+ limit,
+ events,
+ ["id"],
+ );
+
+ return {
+ data: events,
+ pagination,
+ };
+ });
+
// TODO This is dev only endpoint to monitor sync protocols.
server.get("/debug/sync-protocols", {
schema: {