Skip to content

Commit 82512a7

Browse files
authored
add: direct faucet for c-chain (#2602)
* add: direct faucet for c-chain * good now
1 parent 7a3cfe6 commit 82512a7

File tree

4 files changed

+896
-345
lines changed

4 files changed

+896
-345
lines changed

app/api/cchain-faucet/route.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { createWalletClient, http, parseEther, createPublicClient } from 'viem';
3+
import { privateKeyToAccount } from 'viem/accounts';
4+
import { avalancheFuji, avalanche } from 'viem/chains';
5+
import { getAuthSession } from '@/lib/auth/authSession';
6+
import { rateLimit } from '@/lib/rateLimit';
7+
8+
const SERVER_PRIVATE_KEY = process.env.FAUCET_C_CHAIN_PRIVATE_KEY;
9+
const FAUCET_C_CHAIN_ADDRESS = process.env.FAUCET_C_CHAIN_ADDRESS;
10+
const FIXED_AMOUNT = '2';
11+
12+
if (!SERVER_PRIVATE_KEY || !FAUCET_C_CHAIN_ADDRESS) {
13+
console.error('necessary environment variables for C-Chain faucet are not set');
14+
}
15+
16+
interface TransferResponse {
17+
success: boolean;
18+
txHash?: string;
19+
sourceAddress?: string;
20+
destinationAddress?: string;
21+
amount?: string;
22+
message?: string;
23+
}
24+
25+
async function transferCChainAVAX(
26+
sourcePrivateKey: string,
27+
sourceAddress: string,
28+
destinationAddress: string,
29+
isTestnet: boolean
30+
): Promise<{ txHash: string }> {
31+
const chain = isTestnet ? avalancheFuji : avalanche;
32+
const account = privateKeyToAccount(sourcePrivateKey as `0x${string}`);
33+
const walletClient = createWalletClient({
34+
account,
35+
chain,
36+
transport: http()
37+
});
38+
39+
const publicClient = createPublicClient({
40+
chain,
41+
transport: http()
42+
});
43+
44+
const balance = await publicClient.getBalance({
45+
address: sourceAddress as `0x${string}`
46+
});
47+
48+
const amountToSend = parseEther(FIXED_AMOUNT);
49+
50+
if (balance < amountToSend) {
51+
throw new Error('Insufficient faucet balance');
52+
}
53+
54+
const txHash = await walletClient.sendTransaction({
55+
to: destinationAddress as `0x${string}`,
56+
value: amountToSend,
57+
});
58+
59+
return { txHash };
60+
}
61+
62+
async function validateFaucetRequest(request: NextRequest): Promise<NextResponse | null> {
63+
try {
64+
const session = await getAuthSession();
65+
if (!session?.user) {
66+
return NextResponse.json(
67+
{ success: false, message: 'Authentication required' },
68+
{ status: 401 }
69+
);
70+
}
71+
72+
if (!SERVER_PRIVATE_KEY || !FAUCET_C_CHAIN_ADDRESS) {
73+
return NextResponse.json(
74+
{ success: false, message: 'Server not properly configured' },
75+
{ status: 500 }
76+
);
77+
}
78+
79+
const searchParams = request.nextUrl.searchParams;
80+
const destinationAddress = searchParams.get('address');
81+
82+
if (!destinationAddress) {
83+
return NextResponse.json(
84+
{ success: false, message: 'Destination address is required' },
85+
{ status: 400 }
86+
);
87+
}
88+
89+
if (!/^0x[a-fA-F0-9]{40}$/.test(destinationAddress)) {
90+
return NextResponse.json(
91+
{ success: false, message: 'Invalid Ethereum address format' },
92+
{ status: 400 }
93+
);
94+
}
95+
96+
if (destinationAddress.toLowerCase() === FAUCET_C_CHAIN_ADDRESS?.toLowerCase()) {
97+
return NextResponse.json(
98+
{ success: false, message: 'Cannot send tokens to the faucet address' },
99+
{ status: 400 }
100+
);
101+
}
102+
return null;
103+
} catch (error) {
104+
console.error('Validation failed:', error);
105+
return NextResponse.json(
106+
{
107+
success: false,
108+
message: error instanceof Error ? error.message : 'Failed to validate request'
109+
},
110+
{ status: 500 }
111+
);
112+
}
113+
}
114+
115+
async function handleFaucetRequest(request: NextRequest): Promise<NextResponse> {
116+
try {
117+
const searchParams = request.nextUrl.searchParams;
118+
const destinationAddress = searchParams.get('address')!;
119+
const isTestnet = true;
120+
121+
const tx = await transferCChainAVAX(
122+
SERVER_PRIVATE_KEY!,
123+
FAUCET_C_CHAIN_ADDRESS!,
124+
destinationAddress,
125+
isTestnet
126+
);
127+
128+
const response: TransferResponse = {
129+
success: true,
130+
txHash: tx.txHash,
131+
sourceAddress: FAUCET_C_CHAIN_ADDRESS,
132+
destinationAddress,
133+
amount: FIXED_AMOUNT
134+
};
135+
136+
return NextResponse.json(response);
137+
138+
} catch (error) {
139+
console.error('C-Chain transfer failed:', error);
140+
141+
const response: TransferResponse = {
142+
success: false,
143+
message: error instanceof Error ? error.message : 'Failed to complete transfer'
144+
};
145+
146+
return NextResponse.json(response, { status: 500 });
147+
}
148+
}
149+
150+
export async function GET(request: NextRequest): Promise<NextResponse> {
151+
const validationResponse = await validateFaucetRequest(request);
152+
153+
if (validationResponse) {
154+
return validationResponse;
155+
}
156+
157+
const rateLimitHandler = rateLimit(handleFaucetRequest, {
158+
windowMs: 24 * 60 * 60 * 1000,
159+
maxRequests: 1
160+
});
161+
162+
return rateLimitHandler(request);
163+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"use client";
2+
import { useState } from "react";
3+
import {
4+
AlertDialog,
5+
AlertDialogAction,
6+
AlertDialogContent,
7+
AlertDialogDescription,
8+
AlertDialogFooter,
9+
AlertDialogHeader,
10+
AlertDialogTitle,
11+
} from "../AlertDialog";
12+
import { useWalletStore } from "../../stores/walletStore";
13+
14+
const LOW_BALANCE_THRESHOLD = 1;
15+
16+
export const CChainFaucetButton = () => {
17+
const { walletEVMAddress, isTestnet, cChainBalance, updateCChainBalance } =
18+
useWalletStore();
19+
20+
const [isRequestingCTokens, setIsRequestingCTokens] = useState(false);
21+
const [isAlertDialogOpen, setIsAlertDialogOpen] = useState(false);
22+
const [alertDialogTitle, setAlertDialogTitle] = useState("Error");
23+
const [alertDialogMessage, setAlertDialogMessage] = useState("");
24+
const [isLoginError, setIsLoginError] = useState(false);
25+
const handleLogin = () => {
26+
window.location.href = "/login";
27+
};
28+
29+
const handleCChainTokenRequest = async () => {
30+
if (isRequestingCTokens || !walletEVMAddress) return;
31+
setIsRequestingCTokens(true);
32+
33+
try {
34+
const response = await fetch(
35+
`/api/cchain-faucet?address=${walletEVMAddress}`
36+
);
37+
const rawText = await response.text();
38+
let data;
39+
40+
try {
41+
data = JSON.parse(rawText);
42+
} catch (parseError) {
43+
throw new Error(`Invalid response: ${rawText.substring(0, 100)}...`);
44+
}
45+
46+
if (!response.ok) {
47+
if (response.status === 401) {
48+
throw new Error("Please login first");
49+
}
50+
if (response.status === 429) {
51+
throw new Error(
52+
data.message || "Rate limit exceeded. Please try again later."
53+
);
54+
}
55+
throw new Error(
56+
data.message || `Error ${response.status}: Failed to get tokens`
57+
);
58+
}
59+
60+
if (data.success) {
61+
console.log("C-Chain token request successful, txHash:", data.txHash);
62+
setTimeout(() => updateCChainBalance(), 3000);
63+
} else {
64+
throw new Error(data.message || "Failed to get tokens");
65+
}
66+
} catch (error) {
67+
console.error("C-Chain token request error:", error);
68+
const errorMessage =
69+
error instanceof Error ? error.message : "Unknown error occurred";
70+
if (errorMessage.includes("login") || errorMessage.includes("401")) {
71+
setAlertDialogTitle("Authentication Required");
72+
setAlertDialogMessage(
73+
"You need to be logged in to request free tokens from the C-Chain Faucet."
74+
);
75+
setIsLoginError(true);
76+
setIsAlertDialogOpen(true);
77+
} else {
78+
setAlertDialogTitle("Faucet Request Failed");
79+
setAlertDialogMessage(errorMessage);
80+
setIsLoginError(false);
81+
setIsAlertDialogOpen(true);
82+
}
83+
} finally {
84+
setIsRequestingCTokens(false);
85+
}
86+
};
87+
88+
if (!isTestnet) {
89+
return null;
90+
}
91+
92+
return (
93+
<>
94+
<AlertDialog open={isAlertDialogOpen} onOpenChange={setIsAlertDialogOpen}>
95+
<AlertDialogContent>
96+
<AlertDialogHeader>
97+
<AlertDialogTitle>{alertDialogTitle}</AlertDialogTitle>
98+
<AlertDialogDescription>
99+
{alertDialogMessage}
100+
</AlertDialogDescription>
101+
</AlertDialogHeader>
102+
<AlertDialogFooter className="flex gap-2">
103+
{isLoginError ? (
104+
<>
105+
<AlertDialogAction
106+
onClick={handleLogin}
107+
className="bg-blue-500 hover:bg-blue-600"
108+
>
109+
Login
110+
</AlertDialogAction>
111+
<AlertDialogAction className="bg-zinc-200 hover:bg-zinc-300 text-zinc-800">
112+
Close
113+
</AlertDialogAction>
114+
</>
115+
) : (
116+
<AlertDialogAction>OK</AlertDialogAction>
117+
)}
118+
</AlertDialogFooter>
119+
</AlertDialogContent>
120+
</AlertDialog>
121+
122+
<button
123+
onClick={handleCChainTokenRequest}
124+
disabled={isRequestingCTokens}
125+
className={`px-2 py-1 text-xs font-medium text-white rounded transition-colors ${
126+
cChainBalance < LOW_BALANCE_THRESHOLD
127+
? "bg-blue-500 hover:bg-blue-600 shimmer"
128+
: "bg-zinc-600 hover:bg-zinc-700"
129+
} ${isRequestingCTokens ? "opacity-50 cursor-not-allowed" : ""}`}
130+
title="Get free C-Chain AVAX"
131+
>
132+
{isRequestingCTokens ? "Requesting..." : "Faucet"}
133+
</button>
134+
</>
135+
);
136+
};

0 commit comments

Comments
 (0)