इस ट्यूटोरियल में, हम Solana पर एक साधारण बैंक प्रोग्राम बनाएंगे जिसमें वे सभी बुनियादी सुविधाएं होंगी जिनकी आप एक नियमित बैंक से अपेक्षा करते हैं। उपयोगकर्ता खाते बना सकते हैं, बैलेंस चेक कर सकते हैं, फंड जमा कर सकते हैं, और आवश्यकता पड़ने पर अपने फंड निकाल सकते हैं। जमा किया गया SOL हमारे प्रोग्राम के स्वामित्व वाले एक बैंक PDA में स्टोर किया जाएगा।
यहां उस चीज़ का एक Solidity प्रतिनिधित्व (representation) दिया गया है जिसे हम Anchor के साथ बनाने का प्रयास कर रहे हैं।
Solidity कोड में, हम स्टेट (state) को होल्ड करने के लिए एक User और Bank struct को परिभाषित करते हैं। ये उन अलग-अलग अकाउंट्स को दर्शाते हैं जिन्हें हम Solana प्रोग्राम में बनाएंगे। कोड में initialize फंक्शन बैंक के कुल डिपॉजिट को शून्य पर सेट करता है, लेकिन Solana प्रोग्राम में इसमें बैंक अकाउंट को डिप्लॉय करना और इनिशियलाइज़ करना शामिल होगा। इसी तरह, Solidity कोड में createUserAccount फंक्शन केवल स्टोरेज को अपडेट करता है, जबकि Solana में यह ऑन-चेन एक नया यूज़र अकाउंट बनाएगा और इनिशियलाइज़ करेगा। यह भी ध्यान दें कि डिपॉजिट और विड्रॉल यूज़र और बैंक दोनों के स्टेट को कैसे अपडेट करते हैं, साथ ही कस्टम एरर्स उन नियमों को लागू करते हैं जब बैलेंस अपर्याप्त या अमान्य होता है।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract BasicBank {
// Custom errors
error ZeroAmount();
error InsufficientBalance();
error Overflow();
error Underflow();
error InsufficientFunds();
error UnauthorizedAccess();
// Bank struct to track total deposits across all users
struct Bank {
uint256 totalDeposits;
}
// User account struct to store individual balances
struct UserAccount {
address owner;
uint256 balance;
}
// The bank state
Bank public bank;
// Mapping from user address to their user account
mapping(address => UserAccount) public userAccounts;
// Initialize the bank
function initialize() external {
bank.totalDeposits = 0;
}
// Create a user account
function createUserAccount() external {
// Ensure account doesn't already exist
require(userAccounts[msg.sender].owner == address(0), "Account already created");
// Initialize the user account
userAccounts[msg.sender].owner = msg.sender;
userAccounts[msg.sender].balance = 0;
}
// Deposit ETH into the bank
function deposit(uint256 amount) external payable {
// Ensure amount is greater than zero
if (amount == 0) {
revert ZeroAmount();
}
// Ensure account exists and is owned by caller
if (userAccounts[msg.sender].owner != msg.sender) {
revert UnauthorizedAccess();
}
// Ensure the correct amount was sent
require(msg.value == amount, "Amount mismatch");
// Update user balance with checks for overflow
uint256 newUserBalance = userAccounts[msg.sender].balance + amount;
if (newUserBalance < userAccounts[msg.sender].balance) {
revert Overflow();
}
userAccounts[msg.sender].balance = newUserBalance;
// Update bank total deposits with checks for overflow
uint256 newTotalDeposits = bank.totalDeposits + amount;
if (newTotalDeposits < bank.totalDeposits) {
revert Overflow();
}
bank.totalDeposits = newTotalDeposits;
}
// Withdraw ETH from the bank
function withdraw(uint256 amount) external {
// Ensure amount is greater than zero
if (amount == 0) {
revert ZeroAmount();
}
// Ensure account exists and is owned by caller
if (userAccounts[msg.sender].owner != msg.sender) {
revert UnauthorizedAccess();
}
// Check if user has enough balance
if (userAccounts[msg.sender].balance < amount) {
revert InsufficientBalance();
}
// Update user balance with checks for underflow
uint256 newBalance = userAccounts[msg.sender].balance - amount;
userAccounts[msg.sender].balance = newBalance;
// Update bank total deposits with checks for underflow
uint256 newTotalDeposits = bank.totalDeposits - amount;
if (newTotalDeposits > bank.totalDeposits) {
revert Underflow();
}
bank.totalDeposits = newTotalDeposits;
// Transfer ETH to the user
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
// Get the balance of the caller's bank account
function getBalance() external view returns (uint256) {
// Ensure account exists and is owned by caller
if (userAccounts[msg.sender].owner != msg.sender) {
revert UnauthorizedAccess();
}
return userAccounts[msg.sender].balance;
}
}
Basic Bank प्रोग्राम बनाना
कोड में गहराई से जाने से पहले, यहां बताया गया है कि हमारा बुनियादी बैंक प्रोग्राम कैसे काम करेगा। हमें निम्नलिखित कार्यक्षमताओं (functionalities) की आवश्यकता होगी:
- बैंक को सेट अप और इनिशियलाइज़ करने का एक तरीका
- उपयोगकर्ताओं के लिए अपने स्वयं के खाते बनाने का एक तरीका
- उपयोगकर्ताओं के लिए फंड जमा करने का एक तरीका
- उपयोगकर्ताओं के लिए फंड निकालने का एक तरीका
- बैलेंस चेक करने का एक तरीका
हमें निम्नलिखित स्टोरेज की आवश्यकता होगी:
- सभी उपयोगकर्ताओं के कुल डिपॉजिट को ट्रैक करने के लिए एक केंद्रीय अकाउंट
- व्यक्तिगत अकाउंट, जो उपयोगकर्ताओं द्वारा अपने बैलेंस और स्वामित्व (ownership) को ट्रैक करने के लिए बनाए जा सकते हैं
ये सभी तत्व मिलकर हमारे बुनियादी बैंक प्रोग्राम को बनाते हैं।
तो चलिए, शुरू करते हैं।
Bank PDA और उपयोगकर्ता अकाउंट
basic_bank नाम का एक नया Anchor प्रोजेक्ट बनाएं, और नीचे दिया गया प्रोग्राम कोड जोड़ें। प्रोग्राम दो निर्देशों (instructions) को परिभाषित करता है: initialize, जो डिपॉजिट रखने के लिए एक बैंक PDA बनाता है, और create_user_account, जो प्रत्येक उपयोगकर्ता के कुल डिपॉजिट को ट्रैक करने के लिए एक उपयोगकर्ता-विशिष्ट (user-specific) PDA सेट करता है। उपयोगकर्ताओं द्वारा जमा किया गया वास्तविक SOL बैंक PDA में स्टोर किया जाएगा।
हमारे पास उपयोगकर्ताओं के लिए एक समर्पित अकाउंट है क्योंकि Solana में, सभी प्रोग्राम डेटा को अपने स्वयं के अकाउंट के रूप में मौजूद होना चाहिए। Ethereum के विपरीत, जहाँ हम डिपॉजिट को स्टोर करने के लिए मैपिंग का उपयोग कर सकते हैं (mapping(address => deposited_amount)), Solana को स्टेट स्टोर करने के लिए स्पष्ट अकाउंट आवंटन (explicit account allocation) की आवश्यकता होती है। इसलिए, हम उपयोगकर्ता का पता (address) और उनकी जमा राशि (deposit amount) दोनों को स्टोर करने के लिए एक user_account PDA बनाते हैं।
use anchor_lang::prelude::*;
use anchor_lang::solana_program::rent::Rent;
use anchor_lang::solana_program::system_instruction;
use anchor_lang::solana_program::program as solana_program;
declare_id!("u9tNA22L1oRZyF3RKoPVUYTAc1zCYSC5BySKFddZnfN"); // RUN ANCHOR SYNC TO UPDATE YOUR PROGRAM ID
#[program]
pub mod basic_bank {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// Initialize the bank account
let bank = &mut ctx.accounts.bank;
bank.total_deposits = 0;
msg!("Bank initialized");
Ok(())
}
pub fn create_user_account(ctx: Context<CreateUserAccount>) -> Result<()> {
// Initialize the user account
let user_account = &mut ctx.accounts.user_account;
user_account.owner = ctx.accounts.user.key();
user_account.balance = 0;
msg!("User account created for: {:?}", user_account.owner);
Ok(())
}
}
आइए हमारे बुनियादी बैंक प्रोग्राम के लिए अकाउंट structs को परिभाषित करें। इन structs का उपयोग initialize और create_user_account दोनों फ़ंक्शंस द्वारा किया जाता है, वे हैं:
Initialize****struct, जिसमें शामिल है:bank: कुल डिपॉजिट को ट्रैक करने के लिए बनाया जा रहा नया बैंक अकाउंटpayer: ट्रांज़ैक्शन फीस और रेंट (rent) का भुगतान करने वाला अकाउंटsystem_program: नया अकाउंट बनाने के लिए आवश्यक
CreateUserAccountstruct में शामिल है:bank: मुख्य बैंक अकाउंट का संदर्भ (Reference)user_account: उपयोगकर्ता की पब्लिक की (public key) से प्राप्त एक PDA जो उनका बैलेंस स्टोर करता हैuser: वह हस्ताक्षरकर्ता (signer) जो इस अकाउंट का मालिक है और इसके निर्माण के लिए भुगतान करता हैsystem_program: अकाउंट निर्माण के लिए आवश्यक
- अंत में, हम एक
Bankऔर एकUserAccountstruct परिभाषित करते हैं जो उनके संबंधित अकाउंट्स की डेटा संरचना को परिभाषित करते हैं।Bankstruct में बैंक अकाउंट में सभी डिपॉजिट को ट्रैक करने के लिए एकtotal_depositsफ़ील्ड होता है, जबकिUserAccountstruct उपयोगकर्ता अकाउंट PDA में उपयोगकर्ता की पब्लिक की और उनके व्यक्तिगत बैलेंस को स्टोर करता है।
// ACCOUNT STRUCT TO CREATE THE BANK PDA TO STORE TO
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = payer,
space = 8 + Bank::INIT_SPACE)] // discriminator + u64
pub bank: Account<'info, Bank>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
// ACCOUNT STRUCT FOR CREATING INDIVIDUAL USER ACCOUNT
#[derive(Accounts)]
pub struct CreateUserAccount<'info> {
#[account(mut)]
pub bank: Account<'info, Bank>,
#[account(
init,
payer = user,
space = 8 + UserAccount::INIT_SPACE, // discriminator + pubkey + u64
seeds = [b"user-account", user.key().as_ref()],
bump
)]
pub user_account: Account<'info, UserAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
// BANK ACCOUNT TO TRACK TOTAL DEPOSITS ACROSS ALL USERS
#[account]
#[derive(InitSpace)]
pub struct Bank {
pub total_deposits: u64,
}
// USER-SPECIFIC ACCOUNT TO TRACK INDIVIDUAL USER BALANCES
#[account]
#[derive(InitSpace)]
pub struct UserAccount {
pub owner: Pubkey,
pub balance: u64,
}
इसके बाद, उपयोगकर्ता अकाउंट को इनिशियलाइज़ करने और बनाने के लिए नीचे दिया गया टेस्ट जोड़ें।
यह टेस्ट निम्नलिखित कार्य करता है:
- बैंक अकाउंट के लिए एक की-पेयर (keypair) जनरेट करता है
- सभी ऑपरेशन्स के लिए हस्ताक्षरकर्ता (signer) के रूप में प्रोवाइडर के वॉलेट (हमारा डिफ़ॉल्ट Anchor वॉलेट) का उपयोग करता है
- टेस्ट अमाउंट वैल्यूज़ सेट करता है (डिपॉजिट के लिए 1 SOL, विड्रॉल के लिए 0.5 SOL)
- हस्ताक्षरकर्ता की पब्लिक की के लिए उपयोगकर्ता अकाउंट के लिए एक PDA डिराइव (derive) करता है
- बैंक अकाउंट सेट अप करने के लिए
initializeको कॉल करता है और यह सत्यापित (verify) करता है कि यह शून्य बैलेंस के साथ शुरू होता है। - उपयोगकर्ता-विशिष्ट PDA को इनिशियलाइज़ करने के लिए
createUserAccountको कॉल करता है और दावा (assert) करता है कि उपयोगकर्ता का पता और बैलेंस सही ढंग से रिकॉर्ड किए गए हैं।
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Keypair, PublicKey } from "@solana/web3.js";
import { assert } from "chai";
import { BasicBank } from "../target/types/basic_bank";
describe("basic_bank", () => {
// Configure the client to use the local cluster
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.BasicBank as Program<BasicBank>;
const provider = anchor.AnchorProvider.env();
// Generate a new keypair for the bank account
const bankAccount = Keypair.generate();
// Use provider's wallet as the signer
const signer = provider.wallet;
// Test deposit amount
const depositAmount = new anchor.BN(1_000_000_000); // 1 SOL in lamports
const withdrawAmount = new anchor.BN(500_000_000); // 0.5 SOL in lamports
// Find PDA for user accounts
const [userAccountPDA] = PublicKey.findProgramAddressSync(
[Buffer.from("user-account"), signer.publicKey.toBuffer()],
program.programId,
);
it("Initializes the bank account", async () => {
// Initialize the bank account
const tx = await program.methods
.initialize()
.accounts({
bank: bankAccount.publicKey,
payer: signer.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([bankAccount])
.rpc();
console.log("Initialize transaction signature", tx);
// Fetch the bank account data
const bankData = await program.account.bank.fetch(bankAccount.publicKey);
// Verify the bank is initialized correctly
assert.equal(bankData.totalDeposits.toString(), "0");
});
it("Creates a user account", async () => {
// Create user account for the signer
const tx = await program.methods
.createUserAccount()
.accounts({
bank: bankAccount.publicKey,
userAccount: userAccountPDA,
user: signer.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Create user account transaction signature", tx);
// Fetch the user account data
const userAccountData = await program.account.userAccount.fetch(userAccountPDA);
// Verify the user account is set correctly
assert.equal(userAccountData.owner.toString(), signer.publicKey.toString());
assert.equal(userAccountData.balance.toString(), "0");
});
});
टेस्ट चलाएं, यह पास होना चाहिए।

अब हम बैंक में फंड जमा करने के लिए कोड इम्प्लीमेंट करेंगे, और इसके लिए एक टेस्ट लिखेंगे।
बैंक में SOL डिपॉजिट करना
हमारे प्रोग्राम में नीचे दिया गया deposit फंक्शन जोड़ें।
यह निम्नलिखित कार्य करता है:
- मान्य (Validate) करता है कि डिपॉजिट राशि शून्य से अधिक है
- उपयोगकर्ता के वॉलेट से बैंक अकाउंट में SOL खींचने (pull) के लिए एक System
transferनिर्देश (CPI के माध्यम से) बनाता और निष्पादित (execute) करता है - उपयोगकर्ता के
user_accountPDA में जमा राशि जोड़कर उनके अकाउंट बैलेंस को सुरक्षित रूप से बढ़ाता है - उसी राशि के साथ बैंक के कुल डिपॉजिट को अपडेट करता है
- जमा की गई राशि और उपयोगकर्ता का पता लॉग (Log) करता है
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
// Ensure deposit amount is greater than zero
require!(amount > 0, BankError::ZeroAmount);
let user = &ctx.accounts.user.key();
let bank = &ctx.accounts.bank.key();
// Transfer SOL from user to bank account using System Program
let transfer_ix = system_instruction::transfer(user, bank, amount);
solana_program::invoke(
&transfer_ix,
&[
ctx.accounts.user.to_account_info(),
ctx.accounts.bank.to_account_info(),
],
)?;
// Update user balance
let user_account = &mut ctx.accounts.user_account;
user_account.balance = user_account
.balance
.checked_add(amount)
.ok_or(BankError::Overflow)?;
// Update bank total deposits
let bank = &mut ctx.accounts.bank;
bank.total_deposits = bank
.total_deposits
.checked_add(amount)
.ok_or(BankError::Overflow)?;
msg!("Deposited {} lamports for {}", amount, user);
Ok(())
}
ध्यान दें कि कैसे हम उपयोगकर्ता के वॉलेट से बैंक में SOL खींचने (pull) के लिए सिस्टम transfer निर्देश का उपयोग करते हैं, हम बाद में इस पैटर्न पर वापस आएंगे।
अब, Deposit अकाउंट struct जोड़ें
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut)]
pub bank: Account<'info, Bank>,
#[account(
mut,
seeds = [b"user-account", user.key().as_ref()],
bump,
constraint = user_account.owner == user.key() @ BankError::UnauthorizedAccess // Ensure the signer owns the account
)]
pub user_account: Account<'info, UserAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
कस्टम BankError जोड़ें
#[error_code]
pub enum BankError {
#[msg("Amount must be greater than zero")]
ZeroAmount,
#[msg("Insufficient balance for withdrawal")]
InsufficientBalance,
#[msg("Arithmetic overflow")]
Overflow,
#[msg("Arithmetic underflow")]
Underflow,
#[msg("Insufficient funds in the bank account")]
InsufficientFunds,
#[msg("Unauthorized access to user account")]
UnauthorizedAccess,
}
अब नीचे दिए गए टेस्ट ब्लॉक के साथ प्रोग्राम टेस्ट को अपडेट करें।
डिपॉजिट टेस्ट निम्नलिखित कार्य करता है:
- यह उपयोगकर्ता और बैंक अकाउंट्स दोनों के प्रारंभिक (initial) SOL बैलेंस को रिकॉर्ड करता है
- उपयोगकर्ता से बैंक में 1 SOL का डिपॉजिट ट्रांज़ैक्शन सबमिट करता है
- सत्यापित करता है कि उपयोगकर्ता अकाउंट का बैलेंस रिकॉर्ड और बैंक का कुल डिपॉजिट रिकॉर्ड दोनों नई जमा राशि के साथ सही ढंग से अपडेट किए गए हैं
- यह जांचता है कि बैंक बैलेंस बढ़ा है और उपयोगकर्ता बैलेंस उचित रूप से कम हुआ है (ट्रांज़ैक्शन फीस को ध्यान में रखते हुए) ताकि यह पुष्टि हो सके कि वास्तविक SOL ट्रांसफर हुआ है
- अंत में, यह सभी बैलेंस परिवर्तनों को लॉग करता है
it("Deposits funds into the bank", async () => {
// Get initial SOL balances
const initialUserBalance = await provider.connection.getBalance(signer.publicKey);
const initialBankBalance = await provider.connection.getBalance(bankAccount.publicKey);
console.log(`Initial user SOL balance: ${initialUserBalance / 1e9} SOL`);
console.log(`Initial bank SOL balance: ${initialBankBalance / 1e9} SOL`);
// Deposit funds into the bank
const tx = await program.methods
.deposit(depositAmount)
.accounts({
bank: bankAccount.publicKey,
userAccount: userAccountPDA,
user: signer.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Deposit transaction signature", tx);
// Get the user's account balance
const userAccountData = await program.account.userAccount.fetch(userAccountPDA);
// Verify the tracked balance is correct
assert.equal(userAccountData.balance.toString(), depositAmount.toString());
// Verify bank total tracked deposits
const bankData = await program.account.bank.fetch(bankAccount.publicKey);
assert.equal(bankData.totalDeposits.toString(), depositAmount.toString());
// Get final SOL balances
const finalUserBalance = await provider.connection.getBalance(signer.publicKey);
const finalBankBalance = await provider.connection.getBalance(bankAccount.publicKey);
console.log(`Final user SOL balance: ${finalUserBalance / 1e9} SOL`);
console.log(`Final bank SOL balance: ${finalBankBalance / 1e9} SOL`);
// Check actual SOL transfers (accounting for tx fees)
assert.isTrue(finalBankBalance > initialBankBalance);
// User balance should be reduced by deposit amount + some tx fees
assert.isTrue(finalUserBalance < initialUserBalance - Number(depositAmount));
assert.isTrue(finalUserBalance > initialUserBalance - Number(depositAmount) - 10000); // Account for a reasonable tx fees
});
टेस्ट चलाएं, और यह पास हो जाता है।

इसके बाद, हम अपने बैंक में उपयोगकर्ता की जमा राशि को प्राप्त (retrieve) करने के लिए एक फंक्शन जोड़ेंगे।
उपयोगकर्ता का बैलेंस प्राप्त करना
नीचे दिया गया get_balance फंक्शन उपयोगकर्ता के लिए लैम्पोर्ट (lamport) बैलेंस प्राप्त करता है और लौटाता है।
pub fn get_balance(ctx: Context<GetBalance>) -> Result<u64> {
// Get user account
let user_account = &ctx.accounts.user_account;
let balance = user_account.balance;
msg!("Balance for {}: {} lamports", user_account.owner, balance);
Ok(balance)
}
GetBalance अकाउंट struct जोड़ें।
#[derive(Accounts)]
pub struct GetBalance<'info> {
pub bank: Account<'info, Bank>,
#[account(
seeds = [b"user-account", user.key().as_ref()],
bump,
constraint = user_account.owner == user.key() @ BankError::UnauthorizedAccess // Ensure the signer owns the account
)]
pub user_account: Account<'info, UserAccount>,
pub user: Signer<'info>,
}
फंक्शन के लिए टेस्ट जोड़ें।
यह टेस्ट हमारे Anchor प्रोग्राम के getBalance फंक्शन को इनवोक (invoke) करता है और दावा (assert) करता है कि लौटाया गया बैलेंस उस राशि के बराबर है जिसे हमने पहले जमा किया था (1 SOL)।
it("Retrieves user balance", async () => {
// Get the user's balance
const balance = await program.methods
.getBalance()
.accounts({
bank: bankAccount.publicKey,
userAccount: userAccountPDA,
user: signer.publicKey,
})
.view(); // The .view() method in Anchor is used to call instructions that only read data (view functions) without submitting an actual transaction
// Verify the balance is correct
assert.equal(balance.toString(), depositAmount.toString());
console.log(`User balance: ${Number(balance) / 1e9} SOL`);
});
टेस्ट चलाएं, यह पास हो जाता है।

अब, हम एक विड्रॉल (withdraw) इम्प्लीमेंटेशन जोड़ेंगे और इसके लिए टेस्ट लिखेंगे।
बैंक से बैलेंस निकालना (Withdraw): सफलता का मामला (Success case)
हमारे बैंक से उपयोगकर्ता की जमा राशि निकालने के लिए नीचे दिया गया कोड जोड़ें।
withdraw फंक्शन निम्नलिखित कार्य करता है:
- मान्य करता है कि निकाली जाने वाली राशि शून्य से अधिक है
- जांचता है कि क्या उपयोगकर्ता के पास विड्रॉल के लिए पर्याप्त बैलेंस है
- चेक्ड अर्थमेटिक (checked arithmetic) का उपयोग करके उपयोगकर्ता अकाउंट बैलेंस और बैंक के कुल डिपॉजिट को अपडेट करता है
- बैंक अकाउंट को रेंट-एक्ज़ेम्प्ट (rent-exempt) रखने के लिए आवश्यक न्यूनतम बैलेंस की गणना करता है
- बैंक से सुरक्षित ट्रांसफर राशि निर्धारित करता है जो रेंट-एक्ज़ेम्प्ट न्यूनतम (minimum) को सुरक्षित रखती है
- सीधे लैम्पोर्ट मैनिपुलेशन (direct lamport manipulation) का उपयोग करके बैंक से उपयोगकर्ता के वॉलेट में SOL ट्रांसफर करता है (चूंकि हमारा प्रोग्राम बैंक अकाउंट का मालिक है)
- विड्रॉल विवरण को लॉग करता है
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
// Ensure withdraw amount is greater than zero
require!(amount > 0, BankError::ZeroAmount);
// Get accounts
let bank = &mut ctx.accounts.bank;
let user_account = &mut ctx.accounts.user_account;
let user = ctx.accounts.user.key();
// Check if the user has enough balance
require!(
user_account.balance >= amount,
BankError::InsufficientBalance
);
// Update user balance
user_account.balance = user_account
.balance
.checked_sub(amount)
.ok_or(BankError::Underflow)?;
// Update bank total deposits
bank.total_deposits = bank
.total_deposits
.checked_sub(amount)
.ok_or(BankError::Underflow)?;
// The bank account must keep enough lamports to stay rent-exempt,
// otherwise the runtime will garbage-collect it.
let rent = Rent::get()?;
let bank_account_info = ctx.accounts.bank.to_account_info();
let minimum_balance = rent.minimum_balance(bank_account_info.data_len());
// Only transfer what the bank can afford after reserving rent.
// Cap at the requested amount so we never send more than asked.
let available_lamports = bank_account_info.lamports();
let transfer_amount = amount.min(available_lamports.saturating_sub(minimum_balance));
// Transfer SOL: subtract from bank account and add to user wallet
**bank_account_info.try_borrow_mut_lamports()? -= transfer_amount;
**ctx.accounts.user.try_borrow_mut_lamports()? += transfer_amount;
msg!("Withdrawn {} lamports for {}", amount, user);
Ok(())
}
याद करें कि deposit फंक्शन से, हम उपयोगकर्ता के अकाउंट से बैंक में SOL खींचने के लिए System Program के transfer फंक्शन का उपयोग करते हैं। हम ऐसा इसलिए करते हैं क्योंकि System Program सभी नियमित वॉलेट्स (जैसे Ethereum में EOAs) का मालिक होता है और उसके पास उनके बैलेंस को संशोधित (modify) करने की अनुमति होती है।

लेकिन withdraw फंक्शन में, हम बैंक और उपयोगकर्ता अकाउंट PDA दोनों के लैम्पोर्ट बैलेंस को सीधे संशोधित कर सकते हैं। ऐसा इसलिए है क्योंकि हमारा प्रोग्राम दोनों PDAs का मालिक है (हमने उन्हें डिप्लॉय किया है)।

अब Withdraw अकाउंट struct जोड़ें
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut)]
pub bank: Account<'info, Bank>,
#[account(
mut,
seeds = [b"user-account", user.key().as_ref()],
bump,
constraint = user_account.owner == user.key() @ BankError::UnauthorizedAccess
)]
pub user_account: Account<'info, UserAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
withdraw फंक्शन के लिए टेस्ट जोड़ें।
यह निम्नलिखित कार्य करता है:
- यह उपयोगकर्ता अकाउंट के प्रारंभिक बैलेंस को रिकॉर्ड करता है और उपयोगकर्ता तथा बैंक दोनों के SOL बैलेंस प्राप्त करता है
- बैंक से उपयोगकर्ता को 0.5 SOL का विड्रॉल ट्रांज़ैक्शन सबमिट करता है
- सत्यापित करता है कि विड्रॉल के बाद उपयोगकर्ता अकाउंट बैलेंस और बैंक के कुल डिपॉजिट दोनों सही ढंग से अपडेट किए गए हैं
- यह जांचता है कि उपयोगकर्ता का बैलेंस बढ़ा है (ट्रांज़ैक्शन फीस को ध्यान में रखते हुए) ताकि पुष्टि हो सके कि वास्तविक SOL ट्रांसफर हुआ है
- अंत में, हम ट्रांज़ैक्शन के लिए सभी बैलेंस परिवर्तनों को लॉग करते हैं
it("Withdraws funds from the bank", async () => {
// Get the initial balance
const userAccountData = await program.account.userAccount.fetch(userAccountPDA);
const initialBalance = userAccountData.balance;
// Get initial SOL balances
const initialUserBalance = await provider.connection.getBalance(signer.publicKey);
const initialBankBalance = await provider.connection.getBalance(bankAccount.publicKey);
console.log(`Initial user SOL balance: ${initialUserBalance / 1e9} SOL`);
console.log(`Initial bank SOL balance: ${initialBankBalance / 1e9} SOL`);
// Withdraw funds from the bank
const tx = await program.methods
.withdraw(withdrawAmount)
.accounts({
bank: bankAccount.publicKey,
userAccount: userAccountPDA,
user: signer.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Withdraw transaction signature", tx);
// Get the new balance
const updatedUserAccountData = await program.account.userAccount.fetch(userAccountPDA);
const newBalance = updatedUserAccountData.balance;
// Verify the balance is correct
const expectedBalance = initialBalance.sub(withdrawAmount);
assert.equal(newBalance.toString(), expectedBalance.toString());
// Verify bank total deposits
const bankData = await program.account.bank.fetch(bankAccount.publicKey);
assert.equal(bankData.totalDeposits.toString(), expectedBalance.toString());
// Get final SOL balances
const finalUserBalance = await provider.connection.getBalance(signer.publicKey);
const finalBankBalance = await provider.connection.getBalance(bankAccount.publicKey);
console.log(`Final user SOL balance: ${finalUserBalance / 1e9} SOL`);
console.log(`Final bank SOL balance: ${finalBankBalance / 1e9} SOL`);
// Check actual SOL transfers
// User balance should increase by withdraw amount (minus tx fees)
// Since the user pays tx fees, the final balance might be slightly less than expected
assert.isTrue(finalUserBalance < initialUserBalance + Number(withdrawAmount));
assert.isTrue(finalUserBalance > initialUserBalance - 10000); // Allow for reasonable tx fees
// Bank balance should decrease by withdraw amount
assert.isTrue(finalBankBalance <= initialBankBalance);
});
अब टेस्ट चलाएं। यह पास हो जाता है।

बैंक से बैलेंस निकालना: विफलता का मामला (Failure case)
आइए यह पुष्टि करने के लिए एक और टेस्ट ब्लॉक जोड़ें कि उपयोगकर्ता अपने द्वारा जमा की गई राशि से अधिक नहीं निकाल सकते हैं।
निम्नलिखित कोड जोड़ें, यह निम्नलिखित कार्य करता है:
- यह बैंक से एक अत्यधिक राशि (10 SOL) निकालने का प्रयास करता है
- अपेक्षा करता है कि ट्रांज़ैक्शन एक अपर्याप्त बैलेंस (insufficient balance) एरर के साथ विफल हो जाएगा
- अपेक्षित विफलता (expected failure) को पकड़ने के लिए
try/catchब्लॉक का उपयोग करता है और प्राप्त वास्तविक एरर संदेश को लॉग करता है - सत्यापित करता है कि एरर में “insufficient balance” टेक्स्ट शामिल है
- यदि कोई एरर नहीं फेंका जाता है (जो एक गायब वैलिडेशन का संकेत देगा - जो हम नहीं चाहते हैं) तो टेस्ट को विफल (Fail) कर देता है
it("Prevents users from withdrawing more than their balance", async () => {
// Try to withdraw more than the balance
const excessiveWithdrawAmount = new anchor.BN(10_000_000); // 10 SOL
try {
await program.methods
.withdraw(excessiveWithdrawAmount)
.accounts({
bank: bankAccount.publicKey,
userAccount: userAccountPDA,
user: signer.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// If we reach here, the test failed
assert.fail("Should have thrown an error for insufficient balance");
} catch (error) {
// Log the actual error
console.log("Error received:", error.toString());
// Check for multiple possible error messages that could indicate insufficient balance
const errorMsg = error.toString().toLowerCase();
assert.isTrue(
errorMsg.includes("insufficient balance") ||
errorMsg.includes("0x7d3")
);
}
});
अंत में, टेस्ट चलाएं। यह पास हो जाता है।

यह हमारे बुनियादी बैंक प्रोग्राम को पूरा करता है।
यह लेख Solana पर एक ट्यूटोरियल सीरीज़ का हिस्सा है।