Skip to content

Commit 281f140

Browse files
committed
Add public devnet faucet for staking
1 parent fcaa048 commit 281f140

File tree

6 files changed

+420
-32
lines changed

6 files changed

+420
-32
lines changed

Anchor.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ members = [
33
"programs/dacit_program"
44
]
55

6+
[programs.devnet]
7+
dacit_program = "BHd8TSJu2GF5MntXPSsYrtderZYc4UGXzrVLj9GPoeh1"
8+
69
[provider]
710
cluster = "devnet"
811
wallet = "~/.config/solana/id.json"

frontend/components/StakePanel.tsx

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import {
1111
} from "@solana/spl-token";
1212
import {
1313
getConfigPDA,
14+
getFaucetStatePDA,
1415
getMintPDA,
1516
getProgram,
1617
getStakeStatePDA,
1718
getTokenProgramId,
1819
getVaultPDA,
20+
PUBLIC_FAUCET_MAX_BASE_UNITS,
21+
TOKEN_BASE_UNITS,
1922
} from "../utils/anchor";
2023

2124
interface StakePanelProps {
@@ -26,9 +29,9 @@ interface StakePanelProps {
2629
* StakePanel component allows users to stake DACIT tokens.
2730
* Connects to the Anchor program and submits stake transactions.
2831
*
29-
* NOTE: This panel is wired to the generated Anchor IDL and the active devnet
30-
* program address. The staking transaction still assumes the program has been
31-
* initialized and the connected wallet already holds DACIT tokens.
32+
* NOTE: This panel is wired to the generated Anchor IDL and active devnet
33+
* program address. It can bootstrap first-time wallets by initializing the
34+
* program, creating the ATA, and using the public faucet when needed.
3235
*/
3336
export default function StakePanel({ className = "" }: StakePanelProps) {
3437
const { connection } = useConnection();
@@ -52,13 +55,15 @@ export default function StakePanel({ className = "" }: StakePanelProps) {
5255
throw new Error("Connected wallet does not support Anchor transactions.");
5356
}
5457

55-
const stakeAmount = BigInt(Math.trunc(amount));
58+
const normalizedAmount = Number.isFinite(amount) ? amount : 0;
59+
const stakeAmount = BigInt(Math.round(normalizedAmount * TOKEN_BASE_UNITS));
5660
if (stakeAmount <= 0n) {
57-
throw new Error("Enter a whole-number DACIT amount greater than zero.");
61+
throw new Error("Enter a DACIT amount greater than zero.");
5862
}
5963

6064
const program = getProgram(wallet as AnchorWallet);
6165
const [configPda] = getConfigPDA();
66+
const [faucetStatePda] = getFaucetStatePDA(wallet.publicKey);
6267
const [stakeStatePda] = getStakeStatePDA(wallet.publicKey);
6368
const [mintPda] = getMintPDA();
6469
const [vaultPda] = getVaultPDA(mintPda);
@@ -107,28 +112,46 @@ export default function StakePanel({ className = "" }: StakePanelProps) {
107112
const userAccount = await getAccount(connection, userAta);
108113
const currentBalance = userAccount.amount;
109114
if (currentBalance < stakeAmount) {
110-
if (!configAuthority || !configAuthority.equals(wallet.publicKey)) {
111-
throw new Error(
112-
`Wallet balance is ${currentBalance.toString()} DACIT. Connect the authority wallet to mint test tokens or lower the stake amount.`
113-
);
114-
}
115-
116115
const topUpAmount = stakeAmount - currentBalance;
117-
setStatus(`Minting ${topUpAmount.toString()} DACIT from the devnet faucet...`);
118-
await program.methods
119-
.mintToUser(new anchor.BN(topUpAmount.toString()))
120-
.accounts({
121-
config: configPda,
122-
mint: mintPda,
123-
userAta,
124-
user: wallet.publicKey,
125-
authority: wallet.publicKey,
126-
tokenProgram,
127-
systemProgram: anchor.web3.SystemProgram.programId,
128-
associatedTokenProgram: anchor.utils.token.ASSOCIATED_PROGRAM_ID,
129-
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
130-
})
131-
.rpc();
116+
if (configAuthority && configAuthority.equals(wallet.publicKey)) {
117+
setStatus(`Minting ${(Number(topUpAmount) / TOKEN_BASE_UNITS).toFixed(9)} DACIT from the authority faucet...`);
118+
await program.methods
119+
.mintToUser(new anchor.BN(topUpAmount.toString()))
120+
.accounts({
121+
config: configPda,
122+
mint: mintPda,
123+
userAta,
124+
user: wallet.publicKey,
125+
authority: wallet.publicKey,
126+
tokenProgram,
127+
systemProgram: anchor.web3.SystemProgram.programId,
128+
associatedTokenProgram: anchor.utils.token.ASSOCIATED_PROGRAM_ID,
129+
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
130+
})
131+
.rpc();
132+
} else {
133+
if (topUpAmount > BigInt(PUBLIC_FAUCET_MAX_BASE_UNITS)) {
134+
throw new Error(
135+
`Need ${(Number(topUpAmount) / TOKEN_BASE_UNITS).toFixed(9)} DACIT, but the public faucet caps each claim at ${(PUBLIC_FAUCET_MAX_BASE_UNITS / TOKEN_BASE_UNITS).toFixed(9)} DACIT. Lower the stake amount or wait for another faucet claim.`
136+
);
137+
}
138+
139+
setStatus(`Claiming ${(Number(topUpAmount) / TOKEN_BASE_UNITS).toFixed(9)} DACIT from the public faucet...`);
140+
await program.methods
141+
.claimFaucet(new anchor.BN(topUpAmount.toString()))
142+
.accounts({
143+
config: configPda,
144+
mint: mintPda,
145+
faucetState: faucetStatePda,
146+
user: wallet.publicKey,
147+
userAta,
148+
tokenProgram,
149+
systemProgram: anchor.web3.SystemProgram.programId,
150+
associatedTokenProgram: anchor.utils.token.ASSOCIATED_PROGRAM_ID,
151+
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
152+
})
153+
.rpc();
154+
}
132155
}
133156

134157
setStatus("Submitting stake transaction...");
@@ -148,7 +171,7 @@ export default function StakePanel({ className = "" }: StakePanelProps) {
148171
})
149172
.rpc();
150173

151-
alert(`Successfully staked ${amount} DACIT tokens!`);
174+
alert(`Successfully staked ${normalizedAmount} DACIT tokens!`);
152175
setAmount(0);
153176
setStatus(null);
154177
} catch (err) {
@@ -188,6 +211,7 @@ export default function StakePanel({ className = "" }: StakePanelProps) {
188211
type="number"
189212
value={amount}
190213
min={0}
214+
step="0.000000001"
191215
onChange={(e) => setAmount(Number(e.target.value))}
192216
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
193217
placeholder="Enter amount"
@@ -215,8 +239,8 @@ export default function StakePanel({ className = "" }: StakePanelProps) {
215239

216240
{wallet.publicKey && (
217241
<p className="text-xs text-gray-500 text-center">
218-
First run will initialize the program, create your ATA, and mint DACIT if this wallet
219-
is the configured authority.
242+
First run will initialize the program, create your ATA, and claim up to 100 DACIT
243+
from the public faucet when needed.
220244
</p>
221245
)}
222246
</div>

frontend/utils/anchor.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import { AnchorWallet } from "@solana/wallet-adapter-react";
77
import idl from "./idl/dacit_program.json";
88

99
const PROGRAM_ID = new anchor.web3.PublicKey(
10-
"7rrFC6iDci7eDwq4Dvzhw1X5ejsL6EkpW79g5s25NbwQ"
10+
"BHd8TSJu2GF5MntXPSsYrtderZYc4UGXzrVLj9GPoeh1"
1111
);
1212

1313
// Network endpoint - change to mainnet-beta for production
1414
const DEVNET_ENDPOINT = "https://api.devnet.solana.com";
15+
export const TOKEN_DECIMALS = 9;
16+
export const TOKEN_BASE_UNITS = 10 ** TOKEN_DECIMALS;
17+
export const PUBLIC_FAUCET_MAX_BASE_UNITS = 100 * TOKEN_BASE_UNITS;
1518

1619
/**
1720
* Creates an Anchor Provider instance for interacting with the Solana network.
@@ -76,6 +79,14 @@ export const getMintPDA = (): [anchor.web3.PublicKey, number] =>
7679
PROGRAM_ID
7780
);
7881

82+
export const getFaucetStatePDA = (
83+
userPubkey: anchor.web3.PublicKey
84+
): [anchor.web3.PublicKey, number] =>
85+
anchor.web3.PublicKey.findProgramAddressSync(
86+
[Buffer.from("faucet-state"), userPubkey.toBuffer()],
87+
PROGRAM_ID
88+
);
89+
7990
/**
8091
* Derives a PDA for the emission schedule account.
8192
*

0 commit comments

Comments
 (0)