Skip to content

Commit

Permalink
feat(explorer): show transactions (#3062)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Ingersoll <[email protected]>
  • Loading branch information
karooolis and holic authored Oct 3, 2024
1 parent 61930ee commit bbd5e31
Show file tree
Hide file tree
Showing 24 changed files with 675 additions and 108 deletions.
5 changes: 5 additions & 0 deletions .changeset/blue-starfishes-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/explorer": patch
---

Observe tab is now populated by transactions flowing through the world, in addition to local transactions when using the `observer` transport wrapper.
Original file line number Diff line number Diff line change
Expand Up @@ -116,38 +116,42 @@ export function TablesViewer({
</div>
)}
{!isLoading && (
<Table>
<TableHeader>
{reactTable.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{reactTable.getRowModel().rows?.length ? (
reactTable.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
<div className="relative w-full overflow-auto">
<Table>
<TableHeader>
{reactTable.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={tableColumns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
))}
</TableHeader>
<TableBody>
{reactTable.getRowModel().rows?.length ? (
reactTable.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={tableColumns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)}
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export function Form() {
const { data, isFetched } = useWorldAbiQuery();
const [filterValue, setFilterValue] = useState("");
const deferredFilterValue = useDeferredValue(filterValue);
const filteredFunctions = data?.abi?.filter((item) =>
item.name.toLowerCase().includes(deferredFilterValue.toLowerCase()),
const filteredFunctions = data?.abi?.filter(
(item) => item.type === "function" && item.name.toLowerCase().includes(deferredFilterValue.toLowerCase()),
);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Hex } from "viem";
import { useChain } from "../../../../hooks/useChain";
import { blockExplorerTransactionUrl } from "../../../../utils/blockExplorerTransactionUrl";

export function BlockExplorerLink({ hash, children }: { hash?: Hex; children: React.ReactNode }) {
const { id: chainId } = useChain();
const url = blockExplorerTransactionUrl({ chainId, hash });

if (!url) return children;
return (
<a href={url} target="_blank" rel="noopener noreferrer" className="flex hover:underline">
{children}
</a>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Hex } from "viem";
import { useTransactionConfirmations } from "wagmi";
import { Skeleton } from "../../../../../../components/ui/Skeleton";
import { useChain } from "../../../../hooks/useChain";

export function Confirmations({ hash }: { hash?: Hex }) {
const { id: chainId } = useChain();
const { data: confirmations } = useTransactionConfirmations({
hash,
chainId,
query: {
refetchInterval: 1000,
},
});

if (!confirmations) return <Skeleton className="h-4 w-[50px]" />;
return (
<span className="flex items-center text-xs font-extrabold text-green-600">
<span className="mr-2 inline-block h-2 w-2 animate-pulse rounded-full bg-success"></span>
<span className="opacity-70">{confirmations.toString()}</span>
</span>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect, useState } from "react";
import { timeAgo } from "../../../../utils/timeAgo";

export function TimeAgo({ timestamp }: { timestamp: bigint }) {
const [ago, setAgo] = useState(() => timeAgo(timestamp));

useEffect(() => {
const timer = setInterval(() => {
setAgo(timeAgo(timestamp));
}, 1000);

return () => clearInterval(timer);
}, [timestamp]);

return (
<span className="text-white/60" title={new Date(Number(timestamp) * 1000).toISOString()}>
{ago}
</span>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { formatEther } from "viem";
import { Row, flexRender } from "@tanstack/react-table";
import { Separator } from "../../../../../../components/ui/Separator";
import { Skeleton } from "../../../../../../components/ui/Skeleton";
import { TableCell, TableRow } from "../../../../../../components/ui/Table";
import { cn } from "../../../../../../utils";
import { Confirmations } from "./Confirmations";
import { columns } from "./TransactionsTable";
import { WatchedTransaction } from "./useTransactionWatcher";

function TranctionTableRowDataCell({ label, children }: { label: string; 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>
</div>
);
}

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

return (
<>
<TableRow
className={cn("relative cursor-pointer", {
"bg-muted/50": row.getIsExpanded(),
})}
onClick={() => row.toggleExpanded()}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}

{row.getIsExpanded() ? (
<ChevronUpIcon className="absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 text-white/60" />
) : (
<ChevronDownIcon className="absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 text-white/60" />
)}
</TableRow>

{row.getIsExpanded() && (
<TableRow className="border-b-white/20 bg-muted/50 hover:bg-muted/50">
<TableCell colSpan={columns.length}>
{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">
{receipt?.effectiveGasPrice.toString()}
</TranctionTableRowDataCell>
<TranctionTableRowDataCell label="Tx cost">
{receipt ? `${formatEther(receipt.gasUsed * receipt.effectiveGasPrice)} ETH` : null}
</TranctionTableRowDataCell>
</div>

<Separator className="my-5" />

<div className="flex items-start gap-x-4">
<h3 className="w-[45px] 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) => (
<div key={idx} className="flex">
<span className="flex-shrink-0 text-xs text-white/60">arg {idx + 1}:</span>
<span className="ml-2 text-xs">{String(arg)}</span>
</div>
))}
</div>
) : (
<p className="text-2xs uppercase text-white/60">No inputs</p>
)}
</div>

{data.error ? (
<>
<Separator className="my-5" />
<div className="flex items-start gap-x-4">
<h3 className="w-[45px] 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}
</div>
) : (
<p className="text-2xs uppercase text-white/60">No error</p>
)}
</div>
</>
) : null}

<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>
{Array.isArray(logs) && logs.length > 0 ? (
<div className="flex-grow break-all border border-white/20 p-2 pb-3">
<ul>
{logs.map((log, idx) => {
const eventName = "eventName" in log ? log.eventName : null;
const args = "args" in log ? (log.args as Record<string, unknown>) : null;
return (
<li key={idx}>
{Boolean(eventName) && <span className="text-xs">{eventName?.toString()}:</span>}
{args && (
<ul className="list-inside">
{Object.entries(args).map(([key, value]) => (
<li key={key} className="mt-1 flex">
<span className="flex-shrink-0 text-xs text-white/60">{key}: </span>
<span className="ml-2 text-xs">{value as never}</span>
</li>
))}
</ul>
)}
{idx < logs.length - 1 && <Separator className="my-4" />}
</li>
);
})}
</ul>
</div>
) : (
<p className="text-2xs uppercase text-white/60">No logs</p>
)}
</div>
</>
)}
</TableCell>
</TableRow>
)}
</>
);
}
Loading

0 comments on commit bbd5e31

Please sign in to comment.