Skip to content

Commit 8de47f3

Browse files
committed
Automatic abi fetch
1 parent 52bef84 commit 8de47f3

File tree

8 files changed

+599
-42
lines changed

8 files changed

+599
-42
lines changed

app/api/web3/fetch-abi/route.ts

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import { ethers } from "ethers";
2+
import { NextResponse } from "next/server";
3+
import { auth } from "@/lib/auth";
4+
5+
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY || "";
6+
7+
/**
8+
* Get Etherscan API URL and chainid based on network (using v2 API)
9+
* V2 uses a single base URL with chainid parameter instead of network-specific subdomains
10+
*/
11+
function getEtherscanApiConfig(network: string): {
12+
baseUrl: string;
13+
chainid: string;
14+
} {
15+
const configs: Record<string, { baseUrl: string; chainid: string }> = {
16+
mainnet: {
17+
baseUrl: "https://api.etherscan.io/v2/api",
18+
chainid: "1",
19+
},
20+
sepolia: {
21+
baseUrl: "https://api.etherscan.io/v2/api",
22+
chainid: "11155111",
23+
},
24+
};
25+
26+
const config = configs[network];
27+
if (!config) {
28+
throw new Error(`Unsupported network: ${network}`);
29+
}
30+
31+
return config;
32+
}
33+
34+
/**
35+
* Parse Etherscan error message into user-friendly message
36+
*/
37+
function parseEtherscanError(
38+
data: { status: string; message: string; result: string },
39+
contractAddress: string,
40+
network: string
41+
): string {
42+
const errorMessage =
43+
data.result || data.message || "Failed to fetch ABI from Etherscan";
44+
const lowerMessage = errorMessage.toLowerCase();
45+
46+
// Log the full response for debugging
47+
console.error("[Etherscan] API error response:", {
48+
status: data.status,
49+
message: data.message,
50+
result: data.result,
51+
contractAddress,
52+
network,
53+
});
54+
55+
// Provide user-friendly error messages for common cases
56+
if (
57+
lowerMessage.includes("not verified") ||
58+
lowerMessage.includes("source code not verified")
59+
) {
60+
return "Contract source code not verified on Etherscan. Please provide ABI manually.";
61+
}
62+
63+
if (
64+
lowerMessage.includes("invalid api key") ||
65+
lowerMessage.includes("api key")
66+
) {
67+
return "Etherscan API key is invalid or not configured. Please contact support.";
68+
}
69+
70+
if (
71+
lowerMessage.includes("rate limit") ||
72+
lowerMessage.includes("max rate limit")
73+
) {
74+
return "Etherscan API rate limit exceeded. Please try again in a few moments.";
75+
}
76+
77+
// Handle deprecated V1 endpoint error
78+
if (
79+
lowerMessage.includes("deprecated") ||
80+
lowerMessage.includes("v1 endpoint") ||
81+
lowerMessage.includes("v2-migration")
82+
) {
83+
return "Etherscan API endpoint needs to be updated. Please contact support.";
84+
}
85+
86+
// For "NOTOK" generic errors, provide a more helpful message
87+
if (errorMessage === "NOTOK" || data.message === "NOTOK") {
88+
return "Unable to fetch ABI from Etherscan. The contract may not be verified, or there may be an API issue. Please try providing the ABI manually.";
89+
}
90+
91+
// For other errors, use the result message if available
92+
return errorMessage;
93+
}
94+
95+
/**
96+
* Fetch ABI from Etherscan API
97+
*/
98+
async function fetchAbiFromEtherscan(
99+
contractAddress: string,
100+
network: string
101+
): Promise<string> {
102+
console.log("[Etherscan] fetchAbiFromEtherscan called with:", {
103+
contractAddress,
104+
network,
105+
});
106+
107+
if (!ETHERSCAN_API_KEY) {
108+
console.error("[Etherscan] API key not configured");
109+
throw new Error("Etherscan API key not configured");
110+
}
111+
112+
console.log("[Etherscan] API key present:", ETHERSCAN_API_KEY ? "Yes" : "No");
113+
114+
// Validate contract address
115+
if (!ethers.isAddress(contractAddress)) {
116+
console.error("[Etherscan] Invalid contract address:", contractAddress);
117+
throw new Error(`Invalid contract address: ${contractAddress}`);
118+
}
119+
120+
console.log("[Etherscan] Contract address validated");
121+
122+
const { baseUrl, chainid } = getEtherscanApiConfig(network);
123+
console.log("[Etherscan] Base URL:", baseUrl);
124+
console.log("[Etherscan] Chain ID:", chainid);
125+
126+
const url = new URL(baseUrl);
127+
url.searchParams.set("chainid", chainid);
128+
url.searchParams.set("module", "contract");
129+
url.searchParams.set("action", "getabi");
130+
url.searchParams.set("address", contractAddress);
131+
url.searchParams.set("apikey", ETHERSCAN_API_KEY);
132+
133+
const requestUrl = url.toString();
134+
console.log(
135+
"[Etherscan] Full request URL:",
136+
requestUrl.replace(ETHERSCAN_API_KEY, "***")
137+
);
138+
139+
console.log("[Etherscan] Making fetch request...");
140+
const response = await fetch(requestUrl);
141+
console.log(
142+
"[Etherscan] Response status:",
143+
response.status,
144+
response.statusText
145+
);
146+
console.log("[Etherscan] Response ok:", response.ok);
147+
148+
if (!response.ok) {
149+
console.error("[Etherscan] HTTP error response:", {
150+
status: response.status,
151+
statusText: response.statusText,
152+
});
153+
throw new Error(
154+
`Etherscan API error: ${response.status} ${response.statusText}`
155+
);
156+
}
157+
158+
console.log("[Etherscan] Parsing JSON response...");
159+
const data = (await response.json()) as {
160+
status: string;
161+
message: string;
162+
result: string;
163+
};
164+
165+
console.log("[Etherscan] Response data:", {
166+
status: data.status,
167+
message: data.message,
168+
resultLength: data.result ? data.result.length : 0,
169+
});
170+
171+
if (data.status === "0") {
172+
// Etherscan returns status "0" for errors
173+
console.error("[Etherscan] Etherscan API returned error status");
174+
const errorMessage = parseEtherscanError(data, contractAddress, network);
175+
throw new Error(errorMessage);
176+
}
177+
178+
if (!data.result || data.result === "Contract source code not verified") {
179+
console.error("[Etherscan] Contract not verified or no result");
180+
throw new Error(
181+
"Contract source code not verified on Etherscan. Please provide ABI manually."
182+
);
183+
}
184+
185+
// Validate that result is valid JSON
186+
try {
187+
console.log("[Etherscan] Validating ABI JSON...");
188+
const abi = JSON.parse(data.result);
189+
if (!Array.isArray(abi)) {
190+
console.error("[Etherscan] ABI is not an array");
191+
throw new Error("Invalid ABI format: expected array");
192+
}
193+
console.log(
194+
"[Etherscan] ABI validated successfully, item count:",
195+
abi.length
196+
);
197+
return data.result;
198+
} catch (error) {
199+
console.error("[Etherscan] Failed to parse ABI:", error);
200+
throw new Error(
201+
`Invalid ABI format from Etherscan: ${
202+
error instanceof Error ? error.message : "Unknown error"
203+
}`
204+
);
205+
}
206+
}
207+
208+
export async function POST(request: Request) {
209+
try {
210+
console.log("[Etherscan] POST request received");
211+
212+
// Authenticate user
213+
const session = await auth.api.getSession({
214+
headers: request.headers,
215+
});
216+
217+
if (!session?.user) {
218+
console.log("[Etherscan] Unauthorized - no session");
219+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
220+
}
221+
222+
console.log("[Etherscan] User authenticated:", session.user.id);
223+
224+
// Parse request body
225+
const body = (await request.json().catch(() => ({}))) as {
226+
contractAddress?: string;
227+
network?: string;
228+
};
229+
230+
console.log("[Etherscan] Request body:", body);
231+
232+
const { contractAddress, network } = body;
233+
234+
if (!contractAddress) {
235+
console.log("[Etherscan] Missing contract address");
236+
return NextResponse.json(
237+
{ error: "Contract address is required" },
238+
{ status: 400 }
239+
);
240+
}
241+
242+
if (!network) {
243+
console.log("[Etherscan] Missing network");
244+
return NextResponse.json(
245+
{ error: "Network is required" },
246+
{ status: 400 }
247+
);
248+
}
249+
250+
console.log("[Etherscan] Fetching ABI for:", { contractAddress, network });
251+
252+
// Fetch ABI from Etherscan
253+
const abi = await fetchAbiFromEtherscan(contractAddress, network);
254+
255+
console.log("[Etherscan] Successfully fetched ABI, length:", abi.length);
256+
257+
return NextResponse.json({
258+
success: true,
259+
abi,
260+
});
261+
} catch (error) {
262+
console.error("[Etherscan] Failed to fetch ABI:", error);
263+
console.error(
264+
"[Etherscan] Error stack:",
265+
error instanceof Error ? error.stack : "No stack"
266+
);
267+
return NextResponse.json(
268+
{
269+
error:
270+
error instanceof Error
271+
? error.message
272+
: "Failed to fetch ABI from Etherscan",
273+
},
274+
{ status: 500 }
275+
);
276+
}
277+
}

components/ui/checkbox.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5+
import { CheckIcon } from "lucide-react"
6+
7+
import { cn } from "@/lib/utils"
8+
9+
function Checkbox({
10+
className,
11+
...props
12+
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
13+
return (
14+
<CheckboxPrimitive.Root
15+
data-slot="checkbox"
16+
className={cn(
17+
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
18+
className
19+
)}
20+
{...props}
21+
>
22+
<CheckboxPrimitive.Indicator
23+
data-slot="checkbox-indicator"
24+
className="grid place-content-center text-current transition-none"
25+
>
26+
<CheckIcon className="size-3.5" />
27+
</CheckboxPrimitive.Indicator>
28+
</CheckboxPrimitive.Root>
29+
)
30+
}
31+
32+
export { Checkbox }

0 commit comments

Comments
 (0)