-
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)