Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(explorer): custom chain support #3393

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
514c3b7
wip crosschain module
vdrg Dec 3, 2024
3a1e110
refactor module
vdrg Dec 3, 2024
3c66785
Add timestamp validation
vdrg Dec 3, 2024
6a6dba3
Add op chains to explorer
vdrg Dec 4, 2024
c71d8b7
Refactor crosschain module to support bridging
vdrg Dec 4, 2024
be5510d
Refactor api and CrosschainRecord table
vdrg Dec 4, 2024
c4397dc
Remove copied changelog
vdrg Dec 4, 2024
d980b1d
Add remove (wip)
vdrg Dec 5, 2024
121048b
Improve crosschain record validation
vdrg Dec 6, 2024
1b05ade
Fix optimism dependency
vdrg Dec 9, 2024
0817994
pnpm-lock
vdrg Dec 9, 2024
610c894
poc for custom chain configs
karooolis Dec 11, 2024
54c836d
add custom chain name
karooolis Dec 11, 2024
91fff13
Merge branch 'main' into kumpis/explorer-custom-rpc
karooolis Dec 11, 2024
6c902c9
Merge branch 'main' into kumpis/explorer-custom-rpc
karooolis Dec 11, 2024
31a8ef4
Merge branch 'kumpis/explorer-custom-rpc' of github.com:latticexyz/mu…
karooolis Dec 11, 2024
77d13fa
type custom chain wip
karooolis Dec 11, 2024
7cfc3e2
get world abi client
karooolis Dec 11, 2024
0bf4ad7
configure local explorer example
karooolis Dec 11, 2024
9c93236
run local explorer example build
karooolis Dec 11, 2024
fd54693
visualize crosschain records
karooolis Dec 12, 2024
d6fe4d3
blur non-owned records
karooolis Dec 12, 2024
373b5de
remove unused vars
karooolis Dec 12, 2024
fc50d1c
add bridged overlay
karooolis Dec 12, 2024
01eb9d9
remove default ws setting
karooolis Dec 13, 2024
4a88f0e
disable world abi query
karooolis Dec 13, 2024
c132111
increase polling interval
karooolis Dec 13, 2024
9d9facb
increase polling interval
karooolis Dec 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions examples/local-explorer/mprocs.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
procs:
client:
cwd: packages/client
shell: pnpm run dev
contracts:
# client:
# cwd: packages/client
# shell: pnpm run dev
# contracts:
# cwd: packages/contracts
# shell: pnpm mud dev-contracts --rpc http://127.0.0.1:8545
# anvil:
# cwd: packages/contracts
# shell: anvil --base-fee 0 --block-time 2
# explorer:
# cwd: packages/contracts
# shell: pnpm explorer --dev

explorer1:
cwd: packages/contracts
shell: pnpm mud dev-contracts --rpc http://127.0.0.1:8545
anvil:
shell: pnpm explorer --chainId 901 --chainName customA --rpc http://127.0.0.1:9545 --port 19545 --indexerPort 13001 --indexerDatabase indexer19545.db
explorer2:
cwd: packages/contracts
shell: anvil --base-fee 0 --block-time 2
explorer:
cwd: packages/contracts
shell: pnpm explorer --dev
shell: pnpm explorer --chainId 902 --chainName customB --rpc http://127.0.0.1:9546 --port 19546 --indexerPort 13002 --indexerDatabase indexer19546.db
2 changes: 2 additions & 0 deletions packages/explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@latticexyz/store-sync": "workspace:*",
"@latticexyz/world": "workspace:*",
"@monaco-editor/react": "^4.6.0",
"@next/env": "^15.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
Expand All @@ -65,6 +66,7 @@
"lucide-react": "^0.408.0",
"monaco-editor": "^0.52.0",
"next": "14.2.5",
"next-runtime-env": "^3.2.2",
"node-sql-parser": "^5.3.3",
"nuqs": "^1.19.2",
"query-string": "^9.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function Providers({ children }: { children: ReactNode }) {
},
ssr: true,
pollingInterval: {
[chain.id]: chain.id === 31337 ? 100 : 500,
[chain.id]: 5000000,
},
});
}, [chain]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ArrowUpDownIcon, LoaderIcon, TriangleAlertIcon } from "lucide-react";
import { ArrowUpDownIcon, LoaderIcon, LockIcon, TriangleAlertIcon } from "lucide-react";
import { parseAsJson, parseAsString, useQueryState } from "nuqs";
import { Hex, encodeAbiParameters, keccak256, pad } from "viem";
import { useMemo } from "react";
import { Table as TableType } from "@latticexyz/config";
import { getKeySchema, getKeyTuple } from "@latticexyz/protocol-parser/internal";
Expand All @@ -18,6 +19,7 @@ import { Button } from "../../../../../../components/ui/Button";
import { Input } from "../../../../../../components/ui/Input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../../components/ui/Table";
import { cn } from "../../../../../../utils";
import { useCrosschainRecordsQuery } from "../../../../queries/useCrosschainRecordsQuery";
import { TData, TDataRow, useTableDataQuery } from "../../../../queries/useTableDataQuery";
import { EditableTableCell } from "./EditableTableCell";
import { typeSortingFn } from "./utils/typeSortingFn";
Expand All @@ -27,6 +29,7 @@ const initialRows: TData["rows"] = [];

export function TablesViewer({ table, query }: { table?: TableType; query?: string }) {
const { data: tableData, isLoading: isTDataLoading, isFetched, isError, error } = useTableDataQuery({ table, query });
const { data: crosschainRecords } = useCrosschainRecordsQuery(table);
const isLoading = isTDataLoading || !isFetched;
const [globalFilter, setGlobalFilter] = useQueryState("filter", parseAsString.withDefault(""));
const [sorting, setSorting] = useQueryState("sort", parseAsJson<SortingState>().withDefault(initialSortingState));
Expand Down Expand Up @@ -138,13 +141,56 @@ export function TablesViewer({ table, query }: { table?: TableType; query?: stri
</TableHeader>
<TableBody>
{!isError && reactTable.getRowModel().rows?.length ? (
reactTable.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))
reactTable.getRowModel().rows.map((row) => {
if (!table) return null;

try {
if (crosschainRecords?.rows) {
const keySchema = getKeySchema(table);
const keys = Object.keys(keySchema);
const keySchemaValues = keys.map((key) => pad(row.original[key!] as Hex));
const keyHash = keccak256(encodeAbiParameters([{ type: "bytes32[]" }], [keySchemaValues]));
const crosschainRecord = crosschainRecords.rows.find((record) => record.keyHash === keyHash);

if (crosschainRecord && !crosschainRecord.owned) {
return (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"} className="relative">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
<td className="pointer-events-none absolute inset-0 bg-black/30 backdrop-blur-sm">
<div className="ml-[160px] flex h-full items-center font-mono text-xs uppercase">
Bridged <LockIcon className="ml-2 h-3 w-3" />
</div>
</td>
</TableRow>
);
}
}
} catch (error) {
return (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
);
}

return (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
);
})
) : (
<TableRow>
<TableCell
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type Props = {
};

export default async function WorldsPage({ params: { chainName } }: Props) {
// TODO: make sure it works with custom chain
const worlds = await fetchWorlds(chainName);
if (worlds.length === 1) {
return redirect(`/${chainName}/worlds/${worlds[0]}`);
Expand Down
6 changes: 3 additions & 3 deletions packages/explorer/src/app/(explorer)/api/world-abi/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { fetchBlockLogs } from "@latticexyz/block-logs-stream";
import { helloStoreEvent } from "@latticexyz/store";
import { getWorldAbi } from "@latticexyz/store-sync/world";
import { helloWorldEvent } from "@latticexyz/world";
import { chainIdToName, supportedChainId, supportedChains, validateChainId } from "../../../../common";
import { getChainById, supportedChainId, validateChainId } from "../../../../common";

export const dynamic = "force-dynamic";

async function getClient(chainId: supportedChainId) {
const chain = supportedChains[chainIdToName[chainId]];
const chain = getChainById(chainId);
const client = createClient({
chain,
transport: http(),
Expand All @@ -20,7 +20,7 @@ async function getClient(chainId: supportedChainId) {
}

function getIndexerUrl(chainId: supportedChainId) {
const chain = supportedChains[chainIdToName[chainId]];
const chain = getChainById(chainId);
return "indexerUrl" in chain ? chain.indexerUrl : undefined;
}

Expand Down
5 changes: 2 additions & 3 deletions packages/explorer/src/app/(explorer)/hooks/useChain.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useParams } from "next/navigation";
import { Chain } from "viem";
import { supportedChains, validateChainName } from "../../../common";
import { getChain, validateChainName } from "../../../common";

export function useChain(): Chain {
const { chainName } = useParams();
validateChainName(chainName);

const chain = supportedChains[chainName];
return chain;
return getChain(chainName);
}
4 changes: 4 additions & 0 deletions packages/explorer/src/app/(explorer)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { PublicEnvScript } from "next-runtime-env";
import { Inter, JetBrains_Mono } from "next/font/google";
import { Toaster } from "sonner";
import { Theme } from "@radix-ui/themes";
Expand Down Expand Up @@ -32,6 +33,9 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<head>
<PublicEnvScript />
</head>
<body className={`${inter.variable} ${jetbrains.variable} dark`}>
<Theme appearance="dark">
<div className="container">{children}</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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";

export type TDataRow = Record<string, unknown>;
export type TData = {
columns: string[];
rows: TDataRow[];
};

export function useCrosschainRecordsQuery(viewedTable: Table | undefined) {
const { chainName, worldAddress } = useParams();
const { id: chainId } = useChain();

const tableName = "crosschain__crosschain_record";
const decodedQuery = `SELECT * FROM "${worldAddress}__${tableName}"`;
const table = {
tableId: "0x746263726f7373636861696e0000000043726f7373636861696e5265636f7264",
name: "CrosschainRecord",
namespace: "crosschain",
label: "CrosschainRecord",
namespaceLabel: "crosschain",
type: "table",
schema: {
tableId: { type: "bytes32", internalType: "bytes32" },
keyHash: { type: "bytes32", internalType: "bytes32" },
blockNumber: { type: "uint256", internalType: "uint256" },
timestamp: { type: "uint256", internalType: "uint256" },
owned: { type: "bool", internalType: "bool" },
},
key: ["tableId", "keyHash"],
} as const;

return useQuery<DozerResponse, Error, TData | undefined>({
queryKey: ["crosschainRecords", chainName, worldAddress, decodedQuery],
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: decodedQuery,
},
]),
});

const data = await response.json();
if (!response.ok) {
throw new Error(data.msg || "Network response was not ok");
}

return data;
},
select: (data: DozerResponse): TData | undefined => {
if (!data?.result?.[0]) return undefined;

const indexer = indexerForChainId(chainId);
const result = data.result[0];
// if columns are undefined, the result is empty
if (!result[0]) return undefined;

const schema = Object.keys(table.schema);
const columns = result[0]
?.map((columnKey) => {
const schemaKey = schema.find((schemaKey) => schemaKey.toLowerCase() === columnKey);
return schemaKey || columnKey;
})
.filter((key) => schema.includes(key));

const rows = result
.slice(1)
.map((row) =>
Object.fromEntries(
columns.map((key, index) => {
const value = row[index];
const type = table.schema[key as keyof typeof table.schema];
if (type?.type === "bool") {
return [key, indexer.type === "sqlite" ? value === "1" : value];
}
return [key, value];
}),
),
)
.filter((row) => row.tableId === viewedTable?.tableId);

return {
columns,
rows,
};
},
refetchInterval: (query) => {
if (query.state.error) return false;
return 500;
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function useTableDataQuery({ table, query }: Props) {
enabled: !!table && !!query,
refetchInterval: (query) => {
if (query.state.error) return false;
return 1000;
return 500;
},
});
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useParams } from "next/navigation";
import { AbiFunction, Hex } from "viem";
import { UseQueryResult, useQuery } from "@tanstack/react-query";
import { supportedChains, validateChainName } from "../../../common";
import { getChain, validateChainName } from "../../../common";

type AbiQueryResult = {
abi: AbiFunction[];
Expand All @@ -11,7 +11,7 @@ type AbiQueryResult = {
export function useWorldAbiQuery(): UseQueryResult<AbiQueryResult> {
const { chainName, worldAddress } = useParams();
validateChainName(chainName);
const { id: chainId } = supportedChains[chainName];
const { id: chainId } = getChain(chainName);

return useQuery({
queryKey: ["worldAbi", chainName, worldAddress],
Expand All @@ -28,6 +28,7 @@ export function useWorldAbiQuery(): UseQueryResult<AbiQueryResult> {
isWorldDeployed: data.isWorldDeployed,
};
},
refetchInterval: 5000,
refetchInterval: 5000000,
enabled: false,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function blockExplorerTransactionUrl({
validateChainId(chainId);

const chainName = chainIdToName[chainId];
const chain = supportedChains[chainName];
const chain = supportedChains[chainName!]; // TODO: fix types
const explorerUrl = "blockExplorers" in chain && chain.blockExplorers?.default.url;
if (!explorerUrl) return undefined;
return `${explorerUrl}/tx/${hash}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ export function indexerForChainId(chainId: number): { type: "sqlite" | "hosted";
return { type: "sqlite", url: "/api/sqlite-indexer" };
}

// TODO: improve logic
const chainName = chainIdToName[chainId];
if (!chainName) {
return { type: "sqlite", url: "/api/sqlite-indexer" };
}

const chain = supportedChains[chainName] as MUDChain;
return { type: "hosted", url: new URL("/q", chain.indexerUrl).toString() };
}
Loading
Loading