diff --git a/examples/local-explorer/mprocs.yaml b/examples/local-explorer/mprocs.yaml index c3a8d4d594..7c3ea322b9 100644 --- a/examples/local-explorer/mprocs.yaml +++ b/examples/local-explorer/mprocs.yaml @@ -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 diff --git a/packages/explorer/package.json b/packages/explorer/package.json index d92e80c83d..191ae82904 100644 --- a/packages/explorer/package.json +++ b/packages/explorer/package.json @@ -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", @@ -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", diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/Providers.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/Providers.tsx index 43357d18a9..d0827c33af 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/Providers.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/Providers.tsx @@ -33,7 +33,7 @@ export function Providers({ children }: { children: ReactNode }) { }, ssr: true, pollingInterval: { - [chain.id]: chain.id === 31337 ? 100 : 500, + [chain.id]: 5000000, }, }); }, [chain]); diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx index 5703311358..7502f2d16d 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx @@ -1,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"; @@ -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"; @@ -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().withDefault(initialSortingState)); @@ -138,13 +141,56 @@ export function TablesViewer({ table, query }: { table?: TableType; query?: stri {!isError && reactTable.getRowModel().rows?.length ? ( - reactTable.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - {flexRender(cell.column.columnDef.cell, cell.getContext())} - ))} - - )) + 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 ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + +
+ Bridged +
+ +
+ ); + } + } + } catch (error) { + return ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ); + } + + return ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ); + }) ) : ( ) { return ( + + +
{children}
diff --git a/packages/explorer/src/app/(explorer)/queries/useCrosschainRecordsQuery.ts b/packages/explorer/src/app/(explorer)/queries/useCrosschainRecordsQuery.ts new file mode 100644 index 0000000000..fef7737fd7 --- /dev/null +++ b/packages/explorer/src/app/(explorer)/queries/useCrosschainRecordsQuery.ts @@ -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; +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({ + 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; + }, + }); +} diff --git a/packages/explorer/src/app/(explorer)/queries/useTableDataQuery.ts b/packages/explorer/src/app/(explorer)/queries/useTableDataQuery.ts index 6756d0ef5f..867aad76fd 100644 --- a/packages/explorer/src/app/(explorer)/queries/useTableDataQuery.ts +++ b/packages/explorer/src/app/(explorer)/queries/useTableDataQuery.ts @@ -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; }, }); } diff --git a/packages/explorer/src/app/(explorer)/queries/useWorldAbiQuery.ts b/packages/explorer/src/app/(explorer)/queries/useWorldAbiQuery.ts index 083d820412..2177e7810f 100644 --- a/packages/explorer/src/app/(explorer)/queries/useWorldAbiQuery.ts +++ b/packages/explorer/src/app/(explorer)/queries/useWorldAbiQuery.ts @@ -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[]; @@ -11,7 +11,7 @@ type AbiQueryResult = { export function useWorldAbiQuery(): UseQueryResult { const { chainName, worldAddress } = useParams(); validateChainName(chainName); - const { id: chainId } = supportedChains[chainName]; + const { id: chainId } = getChain(chainName); return useQuery({ queryKey: ["worldAbi", chainName, worldAddress], @@ -28,6 +28,7 @@ export function useWorldAbiQuery(): UseQueryResult { isWorldDeployed: data.isWorldDeployed, }; }, - refetchInterval: 5000, + refetchInterval: 5000000, + enabled: false, }); } diff --git a/packages/explorer/src/app/(explorer)/utils/blockExplorerTransactionUrl.ts b/packages/explorer/src/app/(explorer)/utils/blockExplorerTransactionUrl.ts index aa8a43a0b0..dd31407000 100644 --- a/packages/explorer/src/app/(explorer)/utils/blockExplorerTransactionUrl.ts +++ b/packages/explorer/src/app/(explorer)/utils/blockExplorerTransactionUrl.ts @@ -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}`; diff --git a/packages/explorer/src/app/(explorer)/utils/indexerForChainId.ts b/packages/explorer/src/app/(explorer)/utils/indexerForChainId.ts index e7f23ed1ae..a7c76b9361 100644 --- a/packages/explorer/src/app/(explorer)/utils/indexerForChainId.ts +++ b/packages/explorer/src/app/(explorer)/utils/indexerForChainId.ts @@ -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() }; } diff --git a/packages/explorer/src/bin/explorer.ts b/packages/explorer/src/bin/explorer.ts index 218018aaba..7073ebeac4 100755 --- a/packages/explorer/src/bin/explorer.ts +++ b/packages/explorer/src/bin/explorer.ts @@ -3,7 +3,7 @@ import { rm } from "fs/promises"; import path from "path"; import process from "process"; import { fileURLToPath } from "url"; -import { anvil } from "viem/chains"; +// import { anvil } from "viem/chains"; import yargs from "yargs"; import { ChildProcess, spawn } from "child_process"; import { validateChainId } from "../common"; @@ -31,12 +31,36 @@ const argv = yargs(process.argv.slice(2)) type: "number", default: process.env.CHAIN_ID || 31337, }, + chainName: { + alias: "n", + description: "Chain name", + type: "string", + default: process.env.CHAIN_NAME || "custom", + }, + rpcHttpUrl: { + alias: ["rpc", "rpcUrl", "rpcHttpUrl"], + description: "RPC HTTP URL", + type: "string", + default: process.env.RPC_HTTP_URL || "http://127.0.0.1:8545", + }, + rpcWsUrl: { + alias: ["rpcWs", "rpcWsUrl"], + description: "RPC WebSocket URL", + type: "string", + default: process.env.RPC_WS_URL, + }, indexerDatabase: { alias: "i", description: "Path to the indexer database", type: "string", default: process.env.INDEXER_DATABASE || "indexer.db", }, + indexerPort: { + alias: "ip", + description: "Port number for the indexer", + type: "number", + default: process.env.INDEXER_PORT || 3001, + }, dev: { alias: "D", description: "Run in development mode", @@ -45,12 +69,16 @@ const argv = yargs(process.argv.slice(2)) }, }) .check((argv) => { + // skip chainId validation if custom configs are provided + if (argv.rpcHttpUrl || argv.rpcWsUrl) { + return true; + } validateChainId(Number(argv.chainId)); return true; }) .parseSync(); -const { port, hostname, chainId, indexerDatabase, dev } = argv; +const { port, hostname, chainId, chainName, rpcHttpUrl, rpcWsUrl, indexerDatabase, indexerPort, dev } = argv; const indexerDatabasePath = path.join(packageRoot, indexerDatabase); let explorerProcess: ChildProcess; @@ -60,7 +88,15 @@ async function startExplorer() { const env = { ...process.env, CHAIN_ID: chainId.toString(), + CHAIN_NAME: chainName, + RPC_HTTP_URL: rpcHttpUrl, + RPC_WS_URL: rpcWsUrl, INDEXER_DATABASE: indexerDatabasePath, + + NEXT_PUBLIC_CHAIN_ID: chainId.toString(), + NEXT_PUBLIC_CHAIN_NAME: chainName, + NEXT_PUBLIC_RPC_HTTP_URL: rpcHttpUrl, + NEXT_PUBLIC_RPC_WS_URL: rpcWsUrl, }; if (dev) { @@ -87,22 +123,24 @@ async function startExplorer() { } async function startStoreIndexer() { - if (chainId !== anvil.id) { - console.log("Skipping SQLite indexer for non-anvil chain ID", chainId); - return; - } + // TODO: make conditional skipping + // if (chainId !== anvil.id) { + // console.log("Skipping SQLite indexer for non-anvil chain ID", chainId); + // return; + // } await rm(indexerDatabasePath, { recursive: true, force: true }); - console.log("Running SQLite indexer for anvil..."); + console.log("Running SQLite indexer..."); // TODO: running for custom chain indexerProcess = spawn("sh", ["node_modules/.bin/sqlite-indexer"], { cwd: packageRoot, stdio: "inherit", env: { DEBUG: "mud:*", - RPC_HTTP_URL: "http://127.0.0.1:8545", + RPC_HTTP_URL: rpcHttpUrl, FOLLOW_BLOCK_TAG: "latest", SQLITE_FILENAME: indexerDatabase, + PORT: indexerPort.toString(), ...process.env, }, }); diff --git a/packages/explorer/src/common.ts b/packages/explorer/src/common.ts index da18b9bc32..8a45be4d4a 100644 --- a/packages/explorer/src/common.ts +++ b/packages/explorer/src/common.ts @@ -1,34 +1,92 @@ +import { env } from "next-runtime-env"; +import { Chain, defineChain } from "viem"; import { anvil } from "viem/chains"; import { garnet, redstone, rhodolite } from "@latticexyz/common/chains"; export const internalNamespaces = ["world", "store", "metadata", "puppet", "erc20-puppet", "erc721-puppet"]; -export const supportedChains = { anvil, rhodolite, garnet, redstone } as const; -export type supportedChains = typeof supportedChains; +export const constructCustomChain = (): Chain | null => { + const chainId = env("NEXT_PUBLIC_CHAIN_ID"); + const chainName = env("NEXT_PUBLIC_CHAIN_NAME"); + const rpcHttpUrl = env("NEXT_PUBLIC_RPC_HTTP_URL"); + const rpcWsUrl = env("NEXT_PUBLIC_RPC_WS_URL"); + + if (!chainId || !chainName || !rpcHttpUrl) { + return null; + } + + return defineChain({ + id: Number(chainId), + name: chainName, + nativeCurrency: { + decimals: 18, + name: "Ether", + symbol: "ETH", + }, + rpcUrls: { + default: { + http: [rpcHttpUrl], + ...(rpcWsUrl ? { webSocket: [rpcWsUrl] } : {}), + }, + }, + }); +}; + +export const customChain = constructCustomChain(); + +export const supportedChains = { + anvil, + rhodolite, + garnet, + redstone, +} as const; +export type supportedChains = typeof supportedChains; export type supportedChainName = keyof supportedChains; export type supportedChainId = supportedChains[supportedChainName]["id"]; +export const getChain = (chainName: supportedChainName | string) => { + if (chainName === customChain?.name) { + return customChain; + } else if (chainName in supportedChains) { + // @ts-expect-error ignore for now TODO: fix types + return supportedChains[chainName]; + } + return null; +}; + +export const getChainById = (chainId: supportedChainId | number) => { + if (chainId === customChain?.id) { + return customChain; + } else if (chainId in supportedChains) { + // @ts-expect-error ignore for now TODO: fix types + return supportedChains[chainId]; + } + return null; +}; + export const chainIdToName = Object.fromEntries( Object.entries(supportedChains).map(([chainName, chain]) => [chain.id, chainName]), ) as Record; -export function isValidChainId(chainId: unknown): chainId is supportedChainId { - return typeof chainId === "number" && chainId in chainIdToName; +export function isValidChainId(chainId: unknown): chainId is supportedChainId | number { + return typeof chainId === "number" && (chainId in chainIdToName || customChain?.id === chainId); } -export function isValidChainName(name: unknown): name is supportedChainName { - return typeof name === "string" && name in supportedChains; +export function isValidChainName(name: unknown): name is supportedChainName | string { + return typeof name === "string" && (name in supportedChains || customChain?.name === name); } export function validateChainId(chainId: unknown): asserts chainId is supportedChainId { if (!isValidChainId(chainId)) { - throw new Error(`Invalid chain ID. Supported chains are: ${Object.keys(chainIdToName).join(", ")}.`); + const supportedIds = Object.keys(chainIdToName); + throw new Error(`Invalid chain ID. Supported chains are: ${supportedIds.join(", ")}.`); } } -export function validateChainName(name: unknown): asserts name is supportedChainName { +export function validateChainName(name: unknown): asserts name is supportedChainName | string { if (!isValidChainName(name)) { - throw new Error(`Invalid chain name. Supported chains are: ${Object.keys(supportedChains).join(", ")}.`); + const supportedNames = Object.keys(supportedChains); + throw new Error(`Invalid chain name. Supported chains are: ${supportedNames.join(", ")}.`); } } diff --git a/packages/explorer/src/components/LatestBlock.tsx b/packages/explorer/src/components/LatestBlock.tsx index b493022b82..8ddcdfe896 100644 --- a/packages/explorer/src/components/LatestBlock.tsx +++ b/packages/explorer/src/components/LatestBlock.tsx @@ -8,7 +8,8 @@ export function LatestBlock() { watch: true, chainId, query: { - refetchInterval: 1000, + refetchInterval: 10000000, + enabled: false, }, }); diff --git a/packages/explorer/src/components/Navigation.tsx b/packages/explorer/src/components/Navigation.tsx index 95921087fe..359b4ca04a 100644 --- a/packages/explorer/src/components/Navigation.tsx +++ b/packages/explorer/src/components/Navigation.tsx @@ -1,10 +1,10 @@ "use client"; -import { Loader } from "lucide-react"; +// 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 { useWorldAbiQuery } from "../app/(explorer)/queries/useWorldAbiQuery"; import { LatestBlock } from "../components/LatestBlock"; import { Separator } from "../components/ui/Separator"; import { cn } from "../utils"; @@ -28,7 +28,7 @@ function NavigationLink({ href, children }: { href: string; children: React.Reac } export function Navigation() { - const { data, isFetched } = useWorldAbiQuery(); + // const { data, isFetched } = useWorldAbiQuery(); return (
@@ -38,11 +38,11 @@ export function Navigation() { Observe
- {isFetched && !data?.isWorldDeployed && ( + {/* {isFetched && !data?.isWorldDeployed && (

Waiting for world deploy

- )} + )} */}
diff --git a/packages/world-module-crosschain/.gitignore b/packages/world-module-crosschain/.gitignore new file mode 100644 index 0000000000..1e4ded714a --- /dev/null +++ b/packages/world-module-crosschain/.gitignore @@ -0,0 +1,2 @@ +cache +out diff --git a/packages/world-module-crosschain/.solhint.json b/packages/world-module-crosschain/.solhint.json new file mode 100644 index 0000000000..4e2baa8be7 --- /dev/null +++ b/packages/world-module-crosschain/.solhint.json @@ -0,0 +1,8 @@ +{ + "extends": "solhint:recommended", + "rules": { + "compiler-version": ["error", ">=0.8.0"], + "avoid-low-level-calls": "off", + "func-visibility": ["warn", { "ignoreConstructors": true }] + } +} diff --git a/packages/world-module-crosschain/README.md b/packages/world-module-crosschain/README.md new file mode 100644 index 0000000000..bb63585de0 --- /dev/null +++ b/packages/world-module-crosschain/README.md @@ -0,0 +1 @@ +# Metadata world module diff --git a/packages/world-module-crosschain/foundry.toml b/packages/world-module-crosschain/foundry.toml new file mode 100644 index 0000000000..f0e017f5a0 --- /dev/null +++ b/packages/world-module-crosschain/foundry.toml @@ -0,0 +1,15 @@ +[profile.default] +solc = "0.8.24" +ffi = false +fuzz_runs = 256 +optimizer = true +optimizer_runs = 3000 +verbosity = 2 +allow_paths = ["../../node_modules", "../"] +src = "src" +out = "out" +bytecode_hash = "none" +extra_output_files = [ + "abi", + "evm.bytecode" +] diff --git a/packages/world-module-crosschain/mud.config.ts b/packages/world-module-crosschain/mud.config.ts new file mode 100644 index 0000000000..a20e76757b --- /dev/null +++ b/packages/world-module-crosschain/mud.config.ts @@ -0,0 +1,32 @@ +import { defineWorld } from "@latticexyz/world"; + +export default defineWorld({ + userTypes: { + ResourceId: { + type: "bytes32", + filePath: "@latticexyz/store/src/ResourceId.sol", + }, + }, + namespaces: { + root: { + namespace: "", + }, + crosschain: { + tables: { + CrosschainRecord: { + schema: { + tableId: "ResourceId", + keyHash: "bytes32", + blockNumber: "uint256", + timestamp: "uint256", + owned: "bool", + }, + key: ["tableId", "keyHash"], + }, + }, + }, + }, + codegen: { + generateSystemLibraries: true, + }, +}); diff --git a/packages/world-module-crosschain/package.json b/packages/world-module-crosschain/package.json new file mode 100644 index 0000000000..f3f0777087 --- /dev/null +++ b/packages/world-module-crosschain/package.json @@ -0,0 +1,63 @@ +{ + "name": "@latticexyz/world-module-crosschain", + "version": "2.2.14", + "description": "Crosschain world module", + "repository": { + "type": "git", + "url": "https://github.com/latticexyz/mud.git", + "directory": "packages/world-module-crosschain" + }, + "license": "MIT", + "type": "module", + "exports": { + "./mud.config": "./dist/mud.config.js", + "./out/*": "./out/*" + }, + "typesVersions": { + "*": { + "mud.config": [ + "./dist/mud.config.d.ts" + ] + } + }, + "files": [ + "dist", + "out", + "src" + ], + "scripts": { + "build": "pnpm run build:mud && pnpm run build:abi && pnpm run build:abi-ts && pnpm run build:js", + "build:abi": "forge build", + "build:abi-ts": "abi-ts", + "build:js": "tsup", + "build:mud": "tsx ./ts/build.ts", + "clean": "pnpm run clean:abi && pnpm run clean:js && pnpm run clean:mud", + "clean:abi": "forge clean", + "clean:js": "shx rm -rf dist", + "clean:mud": "shx rm -rf src/**/codegen", + "dev": "tsup --watch", + "gas-report": "gas-report --save gas-report.json", + "lint": "solhint --config ./.solhint.json 'src/**/*.sol'", + "test": "forge test", + "test:ci": "pnpm run test" + }, + "dependencies": { + "@latticexyz/schema-type": "workspace:*", + "@latticexyz/store": "workspace:*", + "@latticexyz/world": "workspace:*", + "optimism": "https://github.com/ethereum-optimism/optimism#ecdb788a18022d4b78d6288c1f6ec94c152d02f8" + }, + "devDependencies": { + "@latticexyz/abi-ts": "workspace:*", + "@latticexyz/gas-report": "workspace:*", + "@types/node": "^18.15.11", + "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", + "forge-std": "https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1", + "solhint": "^3.3.7", + "tsup": "^6.7.0", + "vitest": "0.34.6" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/world-module-crosschain/remappings.txt b/packages/world-module-crosschain/remappings.txt new file mode 100644 index 0000000000..e7777afad9 --- /dev/null +++ b/packages/world-module-crosschain/remappings.txt @@ -0,0 +1,4 @@ +ds-test/=node_modules/ds-test/src/ +forge-std/=node_modules/forge-std/src/ +@latticexyz/=node_modules/@latticexyz/ +@contracts-bedrock/=node_modules/optimism/packages/contracts-bedrock/src/ diff --git a/packages/world-module-crosschain/src/CrosschainModule.sol b/packages/world-module-crosschain/src/CrosschainModule.sol new file mode 100644 index 0000000000..e07c967a61 --- /dev/null +++ b/packages/world-module-crosschain/src/CrosschainModule.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { IWorldRegistrationSystem } from "@latticexyz/world/src/codegen/interfaces/IWorldRegistrationSystem.sol"; +import { System } from "@latticexyz/world/src/System.sol"; +import { revertWithBytes } from "@latticexyz/world/src/revertWithBytes.sol"; +import { Module } from "@latticexyz/world/src/Module.sol"; +import { ResourceId, WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; +import { ResourceIds } from "@latticexyz/store/src/codegen/tables/ResourceIds.sol"; +import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol"; +import { ROOT_NAMESPACE } from "@latticexyz/world/src/constants.sol"; + +import { CrosschainSystem } from "./namespaces/root/CrosschainSystem.sol"; +import { CrosschainRecord } from "./namespaces/crosschain/codegen/tables/CrosschainRecord.sol"; + +contract CrosschainModule is Module { + using WorldResourceIdInstance for ResourceId; + + CrosschainSystem private immutable crosschainSystem = new CrosschainSystem(); + + function installRoot(bytes memory) public { + if (!ResourceIds.getExists(CrosschainRecord._tableId)) { + CrosschainRecord.register(); + } + + ResourceId crosschainSystemId = WorldResourceIdLib.encode(RESOURCE_SYSTEM, ROOT_NAMESPACE, "CrosschainSystem"); + if (!ResourceIds.getExists(crosschainSystemId)) { + _registerSystem(crosschainSystemId, crosschainSystem); + _registerRootFunctionSelector(crosschainSystemId, "crosschainRead(bytes32,bytes32[])"); + _registerRootFunctionSelector( + crosschainSystemId, + "crosschainWrite((address,uint256,uint256,uint256,uint256),bytes)" + ); + } + } + + function install(bytes memory) public pure { + revert Module_NonRootInstallNotSupported(); + } + + function _registerSystem(ResourceId systemId, System system) internal { + (bool success, bytes memory returnData) = _world().delegatecall( + abi.encodeCall(IWorldRegistrationSystem.registerSystem, (systemId, system, true)) + ); + if (!success) revertWithBytes(returnData); + } + + function _registerRootFunctionSelector(ResourceId systemId, string memory functionSignature) internal { + (bool success, bytes memory returnData) = _world().delegatecall( + abi.encodeCall( + IWorldRegistrationSystem.registerRootFunctionSelector, + (systemId, functionSignature, functionSignature) + ) + ); + + if (!success) revertWithBytes(returnData); + } +} diff --git a/packages/world-module-crosschain/src/codegen/world/ICrosschainSystem.sol b/packages/world-module-crosschain/src/codegen/world/ICrosschainSystem.sol new file mode 100644 index 0000000000..0ac1542057 --- /dev/null +++ b/packages/world-module-crosschain/src/codegen/world/ICrosschainSystem.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { Identifier } from "@contracts-bedrock/L2/interfaces/ICrossL2Inbox.sol"; + +/** + * @title ICrosschainSystem + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. + */ +interface ICrosschainSystem { + error WrongWorld(); + error NotCrosschainRecord(); + error MoreRecentRecordExists(); + error RecordNotOwned(); + error RecordAlreadyExists(); + error RecordBridgedToADifferentChain(); + error InvalidRecordTimestamp(); + + function create(ResourceId tableId, bytes32[] memory keyTuple) external; + + function remove(ResourceId tableId, bytes32[] memory keyTuple) external; + + function bridge(ResourceId tableId, bytes32[] memory keyTuple, uint256 targetChain) external; + + function crosschainRead(ResourceId tableId, bytes32[] calldata keyTuple) external; + + function crosschainWrite(Identifier calldata identifier, bytes calldata _crosschainRead) external; +} diff --git a/packages/world-module-crosschain/src/codegen/world/IWorld.sol b/packages/world-module-crosschain/src/codegen/world/IWorld.sol new file mode 100644 index 0000000000..03eb47ea8a --- /dev/null +++ b/packages/world-module-crosschain/src/codegen/world/IWorld.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; +import { ICrosschainSystem } from "./ICrosschainSystem.sol"; + +/** + * @title IWorld + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @notice This interface integrates all systems and associated function selectors + * that are dynamically registered in the World during deployment. + * @dev This is an autogenerated file; do not edit manually. + */ +interface IWorld is IBaseWorld, ICrosschainSystem {} diff --git a/packages/world-module-crosschain/src/namespaces/crosschain/codegen/index.sol b/packages/world-module-crosschain/src/namespaces/crosschain/codegen/index.sol new file mode 100644 index 0000000000..62a82d4d01 --- /dev/null +++ b/packages/world-module-crosschain/src/namespaces/crosschain/codegen/index.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { CrosschainRecord, CrosschainRecordData } from "./tables/CrosschainRecord.sol"; diff --git a/packages/world-module-crosschain/src/namespaces/crosschain/codegen/tables/CrosschainRecord.sol b/packages/world-module-crosschain/src/namespaces/crosschain/codegen/tables/CrosschainRecord.sol new file mode 100644 index 0000000000..b2ff4fde96 --- /dev/null +++ b/packages/world-module-crosschain/src/namespaces/crosschain/codegen/tables/CrosschainRecord.sol @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +// Import store internals +import { IStore } from "@latticexyz/store/src/IStore.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; +import { Bytes } from "@latticexyz/store/src/Bytes.sol"; +import { Memory } from "@latticexyz/store/src/Memory.sol"; +import { SliceLib } from "@latticexyz/store/src/Slice.sol"; +import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; +import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { Schema } from "@latticexyz/store/src/Schema.sol"; +import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +// Import user types +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +struct CrosschainRecordData { + uint256 blockNumber; + uint256 timestamp; + bool owned; +} + +library CrosschainRecord { + // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "crosschain", name: "CrosschainRecord", typeId: RESOURCE_TABLE });` + ResourceId constant _tableId = ResourceId.wrap(0x746263726f7373636861696e0000000043726f7373636861696e5265636f7264); + + FieldLayout constant _fieldLayout = + FieldLayout.wrap(0x0041030020200100000000000000000000000000000000000000000000000000); + + // Hex-encoded key schema of (bytes32, bytes32) + Schema constant _keySchema = Schema.wrap(0x004002005f5f0000000000000000000000000000000000000000000000000000); + // Hex-encoded value schema of (uint256, uint256, bool) + Schema constant _valueSchema = Schema.wrap(0x004103001f1f6000000000000000000000000000000000000000000000000000); + + /** + * @notice Get the table's key field names. + * @return keyNames An array of strings with the names of key fields. + */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](2); + keyNames[0] = "tableId"; + keyNames[1] = "keyHash"; + } + + /** + * @notice Get the table's value field names. + * @return fieldNames An array of strings with the names of value fields. + */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](3); + fieldNames[0] = "blockNumber"; + fieldNames[1] = "timestamp"; + fieldNames[2] = "owned"; + } + + /** + * @notice Register the table with its config. + */ + function register() internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register() internal { + StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Get blockNumber. + */ + function getBlockNumber(ResourceId tableId, bytes32 keyHash) internal view returns (uint256 blockNumber) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get blockNumber. + */ + function _getBlockNumber(ResourceId tableId, bytes32 keyHash) internal view returns (uint256 blockNumber) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Set blockNumber. + */ + function setBlockNumber(ResourceId tableId, bytes32 keyHash, uint256 blockNumber) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((blockNumber)), _fieldLayout); + } + + /** + * @notice Set blockNumber. + */ + function _setBlockNumber(ResourceId tableId, bytes32 keyHash, uint256 blockNumber) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((blockNumber)), _fieldLayout); + } + + /** + * @notice Get timestamp. + */ + function getTimestamp(ResourceId tableId, bytes32 keyHash) internal view returns (uint256 timestamp) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 1, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get timestamp. + */ + function _getTimestamp(ResourceId tableId, bytes32 keyHash) internal view returns (uint256 timestamp) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 1, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Set timestamp. + */ + function setTimestamp(ResourceId tableId, bytes32 keyHash, uint256 timestamp) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + StoreSwitch.setStaticField(_tableId, _keyTuple, 1, abi.encodePacked((timestamp)), _fieldLayout); + } + + /** + * @notice Set timestamp. + */ + function _setTimestamp(ResourceId tableId, bytes32 keyHash, uint256 timestamp) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + StoreCore.setStaticField(_tableId, _keyTuple, 1, abi.encodePacked((timestamp)), _fieldLayout); + } + + /** + * @notice Get owned. + */ + function getOwned(ResourceId tableId, bytes32 keyHash) internal view returns (bool owned) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 2, _fieldLayout); + return (_toBool(uint8(bytes1(_blob)))); + } + + /** + * @notice Get owned. + */ + function _getOwned(ResourceId tableId, bytes32 keyHash) internal view returns (bool owned) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 2, _fieldLayout); + return (_toBool(uint8(bytes1(_blob)))); + } + + /** + * @notice Set owned. + */ + function setOwned(ResourceId tableId, bytes32 keyHash, bool owned) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + StoreSwitch.setStaticField(_tableId, _keyTuple, 2, abi.encodePacked((owned)), _fieldLayout); + } + + /** + * @notice Set owned. + */ + function _setOwned(ResourceId tableId, bytes32 keyHash, bool owned) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + StoreCore.setStaticField(_tableId, _keyTuple, 2, abi.encodePacked((owned)), _fieldLayout); + } + + /** + * @notice Get the full data. + */ + function get(ResourceId tableId, bytes32 keyHash) internal view returns (CrosschainRecordData memory _table) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + (bytes memory _staticData, EncodedLengths _encodedLengths, bytes memory _dynamicData) = StoreSwitch.getRecord( + _tableId, + _keyTuple, + _fieldLayout + ); + return decode(_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Get the full data. + */ + function _get(ResourceId tableId, bytes32 keyHash) internal view returns (CrosschainRecordData memory _table) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + (bytes memory _staticData, EncodedLengths _encodedLengths, bytes memory _dynamicData) = StoreCore.getRecord( + _tableId, + _keyTuple, + _fieldLayout + ); + return decode(_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Set the full data using individual values. + */ + function set(ResourceId tableId, bytes32 keyHash, uint256 blockNumber, uint256 timestamp, bool owned) internal { + bytes memory _staticData = encodeStatic(blockNumber, timestamp, owned); + + EncodedLengths _encodedLengths; + bytes memory _dynamicData; + + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + StoreSwitch.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Set the full data using individual values. + */ + function _set(ResourceId tableId, bytes32 keyHash, uint256 blockNumber, uint256 timestamp, bool owned) internal { + bytes memory _staticData = encodeStatic(blockNumber, timestamp, owned); + + EncodedLengths _encodedLengths; + bytes memory _dynamicData; + + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + StoreCore.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData, _fieldLayout); + } + + /** + * @notice Set the full data using the data struct. + */ + function set(ResourceId tableId, bytes32 keyHash, CrosschainRecordData memory _table) internal { + bytes memory _staticData = encodeStatic(_table.blockNumber, _table.timestamp, _table.owned); + + EncodedLengths _encodedLengths; + bytes memory _dynamicData; + + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + StoreSwitch.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Set the full data using the data struct. + */ + function _set(ResourceId tableId, bytes32 keyHash, CrosschainRecordData memory _table) internal { + bytes memory _staticData = encodeStatic(_table.blockNumber, _table.timestamp, _table.owned); + + EncodedLengths _encodedLengths; + bytes memory _dynamicData; + + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + StoreCore.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData, _fieldLayout); + } + + /** + * @notice Decode the tightly packed blob of static data using this table's field layout. + */ + function decodeStatic(bytes memory _blob) internal pure returns (uint256 blockNumber, uint256 timestamp, bool owned) { + blockNumber = (uint256(Bytes.getBytes32(_blob, 0))); + + timestamp = (uint256(Bytes.getBytes32(_blob, 32))); + + owned = (_toBool(uint8(Bytes.getBytes1(_blob, 64)))); + } + + /** + * @notice Decode the tightly packed blobs using this table's field layout. + * @param _staticData Tightly packed static fields. + * + * + */ + function decode( + bytes memory _staticData, + EncodedLengths, + bytes memory + ) internal pure returns (CrosschainRecordData memory _table) { + (_table.blockNumber, _table.timestamp, _table.owned) = decodeStatic(_staticData); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId tableId, bytes32 keyHash) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId tableId, bytes32 keyHash) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @notice Tightly pack static (fixed length) data using this table's schema. + * @return The static data, encoded into a sequence of bytes. + */ + function encodeStatic(uint256 blockNumber, uint256 timestamp, bool owned) internal pure returns (bytes memory) { + return abi.encodePacked(blockNumber, timestamp, owned); + } + + /** + * @notice Encode all of a record's fields. + * @return The static (fixed length) data, encoded into a sequence of bytes. + * @return The lengths of the dynamic fields (packed into a single bytes32 value). + * @return The dynamic (variable length) data, encoded into a sequence of bytes. + */ + function encode( + uint256 blockNumber, + uint256 timestamp, + bool owned + ) internal pure returns (bytes memory, EncodedLengths, bytes memory) { + bytes memory _staticData = encodeStatic(blockNumber, timestamp, owned); + + EncodedLengths _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple(ResourceId tableId, bytes32 keyHash) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(tableId); + _keyTuple[1] = keyHash; + + return _keyTuple; + } +} + +/** + * @notice Cast a value to a bool. + * @dev Boolean values are encoded as uint8 (1 = true, 0 = false), but Solidity doesn't allow casting between uint8 and bool. + * @param value The uint8 value to convert. + * @return result The boolean value. + */ +function _toBool(uint8 value) pure returns (bool result) { + assembly { + result := value + } +} diff --git a/packages/world-module-crosschain/src/namespaces/root/CrosschainSystem.sol b/packages/world-module-crosschain/src/namespaces/root/CrosschainSystem.sol new file mode 100644 index 0000000000..741f4c2080 --- /dev/null +++ b/packages/world-module-crosschain/src/namespaces/root/CrosschainSystem.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { EncodedLengths } from "@latticexyz/store/src/EncodedLengths.sol"; +import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; + +import { System } from "@latticexyz/world/src/System.sol"; +import { ROOT_NAMESPACE } from "@latticexyz/world/src/constants.sol"; +import { WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; +import { AccessControl } from "@latticexyz/world/src/AccessControl.sol"; + +import { Identifier, ICrossL2Inbox } from "@contracts-bedrock/L2/interfaces/ICrossL2Inbox.sol"; +import { Predeploys } from "@contracts-bedrock/libraries/Predeploys.sol"; + +import { CrosschainRecord, CrosschainRecordData } from "../crosschain/codegen/tables/CrosschainRecord.sol"; + +contract CrosschainSystem is System { + using WorldResourceIdInstance for ResourceId; + + // TODO: rename errors and events + error WrongWorld(); + error NotCrosschainRecord(); + error MoreRecentRecordExists(); + error RecordNotOwned(); + error RecordAlreadyExists(); + error RecordBridgedToADifferentChain(); + error InvalidRecordTimestamp(); + + event World_CrosschainRecord( + ResourceId indexed tableId, + bytes32[] keyTuple, + bytes staticData, + EncodedLengths encodedLengths, + bytes dynamicData, + uint256 toChainId + ); + + event World_CrosschainRecordRemoved(ResourceId indexed tableId, bytes32[] keyTuple); + + function create(ResourceId tableId, bytes32[] memory keyTuple) external { + AccessControl._requireAccess(tableId.getNamespaceId(), _msgSender()); + + bytes32 keyHash = keccak256(abi.encode(keyTuple)); + + CrosschainRecordData memory data = CrosschainRecord.get(tableId, keyHash); + if (data.blockNumber > 0) { + revert RecordAlreadyExists(); + } + + data.blockNumber = block.number; + data.timestamp = block.timestamp; + data.owned = true; + + CrosschainRecord.set(tableId, keyHash, data); + } + + function remove(ResourceId tableId, bytes32[] memory keyTuple) external { + AccessControl._requireAccess(tableId.getNamespaceId(), _msgSender()); + + bytes32 keyHash = keccak256(abi.encode(keyTuple)); + + CrosschainRecordData memory data = CrosschainRecord.get(tableId, keyHash); + if (!data.owned) { + revert RecordNotOwned(); + } + + CrosschainRecord.deleteRecord(tableId, keyHash); + + emit World_CrosschainRecordRemoved(tableId, keyTuple); + } + + function bridge(ResourceId tableId, bytes32[] memory keyTuple, uint256 targetChain) external { + AccessControl._requireAccess(tableId.getNamespaceId(), _msgSender()); + + bytes32 keyHash = keccak256(abi.encode(keyTuple)); + + CrosschainRecordData memory data = CrosschainRecord.get(tableId, keyHash); + + // We can only bridge records we own + if (!data.owned) { + revert RecordNotOwned(); + } + + data.blockNumber = block.number; + data.timestamp = block.timestamp; + + // We don't own this record anymore + data.owned = false; + + CrosschainRecord.set(tableId, keyHash, data); + + (bytes memory staticData, EncodedLengths encodedLengths, bytes memory dynamicData) = StoreCore.getRecord( + tableId, + keyTuple + ); + + emit World_CrosschainRecord(tableId, keyTuple, staticData, encodedLengths, dynamicData, targetChain); + } + + /** + * @dev Anyone can call this method so other chains can consume the record data. + * Can only be called for records owned by this world + */ + function crosschainRead(ResourceId tableId, bytes32[] calldata keyTuple) external { + bytes32 keyHash = keccak256(abi.encode(keyTuple)); + CrosschainRecordData memory data = CrosschainRecord.get(tableId, keyHash); + + // We can only bridge records we own + if (!data.owned) { + revert RecordNotOwned(); + } + + // TODO: should we update metadata? + + (bytes memory staticData, EncodedLengths encodedLengths, bytes memory dynamicData) = StoreCore.getRecord( + tableId, + keyTuple + ); + + // using toChainId == block.chainid means that other chains can't own this record + emit World_CrosschainRecord(tableId, keyTuple, staticData, encodedLengths, dynamicData, block.chainid); + } + + // TODO: add crosschainRemove or add that logic to crosschainWrite (depending on the selector) + + // Anyone can call this to verify a crosschain record and store it + function crosschainWrite(Identifier calldata identifier, bytes calldata _crosschainRead) external { + if (identifier.origin != address(this)) revert WrongWorld(); + + // TODO: check tableId resource type? + (bytes32 selector, ResourceId tableId) = abi.decode(_crosschainRead[:64], (bytes32, ResourceId)); + if (selector != World_CrosschainRecord.selector) revert NotCrosschainRecord(); + + ICrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(identifier, keccak256(_crosschainRead)); + + ( + bytes32[] memory keyTuple, + bytes memory staticData, + EncodedLengths encodedLengths, + bytes memory dynamicData, + uint256 toChainId + ) = abi.decode(_crosschainRead[64:], (bytes32[], bytes, EncodedLengths, bytes, uint256)); + + bytes32 keyHash = keccak256(abi.encode(keyTuple)); + + // If we own the record, then writes are not allowed as it would override it + CrosschainRecordData memory data = CrosschainRecord.get(tableId, keyHash); + if (data.owned) { + revert RecordAlreadyExists(); + } + + // If toChainId == block.chainId it means it was bridged to this world + // If toChainId != block.chainid it means it we can consume but we don't own it + if (toChainId == block.chainid) { + data.owned = true; + } + + // If timestamp is in the future, revert + // This also creates a total ordering across chains + if (block.timestamp < identifier.timestamp) { + revert InvalidRecordTimestamp(); + } + + if (identifier.timestamp < data.timestamp) { + revert MoreRecentRecordExists(); + } + + data.blockNumber = identifier.blockNumber; + data.timestamp = identifier.timestamp; + + CrosschainRecord.set(tableId, keyHash, data); + + StoreCore.setRecord(tableId, keyTuple, staticData, encodedLengths, dynamicData); + } +} diff --git a/packages/world-module-crosschain/src/namespaces/root/codegen/systems/CrosschainSystemLib.sol b/packages/world-module-crosschain/src/namespaces/root/codegen/systems/CrosschainSystemLib.sol new file mode 100644 index 0000000000..0817d2dcda --- /dev/null +++ b/packages/world-module-crosschain/src/namespaces/root/codegen/systems/CrosschainSystemLib.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { CrosschainSystem } from "../../CrosschainSystem.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { Identifier } from "@contracts-bedrock/L2/interfaces/ICrossL2Inbox.sol"; +import { revertWithBytes } from "@latticexyz/world/src/revertWithBytes.sol"; +import { IWorldCall } from "@latticexyz/world/src/IWorldKernel.sol"; +import { SystemCall } from "@latticexyz/world/src/SystemCall.sol"; +import { Systems } from "@latticexyz/world/src/codegen/tables/Systems.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; + +type CrosschainSystemType is bytes32; + +// equivalent to WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "", name: "CrosschainSystem" })) +CrosschainSystemType constant crosschainSystem = CrosschainSystemType.wrap( + 0x7379000000000000000000000000000043726f7373636861696e53797374656d +); + +struct CallWrapper { + ResourceId systemId; + address from; +} + +struct RootCallWrapper { + ResourceId systemId; + address from; +} + +/** + * @title CrosschainSystemLib + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This library is automatically generated from the corresponding system contract. Do not edit manually. + */ +library CrosschainSystemLib { + error CrosschainSystemLib_CallingFromRootSystem(); + error WrongWorld(); + error NotCrosschainRecord(); + error MoreRecentRecordExists(); + error RecordNotOwned(); + error RecordAlreadyExists(); + error RecordBridgedToADifferentChain(); + error InvalidRecordTimestamp(); + + function create(CrosschainSystemType self, ResourceId tableId, bytes32[] memory keyTuple) internal { + return CallWrapper(self.toResourceId(), address(0)).create(tableId, keyTuple); + } + + function remove(CrosschainSystemType self, ResourceId tableId, bytes32[] memory keyTuple) internal { + return CallWrapper(self.toResourceId(), address(0)).remove(tableId, keyTuple); + } + + function bridge( + CrosschainSystemType self, + ResourceId tableId, + bytes32[] memory keyTuple, + uint256 targetChain + ) internal { + return CallWrapper(self.toResourceId(), address(0)).bridge(tableId, keyTuple, targetChain); + } + + function crosschainRead(CrosschainSystemType self, ResourceId tableId, bytes32[] calldata keyTuple) internal { + return CallWrapper(self.toResourceId(), address(0)).crosschainRead(tableId, keyTuple); + } + + function crosschainWrite( + CrosschainSystemType self, + Identifier calldata identifier, + bytes calldata _crosschainRead + ) internal { + return CallWrapper(self.toResourceId(), address(0)).crosschainWrite(identifier, _crosschainRead); + } + + function create(CallWrapper memory self, ResourceId tableId, bytes32[] memory keyTuple) internal { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert CrosschainSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(CrosschainSystem.create, (tableId, keyTuple)); + self.from == address(0) + ? _world().call(self.systemId, systemCall) + : _world().callFrom(self.from, self.systemId, systemCall); + } + + function remove(CallWrapper memory self, ResourceId tableId, bytes32[] memory keyTuple) internal { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert CrosschainSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(CrosschainSystem.remove, (tableId, keyTuple)); + self.from == address(0) + ? _world().call(self.systemId, systemCall) + : _world().callFrom(self.from, self.systemId, systemCall); + } + + function bridge( + CallWrapper memory self, + ResourceId tableId, + bytes32[] memory keyTuple, + uint256 targetChain + ) internal { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert CrosschainSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(CrosschainSystem.bridge, (tableId, keyTuple, targetChain)); + self.from == address(0) + ? _world().call(self.systemId, systemCall) + : _world().callFrom(self.from, self.systemId, systemCall); + } + + function crosschainRead(CallWrapper memory self, ResourceId tableId, bytes32[] calldata keyTuple) internal { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert CrosschainSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(CrosschainSystem.crosschainRead, (tableId, keyTuple)); + self.from == address(0) + ? _world().call(self.systemId, systemCall) + : _world().callFrom(self.from, self.systemId, systemCall); + } + + function crosschainWrite( + CallWrapper memory self, + Identifier calldata identifier, + bytes calldata _crosschainRead + ) internal { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert CrosschainSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(CrosschainSystem.crosschainWrite, (identifier, _crosschainRead)); + self.from == address(0) + ? _world().call(self.systemId, systemCall) + : _world().callFrom(self.from, self.systemId, systemCall); + } + + function create(RootCallWrapper memory self, ResourceId tableId, bytes32[] memory keyTuple) internal { + bytes memory systemCall = abi.encodeCall(CrosschainSystem.create, (tableId, keyTuple)); + SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value); + } + + function remove(RootCallWrapper memory self, ResourceId tableId, bytes32[] memory keyTuple) internal { + bytes memory systemCall = abi.encodeCall(CrosschainSystem.remove, (tableId, keyTuple)); + SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value); + } + + function bridge( + RootCallWrapper memory self, + ResourceId tableId, + bytes32[] memory keyTuple, + uint256 targetChain + ) internal { + bytes memory systemCall = abi.encodeCall(CrosschainSystem.bridge, (tableId, keyTuple, targetChain)); + SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value); + } + + function crosschainRead(RootCallWrapper memory self, ResourceId tableId, bytes32[] calldata keyTuple) internal { + bytes memory systemCall = abi.encodeCall(CrosschainSystem.crosschainRead, (tableId, keyTuple)); + SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value); + } + + function crosschainWrite( + RootCallWrapper memory self, + Identifier calldata identifier, + bytes calldata _crosschainRead + ) internal { + bytes memory systemCall = abi.encodeCall(CrosschainSystem.crosschainWrite, (identifier, _crosschainRead)); + SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value); + } + + function callFrom(CrosschainSystemType self, address from) internal pure returns (CallWrapper memory) { + return CallWrapper(self.toResourceId(), from); + } + + function callAsRoot(CrosschainSystemType self) internal view returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), msg.sender); + } + + function callAsRootFrom(CrosschainSystemType self, address from) internal pure returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), from); + } + + function toResourceId(CrosschainSystemType self) internal pure returns (ResourceId) { + return ResourceId.wrap(CrosschainSystemType.unwrap(self)); + } + + function fromResourceId(ResourceId resourceId) internal pure returns (CrosschainSystemType) { + return CrosschainSystemType.wrap(resourceId.unwrap()); + } + + function getAddress(CrosschainSystemType self) internal view returns (address) { + return Systems.getSystem(self.toResourceId()); + } + + function _world() private view returns (IWorldCall) { + return IWorldCall(StoreSwitch.getStoreAddress()); + } +} + +using CrosschainSystemLib for CrosschainSystemType global; +using CrosschainSystemLib for CallWrapper global; +using CrosschainSystemLib for RootCallWrapper global; diff --git a/packages/world-module-crosschain/ts/build.ts b/packages/world-module-crosschain/ts/build.ts new file mode 100644 index 0000000000..9f24536f54 --- /dev/null +++ b/packages/world-module-crosschain/ts/build.ts @@ -0,0 +1,18 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { tablegen } from "@latticexyz/store/codegen"; +import { worldgen } from "@latticexyz/world/node"; + +/** + * To avoid circular dependencies, we run a very similar `build` step as `cli` package here. + */ + +// TODO: move tablegen/worldgen to CLI commands from store/world we can run in package.json instead of a custom script +// (https://github.com/latticexyz/mud/issues/3030) + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const configPath = "../mud.config"; + +const { default: config } = await import(configPath); +const rootDir = path.dirname(path.join(__dirname, configPath)); +await Promise.all([tablegen({ rootDir, config }), worldgen({ rootDir, config })]); diff --git a/packages/world-module-crosschain/tsconfig.json b/packages/world-module-crosschain/tsconfig.json new file mode 100644 index 0000000000..9b0bf57752 --- /dev/null +++ b/packages/world-module-crosschain/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["mud.config.ts", "ts"] +} diff --git a/packages/world-module-crosschain/tsup.config.ts b/packages/world-module-crosschain/tsup.config.ts new file mode 100644 index 0000000000..1636dcc6c3 --- /dev/null +++ b/packages/world-module-crosschain/tsup.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "tsup"; + +export default defineConfig((opts) => ({ + entry: { + "mud.config": "mud.config.ts", + }, + target: "esnext", + format: ["esm"], + sourcemap: true, + minify: true, + // don't generate DTS during watch mode because it's slow + // we're likely using TS source in this mode anyway + dts: !opts.watch, + // don't clean during watch mode to avoid removing + // previously-built DTS files, which other build tasks + // depend on + clean: !opts.watch, +})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5203926f7..f79c1aec2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -501,6 +501,9 @@ importers: '@monaco-editor/react': specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.52.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@next/env': + specifier: ^15.1.0 + version: 15.1.0 '@radix-ui/react-checkbox': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.2.7)(@types/react@18.2.22)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -558,6 +561,9 @@ importers: next: specifier: 14.2.5 version: 14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.8.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + next-runtime-env: + specifier: ^3.2.2 + version: 3.2.2(next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.8.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0) node-sql-parser: specifier: ^5.3.3 version: 5.3.3 @@ -836,10 +842,10 @@ importers: version: 8.3.4 jest: specifier: ^29.3.1 - version: 29.5.0(@types/node@18.19.50) + 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.19.50))(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.47)(typescript@5.4.2) @@ -1252,10 +1258,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.19.50) 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.19.50))(typescript@5.4.2) tsup: specifier: ^6.7.0 version: 6.7.0(postcss@8.4.47)(typescript@5.4.2) @@ -1333,6 +1339,46 @@ importers: specifier: 0.34.6 version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.33.0) + packages/world-module-crosschain: + dependencies: + '@latticexyz/schema-type': + specifier: workspace:* + version: link:../schema-type + '@latticexyz/store': + specifier: workspace:* + version: link:../store + '@latticexyz/world': + specifier: workspace:* + version: link:../world + optimism: + specifier: https://github.com/ethereum-optimism/optimism#ecdb788a18022d4b78d6288c1f6ec94c152d02f8 + version: optimism#ecdb788a18022d4b78d6288c1f6ec94c152d02f8@https://codeload.github.com/ethereum-optimism/optimism/tar.gz/ecdb788a18022d4b78d6288c1f6ec94c152d02f8 + devDependencies: + '@latticexyz/abi-ts': + specifier: workspace:* + version: link:../abi-ts + '@latticexyz/gas-report': + specifier: workspace:* + version: link:../gas-report + '@types/node': + specifier: ^18.15.11 + version: 18.19.50 + ds-test: + specifier: https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0 + version: https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0 + forge-std: + specifier: https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1 + version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/74cfb77e308dd188d2f58864aaf44963ae6b88b1 + solhint: + specifier: ^3.3.7 + version: 3.3.7 + tsup: + specifier: ^6.7.0 + version: 6.7.0(postcss@8.4.47)(typescript@5.4.2) + vitest: + specifier: 0.34.6 + version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.33.0) + packages/world-module-erc20: dependencies: '@latticexyz/schema-type': @@ -3676,6 +3722,9 @@ packages: '@next/env@14.2.5': resolution: {integrity: sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==} + '@next/env@15.1.0': + resolution: {integrity: sha512-UcCO481cROsqJuszPPXJnb7GGuLq617ve4xuAyyNG4VSSocJNtMU5Fsx+Lp6mlN8c7W58aZLc5y6D/2xNmaK+w==} + '@next/eslint-plugin-next@14.2.3': resolution: {integrity: sha512-L3oDricIIjgj1AVnRdRor21gI7mShlSwU/1ZGHmqM3LzHhXXhdkrfeNY5zif25Bi5Dd7fiJHsbhoZCHfXYvlAw==} @@ -6895,6 +6944,7 @@ packages: eciesjs@0.3.20: resolution: {integrity: sha512-Rz5AB8v9+xmMdS/R7RzWPe/R8DP5QfyrkA6ce4umJopoB5su2H2aDy/GcgIfwhmCwxnBkqGf/PbGzmKcGtIgGA==} + deprecated: Please upgrade to v0.4+ ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -9048,6 +9098,12 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + next-runtime-env@3.2.2: + resolution: {integrity: sha512-S5S6NxIf3XeaVc9fLBN2L5Jzu+6dLYCXeOaPQa1RzKRYlG2BBayxXOj6A4VsciocyNkJMazW1VAibtbb1/ZjAw==} + peerDependencies: + next: ^14 + react: ^18 + next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -9314,6 +9370,10 @@ packages: openurl@1.1.1: resolution: {integrity: sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==} + optimism#ecdb788a18022d4b78d6288c1f6ec94c152d02f8@https://codeload.github.com/ethereum-optimism/optimism/tar.gz/ecdb788a18022d4b78d6288c1f6ec94c152d02f8: + resolution: {tarball: https://codeload.github.com/ethereum-optimism/optimism/tar.gz/ecdb788a18022d4b78d6288c1f6ec94c152d02f8} + version: 0.0.0 + optionator@0.8.3: resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} engines: {node: '>= 0.8.0'} @@ -14526,6 +14586,8 @@ snapshots: '@next/env@14.2.5': {} + '@next/env@15.1.0': {} + '@next/eslint-plugin-next@14.2.3': dependencies: glob: 10.3.10 @@ -18907,7 +18969,7 @@ snapshots: debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 @@ -18919,7 +18981,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -18940,7 +19002,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3 @@ -21526,6 +21588,11 @@ snapshots: neo-async@2.6.2: {} + next-runtime-env@3.2.2(next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.8.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0): + dependencies: + next: 14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.8.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + next-tick@1.1.0: {} next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.8.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): @@ -21796,6 +21863,8 @@ snapshots: openurl@1.1.1: {} + optimism#ecdb788a18022d4b78d6288c1f6ec94c152d02f8@https://codeload.github.com/ethereum-optimism/optimism/tar.gz/ecdb788a18022d4b78d6288c1f6ec94c152d02f8: {} + optionator@0.8.3: dependencies: deep-is: 0.1.4