Skip to content

Commit

Permalink
fix(explorer): various fixes (#3195)
Browse files Browse the repository at this point in the history
  • Loading branch information
holic authored Sep 19, 2024
1 parent 068c719 commit 55ae822
Show file tree
Hide file tree
Showing 14 changed files with 136 additions and 112 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilly-readers-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/explorer": patch
---

Refactored `observer` initialization to reuse bridge iframes with the same `url`.
5 changes: 5 additions & 0 deletions .changeset/mean-radios-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/explorer": patch
---

Fixed favicon paths and fixed a few issues where we were incorrectly redirecting based on the chain name or ID.
10 changes: 10 additions & 0 deletions .changeset/unlucky-icons-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@latticexyz/explorer": patch
---

Fixed an issue where the `observer` Viem client decorator required an empty object arg when no options are used.

```diff
-client.extend(observer({}));
+client.extend(observer());
```
7 changes: 6 additions & 1 deletion packages/explorer/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ export default function config() {
},
redirects: async () => {
return [
{
source: "/worlds/:path*",
destination: "/anvil/worlds/:path*",
permanent: false,
},
{
source: "/:chainName/worlds/:worldAddress/explorer",
destination: "/:chainName/worlds/:worldAddress/explore",
permanent: true,
permanent: false,
},
];
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import { ExternalLink, RefreshCwIcon } from "lucide-react";
import Link from "next/link";
import { Button } from "../components/ui/Button";
import { useWorldUrl } from "../hooks/useWorldUrl";
import { Button } from "../../components/ui/Button";
import { useWorldUrl } from "../../hooks/useWorldUrl";

type Props = {
error: Error & { digest?: string };
Expand All @@ -14,7 +14,7 @@ export default function Error({ reset, error }: Props) {
const getUrl = useWorldUrl();
return (
<main className="px-6 py-24 text-center">
<p className="text-3xl font-semibold text-orange-600">400</p>
<p className="text-3xl font-semibold text-orange-600">500</p>
<h1 className="mt-4 text-3xl font-bold tracking-tight text-white sm:text-5xl">Something went wrong :(</h1>

{error.message && (
Expand Down
File renamed without changes.
File renamed without changes
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ const jetbrains = JetBrains_Mono({
export const metadata: Metadata = {
title: "World Explorer",
description: "World Explorer is a tool for visually exploring and manipulating the state of worlds",
icons: {
icon: "/favicon.svg",
},
};

export default function RootLayout({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { Button } from "../components/ui/Button";
import { useWorldUrl } from "../hooks/useWorldUrl";
import { Button } from "../../components/ui/Button";
import { useWorldUrl } from "../../hooks/useWorldUrl";

export default function NotFound() {
const getUrl = useWorldUrl();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { redirect } from "next/navigation";
import { supportedChainsById, validateChainId } from "../common";
import { chainIdToName, validateChainId } from "../../common";

export const dynamic = "force-dynamic";

export default function IndexPage() {
const chainId = Number(process.env.CHAIN_ID);
validateChainId(chainId);

const chainName = supportedChainsById[chainId];
const chainName = chainIdToName[chainId] ?? "anvil";
return redirect(`/${chainName}/worlds`);
}
8 changes: 4 additions & 4 deletions packages/explorer/src/app/api/world/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { getBlockNumber, getLogs } from "viem/actions";
import { helloStoreEvent } from "@latticexyz/store";
import { helloWorldEvent } from "@latticexyz/world";
import { getWorldAbi } from "@latticexyz/world/internal";
import { SupportedChainIds, supportedChainsById, validateChainId } from "../../../common";
import { supportedChainId, supportedChains, validateChainId } from "../../../common";

export const dynamic = "force-dynamic";

async function getClient(chainId: SupportedChainIds) {
const chain = supportedChainsById[chainId];
async function getClient(chainId: supportedChainId) {
const chain = Object.values(supportedChains).find((c) => c.id === chainId);
const client = createWalletClient({
chain,
transport: http(),
Expand All @@ -17,7 +17,7 @@ async function getClient(chainId: SupportedChainIds) {
return client;
}

async function getParameters(chainId: SupportedChainIds, worldAddress: Address) {
async function getParameters(chainId: supportedChainId, worldAddress: Address) {
const client = await getClient(chainId);
const toBlock = await getBlockNumber(client);
const logs = await getLogs(client, {
Expand Down
24 changes: 13 additions & 11 deletions packages/explorer/src/common.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { anvil, garnet, redstone } from "viem/chains";

export const supportedChains = { anvil, garnet, redstone } as const;
export const supportedChainsById = Object.fromEntries(
Object.entries(supportedChains).map(([, chain]) => [chain.id, chain]),
);
export type supportedChains = typeof supportedChains;

export type supportedChainName = keyof supportedChains;
export type supportedChainId = supportedChains[supportedChainName]["id"];

export type SupportedChainIds = (typeof supportedChains)[keyof typeof supportedChains]["id"];
export type SupportedChainNames = keyof typeof supportedChains;
export const chainIdToName = Object.fromEntries(
Object.entries(supportedChains).map(([chainName, chain]) => [chain.id, chainName]),
);

export function validateChainId(chainId: number): asserts chainId is SupportedChainIds {
if (!(chainId in supportedChainsById)) {
throw new Error(`Invalid chain id. Supported chains are: ${Object.keys(supportedChainsById).join(", ")}.`);
export function validateChainId(chainId: unknown): asserts chainId is supportedChainId {
if (!(typeof chainId === "number" && chainId in chainIdToName)) {
throw new Error(`Invalid chain ID. Supported chains are: ${Object.keys(chainIdToName).join(", ")}.`);
}
}

export function validateChainName(name: string | string[] | undefined): asserts name is SupportedChainNames {
if (Array.isArray(name) || typeof name !== "string" || !(name in supportedChains)) {
throw new Error(`Invalid chain name. Supported chains are: ${Object.keys(supportedChainsById).join(", ")}.`);
export function validateChainName(name: unknown): asserts name is supportedChainName {
if (!(typeof name === "string" && name in supportedChains)) {
throw new Error(`Invalid chain name. Supported chains are: ${Object.keys(supportedChains).join(", ")}.`);
}
}
95 changes: 50 additions & 45 deletions packages/explorer/src/observer/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,56 +26,61 @@ export type CreateBridgeOpts = {
};

export function createBridge({ url, timeout = 10_000 }: CreateBridgeOpts): EmitMessage {
const emit = Promise.withResolvers<EmitMessage>();
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";
const bridge = new Promise<HTMLIFrameElement>((resolve, reject) => {
const iframe =
Array.from(document.querySelectorAll("iframe[data-bridge][src]"))
.filter((el): el is HTMLIFrameElement => true)
.find((el) => el.src === url) ?? document.createElement("iframe");

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 },
);
if (iframe.dataset.bridge === "ready") {
debug("reusing observer iframe", iframe.src);
return resolve(iframe);
}

// 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);
Promise.race([
new Promise<void>((resolve, reject) => {
iframe.addEventListener("load", () => resolve(), { once: true });
iframe.addEventListener("error", (error) => reject(error), { once: true });
}),
wait(timeout).then(() => {
throw new Error("Timed out waiting for observer iframe to load.");
}),
]).then(
() => {
debug("observer iframe ready", iframe.src);
iframe.dataset.bridge = "ready";
resolve(iframe);
},
(error) => {
debug("observer iframe error", error);
iframe.remove();
reject(error);
},
);

emit.promise.catch(() => {
iframe.remove();
if (iframe.dataset.bridge !== "loading") {
iframe.tabIndex = -1;
iframe.ariaHidden = "true";
iframe.style.position = "absolute";
iframe.style.border = "0";
iframe.style.width = "0";
iframe.style.height = "0";
iframe.dataset.bridge = "loading";
iframe.src = url;
debug("mounting observer iframe", url);
parent.document.body.appendChild(iframe);
}
});

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),
return (type, data) => {
debug("got message for bridge", type, data);
bridge.then(
(iframe) => {
debug("posting message to bridge", type, data);
const message = wrapMessage({ type, time: Date.now(), ...data });
iframe.contentWindow!.postMessage(message, "*");
},
(error) => debug("could not deliver message", type, data, error),
);
};
}
77 changes: 36 additions & 41 deletions packages/explorer/src/observer/decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,62 +11,57 @@ export type ObserverOptions = {
waitForStateChange?: WaitForStateChange;
};

let writeCounter = 0;

export function observer<transport extends Transport, chain extends Chain, account extends Account>({
explorerUrl = "http://localhost:13690",
waitForStateChange,
}: ObserverOptions): (
}: ObserverOptions = {}): (
client: Client<transport, chain, account>,
) => Pick<WalletActions<chain, account>, "writeContract"> {
const emit = createBridge({ url: `${explorerUrl}/internal/observer-relay` });

setInterval(() => {
emit("ping", {});
}, 2000);
return (client) => ({
async writeContract(args) {
const writeId = `${client.uid}-${++writeCounter}`;
const write = getAction(client, writeContract, "writeContract")(args);

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)!;

// `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 });
});

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 = getAction(client, waitForTransactionReceipt, "waitForTransactionReceipt")({ hash });
emit("waitForTransactionReceipt", { writeId });
const receipt = waitForStateChange(hash);
emit("waitForStateChange", { writeId });
Promise.allSettled([receipt]).then(([result]) => {
emit("waitForTransactionReceipt:result", { ...result, writeId });
emit("waitForStateChange: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;
},
};
};
return write;
},
});
}

0 comments on commit 55ae822

Please sign in to comment.