solana-faucet-sdk
v2.0.0
Published
TypeScript SDK for interacting with the [Zebec Solana Faucet](src/artifacts/zebec_solana_faucet.json) on-chain program. The faucet lets an admin configure one or more SPL token mints, refill their balances, and lets users request tokens subject to a coold
Readme
Solana Faucet SDK
TypeScript SDK for interacting with the Zebec Solana Faucet on-chain program. The faucet lets an admin configure one or more SPL token mints, refill their balances, and lets users request tokens subject to a cooldown.
- Program ID:
73o1ngBeTrBcC4nSiJFEnA21GtbtLEcwUcF6YWsPritQ - Network:
devnet
Installation
npm install solana-faucet-sdk
# or
yarn add solana-faucet-sdkPeer dependencies you will typically already have in a Solana project: @solana/web3.js, @coral-xyz/anchor, @solana/spl-token, bn.js.
Quick start
import { Keypair, Connection } from "@solana/web3.js";
import { Wallet } from "@coral-xyz/anchor";
import { createAnchorProvider, FaucetService } from "solana-faucet-sdk";
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const wallet = new Wallet(Keypair.generate()); // replace with your signing wallet
const provider = createAnchorProvider(connection, wallet);
const service = FaucetService.create(provider, "devnet");
// Read on-chain faucet configs
const configs = await service.getFaucetConfigs();
console.log(configs.admin.toBase58(), configs.mintMaps);Every state-changing method returns a TransactionPayload from @zebec-network/solana-common. Call .execute() to sign and send:
const payload = await service.requestFromFaucet({
requester: wallet.publicKey,
requesterTokenAccount,
faucetTokenAccount,
tokenMint,
});
const signature = await payload.execute({ commitment: "confirmed" });Concepts
The on-chain program tracks two accounts:
Faucet— a single PDA derived from the seed"faucet". It stores theadmin, the cooldown period, and a list ofMintMapentries (one per supported token mint, each with its own per-request amount and current balance).UserRequest— a per-user PDA derived from["user_request", requester]. It stores a list ofMintRequestTimestampentries — the last time the user pulled from each mint.
Mints are added/removed dynamically by the admin; the SDK does not hardcode any specific token.
Providers
The SDK accepts any Anchor Provider. Two helpers are included:
import {
createAnchorProvider,
createReadonlyProvider,
} from "solana-faucet-sdk";
// For signing transactions:
const provider = createAnchorProvider(connection, wallet /* AnchorWallet */);
// For read-only RPC calls (no signing):
const readonly = createReadonlyProvider(connection, walletAddress?);AnchorWallet requires publicKey, signTransaction, and signAllTransactions.
PDA helpers
import { deriveFaucetPda, deriveUserRequestPda } from "solana-faucet-sdk";
const [faucet] = deriveFaucetPda(programId);
const [userRequest] = deriveUserRequestPda(requester, programId);FaucetService
Construct via the static factory:
const service = FaucetService.create(provider, "devnet");
service.faucetProgramId; // PublicKey of the on-chain programEvery transactional method has two forms:
get<Name>Instruction(...)— returns a rawTransactionInstructionyou can compose into your own transaction.<name>(params)— returns aTransactionPayloadready to execute.
Admin: initialize the faucet
Creates the singleton Faucet PDA. Only callable once per program; the signer becomes the initial admin.
await service.initFaucet({
admin: wallet.publicKey,
faucetCooldownPeriod: 86_400, // seconds between requests per mint per user
});Admin: add or update a mint
Registers a new SPL token mint with the faucet, or updates an existing mint's per-request amount. Creates the faucet's associated token account for the mint on first insert. amountPerRequest is a human-readable amount — the SDK fetches mint decimals and scales it for you.
await service.upsertMintMap({
admin: wallet.publicKey,
faucetTokenAccount, // faucet's ATA for tokenMint
tokenMint,
amountPerRequest: 100, // 100 tokens per request (scaled internally using on-chain mint decimals)
});Admin: remove a mint
Drains the faucet's balance for that mint back to an admin token account and removes the mint from the faucet's mint map.
await service.removeMintMap({
admin: wallet.publicKey,
faucetTokenAccount,
adminTokenAccount,
tokenMint,
});Anyone: refill a mint balance
Anyone holding the token can top up the faucet's balance for a registered mint. The amount here is a human-readable amount — the SDK fetches mint decimals and scales it for you.
await service.refillFaucet({
refiller: wallet.publicKey,
refillerTokenAccount,
faucetTokenAccount,
tokenMint,
amount: 1_000, // 1,000 tokens (scaled internally using on-chain mint decimals)
});User: request tokens
Transfers amountPerRequest of the given mint from the faucet to the requester, subject to the cooldown.
await service.requestFromFaucet({
requester: wallet.publicKey,
requesterTokenAccount,
faucetTokenAccount,
tokenMint,
});If the cooldown for this mint has not elapsed, the program returns CooldownNotElapsed.
Admin: update faucet
Rotate admin and/or change the cooldown.
await service.updateFaucet({
admin: wallet.publicKey,
newAdmin,
faucetCooldownPeriod: 3_600,
});Read: faucet configs
const configs = await service.getFaucetConfigs();
// {
// admin: PublicKey,
// mintMaps: [
// {
// mintAddress: PublicKey,
// amountPerRequest: string, // human-readable, scaled by on-chain mint decimals
// balance: string, // human-readable, scaled by on-chain mint decimals
// },
// ...
// ],
// faucetCooldownPeriod: string, // seconds, stringified
// }Read: a user's cooldown state
const info = await service.getUserCooldownPeriod(requester);
// {
// requester: PublicKey,
// mintRequestTimestamps: [
// { mintAddress: PublicKey, lastRequestTimestamp: string },
// ...
// ],
// faucetCooldownPeriod: string,
// }To check whether a user can request a given mint:
const now = Math.floor(Date.now() / 1000);
const entry = info.mintRequestTimestamps.find((t) =>
t.mintAddress.equals(tokenMint),
);
const last = entry ? Number(entry.lastRequestTimestamp) : 0;
const elapsed = now - last;
const ready = elapsed >= Number(info.faucetCooldownPeriod);Types
type Numeric = string | number;
type MintMap = {
mintAddress: PublicKey;
amountPerRequest: string; // human-readable, scaled by mint decimals (e.g. "100")
balance: string; // human-readable, scaled by mint decimals (e.g. "1000")
};
type FaucetConfigsInfo = {
admin: PublicKey;
mintMaps: MintMap[];
faucetCooldownPeriod: string; // u64 (seconds), stringified
};
type MintRequestTimestamp = {
mintAddress: PublicKey;
lastRequestTimestamp: string; // i64, stringified
};
type UserCooldownPeriod = {
requester: PublicKey;
mintRequestTimestamps: MintRequestTimestamp[];
faucetCooldownPeriod: string;
};u64/i64 raw values (cooldown periods, timestamps) are stringified to avoid JS number-precision loss — convert with BigInt(...) or new BN(...) as needed. Token amounts returned from getters (amountPerRequest, balance) are already formatted to UI units using each mint's on-chain decimals; if you need base units, scale by 10 ** decimals or call getMintDecimals + parseToken from @zebec-network/solana-common.
How-tos
Compose multiple instructions
The low-level get<Name>Instruction(...) getters take base units (BN) directly — scale by 10 ** decimals yourself when using them.
const ix1 = await service.getUpsertMintMapInstruction(
admin,
faucet,
faucetTokenAccount,
tokenMint,
{ amountPerRequest: new BN(100_000_000) }, // base units (e.g. 100 tokens at 6 decimals)
);
const ix2 = await service.getRefillFaucetInstruction(
admin,
adminTokenAccount,
faucet,
faucetTokenAccount,
tokenMint,
{ amount: new BN(1_000_000_000) },
);
// Build your own transaction with [ix1, ix2]...Derive ATAs
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
const [faucet] = deriveFaucetPda(service.faucetProgramId);
const faucetTokenAccount = getAssociatedTokenAddressSync(
tokenMint,
faucet,
true,
);
const userTokenAccount = getAssociatedTokenAddressSync(tokenMint, requester);Read-only usage
const provider = createReadonlyProvider(connection);
const service = FaucetService.create(provider, "devnet");
const configs = await service.getFaucetConfigs(); // works without a signerState-changing methods require a provider that can sign (e.g., AnchorProvider).
On-chain errors
| Code | Name | Meaning |
| ----- | --------------------------- | ------------------------------------------------- |
| 6000 | Unauthorized | Signer is not the admin. |
| 6001 | CooldownNotElapsed | User must wait before requesting this mint again. |
| 6002 | InsufficientFaucetBalance | Faucet doesn't have enough of the requested mint. |
| 6003+ | (see IDL) | Additional validation errors. |
The errors are exposed via the returned TransactionPayload's error map; thrown messages will include the human-readable msg.
Development
yarn install
yarn build # compile to ./dist
yarn test # run e2e mocha tests (requires .env with RPC + keys)
yarn format # biome formatTests expect these env vars (see test/shared.ts):
DEVNET_RPC_URLDEVNET_SECRET_KEYS— JSON array of base58-encoded secret keys
License
MIT
