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: {