एक token sale प्रोग्राम एक स्मार्ट कॉन्ट्रैक्ट है जो एक निश्चित कीमत पर एक विशिष्ट टोकन बेचता है, आमतौर पर SOL जैसे नेटिव टोकन के बदले में। यह सेल तब तक चलती है जब तक कि पूर्व-निर्धारित सप्लाई बिक नहीं जाती या मालिक सेल को समाप्त करने का कोई कदम नहीं उठाता।
हमारा कार्यान्वयन (implementation) इस प्रवाह (flow) का पालन करता है:
- एक उपयोगकर्ता हमारी दर के आधार पर SOL जमा करता है, उदाहरण के लिए, 1 SOL पर 100 टोकन।
- प्रोग्राम SOL को एक ट्रेजरी Program Derived Address (PDA) में स्टोर करता है, जो एक प्रोग्राम-नियंत्रित अकाउंट है।
- SOL प्राप्त होने के बाद, उपयोगकर्ता के लिए टोकन मिंट (mint) किए जाते हैं।
- सेल तब तक जारी रहती है जब तक कि एक पूर्व-निर्धारित सप्लाई कैप तक नहीं पहुंच जाती।
- एक एडमिन ट्रेजरी से एकत्र किए गए SOL को निकाल (withdraw) सकता है।
Token Sale प्रोग्राम बनाना
हम जो Solana प्रोग्राम बनाएंगे, वह खरीदारों के लिए सीधे SPL टोकन मिंट करता है, और इसके लिए हमें मिंट अथॉरिटी के रूप में प्रत्येक ट्रांजेक्शन पर साइन करने की आवश्यकता नहीं होती है। यह एक मानक (standard) तरीका है—अन्यथा एडमिन को मैन्युअल रूप से हर खरीदारी को अप्रूव करना पड़ता, जो कि व्यावहारिक नहीं है।
Token sale के लिए आवश्यक अकाउंट्स बनाना
सबसे पहले, Anchor के साथ एक नया token_sale प्रोग्राम बनाएं और programs/token_sale/src/lib.rs में बॉयलरप्लेट कोड को नीचे दिए गए कोड से बदलें।
नीचे दिया गया कोड हमारे प्रोग्राम की डिपेंडेंसीज़ को इम्पोर्ट करता है और एक initialize फ़ंक्शन को परिभाषित करता है। यह फ़ंक्शन निम्नलिखित कार्य करता है:
- ट्रेजरी से निकासी (withdrawals) को नियंत्रित करने के लिए एडमिन अकाउंट सेट करता है
- हम जो नया टोकन बेच रहे हैं, उसके लिए एक मिंट अकाउंट बनाता है
- टोकन खरीदारी से SOL एकत्र करने के लिए एक ट्रेजरी अकाउंट बनाता है
use anchor_lang::prelude::*;
use anchor_lang::system_program::{transfer, Transfer};
use anchor_spl::token::{mint_to, Mint, MintTo, Token, TokenAccount};
declare_id!("Gm8bFHtX3TapZDqA2tjviP1Qn1f8bLjTf8tbhFcgzcFs"); // REPLACE THIS WITH YOUR PROGRAM ID OR RUN `anchor sync`
// Tokens per SOL, i.e., 1 SOL == 100 of our tokens
const TOKENS_PER_SOL: u64 = 100;
// Max supply: 1000 tokens (with 9 decimals)
const SUPPLY_CAP: u64 = 1000e9 as u64;
#[program]
pub mod token_sale {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// Set the admin key
ctx.accounts.admin_config.admin = ctx.accounts.admin.key();
Ok(())
}
}
उपरोक्त कोड में, हम अपने token sale प्रोग्राम के लिए कॉन्स्टेंट्स को परिभाषित करते हैं: TOKENS_PER_SOL = 100 और SUPPLY_CAP = 1000 (9 डेसीमल्स के साथ)।
इसके बाद, हमारे फ़ंक्शन के लिए Initialize अकाउंट स्ट्रक्ट (struct) जोड़ें। इसमें निम्नलिखित अकाउंट्स शामिल हैं:
admin: वह अकाउंट जो ट्रांजेक्शन फीस का भुगतान करता है और प्रोग्राम एडमिनिस्ट्रेटर के रूप में कार्य करता हैadmin_config: यह एक प्रोग्राम-स्वामित्व (program-owned) वाला अकाउंट है जो एडमिन की पब्लिक की (public key) को स्टोर करता है, ताकि बाद में निकासी के दौरान हम सत्यापित कर सकें कि साइन करने वाला (signer) वही एडमिन है (जैसे Solidity मेंmsg.sender == adminकी जांच करना, जहाँadminएक स्टेट वेरिएबल है जो एडमिन की पब्लिक की को स्टोर करता है)।mint: एक सेल्फ-रेफरेंशियल मिंट PDA जो टोकन मिंट और अपनी खुद की अथॉरिटी दोनों के रूप में कार्य करता है (हम इस कांसेप्ट को बाद में समझाएंगे)treasury: एक PDA जो टोकन सेल से एकत्र किए गए SOL को रखता है- अंत में, हम Token Program और System Program पास करते हैं जिनके साथ हम इंटरैक्ट करते हैं।
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub admin: Signer<'info>, // The transaction signer
#[account(
init,
payer = admin,
space = 8+AdminConfig::INIT_SPACE, // 8 is for the discriminator
)]
pub admin_config: Account<'info, AdminConfig>,
#[account(
init,
payer = admin,
seeds = [b"token_mint"],
bump,
mint::decimals = 9,
mint::authority = mint.key(),
)]
pub mint: Account<'info, Mint>,
/// CHECK: PDA for treasury
#[account(
seeds = [b"treasury"],
bump
)]
pub treasury: AccountInfo<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
// Stores the admin public key
#[account]
#[derive(InitSpace)] // This is a derive attribute macro provided by anchor, it calculates the space needed for the account and gives us access to AdminConfig::INIT_SPACE, as used above
pub struct AdminConfig {
pub admin: Pubkey,
}
#[error_code]
pub enum Errors {
#[msg("Max token supply limit reached")]
SupplyLimit,
#[msg("Math overflow")]
Overflow,
#[msg("Only admin can withdraw")]
UnauthorizedAccess,
#[msg("Not enough SOL in treasury")]
InsufficientFunds,
}
Initialize स्ट्रक्ट अकाउंट्स को समझना
आइए Initialize अकाउंट स्ट्रक्ट में प्रत्येक अकाउंट को विस्तार से समझें और उनके उद्देश्य को जानें:
admin config
admin_config: यह अकाउंट एडमिन की पब्लिक की (जो AdminConfig स्ट्रक्ट द्वारा परिभाषित है) को रखता है और इसका उपयोग यह सुनिश्चित करने के लिए किया जाता है कि केवल एडमिन ही ट्रेजरी से SOL निकाल सके।


mint account
यह हमारे SPL टोकन (बेचे जा रहे टोकन) के लिए मिंट अकाउंट है। हम इसे एक PDA के रूप में बनाते हैं ताकि प्रोग्राम बाद में इसके लिए साइन कर सके (हम इस लेख में इसे बाद में समझाएंगे)।
यह अकाउंट ऑन-चेन (on-chain) तब तक मौजूद नहीं होता जब तक हम initialize को कॉल नहीं करते। उस कॉल में, Anchor निम्नलिखित कार्य करेगा:
- सीड
"token_mint"और प्रोग्राम ID के साथ मिंट PDA एड्रेस की गणना करेगा mint::decimals = 9के साथ अकाउंट बनाएगा (जैसा कि नीचे दिखाया गया है, यही हमने सेट किया है)- मिंट की अथॉरिटी को उसी पर सेट करेगा (
mint::authority = mint.key())। यह हिस्सा महत्वपूर्ण है क्योंकि PDA को खुद की अथॉरिटी बनाकर, केवल हमारा प्रोग्राम, उसी सीड और बम्प (bump) का उपयोग करके,mint_toनिर्देशों (instructions) पर साइन कर सकता है (फिर से, हम समझाएंगे कि यह कैसे काम करता है बाद में इस लेख में)।
treasury
इस PDA का उपयोग विशेष रूप से सेल के दौरान उपयोगकर्ताओं द्वारा भेजे गए SOL (lamports) को रखने के लिए किया जाता है।

अब programs/token_sale/Cargo.toml फ़ाइल को निम्नलिखित के साथ अपडेट करें।
[package]
name = "token_sale"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "token_sale"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] # "anchor-spl/idl-build" was added
[dependencies]
anchor-lang = "0.31.0"
anchor-spl = "0.31.0" # THIS WAS ADDED
अब हमारे प्रोग्राम के लिए टेस्ट अपडेट करें।
यह टेस्ट पिछले ट्यूटोरियल में देखे गए टेस्ट के बहुत समान है। यह बस आवश्यक अकाउंट्स के साथ हमारे प्रोग्राम के initialize निर्देश को कॉल करता है और नए बने मिंट अकाउंट (टोकन) के गुणों (properties) को असर्ट (assert) करता है।
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import {
createAssociatedTokenAccount,
getAccount,
getMint,
TOKEN_PROGRAM_ID
} from "@solana/spl-token";
import * as web3 from "@solana/web3.js";
import { assert } from "chai";
import { TokenSale } from "../target/types/token_sale";
describe("token_sale", async () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.TokenSale as Program<TokenSale>;
const connection = provider.connection;
const adminKp = provider.wallet.payer;
const buyer = adminKp; // Using the same keypair as both admin and buyer for testing
const TOKENS_PER_SOL = 100;
// Generate keypair for admin config account (will be passed as signer to authorize adminConfig account creation)
const adminConfigKp = web3.Keypair.generate();
let mint: anchor.web3.PublicKey;
let treasuryPda: anchor.web3.PublicKey;
let buyerAta: anchor.web3.PublicKey;
it("creates mint", async () => {
[mint] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("token_mint")],
program.programId
);
[treasuryPda] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("treasury")],
program.programId
);
const tx = await program.methods
.initialize()
.accounts({
admin: adminKp.publicKey,
adminConfig: adminConfigKp.publicKey,
mint: mint,
treasury: treasuryPda,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([adminKp, adminConfigKp])
.rpc();
console.log("initialize tx:", tx);
const mintInfo = await getMint(connection, mint);
assert.equal(mintInfo.mintAuthority.toBase58(), mint.toBase58());
assert.equal(Number(mintInfo.supply), 0);
assert.equal(mintInfo.decimals, 9);
});
});
डिपेंडेंसी अपडेट करने के लिए npm install @solana/spl-token रन करें।
टेस्ट रन करें, और यह पास हो जाता है।

1 SOL के साथ 100 टोकन खरीदना
हमने अपना Token Sale प्रोग्राम सेट कर लिया है। अब हम बिक्री के लिए नए टोकन यूनिट्स को मिंट करने के लिए एक फ़ंक्शन जोड़ेंगे, ताकि उपयोगकर्ता हमारे टोकन खरीद सकें।
यह कोड निम्नलिखित कार्य करता है:
lamportइनपुट के आधार पर मिंट किए जाने वाले टोकन की संख्या की गणना करता है।- जांचता है कि हम टोटल सप्लाई से अधिक तो नहीं जा रहे हैं।
- SOL को खरीदार से ट्रेजरी में ट्रांसफर करता है।
- साइनर सीड्स (signer seeds) तैयार करता है ताकि प्रोग्राम मिंट PDA की ओर से साइन कर सके (नीचे दिए गए कोड ब्लॉक के बाद इस पर अधिक जानकारी)।
- मिंट अकाउंट को उसकी खुद की अथॉरिटी के रूप में रखकर मिंट निर्देश को सेट अप करता है।
- साइनर सीड्स का उपयोग करके एक CPI कॉन्टेक्स्ट (CPI context) बनाता है।
- खरीदार के टोकन अकाउंट में टोकन मिंट करता है।
pub fn mint(ctx: Context<MintTokens>, lamports: u64) -> Result<()> {
// Calculate how many tokens to mint (lamports * TOKENS_PER_SOL)
let amount = lamports
.checked_mul(TOKENS_PER_SOL)
.ok_or(Errors::Overflow)?; // If overflow, return error
// Ensure we don't exceed the max supply
let current_supply = ctx.accounts.mint.supply;
let new_supply = current_supply.checked_add(amount).ok_or(Errors::Overflow)?; // If overflow, return error
require!(new_supply <= SUPPLY_CAP, Errors::SupplyLimit);
// Send SOL to treasury
let transfer_instruction = Transfer {
from: ctx.accounts.buyer.to_account_info(),
to: ctx.accounts.treasury.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
transfer_instruction,
);
transfer(cpi_context, lamports)?;
// Create signer seeds for the mint PDA
let bump = ctx.bumps.mint;
let signer_seeds: &[&[&[u8]]] = &[&[b"token mint".as_ref(), &[bump]]];
// Setup mint instruction with mint as its own authority
let mint_to_instruction = MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.buyer_token_account.to_account_info(),
authority: ctx.accounts.mint.to_account_info(),
};
// Create CPI context with `new_with_signer` - allows our token sale program to sign for the mint PDA. This works because the Solana runtime verifies that our program derived the mint PDA with these seeds and bump
// See here for more: <https://github.com/solana-foundation/developer-content/blob/main/content/guides/getstarted/how-to-cpi-with-signer.md>
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
mint_to_instruction,
signer_seeds,
);
mint_to(cpi_ctx, amount)?;
Ok(())
}
हमने मिंट को उसकी खुद की अथॉरिटी के रूप में क्यों सेट किया
ऊपर दिए गए mint() फ़ंक्शन में, आप देख सकते हैं कि हम टोकन मिंट करने के लिए CpiContext::new_with_signer का उपयोग कैसे करते हैं। यह इसलिए काम करता है क्योंकि हमने पहले मिंट अकाउंट को कैसे सेट किया था। याद करें कि इनिशियलाइज़ेशन के दौरान, हमने mint::authority = mint.key() सेट किया था, जिससे मिंट PDA अपनी खुद की अथॉरिटी बन गया था।
यहां बताया गया है कि यह पैटर्न आवश्यक क्यों है।
टोकन मिंटिंग अथॉरिटीज की चुनौती
टोकन मिंटिंग के लिए अथॉरिटी कण्ट्रोल की आवश्यकता होती है। आम तौर पर, आप मिंट अथॉरिटी के रूप में एक विशिष्ट की-पेयर (keypair) असाइन करते हैं, और उस की-पेयर को हर mint_to निर्देश पर साइन करने की आवश्यकता होती है। हालांकि यह सुरक्षा प्रदान करता है, लेकिन यह एक व्यावहारिक समस्या पैदा करता है: हमें इस की-पेयर को मैनेज करना होगा और यह सुनिश्चित करना होगा कि यह हर मिंट ऑपरेशन पर साइन करने के लिए उपलब्ध हो।
हालाँकि, यह तरीका स्वचालित (automated) टोकन सेल के लिए उपयुक्त नहीं है। उपयोगकर्ता तब तक टोकन नहीं खरीद सकते जब तक कि अथॉरिटी की-पेयर प्रत्येक मिंटिंग पर साइन करने के लिए उपलब्ध न हो। यह एक परमिशनलेस (permissionless) सिस्टम बनाने के उद्देश्य को विफल करता है।
PDAs अथॉरिटी की समस्या को कैसे हल करते हैं
पारंपरिक की-पेयर का उपयोग करने के बजाय, हम मिंट PDA को उसकी खुद की अथॉरिटी बनाते हैं। यह पहली बार में भ्रमित करने वाला लगता है: कोई PDA ट्रांजेक्शन पर साइन कैसे कर सकता है जब PDAs के पास प्राइवेट कीज़ (private keys) नहीं होती हैं?
इसका समाधान PDA साइनिंग में है। जब हमारा प्रोग्राम टोकन मिंट करना चाहता है, तो यह CpiContext::new_with_signer का उपयोग उन्हीं सटीक सीड्स के साथ करता है जिनका उपयोग मिंट PDA ("token_mint" + bump) बनाने के लिए किया गया था। Solana रनटाइम यह पहचानता है कि हमारे प्रोग्राम ने इन विशिष्ट सीड्स के साथ इस PDA को डिराइव (derive) किया है, इसलिए यह हमारे प्रोग्राम को उस PDA के लिए साइनर के रूप में कार्य करने की अनुमति देता है।
यह एक उपयोगी पैटर्न बनाता है:
- मिंट अथॉरिटी PDA एड्रेस है (न कि कोई की-पेयर)
- केवल हमारा प्रोग्राम सही सीड्स का उपयोग करके इस PDA के लिए “साइन” कर सकता है
- हमारा प्रोग्राम अप्रत्यक्ष रूप से मिंट अथॉरिटी के रूप में कार्य करता है (क्योंकि यह मिंट PDA का मालिक है) और किसी समर्पित बाहरी साइनर (external signer) की आवश्यकता के बिना डिमांड पर टोकन मिंट कर सकता है
- कोई अन्य व्यक्ति इन टोकन्स को मिंट नहीं कर सकता, भले ही वे सीड्स का पता लगा लें (यहां टोकन केवल तभी मिंट किया जा सकता है जब कोई उपयोगकर्ता इसे हमारे प्रोग्राम के
mintफ़ंक्शन के माध्यम से खरीदता है)
एडमिन को मिंट अथॉरिटी क्यों न बनाएं?
हम mint::authority = admin.key() सेट कर सकते थे, और एडमिन को मिंट अथॉरिटी बना सकते थे। लेकिन जैसा कि पहले कहा गया है, तब एडमिन को हर मिंट ट्रांजेक्शन पर साइन करना पड़ता।
अब चलिए प्रोग्राम के साथ आगे बढ़ते हैं और MintTokens अकाउंट्स स्ट्रक्ट जोड़ते हैं।
MintTokens टोकन सेल/मिंट के दौरान शामिल अकाउंट्स को निर्दिष्ट करता है।
buyer: वह अकाउंट जो टोकन खरीदता है और ट्रांजेक्शन पर साइन भी करता है।mint: बिक्री के लिए हमारा SPL टोकन।buyer_ata: मिंट किए गए टोकन यूनिट्स प्राप्त करने के लिए खरीदार का एसोसिएटेड टोकन अकाउंट (associated token account)।treasury: टोकन सेल से SOL प्राप्त करने वाला अकाउंट।- अंतिम दो अकाउंट्स, Token program (मिंटिंग के लिए) और System program (SOL ट्रांसफर के लिए), वे नेटिव प्रोग्राम्स हैं जिनके साथ हम इंटरैक्ट करते हैं।
#[derive(Accounts)]
pub struct MintTokens<'info> {
#[account(mut)]
pub buyer: Signer<'info>,
#[account(
mut,
seeds = [b"token mint"],
bump
)]
pub mint: Account<'info, Mint>,
#[account(
mut,
token::mint = mint,
token::authority = buyer,
)]
pub buyer_ata: Account<'info, TokenAccount>,
/// CHECK: PDA for treasury
#[account(
mut,
seeds = [b"treasury"],
bump
)]
pub treasury: AccountInfo<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
प्रोग्राम कोड में निम्नलिखित कस्टम एरर (custom error) जोड़ें। यहां के एरर्स का उपयोग mint फ़ंक्शन और हमारे द्वारा जोड़े जाने वाले अगले फ़ंक्शन में किया जाता है।
#[error_code]
pub enum Errors {
#[msg("Max token supply limit reached")]
SupplyLimit,
#[msg("Math overflow")]
Overflow,
#[msg("Only admin can withdraw")]
UnauthorizedAccess,
#[msg("Not enough SOL in treasury")]
InsufficientFunds,
}
अब नीचे दिए गए कोड के साथ टेस्ट को अपडेट करें।
यह टेस्ट निम्नलिखित कार्य करता है:
- हमारे टोकन के लिए खरीदार का ATA बनाता है
- हमारे प्रोग्राम के
mintफ़ंक्शन को कॉल करके 1 SOL मूल्य के हमारे टोकन (100 टोकन) खरीदता है - यह असर्ट करता है कि ट्रेड से ट्रेजरी में SOL की सही मात्रा आ गई है
- यह असर्ट करता है कि खरीदारी के बाद खरीदार के ATA में मिंट किए गए टोकन की सही मात्रा है
it("buys tokens", async () => {
const solToSend = new anchor.BN(1e9); // 1 SOL
const expectedTokenAmount = Number(solToSend) * TOKENS_PER_SOL; // 1*100 tokens
const initialTreasuryBalance = await connection.getBalance(treasuryPda);
// Create buyer's ata
buyerAta = await createAssociatedTokenAccount(
provider.connection,
buyer,
mint,
buyer.publicKey,
undefined,
TOKEN_PROGRAM_ID
);
const buyerAtaInfo = await getAccount(connection, buyerAta, undefined, TOKEN_PROGRAM_ID);
const initialBuyerAtaBalance = Number(buyerAtaInfo.amount);
// Call our program's mint function to purchase tokens
const tx = await program.methods
.mint(solToSend)
.accounts({
buyer: buyer.publicKey,
mint: mint,
buyerAta: buyerAta,
treasury: treasuryPda,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("mint tx:", tx);
console.log("Sent", lamportsToSol(solToSend), "SOL, expecting", toDisplayAmount(expectedTokenAmount), "tokens");
const newTreasuryBalance = await connection.getBalance(treasuryPda);
assert.equal(
newTreasuryBalance - initialTreasuryBalance,
Number(solToSend),
"SOL was not correctly transferred to treasury"
);
const updatedBuyerAtaInfo = await getAccount(connection, buyerAta, undefined, TOKEN_PROGRAM_ID);
const newBuyerAtaBalance = Number(updatedBuyerAtaInfo.amount);
assert.equal(
newBuyerAtaBalance - initialBuyerAtaBalance,
expectedTokenAmount,
"Tokens were not correctly minted"
);
});
अब टेस्ट रन करें, यह पास हो जाता है।

सुनिश्चित करें कि सप्लाई कैप तक पहुंचने पर टोकन सेल समाप्त हो जाए
याद रखें कि सप्लाई कैप 1000 टोकन है और हम ऊपर दिए गए टेस्ट में 1 SOL के साथ 100 मिंट कर रहे हैं। हम 9.2 SOL के साथ 920 टोकन मिंट करने का प्रयास करेंगे ताकि यह पुष्टि हो सके कि टोकन सेल सप्लाई लिमिट से अधिक मिंट करने से रोकती है।
निम्नलिखित टेस्ट ब्लॉक जोड़ें।
यह टेस्ट यह असर्ट करता है कि जब हम 1000 की लिमिट से अधिक खरीदने का प्रयास करते हैं तो टोकन सेल फेल हो जाती है।
it("stops minting when supply cap is reached", async () => {
const mintInfo = await getMint(connection, mint, undefined, TOKEN_PROGRAM_ID);
const currentSupply = Number(mintInfo.supply);
const SUPPLY_CAP = toRawTokenAmount(1000);
const remainingSupply = SUPPLY_CAP - currentSupply;
console.log(`Current supply: ${toDisplayAmount(currentSupply)} tokens, Remaining: ${toDisplayAmount(remainingSupply)} tokens`);
const tokensToMint = remainingSupply + toRawTokenAmount(20);
const solToSend = new anchor.BN(Math.ceil(tokensToMint / TOKENS_PER_SOL));
console.log(`Trying to mint ${toDisplayAmount(tokensToMint)} tokens by sending ${lamportsToSol(solToSend)} SOL`);
try {
await program.methods
.mint(solToSend)
.accounts({
buyer: buyer.publicKey,
mint: mint,
buyerAta: buyerAta,
treasury: treasuryPda,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
assert.fail("Minting succeeded but should have failed due to supply cap");
} catch (error) {
console.log("Expected error:", error.toString().substring(0, 150) + "...");
assert.include(
error.toString(),
"SupplyLimit",
"Expected supply limit error not received"
);
console.log("Supply cap limit correctly enforced");
}
});
टेस्ट रन करें और इसे पास होना चाहिए।

ट्रेजरी अकाउंट का SOL बैलेंस निकालना
अब तक, हमने अपने प्रोग्राम को इनिशियलाइज़ करने और अपने टोकन खरीदने के लिए टेस्ट सेट किए हैं। अब, हम टोकन सेल प्रोग्राम एडमिन (हमारा अकाउंट) के लिए ट्रेजरी अकाउंट से एकत्र किए गए SOL को निकालने (withdraw) का कोड जोड़ेंगे और इस कार्यक्षमता का परीक्षण करेंगे।
टोकन सेल प्रोग्राम में नीचे दिया गया withdraw_funds फ़ंक्शन जोड़ें। यह निम्नलिखित कार्य करता है:
- जांचता है कि निकासी के लिए ट्रेजरी में पर्याप्त बैलेंस है या नहीं।
- साइनर सीड्स तैयार करता है ताकि प्रोग्राम ट्रेजरी PDA की ओर से साइन कर सके।
- System Program के
transferनिर्देश को कॉल करने के लिए CPI कॉन्टेक्स्ट सेट करता है। - प्रोग्राम को ट्रेजरी के लिए साइन करने की अनुमति देने के लिए
CpiContext::new_with_signerका उपयोग करता है। - ट्रेजरी से एडमिन वॉलेट में SOL (
lamports) ट्रांसफर करता है।
नोट: यह Solana के System transfer का उपयोग करता है, न कि SPL टोकन ट्रांसफर का, क्योंकि हम SPL टोकन के बजाय SOL (नेटिव टोकन) ट्रांसफर कर रहे हैं। SPL ट्रांसफर के लिए टोकन अकाउंट्स के साथ इंटरैक्ट करने की आवश्यकता होती है, जबकि SOL ट्रांसफर को सीधे सिस्टम प्रोग्राम द्वारा संभाला जाता है।
pub fn withdraw_funds(ctx: Context<WithdrawFunds>, amount: u64) -> Result<()> {
// Check balance
let treasury_balance = ctx.accounts.treasury.lamports();
require!(treasury_balance >= amount, Errors::InsufficientFunds);
// Create signer seeds for PDA
let bump = ctx.bumps.treasury;
let signer_seeds: &[&[&[u8]]] = &[&[b"treasury".as_ref(), &[bump]]];
// Prepare the CPI context to System Program::transfer
// DO NOT CONFUSE THIS WITH SPL TOKEN TRANSFER
let transfer_instruction = Transfer {
from: ctx.accounts.treasury.to_account_info(),
to: ctx.accounts.admin.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.system_program.to_account_info(),
transfer_instruction,
signer_seeds,
);
transfer(cpi_ctx, amount)?; // DO NOT CONFUSE THIS WITH SPL TOKEN TRANSFER
Ok(())
}
अब WithdrawFunds अकाउंट स्ट्रक्ट जोड़ें, जिसका उपयोग withdraw_funds फ़ंक्शन द्वारा किया जाता है। इसमें निम्नलिखित अकाउंट्स शामिल हैं:
admin: ट्रांजेक्शन साइनर, और हमारा एडमिन अकाउंट।admin_config: वह अकाउंट जो एडमिन की पब्लिक की को स्टोर करता है, जिसमें यह सत्यापित करने के लिए एक कंस्ट्रेंट (constraint) होता है कि साइनर अधिकृत (authorized) है। हम इसे पास करते हैं क्योंकि हमें यह जांचना होता है कि वर्तमान साइनर इनिशियलाइज़ेशन के दौरान स्टोर की गई एडमिन की (admin key) से मेल खाता है या नहीं।treasury: वह म्यूटेबल (mutable) ट्रेजरी PDA जो निकाले जाने वाले SOL को रखता है।system_program: SOL ट्रांसफर को संभालने के लिए System Program।
#[derive(Accounts)]
pub struct WithdrawFunds<'info> {
#[account(mut)]
pub admin: Signer<'info>,
#[account(
constraint = admin_config.admin == admin.key() @ Errors::UnauthorizedAccess // Ensure the signer is authorized
)]
pub admin_config: Account<'info, AdminConfig>,
/// CHECK: PDA for treasury
#[account(
mut,
seeds = [b"treasury"],
bump
)]
pub treasury: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
इसके बाद, टेस्ट को अपडेट करें।
यह टेस्ट ब्लॉक ट्रेजरी PDA से एडमिन की-पेयर में ट्रेजरी बैलेंस का आधा हिस्सा निकालता है और असर्ट करता है कि एडमिन का बैलेंस निकाली गई राशि के बराबर बढ़ गया है, जबकि ट्रेजरी का बैलेंस उसी राशि से घट गया है।
it("allows the admin to withdraw funds from treasury", async () => {
const initialAdminBalance = await connection.getBalance(adminKp.publicKey);
const initialTreasuryBalance = await connection.getBalance(treasuryPda);
console.log("Initial treasury balance:", lamportsToSol(initialTreasuryBalance), "SOL");
console.log("Initial admin balance:", lamportsToSol(initialAdminBalance), "SOL");
assert.isAbove(
initialTreasuryBalance,
0,
"Treasury should have funds from previous tests"
);
const amountToWithdraw = new anchor.BN(Math.floor(initialTreasuryBalance / 2)); // Withdraw half of the treasury balance
try {
const tx = await program.methods
.withdrawFunds(amountToWithdraw)
.accounts({
admin: adminKp.publicKey,
adminConfig: adminConfigKp.publicKey,
treasury: treasuryPda,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("withdrawFunds tx:", tx);
const newAdminBalance = await connection.getBalance(adminKp.publicKey);
const newTreasuryBalance = await connection.getBalance(treasuryPda);
console.log("New treasury balance:", lamportsToSol(newTreasuryBalance), "SOL");
console.log("New admin balance:", lamportsToSol(newAdminBalance), "SOL");
// assert that the treasury balance decreased by the amount we withdrew, which is half of the initial treasury balance
assert.approximately(
initialTreasuryBalance - newTreasuryBalance,
Number(amountToWithdraw),
10000,
"Treasury balance did not decrease by approximately the correct amount"
);
// assert that the admin balance increased by the amount we withdrew
assert.isTrue(
newAdminBalance > initialAdminBalance,
"Admin balance did not increase after withdrawal"
);
} catch (error) {
console.error("Error in withdraw test:", error);
throw error;
}
});
टेस्ट रन करें। हम देख सकते हैं कि ट्रेजरी अकाउंट का SOL बैलेंस कम हो जाता है और हमारा बैलेंस बढ़ जाता है।

टेस्ट करें कि गैर-एडमिन (non-admins) ट्रेजरी से निकासी न कर सकें
यह पुष्टि करने के लिए कि अनधिकृत (unauthorized) अकाउंट ट्रेजरी अकाउंट बैलेंस से SOL नहीं निकाल सकते, निम्नलिखित टेस्ट ब्लॉक जोड़ें।
यहाँ एक अनधिकृत अकाउंट का मतलब है, कोई भी ऐसा अकाउंट जिसकी पब्लिक की उस पब्लिक की से मेल नहीं खाती जिसे हमने पहले इनिशियलाइज़ेशन के दौरान adminConfig अकाउंट में स्टोर किया था।
it("prevents non-admins from withdrawing funds", async () => {
const nonAdminKeypair = web3.Keypair.generate();
const amountToWithdraw = new anchor.BN(1e8);
try {
await program.methods
.withdrawFunds(amountToWithdraw)
.accounts({
admin: nonAdminKeypair.publicKey,
adminConfig: adminConfigKp.publicKey,
treasury: treasuryPda,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([nonAdminKeypair])
.rpc();
assert.fail("Non-admin was able to withdraw funds, but should be prohibited");
} catch (error) {
console.log("Expected error occurred:", error.toString().substring(0, 150) + "...");
assert.include(
error.toString(),
"UnauthorizedAccess",
"Expected unauthorized access error not received"
);
console.log("Non-admin withdrawal was correctly rejected");
}
});
टेस्ट रन करें।

लॉग्स से हम देख सकते हैं कि जब हम किसी अनधिकृत अकाउंट से फंड निकालने का प्रयास करते हैं तो Anchor एक एरर (error) थ्रो करता है।
सारांश
इस ट्यूटोरियल में, हमने एक Token Sale प्रोग्राम (सप्लाई कैप के साथ) बनाकर SPL टोकन का एक व्यावहारिक उपयोग (practical use case) प्रदर्शित किया है, जो उपयोगकर्ताओं को एक निश्चित दर पर हमारे SPL टोकन के बदले SOL का आदान-प्रदान करने की अनुमति देता है।
यह प्रोग्राम दो प्रमुख PDAs का उपयोग करता है: एक सेल्फ-रेफरेंशियल मिंट PDA और एक ट्रेजरी PDA। हमने इनिशियलाइज़ेशन के दौरान mint::authority = mint.key() के साथ मिंट को उसकी खुद की अथॉरिटी के रूप में सेट किया है, जिससे एक अलग मिंट अथॉरिटी अकाउंट की आवश्यकता समाप्त हो जाती है। यह पैटर्न सुनिश्चित करता है कि कोई भी हमारे प्रोग्राम के माध्यम से टोकन खरीद/मिंट कर सकता है, बिना हमें हर बार मिंटिंग को ऑथोराइज़ किए।
ठीक से डिराइव (derive) किए गए सीड्स के साथ CpiContext::new_with_signer का उपयोग करके, हमारा प्रोग्राम खरीदारों के लिए टोकन मिंट कर सकता है और अप्रत्यक्ष रूप से मिंट अथॉरिटी के रूप में कार्य कर सकता है।
यह लेख Solana पर ट्यूटोरियल सीरीज़ का हिस्सा है।