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): fetch partial abi on load #3076

Closed
wants to merge 4 commits into from
Closed
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
5 changes: 4 additions & 1 deletion packages/explorer/src/app/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ReactNode } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createConfig, http } from "@wagmi/core";
import { localhost } from "@wagmi/core/chains";
import { AppStoreProvider } from "../store";

const queryClient = new QueryClient();

Expand All @@ -28,7 +29,9 @@ export const wagmiConfig = createConfig({
export function Providers({ children }: { children: ReactNode }) {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
<QueryClientProvider client={queryClient}>
<AppStoreProvider>{children}</AppStoreProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
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 worldLoaded = logs.length === 2;

return { fromBlock, toBlock };
return { fromBlock, toBlock, worldLoaded };
}

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, worldLoaded } = 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, worldLoaded });
} 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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { waitForTransactionReceipt, writeContract } from "@wagmi/core";
import { Checkbox } from "../../../../components/ui/Checkbox";
import { ACCOUNT_PRIVATE_KEYS } from "../../../../consts";
import { camelCase, cn } from "../../../../lib/utils";
import { useStore } from "../../../../store";
import { useAppStore } from "../../../../store";
import { wagmiConfig } from "../../../Providers";
import { TableConfig } from "../../../api/table/route";

Expand All @@ -27,7 +27,7 @@ type Props = {
export function EditableTableCell({ name, config, keyTuple, value: defaultValue }: Props) {
const queryClient = useQueryClient();
const chainId = useChainId();
const { account } = useStore();
const { account } = useAppStore();
const { worldAddress } = useParams();

const [value, setValue] = useState<unknown>(defaultValue);
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 />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useChainId } from "wagmi";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { readContract, waitForTransactionReceipt, writeContract } from "@wagmi/core";
import { ACCOUNT_PRIVATE_KEYS } from "../../../../consts";
import { useStore } from "../../../../store";
import { useAppStore } from "../../../../store";
import { wagmiConfig } from "../../../Providers";
import { FunctionType } from "./FunctionField";

Expand All @@ -18,7 +18,7 @@ type UseContractMutationProps = {
export function useContractMutation({ abi, operationType }: UseContractMutationProps) {
const queryClient = useQueryClient();
const chainId = useChainId();
const { account } = useStore();
const { account } = useAppStore();
const { worldAddress } = useParams();

return useMutation({
Expand Down
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>
);
}
11 changes: 8 additions & 3 deletions packages/explorer/src/components/AccountSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Address, formatEther } from "viem";
import { useBalance } from "wagmi";
import { ACCOUNTS } from "../consts";
import { useStore } from "../store";
import { useAppStore } from "../store";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/Select";
import { TruncatedHex } from "./ui/TruncatedHex";

function AccountSelectItem({ address, name }: { address: Address; name: string }) {
type Props = {
address: Address;
name: string;
};

function AccountSelectItem({ address, name }: Props) {
const balance = useBalance({
address,
query: {
Expand All @@ -25,7 +30,7 @@ function AccountSelectItem({ address, name }: { address: Address; name: string }
}

export function AccountSelect() {
const { account, setAccount } = useStore();
const { account, setAccount } = useAppStore();
return (
<Select value={account} onValueChange={setAccount}>
<SelectTrigger className="w-[300px] text-left">
Expand Down
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>
);
}
4 changes: 4 additions & 0 deletions packages/explorer/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ 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 +37,8 @@ export function Navigation() {
</Link>
</div>

{(!isFetched || !data?.worldLoaded) && <h4 className="font-mono text-xs">Loading world ...</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[];
worldLoaded: 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 || [],
worldLoaded: data.worldLoaded,
};
},
refetchInterval: 15000,
});
};
13 changes: 0 additions & 13 deletions packages/explorer/src/store.ts

This file was deleted.

21 changes: 21 additions & 0 deletions packages/explorer/src/store/AppStoreProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";

import { type ReactNode, createContext, useRef } from "react";
import { createAppStore } from "./createAppStore";

export type AppStoreApi = ReturnType<typeof createAppStore>;

export const AppStoreContext = createContext<AppStoreApi | undefined>(undefined);

export interface AppStoreProviderProps {
children: ReactNode;
}

export const AppStoreProvider = ({ children }: AppStoreProviderProps) => {
const storeRef = useRef<AppStoreApi>();
if (!storeRef.current) {
storeRef.current = createAppStore();
}

return <AppStoreContext.Provider value={storeRef.current}>{children}</AppStoreContext.Provider>;
};
Loading
Loading