From 20604952d33419f18ab93fcc048db564b56a54b4 Mon Sep 17 00:00:00 2001 From: Karolis Ramanauskas Date: Tue, 17 Sep 2024 21:49:33 +0300 Subject: [PATCH] feat(explorer): active chain as dynamic param (#3181) --- .changeset/perfect-plants-hang.md | 5 ++ packages/explorer/next.config.mjs | 9 ++++ packages/explorer/public/favicon.svg | 50 +++++++++++++++++++ .../src/app/(explorer)/[chainName]/page.tsx | 11 ++++ .../worlds/[worldAddress]}/Providers.tsx | 44 ++++++++-------- .../[worldAddress]/explore/DataExplorer.tsx | 0 .../explore/EditableTableCell.tsx | 16 +++--- .../[worldAddress]/explore/TableSelector.tsx | 2 +- .../[worldAddress]/explore/TablesViewer.tsx | 8 +-- .../worlds/[worldAddress]/explore/page.tsx | 0 .../worlds/[worldAddress]/interact/Form.tsx | 12 ++--- .../[worldAddress]/interact/FunctionField.tsx | 8 +-- .../worlds/[worldAddress]/interact/page.tsx | 0 .../interact/useContractMutation.ts | 12 ++--- .../worlds/[worldAddress]/layout.tsx | 7 +-- .../worlds/[worldAddress]/observe/Write.tsx | 2 +- .../worlds/[worldAddress]/observe/Writes.tsx | 4 +- .../worlds/[worldAddress]/observe/common.ts | 0 .../worlds/[worldAddress]/observe/page.tsx | 0 .../worlds/[worldAddress]/page.tsx | 13 +++++ .../[worldAddress]/utils/bufferToBigInt.ts | 0 .../(explorer)/[chainName]/worlds/page.tsx | 15 ++++++ packages/explorer/src/app/(explorer)/page.tsx | 5 -- .../(explorer)/worlds/[worldAddress]/page.tsx | 5 -- .../src/app/(explorer)/worlds/page.tsx | 9 ---- packages/explorer/src/app/api/world/route.ts | 19 ++++--- .../src/app/{(explorer) => }/error.tsx | 4 +- .../src/app/{(explorer) => }/globals.css | 0 .../src/app/{(explorer) => }/layout.tsx | 28 +++++------ .../src/app/{(explorer) => }/not-found.tsx | 4 +- packages/explorer/src/app/page.tsx | 12 +++++ packages/explorer/src/bin/explorer.ts | 8 ++- packages/explorer/src/common.ts | 31 ++++++------ .../explorer/src/components/ConnectButton.tsx | 7 +-- packages/explorer/src/connectors/anvil.ts | 16 +++--- packages/explorer/src/hooks/useChain.ts | 11 ++++ packages/explorer/src/hooks/useWorldUrl.ts | 4 +- packages/explorer/src/queries/useAbiQuery.ts | 12 +++-- 38 files changed, 253 insertions(+), 140 deletions(-) create mode 100644 .changeset/perfect-plants-hang.md create mode 100644 packages/explorer/public/favicon.svg create mode 100644 packages/explorer/src/app/(explorer)/[chainName]/page.tsx rename packages/explorer/src/app/(explorer)/{ => [chainName]/worlds/[worldAddress]}/Providers.tsx (53%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/explore/DataExplorer.tsx (100%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/explore/EditableTableCell.tsx (90%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/explore/TableSelector.tsx (96%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/explore/TablesViewer.tsx (96%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/explore/page.tsx (100%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/interact/Form.tsx (89%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/interact/FunctionField.tsx (93%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/interact/page.tsx (100%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/interact/useContractMutation.ts (92%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/layout.tsx (52%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/observe/Write.tsx (94%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/observe/Writes.tsx (84%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/observe/common.ts (100%) rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/observe/page.tsx (100%) create mode 100644 packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/page.tsx rename packages/explorer/src/app/(explorer)/{ => [chainName]}/worlds/[worldAddress]/utils/bufferToBigInt.ts (100%) create mode 100644 packages/explorer/src/app/(explorer)/[chainName]/worlds/page.tsx delete mode 100644 packages/explorer/src/app/(explorer)/page.tsx delete mode 100644 packages/explorer/src/app/(explorer)/worlds/[worldAddress]/page.tsx delete mode 100644 packages/explorer/src/app/(explorer)/worlds/page.tsx rename packages/explorer/src/app/{(explorer) => }/error.tsx (92%) rename packages/explorer/src/app/{(explorer) => }/globals.css (100%) rename packages/explorer/src/app/{(explorer) => }/layout.tsx (69%) rename packages/explorer/src/app/{(explorer) => }/not-found.tsx (90%) create mode 100644 packages/explorer/src/app/page.tsx create mode 100644 packages/explorer/src/hooks/useChain.ts diff --git a/.changeset/perfect-plants-hang.md b/.changeset/perfect-plants-hang.md new file mode 100644 index 0000000000..1a74d6c439 --- /dev/null +++ b/.changeset/perfect-plants-hang.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/explorer": patch +--- + +Added ability to connect World Explorer to Redstone and Garnet chains. The active chain is now passed as a dynamic route parameter. diff --git a/packages/explorer/next.config.mjs b/packages/explorer/next.config.mjs index 6eab04cefe..cffb1b98ba 100644 --- a/packages/explorer/next.config.mjs +++ b/packages/explorer/next.config.mjs @@ -8,6 +8,15 @@ export default function config() { config.externals.push("pino-pretty", "lokijs", "encoding"); return config; }, + redirects: async () => { + return [ + { + source: "/:chainName/worlds/:worldAddress/explorer", + destination: "/:chainName/worlds/:worldAddress/explore", + permanent: true, + }, + ]; + }, }; return nextConfig; diff --git a/packages/explorer/public/favicon.svg b/packages/explorer/public/favicon.svg new file mode 100644 index 0000000000..37698f3287 --- /dev/null +++ b/packages/explorer/public/favicon.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/explorer/src/app/(explorer)/[chainName]/page.tsx b/packages/explorer/src/app/(explorer)/[chainName]/page.tsx new file mode 100644 index 0000000000..e6c8fd70b9 --- /dev/null +++ b/packages/explorer/src/app/(explorer)/[chainName]/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from "next/navigation"; + +type Props = { + params: { + chainName: string; + }; +}; + +export default async function ChainPage({ params }: Props) { + return redirect(`/${params.chainName}/worlds`); +} diff --git a/packages/explorer/src/app/(explorer)/Providers.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/Providers.tsx similarity index 53% rename from packages/explorer/src/app/(explorer)/Providers.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/Providers.tsx index b2d407c0be..c1d4a45e8d 100644 --- a/packages/explorer/src/app/(explorer)/Providers.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/Providers.tsx @@ -2,35 +2,37 @@ import { WagmiProvider, createConfig, http } from "wagmi"; import { injected, metaMask, safe } from "wagmi/connectors"; -import { ReactNode } from "react"; +import { ReactNode, useMemo } from "react"; import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit"; import "@rainbow-me/rainbowkit/styles.css"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { getChain } from "../../common"; -import { defaultAnvilConnectors } from "../../connectors/anvil"; +import { getDefaultAnvilConnectors } from "../../../../../connectors/anvil"; +import { useChain } from "../../../../../hooks/useChain"; const queryClient = new QueryClient(); -const chain = getChain(); -export const wagmiConfig = createConfig({ - chains: [chain], - connectors: [ - injected(), - metaMask({ - dappMetadata: { - name: "World Explorer", +export function Providers({ children }: { children: ReactNode }) { + const chain = useChain(); + const wagmiConfig = useMemo(() => { + return createConfig({ + chains: [chain], + connectors: [ + injected(), + metaMask({ + dappMetadata: { + name: "World Explorer", + }, + }), + safe(), + ...getDefaultAnvilConnectors(chain.id), + ], + transports: { + [chain.id]: http(), }, - }), - safe(), - ...defaultAnvilConnectors, - ], - transports: { - [chain.id]: http(), - }, - ssr: true, -}); + ssr: true, + }); + }, [chain]); -export function Providers({ children }: { children: ReactNode }) { return ( diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/DataExplorer.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/DataExplorer.tsx similarity index 100% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/DataExplorer.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/DataExplorer.tsx diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/EditableTableCell.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/EditableTableCell.tsx similarity index 90% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/EditableTableCell.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/EditableTableCell.tsx index 45f493260f..51839a1ad3 100644 --- a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/EditableTableCell.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/EditableTableCell.tsx @@ -2,18 +2,17 @@ import { Loader } from "lucide-react"; import { useParams } from "next/navigation"; import { toast } from "sonner"; import { Hex } from "viem"; -import { useAccount } from "wagmi"; +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 IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { waitForTransactionReceipt, writeContract } from "@wagmi/core"; -import { getChain } from "../../../../../common"; -import { Checkbox } from "../../../../../components/ui/Checkbox"; -import { camelCase, cn } from "../../../../../lib/utils"; -import { TableConfig } from "../../../../api/table/route"; -import { wagmiConfig } from "../../../Providers"; +import { Checkbox } from "../../../../../../components/ui/Checkbox"; +import { useChain } from "../../../../../../hooks/useChain"; +import { camelCase, cn } from "../../../../../../lib/utils"; +import { TableConfig } from "../../../../../api/table/route"; type Props = { name: string; @@ -22,12 +21,11 @@ type Props = { config: TableConfig; }; -const chain = getChain(); -const chainId = chain.id; - export function EditableTableCell({ name, config, keyTuple, value: defaultValue }: Props) { + const wagmiConfig = useConfig(); const queryClient = useQueryClient(); const { worldAddress } = useParams(); + const { id: chainId } = useChain(); const account = useAccount(); const [value, setValue] = useState(defaultValue); diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/TableSelector.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TableSelector.tsx similarity index 96% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/TableSelector.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TableSelector.tsx index 7bd6a8a05c..2429d9930a 100644 --- a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/TableSelector.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TableSelector.tsx @@ -1,7 +1,7 @@ import { Lock } from "lucide-react"; import { useParams } from "next/navigation"; import { internalTableNames } from "@latticexyz/store-sync/sqlite"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../../components/ui/Select"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../../../components/ui/Select"; type Props = { value: string | undefined; diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/TablesViewer.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx similarity index 96% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/TablesViewer.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx index d12254ae82..3d992be635 100644 --- a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/TablesViewer.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx @@ -14,10 +14,10 @@ import { getSortedRowModel, useReactTable, } from "@tanstack/react-table"; -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 { 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 { EditableTableCell } from "./EditableTableCell"; diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/page.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/page.tsx similarity index 100% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/page.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/page.tsx diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/interact/Form.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/Form.tsx similarity index 89% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/interact/Form.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/Form.tsx index 3c78302d5d..15521e3f2d 100644 --- a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/interact/Form.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/Form.tsx @@ -3,12 +3,12 @@ import { Coins, Eye, Send } from "lucide-react"; import { AbiFunction } from "viem"; 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 { 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 { FunctionField } from "./FunctionField"; export function Form() { diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/interact/FunctionField.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/FunctionField.tsx similarity index 93% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/interact/FunctionField.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/FunctionField.tsx index ecb6affad2..0ca1bf7e8c 100644 --- a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/interact/FunctionField.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/FunctionField.tsx @@ -6,10 +6,10 @@ import { z } from "zod"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Button } from "../../../../../components/ui/Button"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../../../../../components/ui/Form"; -import { Input } from "../../../../../components/ui/Input"; -import { Separator } from "../../../../../components/ui/Separator"; +import { Button } from "../../../../../../components/ui/Button"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../../../../../../components/ui/Form"; +import { Input } from "../../../../../../components/ui/Input"; +import { Separator } from "../../../../../../components/ui/Separator"; import { useContractMutation } from "./useContractMutation"; export enum FunctionType { diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/interact/page.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/page.tsx similarity index 100% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/interact/page.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/page.tsx diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/interact/useContractMutation.ts b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts similarity index 92% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/interact/useContractMutation.ts rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts index b877bfb81c..50e02e7b06 100644 --- a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/interact/useContractMutation.ts +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts @@ -1,11 +1,10 @@ import { useParams } from "next/navigation"; import { toast } from "sonner"; import { Abi, AbiFunction, Hex } from "viem"; -import { useAccount } from "wagmi"; +import { useAccount, useConfig } from "wagmi"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { readContract, waitForTransactionReceipt, writeContract } from "@wagmi/core"; -import { getChain } from "../../../../../common"; -import { wagmiConfig } from "../../../Providers"; +import { useChain } from "../../../../../../hooks/useChain"; import { FunctionType } from "./FunctionField"; type UseContractMutationProps = { @@ -13,12 +12,11 @@ type UseContractMutationProps = { operationType: FunctionType; }; -const chain = getChain(); -const chainId = chain.id; - export function useContractMutation({ abi, operationType }: UseContractMutationProps) { - const queryClient = useQueryClient(); const { worldAddress } = useParams(); + const { id: chainId } = useChain(); + const queryClient = useQueryClient(); + const wagmiConfig = useConfig(); const account = useAccount(); return useMutation({ diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/layout.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/layout.tsx similarity index 52% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/layout.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/layout.tsx index c13476dcf2..5b4c354560 100644 --- a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/layout.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/layout.tsx @@ -1,12 +1,13 @@ "use client"; -import { Navigation } from "../../../../components/Navigation"; +import { Navigation } from "../../../../../components/Navigation"; +import { Providers } from "./Providers"; export default function WorldLayout({ children }: { children: React.ReactNode }) { return ( -
+ {children} -
+ ); } diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Write.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/Write.tsx similarity index 94% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Write.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/Write.tsx index b61825e5ce..daa8bdf225 100644 --- a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Write.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/Write.tsx @@ -1,6 +1,6 @@ "use client"; -import { type Write } from "../../../../../observer/store"; +import { type Write } from "../../../../../../observer/store"; import { msPerViewportWidth } from "./common"; export type Props = Write; diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Writes.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/Writes.tsx similarity index 84% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Writes.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/Writes.tsx index 053b509e28..821e849b34 100644 --- a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Writes.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/Writes.tsx @@ -1,8 +1,8 @@ "use client"; import { useStore } from "zustand"; -import { KeepInView } from "../../../../../components/KeepInView"; -import { store } from "../../../../../observer/store"; +import { KeepInView } from "../../../../../../components/KeepInView"; +import { store } from "../../../../../../observer/store"; import { Write } from "./Write"; export function Writes() { diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/common.ts b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/common.ts similarity index 100% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/common.ts rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/common.ts diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/page.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/page.tsx similarity index 100% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/page.tsx rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/page.tsx diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/page.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/page.tsx new file mode 100644 index 0000000000..47b435ef54 --- /dev/null +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/page.tsx @@ -0,0 +1,13 @@ +import { redirect } from "next/navigation"; + +type Props = { + params: { + chainName: string; + worldAddress: string; + }; +}; + +export default async function WorldPage({ params }: Props) { + const { chainName, worldAddress } = params; + return redirect(`/${chainName}/worlds/${worldAddress}/explore`); +} diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/utils/bufferToBigInt.ts b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/utils/bufferToBigInt.ts similarity index 100% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/utils/bufferToBigInt.ts rename to packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/utils/bufferToBigInt.ts diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/page.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/page.tsx new file mode 100644 index 0000000000..4358639446 --- /dev/null +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/page.tsx @@ -0,0 +1,15 @@ +import { notFound, redirect } from "next/navigation"; + +export const dynamic = "force-dynamic"; + +type Props = { + params: { + chainName: string; + }; +}; + +export default function WorldsPage({ params }: Props) { + const worldAddress = process.env.WORLD_ADDRESS; + if (worldAddress) return redirect(`/${params.chainName}/worlds/${worldAddress}`); + return notFound(); +} diff --git a/packages/explorer/src/app/(explorer)/page.tsx b/packages/explorer/src/app/(explorer)/page.tsx deleted file mode 100644 index cd0a8af1d8..0000000000 --- a/packages/explorer/src/app/(explorer)/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from "next/navigation"; - -export default async function IndexPage() { - redirect("/worlds"); -} diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/page.tsx b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/page.tsx deleted file mode 100644 index 2fedbf5643..0000000000 --- a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from "next/navigation"; - -export default async function WorldPage({ params }: { params: { worldAddress: string } }) { - return redirect(`/worlds/${params.worldAddress}/explore`); -} diff --git a/packages/explorer/src/app/(explorer)/worlds/page.tsx b/packages/explorer/src/app/(explorer)/worlds/page.tsx deleted file mode 100644 index 02b20cad15..0000000000 --- a/packages/explorer/src/app/(explorer)/worlds/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { notFound, redirect } from "next/navigation"; - -export const dynamic = "force-dynamic"; - -export default function WorldsPage() { - const worldAddress = process.env.WORLD_ADDRESS; - if (worldAddress) return redirect(`/worlds/${worldAddress}`); - return notFound(); -} diff --git a/packages/explorer/src/app/api/world/route.ts b/packages/explorer/src/app/api/world/route.ts index a0be3b52c2..650dee9bad 100644 --- a/packages/explorer/src/app/api/world/route.ts +++ b/packages/explorer/src/app/api/world/route.ts @@ -3,21 +3,22 @@ import { getBlockNumber, getLogs } from "viem/actions"; import { helloStoreEvent } from "@latticexyz/store"; import { helloWorldEvent } from "@latticexyz/world"; import { getWorldAbi } from "@latticexyz/world/internal"; -import { getChain } from "../../../common"; +import { SupportedChainIds, supportedChainsById, validateChainId } from "../../../common"; export const dynamic = "force-dynamic"; -async function getClient() { +async function getClient(chainId: SupportedChainIds) { + const chain = supportedChainsById[chainId]; const client = createWalletClient({ - chain: getChain(), + chain, transport: http(), }); return client; } -async function getParameters(worldAddress: Address) { - const client = await getClient(); +async function getParameters(chainId: SupportedChainIds, worldAddress: Address) { + const client = await getClient(chainId); const toBlock = await getBlockNumber(client); const logs = await getLogs(client, { strict: true, @@ -35,15 +36,17 @@ async function getParameters(worldAddress: Address) { export async function GET(req: Request) { const { searchParams } = new URL(req.url); - const worldAddress = searchParams.get("address") as Hex; + const worldAddress = searchParams.get("worldAddress") as Hex; + const chainId = Number(searchParams.get("chainId")); + validateChainId(chainId); if (!worldAddress) { return Response.json({ error: "address is required" }, { status: 400 }); } try { - const client = await getClient(); - const { fromBlock, toBlock, isWorldDeployed } = await getParameters(worldAddress); + const client = await getClient(chainId); + const { fromBlock, toBlock, isWorldDeployed } = await getParameters(chainId, worldAddress); const worldAbiResponse = await getWorldAbi({ client, worldAddress, diff --git a/packages/explorer/src/app/(explorer)/error.tsx b/packages/explorer/src/app/error.tsx similarity index 92% rename from packages/explorer/src/app/(explorer)/error.tsx rename to packages/explorer/src/app/error.tsx index 4cfe8b5711..844d3e5723 100644 --- a/packages/explorer/src/app/(explorer)/error.tsx +++ b/packages/explorer/src/app/error.tsx @@ -2,8 +2,8 @@ import { ExternalLink, RefreshCwIcon } from "lucide-react"; import Link from "next/link"; -import { Button } from "../../components/ui/Button"; -import { useWorldUrl } from "../../hooks/useWorldUrl"; +import { Button } from "../components/ui/Button"; +import { useWorldUrl } from "../hooks/useWorldUrl"; type Props = { error: Error & { digest?: string }; diff --git a/packages/explorer/src/app/(explorer)/globals.css b/packages/explorer/src/app/globals.css similarity index 100% rename from packages/explorer/src/app/(explorer)/globals.css rename to packages/explorer/src/app/globals.css diff --git a/packages/explorer/src/app/(explorer)/layout.tsx b/packages/explorer/src/app/layout.tsx similarity index 69% rename from packages/explorer/src/app/(explorer)/layout.tsx rename to packages/explorer/src/app/layout.tsx index 4bcb74b30b..1dd8395953 100644 --- a/packages/explorer/src/app/(explorer)/layout.tsx +++ b/packages/explorer/src/app/layout.tsx @@ -3,7 +3,6 @@ import { Inter, JetBrains_Mono } from "next/font/google"; import { Toaster } from "sonner"; import { Theme } from "@radix-ui/themes"; import "@radix-ui/themes/styles.css"; -import { Providers } from "./Providers"; import "./globals.css"; const inter = Inter({ @@ -20,6 +19,9 @@ const jetbrains = JetBrains_Mono({ export const metadata: Metadata = { title: "World Explorer", description: "World Explorer is a tool for visually exploring and manipulating the state of worlds", + icons: { + icon: "/favicon.svg", + }, }; export default function RootLayout({ @@ -30,19 +32,17 @@ export default function RootLayout({ return ( - - -
- {children} -
- -
-
+ +
+ {children} +
+ +
); diff --git a/packages/explorer/src/app/(explorer)/not-found.tsx b/packages/explorer/src/app/not-found.tsx similarity index 90% rename from packages/explorer/src/app/(explorer)/not-found.tsx rename to packages/explorer/src/app/not-found.tsx index 44059b412c..a4cddf5996 100644 --- a/packages/explorer/src/app/(explorer)/not-found.tsx +++ b/packages/explorer/src/app/not-found.tsx @@ -2,8 +2,8 @@ import { ExternalLink } from "lucide-react"; import Link from "next/link"; -import { Button } from "../../components/ui/Button"; -import { useWorldUrl } from "../../hooks/useWorldUrl"; +import { Button } from "../components/ui/Button"; +import { useWorldUrl } from "../hooks/useWorldUrl"; export default function NotFound() { const getUrl = useWorldUrl(); diff --git a/packages/explorer/src/app/page.tsx b/packages/explorer/src/app/page.tsx new file mode 100644 index 0000000000..cfa56011be --- /dev/null +++ b/packages/explorer/src/app/page.tsx @@ -0,0 +1,12 @@ +import { redirect } from "next/navigation"; +import { supportedChainsById, validateChainId } from "../common"; + +export const dynamic = "force-dynamic"; + +export default function IndexPage() { + const chainId = Number(process.env.CHAIN_ID); + validateChainId(chainId); + + const chainName = supportedChainsById[chainId]; + return redirect(`/${chainName}/worlds`); +} diff --git a/packages/explorer/src/bin/explorer.ts b/packages/explorer/src/bin/explorer.ts index 8362863d61..6e5c551162 100755 --- a/packages/explorer/src/bin/explorer.ts +++ b/packages/explorer/src/bin/explorer.ts @@ -6,7 +6,7 @@ import process from "process"; import { fileURLToPath } from "url"; import yargs from "yargs"; import { ChildProcess, spawn } from "child_process"; -import { chains } from "../common"; +import { validateChainId } from "../common"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -57,9 +57,7 @@ const argv = yargs(process.argv.slice(2)) }, }) .check((argv) => { - if (!chains[Number(argv.chainId)]) { - throw new Error(`Invalid chain ID. Supported chains are: ${Object.keys(chains).join(", ")}.`); - } + validateChainId(Number(argv.chainId)); return true; }) .parseSync(); @@ -71,7 +69,7 @@ let explorerProcess: ChildProcess; async function startExplorer() { const env = { ...process.env, - NEXT_PUBLIC_CHAIN_ID: chainId.toString(), + CHAIN_ID: chainId.toString(), WORLD_ADDRESS: worldAddress?.toString(), INDEXER_DATABASE: path.join(process.cwd(), indexerDatabase), }; diff --git a/packages/explorer/src/common.ts b/packages/explorer/src/common.ts index 22e27c0596..de77ede488 100644 --- a/packages/explorer/src/common.ts +++ b/packages/explorer/src/common.ts @@ -1,22 +1,21 @@ -import { Chain, anvil, garnet, redstone } from "viem/chains"; +import { anvil, garnet, redstone } from "viem/chains"; -export const chains: Partial> = { - [anvil.id]: anvil, - [redstone.id]: redstone, - [garnet.id]: garnet, -}; +export const supportedChains = { anvil, garnet, redstone } as const; +export const supportedChainsById = Object.fromEntries( + Object.entries(supportedChains).map(([, chain]) => [chain.id, chain]), +); -export function getChain() { - const chainId = Number(process.env.NEXT_PUBLIC_CHAIN_ID || anvil.id); - const chain = chains[chainId]; - if (!chain) { - throw new Error(`Chain ID ${chainId} not supported. Supported chains are: ${Object.keys(chains).join(", ")}.`); - } +export type SupportedChainIds = (typeof supportedChains)[keyof typeof supportedChains]["id"]; +export type SupportedChainNames = keyof typeof supportedChains; - return chain; +export function validateChainId(chainId: number): asserts chainId is SupportedChainIds { + if (!(chainId in supportedChainsById)) { + throw new Error(`Invalid chain id. Supported chains are: ${Object.keys(supportedChainsById).join(", ")}.`); + } } -export function isAnvil() { - const chain = getChain(); - return chain.id === anvil.id; +export function validateChainName(name: string | string[] | undefined): asserts name is SupportedChainNames { + if (Array.isArray(name) || typeof name !== "string" || !(name in supportedChains)) { + throw new Error(`Invalid chain name. Supported chains are: ${Object.keys(supportedChainsById).join(", ")}.`); + } } diff --git a/packages/explorer/src/components/ConnectButton.tsx b/packages/explorer/src/components/ConnectButton.tsx index 5b544254f4..e2831d8343 100644 --- a/packages/explorer/src/components/ConnectButton.tsx +++ b/packages/explorer/src/components/ConnectButton.tsx @@ -1,16 +1,17 @@ import { PlugIcon, ZapIcon } from "lucide-react"; +import { anvil } from "viem/chains"; import { ConnectButton as RainbowConnectButton } from "@rainbow-me/rainbowkit"; -import { isAnvil } from "../common"; +import { useChain } from "../hooks/useChain"; import { cn } from "../lib/utils"; import { AccountSelect } from "./AccountSelect"; import { Button } from "./ui/Button"; export function ConnectButton() { + const { id: chainId } = useChain(); return ( {({ account, chain, openAccountModal, openChainModal, openConnectModal, mounted }) => { const connected = mounted && account && chain; - return (
{(() => { if (!connected) { - if (isAnvil()) { + if (chainId === anvil.id) { return ; } diff --git a/packages/explorer/src/connectors/anvil.ts b/packages/explorer/src/connectors/anvil.ts index 9a3a8ad66a..54bd3878b0 100644 --- a/packages/explorer/src/connectors/anvil.ts +++ b/packages/explorer/src/connectors/anvil.ts @@ -2,7 +2,6 @@ import { EIP1193RequestFn, Transport, WalletRpcSchema, http } from "viem"; import { Account, privateKeyToAccount } from "viem/accounts"; import { anvil as anvilChain } from "viem/chains"; import { Connector, createConnector } from "wagmi"; -import { isAnvil } from "../common"; export const defaultAnvilAccounts = ( [ @@ -27,19 +26,24 @@ export type AnvilConnectorOptions = { id: string; name: string; accounts: readonly Account[]; + disabled: boolean; }; // We can't programmatically switch accounts within a connector, but we can switch between connectors, // so create one anvil connector per default anvil account so users can switch between default anvil accounts. -export const defaultAnvilConnectors = defaultAnvilAccounts.map((account, i) => - anvil({ id: `anvil-${i}`, name: `Anvil #${i + 1}`, accounts: [account] }), -); +export const getDefaultAnvilConnectors = (chainId: number) => { + // disable anvil connector if chainId is not anvil + const disabled = chainId !== anvilChain.id; + return defaultAnvilAccounts.map((account, i) => + anvil({ id: `anvil-${i}`, name: `Anvil #${i + 1}`, accounts: [account], disabled }), + ); +}; export function isAnvilConnector(connector: Connector): connector is AnvilConnector { return connector.type === "anvil"; } -export function anvil({ id, name, accounts }: AnvilConnectorOptions) { +export function anvil({ id, name, accounts, disabled }: AnvilConnectorOptions) { if (!accounts.length) throw new Error("missing accounts"); type Provider = ReturnType>>; @@ -70,7 +74,7 @@ export function anvil({ id, name, accounts }: AnvilConnectorOptions) { return http()({ chain: anvilChain }); }, async isAuthorized() { - if (!isAnvil()) return false; + if (disabled) return false; if (!connected) return false; const accounts = await this.getAccounts(); diff --git a/packages/explorer/src/hooks/useChain.ts b/packages/explorer/src/hooks/useChain.ts new file mode 100644 index 0000000000..31556bda55 --- /dev/null +++ b/packages/explorer/src/hooks/useChain.ts @@ -0,0 +1,11 @@ +import { useParams } from "next/navigation"; +import { Chain } from "viem"; +import { supportedChains, validateChainName } from "../common"; + +export function useChain(): Chain { + const { chainName } = useParams(); + validateChainName(chainName); + + const chain = supportedChains[chainName]; + return chain; +} diff --git a/packages/explorer/src/hooks/useWorldUrl.ts b/packages/explorer/src/hooks/useWorldUrl.ts index 5cb1254784..2225b79585 100644 --- a/packages/explorer/src/hooks/useWorldUrl.ts +++ b/packages/explorer/src/hooks/useWorldUrl.ts @@ -2,6 +2,6 @@ import { useParams } from "next/navigation"; export function useWorldUrl() { const params = useParams(); - const { worldAddress } = params; - return (page: string) => `/worlds/${worldAddress}/${page}`; + const { chainName, worldAddress } = params; + return (page: string) => `/${chainName}/worlds/${worldAddress}/${page}`; } diff --git a/packages/explorer/src/queries/useAbiQuery.ts b/packages/explorer/src/queries/useAbiQuery.ts index abe5ffb91c..db8bed764b 100644 --- a/packages/explorer/src/queries/useAbiQuery.ts +++ b/packages/explorer/src/queries/useAbiQuery.ts @@ -1,11 +1,11 @@ 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(worldAddress: Hex) { - const res = await fetch(`/api/world?${new URLSearchParams({ address: worldAddress })}`); +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); } @@ -20,9 +20,11 @@ type AbiQueryResult = { export const useAbiQuery = (): UseQueryResult => { const { worldAddress } = useParams(); + const { id: chainId } = useChain(); + return useQuery({ - queryKey: ["abi", worldAddress], - queryFn: () => getAbi(worldAddress as Hex), + queryKey: ["abi", chainId, worldAddress], + queryFn: () => getAbi(chainId, worldAddress as Hex), select: (data) => { return { abi: data.abi || [],