From a7c8bb21ea7a145c270e6e921fac4e1496a619ca Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Wed, 11 Sep 2024 14:04:22 +0100 Subject: [PATCH 1/6] feat(explorer): write observer --- .../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 => 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 + 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 | 19 +++ packages/explorer/src/observer/store.ts | 44 +++++++ packages/explorer/tsconfig.tsup.json | 3 + packages/explorer/tsup.config.ts | 6 +- pnpm-lock.yaml | 112 ++++++++++++------ 35 files changed, 553 insertions(+), 81 deletions(-) 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/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)/worlds/[worldAddress]/explorer/DataExplorer.tsx b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/DataExplorer.tsx similarity index 100% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explorer/DataExplorer.tsx rename to packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/DataExplorer.tsx diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explorer/EditableTableCell.tsx b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/EditableTableCell.tsx similarity index 100% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explorer/EditableTableCell.tsx rename to packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/EditableTableCell.tsx diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explorer/TableSelector.tsx b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/TableSelector.tsx similarity index 100% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explorer/TableSelector.tsx rename to packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/TableSelector.tsx diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explorer/TablesViewer.tsx b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/TablesViewer.tsx similarity index 100% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explorer/TablesViewer.tsx rename to packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/TablesViewer.tsx diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explorer/page.tsx b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/page.tsx similarity index 100% rename from packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explorer/page.tsx rename to packages/explorer/src/app/(explorer)/worlds/[worldAddress]/explore/page.tsx diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Write.tsx b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Write.tsx new file mode 100644 index 0000000000..b61825e5ce --- /dev/null +++ b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Write.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type Write } from "../../../../../observer/store"; +import { msPerViewportWidth } from "./common"; + +export type Props = Write; + +export function Write({ functionSignature, time: start, events }: Props) { + return ( +
+
+ {functionSignature} {new Date(start).toLocaleTimeString()} +
+
+ {events.map((event) => ( +
+
+
+
+ {event.type} {event.time - start}ms +
+
+
+ ))} +
+
+ ); +} diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Writes.tsx b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Writes.tsx new file mode 100644 index 0000000000..053b509e28 --- /dev/null +++ b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/Writes.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useStore } from "zustand"; +import { KeepInView } from "../../../../../components/KeepInView"; +import { store } from "../../../../../observer/store"; +import { Write } from "./Write"; + +export function Writes() { + const writes = useStore(store, (state) => Object.values(state.writes)); + + return ( + // TODO: replace with h-full once container is stretched to full height +
+ +
+ {writes.length === 0 ? <>Waiting for transactions… : null} + {writes.map((write) => ( +
+ +
+ ))} +
+
+
+ ); +} diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/common.ts b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/common.ts new file mode 100644 index 0000000000..fa7dd9d0b1 --- /dev/null +++ b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/common.ts @@ -0,0 +1 @@ +export const msPerViewportWidth = 1000 * 60 * 1; diff --git a/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/page.tsx b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/page.tsx new file mode 100644 index 0000000000..1cd2e007d7 --- /dev/null +++ b/packages/explorer/src/app/(explorer)/worlds/[worldAddress]/observe/page.tsx @@ -0,0 +1,5 @@ +import { Writes } from "./Writes"; + +export default function ObservePage() { + return ; +} diff --git a/packages/explorer/src/app/internal/layout.tsx b/packages/explorer/src/app/internal/layout.tsx new file mode 100644 index 0000000000..c8f9cee0b7 --- /dev/null +++ b/packages/explorer/src/app/internal/layout.tsx @@ -0,0 +1,7 @@ +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/packages/explorer/src/app/internal/observer-relay/Relay.tsx b/packages/explorer/src/app/internal/observer-relay/Relay.tsx new file mode 100644 index 0000000000..62a66b8741 --- /dev/null +++ b/packages/explorer/src/app/internal/observer-relay/Relay.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { useEffect } from "react"; +import { createRelay } from "../../../observer/relay"; + +export function Relay() { + useEffect(createRelay, []); + return null; +} diff --git a/packages/explorer/src/app/internal/observer-relay/page.tsx b/packages/explorer/src/app/internal/observer-relay/page.tsx new file mode 100644 index 0000000000..85fc8fc574 --- /dev/null +++ b/packages/explorer/src/app/internal/observer-relay/page.tsx @@ -0,0 +1,5 @@ +import { Relay } from "./Relay"; + +export default function ObserverRelayPage() { + return ; +} diff --git a/packages/explorer/bin/explorer.ts b/packages/explorer/src/bin/explorer.ts similarity index 97% rename from packages/explorer/bin/explorer.ts rename to packages/explorer/src/bin/explorer.ts index e7b3bcc06f..88f887ba32 100755 --- a/packages/explorer/bin/explorer.ts +++ b/packages/explorer/src/bin/explorer.ts @@ -9,6 +9,7 @@ import { ChildProcess, spawn } from "child_process"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +const packageRoot = path.join(__dirname, "..", ".."); const argv = yargs(process.argv.slice(2)) .options({ @@ -72,14 +73,14 @@ async function startExplorer() { "node_modules/.bin/next", ["dev", "--port", port.toString(), ...(hostname ? ["--hostname", hostname] : [])], { - cwd: path.join(__dirname, ".."), + cwd: packageRoot, stdio: "inherit", env, }, ); } else { explorerProcess = spawn("node", [".next/standalone/packages/explorer/server.js"], { - cwd: path.join(__dirname, ".."), + cwd: packageRoot, stdio: "inherit", env: { ...env, diff --git a/packages/explorer/src/components/KeepInView.tsx b/packages/explorer/src/components/KeepInView.tsx new file mode 100644 index 0000000000..83161a584f --- /dev/null +++ b/packages/explorer/src/components/KeepInView.tsx @@ -0,0 +1,38 @@ +import { ReactNode, useRef } from "react"; + +export type Props = { + className?: string; + children: ReactNode; + enabled?: boolean; +}; + +export function KeepInView({ className, children, enabled = true }: Props) { + const containerRef = useRef(null); + const hoveredRef = useRef(false); + const scrollBehaviorRef = useRef("auto"); + + // Intentionally not in a `useEffect` so this triggers on every render. + if (!hoveredRef.current && enabled) { + containerRef.current?.scrollIntoView({ + behavior: scrollBehaviorRef.current, + block: "end", + inline: "end", + }); + } + scrollBehaviorRef.current = "smooth"; + + return ( +
{ + hoveredRef.current = true; + }} + onMouseLeave={() => { + hoveredRef.current = false; + }} + className={className} + > + {children} +
+ ); +} diff --git a/packages/explorer/src/components/Navigation.tsx b/packages/explorer/src/components/Navigation.tsx index 1429946851..e4d0d7c2b3 100644 --- a/packages/explorer/src/components/Navigation.tsx +++ b/packages/explorer/src/components/Navigation.tsx @@ -20,12 +20,12 @@ export function Navigation() {
- Data explorer + Explore Interact + + + Observe +
{isFetched && !data?.isWorldDeployed && ( diff --git a/packages/explorer/src/debug.ts b/packages/explorer/src/debug.ts new file mode 100644 index 0000000000..7cab931243 --- /dev/null +++ b/packages/explorer/src/debug.ts @@ -0,0 +1,3 @@ +import createDebug from "debug"; + +export const debug = createDebug("mud:explorer"); diff --git a/packages/explorer/src/exports/observer.ts b/packages/explorer/src/exports/observer.ts new file mode 100644 index 0000000000..84c6797b40 --- /dev/null +++ b/packages/explorer/src/exports/observer.ts @@ -0,0 +1,3 @@ +export { createBridge, type CreateBridgeOpts } from "../observer/bridge"; +export type { Messages, MessageType, EmitMessage } from "../observer/messages"; +export { observer, type ObserverOptions, type WaitForStateChange } from "../observer/decorator"; diff --git a/packages/explorer/src/observer/README.md b/packages/explorer/src/observer/README.md new file mode 100644 index 0000000000..71510e61a2 --- /dev/null +++ b/packages/explorer/src/observer/README.md @@ -0,0 +1,23 @@ +``` +┌─app────────────────────────┐ ┌─explorer───────────────────┐ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ┌─bridge─┐ │ │ │ +│ │ │ │ │ │ +│ │ ───┼relay──► │ +│ │ │ │ │ │ +│ └────────┘ │ │ │ +└────────────────────────────┘ └────────────────────────────┘ +``` + + + +## TODO + +- [ ] figure out why 127.0.0.1 wasn't relaying messages on BroadcastChannel but localhost does +- [ ] Explorer/Next.js app seems to be clearing localStorage.debug on reload? diff --git a/packages/explorer/src/observer/bridge.ts b/packages/explorer/src/observer/bridge.ts new file mode 100644 index 0000000000..4ea01ba098 --- /dev/null +++ b/packages/explorer/src/observer/bridge.ts @@ -0,0 +1,81 @@ +"use client"; + +import { wait } from "@latticexyz/common/utils"; +import { debug } from "./debug"; +import { EmitMessage } from "./messages"; + +export type BridgeEnvelope = { mud: "explorer/observer"; data: unknown }; + +export function isBridgeEnvelope(input: unknown): input is BridgeEnvelope { + return ( + typeof input === "object" && + input !== null && + "mud" in input && + input.mud === "explorer/observer" && + "data" in input + ); +} + +export function wrapMessage(data: unknown): BridgeEnvelope { + return { mud: "explorer/observer", data: data }; +} + +export type CreateBridgeOpts = { + url: string; + timeout?: number; +}; + +export function createBridge({ url, timeout = 10_000 }: CreateBridgeOpts): EmitMessage { + const emit = Promise.withResolvers(); + const iframe = document.createElement("iframe"); + iframe.tabIndex = -1; + iframe.ariaHidden = "true"; + iframe.style.position = "absolute"; + iframe.style.border = "0"; + iframe.style.width = "0"; + iframe.style.height = "0"; + + iframe.addEventListener( + "load", + () => { + debug("observer iframe ready", iframe.src); + // TODO: throw if `iframe.contentWindow` is `null`? + emit.resolve((type, data) => { + const message = wrapMessage({ ...data, type, time: Date.now() }); + debug("posting message to bridge", message); + iframe.contentWindow!.postMessage(message, "*"); + }); + }, + { once: true }, + ); + + iframe.addEventListener( + "error", + (error) => { + debug("observer iframe error", error); + emit.reject(error); + }, + { once: true }, + ); + + // TODO: should we let the caller handle this with their own promise timeout or race? + wait(timeout).then(() => { + emit.reject(new Error("Timed out waiting for observer iframe to load.")); + }); + + debug("mounting observer iframe", url); + iframe.src = url; + parent.document.body.appendChild(iframe); + + emit.promise.catch(() => { + iframe.remove(); + }); + + return (messageType, message) => { + debug("got message for bridge", messageType, message); + emit.promise.then( + (fn) => fn(messageType, message), + (error) => debug("could not deliver message", message, error), + ); + }; +} diff --git a/packages/explorer/src/observer/common.ts b/packages/explorer/src/observer/common.ts new file mode 100644 index 0000000000..04f2bd63cd --- /dev/null +++ b/packages/explorer/src/observer/common.ts @@ -0,0 +1,5 @@ +import { TransactionReceipt } from "viem"; + +export const relayChannelName = "explorer/observer"; + +export type ReceiptSummary = Pick; diff --git a/packages/explorer/src/observer/debug.ts b/packages/explorer/src/observer/debug.ts new file mode 100644 index 0000000000..8e1becd45c --- /dev/null +++ b/packages/explorer/src/observer/debug.ts @@ -0,0 +1,3 @@ +import { debug as parentDebug } from "../debug"; + +export const debug = parentDebug.extend("observer"); diff --git a/packages/explorer/src/observer/decorator.ts b/packages/explorer/src/observer/decorator.ts new file mode 100644 index 0000000000..cee3a22c2b --- /dev/null +++ b/packages/explorer/src/observer/decorator.ts @@ -0,0 +1,72 @@ +import { Account, Chain, Client, Hex, Transport, WalletActions, getAbiItem } from "viem"; +import { waitForTransactionReceipt, writeContract } from "viem/actions"; +import { formatAbiItem, getAction } from "viem/utils"; +import { createBridge } from "./bridge"; +import { ReceiptSummary } from "./common"; + +export type WaitForStateChange = (hash: Hex) => Promise; + +export type ObserverOptions = { + explorerUrl?: string; + waitForStateChange?: WaitForStateChange; +}; + +export function observer({ + explorerUrl = "http://localhost:13690", + waitForStateChange, +}: ObserverOptions): ( + client: Client, +) => Pick, "writeContract"> { + const emit = createBridge({ url: `${explorerUrl}/internal/observer-relay` }); + + setInterval(() => { + emit("ping", {}); + }, 2000); + + return (client) => { + let counter = 0; + return { + async writeContract(args) { + const writeId = `${client.uid}-${++counter}`; + const write = getAction(client, writeContract, "writeContract")(args); + + // `writeContract` above will throw if this isn't present + const functionAbiItem = getAbiItem({ + abi: args.abi, + name: args.functionName, + args: args.args, + } as never)!; + + emit("write", { + writeId, + address: args.address, + functionSignature: formatAbiItem(functionAbiItem), + args: (args.args ?? []) as never, + }); + Promise.allSettled([write]).then(([result]) => { + emit("write:result", { ...result, writeId }); + }); + + write.then((hash) => { + const receipt = getAction(client, waitForTransactionReceipt, "waitForTransactionReceipt")({ hash }); + emit("waitForTransactionReceipt", { writeId }); + Promise.allSettled([receipt]).then(([result]) => { + emit("waitForTransactionReceipt:result", { ...result, writeId }); + }); + }); + + if (waitForStateChange) { + write.then((hash) => { + const receipt = waitForStateChange(hash); + emit("waitForStateChange", { writeId }); + Promise.allSettled([receipt]).then(([result]) => { + emit("waitForStateChange:result", { ...result, writeId }); + }); + }); + } + + return write; + }, + }; + }; +} diff --git a/packages/explorer/src/observer/messages.ts b/packages/explorer/src/observer/messages.ts new file mode 100644 index 0000000000..4c37d7c32d --- /dev/null +++ b/packages/explorer/src/observer/messages.ts @@ -0,0 +1,37 @@ +import { Address, Hash } from "viem"; +import { ReceiptSummary } from "./common"; + +export type Messages = { + ping: {}; + write: { + writeId: string; + address: Address; + functionSignature: string; + args: unknown[]; + }; + "write:result": PromiseSettledResult & { + writeId: string; + }; + waitForTransactionReceipt: { + writeId: string; + }; + "waitForTransactionReceipt:result": PromiseSettledResult & { + writeId: string; + }; + waitForStateChange: { + writeId: string; + }; + "waitForStateChange:result": PromiseSettledResult & { + writeId: string; + }; +}; + +export type MessageType = keyof Messages; +export type Message = { + [k in MessageType]: Omit & { type: k; time: number }; +}[messageType]; + +export type EmitMessage = ( + type: messageType, + data: Messages[messageType], +) => void; diff --git a/packages/explorer/src/observer/relay.ts b/packages/explorer/src/observer/relay.ts new file mode 100644 index 0000000000..8f6635d266 --- /dev/null +++ b/packages/explorer/src/observer/relay.ts @@ -0,0 +1,19 @@ +"use client"; + +import debug from "debug"; +import { isBridgeEnvelope } from "./bridge"; +import { relayChannelName } from "./common"; + +export function createRelay(): () => void { + const channel = new BroadcastChannel(relayChannelName); + function relay(event: MessageEvent) { + if (isBridgeEnvelope(event.data)) { + debug("relaying message from bridge"); + channel.postMessage(event.data.data); + } + } + window.addEventListener("message", relay); + return () => { + window.removeEventListener("message", relay); + }; +} diff --git a/packages/explorer/src/observer/store.ts b/packages/explorer/src/observer/store.ts new file mode 100644 index 0000000000..b0f065ff74 --- /dev/null +++ b/packages/explorer/src/observer/store.ts @@ -0,0 +1,44 @@ +"use client"; + +import { Address } from "viem"; +import { createStore } from "zustand/vanilla"; +import { relayChannelName } from "./common"; +import { debug } from "./debug"; +import { Message, MessageType } from "./messages"; + +export type Write = { + writeId: string; + address: Address; + functionSignature: string; + args: unknown[]; + time: number; + events: Message>[]; +}; + +export type State = { + writes: { + [id: string]: Write; + }; +}; + +export const store = createStore(() => ({ + writes: {}, +})); + +debug("listening for relayed messages", relayChannelName); +const channel = new BroadcastChannel(relayChannelName); +channel.addEventListener("message", ({ data }: MessageEvent) => { + if (data.type === "ping") return; + store.setState((state) => { + const write = data.type === "write" ? ({ ...data, events: [] } satisfies Write) : state.writes[data.writeId]; + return { + writes: { + ...state.writes, + [data.writeId]: { + ...write, + events: [...write.events, data], + }, + }, + }; + }); +}); diff --git a/packages/explorer/tsconfig.tsup.json b/packages/explorer/tsconfig.tsup.json new file mode 100644 index 0000000000..dc787c60b2 --- /dev/null +++ b/packages/explorer/tsconfig.tsup.json @@ -0,0 +1,3 @@ +{ + "extends": ["../../tsconfig.json"] +} diff --git a/packages/explorer/tsup.config.ts b/packages/explorer/tsup.config.ts index 6a842f457b..5a229b1e41 100644 --- a/packages/explorer/tsup.config.ts +++ b/packages/explorer/tsup.config.ts @@ -1,10 +1,12 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["bin/explorer.ts"], + tsconfig: "tsconfig.tsup.json", + entry: ["src/bin/explorer.ts", "src/exports/observer.ts"], target: "esnext", format: ["esm"], + dts: !process.env.TSUP_SKIP_DTS, sourcemap: true, clean: true, - minify: true, + minify: false, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d349fb7f4..324b8fbe3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -494,7 +494,7 @@ importers: version: 3.1.3(@types/react-dom@18.2.7)(@types/react@18.2.22)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@rainbow-me/rainbowkit': specifier: ^2.1.5 - version: 2.1.5(@tanstack/react-query@5.52.0(react@18.2.0))(@types/react@18.2.22)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(viem@2.19.8(bufferutil@4.0.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.7(@tanstack/query-core@5.52.0)(@tanstack/react-query@5.52.0(react@18.2.0))(@types/react@18.2.22)(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react-native@0.75.2(@babel/core@7.21.4)(@babel/preset-env@7.25.3(@babel/core@7.21.4))(@types/react@18.2.22)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.2.0)(typescript@5.4.2)(utf-8-validate@5.0.10))(react@18.2.0)(rollup@3.21.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(viem@2.19.8(bufferutil@4.0.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) + version: 2.1.6(@tanstack/react-query@5.52.0(react@18.2.0))(@types/react@18.2.22)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(viem@2.19.8(bufferutil@4.0.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.7(@tanstack/query-core@5.52.0)(@tanstack/react-query@5.52.0(react@18.2.0))(@types/react@18.2.22)(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react-native@0.75.2(@babel/core@7.21.4)(@babel/preset-env@7.25.3(@babel/core@7.21.4))(@types/react@18.2.22)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.2.0)(typescript@5.4.2)(utf-8-validate@5.0.10))(react@18.2.0)(rollup@3.21.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(viem@2.19.8(bufferutil@4.0.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) '@tanstack/react-query': specifier: ^5.51.3 version: 5.52.0(react@18.2.0) @@ -513,6 +513,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + debug: + specifier: ^4.3.4 + version: 4.3.4 lucide-react: specifier: ^0.408.0 version: 0.408.0(react@18.2.0) @@ -562,6 +565,9 @@ importers: '@types/better-sqlite3': specifier: ^7.6.4 version: 7.6.4 + '@types/debug': + specifier: ^4.1.7 + version: 4.1.7 '@types/minimist': specifier: ^1.2.5 version: 1.2.5 @@ -4036,8 +4042,8 @@ packages: '@types/react-dom': optional: true - '@rainbow-me/rainbowkit@2.1.5': - resolution: {integrity: sha512-Kdef0zu0bUlIOlbyyi3ukmQl7k8s3w0jTcWZxYTicZ/N4L35yX0vEzYgiG4u6OSXlbAQaC7VrkPKugPbSohnLQ==} + '@rainbow-me/rainbowkit@2.1.6': + resolution: {integrity: sha512-DCt6VYuPPxcPY6veuSOa784mHHHN0uSdDBTivdUBssmjTwHMmOrEs6kuKSYTPRu8EAwA1AvIc+ulSVnS022nbg==} engines: {node: '>=12.4'} peerDependencies: '@tanstack/react-query': '>=5.0.0' @@ -4823,17 +4829,17 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@vanilla-extract/css@1.14.0': - resolution: {integrity: sha512-rYfm7JciWZ8PFzBM/HDiE2GLnKI3xJ6/vdmVJ5BSgcCZ5CxRlM9Cjqclni9lGzF3eMOijnUhCd/KV8TOzyzbMA==} + '@vanilla-extract/css@1.15.5': + resolution: {integrity: sha512-N1nQebRWnXvlcmu9fXKVUs145EVwmWtMD95bpiEKtvehHDpUhmO1l2bauS7FGYKbi3dU1IurJbGpQhBclTr1ng==} - '@vanilla-extract/dynamic@2.1.0': - resolution: {integrity: sha512-8zl0IgBYRtgD1h+56Zu13wHTiMTJSVEa4F7RWX9vTB/5Xe2KtjoiqApy/szHPVFA56c+ex6A4GpCQjT1bKXbYw==} + '@vanilla-extract/dynamic@2.1.2': + resolution: {integrity: sha512-9BGMciD8rO1hdSPIAh1ntsG4LPD3IYKhywR7VOmmz9OO4Lx1hlwkSg3E6X07ujFx7YuBfx0GDQnApG9ESHvB2A==} '@vanilla-extract/private@1.0.6': resolution: {integrity: sha512-ytsG/JLweEjw7DBuZ/0JCN4WAQgM9erfSTdS1NQY778hFQSZ6cfCDEZZ0sgVm4k54uNz6ImKB33AYvSR//fjxw==} - '@vanilla-extract/sprinkles@1.6.1': - resolution: {integrity: sha512-N/RGKwGAAidBupZ436RpuweRQHEFGU+mvAqBo8PRMAjJEmHoPDttV8RObaMLrJHWLqvX+XUMinHUnD0hFRQISw==} + '@vanilla-extract/sprinkles@1.6.3': + resolution: {integrity: sha512-oCHlQeYOBIJIA2yWy2GnY5wE2A7hGHDyJplJo4lb+KEIBcJWRnDJDg8ywDwQS5VfWJrBBO3drzYZPFpWQjAMiQ==} peerDependencies: '@vanilla-extract/css': ^1.0.0 @@ -5650,10 +5656,6 @@ packages: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} engines: {node: '>=6'} - clsx@2.1.0: - resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} - engines: {node: '>=6'} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -5929,6 +5931,14 @@ packages: dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-eql@4.1.3: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} @@ -7966,6 +7976,9 @@ packages: resolution: {integrity: sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==} engines: {node: 14 || >=16.14} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -8577,9 +8590,6 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} - outdent@0.8.0: - resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} - p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -9071,6 +9081,11 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -9187,6 +9202,16 @@ packages: '@types/react': optional: true + react-remove-scroll@2.6.0: + resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-router-dom@6.11.0: resolution: {integrity: sha512-Q3mK1c/CYoF++J6ZINz7EZzwlgSOZK/kc7lxIA7PhtWhKju4KfF1WHqlx0kVCIFJAWztuYVpXZeljEbds8z4Og==} engines: {node: '>=14'} @@ -14183,22 +14208,23 @@ snapshots: '@types/react': 18.2.22 '@types/react-dom': 18.2.7 - '@rainbow-me/rainbowkit@2.1.5(@tanstack/react-query@5.52.0(react@18.2.0))(@types/react@18.2.22)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(viem@2.19.8(bufferutil@4.0.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.7(@tanstack/query-core@5.52.0)(@tanstack/react-query@5.52.0(react@18.2.0))(@types/react@18.2.22)(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react-native@0.75.2(@babel/core@7.21.4)(@babel/preset-env@7.25.3(@babel/core@7.21.4))(@types/react@18.2.22)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.2.0)(typescript@5.4.2)(utf-8-validate@5.0.10))(react@18.2.0)(rollup@3.21.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(viem@2.19.8(bufferutil@4.0.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8))': + '@rainbow-me/rainbowkit@2.1.6(@tanstack/react-query@5.52.0(react@18.2.0))(@types/react@18.2.22)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(viem@2.19.8(bufferutil@4.0.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.7(@tanstack/query-core@5.52.0)(@tanstack/react-query@5.52.0(react@18.2.0))(@types/react@18.2.22)(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react-native@0.75.2(@babel/core@7.21.4)(@babel/preset-env@7.25.3(@babel/core@7.21.4))(@types/react@18.2.22)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.2.0)(typescript@5.4.2)(utf-8-validate@5.0.10))(react@18.2.0)(rollup@3.21.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(viem@2.19.8(bufferutil@4.0.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8))': dependencies: '@tanstack/react-query': 5.52.0(react@18.2.0) - '@vanilla-extract/css': 1.14.0 - '@vanilla-extract/dynamic': 2.1.0 - '@vanilla-extract/sprinkles': 1.6.1(@vanilla-extract/css@1.14.0) - clsx: 2.1.0 - qrcode: 1.5.3 + '@vanilla-extract/css': 1.15.5 + '@vanilla-extract/dynamic': 2.1.2 + '@vanilla-extract/sprinkles': 1.6.3(@vanilla-extract/css@1.15.5) + clsx: 2.1.1 + qrcode: 1.5.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.7(@types/react@18.2.22)(react@18.2.0) + react-remove-scroll: 2.6.0(@types/react@18.2.22)(react@18.2.0) ua-parser-js: 1.0.38 viem: 2.19.8(bufferutil@4.0.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(zod@3.23.8) wagmi: 2.12.7(@tanstack/query-core@5.52.0)(@tanstack/react-query@5.52.0(react@18.2.0))(@types/react@18.2.22)(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react-native@0.75.2(@babel/core@7.21.4)(@babel/preset-env@7.25.3(@babel/core@7.21.4))(@types/react@18.2.22)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.2.0)(typescript@5.4.2)(utf-8-validate@5.0.10))(react@18.2.0)(rollup@3.21.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(viem@2.19.8(bufferutil@4.0.8)(typescript@5.4.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) transitivePeerDependencies: - '@types/react' + - babel-plugin-macros '@react-native-community/cli-clean@14.0.0': dependencies: @@ -15401,29 +15427,32 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vanilla-extract/css@1.14.0': + '@vanilla-extract/css@1.15.5': dependencies: '@emotion/hash': 0.9.2 '@vanilla-extract/private': 1.0.6 - chalk: 4.1.2 css-what: 6.1.0 cssesc: 3.0.0 csstype: 3.1.2 + dedent: 1.5.3 deep-object-diff: 1.1.9 deepmerge: 4.3.1 + lru-cache: 10.4.3 media-query-parser: 2.0.2 modern-ahocorasick: 1.0.1 - outdent: 0.8.0 + picocolors: 1.0.1 + transitivePeerDependencies: + - babel-plugin-macros - '@vanilla-extract/dynamic@2.1.0': + '@vanilla-extract/dynamic@2.1.2': dependencies: '@vanilla-extract/private': 1.0.6 '@vanilla-extract/private@1.0.6': {} - '@vanilla-extract/sprinkles@1.6.1(@vanilla-extract/css@1.14.0)': + '@vanilla-extract/sprinkles@1.6.3(@vanilla-extract/css@1.15.5)': dependencies: - '@vanilla-extract/css': 1.14.0 + '@vanilla-extract/css': 1.15.5 '@viem/anvil@0.0.7(bufferutil@4.0.8)(debug@4.3.4)(utf-8-validate@5.0.10)': dependencies: @@ -16622,8 +16651,6 @@ snapshots: clsx@2.0.0: {} - clsx@2.1.0: {} - clsx@2.1.1: {} co@4.6.0: {} @@ -16893,6 +16920,8 @@ snapshots: dedent@0.7.0: {} + dedent@1.5.3: {} + deep-eql@4.1.3: dependencies: type-detect: 4.0.8 @@ -19634,6 +19663,8 @@ snapshots: lru-cache@10.3.0: {} + lru-cache@10.4.3: {} + lru-cache@4.1.5: dependencies: pseudomap: 1.0.2 @@ -20375,8 +20406,6 @@ snapshots: outdent@0.5.0: {} - outdent@0.8.0: {} - p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -20801,6 +20830,12 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + query-string@7.1.3: dependencies: decode-uri-component: 0.2.2 @@ -20954,6 +20989,17 @@ snapshots: optionalDependencies: '@types/react': 18.2.22 + react-remove-scroll@2.6.0(@types/react@18.2.22)(react@18.2.0): + dependencies: + react: 18.2.0 + react-remove-scroll-bar: 2.3.6(@types/react@18.2.22)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.22)(react@18.2.0) + tslib: 2.6.2 + use-callback-ref: 1.3.2(@types/react@18.2.22)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.2.22)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.22 + react-router-dom@6.11.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@remix-run/router': 1.6.0 From 539a280d0df79dc3f1c95dbd800da3acb1978519 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Wed, 11 Sep 2024 06:18:56 -0700 Subject: [PATCH 2/6] Create smart-parents-refuse.md --- .changeset/smart-parents-refuse.md | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .changeset/smart-parents-refuse.md diff --git a/.changeset/smart-parents-refuse.md b/.changeset/smart-parents-refuse.md new file mode 100644 index 0000000000..25e26ba6be --- /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); + }, +}) +``` From 8b216949707db90851b010cd47c71c9206b3074a Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Wed, 11 Sep 2024 14:23:07 +0100 Subject: [PATCH 3/6] prettier --- .changeset/smart-parents-refuse.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/smart-parents-refuse.md b/.changeset/smart-parents-refuse.md index 25e26ba6be..bf160770f5 100644 --- a/.changeset/smart-parents-refuse.md +++ b/.changeset/smart-parents-refuse.md @@ -19,7 +19,7 @@ By default, the `observer` action assumes the World Explorer is running at `http ```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`. @@ -29,5 +29,5 @@ observer({ async waitForStateChange(hash) { return await waitForTransaction(hash); }, -}) +}); ``` From e983a8100b8181b1ee58f53535c5c05c8e1362d6 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 12 Sep 2024 07:48:21 -0700 Subject: [PATCH 4/6] Update packages/explorer/src/observer/bridge.ts Co-authored-by: Karolis Ramanauskas --- packages/explorer/src/observer/bridge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/explorer/src/observer/bridge.ts b/packages/explorer/src/observer/bridge.ts index 4ea01ba098..7be2ebb926 100644 --- a/packages/explorer/src/observer/bridge.ts +++ b/packages/explorer/src/observer/bridge.ts @@ -17,7 +17,7 @@ export function isBridgeEnvelope(input: unknown): input is BridgeEnvelope { } export function wrapMessage(data: unknown): BridgeEnvelope { - return { mud: "explorer/observer", data: data }; + return { mud: "explorer/observer", data }; } export type CreateBridgeOpts = { From 207ffa7f68a2a8c0183abc898de171d3188a2e0c Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 12 Sep 2024 07:48:27 -0700 Subject: [PATCH 5/6] Update packages/explorer/src/observer/relay.ts Co-authored-by: Karolis Ramanauskas --- packages/explorer/src/observer/relay.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/explorer/src/observer/relay.ts b/packages/explorer/src/observer/relay.ts index 8f6635d266..6c64292170 100644 --- a/packages/explorer/src/observer/relay.ts +++ b/packages/explorer/src/observer/relay.ts @@ -15,5 +15,6 @@ export function createRelay(): () => void { window.addEventListener("message", relay); return () => { window.removeEventListener("message", relay); + channel.close(); }; } From 7ef3dafb3301d6ec3d96b2687c4e0da539e931c2 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 12 Sep 2024 15:51:34 +0100 Subject: [PATCH 6/6] update explore links --- packages/explorer/src/app/(explorer)/error.tsx | 2 +- packages/explorer/src/app/(explorer)/not-found.tsx | 2 +- .../explorer/src/app/(explorer)/worlds/[worldAddress]/page.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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) {