Skip to content

Bnb faucet #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions typescript/bnb-faucet/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
152 changes: 152 additions & 0 deletions typescript/bnb-faucet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# πŸͺ™ BNB Testnet Faucet

A modern web faucet for the BNB Smart Chain testnet, built with Next.js (App Router, TypeScript), Supabase, ethers.js, and Tailwind CSS.

🌐 Live Demo
You can try the faucet live here:
πŸ‘‰ https://bnb-faucet.netlify.app/

Built with Next.js, Tailwind CSS, and Supabase.

Deployed on Netlify for fast global delivery.

Sends 0.001 BNB on BNB Testnet (1 claim per address per 24h).

---

## πŸš€ Features

- **Request BNB testnet tokens** directly from the web UI
- **Rate-limited**: 0.001 BNB per address every 24 hours
- **Mainnet eligibility check**: Optionally blocks addresses with β‰₯ X BNB on mainnet
- **Cooldown/anti-spam**: Tracks last claim per address in Supabase
- **Beautiful, responsive UI** with Tailwind CSS
- **TypeScript** codebase for safety and clarity
- **Supabase** for database and claim tracking
- **Environment-configurable**: faucet amount, cooldown, and more
- **Easy to deploy** on Vercel or any Node.js host

---

## πŸ§‘β€πŸ’» Tech Stack

- [Next.js 14+](https://nextjs.org/) (App Router, TypeScript)
- [ethers.js](https://docs.ethers.org/) – blockchain interactions
- [Supabase](https://supabase.com/) – Postgres DB for rate-limiting
- [Tailwind CSS](https://tailwindcss.com/) – modern utility-first styling

---

## βš™οΈ Environment Variables

Copy `.env.local.example` to `.env.local` and fill in your values:

```env
FAUCET_PK=0x... # Private key for funded testnet wallet (never mainnet!)
BNB_RPC=https://... # BNB testnet or opBNB testnet RPC URL
BNB_AMOUNT=0.001 # Amount of BNB to send per request
COOLDOWN_HOURS=24 # Cooldown in hours per address
SUPABASE_URL=https://... # Supabase project URL
SUPABASE_KEY=... # Supabase service role key
CHECK_MAINNET_BALANCE=true # If true, blocks users with enough mainnet BNB
MAINNET_BALANCE_AMOUNT=0.01 # Mainnet balance threshold (in BNB)
MAINNET_RPC=https://bsc-dataseed.binance.org/ # BNB Chain mainnet RPC
```

**Warning:** Always use a testnet wallet for the faucet!

---

## 🏦 Database Setup (Supabase)

Create a table named `faucet_claims`:

```sql
create table faucet_claims (
id bigint generated always as identity primary key,
address text unique not null,
last_claimed timestamptz,
created_at timestamptz not null default now()
);
```

---

## πŸ› οΈ Running Locally

1. Install dependencies:

```bash
npm install
```

2. Add your `.env.local` file as above.

3. Start the app:

```bash
npm run dev
```

4. Visit [http://localhost:3000](http://localhost:3000) to try it out!

---

## πŸ–₯️ API Endpoints

- **POST `/api/faucet`** – Request BNB for an address
Request body: `{ "address": "0x..." }`
Responses:
- Success: `{ success: true, txHash, amount }`
- Cooldown: `{ error: "Cooldown active", timeLeftSeconds, nextClaimAt }`
- Not eligible: `{ error: "Address has enough mainnet BNB", ... }`
- Error: `{ error, details }`

- **GET `/api/faucet-config`** – Returns faucet config (amount, cooldown, etc)

---

## πŸ‘¨β€πŸŽ¨ Frontend Features

- Enter wallet address and request testnet BNB
- See success (with BscScan link), error, or cooldown timer
- Mobile-friendly, fast, and simple

---

## πŸ›‘οΈ Security & Abuse Protection

- Cooldown enforced per address
- Optional mainnet balance check to block hoarders
- Use a testnet-only private key
- Consider adding CAPTCHA or wallet signature in production

---

## 🏁 Deployment

You can deploy to [Vercel](https://vercel.com/) or any Node.js/Vercel-compatible host.

- **Remember:** Set all env variables in your deployment settings!

---

## 🀝 License

MIT – Use, remix, and hack freely!

---

## πŸ™ Credits

Built using:
- [Next.js](https://nextjs.org/)
- [ethers.js](https://docs.ethers.org/)
- [Supabase](https://supabase.com/)
- [Tailwind CSS](https://tailwindcss.com/)

---

## ✨ Hackathon Ready

Showcase it live, or fork for your own chain/testnet faucet!
10 changes: 10 additions & 0 deletions typescript/bnb-faucet/app/api/faucet-config/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NextResponse } from "next/server";

export async function GET() {
return NextResponse.json({
bnbAmount: process.env.BNB_AMOUNT || "0.0001",
cooldownHours: process.env.COOLDOWN_HOURS || "24",
checkMainnetBalance: process.env.CHECK_MAINNET_BALANCE === "false",
mainnetBalanceAmount: process.env.MAINNET_BALANCE_AMOUNT || "0.01"
});
}
146 changes: 146 additions & 0 deletions typescript/bnb-faucet/app/api/faucet/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { NextRequest, NextResponse } from "next/server";
import { ethers } from "ethers";
import { createClient } from "@supabase/supabase-js";

// Load environment variables for faucet setup
const FAUCET_PK = process.env.FAUCET_PK!; // Faucet wallet private key
const BNB_RPC = process.env.BNB_RPC!; // Testnet RPC endpoint
const BNB_AMOUNT = process.env.BNB_AMOUNT || "0.0001"; // Amount to send per claim
const COOLDOWN_HOURS = parseInt(process.env.COOLDOWN_HOURS || "24"); // Cooldown duration per wallet
const SUPABASE_URL = process.env.SUPABASE_URL!; // Supabase project URL
const SUPABASE_KEY = process.env.SUPABASE_KEY!; // Supabase service key
const CHECK_MAINNET_BALANCE = process.env.CHECK_MAINNET_BALANCE === "true"; // Toggle mainnet check
const MAINNET_BALANCE_AMOUNT = process.env.MAINNET_BALANCE_AMOUNT || "0.01"; // Threshold
const MAINNET_RPC = process.env.MAINNET_RPC || "https://bsc-dataseed.binance.org/"; // Mainnet RPC

// Setup provider and wallet for testnet
const provider = new ethers.JsonRpcProvider(BNB_RPC);
const wallet = new ethers.Wallet(FAUCET_PK, provider);

// Initialize Supabase client
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);

// Type-safe error message handler
function getErrorMessage(err: unknown): string {
if (typeof err === "string") return err;
if (err instanceof Error) return err.message;
return JSON.stringify(err);
}

// Faucet request endpoint
export async function POST(req: NextRequest) {
const { address } = await req.json();

// 1. Validate that the address is correct
if (!ethers.isAddress(address)) {
return NextResponse.json({ error: "Invalid address" }, { status: 400 });
}

// 2. Optionally check the user's balance on BNB mainnet
if (CHECK_MAINNET_BALANCE) {
try {
const mainnetProvider = new ethers.JsonRpcProvider(MAINNET_RPC);
const bal = await mainnetProvider.getBalance(address);
const required = ethers.parseEther(MAINNET_BALANCE_AMOUNT);

if (bal >= required) {
return NextResponse.json(
{
error: "Address has enough mainnet BNB",
mainnetBalance: ethers.formatEther(bal),
required: MAINNET_BALANCE_AMOUNT,
},
{ status: 403 }
);
}
} catch (err: unknown) {
return NextResponse.json(
{ error: "Mainnet balance check failed", details: getErrorMessage(err) },
{ status: 500 }
);
}
}

// 3. Check cooldown for this address
try {
const { data } = await supabase
.from("faucet_claims")
.select("last_claimed")
.eq("address", address.toLowerCase())
.single();

const now = new Date();
let canSend = true;
let timeLeft = 0;

if (data?.last_claimed) {
const last = new Date(data.last_claimed);
const nextAllowed = new Date(last.getTime() + COOLDOWN_HOURS * 3600 * 1000);

if (nextAllowed > now) {
canSend = false;
timeLeft = Math.ceil((nextAllowed.getTime() - now.getTime()) / 1000); // seconds
}
}

if (!canSend) {
return NextResponse.json(
{
error: "Cooldown active",
timeLeftSeconds: timeLeft,
nextClaimAt: new Date(now.getTime() + timeLeft * 1000).toISOString(),
},
{ status: 429 }
);
}

// 4. Check if the faucet wallet has enough funds
try {
const faucetBalance = await provider.getBalance(wallet.address);
const amountToSend = ethers.parseEther(BNB_AMOUNT);

if (faucetBalance < amountToSend) {
return NextResponse.json(
{
error: "Faucet depleted",
message:
"Faucet wallet has insufficient balance. Please contact the maintainer or try again later.",
currentBalance: ethers.formatEther(faucetBalance),
required: BNB_AMOUNT,
},
{ status: 503 }
);
}

// 5. Send BNB to user address
const tx = await wallet.sendTransaction({
to: address,
value: amountToSend,
});

// 6. Update claim history in Supabase
await supabase.from("faucet_claims").upsert(
[
{
address: address.toLowerCase(),
last_claimed: now.toISOString(),
},
],
{ onConflict: "address" }
);

// 7. Return success response
return NextResponse.json({ success: true, txHash: tx.hash, amount: BNB_AMOUNT });
} catch (err: unknown) {
return NextResponse.json(
{ error: "Transaction failed", details: getErrorMessage(err) },
{ status: 500 }
);
}
} catch (err: unknown) {
return NextResponse.json(
{ error: "Database error", details: getErrorMessage(err) },
{ status: 500 }
);
}
}
Binary file added typescript/bnb-faucet/app/favicon.ico
Binary file not shown.
26 changes: 26 additions & 0 deletions typescript/bnb-faucet/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import "tailwindcss";

:root {
--background: #ffffff;
--foreground: #171717;
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
Loading