Build a custom solana faucet using Next.js and solana/web3.js - Part 1
Airdrop devnet/testnet SOL tokens for building on Solana.
Introduction
Although the community agrees that building on Solana is synonymous to "Chewing glass", developers still love to buidl on the innovative and cool Layer 1.
To get started building, we first need to get ourselves some SOL, the native token of Solana for transaction charges, gas fees etc.
While this can be done via the solana-cli
(here), a custom faucet offers better UX and is still pretty cool. So that's what we're going to build today.
The tools
We're going to use the following :
- Next.js for building the frontend.
- Chakra-ui for styling
- solana/web3.js to interact with Solana blockchain
- Phantom as our wallet
Let's get buidling !!
The setup
We're going to start by installing the necessary dependencies and setting up our next.js boiler plate app using create-next-app
.
In addition please also install the Phantom wallet, and set up a new wallet. Once you have these down, let's begin with coding.
Frontend
Firstly, let's set up the ChakraProvider
, that lets us use the Chakra-ui components. Inside _app.tsx
file, wrap Component
with <ChakraProvider>
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { ChakraProvider } from "@chakra-ui/react";
function MyApp({ Component, pageProps }: AppProps) {
return (
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
);
}
export default MyApp;
Note: I'll start applying styles to components gradually as we go on, but feel free to give it your own touch.
Now, let's set up a very simple component for our faucet. The functionality we're aiming for is:
- User should be able to select devnet/testnet to receive airdrop.
- User should be able to input an address into the input box. If the address is valid, then they should be able to request for airdrop
So simple template of faucet component would look like this.
import { Button, Input, VStack } from "@chakra-ui/react";
import React, { useState } from "react";
const Faucet = () => {
const [address, setAddress] = useState("");
return (
<VStack>
<Input placeholder="Enter solana wallet address"
value={address}
onChange={(e) => setAddress(e.target.value)}
/>
<Button>
Request airdrop
</Button>
</VStack>
);
};
export default Faucet;
Address validation
Next, let's restrict the airdrop to only valid addresses. We'll use
PublicKey
class from solana/web3.js
to first convert the string from Input to create a PublicKey object. Then we can use isOnCurve()
function from PublicKey
which returns true
if the address is valid.
import { PublicKey } from "@solana/web3.js";
const validateSolanaAddress = (addr: string) => {
let publicKey: PublicKey;
try {
publicKey = new PublicKey(addr);
return PublicKey.isOnCurve(publicKey.toBytes());
} catch (err) {
return false;
}
}
Finally we'll attach anonChange
event Listener onto the input field, which sets the address each time there is a change in input. We'll make use of useEffect
react hook with address
as a dependency to receive up-to-date value of address and validate address each time. Let's also disable our button until the address is valid.
So putting this together we have
import { Button, Input, VStack} from "@chakra-ui/react";
import React, { useEffect, useState, ChangeEvent } from "react";
import { PublicKey } from "@solana/web3.js";
const Faucet = () => {
const [address, setAddress] = useState<any>("");
const [isValid, setisValid] = useState<boolean>(false);
const validateSolanaAddress = (addr: string) => {
let publicKey: PublicKey;
try {
publicKey = new PublicKey(addr);
return PublicKey.isOnCurve(publicKey.toBytes());
} catch (err) {
return false;
}
};
useEffect(() => {
const isValid = validateSolanaAddress(address);
setisValid(isValid);
}, [address]);
return (
<VStack>
<Input
placeholder="Enter solana wallet address"
value={address}
onChange={(e: ChangeEvent<HTMLInputElement>) => setAddress(e.target.value)}
/>
<Button disabled={!isValid}>
Request airdrop
</Button>
</VStack>
);
};
export default Faucet;
Requesting airdrop
Nextup, we've got to let the user choose where they'd want to receive the airdrop, devnet or testnet. This can be done in anyway you see fit, but I'm going with a simple slide button.
Let's build the SliderButton
component.
import React from "react";
import { Button } from "@chakra-ui/react";
interface SliderButtonProps {
isTestNet: boolean;
setIsTestNet: Function;
}
const activeBtn = {
border: "4px solid springgreen",
backgroundColor: "springgreen",
borderRadius: "50rem",
};
const inActiveBtn = { border: "4px solid grey", backgroundColor: "grey" };
const SliderButton = ({ isTestNet, setIsTestNet }: SliderButtonProps) => {
return (
<div
style={{
position: "relative",
borderRadius: "50px",
overflow: "hidden",
backgroundColor: "GrayText",
}}
>
<Button
onClick={() => setIsTestNet(true)}
style={isTestNet ? activeBtn : inActiveBtn}
>
{isTestNet ? "โ
" : ""} Testnet
</Button>
<Button
onClick={() => setIsTestNet(false)}
style={!isTestNet ? activeBtn : inActiveBtn}
>
{!isTestNet ? "โ
" : ""} Devnet
</Button>
</div>
);
};
export default SliderButton
Finally for requesting the airdrop, we'll make use of Connection
class, which helps us to create a connection to a fullnode JSON RPC endpoint.
RPC endpoint for devnet: api.devnet.solana.com
RPC endpoint for testnet: api.testnet.solana.com
Once we have connection in place, we'll use requestAirdrop
function, which takes in publicKey (to send the airdrop) and amount (in Lamports
) as arguments.
Lamports is to SOL as Gwei is to Ether, a fractional unit.
As docs say:
The system may perform micropayments of fractional SOLs, which are called lamports. They are named in honor of Solana's biggest technical influence, Leslie Lamport. A lamport has a value of 0.000000001 SOL.
Which means 1 SOL = 1000000000 Lamports. Let's also add a toaster for showing success/error messages. Putting it together, we have.
import { Button, Input, VStack, useToast } from "@chakra-ui/react";
.
.
.
const [isTestNet, setIsTestNet] = useState<boolean>(false);
const toast = useToast();
const requestAirDrop = async () => {
try {
const NODE_RPC = isTestNet
? "https://api.testnet.solana.com"
: "https://api.devnet.solana.com";
const CONNECTION = new Connection(NODE_RPC);
const confirmation = await CONNECTION.requestAirdrop(
new PublicKey(address),
1000000000
);
toast({
position: "top",
title: "Airdrop successful",
description: "Please check your wallet",
status: "success",
duration: 5000,
isClosable: true,
});
} catch (err) {
console.error("Error: ", err);
toast({
position: "top",
title: "Airdrop failed",
description: "Something went wrong ",
status: "error",
duration: 5000,
isClosable: true,
});
}
};
Putting it all together
Finally with all the fuctionality added, our Faucet
component should look like this.
import { Button, Input, VStack, useToast } from "@chakra-ui/react";
import React, { useEffect, useState } from "react";
import { PublicKey, Connection } from "@solana/web3.js";
import SliderButton from "./SliderButton";
const Faucet = () => {
const [address, setAddress] = useState<any>("");
const [isValid, setisValid] = useState<boolean>(false);
const [isTestNet, setIsTestNet] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const toast = useToast();
const validateSolanaAddress = (addr: string) => {
let publicKey: PublicKey;
try {
publicKey = new PublicKey(addr);
return PublicKey.isOnCurve(publicKey.toBytes());
} catch (err) {
return false;
}
};
useEffect(() => {
const isValid = validateSolanaAddress(address);
setisValid(isValid);
}, [address]);
const requestAirDrop = async () => {
try {
const NODE_RPC = isTestNet
? "https://api.testnet.solana.com"
: "https://api.devnet.solana.com";
const CONNECTION = new Connection(NODE_RPC);
setLoading(true);
const confirmation = await CONNECTION.requestAirdrop(
new PublicKey(address),
1000000000
);
setLoading(false);
toast({
position: "top",
title: "Airdrop succesful",
description: `Txn Hash ${confirmation}. Please check your wallet and SolScan`,
status: "success",
duration: 5000,
isClosable: true,
});
setAddress("");
} catch (err) {
console.log("Error: ", err);
setLoading(false);
toast({
position: "top",
title: "Airdrop failed",
description: "Something went wrong.",
status: "error",
duration: 5000,
isClosable: true,
});
}
};
return (
<VStack>
<SliderButton isTestNet={isTestNet} setIsTestNet={setIsTestNet} />
<Input
placeholder="Enter solana wallet address"
value={address}
size="lg"
width={"lg"}
textAlign="center"
onChange={(e: any) => setAddress(e.target.value)}
color={"blackAlpha.900"}
backgroundColor="plum"
_placeholder={{ color: "blackAlpha.700" }}
/>
<Button
mt={20}
onClick={requestAirDrop}
disabled={!isValid}
isLoading={loading}
>
Request airdrop
</Button>
</VStack>
);
};
export default Faucet;
Finally, place the <Faucet>
component into _app.tsx
Lets test it out
Feel free to style and customise it as you'd like. This is what I've made :)
You can view the transaction details by copying the transaction hash and checking in Solana explorer (Make sure you choose the correct network as transaction )
Next steps:
Let's improve the security of out faucet and also we'll deploy the faucet live to vercel. That's coming up in part-2
Note: If you prefer learning via video, check out this youtube playlist I have created for this article.