From 784e5a98e679388ad6bc941cd1bc9b6486cf276d Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 12 Sep 2024 14:55:40 +0000 Subject: [PATCH] feat(explorer): write observer (#3169) Co-authored-by: Karolis Ramanauskas --- .changeset/smart-parents-refuse.md | 33 ++++++ .../packages/client/package.json | 2 +- .../packages/client/src/index.tsx | 17 --- .../packages/client/src/mud/setupNetwork.ts | 21 ++-- examples/local-explorer/pnpm-lock.yaml | 6 +- package.json | 2 +- packages/explorer/bin/explorer.js | 3 + packages/explorer/package.json | 30 +++-- .../explorer/src/app/(explorer)/error.tsx | 2 +- .../explorer/src/app/(explorer)/not-found.tsx | 2 +- .../{explorer => explore}/DataExplorer.tsx | 0 .../EditableTableCell.tsx | 0 .../{explorer => explore}/TableSelector.tsx | 0 .../{explorer => explore}/TablesViewer.tsx | 0 .../{explorer => explore}/page.tsx | 0 .../worlds/[worldAddress]/observe/Write.tsx | 31 +++++ .../worlds/[worldAddress]/observe/Writes.tsx | 26 ++++ .../worlds/[worldAddress]/observe/common.ts | 1 + .../worlds/[worldAddress]/observe/page.tsx | 5 + .../(explorer)/worlds/[worldAddress]/page.tsx | 2 +- packages/explorer/src/app/internal/layout.tsx | 7 ++ .../src/app/internal/observer-relay/Relay.tsx | 9 ++ .../src/app/internal/observer-relay/page.tsx | 5 + packages/explorer/{ => src}/bin/explorer.ts | 5 +- .../explorer/src/components/KeepInView.tsx | 38 ++++++ .../explorer/src/components/Navigation.tsx | 15 ++- packages/explorer/src/debug.ts | 3 + packages/explorer/src/exports/observer.ts | 3 + packages/explorer/src/observer/README.md | 23 ++++ packages/explorer/src/observer/bridge.ts | 81 +++++++++++++ packages/explorer/src/observer/common.ts | 5 + packages/explorer/src/observer/debug.ts | 3 + packages/explorer/src/observer/decorator.ts | 72 +++++++++++ packages/explorer/src/observer/messages.ts | 37 ++++++ packages/explorer/src/observer/relay.ts | 20 ++++ packages/explorer/src/observer/store.ts | 44 +++++++ packages/explorer/tsconfig.tsup.json | 3 + packages/explorer/tsup.config.ts | 6 +- pnpm-lock.yaml | 112 ++++++++++++------ 39 files changed, 590 insertions(+), 84 deletions(-) create mode 100644 .changeset/smart-parents-refuse.md create mode 100755 packages/explorer/bin/explorer.js rename packages/explorer/src/app/(explorer)/worlds/[worldAddress]/{explorer => explore}/DataExplorer.tsx (100%) rename packages/explorer/src/app/(explorer)/worlds/[worldAddress]/{explorer => explore}/EditableTableCell.tsx (100%) rename packages/explorer/src/app/(explorer)/worlds/[worldAddress]/{explorer => explore}/TableSelector.tsx (100%) rename packages/explorer/src/app/(explorer)/worlds/[worldAddress]/{explorer => explore}/TablesViewer.tsx (100%) rename packages/explorer/src/app/(explorer)/worlds/[worldAddress]/{explorer => explore}/page.tsx (100%) create mode 100644 packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Write.tsx create mode 100644 packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Writes.tsx create mode 100644 packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/common.ts create mode 100644 packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/page.tsx create mode 100644 packages/explorer/src/app/internal/layout.tsx create mode 100644 packages/explorer/src/app/internal/observer-relay/Relay.tsx create mode 100644 packages/explorer/src/app/internal/observer-relay/page.tsx rename packages/explorer/{ => src}/bin/explorer.ts (97%) create mode 100644 packages/explorer/src/components/KeepInView.tsx create mode 100644 packages/explorer/src/debug.ts create mode 100644 packages/explorer/src/exports/observer.ts create mode 100644 packages/explorer/src/observer/README.md create mode 100644 packages/explorer/src/observer/bridge.ts create mode 100644 packages/explorer/src/observer/common.ts create mode 100644 packages/explorer/src/observer/debug.ts create mode 100644 packages/explorer/src/observer/decorator.ts create mode 100644 packages/explorer/src/observer/messages.ts create mode 100644 packages/explorer/src/observer/relay.ts create mode 100644 packages/explorer/src/observer/store.ts create mode 100644 packages/explorer/tsconfig.tsup.json diff --git a/.changeset/smart-parents-refuse.md b/.changeset/smart-parents-refuse.md new file mode 100644 index 0000000000..bf160770f5 --- /dev/null +++ b/.changeset/smart-parents-refuse.md @@ -0,0 +1,33 @@ +--- +"@latticexyz/explorer": patch +--- + +World Explorer package now exports an `observer` Viem decorator that can be used to get visibility into contract writes initiated from your app. You can watch these writes stream in on the new "Observe" tab of the World Explorer. + +```ts +import { createClient, publicActions, walletActions } from "viem"; +import { observer } from "@latticexyz/explorer/observer"; + +const client = createClient({ ... }) + .extend(publicActions) + .extend(walletActions) + .extend(observer()); +``` + +By default, the `observer` action assumes the World Explorer is running at `http://localhost:13690`, but this can be customized with the `explorerUrl` option. + +```ts +observer({ + explorerUrl: "http://localhost:4444", +}); +``` + +If you want to measure the timing of transaction-to-state-change, you can also pass in a `waitForStateChange` function that takes a transaction hash and returns a partial [`TransactionReceipt`](https://viem.sh/docs/glossary/types#transactionreceipt) with `blockNumber`, `status`, and `transactionHash`. This mirrors the `waitForTransaction` function signature returned by `syncTo...` helper in `@latticexyz/store-sync`. + +```ts +observer({ + async waitForStateChange(hash) { + return await waitForTransaction(hash); + }, +}); +``` diff --git a/examples/local-explorer/packages/client/package.json b/examples/local-explorer/packages/client/package.json index cb8f88f0b0..8dbe1b0f80 100644 --- a/examples/local-explorer/packages/client/package.json +++ b/examples/local-explorer/packages/client/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@latticexyz/common": "link:../../../../packages/common", - "@latticexyz/dev-tools": "link:../../../../packages/dev-tools", + "@latticexyz/explorer": "link:../../../../packages/explorer", "@latticexyz/react": "link:../../../../packages/react", "@latticexyz/schema-type": "link:../../../../packages/schema-type", "@latticexyz/store-sync": "link:../../../../packages/store-sync", diff --git a/examples/local-explorer/packages/client/src/index.tsx b/examples/local-explorer/packages/client/src/index.tsx index c9b662c9f0..5362fcf0a4 100644 --- a/examples/local-explorer/packages/client/src/index.tsx +++ b/examples/local-explorer/packages/client/src/index.tsx @@ -2,7 +2,6 @@ import ReactDOM from "react-dom/client"; 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"); @@ -15,20 +14,4 @@ setup().then(async (result) => { , ); - - // 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$, - storedBlockLogs$: result.network.storedBlockLogs$, - worldAddress: result.network.worldContract.address, - worldAbi: result.network.worldContract.abi, - write$: result.network.write$, - useStore: result.network.useStore, - }); - } }); diff --git a/examples/local-explorer/packages/client/src/mud/setupNetwork.ts b/examples/local-explorer/packages/client/src/mud/setupNetwork.ts index dba19e3f4c..2ff379e41e 100644 --- a/examples/local-explorer/packages/client/src/mud/setupNetwork.ts +++ b/examples/local-explorer/packages/client/src/mud/setupNetwork.ts @@ -16,9 +16,9 @@ import { import { syncToZustand } from "@latticexyz/store-sync/zustand"; import { getNetworkConfig } from "./getNetworkConfig"; import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json"; -import { createBurnerAccount, transportObserver, ContractWrite } from "@latticexyz/common"; -import { transactionQueue, writeObserver } from "@latticexyz/common/actions"; -import { Subject, share } from "rxjs"; +import { createBurnerAccount, transportObserver } from "@latticexyz/common"; +import { transactionQueue } from "@latticexyz/common/actions"; +import { observer, type WaitForStateChange } from "@latticexyz/explorer/observer"; /* * Import our MUD config, which includes strong types for @@ -34,6 +34,7 @@ export type SetupNetworkResult = Awaited>; export async function setupNetwork() { const networkConfig = await getNetworkConfig(); + const waitForStateChange = Promise.withResolvers(); /* * Create a viem public (read only) client @@ -47,12 +48,6 @@ export async function setupNetwork() { const publicClient = createPublicClient(clientOptions); - /* - * Create an observable for contract writes that we can - * pass into MUD dev tools for transaction observability. - */ - const write$ = new Subject(); - /* * Create a temporary wallet and a viem client for it * (see https://viem.sh/docs/clients/wallet.html). @@ -63,7 +58,11 @@ export async function setupNetwork() { account: burnerAccount, }) .extend(transactionQueue()) - .extend(writeObserver({ onWrite: (write) => write$.next(write) })); + .extend( + observer({ + waitForStateChange: (hash) => waitForStateChange.promise.then((fn) => fn(hash)), + }), + ); /* * Create an object for communicating with the deployed World. @@ -86,6 +85,7 @@ export async function setupNetwork() { publicClient, startBlock: BigInt(networkConfig.initialBlockNumber), }); + waitForStateChange.resolve(waitForTransaction); return { tables, @@ -96,6 +96,5 @@ export async function setupNetwork() { storedBlockLogs$, waitForTransaction, worldContract, - write$: write$.asObservable().pipe(share()), }; } diff --git a/examples/local-explorer/pnpm-lock.yaml b/examples/local-explorer/pnpm-lock.yaml index 35e2931c00..db7ba2d842 100644 --- a/examples/local-explorer/pnpm-lock.yaml +++ b/examples/local-explorer/pnpm-lock.yaml @@ -47,9 +47,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/explorer': + specifier: link:../../../../packages/explorer + version: link:../../../../packages/explorer '@latticexyz/react': specifier: link:../../../../packages/react version: link:../../../../packages/react diff --git a/package.json b/package.json index ff3e249430..425b16aca4 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "build": "turbo run build", "changelog:generate": "tsx scripts/changelog.ts", "clean": "turbo run clean", - "dev": "TSUP_SKIP_DTS=true turbo run dev --concurrency 100 --filter=!@latticexyz/explorer", + "dev": "TSUP_SKIP_DTS=true turbo run dev --concurrency 100", "dist-tag-rm": "pnpm recursive exec -- sh -c 'npm dist-tag rm $(cat package.json | jq -r \".name\") $TAG || true'", "docs:generate:api": "tsx scripts/render-api-docs.ts", "foundryup": "curl -L https://foundry.paradigm.xyz | bash && bash ~/.foundry/bin/foundryup", diff --git a/packages/explorer/bin/explorer.js b/packages/explorer/bin/explorer.js new file mode 100755 index 0000000000..6d4f0839cf --- /dev/null +++ b/packages/explorer/bin/explorer.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node +// workaround for https://github.com/pnpm/pnpm/issues/1801 +import "../dist/bin/explorer.js"; diff --git a/packages/explorer/package.json b/packages/explorer/package.json index 53f98600e5..806b76ee5d 100644 --- a/packages/explorer/package.json +++ b/packages/explorer/package.json @@ -3,23 +3,35 @@ "version": "2.2.3", "description": "World Explorer is a tool for visually exploring and manipulating the state of worlds", "type": "module", + "exports": { + "./observer": "./dist/exports/observer.js" + }, + "typesVersions": { + "*": { + "observer": [ + "./dist/exports/observer.d.ts" + ] + } + }, "bin": { - "explorer": "./dist/explorer.js" + "explorer": "./bin/explorer.js" }, "files": [ + "bin", "dist", ".next/standalone/packages/explorer" ], "scripts": { - "build": "pnpm run build:explorer && pnpm run build:bin", - "build:bin": "tsup", + "build": "pnpm run build:js && pnpm run build:explorer", "build:explorer": "next build && shx cp -r .next/static .next/standalone/packages/explorer/.next", - "clean": "pnpm run clean:explorer && pnpm run clean:bin", - "clean:bin": "shx rm -rf dist", + "build:js": "tsup", + "clean": "pnpm run clean:js && pnpm run clean:explorer", "clean:explorer": "shx rm -rf .next .turbo", - "dev": "next dev --port 13690", - "lint": "next lint", - "start": "node .next/standalone/packages/explorer/server.js" + "clean:js": "shx rm -rf dist", + "dev": "tsup --watch", + "explorer:dev": "next dev --port 13690", + "explorer:start": "node .next/standalone/packages/explorer/server.js", + "lint": "next lint" }, "dependencies": { "@hookform/resolvers": "^3.9.0", @@ -44,6 +56,7 @@ "better-sqlite3": "^8.6.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "debug": "^4.3.4", "lucide-react": "^0.408.0", "next": "14.2.5", "query-string": "^9.1.0", @@ -62,6 +75,7 @@ "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/better-sqlite3": "^7.6.4", + "@types/debug": "^4.1.7", "@types/minimist": "^1.2.5", "@types/node": "^18.15.11", "@types/react": "18.2.22", diff --git a/packages/explorer/src/app/(explorer)/error.tsx b/packages/explorer/src/app/(explorer)/error.tsx index 2c1604616b..4cfe8b5711 100644 --- a/packages/explorer/src/app/(explorer)/error.tsx +++ b/packages/explorer/src/app/(explorer)/error.tsx @@ -25,7 +25,7 @@ export default function Error({ reset, error }: Props) {