Skip to content

Commit fe0396e

Browse files
committed
Update
1 parent e67a300 commit fe0396e

File tree

7 files changed

+111
-39
lines changed

7 files changed

+111
-39
lines changed

apps/dashboard/.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,8 @@ UNTHREAD_PRO_TIER_ID=""
9696
NEXT_PUBLIC_DEMO_ENGINE_URL=""
9797

9898
# API server secret (required for thirdweb.com SIWE login). Copy from Vercel.
99-
API_SERVER_SECRET=""
99+
API_SERVER_SECRET=""
100+
101+
# Used for the Faucet page (/<chain_id>)
102+
NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
103+
TURNSTILE_SECRET_KEY=""

apps/dashboard/next.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const ContentSecurityPolicy = `
88
style-src 'self' 'unsafe-inline' vercel.live;
99
font-src 'self' vercel.live assets.vercel.com framerusercontent.com;
1010
frame-src * data:;
11-
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com framerusercontent.com events.framer.com;
11+
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com framerusercontent.com events.framer.com challenges.cloudflare.com;
1212
connect-src * data: blob:;
1313
worker-src 'self' blob:;
1414
block-all-mixed-content;

apps/dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@emotion/react": "11.13.3",
3030
"@emotion/styled": "11.13.0",
3131
"@hookform/resolvers": "^3.9.0",
32+
"@marsidev/react-turnstile": "^1.0.2",
3233
"@n8tb1t/use-scroll-position": "^2.0.3",
3334
"@radix-ui/react-alert-dialog": "^1.1.2",
3435
"@radix-ui/react-avatar": "^1.1.1",

apps/dashboard/src/@/constants/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ export const DASHBOARD_STORAGE_URL =
2323

2424
export const API_SERVER_URL =
2525
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
26+
27+
export const TURNSTILE_SITE_KEY =
28+
process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "";

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22

33
import { Spinner } from "@/components/ui/Spinner/Spinner";
44
import { Button } from "@/components/ui/button";
5-
import { THIRDWEB_ENGINE_FAUCET_WALLET } from "@/constants/env";
5+
import { Form } from "@/components/ui/form";
6+
import {
7+
THIRDWEB_ENGINE_FAUCET_WALLET,
8+
TURNSTILE_SITE_KEY,
9+
} from "@/constants/env";
610
import { useThirdwebClient } from "@/constants/thirdweb.client";
711
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
12+
import { Turnstile } from "@marsidev/react-turnstile";
813
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
914
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
1015
import { mapV4ChainToV5Chain } from "contexts/map-chains";
1116
import { useTrack } from "hooks/analytics/useTrack";
17+
import { useForm } from "react-hook-form";
1218
import { toast } from "sonner";
1319
import { toUnits } from "thirdweb";
1420
import type { ChainMetadata } from "thirdweb/chains";
@@ -29,6 +35,8 @@ function formatTime(seconds: number) {
2935
return rtf.format(+seconds, "second");
3036
}
3137

38+
type TurnstileForm = { "cf-turnstile-response": string };
39+
3240
export function FaucetButton({
3341
chain,
3442
amount,
@@ -52,7 +60,7 @@ export function FaucetButton({
5260
const queryClient = useQueryClient();
5361

5462
const claimMutation = useMutation({
55-
mutationFn: async () => {
63+
mutationFn: async (turnstileToken: string) => {
5664
trackEvent({
5765
category: "faucet",
5866
action: "claim",
@@ -67,6 +75,7 @@ export function FaucetButton({
6775
body: JSON.stringify({
6876
chainId: chainId,
6977
toAddress: address,
78+
turnstileToken,
7079
}),
7180
});
7281

@@ -117,6 +126,8 @@ export function FaucetButton({
117126
faucetWalletBalanceQuery.data !== undefined &&
118127
faucetWalletBalanceQuery.data.value < toUnits("1", 17);
119128

129+
const form = useForm<TurnstileForm>();
130+
120131
// loading state
121132
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
122133
return (
@@ -161,28 +172,38 @@ export function FaucetButton({
161172
);
162173
}
163174

175+
const claimFunds = (values: TurnstileForm) => {
176+
const turnstileToken = values["cf-turnstile-response"];
177+
if (!turnstileToken) {
178+
return toast.error("Failed to retrieve captcha token");
179+
}
180+
console.log({ turnstileToken });
181+
// Instead of having a dedicated endpoint (/api/verify-token),
182+
// we can just attach the token in the payload and send it to the claim-faucet endpoint, to avoid a round-trip request
183+
const claimPromise = claimMutation.mutateAsync(turnstileToken.toString());
184+
toast.promise(claimPromise, {
185+
success: `${amount} ${chain.nativeCurrency.symbol} sent successfully`,
186+
error: `Failed to claim ${amount} ${chain.nativeCurrency.symbol}`,
187+
});
188+
};
189+
164190
// eligible to claim and faucet has balance
165191
return (
166192
<div className="flex w-full flex-col text-center">
167-
<Button
168-
variant="primary"
169-
className="w-full gap-2"
170-
onClick={() => {
171-
const claimPromise = claimMutation.mutateAsync();
172-
toast.promise(claimPromise, {
173-
success: `${amount} ${chain.nativeCurrency.symbol} sent successfully`,
174-
error: `Failed to claim ${amount} ${chain.nativeCurrency.symbol}`,
175-
});
176-
}}
177-
>
178-
{claimMutation.isPending ? (
179-
<>
180-
Claiming <Spinner className="size-3" />
181-
</>
182-
) : (
183-
`Get ${amount} ${chain.nativeCurrency.symbol}`
184-
)}
185-
</Button>
193+
<Form {...form}>
194+
<form onSubmit={form.handleSubmit(claimFunds)}>
195+
<Button variant="primary" className="w-full gap-2" type="submit">
196+
{claimMutation.isPending ? (
197+
<>
198+
Claiming <Spinner className="size-3" />
199+
</>
200+
) : (
201+
`Get ${amount} ${chain.nativeCurrency.symbol}`
202+
)}
203+
</Button>
204+
<Turnstile siteKey={TURNSTILE_SITE_KEY} />
205+
</form>
206+
</Form>
186207

187208
{faucetWalletBalanceQuery.data && (
188209
<p className="mt-3 text-muted-foreground text-xs">

apps/dashboard/src/app/api/testnet-faucet/claim/route.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ const THIRDWEB_ACCESS_TOKEN = process.env.THIRDWEB_ACCESS_TOKEN;
1111
interface RequestTestnetFundsPayload {
1212
chainId: number;
1313
toAddress: string;
14+
15+
// Cloudflare Turnstile token received from the client-side
16+
turnstileToken: string;
1417
}
1518

1619
// Note: This handler cannot use "edge" runtime because of Redis usage.
1720
export const POST = async (req: NextRequest) => {
1821
const requestBody = (await req.json()) as RequestTestnetFundsPayload;
19-
const { chainId, toAddress } = requestBody;
22+
const { chainId, toAddress, turnstileToken } = requestBody;
2023
if (Number.isNaN(chainId)) {
2124
throw new Error("Invalid chain ID.");
2225
}
@@ -46,6 +49,42 @@ export const POST = async (req: NextRequest) => {
4649
);
4750
}
4851

52+
if (!turnstileToken) {
53+
return NextResponse.json(
54+
{
55+
error: "Missing Turnstile token.",
56+
},
57+
{ status: 400 },
58+
);
59+
}
60+
61+
// https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
62+
// Validate the token by calling the "/siteverify" API endpoint.
63+
const result = await fetch(
64+
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
65+
{
66+
body: JSON.stringify({
67+
secret: process.env.TURNSTILE_SECRET_KEY,
68+
response: turnstileToken,
69+
remoteip: ipAddress,
70+
}),
71+
method: "POST",
72+
headers: {
73+
"Content-Type": "application/json",
74+
},
75+
},
76+
);
77+
78+
const outcome = await result.json();
79+
if (!outcome.success) {
80+
return NextResponse.json(
81+
{
82+
error: "Could not validate captcha.",
83+
},
84+
{ status: 400 },
85+
);
86+
}
87+
4988
const ipCacheKey = `testnet-faucet:${chainId}:${ipAddress}`;
5089
const addressCacheKey = `testnet-faucet:${chainId}:${toAddress}`;
5190

pnpm-lock.yaml

Lines changed: 19 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)