Skip to content

Commit 5b4e980

Browse files
authored
add multiple changes (#67)
1 parent abb9218 commit 5b4e980

15 files changed

Lines changed: 617 additions & 292 deletions

File tree

frontend/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
"dependencies": {
1414
"@aptabase/web": "^0.4.3",
1515
"@ethui/ui": "^0.0.141",
16+
"@fontsource/source-code-pro": "^5.2.7",
1617
"@hookform/resolvers": "^5.2.1",
1718
"@tailwindcss/typography": "^0.5.16",
18-
"@tanstack/react-query": "^5.66.0",
1919
"@tanstack/nitro-v2-vite-plugin": "^1.133.3",
20+
"@tanstack/react-query": "^5.90.12",
2021
"@tanstack/react-query-devtools": "^5.66.0",
2122
"@tanstack/react-router": "^1.139.14",
2223
"@tanstack/react-router-devtools": "^1.139.14",
2324
"@tanstack/react-router-ssr-query": "^1.139.14",
24-
"@fontsource/source-code-pro": "^5.2.7",
2525
"@tanstack/react-start": "^1.139.14",
2626
"axios": "^1.13.2",
2727
"clsx": "^2.1.1",
@@ -35,6 +35,8 @@
3535
"tailwind-merge": "^3.0.2",
3636
"tailwindcss": "^4.1.5",
3737
"tailwindcss-animate": "^1.0.7",
38+
"viem": "^2.43.5",
39+
"wagmi": "^3.2.0",
3840
"zod": "^4.1.5",
3941
"zustand": "^5.0.4"
4042
},

frontend/src/api/anvil.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import axios from "axios";
2+
import * as chains from "viem/chains";
3+
import { z } from "zod";
4+
5+
export const anvilNodeInfoSchema = z.object({
6+
currentBlockNumber: z.string(),
7+
currentBlockTimestamp: z.number(),
8+
currentBlockHash: z.string(),
9+
hardFork: z.string(),
10+
transactionOrder: z.string(),
11+
environment: z.object({
12+
baseFee: z.string(),
13+
chainId: z.number(),
14+
gasLimit: z.string(),
15+
gasPrice: z.string(),
16+
}),
17+
forkConfig: z
18+
.object({
19+
forkUrl: z.string(),
20+
forkBlockNumber: z.number(),
21+
forkRetryBackoff: z.number(),
22+
})
23+
.nullable(),
24+
});
25+
26+
export type AnvilNodeInfo = z.infer<typeof anvilNodeInfoSchema>;
27+
28+
async function jsonRpcCall<T>(
29+
rpcUrl: string,
30+
method: string,
31+
params: unknown[] = [],
32+
): Promise<T> {
33+
const { data } = await axios.post(rpcUrl, {
34+
jsonrpc: "2.0",
35+
method,
36+
params,
37+
id: 1,
38+
});
39+
40+
if (data.error) {
41+
throw new Error(data.error.message);
42+
}
43+
44+
return data.result;
45+
}
46+
47+
export const anvil = {
48+
getNodeInfo: async (rpcUrl: string): Promise<AnvilNodeInfo> => {
49+
const result = await jsonRpcCall<unknown>(rpcUrl, "anvil_nodeInfo");
50+
return anvilNodeInfoSchema.parse(result);
51+
},
52+
53+
getChainId: async (rpcUrl: string): Promise<number> => {
54+
const result = await jsonRpcCall<string>(rpcUrl, "eth_chainId");
55+
return Number.parseInt(result, 16);
56+
},
57+
};
58+
59+
const allChains = Object.values(chains);
60+
61+
export function getChainName(chainId: number): string {
62+
const chain = allChains.find((c) => c.id === chainId);
63+
return chain?.name ?? `Chain ${chainId}`;
64+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { cn } from "@ethui/ui/lib/utils";
2+
import {
3+
Tooltip,
4+
TooltipContent,
5+
TooltipTrigger,
6+
} from "@ethui/ui/components/shadcn/tooltip";
7+
8+
interface ExternalLinkProps {
9+
href: string;
10+
children: React.ReactNode;
11+
className?: string;
12+
tooltip?: string;
13+
}
14+
15+
export function ExternalLink({
16+
href,
17+
children,
18+
className,
19+
tooltip,
20+
}: ExternalLinkProps) {
21+
const link = (
22+
<a
23+
href={href}
24+
target="_blank"
25+
rel="noopener noreferrer"
26+
className={cn(
27+
"text-xs text-solidity-value hover:text-sky-700",
28+
className,
29+
)}
30+
>
31+
{children}
32+
</a>
33+
);
34+
35+
if (!tooltip) return link;
36+
37+
return (
38+
<Tooltip>
39+
<TooltipTrigger asChild>{link}</TooltipTrigger>
40+
<TooltipContent>
41+
<p>{tooltip}</p>
42+
</TooltipContent>
43+
</Tooltip>
44+
);
45+
}

frontend/src/components/Topbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function Topbar() {
3333
>
3434
<EthuiLogo size={28} />
3535
<span className="font-semibold text-foreground text-lg">
36-
ethui <span className="text-primary">Stacks</span>
36+
ethui <span className="text-primary">stacks</span>
3737
</span>
3838
</button>
3939

frontend/src/hooks/useAnvil.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { anvil } from "~/api/anvil";
3+
4+
export function useAnvilNodeInfo(
5+
stackSlug: string,
6+
rpcUrl: string | undefined,
7+
) {
8+
return useQuery({
9+
queryKey: ["anvilNodeInfo", stackSlug],
10+
queryFn: () => anvil.getNodeInfo(rpcUrl!),
11+
enabled: !!rpcUrl,
12+
});
13+
}
14+
15+
export function useForkChainId(stackSlug: string, forkUrl: string | undefined) {
16+
return useQuery({
17+
queryKey: ["forkChainId", stackSlug],
18+
queryFn: () => anvil.getChainId(forkUrl!),
19+
enabled: !!forkUrl,
20+
});
21+
}

frontend/src/hooks/useStackInfo.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { useMemo, useState } from "react";
2+
import type { Address, Hash } from "viem";
3+
import {
4+
createConfig,
5+
http,
6+
useTransactionReceipt,
7+
useWatchBlocks,
8+
} from "wagmi";
9+
import { Stack } from "~/api/stacks";
10+
import { useAnvilNodeInfo, useForkChainId } from "./useAnvil";
11+
12+
export interface LatestTransaction {
13+
hash: Hash;
14+
success: boolean;
15+
timestamp: number;
16+
}
17+
18+
export function useStackInfo(stack: Stack) {
19+
const config = useMemo(
20+
() =>
21+
createConfig({
22+
chains: [
23+
{
24+
id: stack.chain_id,
25+
name: stack.slug,
26+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
27+
rpcUrls: {
28+
default: { http: [stack.rpc_url] },
29+
},
30+
},
31+
],
32+
transports: {
33+
[stack.chain_id]: http(stack.rpc_url),
34+
},
35+
}),
36+
[stack.chain_id, stack.rpc_url, stack.slug],
37+
);
38+
39+
const [latestStackInfo, setLatestStackInfo] = useState<
40+
| {
41+
latestBlockNumber: number;
42+
latestBlockTimestamp: number;
43+
latestTxHash: Hash;
44+
}
45+
| undefined
46+
>();
47+
48+
const { data: anvilInfo } = useAnvilNodeInfo(stack.slug, stack.rpc_url);
49+
const { data: forkChainId } = useForkChainId(
50+
stack.slug,
51+
stack.anvil_opts?.fork_url,
52+
);
53+
54+
useWatchBlocks({
55+
config,
56+
includeTransactions: true,
57+
emitOnBegin: true,
58+
onBlock(block) {
59+
if (latestStackInfo?.latestBlockNumber === Number(block.number)) return;
60+
const tx = block.transactions[block.transactions.length - 1];
61+
setLatestStackInfo((prev) => ({
62+
latestTxHash: tx?.hash ?? prev?.latestTxHash,
63+
latestBlockNumber: Number(block.number),
64+
latestBlockTimestamp: Number(block.timestamp),
65+
}));
66+
},
67+
});
68+
69+
const { data: receipt } = useTransactionReceipt({
70+
config,
71+
hash: latestStackInfo?.latestTxHash,
72+
});
73+
74+
return {
75+
data: {
76+
liveInfo: {
77+
...latestStackInfo,
78+
txStatusSuccess: receipt?.status === "success",
79+
},
80+
anvilInfo,
81+
forkChainId,
82+
},
83+
};
84+
}

0 commit comments

Comments
 (0)