diff --git a/.changeset/eighty-humans-divide.md b/.changeset/eighty-humans-divide.md new file mode 100644 index 0000000000..40dd5e3976 --- /dev/null +++ b/.changeset/eighty-humans-divide.md @@ -0,0 +1,14 @@ +--- +"@latticexyz/explorer": patch +--- + +Transactions in Observe tab are now populated with timing metrics when using the `observer` Viem decorator in local projects. + +You can wire up your local project to use transaction timings with: + +``` +import { observer } from "@latticexyz/explorer/observer"; + +// Extend the Viem client that is performing writes +walletClient.extend(observer()); +``` diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TimeAgo.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TimeAgo.tsx index ac6babbfc5..8c6f7c0003 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TimeAgo.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TimeAgo.tsx @@ -13,7 +13,7 @@ export function TimeAgo({ timestamp }: { timestamp: bigint }) { }, [timestamp]); return ( - + {ago} ); diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TimingRowExpanded.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TimingRowExpanded.tsx new file mode 100644 index 0000000000..67295392eb --- /dev/null +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TimingRowExpanded.tsx @@ -0,0 +1,37 @@ +import { Separator } from "../../../../../../components/ui/Separator"; +import { type Write } from "../../../../../../observer/store"; +import { cn } from "../../../../../../utils"; +import { useTimings } from "./useTimings"; + +export function TimingRowExpanded(write: Write) { + const timings = useTimings(write); + return ( + <> + +
+

Timing

+
+
+ {timings.map((timing) => ( + <> + {timing.label}: + + {timing.duration}ms + + ))} +
+
+
+ + ); +} diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TimingRowHeader.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TimingRowHeader.tsx new file mode 100644 index 0000000000..eda32a5d0c --- /dev/null +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TimingRowHeader.tsx @@ -0,0 +1,26 @@ +import { type Write } from "../../../../../../observer/store"; +import { cn } from "../../../../../../utils"; +import { useTimings } from "./useTimings"; + +export function TimingRowHeader(write: Write) { + const timings = useTimings(write); + return ( +
+ {timings.map((timing) => ( +
+ ))} +
+ ); +} diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionTableRow.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionTableRow.tsx index 1784f8f69d..9b7d4af1ef 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionTableRow.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionTableRow.tsx @@ -6,6 +6,7 @@ import { Skeleton } from "../../../../../../components/ui/Skeleton"; import { TableCell, TableRow } from "../../../../../../components/ui/Table"; import { cn } from "../../../../../../utils"; import { Confirmations } from "./Confirmations"; +import { TimingRowExpanded } from "./TimingRowExpanded"; import { columns } from "./TransactionsTable"; import { WatchedTransaction } from "./useTransactionWatcher"; @@ -78,7 +79,6 @@ export function TransactionTableRow({ row }: { row: Row }) {
-

Inputs

{Array.isArray(data.functionData?.args) && data.functionData?.args.length > 0 ? ( @@ -100,16 +100,11 @@ export function TransactionTableRow({ row }: { row: Row }) { {data.error ? ( <> -

Error

- {data.error ? ( -
- {data.error.message} -
- ) : ( -

No error

- )} +
+ {data.error.message} +
) : null} @@ -117,10 +112,9 @@ export function TransactionTableRow({ row }: { row: Row }) { {!data.error ? ( <> - -
-

Logs

- {Array.isArray(logs) && logs.length > 0 ? ( +
+

Logs

+ {Array.isArray(logs) && logs.length > 10 ? (
    {logs.map((log, idx) => { @@ -145,12 +139,16 @@ export function TransactionTableRow({ row }: { row: Row }) { })}
+ ) : status === "pending" ? ( + ) : (

No logs

)}
) : null} + + {data.write && } )} diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsTable.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsTable.tsx index 96e5ae58fb..c3b5eacea0 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsTable.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsTable.tsx @@ -10,6 +10,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ". import { TruncatedHex } from "../../../../../../components/ui/TruncatedHex"; import { BlockExplorerLink } from "./BlockExplorerLink"; import { TimeAgo } from "./TimeAgo"; +import { TimingRowHeader } from "./TimingRowHeader"; import { TransactionTableRow } from "./TransactionTableRow"; import { WatchedTransaction, useTransactionWatcher } from "./useTransactionWatcher"; @@ -81,8 +82,13 @@ export const columns = [ header: "Time", cell: (row) => { const timestamp = row.getValue(); - if (!timestamp) return ; - return ; + const write = row.row.original.write; + return ( + <> + {timestamp ? : } + {write && } + + ); }, }), ]; diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/Write.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/Write.tsx deleted file mode 100644 index daa8bdf225..0000000000 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/Write.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"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)/[chainName]/worlds/[worldAddress]/observe/Writes.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/Writes.tsx deleted file mode 100644 index 821e849b34..0000000000 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/Writes.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"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)/[chainName]/worlds/[worldAddress]/observe/useTimings.ts b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTimings.ts new file mode 100644 index 0000000000..5acd6882fd --- /dev/null +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTimings.ts @@ -0,0 +1,46 @@ +import { isDefined } from "@latticexyz/common/utils"; +import { type Write } from "../../../../../../observer/store"; + +const eventConfig = { + write: { priority: 1, label: "write" }, + "write:result": { priority: 2 }, + waitForTransaction: { priority: 3, label: "state update" }, + "waitForTransaction:result": { priority: 4 }, + waitForTransactionReceipt: { priority: 5, label: "transaction receipt" }, + "waitForTransactionReceipt:result": { priority: 6 }, +} as const; + +type EventType = keyof typeof eventConfig; + +export function useTimings({ time: start, events }: Write) { + const maxLen = Math.max(...events.map((event) => event.time - start)); + const sortedEvents = Object.values(events).sort((a, b) => { + const priorityA = eventConfig[a.type as EventType]?.priority; + const priorityB = eventConfig[b.type as EventType]?.priority; + return priorityA - priorityB; + }); + + return sortedEvents + .map((event) => { + const type = event.type as EventType; + if (type.endsWith(":result")) return; + + const writeResult = events.find((e) => e.type === `${type}:result`); + const endTime = writeResult?.time ?? event.time; + const duration = endTime - event.time; + const startOffset = event.time - start; + + const startPercentage = (startOffset / maxLen) * 100; + const widthPercentage = (duration / maxLen) * 100; + + const config = eventConfig[type]; + return { + type, + label: "label" in config ? config.label : type, + duration, + startPercentage, + widthPercentage, + }; + }) + .filter(isDefined); +} diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts index 3c89fcc87e..58d56c2a9e 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts @@ -17,7 +17,7 @@ import { getTransaction, simulateContract, waitForTransactionReceipt } from "wag import { useStore } from "zustand"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Message } from "../../../../../../observer/messages"; -import { Write, store } from "../../../../../../observer/store"; +import { type Write, store } from "../../../../../../observer/store"; import { useChain } from "../../../../hooks/useChain"; import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery"; @@ -135,14 +135,14 @@ export function useTransactionWatcher() { useEffect(() => { for (const write of Object.values(observerWrites)) { const hash = write.hash; - if (write.type === "waitForTransactionReceipt" && hash) { + if (write.type === "waitForTransactionReceipt" && hash && write.address === worldAddress) { const transaction = transactions.find((transaction) => transaction.hash === hash); if (!transaction) { handleTransaction(hash, BigInt(write.time) / 1000n); } } } - }, [handleTransaction, observerWrites, transactions]); + }, [handleTransaction, observerWrites, transactions, worldAddress]); useWatchBlocks({ onBlock(block) { @@ -159,6 +159,8 @@ export function useTransactionWatcher() { const mergedMap = new Map(); for (const write of Object.values(observerWrites)) { + if (write.address !== worldAddress) continue; + const parsedAbiItem = parseAbiItem(`function ${write.functionSignature}`) as AbiFunction; const writeResult = write.events.find((event): event is Message<"write:result"> => event.type === "write:result"); @@ -188,7 +190,7 @@ export function useTransactionWatcher() { } return Array.from(mergedMap.values()).sort((a, b) => Number(b.timestamp ?? 0n) - Number(a.timestamp ?? 0n)); - }, [transactions, observerWrites]); + }, [observerWrites, worldAddress, transactions]); return mergedTransactions; } diff --git a/packages/explorer/src/app/(explorer)/utils/timeAgo.ts b/packages/explorer/src/app/(explorer)/utils/timeAgo.ts index 1ed9496828..dc9f2bcac4 100644 --- a/packages/explorer/src/app/(explorer)/utils/timeAgo.ts +++ b/packages/explorer/src/app/(explorer)/utils/timeAgo.ts @@ -22,5 +22,5 @@ export function timeAgo(timestamp: bigint) { } } - return "just now"; + return "0s ago"; }