diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/SQLEditor.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/SQLEditor.tsx index 96f016a6f7..236042ce82 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/SQLEditor.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/SQLEditor.tsx @@ -1,7 +1,10 @@ "use client"; -import { CommandIcon, CornerDownLeft, LoaderIcon, PauseIcon, PlayIcon } from "lucide-react"; +import { ClockIcon, CommandIcon, CornerDownLeft, LoaderIcon, PauseIcon, PlayIcon } from "lucide-react"; import { KeyCode, KeyMod, editor } from "monaco-editor/esm/vs/editor/editor.api"; +import { useQueryState } from "nuqs"; +import { useConfig } from "wagmi"; +import { getBlock } from "wagmi/actions"; import { useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { Table } from "@latticexyz/config"; @@ -9,7 +12,9 @@ import Editor from "@monaco-editor/react"; import { Tooltip } from "../../../../../../components/Tooltip"; import { Button } from "../../../../../../components/ui/Button"; import { Form, FormField } from "../../../../../../components/ui/Form"; +import { Input } from "../../../../../../components/ui/Input"; import { cn } from "../../../../../../utils"; +import { useChain } from "../../../../hooks/useChain"; import { useTableDataQuery } from "../../../../queries/useTableDataQuery"; import { PAGE_SIZE_OPTIONS, monacoOptions } from "./consts"; import { usePaginationState } from "./hooks/usePaginationState"; @@ -31,6 +36,12 @@ export function SQLEditor({ table, isLiveQuery, setIsLiveQuery }: Props) { const [isUserTriggeredRefetch, setIsUserTriggeredRefetch] = useState(false); const [pagination, setPagination] = usePaginationState(); const [query, setQuery] = useSQLQueryState(); + const [blockHeight, setBlockHeight] = useQueryState("blockHeight"); + const [blockTimestamp, setBlockTimestamp] = useState(null); + const [isLoadingBlock, setIsLoadingBlock] = useState(false); + + const wagmiConfig = useConfig(); + const { id: chainId } = useChain(); const validateQuery = useQueryValidator(table); const { data: tableData, refetch, isRefetching: isTableDataRefetching } = useTableDataQuery({ table, isLiveQuery }); @@ -76,6 +87,32 @@ export function SQLEditor({ table, isLiveQuery, setIsLiveQuery }: Props) { form.reset({ query }); }, [query, form]); + // Fetch block timestamp when blockHeight changes + useEffect(() => { + const fetchBlockTimestamp = async () => { + if (!blockHeight || !wagmiConfig || !chainId) { + setBlockTimestamp(null); + return; + } + + setIsLoadingBlock(true); + try { + const block = await getBlock(wagmiConfig, { + chainId, + blockNumber: BigInt(blockHeight), + }); + setBlockTimestamp(Number(block.timestamp)); + } catch (error) { + console.error("Failed to fetch block timestamp:", error); + setBlockTimestamp(null); + } finally { + setIsLoadingBlock(false); + } + }; + + fetchBlockTimestamp(); + }, [blockHeight, wagmiConfig, chainId]); + const updateHeight = () => { if (editorRef.current) { const contentHeight = Math.min(200, editorRef.current.getContentHeight()); @@ -182,6 +219,29 @@ export function SQLEditor({ table, isLiveQuery, setIsLiveQuery }: Props) { ) : null} + setBlockHeight(e.target.value)} + /> + + {blockHeight && ( +
+ + {isLoadingBlock ? ( + Loading... + ) : blockTimestamp ? ( + + {new Date(blockTimestamp * 1000).toLocaleString()} + + ) : ( + Invalid block + )} +
+ )} +