diff --git a/frontend/src/components/Editor/BlockPicker.jsx b/frontend/src/components/Editor/BlockPicker.jsx index fcc78f918..ae3cde453 100644 --- a/frontend/src/components/Editor/BlockPicker.jsx +++ b/frontend/src/components/Editor/BlockPicker.jsx @@ -6,15 +6,19 @@ import { Badge, InputGroup, FormControl, + Dropdown, + ButtonGroup, } from "react-bootstrap"; -import { Play, Plus } from "react-bootstrap-icons"; +import { Play, Plus, Stop } from "react-bootstrap-icons"; export const BlockPicker = ({ heights = [], setHeights, executeIndexerFunction, latestHeight, + isExecuting, + stopExecution, }) => { const [inputValue, setInputValue] = useState(String(latestHeight)); @@ -37,24 +41,71 @@ export const BlockPicker = ({ value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> - - Stop Indexer Execution} + > + + + } + {!isExecuting && (<> Test Indexer Function In Browser} + overlay={Add Block To Debug List} > - + + + { + (() => { + if (heights.length > 0) { + return "Test Indexer Function With Debug List" + } else if (inputValue) { + return "Test Indexer Function With Specific Block" + } else { + return "Follow the Tip of the Network" + } + })() + } + } + > + + + + + + executeIndexerFunction("latest")}>Follow The Network + executeIndexerFunction("debugList")}>Execute From Debug List + + + ) + } + + - + ); }; diff --git a/frontend/src/components/Editor/Editor.js b/frontend/src/components/Editor/Editor.js index c55c1d371..62d10c5ba 100644 --- a/frontend/src/components/Editor/Editor.js +++ b/frontend/src/components/Editor/Editor.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback } from "react"; +import React, { useEffect, useState, useCallback, useMemo } from "react"; import { formatSQL, formatIndexingCode, @@ -16,6 +16,7 @@ import { ResetChangesModal } from "../Modals/resetChanges"; import { FileSwitcher } from "./FileSwitcher"; import EditorButtons from "./EditorButtons"; import { PublishModal } from "../Modals/PublishModal"; +import {getLatestBlockHeight} from "../../utils/getLatestBlockHeight"; const BLOCKHEIGHT_LIMIT = 3600; const contractRegex = RegExp( @@ -46,7 +47,7 @@ const Editor = ({ } }; - const indexerRunner = new IndexerRunner(handleLog); + const indexerRunner = useMemo(() => new IndexerRunner(handleLog), []); const [indexingCode, setIndexingCode] = useState(defaultCode); const [schema, setSchema] = useState(defaultSchema); @@ -59,6 +60,12 @@ const Editor = ({ const [isContractFilterValid, setIsContractFilterValid] = useState(true); const [contractFilter, setContractFilter] = useState("social.near"); const { height, selectedTab, currentUserAccountId } = useInitialPayload(); + const [isExecutingIndexerFunction, setIsExecutingIndexerFunction] = useState(false) + + const requestLatestBlockHeight = async () => { + const blockHeight = getLatestBlockHeight() + return blockHeight + } const handleOptionChange = (event) => { setSelectedOption(event.target.value); @@ -301,8 +308,26 @@ const Editor = ({ } } - async function executeIndexerFunction() { - await indexerRunner.executeIndexerFunction(heights,indexingCode) + async function executeIndexerFunction(option = "latest", startingBlockHeight = null) { + setIsExecutingIndexerFunction(() => true) + + switch (option) { + case "debugList": + await indexerRunner.executeIndexerFunctionOnHeights(heights, indexingCode, option) + break + case "specific": + if (startingBlockHeight === null && Number(startingBlockHeight) === 0) { + console.log("Invalid Starting Block Height: starting block height is null or 0") + break + } + + await indexerRunner.start(startingBlockHeight, indexingCode, option) + break + case "latest": + const latestHeight = await requestLatestBlockHeight() + if (latestHeight) await indexerRunner.start(latestHeight - 10, indexingCode, option) + } + setIsExecutingIndexerFunction(() => false) } return ( @@ -331,6 +356,8 @@ const Editor = ({ debugMode={debugMode} heights={heights} setHeights={setHeights} + isExecuting={isExecutingIndexerFunction} + stopExecution={() => indexerRunner.stop()} contractFilter={contractFilter} handleSetContractFilter={handleSetContractFilter} isContractFilterValid={isContractFilterValid} diff --git a/frontend/src/components/Editor/EditorButtons.jsx b/frontend/src/components/Editor/EditorButtons.jsx index 11066d3fc..d0a315fa5 100644 --- a/frontend/src/components/Editor/EditorButtons.jsx +++ b/frontend/src/components/Editor/EditorButtons.jsx @@ -37,6 +37,8 @@ const EditorButtons = ({ getActionButtonText, submit, debugMode, + isExecuting, + stopExecution, heights, setHeights, setShowPublishModal, @@ -104,6 +106,8 @@ const EditorButtons = ({ setHeights={setHeights} executeIndexerFunction={executeIndexerFunction} latestHeight={latestHeight} + isExecuting={isExecuting} + stopExecution={stopExecution} /> )} diff --git a/frontend/src/utils/fetchBlock.js b/frontend/src/utils/fetchBlock.js index f773c109a..70654f4a9 100644 --- a/frontend/src/utils/fetchBlock.js +++ b/frontend/src/utils/fetchBlock.js @@ -1,14 +1,19 @@ const BLOCK_FETCHER_API = "https://70jshyr5cb.execute-api.eu-central-1.amazonaws.com/block/"; +const GENESIS_BLOCK_HEIGHT = 52945886; export async function fetchBlockDetails(blockHeight) { - try { - const response = await fetch( - `${BLOCK_FETCHER_API}${String(blockHeight)}` - ); - const block_details = await response.json(); - return block_details; - } catch { - console.log(`Error Fetching Block Height details at ${blockHeight}`); - } + if (blockHeight <= GENESIS_BLOCK_HEIGHT) { + throw new Error(`Block Height must be greater than genesis block height #${GENESIS_BLOCK_HEIGHT}`); } + try { + const response = await fetch( + `${BLOCK_FETCHER_API}${String(blockHeight)}` + ); + const block_details = await response.json(); + return block_details; + } catch { + // console.log(`Error Fetching Block Height details at ${blockHeight}`); + throw new Error(`Error Fetching Block Height details at BlockHeight #${blockHeight}`); + } +} diff --git a/frontend/src/utils/indexerRunner.js b/frontend/src/utils/indexerRunner.js index 4007d56f5..123226bbb 100644 --- a/frontend/src/utils/indexerRunner.js +++ b/frontend/src/utils/indexerRunner.js @@ -1,29 +1,86 @@ import { Block } from "@near-lake/primitives"; import { Buffer } from "buffer"; -import {fetchBlockDetails} from "./fetchBlock"; +import { fetchBlockDetails } from "./fetchBlock"; global.Buffer = Buffer; export default class IndexerRunner { constructor(handleLog) { this.handleLog = handleLog; + this.currentHeight = 0; + this.shouldStop = false; } - async executeIndexerFunction(heights, indexingCode) { + async start(startingHeight, indexingCode, option) { + this.currentHeight = startingHeight; + this.shouldStop = false; console.clear() console.group('%c Welcome! Lets test your indexing logic on some Near Blocks!', 'color: white; background-color: navy; padding: 5px;'); - if(heights.length === 0) { + if (option == "specific" && !Number(startingHeight)) { + console.log("No Start Block Height Provided to Stream Blocks From") + this.stop() + console.groupEnd() + return + } + console.log(`Streaming Blocks Starting from ${option} Block #${this.currentHeight}`) + while (!this.shouldStop) { + console.group(`Block Height #${this.currentHeight}`) + let blockDetails; + try { + blockDetails = await fetchBlockDetails(this.currentHeight); + } catch (error) { + console.log(error) + this.stop() + } + if (blockDetails) { + await this.executeIndexerFunction(this.currentHeight, blockDetails, indexingCode); + this.currentHeight++; + await this.delay(1000); + } + console.groupEnd() + + } + } + + stop() { + this.shouldStop = true; + console.log("%c Stopping Block Processing", 'color: white; background-color: red; padding: 5px;') + } + + delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + async executeIndexerFunction(height, blockDetails, indexingCode) { + let innerCode = indexingCode.match(/getBlock\s*\([^)]*\)\s*{([\s\S]*)}/)[1]; + if (blockDetails) { + const block = Block.fromStreamerMessage(blockDetails); + block.actions() + block.receipts() + block.events() + + console.log(block) + await this.runFunction(blockDetails, height, innerCode); + } + } + + async executeIndexerFunctionOnHeights(heights, indexingCode) { + console.clear() + console.group('%c Welcome! Lets test your indexing logic on some Near Blocks!', 'color: white; background-color: navy; padding: 5px;'); + if (heights.length === 0) { console.warn("No Block Heights Selected") + return } console.log("Note: GraphQL Mutations & Queries will not be executed on your database. They will simply return an empty object. Please keep this in mind as this may cause unintended behavior of your indexer function.") - let innerCode = indexingCode.match(/getBlock\s*\([^)]*\)\s*{([\s\S]*)}/)[1]; - // for loop with await for await (const height of heights) { console.group(`Block Height #${height}`) - const block_details = await fetchBlockDetails(height); - console.time('Indexing Execution Complete') - if (block_details) { - await this.runFunction(block_details, height, innerCode); + let blockDetails; + try { + blockDetails = await fetchBlockDetails(height); + } catch (error) { + console.log(error) } + console.time('Indexing Execution Complete') + this.executeIndexerFunction(height, blockDetails, indexingCode) console.timeEnd('Indexing Execution Complete') console.groupEnd() } @@ -56,7 +113,7 @@ export default class IndexerRunner { "", () => { console.group(`Setting Key/Value`); - console.log({key: value}); + console.log({[key]: value}); console.groupEnd(); } ); @@ -92,7 +149,6 @@ export default class IndexerRunner { }, }; - // Call the wrapped function, passing the imported Block and streamerMessage wrappedFunction(Block, streamerMessage, context); } @@ -117,45 +173,6 @@ export default class IndexerRunner { ); } - // async runGraphQLQuery( - // operation, - // variables, - // function_name, - // block_height, - // hasuraRoleName, - // logError = true - // ) { - // const response = await this.deps.fetch( - // `${process.env.HASURA_ENDPOINT}/v1/graphql`, - // { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // ...(hasuraRoleName && { "X-Hasura-Role": hasuraRoleName }), - // }, - // body: JSON.stringify({ - // query: operation, - // ...(variables && { variables }), - // }), - // } - // ); - // - // const { data, errors } = await response.json(); - // - // if (response.status !== 200 || errors) { - // if (logError) { - // } - // throw new Error( - // `Failed to write graphql, http status: ${ - // response.status - // }, errors: ${JSON.stringify(errors, null, 2)}` - // ); - // } - // - // return data; - // } - // - renameUnderscoreFieldsToCamelCase(value) { if (value && typeof value === "object" && !Array.isArray(value)) { // It's a non-null, non-array object, create a replacement with the keys initially-capped