From 939916bcd5c9f3caf0399e9ab7689e77e6bef7ad Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Mon, 14 Aug 2023 06:33:01 -0700 Subject: [PATCH] feat(dev-tools): use new sync stack (#1284) --- .changeset/large-sloths-camp.md | 16 +++ .changeset/soft-dryers-invite.md | 33 ++++++ .changeset/twenty-birds-scream.md | 22 ++++ .../client-vanilla/src/mud/setupNetwork.ts | 4 +- .../packages/client-phaser/package.json | 1 + .../src/layers/network/createNetworkLayer.ts | 8 +- .../client-phaser/src/mud/setupNetwork.ts | 12 +- .../packages/client-phaser/src/ui/App.tsx | 22 +++- .../packages/client-react/src/index.tsx | 21 +++- .../client-react/src/mud/setupNetwork.ts | 24 ++-- .../packages/client-vanilla/src/index.ts | 24 +++- .../client-vanilla/src/mud/setupNetwork.ts | 12 +- examples/minimal/pnpm-lock.yaml | 3 + packages/common/src/createContract.ts | 108 ++++++++++++------ packages/dev-tools/README.md | 19 ++- packages/dev-tools/package.json | 15 ++- packages/dev-tools/src/DevToolsContext.tsx | 56 +++++++++ packages/dev-tools/src/RootPage.tsx | 20 ++-- .../dev-tools/src/actions/ActionsPage.tsx | 13 ++- ...ransactionSummary.tsx => WriteSummary.tsx} | 71 ++++++------ .../dev-tools/src/actions/getTransaction.ts | 20 ++-- .../src/actions/getTransactionReceipt.ts | 52 ++------- .../src/actions/getTransactionResult.ts | 27 ++--- packages/dev-tools/src/common.ts | 18 +++ packages/dev-tools/src/events/EventIcon.tsx | 8 +- packages/dev-tools/src/events/EventsPage.tsx | 10 +- ...tsTable.tsx => StorageOperationsTable.tsx} | 26 +++-- packages/dev-tools/src/mount.tsx | 12 +- packages/dev-tools/src/recs/ComponentData.tsx | 21 ++++ .../dev-tools/src/recs/ComponentDataTable.tsx | 56 +++++++++ .../ComponentsPage.tsx} | 45 ++++---- .../serializeWithoutIndexedValues.ts | 0 packages/dev-tools/src/router.tsx | 10 +- .../dev-tools/src/summary/AccountSummary.tsx | 7 +- .../dev-tools/src/summary/ActionsSummary.tsx | 12 +- .../src/summary/ComponentsSummary.tsx | 32 ++++++ .../dev-tools/src/summary/EventsSummary.tsx | 8 +- .../dev-tools/src/summary/NetworkSummary.tsx | 14 +-- .../dev-tools/src/summary/SummaryPage.tsx | 14 ++- .../dev-tools/src/summary/TablesSummary.tsx | 27 ----- packages/dev-tools/src/tables/Table.tsx | 61 ---------- packages/dev-tools/src/tables/useTables.ts | 18 --- packages/dev-tools/src/useStore.ts | 86 -------------- packages/react/src/index.ts | 1 + .../{dev-tools => react}/src/usePromise.ts | 0 packages/store-sync/src/recs/common.ts | 2 + .../src/recs/defineInternalComponents.ts | 20 +++- packages/store-sync/src/recs/index.ts | 1 + .../store-sync/src/recs/isStoreComponent.ts | 13 +++ pnpm-lock.yaml | 13 ++- templates/phaser/packages/client/package.json | 1 + .../phaser/packages/client/src/index.tsx | 2 - .../src/layers/network/createNetworkLayer.ts | 8 +- .../packages/client/src/mud/setupNetwork.ts | 12 +- .../phaser/packages/client/src/ui/App.tsx | 23 +++- templates/react/packages/client/package.json | 1 + templates/react/packages/client/src/index.tsx | 21 +++- .../packages/client/src/mud/setupNetwork.ts | 12 +- .../threejs/packages/client/package.json | 1 + .../threejs/packages/client/src/index.tsx | 21 +++- .../packages/client/src/mud/setupNetwork.ts | 12 +- .../vanilla/packages/client/package.json | 1 + .../vanilla/packages/client/src/index.ts | 19 ++- .../packages/client/src/mud/setupNetwork.ts | 12 +- 64 files changed, 775 insertions(+), 509 deletions(-) create mode 100644 .changeset/large-sloths-camp.md create mode 100644 .changeset/soft-dryers-invite.md create mode 100644 .changeset/twenty-birds-scream.md create mode 100644 packages/dev-tools/src/DevToolsContext.tsx rename packages/dev-tools/src/actions/{TransactionSummary.tsx => WriteSummary.tsx} (76%) create mode 100644 packages/dev-tools/src/common.ts rename packages/dev-tools/src/events/{EventsTable.tsx => StorageOperationsTable.tsx} (59%) create mode 100644 packages/dev-tools/src/recs/ComponentData.tsx create mode 100644 packages/dev-tools/src/recs/ComponentDataTable.tsx rename packages/dev-tools/src/{tables/TablesPage.tsx => recs/ComponentsPage.tsx} (68%) rename packages/dev-tools/src/{tables => recs}/serializeWithoutIndexedValues.ts (100%) create mode 100644 packages/dev-tools/src/summary/ComponentsSummary.tsx delete mode 100644 packages/dev-tools/src/summary/TablesSummary.tsx delete mode 100644 packages/dev-tools/src/tables/Table.tsx delete mode 100644 packages/dev-tools/src/tables/useTables.ts delete mode 100644 packages/dev-tools/src/useStore.ts rename packages/{dev-tools => react}/src/usePromise.ts (100%) create mode 100644 packages/store-sync/src/recs/isStoreComponent.ts diff --git a/.changeset/large-sloths-camp.md b/.changeset/large-sloths-camp.md new file mode 100644 index 0000000000..7113467253 --- /dev/null +++ b/.changeset/large-sloths-camp.md @@ -0,0 +1,16 @@ +--- +"@latticexyz/common": minor +--- + +`createContract` now has an `onWrite` callback so you can observe writes. This is useful for wiring up the transanction log in MUD dev tools. + +```ts +import { createContract, ContractWrite } from "@latticexyz/common"; +import { Subject } from "rxjs"; + +const write$ = new Subject(); +creactContract({ + ... + onWrite: (write) => write$.next(write), +}); +``` diff --git a/.changeset/soft-dryers-invite.md b/.changeset/soft-dryers-invite.md new file mode 100644 index 0000000000..34fcc37c14 --- /dev/null +++ b/.changeset/soft-dryers-invite.md @@ -0,0 +1,33 @@ +--- +"@latticexyz/dev-tools": major +"create-mud": major +--- + +MUD dev tools is updated to latest sync stack. You must now pass in all of its data requirements rather than relying on magic globals. + +```diff +import { mount as mountDevTools } from "@latticexyz/dev-tools"; + +- mountDevTools(); ++ mountDevTools({ ++ config, ++ publicClient, ++ walletClient, ++ latestBlock$, ++ blockStorageOperations$, ++ worldAddress, ++ worldAbi, ++ write$, ++ // if you're using recs ++ recsWorld, ++ }); +``` + +It's also advised to wrap dev tools so that it is only mounted during development mode. Here's how you do this with Vite: + +```ts +// https://vitejs.dev/guide/env-and-mode.html +if (import.meta.env.DEV) { + mountDevTools({ ... }); +} +``` diff --git a/.changeset/twenty-birds-scream.md b/.changeset/twenty-birds-scream.md new file mode 100644 index 0000000000..60f766164e --- /dev/null +++ b/.changeset/twenty-birds-scream.md @@ -0,0 +1,22 @@ +--- +"@latticexyz/react": minor +--- + +Adds a `usePromise` hook that returns a [native `PromiseSettledResult` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled). + +```tsx +const promise = fetch(url); +const result = usePromise(promise); + +if (result.status === "idle" || result.status === "pending") { + return <>fetching; +} + +if (result.status === "rejected") { + return <>error fetching: {String(result.reason)}; +} + +if (result.status === "fulfilled") { + return <>fetch status: {result.value.status}; +} +``` diff --git a/e2e/packages/client-vanilla/src/mud/setupNetwork.ts b/e2e/packages/client-vanilla/src/mud/setupNetwork.ts index 85708917a0..4a615e6381 100644 --- a/e2e/packages/client-vanilla/src/mud/setupNetwork.ts +++ b/e2e/packages/client-vanilla/src/mud/setupNetwork.ts @@ -4,8 +4,8 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs"; import { getNetworkConfig } from "./getNetworkConfig"; import { world } from "./world"; import { IWorld__factory } from "contracts/types/ethers-contracts/factories/IWorld__factory"; -import storeConfig from "contracts/mud.config"; import { createBurnerAccount, createContract, transportObserver } from "@latticexyz/common"; +import mudConfig from "contracts/mud.config"; export type SetupNetworkResult = Awaited>; @@ -37,7 +37,7 @@ export async function setupNetwork() { const { components, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({ world, - config: storeConfig, + config: mudConfig, address: networkConfig.worldAddress as Hex, publicClient, startBlock: BigInt(networkConfig.initialBlockNumber), diff --git a/examples/minimal/packages/client-phaser/package.json b/examples/minimal/packages/client-phaser/package.json index 61c2199ec2..a8d9dee5ee 100644 --- a/examples/minimal/packages/client-phaser/package.json +++ b/examples/minimal/packages/client-phaser/package.json @@ -13,6 +13,7 @@ "@ethersproject/providers": "^5.7.2", "@improbable-eng/grpc-web": "^0.15.0", "@latticexyz/common": "link:../../../../packages/common", + "@latticexyz/dev-tools": "link:../../../../packages/dev-tools", "@latticexyz/network": "link:../../../../packages/network", "@latticexyz/phaserx": "link:../../../../packages/phaserx", "@latticexyz/react": "link:../../../../packages/react", diff --git a/examples/minimal/packages/client-phaser/src/layers/network/createNetworkLayer.ts b/examples/minimal/packages/client-phaser/src/layers/network/createNetworkLayer.ts index dcedc378ef..915108240f 100644 --- a/examples/minimal/packages/client-phaser/src/layers/network/createNetworkLayer.ts +++ b/examples/minimal/packages/client-phaser/src/layers/network/createNetworkLayer.ts @@ -4,16 +4,12 @@ import { setup } from "../../mud/setup"; export type NetworkLayer = Awaited>; export const createNetworkLayer = async () => { - const { components, systemCalls } = await setup(); - - // Give components a Human-readable ID - Object.entries(components).forEach(([name, component]) => { - component.id = name; - }); + const { components, systemCalls, network } = await setup(); return { world, components, systemCalls, + network, }; }; diff --git a/examples/minimal/packages/client-phaser/src/mud/setupNetwork.ts b/examples/minimal/packages/client-phaser/src/mud/setupNetwork.ts index 6882d73cb1..d42c811ec9 100644 --- a/examples/minimal/packages/client-phaser/src/mud/setupNetwork.ts +++ b/examples/minimal/packages/client-phaser/src/mud/setupNetwork.ts @@ -4,8 +4,9 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs"; import { getNetworkConfig } from "./getNetworkConfig"; import { world } from "./world"; import { IWorld__factory } from "contracts/types/ethers-contracts/factories/IWorld__factory"; -import storeConfig from "contracts/mud.config"; -import { createBurnerAccount, createContract, transportObserver } from "@latticexyz/common"; +import { createBurnerAccount, createContract, transportObserver, ContractWrite } from "@latticexyz/common"; +import { Subject, share } from "rxjs"; +import mudConfig from "contracts/mud.config"; export type SetupNetworkResult = Awaited>; @@ -26,16 +27,18 @@ export async function setupNetwork() { account: burnerAccount, }); + const write$ = new Subject(); const worldContract = createContract({ address: networkConfig.worldAddress as Hex, abi: IWorld__factory.abi, publicClient, walletClient: burnerWalletClient, + onWrite: (write) => write$.next(write), }); const { components, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({ world, - config: storeConfig, + config: mudConfig, address: networkConfig.worldAddress as Hex, publicClient, startBlock: BigInt(networkConfig.initialBlockNumber), @@ -71,9 +74,10 @@ export async function setupNetwork() { playerEntity: encodeEntity({ address: "address" }, { address: burnerWalletClient.account.address }), publicClient, walletClient: burnerWalletClient, - worldContract, latestBlock$, blockStorageOperations$, waitForTransaction, + worldContract, + write$: write$.asObservable().pipe(share()), }; } diff --git a/examples/minimal/packages/client-phaser/src/ui/App.tsx b/examples/minimal/packages/client-phaser/src/ui/App.tsx index 627503738b..314e5572d4 100644 --- a/examples/minimal/packages/client-phaser/src/ui/App.tsx +++ b/examples/minimal/packages/client-phaser/src/ui/App.tsx @@ -3,13 +3,31 @@ import { useNetworkLayer } from "./hooks/useNetworkLayer"; import { useStore } from "../store"; import { PhaserLayer } from "./PhaserLayer"; import { UIRoot } from "./UIRoot"; +import mudConfig from "contracts/mud.config"; export const App = () => { const networkLayer = useNetworkLayer(); useEffect(() => { - if (networkLayer) { - useStore.setState({ networkLayer }); + if (!networkLayer) return; + + useStore.setState({ networkLayer }); + + // https://vitejs.dev/guide/env-and-mode.html + if (import.meta.env.DEV) { + import("@latticexyz/dev-tools").then(({ mount: mountDevTools }) => + mountDevTools({ + config: mudConfig, + publicClient: networkLayer.network.publicClient, + walletClient: networkLayer.network.walletClient, + latestBlock$: networkLayer.network.latestBlock$, + blockStorageOperations$: networkLayer.network.blockStorageOperations$, + worldAddress: networkLayer.network.worldContract.address, + worldAbi: networkLayer.network.worldContract.abi, + write$: networkLayer.network.write$, + recsWorld: networkLayer.world, + }) + ); } }, [networkLayer]); diff --git a/examples/minimal/packages/client-react/src/index.tsx b/examples/minimal/packages/client-react/src/index.tsx index 2b0e94eecd..9b3f9121e1 100644 --- a/examples/minimal/packages/client-react/src/index.tsx +++ b/examples/minimal/packages/client-react/src/index.tsx @@ -2,18 +2,33 @@ import ReactDOM from "react-dom/client"; import { App } from "./App"; import { setup } from "./mud/setup"; import { MUDProvider } from "./MUDContext"; -import { mount as mountDevTools } from "@latticexyz/dev-tools"; +import mudConfig from "contracts/mud.config"; const rootElement = document.getElementById("react-root"); if (!rootElement) throw new Error("React root not found"); const root = ReactDOM.createRoot(rootElement); // TODO: figure out if we actually want this to be async or if we should render something else in the meantime -setup().then((result) => { +setup().then(async (result) => { root.render( ); - mountDevTools(); + + // https://vitejs.dev/guide/env-and-mode.html + if (import.meta.env.DEV) { + const { mount: mountDevTools } = await import("@latticexyz/dev-tools"); + mountDevTools({ + config: mudConfig, + publicClient: result.network.publicClient, + walletClient: result.network.walletClient, + latestBlock$: result.network.latestBlock$, + blockStorageOperations$: result.network.blockStorageOperations$, + worldAddress: result.network.worldContract.address, + worldAbi: result.network.worldContract.abi, + write$: result.network.write$, + recsWorld: result.network.world, + }); + } }); diff --git a/examples/minimal/packages/client-react/src/mud/setupNetwork.ts b/examples/minimal/packages/client-react/src/mud/setupNetwork.ts index 54b0103087..3ecb98b260 100644 --- a/examples/minimal/packages/client-react/src/mud/setupNetwork.ts +++ b/examples/minimal/packages/client-react/src/mud/setupNetwork.ts @@ -4,8 +4,9 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs"; import { getNetworkConfig } from "./getNetworkConfig"; import { world } from "./world"; import { IWorld__factory } from "contracts/types/ethers-contracts/factories/IWorld__factory"; -import storeConfig from "contracts/mud.config"; -import { createBurnerAccount, createContract, transportObserver } from "@latticexyz/common"; +import { ContractWrite, createBurnerAccount, createContract, transportObserver } from "@latticexyz/common"; +import { Subject, share } from "rxjs"; +import mudConfig from "contracts/mud.config"; export type SetupNetworkResult = Awaited>; @@ -26,9 +27,18 @@ export async function setupNetwork() { account: burnerAccount, }); + const write$ = new Subject(); + const worldContract = createContract({ + address: networkConfig.worldAddress as Hex, + abi: IWorld__factory.abi, + publicClient, + walletClient: burnerWalletClient, + onWrite: (write) => write$.next(write), + }); + const { components, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({ world, - config: storeConfig, + config: mudConfig, address: networkConfig.worldAddress as Hex, publicClient, startBlock: BigInt(networkConfig.initialBlockNumber), @@ -67,11 +77,7 @@ export async function setupNetwork() { latestBlock$, blockStorageOperations$, waitForTransaction, - worldContract: createContract({ - address: networkConfig.worldAddress as Hex, - abi: IWorld__factory.abi, - publicClient, - walletClient: burnerWalletClient, - }), + worldContract, + write$: write$.asObservable().pipe(share()), }; } diff --git a/examples/minimal/packages/client-vanilla/src/index.ts b/examples/minimal/packages/client-vanilla/src/index.ts index a6f7b63509..fe63aa1950 100644 --- a/examples/minimal/packages/client-vanilla/src/index.ts +++ b/examples/minimal/packages/client-vanilla/src/index.ts @@ -1,10 +1,8 @@ import { setup } from "./mud/setup"; -import { mount as mountDevTools } from "@latticexyz/dev-tools"; +import mudConfig from "contracts/mud.config"; -const { - components, - network: { worldContract, waitForTransaction }, -} = await setup(); +const { components, network } = await setup(); +const { worldContract, waitForTransaction } = network; // Components expose a stream that triggers when the component is updated. components.CounterTable.update$.subscribe((update) => { @@ -56,4 +54,18 @@ document.getElementById("chat-form")?.addEventListener("submit", (e) => { (window as any).sendMessage(); }); -mountDevTools(); +// https://vitejs.dev/guide/env-and-mode.html +if (import.meta.env.DEV) { + const { mount: mountDevTools } = await import("@latticexyz/dev-tools"); + mountDevTools({ + config: mudConfig, + publicClient: network.publicClient, + walletClient: network.walletClient, + latestBlock$: network.latestBlock$, + blockStorageOperations$: network.blockStorageOperations$, + worldAddress: network.worldContract.address, + worldAbi: network.worldContract.abi, + write$: network.write$, + recsWorld: network.world, + }); +} diff --git a/examples/minimal/packages/client-vanilla/src/mud/setupNetwork.ts b/examples/minimal/packages/client-vanilla/src/mud/setupNetwork.ts index 6882d73cb1..d42c811ec9 100644 --- a/examples/minimal/packages/client-vanilla/src/mud/setupNetwork.ts +++ b/examples/minimal/packages/client-vanilla/src/mud/setupNetwork.ts @@ -4,8 +4,9 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs"; import { getNetworkConfig } from "./getNetworkConfig"; import { world } from "./world"; import { IWorld__factory } from "contracts/types/ethers-contracts/factories/IWorld__factory"; -import storeConfig from "contracts/mud.config"; -import { createBurnerAccount, createContract, transportObserver } from "@latticexyz/common"; +import { createBurnerAccount, createContract, transportObserver, ContractWrite } from "@latticexyz/common"; +import { Subject, share } from "rxjs"; +import mudConfig from "contracts/mud.config"; export type SetupNetworkResult = Awaited>; @@ -26,16 +27,18 @@ export async function setupNetwork() { account: burnerAccount, }); + const write$ = new Subject(); const worldContract = createContract({ address: networkConfig.worldAddress as Hex, abi: IWorld__factory.abi, publicClient, walletClient: burnerWalletClient, + onWrite: (write) => write$.next(write), }); const { components, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({ world, - config: storeConfig, + config: mudConfig, address: networkConfig.worldAddress as Hex, publicClient, startBlock: BigInt(networkConfig.initialBlockNumber), @@ -71,9 +74,10 @@ export async function setupNetwork() { playerEntity: encodeEntity({ address: "address" }, { address: burnerWalletClient.account.address }), publicClient, walletClient: burnerWalletClient, - worldContract, latestBlock$, blockStorageOperations$, waitForTransaction, + worldContract, + write$: write$.asObservable().pipe(share()), }; } diff --git a/examples/minimal/pnpm-lock.yaml b/examples/minimal/pnpm-lock.yaml index 2cf38b9362..bd8a0af437 100644 --- a/examples/minimal/pnpm-lock.yaml +++ b/examples/minimal/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@latticexyz/common': specifier: link:../../../../packages/common version: link:../../../../packages/common + '@latticexyz/dev-tools': + specifier: link:../../../../packages/dev-tools + version: link:../../../../packages/dev-tools '@latticexyz/network': specifier: link:../../../../packages/network version: link:../../../../packages/network diff --git a/packages/common/src/createContract.ts b/packages/common/src/createContract.ts index 55be0305be..1a72a204e6 100644 --- a/packages/common/src/createContract.ts +++ b/packages/common/src/createContract.ts @@ -5,6 +5,7 @@ import { Chain, GetContractParameters, GetContractReturnType, + Hex, PublicClient, SimulateContractParameters, Transport, @@ -31,6 +32,24 @@ function getFunctionParameters(values: [args?: readonly unknown[], options?: obj return { args, options }; } +export type ContractWrite = { + id: string; + request: WriteContractParameters; + result: Promise; +}; + +export type CreateContractOptions< + TTransport extends Transport, + TAddress extends Address, + TAbi extends Abi, + TChain extends Chain, + TAccount extends Account, + TPublicClient extends PublicClient, + TWalletClient extends WalletClient +> = Required> & { + onWrite?: (write: ContractWrite) => void; +}; + export function createContract< TTransport extends Transport, TAddress extends Address, @@ -44,8 +63,15 @@ export function createContract< address, publicClient, walletClient, -}: Required< - GetContractParameters + onWrite, +}: CreateContractOptions< + TTransport, + TAddress, + TAbi, + TChain, + TAccount, + TPublicClient, + TWalletClient >): GetContractReturnType { const contract = getContract({ abi, @@ -55,6 +81,7 @@ export function createContract< }) as unknown as GetContractReturnType; if (contract.write) { + let nextWriteId = 0; const nonceManager = createNonceManager({ publicClient: publicClient as PublicClient, address: walletClient.account.address, @@ -65,14 +92,24 @@ export function createContract< {}, { get(_, functionName: string): GetContractReturnType["write"][string] { - return async (...parameters) => { - const { args, options } = < - { - args: unknown[]; - options: UnionOmit; - } - >getFunctionParameters(parameters as any); + async function prepareWrite( + options: WriteContractParameters + ): Promise> { + if (options.gas) { + debug("gas provided, skipping simulate", functionName, options); + return options as unknown as WriteContractParameters; + } + + debug("simulating write", functionName, options); + const { request } = await publicClient.simulateContract({ + ...options, + account: options.account ?? walletClient.account, + } as unknown as SimulateContractParameters); + return request as unknown as WriteContractParameters; + } + + async function write(options: WriteContractParameters): Promise { // Temporarily override base fee for our default anvil config // TODO: replace with https://github.com/wagmi-dev/viem/pull/1006 once merged // TODO: more specific mud foundry check? or can we safely assume anvil+mud will be block fee zero for now? @@ -86,34 +123,7 @@ export function createContract< options.maxPriorityFeePerGas = 0n; } - async function prepareWrite(): Promise< - WriteContractParameters - > { - if (options.gas) { - debug("gas provided, skipping simulate", functionName, args, options); - return { - address, - abi, - functionName, - args, - ...options, - } as unknown as WriteContractParameters; - } - - debug("simulating write", functionName, args, options); - const { request } = await publicClient.simulateContract({ - address, - abi, - functionName, - args, - ...options, - account: options.account ?? walletClient.account, - } as unknown as SimulateContractParameters); - - return request as unknown as WriteContractParameters; - } - - const preparedWrite = await prepareWrite(); + const preparedWrite = await prepareWrite(options); return await pRetry( async () => { @@ -142,6 +152,30 @@ export function createContract< }, } ); + } + + return (...parameters) => { + const id = `${walletClient.chain.id}:${walletClient.account.address}:${nextWriteId++}`; + const { args, options } = < + { + args: unknown[]; + options: UnionOmit; + } + >getFunctionParameters(parameters as any); + + const request = { + address, + abi, + functionName, + args, + ...options, + }; + + const result = write(request); + + onWrite?.({ id, request, result }); + + return result; }; }, } diff --git a/packages/dev-tools/README.md b/packages/dev-tools/README.md index 1d419dde4f..cbab052e80 100644 --- a/packages/dev-tools/README.md +++ b/packages/dev-tools/README.md @@ -13,9 +13,20 @@ npm install @latticexyz/dev-tools ## Usage ```ts -import { mount as mountDevTools } from "@latticexyz/dev-tools"; - -if (process.env.NODE_ENV !== "production") { - mountDevTools(); +// https://vitejs.dev/guide/env-and-mode.html +if (import.meta.env.DEV) { + const { mount: mountDevTools } = await import("@latticexyz/dev-tools"); + mountDevTools({ + config, + publicClient, + walletClient, + latestBlock$, + blockStorageOperations$, + worldAddress, + worldAbi, + write$, + // if you're using recs + recsWorld, + }); } ``` diff --git a/packages/dev-tools/package.json b/packages/dev-tools/package.json index 9a7f1d5268..547ae9b4a0 100644 --- a/packages/dev-tools/package.json +++ b/packages/dev-tools/package.json @@ -23,9 +23,10 @@ }, "dependencies": { "@latticexyz/common": "workspace:*", - "@latticexyz/network": "workspace:*", "@latticexyz/react": "workspace:*", - "@latticexyz/std-client": "workspace:*", + "@latticexyz/recs": "workspace:*", + "@latticexyz/store": "workspace:*", + "@latticexyz/store-sync": "workspace:*", "@latticexyz/utils": "workspace:*", "@latticexyz/world": "workspace:*", "abitype": "0.9.3", @@ -49,10 +50,12 @@ "vitest": "0.31.4" }, "peerDependencies": { - "@latticexyz/network": "2.0.0-next.1", - "@latticexyz/std-client": "2.0.0-next.1", - "@latticexyz/utils": "2.0.0-next.1", - "@latticexyz/world": "2.0.0-next.1" + "@latticexyz/common": "*", + "@latticexyz/recs": "*", + "@latticexyz/store": "*", + "@latticexyz/store-sync": "*", + "@latticexyz/utils": "*", + "@latticexyz/world": "*" }, "publishConfig": { "access": "public" diff --git a/packages/dev-tools/src/DevToolsContext.tsx b/packages/dev-tools/src/DevToolsContext.tsx new file mode 100644 index 0000000000..8a9d359e1f --- /dev/null +++ b/packages/dev-tools/src/DevToolsContext.tsx @@ -0,0 +1,56 @@ +import { createContext, ReactNode, useContext, useEffect, useState } from "react"; +import { DevToolsOptions } from "./common"; +import { ContractWrite } from "@latticexyz/common"; +import { StorageOperation } from "@latticexyz/store-sync"; +import { StoreConfig } from "@latticexyz/store"; + +type DevToolsContextValue = DevToolsOptions & { + writes: ContractWrite[]; + storageOperations: StorageOperation[]; +}; + +const DevToolsContext = createContext(null); + +type Props = { + children: ReactNode; + value: DevToolsOptions; +}; + +export const DevToolsProvider = ({ children, value }: Props) => { + const currentValue = useContext(DevToolsContext); + if (currentValue) throw new Error("DevToolsProvider can only be used once"); + + const [writes, setWrites] = useState([]); + useEffect(() => { + const sub = value.write$.subscribe((write) => { + setWrites((val) => [...val, write]); + }); + return () => sub.unsubscribe(); + }, [value.write$]); + + const [storageOperations, setStorageOperations] = useState[]>([]); + useEffect(() => { + const sub = value.blockStorageOperations$.subscribe(({ operations }) => { + setStorageOperations((val) => [...val, ...operations]); + }); + return () => sub.unsubscribe(); + }, [value.blockStorageOperations$]); + + return ( + + {children} + + ); +}; + +export const useDevToolsContext = () => { + const value = useContext(DevToolsContext); + if (!value) throw new Error("Must be used within a DevToolsProvider"); + return value; +}; diff --git a/packages/dev-tools/src/RootPage.tsx b/packages/dev-tools/src/RootPage.tsx index 5213ae30fd..575596d9c8 100644 --- a/packages/dev-tools/src/RootPage.tsx +++ b/packages/dev-tools/src/RootPage.tsx @@ -1,8 +1,10 @@ import { twMerge } from "tailwind-merge"; import { Outlet } from "react-router-dom"; import { NavButton } from "./NavButton"; +import { useDevToolsContext } from "./DevToolsContext"; export function RootPage() { + const { recsWorld } = useDevToolsContext(); return ( <>
@@ -30,14 +32,16 @@ export function RootPage() { > Store log - - twMerge("py-1.5 px-3", isActive ? "bg-slate-800 text-white" : "hover:bg-blue-800 hover:text-white") - } - > - Store data - + {recsWorld ? ( + + twMerge("py-1.5 px-3", isActive ? "bg-slate-800 text-white" : "hover:bg-blue-800 hover:text-white") + } + > + Components + + ) : null}
diff --git a/packages/dev-tools/src/actions/ActionsPage.tsx b/packages/dev-tools/src/actions/ActionsPage.tsx index 836023e531..535783683d 100644 --- a/packages/dev-tools/src/actions/ActionsPage.tsx +++ b/packages/dev-tools/src/actions/ActionsPage.tsx @@ -1,9 +1,10 @@ import { useRef, useEffect } from "react"; -import { useStore } from "../useStore"; -import { TransactionSummary } from "./TransactionSummary"; +import { WriteSummary } from "./WriteSummary"; +import { useDevToolsContext } from "../DevToolsContext"; export function ActionsPage() { - const transactions = useStore((state) => state.transactions); + const { writes } = useDevToolsContext(); + const containerRef = useRef(null); const hoveredRef = useRef(false); const scrollBehaviorRef = useRef("auto"); @@ -13,7 +14,7 @@ export function ActionsPage() { containerRef.current?.scrollIntoView({ behavior: scrollBehaviorRef.current, block: "end" }); } scrollBehaviorRef.current = "smooth"; - }, [transactions]); + }, [writes]); return (
- {transactions.length ? ( - transactions.map((hash) => ) + {writes.length ? ( + writes.map((write) => ) ) : ( <>Waiting for transactions… )} diff --git a/packages/dev-tools/src/actions/TransactionSummary.tsx b/packages/dev-tools/src/actions/WriteSummary.tsx similarity index 76% rename from packages/dev-tools/src/actions/TransactionSummary.tsx rename to packages/dev-tools/src/actions/WriteSummary.tsx index 7bc5076872..2b185a9903 100644 --- a/packages/dev-tools/src/actions/TransactionSummary.tsx +++ b/packages/dev-tools/src/actions/WriteSummary.tsx @@ -1,47 +1,42 @@ -import { decodeEventLog, decodeFunctionData, Hex, AbiEventSignatureNotFoundError } from "viem"; +import { decodeEventLog, AbiEventSignatureNotFoundError } from "viem"; import { twMerge } from "tailwind-merge"; import { isDefined } from "@latticexyz/common/utils"; -import { TableId } from "@latticexyz/common/deprecated"; -import { keyTupleToEntityID } from "@latticexyz/network/dev"; -import { useStore } from "../useStore"; import { PendingIcon } from "../icons/PendingIcon"; -import { usePromise } from "../usePromise"; +import { usePromise } from "@latticexyz/react"; import { truncateHex } from "../truncateHex"; import { serialize } from "../serialize"; import { getTransaction } from "./getTransaction"; import { getTransactionReceipt } from "./getTransactionReceipt"; import { getTransactionResult } from "./getTransactionResult"; import { ErrorTrace } from "../ErrorTrace"; +import { ContractWrite, hexToTableId } from "@latticexyz/common"; +import { useDevToolsContext } from "../DevToolsContext"; +import { hexKeyTupleToEntity } from "@latticexyz/store-sync/recs"; type Props = { - hash: Hex; + write: ContractWrite; }; // TODO: show block number or relative timestamp (e.g. 3s ago) -export function TransactionSummary({ hash }: Props) { - const publicClient = useStore((state) => state.publicClient); - const worldAbi = useStore((state) => state.worldAbi); +export function WriteSummary({ write }: Props) { + const { publicClient, worldAbi } = useDevToolsContext(); + const blockExplorer = publicClient.chain.blockExplorers?.default.url; - if (!publicClient) { - throw new Error("Can't display transactions without a public client"); - } - - const transactionPromise = getTransaction(publicClient, hash); - const transactionReceiptPromise = getTransactionReceipt(publicClient, hash); - const transactionResultPromise = getTransactionResult(publicClient, hash); + const hash = usePromise(write.result); + const transactionPromise = getTransaction(publicClient, write); + const transactionReceiptPromise = getTransactionReceipt(publicClient, write); + const transactionResultPromise = getTransactionResult(publicClient, worldAbi, write); const transaction = usePromise(transactionPromise); const transactionReceipt = usePromise(transactionReceiptPromise); const transactionResult = usePromise(transactionResultPromise); - const isPending = transactionReceipt.status === "pending"; - const isRevert = transactionReceipt.status === "fulfilled" && transactionReceipt.value.status === "reverted"; + const isPending = hash.status === "pending" || transactionReceipt.status === "pending"; + const isRevert = + hash.status === "rejected" || + (transactionReceipt.status === "fulfilled" && transactionReceipt.value.status === "reverted"); // TODO: move all this into their getTransaction functions - const functionData = - worldAbi && transaction.status === "fulfilled" && transaction.value.input - ? decodeFunctionData({ abi: worldAbi, data: transaction.value.input }) - : null; const returnData = transactionResult.status === "fulfilled" ? transactionResult.value.result : null; const events = worldAbi && transactionReceipt.status === "fulfilled" @@ -61,8 +56,6 @@ export function TransactionSummary({ hash }: Props) { .filter(isDefined) : null; - const blockExplorer = publicClient?.chain.blockExplorers?.default.url; - return (
{ @@ -80,7 +73,7 @@ export function TransactionSummary({ hash }: Props) { )} >
- {functionData?.functionName}({functionData?.args?.map((value) => serialize(value)).join(", ")}) + {write.request.functionName}({write.request.args?.map((value) => serialize(value)).join(", ")})
{transactionReceipt.status === "fulfilled" ? ( ) : null} - - tx {truncateHex(hash)} - + {hash.status === "fulfilled" ? ( + + tx {truncateHex(hash.value)} + + ) : null}
{isPending ? : isRevert ? <>⚠ : <>✓}
@@ -136,7 +131,7 @@ export function TransactionSummary({ hash }: Props) { {events.map(({ eventName, args }, i) => { - const table = TableId.fromHex((args as any).table); + const table = hexToTableId((args as any).table); return ( @@ -148,7 +143,7 @@ export function TransactionSummary({ hash }: Props) { {eventName === "StoreDeleteRecord" ? - : null} - {keyTupleToEntityID((args as any).key)} + {hexKeyTupleToEntity((args as any).key)} {(args as any).data} diff --git a/packages/dev-tools/src/actions/getTransaction.ts b/packages/dev-tools/src/actions/getTransaction.ts index 78523b4c16..eba0191ee4 100644 --- a/packages/dev-tools/src/actions/getTransaction.ts +++ b/packages/dev-tools/src/actions/getTransaction.ts @@ -1,17 +1,19 @@ -import { Hex, Transaction, PublicClient, Chain, Transport } from "viem"; +import { ContractWrite } from "@latticexyz/common"; +import { Transaction, PublicClient, Chain, Transport } from "viem"; // TODO: something about this fails when doing lots of simultaneous requests for transactions // not sure if its viem or failed RPC requests or what, but the promises get stuck/never resolve // TODO: use IndexedDB cache for these? -type CacheKey = `${number}:${Hex}`; -const cache: Record> = {}; +const cache: Record> = {}; -export const getTransaction = (publicClient: PublicClient, hash: Hex) => { - const key: CacheKey = `${publicClient.chain.id}:${hash}`; - if (!cache[key]) { - cache[key] = publicClient.getTransaction({ hash }); +export function getTransaction( + publicClient: PublicClient, + write: ContractWrite +): Promise { + if (!cache[write.id]) { + cache[write.id] = write.result.then((hash) => publicClient.getTransaction({ hash })); } - return cache[key]; -}; + return cache[write.id]; +} diff --git a/packages/dev-tools/src/actions/getTransactionReceipt.ts b/packages/dev-tools/src/actions/getTransactionReceipt.ts index cc8708b6fa..531dcb06d3 100644 --- a/packages/dev-tools/src/actions/getTransactionReceipt.ts +++ b/packages/dev-tools/src/actions/getTransactionReceipt.ts @@ -1,46 +1,16 @@ -import { - Hex, - TransactionReceipt, - PublicClient, - Chain, - TransactionNotFoundError, - TransactionReceiptNotFoundError, - Transport, -} from "viem"; +import { ContractWrite } from "@latticexyz/common"; +import { TransactionReceipt, PublicClient, Chain, Transport } from "viem"; // TODO: use IndexedDB cache for these? -type CacheKey = `${number}:${Hex}`; -const cache: Record> = {}; +const cache: Record> = {}; -export const getTransactionReceipt = (publicClient: PublicClient, hash: Hex) => { - const key: CacheKey = `${publicClient.chain.id}:${hash}`; - - if (!cache[key]) { - // When kicking off multiple `waitForTransactionReceipt` calls at once, the latter promises never seem to resolve. - // Instead, we'll do a very naive version of that here that doesn't handle replacements. - // TODO: make a repro case for viem - cache[key] = new Promise((resolve, reject) => { - const unwatch = publicClient.watchBlockNumber({ - onBlockNumber: async (_blockNumber) => { - try { - const receipt = await publicClient.getTransactionReceipt({ hash }); - unwatch(); - resolve(receipt); - } catch (error) { - if (error instanceof TransactionNotFoundError || error instanceof TransactionReceiptNotFoundError) { - // allow it to retry on the next block - return; - } - unwatch(); - reject(error); - } - }, - }); - }); - // TODO: replace the above with this line once viem is fixed - // cache[key] = publicClient.waitForTransactionReceipt({ hash }); - // TODO: figure out how to handle tx replacements: https://viem.sh/docs/actions/public/waitForTransactionReceipt.html#onreplaced-optional +export function getTransactionReceipt( + publicClient: PublicClient, + write: ContractWrite +): Promise { + if (!cache[write.id]) { + cache[write.id] = write.result.then((hash) => publicClient.waitForTransactionReceipt({ hash })); } - return cache[key]; -}; + return cache[write.id]; +} diff --git a/packages/dev-tools/src/actions/getTransactionResult.ts b/packages/dev-tools/src/actions/getTransactionResult.ts index 9c535f074f..e2c3f70d65 100644 --- a/packages/dev-tools/src/actions/getTransactionResult.ts +++ b/packages/dev-tools/src/actions/getTransactionResult.ts @@ -1,23 +1,24 @@ -import { Hex, SimulateContractReturnType, PublicClient, Chain, decodeFunctionData, Transport } from "viem"; +import { SimulateContractReturnType, PublicClient, Chain, decodeFunctionData, Transport, Abi } from "viem"; import { getTransaction } from "./getTransaction"; import { getTransactionReceipt } from "./getTransactionReceipt"; -import { useStore } from "../useStore"; +import { ContractWrite } from "@latticexyz/common"; // TODO: something about this fails when doing lots of simultaneous requests for transactions // not sure if its viem or failed RPC requests or what, but the promises get stuck/never resolve // TODO: use IndexedDB cache for these? -type CacheKey = `${number}:${Hex}`; -const cache: Record> = {}; +const cache: Record> = {}; -export const getTransactionResult = (publicClient: PublicClient, hash: Hex) => { - const key: CacheKey = `${publicClient.chain.id}:${hash}`; - if (!cache[key]) { - const { worldAbi } = useStore.getState(); - const transaction = getTransaction(publicClient, hash); - const transactionReceipt = getTransactionReceipt(publicClient, hash); - cache[key] = Promise.all([transaction, transactionReceipt]).then(([tx, receipt]) => { +export function getTransactionResult( + publicClient: PublicClient, + worldAbi: Abi, + write: ContractWrite +): Promise { + if (!cache[write.id]) { + const transaction = getTransaction(publicClient, write); + const transactionReceipt = getTransactionReceipt(publicClient, write); + cache[write.id] = Promise.all([transaction, transactionReceipt]).then(([tx, receipt]) => { const { functionName, args } = decodeFunctionData({ abi: worldAbi, data: tx.input }); return publicClient.simulateContract({ account: tx.from, @@ -31,5 +32,5 @@ export const getTransactionResult = (publicClient: PublicClient = { + config: TConfig; + publicClient: PublicClient; + walletClient: WalletClient; + latestBlock$: Observable; + blockStorageOperations$: Observable>; + worldAddress: string | null; + worldAbi: Abi; + write$: Observable; + recsWorld?: RecsWorld; +}; diff --git a/packages/dev-tools/src/events/EventIcon.tsx b/packages/dev-tools/src/events/EventIcon.tsx index a6d82f5d45..6aba7d871d 100644 --- a/packages/dev-tools/src/events/EventIcon.tsx +++ b/packages/dev-tools/src/events/EventIcon.tsx @@ -1,8 +1,8 @@ -import { StoreEvent } from "../useStore"; -import { exhaustiveCheck } from "../exhaustiveCheck"; +import { assertExhaustive } from "@latticexyz/common/utils"; +import { StoreEventsAbiItem } from "@latticexyz/store"; type Props = { - eventName: StoreEvent["event"]; + eventName: StoreEventsAbiItem["name"]; }; export function EventIcon({ eventName }: Props) { @@ -16,6 +16,6 @@ export function EventIcon({ eventName }: Props) { case "StoreEphemeralRecord": return ~; default: - return exhaustiveCheck(eventName, `Unexpected event name: ${eventName}`); + return assertExhaustive(eventName, `Unexpected event name: ${eventName}`); } } diff --git a/packages/dev-tools/src/events/EventsPage.tsx b/packages/dev-tools/src/events/EventsPage.tsx index 23deb098c2..59e3f21888 100644 --- a/packages/dev-tools/src/events/EventsPage.tsx +++ b/packages/dev-tools/src/events/EventsPage.tsx @@ -1,9 +1,9 @@ import { useRef, useEffect } from "react"; -import { useStore } from "../useStore"; -import { EventsTable } from "./EventsTable"; +import { useDevToolsContext } from "../DevToolsContext"; +import { StorageOperationsTable } from "./StorageOperationsTable"; export function EventsPage() { - const events = useStore((state) => state.storeEvents); + const { storageOperations } = useDevToolsContext(); const containerRef = useRef(null); const hoveredRef = useRef(false); const scrollBehaviorRef = useRef("auto"); @@ -13,7 +13,7 @@ export function EventsPage() { containerRef.current?.scrollIntoView({ behavior: scrollBehaviorRef.current, block: "end" }); } scrollBehaviorRef.current = "smooth"; - }, [events]); + }, [storageOperations]); return (
- +
); } diff --git a/packages/dev-tools/src/events/EventsTable.tsx b/packages/dev-tools/src/events/StorageOperationsTable.tsx similarity index 59% rename from packages/dev-tools/src/events/EventsTable.tsx rename to packages/dev-tools/src/events/StorageOperationsTable.tsx index 360bef8146..b305fbd779 100644 --- a/packages/dev-tools/src/events/EventsTable.tsx +++ b/packages/dev-tools/src/events/StorageOperationsTable.tsx @@ -1,14 +1,15 @@ +import { StorageOperation } from "@latticexyz/store-sync"; import { serialize } from "../serialize"; -import { StoreEvent } from "../useStore"; import { EventIcon } from "./EventIcon"; +import { StoreConfig } from "@latticexyz/store"; // TODO: use react-table or similar for better perf with lots of logs type Props = { - events: StoreEvent[]; + operations: StorageOperation[]; }; -export function EventsTable({ events }: Props) { +export function StorageOperationsTable({ operations }: Props) { return ( @@ -21,17 +22,22 @@ export function EventsTable({ events }: Props) { - {events.map((event) => ( - - + {operations.map((operation) => ( + + - + + - ))} diff --git a/packages/dev-tools/src/mount.tsx b/packages/dev-tools/src/mount.tsx index b266aaa4df..bfaff6c910 100644 --- a/packages/dev-tools/src/mount.tsx +++ b/packages/dev-tools/src/mount.tsx @@ -1,7 +1,12 @@ +import type { StoreConfig } from "@latticexyz/store"; +import type { DevToolsOptions } from "./common"; + const containerId = "mud-dev-tools"; // TODO: rework to always return a unmount function (not a promise or possibly undefined) -export async function mount() { +export async function mount( + opts: DevToolsOptions +): Promise<(() => void) | undefined> { if (typeof window === "undefined") { console.warn("MUD dev-tools should only be used in browser bundles"); return; @@ -16,6 +21,7 @@ export async function mount() { const React = await import("react"); const ReactDOM = await import("react-dom/client"); const { App } = await import("./App"); + const { DevToolsProvider } = await import("./DevToolsContext"); const rootElement = document.createElement("div"); rootElement.id = containerId; @@ -30,7 +36,9 @@ export async function mount() { const root = ReactDOM.createRoot(rootElement); root.render( - + + + ); diff --git a/packages/dev-tools/src/recs/ComponentData.tsx b/packages/dev-tools/src/recs/ComponentData.tsx new file mode 100644 index 0000000000..8066d8c5bc --- /dev/null +++ b/packages/dev-tools/src/recs/ComponentData.tsx @@ -0,0 +1,21 @@ +import { useParams } from "react-router-dom"; +import { useDevToolsContext } from "../DevToolsContext"; +import { ComponentDataTable } from "./ComponentDataTable"; +import { isStoreComponent } from "@latticexyz/store-sync/recs"; + +// TODO: use react-table or similar for better perf with lots of logs + +export function ComponentData() { + const { recsWorld: world } = useDevToolsContext(); + if (!world) throw new Error("Missing recsWorld"); + + const { id: idParam } = useParams(); + const component = world.components.find((component) => component.id === idParam); + + // TODO: error message or redirect? + if (!component || !isStoreComponent(component)) return null; + + // key here is useful to force a re-render on component changes, + // otherwise state hangs around from previous render during navigation (entities) + return ; +} diff --git a/packages/dev-tools/src/recs/ComponentDataTable.tsx b/packages/dev-tools/src/recs/ComponentDataTable.tsx new file mode 100644 index 0000000000..97662a8069 --- /dev/null +++ b/packages/dev-tools/src/recs/ComponentDataTable.tsx @@ -0,0 +1,56 @@ +import { useEntityQuery } from "@latticexyz/react"; +import { Component, Has, Schema, getComponentValueStrict } from "@latticexyz/recs"; +import { StoreComponentMetadata, decodeEntity } from "@latticexyz/store-sync/recs"; + +// TODO: use react-table or similar for better perf with lots of logs + +type Props = { + component: Component; +}; + +export function ComponentDataTable({ component }: Props) { + // TODO: this breaks when navigating because its state still has entity IDs from prev page + const entities = useEntityQuery([Has(component)]); + + return ( +
{event.blockNumber}
+ {operation.log.blockNumber.toString()} + - {event.table.namespace}:{event.table.name} + {operation.namespace}:{operation.name} {event.keyTuple}{serialize(operation.key)} - + + + {operation.type === "SetRecord" ? serialize(operation.value) : null} + {operation.type === "SetField" ? serialize({ [operation.fieldName]: operation.fieldValue }) : null} {serialize(event.namedValues)}
+ + + {Object.keys(component.metadata.keySchema).map((name) => ( + + ))} + {Object.keys(component.metadata.valueSchema).map((name) => ( + + ))} + + + + {entities.map((entity) => { + const key = decodeEntity(component.metadata.keySchema, entity); + const value = getComponentValueStrict(component, entity); + return ( + + {Object.keys(component.metadata.keySchema).map((name) => ( + + ))} + {Object.keys(component.metadata.valueSchema).map((name) => { + const fieldValue = value[name]; + return ( + + ); + })} + + ); + })} + +
+ {name} + + {name} +
+ {String(key[name])} + + {Array.isArray(fieldValue) ? fieldValue.map(String).join(", ") : String(fieldValue)} +
+ ); +} diff --git a/packages/dev-tools/src/tables/TablesPage.tsx b/packages/dev-tools/src/recs/ComponentsPage.tsx similarity index 68% rename from packages/dev-tools/src/tables/TablesPage.tsx rename to packages/dev-tools/src/recs/ComponentsPage.tsx index 96831c3449..d4f53bb62e 100644 --- a/packages/dev-tools/src/tables/TablesPage.tsx +++ b/packages/dev-tools/src/recs/ComponentsPage.tsx @@ -1,24 +1,27 @@ import { Outlet, useNavigate, useParams } from "react-router-dom"; import { NavButton } from "../NavButton"; -import { useTables } from "./useTables"; import { useEffect, useRef } from "react"; import { twMerge } from "tailwind-merge"; +import { useDevToolsContext } from "../DevToolsContext"; +import { isStoreComponent } from "@latticexyz/store-sync/recs"; -export function TablesPage() { - const detailsRef = useRef(null); - const tables = useTables(); - const { table: tableParam } = useParams(); +export function ComponentsPage() { + const { recsWorld: world } = useDevToolsContext(); + if (!world) throw new Error("Missing recsWorld"); + const components = world.components.filter(isStoreComponent); // TODO: lift up selected component so we can remember previous selection between tab nav - const { tableId: selectedTableId, component: selectedComponent } = - tables.find(({ component }) => component === tableParam) ?? {}; + const { id: idParam } = useParams(); + const selectedComponent = components.find((component) => component.id === idParam); + + const detailsRef = useRef(null); const navigate = useNavigate(); useEffect(() => { - if (tables.length && !selectedComponent) { - navigate(tables[0].component); + if (components.length && !selectedComponent) { + navigate(components[0].id); } - }, [tables, selectedComponent]); + }, [components, selectedComponent]); useEffect(() => { const listener = (event: MouseEvent) => { @@ -32,42 +35,40 @@ export function TablesPage() { return (
- {!tables.length ? ( - <>Waiting for tables… + {!components.length ? ( + <>Waiting for components… ) : (
-

Table

+

Component

- {selectedTableId ? ( - - {selectedTableId.namespace}:{selectedTableId.name} - + {selectedComponent ? ( + {selectedComponent.metadata.componentName} ) : ( - Pick a table… + Pick a component… )}
- {tables.map(({ component, tableId }) => ( + {components.map((component) => ( { if (detailsRef.current) { detailsRef.current.open = false; } }} > - {tableId.namespace}:{tableId.name} + {component.metadata.componentName} ))}
diff --git a/packages/dev-tools/src/tables/serializeWithoutIndexedValues.ts b/packages/dev-tools/src/recs/serializeWithoutIndexedValues.ts similarity index 100% rename from packages/dev-tools/src/tables/serializeWithoutIndexedValues.ts rename to packages/dev-tools/src/recs/serializeWithoutIndexedValues.ts diff --git a/packages/dev-tools/src/router.tsx b/packages/dev-tools/src/router.tsx index 67ceab6714..ccef99b707 100644 --- a/packages/dev-tools/src/router.tsx +++ b/packages/dev-tools/src/router.tsx @@ -1,11 +1,11 @@ import { createMemoryRouter, createRoutesFromElements, Route } from "react-router-dom"; import { RootPage } from "./RootPage"; +import { RouteError } from "./RouteError"; import { EventsPage } from "./events/EventsPage"; import { SummaryPage } from "./summary/SummaryPage"; import { ActionsPage } from "./actions/ActionsPage"; -import { TablesPage } from "./tables/TablesPage"; -import { Table } from "./tables/Table"; -import { RouteError } from "./RouteError"; +import { ComponentsPage } from "./recs/ComponentsPage"; +import { ComponentData } from "./recs/ComponentData"; export const router = createMemoryRouter( createRoutesFromElements( @@ -13,8 +13,8 @@ export const router = createMemoryRouter( } /> } /> } /> - }> - } /> + }> + } /> ) diff --git a/packages/dev-tools/src/summary/AccountSummary.tsx b/packages/dev-tools/src/summary/AccountSummary.tsx index 5ffe9a5175..813f48238e 100644 --- a/packages/dev-tools/src/summary/AccountSummary.tsx +++ b/packages/dev-tools/src/summary/AccountSummary.tsx @@ -1,10 +1,9 @@ import { useEffect, useState } from "react"; -import { useStore } from "../useStore"; import { formatUnits } from "viem"; +import { useDevToolsContext } from "../DevToolsContext"; export function AccountSummary() { - const publicClient = useStore((state) => state.publicClient); - const walletClient = useStore((state) => state.walletClient); + const { publicClient, walletClient } = useDevToolsContext(); const [balance, setBalance] = useState(null); @@ -29,7 +28,7 @@ export function AccountSummary() {
{walletClient?.account?.address}
Balance
- {publicClient && balance ? ( + {publicClient && balance != null ? ( <> {formatUnits(balance, publicClient.chain.nativeCurrency.decimals).replace(/(\.\d{4})\d+$/, "$1")}{" "} {publicClient.chain.nativeCurrency.symbol} diff --git a/packages/dev-tools/src/summary/ActionsSummary.tsx b/packages/dev-tools/src/summary/ActionsSummary.tsx index ebb9173c67..0445369b66 100644 --- a/packages/dev-tools/src/summary/ActionsSummary.tsx +++ b/packages/dev-tools/src/summary/ActionsSummary.tsx @@ -1,17 +1,17 @@ -import { useStore } from "../useStore"; import { NavButton } from "../NavButton"; -import { TransactionSummary } from "../actions/TransactionSummary"; +import { useDevToolsContext } from "../DevToolsContext"; +import { WriteSummary } from "../actions/WriteSummary"; export function ActionsSummary() { - const transactions = useStore((state) => state.transactions.slice(-3)); + const { writes } = useDevToolsContext(); return ( <> - {transactions.length ? ( + {writes.length ? ( <>
- {transactions.map((hash) => ( - + {writes.slice(-5).map((write) => ( + ))}
diff --git a/packages/dev-tools/src/summary/ComponentsSummary.tsx b/packages/dev-tools/src/summary/ComponentsSummary.tsx new file mode 100644 index 0000000000..102f441f66 --- /dev/null +++ b/packages/dev-tools/src/summary/ComponentsSummary.tsx @@ -0,0 +1,32 @@ +import { World } from "@latticexyz/recs"; +import { NavButton } from "../NavButton"; +import { isStoreComponent } from "@latticexyz/store-sync/recs"; + +type Props = { + world: World; +}; + +export function ComponentsSummary({ world }: Props) { + const componentsWithName = world.components.filter(isStoreComponent); + return ( + <> + {componentsWithName.length ? ( + <> +
+ {componentsWithName.map((component) => ( + + {String(component.metadata.componentName)} + + ))} +
+ + ) : ( +
Waiting for components…
+ )} + + ); +} diff --git a/packages/dev-tools/src/summary/EventsSummary.tsx b/packages/dev-tools/src/summary/EventsSummary.tsx index c2f037e025..34e7bcebfb 100644 --- a/packages/dev-tools/src/summary/EventsSummary.tsx +++ b/packages/dev-tools/src/summary/EventsSummary.tsx @@ -1,12 +1,12 @@ -import { EventsTable } from "../events/EventsTable"; -import { useStore } from "../useStore"; import { NavButton } from "../NavButton"; +import { useDevToolsContext } from "../DevToolsContext"; +import { StorageOperationsTable } from "../events/StorageOperationsTable"; export function EventsSummary() { - const events = useStore((state) => state.storeEvents.slice(-10)); + const { storageOperations } = useDevToolsContext(); return ( <> - + See more diff --git a/packages/dev-tools/src/summary/NetworkSummary.tsx b/packages/dev-tools/src/summary/NetworkSummary.tsx index 0e866156c7..10415e018b 100644 --- a/packages/dev-tools/src/summary/NetworkSummary.tsx +++ b/packages/dev-tools/src/summary/NetworkSummary.tsx @@ -1,15 +1,16 @@ -import { useStore } from "../useStore"; +import { useObservableValue } from "@latticexyz/react"; +import { useDevToolsContext } from "../DevToolsContext"; +import { map } from "rxjs"; export function NetworkSummary() { - const publicClient = useStore((state) => state.publicClient); - const blockNumber = useStore((state) => state.blockNumber); - const worldAddress = useStore((state) => state.worldAddress); + const { publicClient, worldAddress, latestBlock$ } = useDevToolsContext(); + const blockNumber = useObservableValue(latestBlock$.pipe(map((block) => block.number))); return (
Chain
- {publicClient?.chain?.id} ({publicClient?.chain?.name}) + {publicClient.chain?.id} ({publicClient.chain?.name})
Block number
@@ -18,9 +19,6 @@ export function NetworkSummary() {
RPC
Connected ✓
-
MODE
-
Not available
-
World
{worldAddress}
diff --git a/packages/dev-tools/src/summary/SummaryPage.tsx b/packages/dev-tools/src/summary/SummaryPage.tsx index a04e03735c..7ff54f8881 100644 --- a/packages/dev-tools/src/summary/SummaryPage.tsx +++ b/packages/dev-tools/src/summary/SummaryPage.tsx @@ -2,14 +2,16 @@ import { NetworkSummary } from "./NetworkSummary"; import { AccountSummary } from "./AccountSummary"; import { EventsSummary } from "./EventsSummary"; import { ActionsSummary } from "./ActionsSummary"; -import { TablesSummary } from "./TablesSummary"; +import { ComponentsSummary } from "./ComponentsSummary"; import packageJson from "../../package.json"; +import { useDevToolsContext } from "../DevToolsContext"; const isLinked = Object.entries(packageJson.dependencies).some( ([name, version]) => name.startsWith("@latticexyz/") && version.startsWith("link:") ); export function SummaryPage() { + const { recsWorld } = useDevToolsContext(); return (
@@ -29,10 +31,12 @@ export function SummaryPage() {

Recent store events

-
-

Tables

- -
+ {recsWorld ? ( +
+

Components

+ +
+ ) : null}
MUD {isLinked ? <>v{packageJson.version} : <>linked} diff --git a/packages/dev-tools/src/summary/TablesSummary.tsx b/packages/dev-tools/src/summary/TablesSummary.tsx deleted file mode 100644 index 7592ab1811..0000000000 --- a/packages/dev-tools/src/summary/TablesSummary.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { NavButton } from "../NavButton"; -import { useTables } from "../tables/useTables"; - -export function TablesSummary() { - const tables = useTables(); - return ( - <> - {tables.length ? ( - <> -
- {tables.map((table) => ( - - {table.tableId.namespace}:{table.tableId.name} - - ))} -
- - ) : ( -
Waiting for tables…
- )} - - ); -} diff --git a/packages/dev-tools/src/tables/Table.tsx b/packages/dev-tools/src/tables/Table.tsx deleted file mode 100644 index dbc9aa5f5e..0000000000 --- a/packages/dev-tools/src/tables/Table.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { unpackTuple } from "@latticexyz/utils"; -import { serializeWithoutIndexedValues } from "./serializeWithoutIndexedValues"; -import { useParams } from "react-router-dom"; -import { useStore } from "../useStore"; -import { filter, distinctUntilChanged, map, of } from "rxjs"; -import { useObservableValue } from "@latticexyz/react"; - -// TODO: use react-table or similar for better perf with lots of logs -// TODO: this will need refactoring once we have better v2 client code, for now we're leaning on v1 cache store (ECS based) - -export function Table() { - const cacheStore = useStore((state) => state.cacheStore); - const { table } = useParams(); - - // Rerender when we detect a change to the table - const lastBlockNumber = useObservableValue( - cacheStore - ? cacheStore.componentUpdate$ - .pipe(filter(({ component }) => component == table)) - .pipe(map(({ blockNumber }) => blockNumber)) - .pipe(distinctUntilChanged()) - : of(0) - ); - - if (!cacheStore) return null; - if (!table) return null; - - const componentIndex = cacheStore.components.indexOf(table); - const cacheStoreKeys = Array.from(cacheStore.state.keys()).filter((key) => { - const [component] = unpackTuple(key); - return component === componentIndex; - }); - - // TODO: get fields and turn into columns instead of a single json value - - return ( - - - - - - - - - {cacheStoreKeys.map((key) => { - const [_componentIndex, entityIndex] = unpackTuple(key); - const entityId = cacheStore.entities[entityIndex]; - const value = cacheStore.state.get(key); - return ( - - - - - ); - })} - -
keyvalue
{entityId} - {serializeWithoutIndexedValues(value)} -
- ); -} diff --git a/packages/dev-tools/src/tables/useTables.ts b/packages/dev-tools/src/tables/useTables.ts deleted file mode 100644 index 21844de28a..0000000000 --- a/packages/dev-tools/src/tables/useTables.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isDefined } from "@latticexyz/common/utils"; -import { TableId } from "@latticexyz/common/deprecated"; -import { useStore } from "../useStore"; - -export function useTables() { - const cacheStore = useStore((state) => state.cacheStore); - if (!cacheStore) { - return []; - } - return cacheStore.components - .map((component) => { - const tableId = TableId.parse(component); - if (!tableId) return; - return { component, tableId }; - }) - .filter(isDefined) - .sort((a, b) => a.component.localeCompare(b.component)); -} diff --git a/packages/dev-tools/src/useStore.ts b/packages/dev-tools/src/useStore.ts deleted file mode 100644 index 8c8dcc4349..0000000000 --- a/packages/dev-tools/src/useStore.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - storeEvent$, - transactionHash$, - publicClient$, - walletClient$, - cacheStore$, - worldAddress$, -} from "@latticexyz/network/dev"; -import { PublicClient, WalletClient, Hex, Chain, Transport } from "viem"; -import { Abi } from "abitype"; -import { create } from "zustand"; -import { worldAbi$ } from "@latticexyz/std-client/dev"; -import { CacheStore } from "@latticexyz/network"; -import { IWorldKernel__factory } from "@latticexyz/world/types/ethers-contracts/factories/IWorldKernel.sol/IWorldKernel__factory"; -import { ObservedValueOf } from "rxjs"; - -export type StoreEvent = ObservedValueOf; - -export const useStore = create<{ - storeEvents: StoreEvent[]; - transactions: Hex[]; - cacheStore: CacheStore | null; - publicClient: PublicClient | null; - walletClient: WalletClient | null; - blockNumber: bigint | null; - worldAbi: Abi; - worldAddress: string | null; -}>(() => ({ - storeEvents: [], - transactions: [], // TODO: populate from recent wallet txs? - cacheStore: null, - publicClient: null, - walletClient: null, - blockNumber: null, - worldAbi: IWorldKernel__factory.abi, - worldAddress: null, -})); - -// TODO: clean up listeners - -storeEvent$.subscribe((storeEvent) => { - // TODO: narrow down to the chain/world we care about? - useStore.setState((state) => ({ - storeEvents: [...state.storeEvents, storeEvent], - })); -}); - -transactionHash$.subscribe((hash) => { - const { publicClient } = useStore.getState(); - if (!publicClient) { - console.log("Got transaction hash, but no public client to fetch it", hash); - return; - } - - useStore.setState((state) => ({ - transactions: [...state.transactions, hash as Hex], - })); -}); - -cacheStore$.subscribe((cacheStore) => { - useStore.setState({ cacheStore }); -}); - -publicClient$.subscribe((publicClient) => { - useStore.setState({ publicClient }); - - // TODO: unwatch if publicClient changes - publicClient?.watchBlockNumber({ - onBlockNumber: (blockNumber) => { - useStore.setState({ blockNumber }); - }, - emitOnBegin: true, - }); -}); - -walletClient$.subscribe((walletClient) => { - useStore.setState({ walletClient }); -}); - -worldAbi$.subscribe((worldAbi) => { - useStore.setState({ worldAbi: worldAbi ?? IWorldKernel__factory.abi }); -}); - -worldAddress$.subscribe((worldAddress) => { - useStore.setState({ worldAddress }); -}); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index b3d23ee8b1..19f2630efb 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -2,4 +2,5 @@ export * from "./useComponentValue"; export * from "./useDeprecatedComputedValue"; export * from "./useEntityQuery"; export * from "./useObservableValue"; +export * from "./usePromise"; export * from "./store-cache"; diff --git a/packages/dev-tools/src/usePromise.ts b/packages/react/src/usePromise.ts similarity index 100% rename from packages/dev-tools/src/usePromise.ts rename to packages/react/src/usePromise.ts diff --git a/packages/store-sync/src/recs/common.ts b/packages/store-sync/src/recs/common.ts index 017d0f5bf1..6e28f72930 100644 --- a/packages/store-sync/src/recs/common.ts +++ b/packages/store-sync/src/recs/common.ts @@ -4,6 +4,8 @@ import { SchemaAbiTypeToRecsType } from "./schemaAbiTypeToRecsType"; import { SchemaAbiType } from "@latticexyz/schema-type"; export type StoreComponentMetadata = { + componentName: string; + tableName: string; keySchema: KeySchema; valueSchema: ValueSchema; }; diff --git a/packages/store-sync/src/recs/defineInternalComponents.ts b/packages/store-sync/src/recs/defineInternalComponents.ts index 785fddc85f..e73627471a 100644 --- a/packages/store-sync/src/recs/defineInternalComponents.ts +++ b/packages/store-sync/src/recs/defineInternalComponents.ts @@ -1,4 +1,4 @@ -import { World, defineComponent, Type } from "@latticexyz/recs"; +import { World, defineComponent, Type, Component, Schema } from "@latticexyz/recs"; import { Table } from "../common"; import { StoreComponentMetadata } from "./common"; @@ -8,7 +8,14 @@ export function defineInternalComponents(world: World) { TableMetadata: defineComponent<{ table: Type.T }, StoreComponentMetadata, Table>( world, { table: Type.T }, - { metadata: { keySchema: {}, valueSchema: {} } } + { + metadata: { + componentName: "TableMetadata", + tableName: "recs:TableMetadata", + keySchema: {}, + valueSchema: {}, + }, + } ), SyncProgress: defineComponent( world, @@ -20,8 +27,13 @@ export function defineInternalComponents(world: World) { lastBlockNumberProcessed: Type.BigInt, }, { - metadata: { keySchema: {}, valueSchema: {} }, + metadata: { + componentName: "SyncProgress", + tableName: "recs:SyncProgress", + keySchema: {}, + valueSchema: {}, + }, } ), - }; + } as const satisfies Record>; } diff --git a/packages/store-sync/src/recs/index.ts b/packages/store-sync/src/recs/index.ts index 706b5d4c65..d7df9e00b3 100644 --- a/packages/store-sync/src/recs/index.ts +++ b/packages/store-sync/src/recs/index.ts @@ -3,6 +3,7 @@ export * from "./decodeEntity"; export * from "./encodeEntity"; export * from "./entityToHexKeyTuple"; export * from "./hexKeyTupleToEntity"; +export * from "./isStoreComponent"; export * from "./recsStorage"; export * from "./singletonEntity"; export * from "./syncToRecs"; diff --git a/packages/store-sync/src/recs/isStoreComponent.ts b/packages/store-sync/src/recs/isStoreComponent.ts new file mode 100644 index 0000000000..bcafc86d8d --- /dev/null +++ b/packages/store-sync/src/recs/isStoreComponent.ts @@ -0,0 +1,13 @@ +import { Component, Schema } from "@latticexyz/recs"; +import { StoreComponentMetadata } from "./common"; + +export function isStoreComponent( + component: Component +): component is Component { + return ( + component.metadata?.componentName != null && + component.metadata?.tableName != null && + component.metadata?.keySchema != null && + component.metadata?.valueSchema != null + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bead10e918..ab5561955b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -313,15 +313,18 @@ importers: '@latticexyz/common': specifier: workspace:* version: link:../common - '@latticexyz/network': - specifier: workspace:* - version: link:../network '@latticexyz/react': specifier: workspace:* version: link:../react - '@latticexyz/std-client': + '@latticexyz/recs': specifier: workspace:* - version: link:../std-client + version: link:../recs + '@latticexyz/store': + specifier: workspace:* + version: link:../store + '@latticexyz/store-sync': + specifier: workspace:* + version: link:../store-sync '@latticexyz/utils': specifier: workspace:* version: link:../utils diff --git a/templates/phaser/packages/client/package.json b/templates/phaser/packages/client/package.json index 740a0d8ee3..f81e6be20a 100644 --- a/templates/phaser/packages/client/package.json +++ b/templates/phaser/packages/client/package.json @@ -28,6 +28,7 @@ "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", + "rxjs": "7.5.5", "simplex-noise": "^4.0.1", "styled-components": "^5.3.10", "use-resize-observer": "^9.1.0", diff --git a/templates/phaser/packages/client/src/index.tsx b/templates/phaser/packages/client/src/index.tsx index d3666e1a86..569c89350c 100644 --- a/templates/phaser/packages/client/src/index.tsx +++ b/templates/phaser/packages/client/src/index.tsx @@ -1,11 +1,9 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { App } from "./ui/App"; -import { mount as mountDevTools } from "@latticexyz/dev-tools"; const rootElement = document.getElementById("react-root"); if (!rootElement) throw new Error("React root not found"); const root = ReactDOM.createRoot(rootElement); root.render(); -mountDevTools(); diff --git a/templates/phaser/packages/client/src/layers/network/createNetworkLayer.ts b/templates/phaser/packages/client/src/layers/network/createNetworkLayer.ts index 376fc506bd..2904fce3a9 100644 --- a/templates/phaser/packages/client/src/layers/network/createNetworkLayer.ts +++ b/templates/phaser/packages/client/src/layers/network/createNetworkLayer.ts @@ -4,16 +4,12 @@ import { setup } from "../../mud/setup"; export type NetworkLayer = Awaited>; export const createNetworkLayer = async () => { - const { components, systemCalls } = await setup(); - - // Give components a Human-readable ID - Object.entries(components).forEach(([name, component]) => { - component.id = name; - }); + const { components, systemCalls, network } = await setup(); return { world, systemCalls, components, + network, }; }; diff --git a/templates/phaser/packages/client/src/mud/setupNetwork.ts b/templates/phaser/packages/client/src/mud/setupNetwork.ts index 6882d73cb1..d42c811ec9 100644 --- a/templates/phaser/packages/client/src/mud/setupNetwork.ts +++ b/templates/phaser/packages/client/src/mud/setupNetwork.ts @@ -4,8 +4,9 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs"; import { getNetworkConfig } from "./getNetworkConfig"; import { world } from "./world"; import { IWorld__factory } from "contracts/types/ethers-contracts/factories/IWorld__factory"; -import storeConfig from "contracts/mud.config"; -import { createBurnerAccount, createContract, transportObserver } from "@latticexyz/common"; +import { createBurnerAccount, createContract, transportObserver, ContractWrite } from "@latticexyz/common"; +import { Subject, share } from "rxjs"; +import mudConfig from "contracts/mud.config"; export type SetupNetworkResult = Awaited>; @@ -26,16 +27,18 @@ export async function setupNetwork() { account: burnerAccount, }); + const write$ = new Subject(); const worldContract = createContract({ address: networkConfig.worldAddress as Hex, abi: IWorld__factory.abi, publicClient, walletClient: burnerWalletClient, + onWrite: (write) => write$.next(write), }); const { components, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({ world, - config: storeConfig, + config: mudConfig, address: networkConfig.worldAddress as Hex, publicClient, startBlock: BigInt(networkConfig.initialBlockNumber), @@ -71,9 +74,10 @@ export async function setupNetwork() { playerEntity: encodeEntity({ address: "address" }, { address: burnerWalletClient.account.address }), publicClient, walletClient: burnerWalletClient, - worldContract, latestBlock$, blockStorageOperations$, waitForTransaction, + worldContract, + write$: write$.asObservable().pipe(share()), }; } diff --git a/templates/phaser/packages/client/src/ui/App.tsx b/templates/phaser/packages/client/src/ui/App.tsx index cd1deb6440..47c66ae8f8 100644 --- a/templates/phaser/packages/client/src/ui/App.tsx +++ b/templates/phaser/packages/client/src/ui/App.tsx @@ -3,20 +3,37 @@ import { useNetworkLayer } from "./hooks/useNetworkLayer"; import { useStore } from "../store"; import { PhaserLayer } from "./PhaserLayer"; import { UIRoot } from "./UIRoot"; +import mudConfig from "contracts/mud.config"; export const App = () => { const networkLayer = useNetworkLayer(); useEffect(() => { - if (networkLayer) { - useStore.setState({ networkLayer }); + if (!networkLayer) return; + + useStore.setState({ networkLayer }); + + // https://vitejs.dev/guide/env-and-mode.html + if (import.meta.env.DEV) { + import("@latticexyz/dev-tools").then(({ mount: mountDevTools }) => + mountDevTools({ + config: mudConfig, + publicClient: networkLayer.network.publicClient, + walletClient: networkLayer.network.walletClient, + latestBlock$: networkLayer.network.latestBlock$, + blockStorageOperations$: networkLayer.network.blockStorageOperations$, + worldAddress: networkLayer.network.worldContract.address, + worldAbi: networkLayer.network.worldContract.abi, + write$: networkLayer.network.write$, + recsWorld: networkLayer.world, + }) + ); } }, [networkLayer]); return (
-
); diff --git a/templates/react/packages/client/package.json b/templates/react/packages/client/package.json index 93277fa1e5..678c504b09 100644 --- a/templates/react/packages/client/package.json +++ b/templates/react/packages/client/package.json @@ -26,6 +26,7 @@ "ethers": "^5.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "rxjs": "7.5.5", "viem": "1.3.1" }, "devDependencies": { diff --git a/templates/react/packages/client/src/index.tsx b/templates/react/packages/client/src/index.tsx index 75ee62727d..9b3f9121e1 100644 --- a/templates/react/packages/client/src/index.tsx +++ b/templates/react/packages/client/src/index.tsx @@ -1,19 +1,34 @@ import ReactDOM from "react-dom/client"; -import { mount as mountDevTools } from "@latticexyz/dev-tools"; import { App } from "./App"; import { setup } from "./mud/setup"; import { MUDProvider } from "./MUDContext"; +import mudConfig from "contracts/mud.config"; const rootElement = document.getElementById("react-root"); if (!rootElement) throw new Error("React root not found"); const root = ReactDOM.createRoot(rootElement); // TODO: figure out if we actually want this to be async or if we should render something else in the meantime -setup().then((result) => { +setup().then(async (result) => { root.render( ); - mountDevTools(); + + // https://vitejs.dev/guide/env-and-mode.html + if (import.meta.env.DEV) { + const { mount: mountDevTools } = await import("@latticexyz/dev-tools"); + mountDevTools({ + config: mudConfig, + publicClient: result.network.publicClient, + walletClient: result.network.walletClient, + latestBlock$: result.network.latestBlock$, + blockStorageOperations$: result.network.blockStorageOperations$, + worldAddress: result.network.worldContract.address, + worldAbi: result.network.worldContract.abi, + write$: result.network.write$, + recsWorld: result.network.world, + }); + } }); diff --git a/templates/react/packages/client/src/mud/setupNetwork.ts b/templates/react/packages/client/src/mud/setupNetwork.ts index 6882d73cb1..d42c811ec9 100644 --- a/templates/react/packages/client/src/mud/setupNetwork.ts +++ b/templates/react/packages/client/src/mud/setupNetwork.ts @@ -4,8 +4,9 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs"; import { getNetworkConfig } from "./getNetworkConfig"; import { world } from "./world"; import { IWorld__factory } from "contracts/types/ethers-contracts/factories/IWorld__factory"; -import storeConfig from "contracts/mud.config"; -import { createBurnerAccount, createContract, transportObserver } from "@latticexyz/common"; +import { createBurnerAccount, createContract, transportObserver, ContractWrite } from "@latticexyz/common"; +import { Subject, share } from "rxjs"; +import mudConfig from "contracts/mud.config"; export type SetupNetworkResult = Awaited>; @@ -26,16 +27,18 @@ export async function setupNetwork() { account: burnerAccount, }); + const write$ = new Subject(); const worldContract = createContract({ address: networkConfig.worldAddress as Hex, abi: IWorld__factory.abi, publicClient, walletClient: burnerWalletClient, + onWrite: (write) => write$.next(write), }); const { components, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({ world, - config: storeConfig, + config: mudConfig, address: networkConfig.worldAddress as Hex, publicClient, startBlock: BigInt(networkConfig.initialBlockNumber), @@ -71,9 +74,10 @@ export async function setupNetwork() { playerEntity: encodeEntity({ address: "address" }, { address: burnerWalletClient.account.address }), publicClient, walletClient: burnerWalletClient, - worldContract, latestBlock$, blockStorageOperations$, waitForTransaction, + worldContract, + write$: write$.asObservable().pipe(share()), }; } diff --git a/templates/threejs/packages/client/package.json b/templates/threejs/packages/client/package.json index 47a9428025..0cb2eb6f62 100644 --- a/templates/threejs/packages/client/package.json +++ b/templates/threejs/packages/client/package.json @@ -27,6 +27,7 @@ "ethers": "^5.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "rxjs": "7.5.5", "viem": "1.3.1" }, "devDependencies": { diff --git a/templates/threejs/packages/client/src/index.tsx b/templates/threejs/packages/client/src/index.tsx index 75ee62727d..9b3f9121e1 100644 --- a/templates/threejs/packages/client/src/index.tsx +++ b/templates/threejs/packages/client/src/index.tsx @@ -1,19 +1,34 @@ import ReactDOM from "react-dom/client"; -import { mount as mountDevTools } from "@latticexyz/dev-tools"; import { App } from "./App"; import { setup } from "./mud/setup"; import { MUDProvider } from "./MUDContext"; +import mudConfig from "contracts/mud.config"; const rootElement = document.getElementById("react-root"); if (!rootElement) throw new Error("React root not found"); const root = ReactDOM.createRoot(rootElement); // TODO: figure out if we actually want this to be async or if we should render something else in the meantime -setup().then((result) => { +setup().then(async (result) => { root.render( ); - mountDevTools(); + + // https://vitejs.dev/guide/env-and-mode.html + if (import.meta.env.DEV) { + const { mount: mountDevTools } = await import("@latticexyz/dev-tools"); + mountDevTools({ + config: mudConfig, + publicClient: result.network.publicClient, + walletClient: result.network.walletClient, + latestBlock$: result.network.latestBlock$, + blockStorageOperations$: result.network.blockStorageOperations$, + worldAddress: result.network.worldContract.address, + worldAbi: result.network.worldContract.abi, + write$: result.network.write$, + recsWorld: result.network.world, + }); + } }); diff --git a/templates/threejs/packages/client/src/mud/setupNetwork.ts b/templates/threejs/packages/client/src/mud/setupNetwork.ts index 6882d73cb1..d42c811ec9 100644 --- a/templates/threejs/packages/client/src/mud/setupNetwork.ts +++ b/templates/threejs/packages/client/src/mud/setupNetwork.ts @@ -4,8 +4,9 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs"; import { getNetworkConfig } from "./getNetworkConfig"; import { world } from "./world"; import { IWorld__factory } from "contracts/types/ethers-contracts/factories/IWorld__factory"; -import storeConfig from "contracts/mud.config"; -import { createBurnerAccount, createContract, transportObserver } from "@latticexyz/common"; +import { createBurnerAccount, createContract, transportObserver, ContractWrite } from "@latticexyz/common"; +import { Subject, share } from "rxjs"; +import mudConfig from "contracts/mud.config"; export type SetupNetworkResult = Awaited>; @@ -26,16 +27,18 @@ export async function setupNetwork() { account: burnerAccount, }); + const write$ = new Subject(); const worldContract = createContract({ address: networkConfig.worldAddress as Hex, abi: IWorld__factory.abi, publicClient, walletClient: burnerWalletClient, + onWrite: (write) => write$.next(write), }); const { components, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({ world, - config: storeConfig, + config: mudConfig, address: networkConfig.worldAddress as Hex, publicClient, startBlock: BigInt(networkConfig.initialBlockNumber), @@ -71,9 +74,10 @@ export async function setupNetwork() { playerEntity: encodeEntity({ address: "address" }, { address: burnerWalletClient.account.address }), publicClient, walletClient: burnerWalletClient, - worldContract, latestBlock$, blockStorageOperations$, waitForTransaction, + worldContract, + write$: write$.asObservable().pipe(share()), }; } diff --git a/templates/vanilla/packages/client/package.json b/templates/vanilla/packages/client/package.json index 2f2b3602fe..7a9d6363a7 100644 --- a/templates/vanilla/packages/client/package.json +++ b/templates/vanilla/packages/client/package.json @@ -23,6 +23,7 @@ "@latticexyz/world": "link:../../../../packages/world", "contracts": "workspace:*", "ethers": "^5.7.2", + "rxjs": "7.5.5", "viem": "1.3.1" }, "devDependencies": { diff --git a/templates/vanilla/packages/client/src/index.ts b/templates/vanilla/packages/client/src/index.ts index 287bcb959d..71b05b1f8d 100644 --- a/templates/vanilla/packages/client/src/index.ts +++ b/templates/vanilla/packages/client/src/index.ts @@ -1,9 +1,10 @@ -import { mount as mountDevTools } from "@latticexyz/dev-tools"; import { setup } from "./mud/setup"; +import mudConfig from "contracts/mud.config"; const { components, systemCalls: { increment }, + network, } = await setup(); // Components expose a stream that triggers when the component is updated. @@ -19,4 +20,18 @@ components.Counter.update$.subscribe((update) => { console.log("new counter value:", await increment()); }; -mountDevTools(); +// https://vitejs.dev/guide/env-and-mode.html +if (import.meta.env.DEV) { + const { mount: mountDevTools } = await import("@latticexyz/dev-tools"); + mountDevTools({ + config: mudConfig, + publicClient: network.publicClient, + walletClient: network.walletClient, + latestBlock$: network.latestBlock$, + blockStorageOperations$: network.blockStorageOperations$, + worldAddress: network.worldContract.address, + worldAbi: network.worldContract.abi, + write$: network.write$, + recsWorld: network.world, + }); +} diff --git a/templates/vanilla/packages/client/src/mud/setupNetwork.ts b/templates/vanilla/packages/client/src/mud/setupNetwork.ts index 6882d73cb1..d42c811ec9 100644 --- a/templates/vanilla/packages/client/src/mud/setupNetwork.ts +++ b/templates/vanilla/packages/client/src/mud/setupNetwork.ts @@ -4,8 +4,9 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs"; import { getNetworkConfig } from "./getNetworkConfig"; import { world } from "./world"; import { IWorld__factory } from "contracts/types/ethers-contracts/factories/IWorld__factory"; -import storeConfig from "contracts/mud.config"; -import { createBurnerAccount, createContract, transportObserver } from "@latticexyz/common"; +import { createBurnerAccount, createContract, transportObserver, ContractWrite } from "@latticexyz/common"; +import { Subject, share } from "rxjs"; +import mudConfig from "contracts/mud.config"; export type SetupNetworkResult = Awaited>; @@ -26,16 +27,18 @@ export async function setupNetwork() { account: burnerAccount, }); + const write$ = new Subject(); const worldContract = createContract({ address: networkConfig.worldAddress as Hex, abi: IWorld__factory.abi, publicClient, walletClient: burnerWalletClient, + onWrite: (write) => write$.next(write), }); const { components, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({ world, - config: storeConfig, + config: mudConfig, address: networkConfig.worldAddress as Hex, publicClient, startBlock: BigInt(networkConfig.initialBlockNumber), @@ -71,9 +74,10 @@ export async function setupNetwork() { playerEntity: encodeEntity({ address: "address" }, { address: burnerWalletClient.account.address }), publicClient, walletClient: burnerWalletClient, - worldContract, latestBlock$, blockStorageOperations$, waitForTransaction, + worldContract, + write$: write$.asObservable().pipe(share()), }; }