diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..66b1462 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,24 @@ +{ + "extends": [ + "next/core-web-vitals", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "prettier" + ], + "plugins": ["@typescript-eslint", "react", "prettier"], + "rules": { + "prettier/prettier": ["error"], + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-unused-vars": ["warn"], + "@typescript-eslint/no-explicit-any": "warn", + "no-console": ["warn", { "allow": ["warn", "error"] }] + }, + "settings": { + "react": { + "version": "detect" + } + } +} diff --git a/.gitignore b/.gitignore index 26b002a..cde4c61 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ yarn-debug.log* yarn-error.log* # env files (can opt-in for commiting if needed) +!.env.example .env* # vercel diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..57ac98b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/app/api/faucet/route.ts b/app/api/faucet/route.ts index 5db78c5..40c46e5 100644 --- a/app/api/faucet/route.ts +++ b/app/api/faucet/route.ts @@ -11,52 +11,80 @@ const akaveChain = { nativeCurrency: { name: 'AKVT', symbol: 'AKVT', - decimals: 18 + decimals: 18, }, rpcUrls: { default: { - http: ['https://n1-us.akave.ai/ext/bc/2JMWNmZbYvWcJRPPy1siaDBZaDGTDAaqXoY5UBKh4YrhNFzEce/rpc'] + http: [ + 'https://n1-us.akave.ai/ext/bc/2JMWNmZbYvWcJRPPy1siaDBZaDGTDAaqXoY5UBKh4YrhNFzEce/rpc', + ], }, public: { - http: ['https://n1-us.akave.ai/ext/bc/2JMWNmZbYvWcJRPPy1siaDBZaDGTDAaqXoY5UBKh4YrhNFzEce/rpc'] - } - } + http: [ + 'https://n1-us.akave.ai/ext/bc/2JMWNmZbYvWcJRPPy1siaDBZaDGTDAaqXoY5UBKh4YrhNFzEce/rpc', + ], + }, + }, } const publicClient = createPublicClient({ chain: akaveChain, - transport: http() + transport: http(), }) +async function verifyRecaptcha(token: string) { + try { + const response = await fetch('https://www.google.com/recaptcha/api/siteverify', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `secret=${process.env.RECAPTCHA_SECRET_KEY}&response=${token}`, + }) + + const data = await response.json() + return data.success + } catch (error) { + console.error('Error verifying reCAPTCHA:', error) + return false + } +} + export async function POST(request: Request) { try { - const { address, email } = await request.json() - + const { address, email, recaptchaToken } = await request.json() + if (!address) { return NextResponse.json({ error: 'Address is required' }, { status: 400 }) } + if (!recaptchaToken) { + return NextResponse.json({ error: 'reCAPTCHA verification is required' }, { status: 400 }) + } + + const isValidRecaptcha = await verifyRecaptcha(recaptchaToken) + if (!isValidRecaptcha) { + return NextResponse.json({ error: 'Invalid reCAPTCHA verification' }, { status: 400 }) + } + // Check user's balance const balance = await publicClient.getBalance({ address }) - + if (balance >= parseEther('10')) { - return NextResponse.json( - { error: 'Address already has more than 10 AKVT' }, - { status: 400 } - ) + return NextResponse.json({ error: 'Address already has more than 10 AKVT' }, { status: 400 }) } const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) const walletClient = createWalletClient({ account, chain: akaveChain, - transport: http() + transport: http(), }) // Send transaction const hash = await walletClient.sendTransaction({ to: address, - value: parseEther('10') + value: parseEther('10'), }) // Store email if provided @@ -72,9 +100,6 @@ export async function POST(request: Request) { return NextResponse.json({ hash }) } catch (error) { console.error(error) - return NextResponse.json( - { error: 'Failed to process request' }, - { status: 500 } - ) + return NextResponse.json({ error: 'Failed to process request' }, { status: 500 }) } } diff --git a/app/layout.tsx b/app/layout.tsx index 7428421..0a1dc18 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,31 +1,31 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import './globals.css' +import Script from 'next/script' -const inter = Inter({ subsets: ["latin"] }); +const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { - title: "Akave Faucet", - description: "Claim AKVF tokens", + title: 'Akave Faucet', + description: 'Claim AKVF tokens', icons: { icon: [ { - url: "/favicon-32x32.png", - sizes: "32x32", - type: "image/png", + url: '/favicon-32x32.png', + sizes: '32x32', + type: 'image/png', }, ], }, -}; +} -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( + +