Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(explorer): abi fetch loaders #3079

Merged
merged 4 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/explorer/src/app/api/world/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ async function getParameters(worldAddress: Address) {
fromBlock: "earliest",
toBlock,
});
const fromBlock = logs[0].blockNumber;
const fromBlock = logs[0]?.blockNumber ?? 0n;
// world is considered loaded when both events are emitted
const isWorldDeployed = logs.length === 2;

return { fromBlock, toBlock };
return { fromBlock, toBlock, isWorldDeployed };
}

export async function GET(req: Request) {
Expand All @@ -42,7 +44,7 @@ export async function GET(req: Request) {

try {
const client = await getClient();
const { fromBlock, toBlock } = await getParameters(worldAddress);
const { fromBlock, toBlock, isWorldDeployed } = await getParameters(worldAddress);
const worldAbiResponse = await getWorldAbi({
client,
worldAddress,
Expand All @@ -53,7 +55,7 @@ export async function GET(req: Request) {
.filter((entry): entry is AbiFunction => entry.type === "function")
.sort((a, b) => a.name.localeCompare(b.name));

return Response.json({ abi });
return Response.json({ abi, isWorldDeployed });
} catch (error: unknown) {
if (error instanceof Error) {
return Response.json({ error: error.message }, { status: 400 });
Expand Down
2 changes: 0 additions & 2 deletions packages/explorer/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Inter, JetBrains_Mono } from "next/font/google";
import { Toaster } from "sonner";
import { Theme } from "@radix-ui/themes";
import "@radix-ui/themes/styles.css";
import { Navigation } from "../components/Navigation";
import { Providers } from "./Providers";
import "./globals.css";

Expand Down Expand Up @@ -39,7 +38,6 @@ export default function RootLayout({
fontFamily: "var(--font-jetbrains-mono)",
}}
>
<Navigation />
{children}
</div>
<Toaster richColors />
Expand Down
47 changes: 33 additions & 14 deletions packages/explorer/src/app/worlds/[worldAddress]/interact/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,25 @@ import { Coins, Eye, Send } from "lucide-react";
import { AbiFunction } from "viem";
import { useDeferredValue, useState } from "react";
import { Input } from "../../../../components/ui/Input";
import { Separator } from "../../../../components/ui/Separator";
import { Skeleton } from "../../../../components/ui/Skeleton";
import { useHashState } from "../../../../hooks/useHashState";
import { cn } from "../../../../lib/utils";
import { useAbiQuery } from "../../../../queries/useAbiQuery";
import { FunctionField } from "./FunctionField";

type Props = {
abi: AbiFunction[];
};

export function Form({ abi }: Props) {
export function Form() {
const [hash] = useHashState();
const { data, isFetched } = useAbiQuery();
const [filterValue, setFilterValue] = useState("");
const deferredFilterValue = useDeferredValue(filterValue);
const filteredFunctions = abi.filter((item) => item.name.toLowerCase().includes(deferredFilterValue.toLowerCase()));
const filteredFunctions = data?.abi?.filter((item) =>
item.name.toLowerCase().includes(deferredFilterValue.toLowerCase()),
);

return (
<div className="flex">
<div className="w-[350px]">
<div className="flex min-h-full">
<div className="w-[320px] flex-shrink-0">
<div className="sticky top-2 pr-4">
<h4 className="py-4 text-xs font-semibold uppercase opacity-70">Jump to:</h4>

Expand All @@ -39,11 +41,16 @@ export function Form({ abi }: Props) {
maxHeight: "calc(100vh - 160px)",
}}
>
{filteredFunctions.map((abi, index) => {
if ((abi as AbiFunction).type !== "function") {
return null;
}
{!isFetched &&
Array.from({ length: 10 }).map((_, index) => {
return (
<li key={index} className="pt-2">
<Skeleton className="h-[25px]" />
</li>
);
})}

{filteredFunctions?.map((abi, index) => {
return (
<li key={index}>
<a
Expand Down Expand Up @@ -71,8 +78,20 @@ export function Form({ abi }: Props) {
</div>
</div>

<div className="border-l pl-4">
{filteredFunctions.map((abi) => {
<div className="min-h-full w-full border-l pl-4">
{!isFetched && (
<>
<Skeleton className="h-[100px]" />
<Separator className="my-4" />
<Skeleton className="h-[100px]" />
<Separator className="my-4" />
<Skeleton className="h-[100px]" />
<Separator className="my-4" />
<Skeleton className="h-[100px]" />
</>
)}

{filteredFunctions?.map((abi) => {
return <FunctionField key={JSON.stringify(abi)} abi={abi as AbiFunction} />;
})}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,5 @@
import { headers } from "next/headers";
import { Hex } from "viem";
import { Form } from "./Form";

async function getABI(worldAddress: Hex) {
const headersList = headers();
const protocol = headersList.get("x-forwarded-proto");
const host = headersList.get("host");

const res = await fetch(`${protocol}://${host}/api/world?${new URLSearchParams({ address: worldAddress })}`);
const data = await res.json();

if (!res.ok) {
throw new Error(data.error);
}

return data;
}

export default async function InteractPage({ params }: { params: { worldAddress: Hex } }) {
const { worldAddress } = params;
const data = await getABI(worldAddress);
return <Form abi={data.abi} />;
export default async function InteractPage() {
return <Form />;
}
12 changes: 12 additions & 0 deletions packages/explorer/src/app/worlds/[worldAddress]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";

import { Navigation } from "../../../components/Navigation";

export default function WorldLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<Navigation />
{children}
</div>
);
}
29 changes: 15 additions & 14 deletions packages/explorer/src/components/LatestBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import { useBlockNumber } from "wagmi";
import { Skeleton } from "./ui/Skeleton";

export function LatestBlock() {
const { data: block } = useBlockNumber({
watch: true,
});

if (block === undefined || block === 0n) {
return;
}

return (
<div className="inline-block">
<div className="flex items-center justify-between text-xs font-extrabold text-green-600">
<span
className="mr-2 inline-block h-[8px] w-[8px] animate-pulse rounded-full"
style={{
background: "rgb(64, 182, 107)",
}}
></span>
<span className="opacity-70">{block.toString()}</span>
</div>
<div className="inline-block w-[50px]">
{block ? (
<div className="flex items-center justify-end text-xs font-extrabold text-green-600">
<span
className="mr-2 inline-block h-[8px] w-[8px] animate-pulse rounded-full"
style={{
background: "rgb(64, 182, 107)",
}}
></span>
<span className="opacity-70">{block.toString()}</span>
</div>
) : (
<Skeleton className="h-[10px]" />
)}
</div>
);
}
9 changes: 9 additions & 0 deletions packages/explorer/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
"use client";

import { Loader } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { LatestBlock } from "../components/LatestBlock";
import { Separator } from "../components/ui/Separator";
import { useWorldUrl } from "../hooks/useWorldUrl";
import { cn } from "../lib/utils";
import { useAbiQuery } from "../queries/useAbiQuery";
import { AccountSelect } from "./AccountSelect";

export function Navigation() {
const pathname = usePathname();
const getLinkUrl = useWorldUrl();
const { data, isFetched } = useAbiQuery();

return (
<div className="mb-8">
Expand All @@ -35,6 +38,12 @@ export function Navigation() {
</Link>
</div>

{isFetched && !data?.isWorldDeployed && (
<h4 className="font-mono text-sm font-bold uppercase opacity-70">
Waiting for world deploy <Loader className="inline-block h-4 w-4 animate-spin" />
</h4>
)}

<div className="flex items-center gap-x-4">
<LatestBlock />
<AccountSelect />
Expand Down
7 changes: 7 additions & 0 deletions packages/explorer/src/components/ui/Skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { cn } from "../../lib/utils";

function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />;
}

export { Skeleton };
34 changes: 34 additions & 0 deletions packages/explorer/src/queries/useAbiQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useParams } from "next/navigation";
import { AbiFunction, Hex } from "viem";
import { UseQueryResult, useQuery } from "@tanstack/react-query";

export async function getAbi(worldAddress: Hex) {
const res = await fetch(`/api/world?${new URLSearchParams({ address: worldAddress })}`);
const data = await res.json();

if (!res.ok) {
throw new Error(data.error);
}

return data;
}

type AbiQueryResult = {
abi: AbiFunction[];
isWorldDeployed: boolean;
};

export const useAbiQuery = (): UseQueryResult<AbiQueryResult> => {
const { worldAddress } = useParams();
return useQuery({
queryKey: ["abi", worldAddress],
queryFn: () => getAbi(worldAddress as Hex),
select: (data) => {
return {
abi: data.abi || [],
isWorldDeployed: data.isWorldDeployed,
};
},
refetchInterval: 15000,
});
};
Loading