Skip to content

Commit

Permalink
feat(explorer): integrate rejected transactions (#3251)
Browse files Browse the repository at this point in the history
  • Loading branch information
karooolis authored Oct 4, 2024
1 parent 57cf61a commit 71eb348
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/neat-impalas-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/explorer": patch
---

Observe tab is now populated by rejected transactions coming from the `observer` transport wrapper.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export function Confirmations({ hash }: { hash?: Hex }) {
chainId,
query: {
refetchInterval: 1000,
enabled: !!hash,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,29 @@ import { Confirmations } from "./Confirmations";
import { columns } from "./TransactionsTable";
import { WatchedTransaction } from "./useTransactionWatcher";

function TranctionTableRowDataCell({ label, children }: { label: string; children: React.ReactNode }) {
function TransactionTableRowDataCell({
label,
status,
children,
}: {
label: string;
status: WatchedTransaction["status"];
children: React.ReactNode;
}) {
return (
<div>
<h3 className="text-2xs font-bold uppercase text-white/60">{label}</h3>
<p className="pt-1 text-xs uppercase">{children ?? <Skeleton className="h-4 w-[100px]" />}</p>
<p className="pt-1 text-xs uppercase">
{children ??
(status === "rejected" ? <span className="text-white/60">N/A</span> : <Skeleton className="h-4 w-[100px]" />)}
</p>
</div>
);
}

export function TransactionTableRow({ row }: { row: Row<WatchedTransaction> }) {
const data = row?.original;
const status = data.status;
const logs = data?.logs;
const receipt = data?.receipt;

Expand Down Expand Up @@ -48,25 +60,27 @@ export function TransactionTableRow({ row }: { row: Row<WatchedTransaction> }) {
{data && (
<>
<div className="grid grid-cols-2 gap-x-2 gap-y-5 sm:grid-cols-4 md:grid-cols-5">
<TranctionTableRowDataCell label="Confirmations">
<Confirmations hash={data.transaction?.hash} />
</TranctionTableRowDataCell>
<TranctionTableRowDataCell label="Tx value">
{data.transaction?.value !== undefined ? `${formatEther(data.transaction.value)} ETH` : null}
</TranctionTableRowDataCell>
<TranctionTableRowDataCell label="Gas used">{receipt?.gasUsed.toString()}</TranctionTableRowDataCell>
<TranctionTableRowDataCell label="Gas price">
<TransactionTableRowDataCell label="Confirmations" status={status}>
{status !== "rejected" ? <Confirmations hash={data.transaction?.hash} /> : null}
</TransactionTableRowDataCell>
<TransactionTableRowDataCell label="Tx value" status={status}>
{data.value ? formatEther(data.value) : 0} ETH
</TransactionTableRowDataCell>
<TransactionTableRowDataCell label="Gas used" status={status}>
{receipt?.gasUsed.toString()}
</TransactionTableRowDataCell>
<TransactionTableRowDataCell label="Gas price" status={status}>
{receipt?.effectiveGasPrice.toString()}
</TranctionTableRowDataCell>
<TranctionTableRowDataCell label="Tx cost">
</TransactionTableRowDataCell>
<TransactionTableRowDataCell label="Tx cost" status={status}>
{receipt ? `${formatEther(receipt.gasUsed * receipt.effectiveGasPrice)} ETH` : null}
</TranctionTableRowDataCell>
</TransactionTableRowDataCell>
</div>

<Separator className="my-5" />

<div className="flex items-start gap-x-4">
<h3 className="w-[45px] text-2xs font-bold uppercase">Inputs</h3>
<h3 className="w-[45px] flex-shrink-0 text-2xs font-bold uppercase">Inputs</h3>
{Array.isArray(data.functionData?.args) && data.functionData?.args.length > 0 ? (
<div className="flex-grow border border-white/20 p-2">
{data.functionData?.args?.map((arg, idx) => (
Expand All @@ -85,7 +99,7 @@ export function TransactionTableRow({ row }: { row: Row<WatchedTransaction> }) {
<>
<Separator className="my-5" />
<div className="flex items-start gap-x-4">
<h3 className="w-[45px] text-2xs font-bold uppercase">Error</h3>
<h3 className="w-[45px] flex-shrink-0 text-2xs font-bold uppercase">Error</h3>
{data.error ? (
<div className="flex-grow whitespace-pre-wrap border border-red-500 p-2 font-mono text-xs">
{data.error.message}
Expand All @@ -100,7 +114,7 @@ export function TransactionTableRow({ row }: { row: Row<WatchedTransaction> }) {
<Separator className="my-5" />

<div className="flex items-start gap-x-4 pb-2">
<h3 className="inline-block w-[45px] pb-2 text-2xs font-bold uppercase">Logs</h3>
<h3 className="inline-block w-[45px] flex-shrink-0 pb-2 text-2xs font-bold uppercase">Logs</h3>
{Array.isArray(logs) && logs.length > 0 ? (
<div className="flex-grow break-all border border-white/20 p-2 pb-3">
<ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export const columns = [
columnHelper.accessor("receipt.blockNumber", {
header: "Block",
cell: (row) => {
const status = row.row.original.status;
if (status === "rejected") return <span className="text-white/60">N/A</span>;

const blockNumber = row.getValue();
if (!blockNumber) return <Skeleton className="h-4 w-full" />;
return (
Expand All @@ -28,7 +31,7 @@ export const columns = [
);
},
}),
columnHelper.accessor("transaction.from", {
columnHelper.accessor("from", {
header: "From",
cell: (row) => {
const from = row.getValue();
Expand All @@ -49,16 +52,19 @@ export const columns = [
return (
<div className="flex items-center">
<Badge variant="secondary">{functionName}</Badge>
{status === "pending" && <CheckCheckIcon className="ml-2 h-4 w-4 text-white/50" />}
{status === "pending" && <CheckCheckIcon className="ml-2 h-4 w-4 text-white/60" />}
{status === "success" && <CheckCheckIcon className="ml-2 h-4 w-4 text-green-400" />}
{status === "reverted" && <XIcon className="ml-2 h-4 w-4 text-red-400" />}
{(status === "reverted" || status === "rejected") && <XIcon className="ml-2 h-4 w-4 text-red-400" />}
</div>
);
},
}),
columnHelper.accessor("hash", {
header: "Tx hash",
cell: (row) => {
const status = row.row.original.status;
if (status === "rejected") return <span className="text-white/60">N/A</span>;

const hash = row.getValue();
if (!hash) return <Skeleton className="h-4 w-full" />;
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useParams } from "next/navigation";
import {
AbiFunction,
Address,
BaseError,
DecodeFunctionDataReturnType,
Hex,
Expand All @@ -15,17 +16,21 @@ import { useConfig, useWatchBlocks } from "wagmi";
import { useStore } from "zustand";
import { useCallback, useEffect, useMemo, useState } from "react";
import { getTransaction, simulateContract, waitForTransactionReceipt } from "@wagmi/core";
import { Message } from "../../../../../../observer/messages";
import { Write, store } from "../../../../../../observer/store";
import { useChain } from "../../../../hooks/useChain";
import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery";

export type WatchedTransaction = {
hash?: Hex;
writeId?: string;
from?: Address;
timestamp?: bigint;
transaction?: Transaction;
functionData?: DecodeFunctionDataReturnType;
value?: bigint;
receipt?: TransactionReceipt;
status: "pending" | "success" | "reverted" | "unknown";
status: "pending" | "success" | "reverted" | "rejected" | "unknown";
write?: Write;
logs?: Log[];
error?: BaseError;
Expand Down Expand Up @@ -63,13 +68,15 @@ export function useTransactionWatcher() {
setTransactions((prevTransactions) => [
{
hash,
from: transaction.from,
timestamp,
transaction,
status: "pending",
functionData: {
functionName,
args,
},
value: transaction.value,
},
...prevTransactions,
]);
Expand Down Expand Up @@ -114,7 +121,7 @@ export function useTransactionWatcher() {
receipt,
logs,
status,
error: transactionError,
error: transactionError as BaseError,
}
: transaction,
),
Expand Down Expand Up @@ -147,19 +154,22 @@ export function useTransactionWatcher() {
});

const mergedTransactions = useMemo((): WatchedTransaction[] => {
const mergedMap = new Map<Hex | undefined, WatchedTransaction>();
const mergedMap = new Map<string | undefined, WatchedTransaction>();

for (const write of Object.values(observerWrites)) {
const parsedAbiItem = parseAbiItem(`function ${write.functionSignature}`) as AbiFunction;
const functionData = {
functionName: parsedAbiItem.name,
args: write.args,
};
const writeResult = write.events.find((event): event is Message<"write:result"> => event.type === "write:result");

mergedMap.set(write.hash, {
status: "pending",
mergedMap.set(write.hash || write.writeId, {
from: write.from,
status: writeResult?.status === "rejected" ? "rejected" : "pending",
timestamp: BigInt(write.time) / 1000n,
functionData,
functionData: {
functionName: parsedAbiItem.name,
args: write.args,
},
value: write.value,
error: writeResult && "reason" in writeResult ? (writeResult.reason as BaseError) : undefined,
write,
});
}
Expand Down
2 changes: 2 additions & 0 deletions packages/explorer/src/observer/decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ export function observer({ explorerUrl = "http://localhost:13690", waitForTransa
emit("write", {
writeId,
address: args.address,
from: client.account!.address,
functionSignature: formatAbiItem(functionAbiItem),
args: (args.args ?? []) as never,
value: args.value,
});
Promise.allSettled([write]).then(([result]) => {
emit("write:result", { ...result, writeId });
Expand Down
6 changes: 3 additions & 3 deletions packages/explorer/src/observer/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ export type Messages = {
write: {
writeId: string;
address: Address;
from: Address;
functionSignature: string;
args: unknown[];
value?: bigint;
};
"write:result": PromiseSettledResult<Hash> & {
writeId: string;
};
"write:result": PromiseSettledResult<Hash> & { writeId: string };
waitForTransactionReceipt: {
writeId: string;
hash: Hash;
Expand Down
3 changes: 3 additions & 0 deletions packages/explorer/src/observer/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ export type Write = {
writeId: string;
type: MessageType;
hash?: Hex;
from?: Address;
address: Address;
functionSignature: string;
args: unknown[];
value?: bigint;
time: number;
events: Message<Exclude<MessageType, "ping">>[];
error?: Error;
};

export type State = {
Expand Down

0 comments on commit 71eb348

Please sign in to comment.