diff --git a/.changeset/cyan-chefs-obey.md b/.changeset/cyan-chefs-obey.md new file mode 100644 index 0000000000..05462415c3 --- /dev/null +++ b/.changeset/cyan-chefs-obey.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/explorer": patch +--- + +Exploring worlds on Redstone and Garnet chains will now retrieve data from the hosted SQL indexer. diff --git a/packages/explorer/package.json b/packages/explorer/package.json index 9582c238d3..5f2d5fd3fa 100644 --- a/packages/explorer/package.json +++ b/packages/explorer/package.json @@ -37,6 +37,7 @@ "dependencies": { "@hookform/resolvers": "^3.9.0", "@latticexyz/common": "workspace:*", + "@latticexyz/config": "workspace:*", "@latticexyz/protocol-parser": "workspace:*", "@latticexyz/schema-type": "workspace:*", "@latticexyz/store": "workspace:*", diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/Providers.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/Providers.tsx index c1d4a45e8d..614b38186c 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/Providers.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/Providers.tsx @@ -7,7 +7,7 @@ import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit"; import "@rainbow-me/rainbowkit/styles.css"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { getDefaultAnvilConnectors } from "../../../../../connectors/anvil"; -import { useChain } from "../../../../../hooks/useChain"; +import { useChain } from "../../../hooks/useChain"; const queryClient = new QueryClient(); diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/DataExplorer.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/DataExplorer.tsx deleted file mode 100644 index f2082085c8..0000000000 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/DataExplorer.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import { Loader } from "lucide-react"; -import { useQuery } from "@tanstack/react-query"; -import { TableSelector } from "./TableSelector"; -import { TablesViewer } from "./TablesViewer"; - -export function DataExplorer() { - const { data: tables, isLoading } = useQuery({ - queryKey: ["tables"], - queryFn: async () => { - const response = await fetch("/api/tables"); - const json = await response.json(); - if (!response.ok) { - throw new Error(json.error); - } - return json; - }, - select: (data) => data.tables.map((table: { name: string }) => table.name), - refetchInterval: 15000, - throwOnError: true, - retry: false, - }); - - if (isLoading) { - return ; - } - - return ( - <> - - - - ); -} diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/EditableTableCell.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/EditableTableCell.tsx index 51839a1ad3..ac5aa1e3ed 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/EditableTableCell.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/EditableTableCell.tsx @@ -4,44 +4,48 @@ import { toast } from "sonner"; import { Hex } from "viem"; import { useAccount, useConfig } from "wagmi"; import { ChangeEvent, useState } from "react"; -import { encodeField, getFieldIndex } from "@latticexyz/protocol-parser/internal"; -import { SchemaAbiType } from "@latticexyz/schema-type/internal"; +import { Table } from "@latticexyz/config"; +import { + ValueSchema, + encodeField, + getFieldIndex, + getSchemaTypes, + getValueSchema, +} from "@latticexyz/protocol-parser/internal"; import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { waitForTransactionReceipt, writeContract } from "@wagmi/core"; import { Checkbox } from "../../../../../../components/ui/Checkbox"; -import { useChain } from "../../../../../../hooks/useChain"; -import { camelCase, cn } from "../../../../../../lib/utils"; -import { TableConfig } from "../../../../../api/table/route"; +import { cn } from "../../../../../../utils"; +import { useChain } from "../../../../hooks/useChain"; type Props = { name: string; - value: string; - keyTuple: string[]; - config: TableConfig; + value: string | undefined; + table: Table; + keyTuple: readonly Hex[]; }; -export function EditableTableCell({ name, config, keyTuple, value: defaultValue }: Props) { +export function EditableTableCell({ name, table, keyTuple, value: defaultValue }: Props) { + const [value, setValue] = useState(defaultValue); const wagmiConfig = useConfig(); const queryClient = useQueryClient(); const { worldAddress } = useParams(); const { id: chainId } = useChain(); const account = useAccount(); - const [value, setValue] = useState(defaultValue); - - const tableId = config?.table_id; - const fieldType = config?.value_schema[camelCase(name)] as SchemaAbiType; + const valueSchema = getValueSchema(table); + const fieldType = valueSchema[name as never].type; const { mutate, isPending } = useMutation({ mutationFn: async (newValue: unknown) => { - const fieldIndex = getFieldIndex(config?.value_schema, camelCase(name)); - const encodedField = encodeField(fieldType, newValue); + const fieldIndex = getFieldIndex(getSchemaTypes(valueSchema), name); + const encodedFieldValue = encodeField(fieldType, newValue); const txHash = await writeContract(wagmiConfig, { abi: IBaseWorldAbi, address: worldAddress as Hex, functionName: "setField", - args: [tableId, keyTuple, fieldIndex, encodedField], + args: [table.tableId, keyTuple, fieldIndex, encodedFieldValue], chainId, }); diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/Explorer.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/Explorer.tsx new file mode 100644 index 0000000000..189f859ca1 --- /dev/null +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/Explorer.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { parseAsString, useQueryState } from "nuqs"; +import { Hex } from "viem"; +import { useEffect } from "react"; +import { useChain } from "../../../../hooks/useChain"; +import { usePrevious } from "../../../../hooks/usePrevious"; +import { useTableDataQuery } from "../../../../queries/useTableDataQuery"; +import { useTablesQuery } from "../../../../queries/useTablesQuery"; +import { constructTableName } from "../../../../utils/constructTableName"; +import { indexerForChainId } from "../../../../utils/indexerForChainId"; +import { SQLEditor } from "./SQLEditor"; +import { TableSelector } from "./TableSelector"; +import { TablesViewer } from "./TablesViewer"; + +export function Explorer() { + const { worldAddress } = useParams(); + const { id: chainId } = useChain(); + const indexer = indexerForChainId(chainId); + const [query, setQuery] = useQueryState("query", parseAsString.withDefault("")); + const [selectedTableId] = useQueryState("tableId"); + const prevSelectedTableId = usePrevious(selectedTableId); + + const { data: tables } = useTablesQuery(); + const table = tables?.find(({ tableId }) => tableId === selectedTableId); + const { data: tableData, isLoading, isFetched } = useTableDataQuery({ table, query }); + + useEffect(() => { + if (table && (!query || prevSelectedTableId !== selectedTableId)) { + const tableName = constructTableName(table, worldAddress as Hex, chainId); + + if (indexer.type === "sqlite") { + setQuery(`SELECT * FROM "${tableName}"`); + } else { + setQuery(`SELECT ${Object.keys(table.schema).join(", ")} FROM ${tableName}`); + } + } + }, [chainId, setQuery, selectedTableId, table, worldAddress, prevSelectedTableId, query, indexer.type]); + + return ( + <> + {indexer.type !== "sqlite" && } + + + + ); +} 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 new file mode 100644 index 0000000000..edffb85f7c --- /dev/null +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/SQLEditor.tsx @@ -0,0 +1,47 @@ +import { PlayIcon } from "lucide-react"; +import { useQueryState } from "nuqs"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { Button } from "../../../../../../components/ui/Button"; +import { Form, FormControl, FormField, FormItem } from "../../../../../../components/ui/Form"; +import { Input } from "../../../../../../components/ui/Input"; + +export function SQLEditor() { + const [query, setQuery] = useQueryState("query"); + const form = useForm({ + defaultValues: { + query: query || "", + }, + }); + + const handleSubmit = form.handleSubmit((data) => { + setQuery(data.query); + }); + + useEffect(() => { + form.reset({ query: query || "" }); + }, [query, form]); + + return ( +
+ +
+ ( + + + + + + )} + /> + + +
+
+ + ); +} diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TableSelector.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TableSelector.tsx index c3f0582dd4..5f8a32e09a 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TableSelector.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TableSelector.tsx @@ -1,9 +1,9 @@ -import { Check, ChevronsUpDown, Lock } from "lucide-react"; -import { useParams } from "next/navigation"; +import { CheckIcon, ChevronsUpDownIcon, Link2Icon, Link2OffIcon } from "lucide-react"; import { useQueryState } from "nuqs"; import { Hex } from "viem"; -import { useEffect, useState } from "react"; -import { internalTableNames } from "@latticexyz/store-sync/sqlite"; +import { useState } from "react"; +import { useEffect } from "react"; +import { Table } from "@latticexyz/config"; import { Button } from "../../../../../../components/ui/Button"; import { Command, @@ -14,20 +14,28 @@ import { CommandList, } from "../../../../../../components/ui/Command"; import { Popover, PopoverContent, PopoverTrigger } from "../../../../../../components/ui/Popover"; -import { cn } from "../../../../../../lib/utils"; +import { cn } from "../../../../../../utils"; -type Props = { - tables: string[]; -}; +function TableSelectorItem({ table, selected, asOption }: { table: Table; selected: boolean; asOption?: boolean }) { + const { type, name, namespace } = table; + return ( +
+ {asOption && } + {type === "offchainTable" && } + {type === "table" && } + {name} {namespace && ({namespace})} +
+ ); +} -export function TableSelector({ tables }: Props) { +export function TableSelector({ tables }: { tables?: Table[] }) { const [selectedTableId, setTableId] = useQueryState("tableId"); const [open, setOpen] = useState(false); - const { worldAddress } = useParams(); + const selectedTableConfig = tables?.find(({ tableId }) => tableId === selectedTableId); useEffect(() => { - if (!selectedTableId && tables.length > 0) { - setTableId(tables[0] as Hex); + if (!selectedTableId && Array.isArray(tables) && tables.length > 0) { + setTableId(tables[0].tableId); } }, [selectedTableId, setTableId, tables]); @@ -35,11 +43,21 @@ export function TableSelector({ tables }: Props) {
- @@ -47,26 +65,20 @@ export function TableSelector({ tables }: Props) { - No framework found. + No table found. - {tables.map((tableId) => { + {tables?.map((table) => { return ( { setTableId(newTableId as Hex); setOpen(false); }} className="font-mono" > - - {(internalTableNames as string[]).includes(tableId) && ( - - )} - {tableId.replace(`${worldAddress}__`, "")} + ); })} diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx index bba5a1d79f..0f2a357669 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx @@ -1,7 +1,8 @@ -import { ArrowUpDown, Loader } from "lucide-react"; -import { parseAsBoolean, parseAsJson, parseAsString, useQueryState } from "nuqs"; -import { internalTableNames } from "@latticexyz/store-sync/sqlite"; -import { useQuery } from "@tanstack/react-query"; +import { ArrowUpDownIcon, LoaderIcon } from "lucide-react"; +import { parseAsJson, parseAsString, useQueryState } from "nuqs"; +import { useMemo } from "react"; +import { Schema, Table as TableType } from "@latticexyz/config"; +import { getKeySchema, getKeyTuple, getSchemaPrimitives } from "@latticexyz/protocol-parser/internal"; import { ColumnDef, SortingState, @@ -12,126 +13,67 @@ import { getSortedRowModel, useReactTable, } from "@tanstack/react-table"; +import { internalNamespaces } from "../../../../../../common"; import { Button } from "../../../../../../components/ui/Button"; -import { Checkbox } from "../../../../../../components/ui/Checkbox"; import { Input } from "../../../../../../components/ui/Input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../../components/ui/Table"; -import { bufferToBigInt } from "../utils/bufferToBigInt"; +import { TableData } from "../../../../queries/useTableDataQuery"; import { EditableTableCell } from "./EditableTableCell"; const initialSortingState: SortingState = []; +const initialRows: TableData["rows"] = []; -export function TablesViewer() { - const [selectedTableId] = useQueryState("tableId", parseAsString.withDefault("")); +export function TablesViewer({ + table, + tableData, + isLoading, +}: { + table?: TableType; + tableData?: TableData; + isLoading: boolean; +}) { const [globalFilter, setGlobalFilter] = useQueryState("filter", parseAsString.withDefault("")); - const [showAllColumns, setShowAllColumns] = useQueryState("showAllColumns", parseAsBoolean.withDefault(false)); const [sorting, setSorting] = useQueryState("sort", parseAsJson().withDefault(initialSortingState)); - const { data: schema } = useQuery({ - queryKey: ["schema", { table: selectedTableId }], - queryFn: async () => { - const response = await fetch(`/api/schema?${new URLSearchParams({ table: selectedTableId })}`); - return response.json(); - }, - select: (data) => { - return data.schema.filter((column: { name: string }) => { - if (showAllColumns) { - return true; - } - return !column.name.startsWith("__"); - }); - }, - enabled: Boolean(selectedTableId), - }); - - const { data: rows } = useQuery({ - queryKey: ["rows", { table: selectedTableId }], - queryFn: async () => { - const response = await fetch(`/api/rows?${new URLSearchParams({ table: selectedTableId })}`); - return response.json(); - }, - select: (data) => { - return data.rows.map((row: object) => { - return Object.fromEntries( - Object.entries(row).map(([key, value]) => { - if (value?.type === "Buffer") { - return [key, bufferToBigInt(value?.data)]; - } - return [key, value]; - }), - ); - }); - }, - enabled: Boolean(selectedTableId), - refetchInterval: 1000, - }); + const tableColumns: ColumnDef>[] = useMemo(() => { + if (!table || !tableData) return []; - const { data: mudTableConfig } = useQuery({ - queryKey: ["table", { selectedTableId }], - queryFn: async () => { - const response = await fetch(`/api/table?${new URLSearchParams({ table: selectedTableId })}`); - return response.json(); - }, - select: (data) => { + return tableData.columns.map((name) => { + const type = table?.schema[name]?.type; return { - ...data.table, - key_schema: JSON.parse(data.table.key_schema).json, - value_schema: JSON.parse(data.table.value_schema).json, - }; - }, - enabled: Boolean(selectedTableId), - }); + accessorKey: name, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const namespace = table?.namespace; + const keySchema = getKeySchema(table); + const value = row.getValue(name)?.toString(); - const columns: ColumnDef<{}>[] = schema?.map(({ name, type }: { name: string; type: string }) => { - return { - accessorKey: name, - header: ({ - column, - }: { - column: { - toggleSorting: (ascending: boolean) => void; - getIsSorted: () => "asc" | "desc" | undefined; - }; - }) => { - return ( - - ); - }, - cell: ({ - row, - }: { - row: { - getValue: (name: string) => string; - }; - }) => { - const keysSchema = Object.keys(mudTableConfig?.key_schema || {}); - const keyTuple = keysSchema.map((key) => row.getValue(key)); - const value = row.getValue(name); - if ( - (selectedTableId && (internalTableNames as string[]).includes(selectedTableId)) || - keysSchema.includes(name) - ) { - return value?.toString(); - } + if (!table || Object.keys(keySchema).includes(name) || internalNamespaces.includes(namespace)) { + return value; + } - return ; - }, - }; - }); + const keyTuple = getKeyTuple(table, row.original as never); + return ; + }, + }; + }); + }, [table, tableData]); - const table = useReactTable({ - data: rows, - columns, + const reactTable = useReactTable({ + data: tableData?.rows ?? initialRows, + columns: tableColumns, initialState: { pagination: { pageSize: 50, @@ -150,88 +92,76 @@ export function TablesViewer() { }, }); - if (!schema || !rows) { - return ( -
- -
- ); - } - return ( <>
table.setGlobalFilter(event.target.value)} + onChange={(event) => reactTable.setGlobalFilter(event.target.value)} className="max-w-sm rounded border px-2 py-1" + disabled={!tableData} /> - -
- { - setShowAllColumns(!showAllColumns); - }} - /> -
- -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - {flexRender(cell.column.columnDef.cell, cell.getContext())} - ))} + {isLoading && ( +
+ +
+ )} + {!isLoading && ( +
+ + {reactTable.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} - )) - ) : ( - - - No results. - - - )} - -
+ ))} + + + {reactTable.getRowModel().rows?.length ? ( + reactTable.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + )) + ) : ( + + + No results. + + + )} + + + )}
+
-
diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/page.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/page.tsx index b019622675..440a52a88e 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/page.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/page.tsx @@ -1,12 +1,5 @@ -import { Suspense } from "react"; -import { DataExplorer } from "./DataExplorer"; +import { Explorer } from "./Explorer"; -export default function ExplorerPage() { - return ( -
- - - -
- ); +export default function ExplorePage() { + return ; } diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/Form.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/Form.tsx index 15521e3f2d..615b090e97 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/Form.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/Form.tsx @@ -6,14 +6,14 @@ import { useDeferredValue, useState } from "react"; import { Input } from "../../../../../../components/ui/Input"; import { Separator } from "../../../../../../components/ui/Separator"; import { Skeleton } from "../../../../../../components/ui/Skeleton"; -import { useHashState } from "../../../../../../hooks/useHashState"; -import { cn } from "../../../../../../lib/utils"; -import { useAbiQuery } from "../../../../../../queries/useAbiQuery"; +import { cn } from "../../../../../../utils"; +import { useHashState } from "../../../../hooks/useHashState"; +import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery"; import { FunctionField } from "./FunctionField"; export function Form() { const [hash] = useHashState(); - const { data, isFetched } = useAbiQuery(); + const { data, isFetched } = useWorldAbiQuery(); const [filterValue, setFilterValue] = useState(""); const deferredFilterValue = useDeferredValue(filterValue); const filteredFunctions = data?.abi?.filter((item) => diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts index 50e02e7b06..df44d64e27 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts @@ -4,7 +4,7 @@ import { Abi, AbiFunction, Hex } from "viem"; import { useAccount, useConfig } from "wagmi"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { readContract, waitForTransactionReceipt, writeContract } from "@wagmi/core"; -import { useChain } from "../../../../../../hooks/useChain"; +import { useChain } from "../../../../hooks/useChain"; import { FunctionType } from "./FunctionField"; type UseContractMutationProps = { diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/utils/bufferToBigInt.ts b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/utils/bufferToBigInt.ts deleted file mode 100644 index 302d98d3d1..0000000000 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/utils/bufferToBigInt.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function bufferToBigInt(bufferData: number[]) { - return BigInt(Buffer.from(bufferData).toString()); -} diff --git a/packages/explorer/src/app/(explorer)/api/sqlite-indexer/route.ts b/packages/explorer/src/app/(explorer)/api/sqlite-indexer/route.ts new file mode 100644 index 0000000000..591c7a16eb --- /dev/null +++ b/packages/explorer/src/app/(explorer)/api/sqlite-indexer/route.ts @@ -0,0 +1,36 @@ +import { getDatabase } from "../utils/getDatabase"; + +type Row = { + [key: string]: string; +}; + +type SqliteTable = Row[] | undefined; + +export const dynamic = "force-dynamic"; + +export async function POST(request: Request) { + const queries = await request.json(); + if (!queries.length) { + return Response.json({ error: "No queries provided" }, { status: 400 }); + } + + try { + const db = getDatabase(); + const result = []; + for (const { query } of queries) { + const data = (await db?.prepare(query).all()) as SqliteTable; + if (!data) { + throw new Error("No data found"); + } + + const columns = Object.keys(data[0]).map((key) => key.replaceAll("_", "").toLowerCase()); + const rows = data.map((row) => Object.values(row).map((value) => value.toString())); + result.push([columns, ...rows]); + } + + return Response.json({ result }); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; + return Response.json({ error: errorMessage }, { status: 400 }); + } +} diff --git a/packages/explorer/src/app/(explorer)/api/utils/decodeTable.ts b/packages/explorer/src/app/(explorer)/api/utils/decodeTable.ts new file mode 100644 index 0000000000..3c4cfd5f20 --- /dev/null +++ b/packages/explorer/src/app/(explorer)/api/utils/decodeTable.ts @@ -0,0 +1,41 @@ +import { Hex, decodeAbiParameters, parseAbiParameters } from "viem"; +import { hexToResource } from "@latticexyz/common"; +import { Schema, Table } from "@latticexyz/config"; +import { getSchemaPrimitives, hexToSchema } from "@latticexyz/protocol-parser/internal"; + +export const decodeTable = ({ + tableId, + keySchema: encodedKeySchema, + valueSchema: encodedValueSchema, + abiEncodedKeyNames, + abiEncodedFieldNames, +}: getSchemaPrimitives): Table => { + const { type, namespace, name } = hexToResource(tableId as Hex); + + const solidityKeySchema = hexToSchema(encodedKeySchema as Hex); + const solidityValueSchema = hexToSchema(encodedValueSchema as Hex); + const keyNames = decodeAbiParameters(parseAbiParameters("string[]"), abiEncodedKeyNames as Hex)[0]; + const fieldNames = decodeAbiParameters(parseAbiParameters("string[]"), abiEncodedFieldNames as Hex)[0]; + + const valueAbiTypes = [...solidityValueSchema.staticFields, ...solidityValueSchema.dynamicFields]; + const keySchema = Object.fromEntries( + solidityKeySchema.staticFields.map((abiType, i) => [keyNames[i], { type: abiType, internalType: abiType }]), + ) satisfies Schema; + const valueSchema = Object.fromEntries( + valueAbiTypes.map((abiType, i) => [fieldNames[i], { type: abiType, internalType: abiType }]), + ) satisfies Schema; + + return { + tableId: tableId as Hex, + name, + namespace, + label: name, + namespaceLabel: namespace, + type: type as Table["type"], + schema: { + ...keySchema, + ...valueSchema, + }, + key: Object.keys(keySchema), + }; +}; diff --git a/packages/explorer/src/app/(explorer)/api/utils/getDatabase.ts b/packages/explorer/src/app/(explorer)/api/utils/getDatabase.ts new file mode 100644 index 0000000000..a27cf23d3e --- /dev/null +++ b/packages/explorer/src/app/(explorer)/api/utils/getDatabase.ts @@ -0,0 +1,18 @@ +import sqliteDB, { Database } from "better-sqlite3"; +import fs from "fs"; + +export function getDatabase(): Database | null { + const dbPath = process.env.INDEXER_DATABASE as string; + if (!fs.existsSync(dbPath)) { + throw new Error( + "Database cannot be found. Make sure --indexerDatabase flag or INDEXER_DATABASE environment variable are set, and the indexer is running.", + ); + } + + const db = new sqliteDB(dbPath); + if (!db) { + throw new Error("Database path found but failed to initialize."); + } + + return db; +} diff --git a/packages/explorer/src/app/api/world/route.ts b/packages/explorer/src/app/(explorer)/api/world-abi/route.ts similarity index 79% rename from packages/explorer/src/app/api/world/route.ts rename to packages/explorer/src/app/(explorer)/api/world-abi/route.ts index 4e4aa12ce9..12ab9df73b 100644 --- a/packages/explorer/src/app/api/world/route.ts +++ b/packages/explorer/src/app/(explorer)/api/world-abi/route.ts @@ -3,12 +3,12 @@ import { getBlockNumber, getLogs } from "viem/actions"; import { helloStoreEvent } from "@latticexyz/store"; import { helloWorldEvent } from "@latticexyz/world"; import { getWorldAbi } from "@latticexyz/world/internal"; -import { supportedChainId, supportedChains, validateChainId } from "../../../common"; +import { chainIdToName, supportedChainId, supportedChains, validateChainId } from "../../../../common"; export const dynamic = "force-dynamic"; async function getClient(chainId: supportedChainId) { - const chain = Object.values(supportedChains).find((c) => c.id === chainId); + const chain = supportedChains[chainIdToName[chainId]]; const client = createWalletClient({ chain, transport: http(), @@ -36,13 +36,13 @@ async function getParameters(chainId: supportedChainId, worldAddress: Address) { export async function GET(req: Request) { const { searchParams } = new URL(req.url); - const worldAddress = searchParams.get("worldAddress") as Hex; const chainId = Number(searchParams.get("chainId")); - validateChainId(chainId); + const worldAddress = searchParams.get("worldAddress") as Hex; - if (!worldAddress) { - return Response.json({ error: "address is required" }, { status: 400 }); + if (!chainId || !worldAddress) { + return Response.json({ error: "Missing chainId or worldAddress" }, { status: 400 }); } + validateChainId(chainId); try { const client = await getClient(chainId); @@ -59,10 +59,7 @@ export async function GET(req: Request) { return Response.json({ abi, isWorldDeployed }); } catch (error: unknown) { - if (error instanceof Error) { - return Response.json({ error: error.message }, { status: 400 }); - } else { - return Response.json({ error: "An unknown error occurred" }, { status: 400 }); - } + const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; + return Response.json({ error: errorMessage }, { status: 400 }); } } diff --git a/packages/explorer/src/app/(explorer)/error.tsx b/packages/explorer/src/app/(explorer)/error.tsx index c599bd6ab8..4dd10754d4 100644 --- a/packages/explorer/src/app/(explorer)/error.tsx +++ b/packages/explorer/src/app/(explorer)/error.tsx @@ -3,7 +3,7 @@ import { ExternalLink, RefreshCwIcon } from "lucide-react"; import Link from "next/link"; import { Button } from "../../components/ui/Button"; -import { useWorldUrl } from "../../hooks/useWorldUrl"; +import { useWorldUrl } from "./hooks/useWorldUrl"; type Props = { error: Error & { digest?: string }; diff --git a/packages/explorer/src/hooks/useChain.ts b/packages/explorer/src/app/(explorer)/hooks/useChain.ts similarity index 77% rename from packages/explorer/src/hooks/useChain.ts rename to packages/explorer/src/app/(explorer)/hooks/useChain.ts index 31556bda55..e6a1b0a54d 100644 --- a/packages/explorer/src/hooks/useChain.ts +++ b/packages/explorer/src/app/(explorer)/hooks/useChain.ts @@ -1,6 +1,6 @@ import { useParams } from "next/navigation"; import { Chain } from "viem"; -import { supportedChains, validateChainName } from "../common"; +import { supportedChains, validateChainName } from "../../../common"; export function useChain(): Chain { const { chainName } = useParams(); diff --git a/packages/explorer/src/hooks/useHashState.ts b/packages/explorer/src/app/(explorer)/hooks/useHashState.ts similarity index 100% rename from packages/explorer/src/hooks/useHashState.ts rename to packages/explorer/src/app/(explorer)/hooks/useHashState.ts diff --git a/packages/explorer/src/app/(explorer)/hooks/usePrevious.ts b/packages/explorer/src/app/(explorer)/hooks/usePrevious.ts new file mode 100644 index 0000000000..4268077936 --- /dev/null +++ b/packages/explorer/src/app/(explorer)/hooks/usePrevious.ts @@ -0,0 +1,9 @@ +import { useEffect, useRef } from "react"; + +export function usePrevious(value: T): T | undefined { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +} diff --git a/packages/explorer/src/hooks/useWorldUrl.ts b/packages/explorer/src/app/(explorer)/hooks/useWorldUrl.ts similarity index 100% rename from packages/explorer/src/hooks/useWorldUrl.ts rename to packages/explorer/src/app/(explorer)/hooks/useWorldUrl.ts diff --git a/packages/explorer/src/app/(explorer)/not-found.tsx b/packages/explorer/src/app/(explorer)/not-found.tsx index 44059b412c..3ffe2a154f 100644 --- a/packages/explorer/src/app/(explorer)/not-found.tsx +++ b/packages/explorer/src/app/(explorer)/not-found.tsx @@ -3,7 +3,7 @@ import { ExternalLink } from "lucide-react"; import Link from "next/link"; import { Button } from "../../components/ui/Button"; -import { useWorldUrl } from "../../hooks/useWorldUrl"; +import { useWorldUrl } from "./hooks/useWorldUrl"; export default function NotFound() { const getUrl = useWorldUrl(); diff --git a/packages/explorer/src/app/(explorer)/queries/useTableDataQuery.ts b/packages/explorer/src/app/(explorer)/queries/useTableDataQuery.ts new file mode 100644 index 0000000000..32b9621e9e --- /dev/null +++ b/packages/explorer/src/app/(explorer)/queries/useTableDataQuery.ts @@ -0,0 +1,63 @@ +import { useParams } from "next/navigation"; +import { Hex } from "viem"; +import { Table } from "@latticexyz/config"; +import { useQuery } from "@tanstack/react-query"; +import { useChain } from "../hooks/useChain"; +import { DozerResponse } from "../types"; +import { indexerForChainId } from "../utils/indexerForChainId"; + +type Props = { + table: Table | undefined; + query: string | undefined; +}; + +export type TableData = { + columns: string[]; + rows: Record[]; +}; + +export function useTableDataQuery({ table, query }: Props) { + const { chainName, worldAddress } = useParams(); + const { id: chainId } = useChain(); + + return useQuery({ + queryKey: ["tableData", chainName, worldAddress, query], + queryFn: async () => { + const indexer = indexerForChainId(chainId); + const response = await fetch(indexer.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify([ + { + address: worldAddress as Hex, + query, + }, + ]), + }); + + return response.json(); + }, + select: (data: DozerResponse) => { + if (!table || !data?.result?.[0]) return; + + const schemaKeys = Object.keys(table.schema); + const result = data.result[0]; + const columnKeys = result[0] + .map((columnKey) => { + const schemaKey = schemaKeys.find((schemaKey) => schemaKey.toLowerCase() === columnKey); + return schemaKey || columnKey; + }) + .filter((key) => schemaKeys.includes(key)); + const rows = result.slice(1).map((row) => Object.fromEntries(columnKeys.map((key, index) => [key, row[index]]))); + + return { + columns: columnKeys, + rows, + }; + }, + enabled: !!table && !!query, + refetchInterval: 1_000, + }); +} diff --git a/packages/explorer/src/app/(explorer)/queries/useTablesQuery.ts b/packages/explorer/src/app/(explorer)/queries/useTablesQuery.ts new file mode 100644 index 0000000000..197826c86c --- /dev/null +++ b/packages/explorer/src/app/(explorer)/queries/useTablesQuery.ts @@ -0,0 +1,56 @@ +import { useParams } from "next/navigation"; +import { Hex } from "viem"; +import { Table } from "@latticexyz/config"; +import mudConfig from "@latticexyz/store/mud.config"; +import { useQuery } from "@tanstack/react-query"; +import { internalNamespaces } from "../../../common"; +import { decodeTable } from "../api/utils/decodeTable"; +import { useChain } from "../hooks/useChain"; +import { DozerResponse } from "../types"; +import { indexerForChainId } from "../utils/indexerForChainId"; + +export function useTablesQuery() { + const { worldAddress, chainName } = useParams(); + const { id: chainId } = useChain(); + + return useQuery({ + queryKey: ["tables", worldAddress, chainName], + queryFn: async () => { + const indexer = indexerForChainId(chainId); + const tableName = "store__Tables"; + const query = + indexer.type === "sqlite" + ? `SELECT * FROM "${worldAddress}__${tableName}"` + : `SELECT ${Object.keys(mudConfig.tables[tableName].schema).join(", ")} FROM ${tableName}`; + + const response = await fetch(indexer.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify([ + { + address: worldAddress as Hex, + query, + }, + ]), + }); + + return response.json(); + }, + select: (data: DozerResponse) => { + return data.result[0] + .slice(1) + .map((row: string[]) => { + return decodeTable({ + tableId: row[0], + keySchema: row[2], + valueSchema: row[3], + abiEncodedKeyNames: row[4], + abiEncodedFieldNames: row[5], + }); + }) + .sort(({ namespace }) => (internalNamespaces.includes(namespace) ? 1 : -1)); + }, + }); +} diff --git a/packages/explorer/src/app/(explorer)/queries/useWorldAbiQuery.ts b/packages/explorer/src/app/(explorer)/queries/useWorldAbiQuery.ts new file mode 100644 index 0000000000..da48a9739c --- /dev/null +++ b/packages/explorer/src/app/(explorer)/queries/useWorldAbiQuery.ts @@ -0,0 +1,33 @@ +import { useParams } from "next/navigation"; +import { AbiFunction, Hex } from "viem"; +import { UseQueryResult, useQuery } from "@tanstack/react-query"; +import { supportedChains, validateChainName } from "../../../common"; + +type AbiQueryResult = { + abi: AbiFunction[]; + isWorldDeployed: boolean; +}; + +export function useWorldAbiQuery(): UseQueryResult { + const { chainName, worldAddress } = useParams(); + validateChainName(chainName); + const { id: chainId } = supportedChains[chainName]; + + return useQuery({ + queryKey: ["worldAbi", chainName, worldAddress], + queryFn: async () => { + const res = await fetch( + `/api/world-abi?${new URLSearchParams({ chainId: chainId.toString(), worldAddress: worldAddress as Hex })}`, + ); + const data = await res.json(); + return data; + }, + select: (data) => { + return { + abi: data.abi || [], + isWorldDeployed: data.isWorldDeployed, + }; + }, + refetchInterval: 15000, + }); +} diff --git a/packages/explorer/src/app/(explorer)/types.ts b/packages/explorer/src/app/(explorer)/types.ts new file mode 100644 index 0000000000..22c479bcb5 --- /dev/null +++ b/packages/explorer/src/app/(explorer)/types.ts @@ -0,0 +1,4 @@ +export type DozerResponse = { + block_height: number; + result: [string[][]]; +}; diff --git a/packages/explorer/src/app/(explorer)/utils/constructTableName.ts b/packages/explorer/src/app/(explorer)/utils/constructTableName.ts new file mode 100644 index 0000000000..e706c56726 --- /dev/null +++ b/packages/explorer/src/app/(explorer)/utils/constructTableName.ts @@ -0,0 +1,13 @@ +import { Hex } from "viem"; +import { Table } from "@latticexyz/config"; +import { snakeCase } from "../../../utils"; +import { indexerForChainId } from "./indexerForChainId"; + +export function constructTableName(table: Table, worldAddress: Hex, chainId: number) { + const indexer = indexerForChainId(chainId); + let tableId = table.name; + if (table.namespace) { + tableId = `${table.namespace}${indexer.type === "sqlite" ? "_" : "__"}${tableId}`; + } + return indexer.type === "sqlite" ? `${worldAddress}__${snakeCase(tableId)}`.toLowerCase() : tableId; +} diff --git a/packages/explorer/src/app/(explorer)/utils/indexerForChainId.ts b/packages/explorer/src/app/(explorer)/utils/indexerForChainId.ts new file mode 100644 index 0000000000..e7f23ed1ae --- /dev/null +++ b/packages/explorer/src/app/(explorer)/utils/indexerForChainId.ts @@ -0,0 +1,15 @@ +import { anvil } from "viem/chains"; +import { MUDChain } from "@latticexyz/common/chains"; +import { chainIdToName, supportedChains, validateChainId } from "../../../common"; + +export function indexerForChainId(chainId: number): { type: "sqlite" | "hosted"; url: string } { + validateChainId(chainId); + + if (chainId === anvil.id) { + return { type: "sqlite", url: "/api/sqlite-indexer" }; + } + + const chainName = chainIdToName[chainId]; + const chain = supportedChains[chainName] as MUDChain; + return { type: "hosted", url: new URL("/q", chain.indexerUrl).toString() }; +} diff --git a/packages/explorer/src/app/api/rows/route.ts b/packages/explorer/src/app/api/rows/route.ts deleted file mode 100644 index 7882ae3364..0000000000 --- a/packages/explorer/src/app/api/rows/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { getDatabase } from "../utils/getDatabase"; - -export const dynamic = "force-dynamic"; - -type Row = { - [key: string]: string; -}; - -type RowsResponse = Row[] | undefined; - -function doesTableExist(table: string) { - const db = getDatabase(); - const result = db?.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(table); - - return Boolean(result); -} - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url); - const table = searchParams.get("table"); - - try { - if (!table || !doesTableExist(table)) { - return Response.json({ error: "table does not exist" }, { status: 400 }); - } - - const db = getDatabase(); - const query = `SELECT * FROM "${table}" LIMIT 30`; - const rows = db?.prepare(query).all() as RowsResponse; - - return Response.json({ rows }); - } catch (error: unknown) { - if (error instanceof Error) { - return Response.json({ error: error.message }, { status: 400 }); - } else { - return Response.json({ error: "An unknown error occurred" }, { status: 400 }); - } - } -} diff --git a/packages/explorer/src/app/api/schema/route.ts b/packages/explorer/src/app/api/schema/route.ts deleted file mode 100644 index c031eb3a12..0000000000 --- a/packages/explorer/src/app/api/schema/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { getDatabase } from "../utils/getDatabase"; - -export const dynamic = "force-dynamic"; - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url); - const table = searchParams.get("table"); - - if (!table) { - return new Response(JSON.stringify({ error: "table is required" }), { - status: 400, - }); - } - - try { - const db = getDatabase(); - const schema = db?.prepare("SELECT * FROM pragma_table_info(?)").all(table); - - return new Response(JSON.stringify({ schema }), { status: 200 }); - } catch (error: unknown) { - if (error instanceof Error) { - return Response.json({ error: error.message }, { status: 400 }); - } else { - return Response.json({ error: "An unknown error occurred" }, { status: 400 }); - } - } -} diff --git a/packages/explorer/src/app/api/table/route.ts b/packages/explorer/src/app/api/table/route.ts deleted file mode 100644 index 8d455a0fba..0000000000 --- a/packages/explorer/src/app/api/table/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Hex } from "viem"; -import { KeySchema, ValueSchema } from "@latticexyz/protocol-parser/internal"; -import { getDatabase } from "../utils/getDatabase"; - -export const dynamic = "force-dynamic"; - -export type TableConfig = { - address: Hex; - id: string; - key_schema: KeySchema; - last_error: string | null; - name: string; - namespace: string; - schema_version: number; - table_id: Hex; - value_schema: ValueSchema; -}; - -export async function GET(req: Request) { - const { searchParams } = new URL(req.url); - const table = searchParams.get("table") as Hex; - - if (!table) { - return Response.json({ error: "table is required" }, { status: 400 }); - } - - try { - const db = getDatabase(); - const tableData = db?.prepare("SELECT * FROM __mudStoreTables WHERE id = ?").get(table) as TableConfig; - - return Response.json({ table: tableData }); - } catch (error: unknown) { - if (error instanceof Error) { - return Response.json({ error: error.message }, { status: 400 }); - } else { - return Response.json({ error: "An unknown error occurred" }, { status: 400 }); - } - } -} diff --git a/packages/explorer/src/app/api/tables/route.ts b/packages/explorer/src/app/api/tables/route.ts deleted file mode 100644 index 2d71f8bc66..0000000000 --- a/packages/explorer/src/app/api/tables/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { getDatabase } from "../utils/getDatabase"; - -export const dynamic = "force-dynamic"; - -export type TableRow = { - name: string; -}; - -export async function GET() { - try { - const db = getDatabase(); - const tables = db?.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as TableRow[]; - - return Response.json({ tables }); - } catch (error: unknown) { - if (error instanceof Error) { - return Response.json({ error: error.message }, { status: 400 }); - } else { - return Response.json({ error: "An unknown error occurred" }, { status: 400 }); - } - } -} diff --git a/packages/explorer/src/common.ts b/packages/explorer/src/common.ts index f139856721..177ee8285f 100644 --- a/packages/explorer/src/common.ts +++ b/packages/explorer/src/common.ts @@ -1,4 +1,7 @@ -import { anvil, garnet, redstone } from "viem/chains"; +import { anvil } from "viem/chains"; +import { garnet, redstone } from "@latticexyz/common/chains"; + +export const internalNamespaces = ["world", "store", "metadata"]; export const supportedChains = { anvil, garnet, redstone } as const; export type supportedChains = typeof supportedChains; @@ -8,7 +11,7 @@ export type supportedChainId = supportedChains[supportedChainName]["id"]; export const chainIdToName = Object.fromEntries( Object.entries(supportedChains).map(([chainName, chain]) => [chain.id, chainName]), -); +) as Record; export function validateChainId(chainId: unknown): asserts chainId is supportedChainId { if (!(typeof chainId === "number" && chainId in chainIdToName)) { diff --git a/packages/explorer/src/components/AccountSelect.tsx b/packages/explorer/src/components/AccountSelect.tsx index 7d2f28e471..e7a276e9fe 100644 --- a/packages/explorer/src/components/AccountSelect.tsx +++ b/packages/explorer/src/components/AccountSelect.tsx @@ -3,7 +3,7 @@ import { useAccount, useBalance, useConnect, useConnectors } from "wagmi"; import { useState } from "react"; import { useConnectModal } from "@rainbow-me/rainbowkit"; import { AnvilConnector, isAnvilConnector } from "../connectors/anvil"; -import { formatBalance } from "../lib/utils"; +import { formatBalance } from "../utils"; import { Button } from "./ui/Button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/Select"; import { TruncatedHex } from "./ui/TruncatedHex"; diff --git a/packages/explorer/src/components/ConnectButton.tsx b/packages/explorer/src/components/ConnectButton.tsx index e2831d8343..362b2d9a86 100644 --- a/packages/explorer/src/components/ConnectButton.tsx +++ b/packages/explorer/src/components/ConnectButton.tsx @@ -1,8 +1,8 @@ import { PlugIcon, ZapIcon } from "lucide-react"; import { anvil } from "viem/chains"; import { ConnectButton as RainbowConnectButton } from "@rainbow-me/rainbowkit"; -import { useChain } from "../hooks/useChain"; -import { cn } from "../lib/utils"; +import { useChain } from "../app/(explorer)/hooks/useChain"; +import { cn } from "../utils"; import { AccountSelect } from "./AccountSelect"; import { Button } from "./ui/Button"; diff --git a/packages/explorer/src/components/LatestBlock.tsx b/packages/explorer/src/components/LatestBlock.tsx index e489e066df..8e8712ed4c 100644 --- a/packages/explorer/src/components/LatestBlock.tsx +++ b/packages/explorer/src/components/LatestBlock.tsx @@ -1,9 +1,12 @@ import { useBlockNumber } from "wagmi"; +import { useChain } from "../app/(explorer)/hooks/useChain"; import { Skeleton } from "./ui/Skeleton"; export function LatestBlock() { + const { id: chainId } = useChain(); const { data: block } = useBlockNumber({ watch: true, + chainId, }); return ( diff --git a/packages/explorer/src/components/Navigation.tsx b/packages/explorer/src/components/Navigation.tsx index 5b883ae503..fe116eb34e 100644 --- a/packages/explorer/src/components/Navigation.tsx +++ b/packages/explorer/src/components/Navigation.tsx @@ -3,17 +3,17 @@ import { Loader } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useWorldUrl } from "../app/(explorer)/hooks/useWorldUrl"; +import { useWorldAbiQuery } from "../app/(explorer)/queries/useWorldAbiQuery"; import { LatestBlock } from "../components/LatestBlock"; import { Separator } from "../components/ui/Separator"; -import { useWorldUrl } from "../hooks/useWorldUrl"; -import { cn } from "../lib/utils"; -import { useAbiQuery } from "../queries/useAbiQuery"; +import { cn } from "../utils"; import { ConnectButton } from "./ConnectButton"; export function Navigation() { const pathname = usePathname(); const getLinkUrl = useWorldUrl(); - const { data, isFetched } = useAbiQuery(); + const { data, isFetched } = useWorldAbiQuery(); return (
diff --git a/packages/explorer/src/components/ui/Button.tsx b/packages/explorer/src/components/ui/Button.tsx index e79f4a694e..e2f2f6b0d1 100644 --- a/packages/explorer/src/components/ui/Button.tsx +++ b/packages/explorer/src/components/ui/Button.tsx @@ -1,7 +1,7 @@ import { type VariantProps, cva } from "class-variance-authority"; import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; -import { cn } from "../../lib/utils"; +import { cn } from "../../utils"; const buttonVariants = cva( cn( diff --git a/packages/explorer/src/components/ui/Checkbox.tsx b/packages/explorer/src/components/ui/Checkbox.tsx index 25db55d579..b7d00a1330 100644 --- a/packages/explorer/src/components/ui/Checkbox.tsx +++ b/packages/explorer/src/components/ui/Checkbox.tsx @@ -3,7 +3,7 @@ import { Check } from "lucide-react"; import * as React from "react"; import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; -import { cn } from "../../lib/utils"; +import { cn } from "../../utils"; const Checkbox = React.forwardRef< React.ElementRef, diff --git a/packages/explorer/src/components/ui/Command.tsx b/packages/explorer/src/components/ui/Command.tsx index 293e9a12f0..9a567ba5fd 100644 --- a/packages/explorer/src/components/ui/Command.tsx +++ b/packages/explorer/src/components/ui/Command.tsx @@ -4,7 +4,7 @@ import { Search } from "lucide-react"; import { Command as CommandPrimitive } from "cmdk"; import * as React from "react"; import { type DialogProps } from "@radix-ui/react-dialog"; -import { cn } from "../../lib/utils"; +import { cn } from "../../utils"; import { Dialog, DialogContent } from "./Dialog"; const Command = React.forwardRef< diff --git a/packages/explorer/src/components/ui/Dialog.tsx b/packages/explorer/src/components/ui/Dialog.tsx index d0e427ba3f..f298cf0e03 100644 --- a/packages/explorer/src/components/ui/Dialog.tsx +++ b/packages/explorer/src/components/ui/Dialog.tsx @@ -3,7 +3,7 @@ import { X } from "lucide-react"; import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { cn } from "../../lib/utils"; +import { cn } from "../../utils"; const Dialog = DialogPrimitive.Root; diff --git a/packages/explorer/src/components/ui/Form.tsx b/packages/explorer/src/components/ui/Form.tsx index bafa560b09..ad5de85d79 100644 --- a/packages/explorer/src/components/ui/Form.tsx +++ b/packages/explorer/src/components/ui/Form.tsx @@ -5,7 +5,7 @@ import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useF import * as LabelPrimitive from "@radix-ui/react-label"; import { Slot } from "@radix-ui/react-slot"; import { Label } from "../../components/ui/Label"; -import { cn } from "../../lib/utils"; +import { cn } from "../../utils"; const Form = FormProvider; diff --git a/packages/explorer/src/components/ui/Input.tsx b/packages/explorer/src/components/ui/Input.tsx index 2fecdd4b43..a133957547 100644 --- a/packages/explorer/src/components/ui/Input.tsx +++ b/packages/explorer/src/components/ui/Input.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { cn } from "../../lib/utils"; +import { cn } from "../../utils"; const Input = React.forwardRef>( ({ className, type, ...props }, ref) => { diff --git a/packages/explorer/src/components/ui/Label.tsx b/packages/explorer/src/components/ui/Label.tsx index 0e2dcc1c71..0aeca5769d 100644 --- a/packages/explorer/src/components/ui/Label.tsx +++ b/packages/explorer/src/components/ui/Label.tsx @@ -3,7 +3,7 @@ import { type VariantProps, cva } from "class-variance-authority"; import * as React from "react"; import * as LabelPrimitive from "@radix-ui/react-label"; -import { cn } from "../../lib/utils"; +import { cn } from "../../utils"; const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"); diff --git a/packages/explorer/src/components/ui/Popover.tsx b/packages/explorer/src/components/ui/Popover.tsx index 9d64057ce0..5521c8b64d 100644 --- a/packages/explorer/src/components/ui/Popover.tsx +++ b/packages/explorer/src/components/ui/Popover.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import * as PopoverPrimitive from "@radix-ui/react-popover"; -import { cn } from "../../lib/utils"; +import { cn } from "../../utils"; const Popover = PopoverPrimitive.Root; diff --git a/packages/explorer/src/components/ui/Select.tsx b/packages/explorer/src/components/ui/Select.tsx index fd1981b5b0..9bd66bb400 100644 --- a/packages/explorer/src/components/ui/Select.tsx +++ b/packages/explorer/src/components/ui/Select.tsx @@ -3,7 +3,7 @@ import { Check, ChevronDown, ChevronUp } from "lucide-react"; import * as React from "react"; import * as SelectPrimitive from "@radix-ui/react-select"; -import { cn } from "../../lib/utils"; +import { cn } from "../../utils"; const Select = SelectPrimitive.Root; diff --git a/packages/explorer/src/components/ui/Separator.tsx b/packages/explorer/src/components/ui/Separator.tsx index ffb3c43130..8bed9ea2d8 100644 --- a/packages/explorer/src/components/ui/Separator.tsx +++ b/packages/explorer/src/components/ui/Separator.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import * as SeparatorPrimitive from "@radix-ui/react-separator"; -import { cn } from "../../lib/utils"; +import { cn } from "../../utils"; const Separator = React.forwardRef< React.ElementRef, diff --git a/packages/explorer/src/components/ui/Skeleton.tsx b/packages/explorer/src/components/ui/Skeleton.tsx index 02a0632828..2a21e5d40b 100644 --- a/packages/explorer/src/components/ui/Skeleton.tsx +++ b/packages/explorer/src/components/ui/Skeleton.tsx @@ -1,4 +1,4 @@ -import { cn } from "../../lib/utils"; +import { cn } from "../../utils"; function Skeleton({ className, ...props }: React.HTMLAttributes) { return
; diff --git a/packages/explorer/src/components/ui/Table.tsx b/packages/explorer/src/components/ui/Table.tsx index a424fe4277..4e90da239b 100644 --- a/packages/explorer/src/components/ui/Table.tsx +++ b/packages/explorer/src/components/ui/Table.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { cn } from "../../lib/utils"; +import { cn } from "../../utils"; const Table = React.forwardRef>( ({ className, ...props }, ref) => ( diff --git a/packages/explorer/src/queries/useAbiQuery.ts b/packages/explorer/src/queries/useAbiQuery.ts deleted file mode 100644 index db8bed764b..0000000000 --- a/packages/explorer/src/queries/useAbiQuery.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useParams } from "next/navigation"; -import { AbiFunction, Hex } from "viem"; -import { UseQueryResult, useQuery } from "@tanstack/react-query"; -import { useChain } from "../hooks/useChain"; - -export async function getAbi(chainId: number, worldAddress: Hex) { - const res = await fetch(`/api/world?${new URLSearchParams({ chainId: String(chainId), worldAddress })}`); - const data = await res.json(); - if (!res.ok) { - throw new Error(data.error); - } - - return data; -} - -type AbiQueryResult = { - abi: AbiFunction[]; - isWorldDeployed: boolean; -}; - -export const useAbiQuery = (): UseQueryResult => { - const { worldAddress } = useParams(); - const { id: chainId } = useChain(); - - return useQuery({ - queryKey: ["abi", chainId, worldAddress], - queryFn: () => getAbi(chainId, worldAddress as Hex), - select: (data) => { - return { - abi: data.abi || [], - isWorldDeployed: data.isWorldDeployed, - }; - }, - refetchInterval: 15000, - }); -}; diff --git a/packages/explorer/src/lib/utils.ts b/packages/explorer/src/utils.ts similarity index 71% rename from packages/explorer/src/lib/utils.ts rename to packages/explorer/src/utils.ts index 4d0663bfbb..cded49550a 100644 --- a/packages/explorer/src/lib/utils.ts +++ b/packages/explorer/src/utils.ts @@ -6,9 +6,8 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } -export function camelCase(str: string) { - const a = str.toLowerCase().replace(/[-_\s.]+(.)?/g, (_, c) => (c ? c.toUpperCase() : "")); - return a.substring(0, 1).toLowerCase() + a.substring(1); +export function snakeCase(str: string) { + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); } export function formatBalance(wei: bigint) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83790d4fb0..f41c62f5a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,21 +6,9 @@ settings: catalogs: default: - '@ark/attest': - specifier: 0.12.1 - version: 0.12.1 - '@ark/util': - specifier: 0.2.2 - version: 0.2.2 '@wagmi/core': specifier: 2.13.5 version: 2.13.5 - abitype: - specifier: 1.0.6 - version: 1.0.6 - arktype: - specifier: 2.0.0-beta.6 - version: 2.0.0-beta.6 viem: specifier: 2.21.6 version: 2.21.6 @@ -465,6 +453,9 @@ importers: '@latticexyz/common': specifier: workspace:* version: link:../common + '@latticexyz/config': + specifier: workspace:* + version: link:../config '@latticexyz/protocol-parser': specifier: workspace:* version: link:../protocol-parser @@ -812,10 +803,10 @@ importers: version: 8.3.4 jest: specifier: ^29.3.1 - version: 29.5.0(@types/node@18.15.11) + version: 29.5.0(@types/node@20.12.12) ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.21.4)(@jest/types@29.6.3)(babel-jest@29.5.0(@babel/core@7.21.4))(jest@29.5.0(@types/node@18.15.11))(typescript@5.4.2) + version: 29.0.5(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.5.0(@babel/core@7.25.2))(jest@29.5.0(@types/node@20.12.12))(typescript@5.4.2) tsup: specifier: ^6.7.0 version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) @@ -1203,10 +1194,10 @@ importers: version: 27.4.1 jest: specifier: ^29.3.1 - version: 29.5.0(@types/node@20.12.12) + version: 29.5.0(@types/node@18.15.11) ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.5.0(@babel/core@7.25.2))(jest@29.5.0(@types/node@20.12.12))(typescript@5.4.2) + version: 29.0.5(@babel/core@7.21.4)(@jest/types@29.6.3)(babel-jest@29.5.0(@babel/core@7.21.4))(jest@29.5.0(@types/node@18.15.11))(typescript@5.4.2) tsup: specifier: ^6.7.0 version: 6.7.0(postcss@8.4.31)(typescript@5.4.2)