हमारे अब तक के Solana tutorials में, हमारे पास केवल एक ही account था जो account को initialize करता था और उसमें लिखता (write) था।
व्यवहार में, यह बहुत प्रतिबंधात्मक है। उदाहरण के लिए, यदि उपयोगकर्ता Alice, Bob को points ट्रांसफर कर रही है, तो Alice को उपयोगकर्ता Bob द्वारा initialize किए गए account में लिखने (write) में सक्षम होना चाहिए।
इस ट्यूटोरियल में हम एक वॉलेट के साथ account को initialize करने और उसे दूसरे वॉलेट के साथ अपडेट करने का प्रदर्शन करेंगे।
Initialization चरण
Accounts को initialize करने के लिए हम जिस Rust कोड का उपयोग कर रहे हैं, वह नहीं बदलता है:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("61As9Y8pREgvFZzps6rpFai8UkageeHT6kW1dnGRiefb");
#[program]
pub mod other_write {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init,
payer = signer,
space=size_of::<MyStorage>() + 8,
seeds = [],
bump)]
pub my_storage: Account<'info, MyStorage>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyStorage {
x: u64,
}
एक वैकल्पिक वॉलेट के साथ initialization ट्रांज़ैक्शन करना
हालाँकि, क्लाइंट कोड में एक महत्वपूर्ण बदलाव है:
- टेस्टिंग के उद्देश्यों के लिए, हम
newKeypairनामक एक नया वॉलेट बनाते हैं। यह Anchor द्वारा डिफ़ॉल्ट रूप से प्रदान किए जाने वाले वॉलेट से अलग है। - हम उस नए वॉलेट में 1 SOL एयरड्रॉप करते हैं ताकि यह ट्रांज़ैक्शन के लिए भुगतान कर सके।
- टिप्पणी
// THIS MUST BE EXPLICITLY SPECIFIEDपर ध्यान दें। हमSignerफ़ील्ड के लिए उस वॉलेट की publicKey पास कर रहे हैं। जब हम Anchor में निर्मित डिफ़ॉल्ट signer का उपयोग करते हैं, तो Anchor इसे हमारे लिए बैकग्राउंड में पास करता है। हालाँकि, जब हम एक अलग वॉलेट का उपयोग करते हैं, तो हमें इसे स्पष्ट रूप से (explicitly) प्रदान करने की आवश्यकता होती है। - हम
.signers([newKeypair])कॉन्फ़िगरेशन के साथ signer कोnewKeypairपर सेट करते हैं।
हम इस कोड स्निपेट के बाद यह समझाएंगे कि हम (ज़ाहिर तौर पर) signer को दो बार क्यों निर्दिष्ट कर रहे हैं:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { OtherWrite } from "../target/types/other_write";
// this airdrops sol to an address
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("other_write", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.OtherWrite as Program<OtherWrite>;
it("Is initialized!", async () => {
const newKeypair = anchor.web3.Keypair.generate();
await airdropSol(newKeypair.publicKey, 1e9); // 1 SOL
let seeds = [];
const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
await program.methods.initialize().accounts({
myStorage: myStorage,
signer: newKeypair.publicKey // ** THIS MUST BE EXPLICITLY SPECIFIED **
}).signers([newKeypair]).rpc();
});
});
यह आवश्यक नहीं है कि key signer को signer ही कहा जाए।
Exercise: Rust कोड में, payer = signer को payer = fren में बदलें और pub signer: Signer<'info> को pub fren: Signer<'info> में बदलें, और टेस्ट में signer: newKeypair.publicKey को fren: newKeypair.publicKey में बदलें। Initialization सफल होना चाहिए और टेस्ट पास होना चाहिए।
Anchor को Signer और publicKey को निर्दिष्ट करने की आवश्यकता क्यों है?
पहली नज़र में यह अनावश्यक लग सकता है कि हम signer को दो बार निर्दिष्ट कर रहे हैं, लेकिन आइए इसे करीब से देखें:

लाल बॉक्स में, हम देखते हैं कि fren फ़ील्ड को Signer account के रूप में निर्दिष्ट किया गया है। Signer टाइप का मतलब है कि Anchor ट्रांज़ैक्शन के सिग्नेचर (signature) को देखेगा और यह सुनिश्चित करेगा कि सिग्नेचर यहाँ पास किए गए पते (address) से मेल खाता है।
हम बाद में देखेंगे कि हम इसका उपयोग यह वैलिडेट करने के लिए कैसे कर सकते हैं कि Signer किसी विशिष्ट ट्रांज़ैक्शन को करने के लिए अधिकृत (authorized) है।
Anchor पूरे समय बैकग्राउंड में ऐसा ही कर रहा है, लेकिन चूँकि हमने एक ऐसे Signer को पास किया है जो Anchor द्वारा डिफ़ॉल्ट रूप से उपयोग किए जाने वाले signer से अलग है, इसलिए हमें स्पष्ट होना होगा कि Signer कौन सा account है।
Error: Solana Anchor में unknown signer
unknown signer त्रुटि (error) तब होती है जब ट्रांज़ैक्शन का signer Signer को पास की गई public key से मेल नहीं खाता है।
मान लीजिए कि हम .signers([newKeypair]) स्पेक को हटाने के लिए टेस्ट को संशोधित करते हैं। इसके बजाय Anchor डिफ़ॉल्ट signer का उपयोग करेगा, और डिफ़ॉल्ट signer हमारे newKeypair वॉलेट की publicKey से मेल नहीं खाएगा:
![Removing .signers([newKeypair])](https://static.wixstatic.com/media/935a00_2b7cffddcb2a490aaea4396ee71a47a1~mv2.png/v1/fill/w_1480,h_334,al_c,lg_1,q_90,enc_auto/935a00_2b7cffddcb2a490aaea4396ee71a47a1~mv2.png)
हमें निम्नलिखित त्रुटि मिलेगी:

इसी तरह, यदि हम publicKey को स्पष्ट रूप से (explicitly) पास नहीं करते हैं, तो Anchor चुपचाप डिफ़ॉल्ट signer का उपयोग करेगा:

और हमें निम्नलिखित Error: unknown signer मिलेगा:

थोड़ा भ्रामक रूप से, Anchor यह नहीं कह रहा है कि signer अज्ञात है क्योंकि इसे निर्दिष्ट नहीं किया गया था। Anchor यह पता लगाने में सक्षम है कि यदि कोई signer निर्दिष्ट नहीं है, तो वह डिफ़ॉल्ट signer का उपयोग करेगा। यदि हम .signers([newKeypair]) कोड और fren: newKeypair.publicKey कोड दोनों को हटा देते हैं, तो Anchor चेक करने के लिए public key, और public key से मेल खाने को सत्यापित करने के लिए signer के सिग्नेचर दोनों के लिए डिफ़ॉल्ट signer का उपयोग करेगा।
निम्नलिखित कोड का परिणाम एक सफल initialization होगा क्योंकि Signer public key और ट्रांज़ैक्शन को साइन करने वाला account दोनों Anchor के डिफ़ॉल्ट signer हैं।
await program.methods.initialize().accounts({
myStorage: myStorage
}).rpc();
});

Bob उस account में लिख (write) सकता है जिसे Alice ने initialize किया है
नीचे हम एक Anchor प्रोग्राम दिखाते हैं जिसमें account को initialize करने और उसमें लिखने (write) के लिए फ़ंक्शंस हैं।
यह हमारे Solana counter प्रोग्राम ट्यूटोरियल से परिचित लगेगा, लेकिन नीचे दिए गए // THIS FIELD MUST BE INCLUDED टिप्पणी द्वारा चिह्नित छोटे से अतिरिक्त भाग पर ध्यान दें:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("61As9Y8pREgvFZzps6rpFai8UkageeHT6kW1dnGRiefb");
#[program]
pub mod other_write {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn update_value(ctx: Context<UpdateValue>, new_value: u64) -> Result<()> {
ctx.accounts.my_storage.x = new_value;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init,
payer = fren,
space=size_of::<MyStorage>() + 8,
seeds = [],
bump)]
pub my_storage: Account<'info, MyStorage>,
#[account(mut)]
pub fren: Signer<'info>, // A public key is passed here
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct UpdateValue<'info> {
#[account(mut, seeds = [], bump)]
pub my_storage: Account<'info, MyStorage>,
// THIS FIELD MUST BE INCLUDED
#[account(mut)]
pub fren: Signer<'info>,
}
#[account]
pub struct MyStorage {
x: u64,
}
निम्नलिखित क्लाइंट कोड Alice और Bob के लिए एक वॉलेट बनाएगा और उन्हें 1-1 SOL एयरड्रॉप करेगा। Alice MyStorage account को initialize करेगी, और Bob इसमें लिखेगा (write):
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { OtherWrite } from "../target/types/other_write";
// this airdrops sol to an address
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("other_write", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.OtherWrite as Program<OtherWrite>;
it("Is initialized!", async () => {
const alice = anchor.web3.Keypair.generate();
const bob = anchor.web3.Keypair.generate();
const airdrop_alice_tx = await anchor.getProvider().connection.requestAirdrop(alice.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdrop_alice_tx);
const airdrop_alice_bob = await anchor.getProvider().connection.requestAirdrop(bob.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdrop_alice_bob);
let seeds = [];
const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
// ALICE INITIALIZE ACCOUNT
await program.methods.initialize().accounts({
myStorage: myStorage,
fren: alice.publicKey
}).signers([alice]).rpc();
// BOB WRITE TO ACCOUNT
await program.methods.updateValue(new anchor.BN(3)).accounts({
myStorage: myStorage,
fren: bob.publicKey
}).signers([bob]).rpc();
let value = await program.account.myStorage.fetch(myStorage);
console.log(`value stored is ${value.x}`);
});
});
Solana accounts में लिखने (writes) को प्रतिबंधित करना
वास्तविक एप्लिकेशन में, हम नहीं चाहते कि Bob मनमाने (arbitrary) accounts में मनमाना डेटा लिखे। आइए एक बुनियादी उदाहरण बनाएं जहां उपयोगकर्ता 10 points के साथ एक account को initialize कर सकते हैं और उन points को दूसरे account में ट्रांसफर कर सकते हैं। (इसमें एक स्पष्ट समस्या यह है कि एक हैकर अलग-अलग वॉलेट का उपयोग करके जितने चाहे उतने accounts बना सकता है, लेकिन यह हमारे उदाहरण के दायरे से बाहर है)।
एक proto-ERC20 प्रोग्राम बनाना
Alice को अपने account और Bob के account दोनों को modify करने में सक्षम होना चाहिए। यानी, उसे अपने points काटने (deduct) और Bob के points जमा (credit) करने में सक्षम होना चाहिए। वह Bob के points नहीं काट पाएगी — केवल Bob ही ऐसा करने में सक्षम होना चाहिए।
परंपरा के अनुसार, हम एक ऐसे पते को “authority” कहते हैं जो Solana में किसी account में विशेषाधिकार प्राप्त (privileged) बदलाव कर सकता है। Account struct में “authority” फ़ील्ड को स्टोर करना एक सामान्य पैटर्न है जो यह दर्शाता है कि केवल वही account उस account पर संवेदनशील संचालन (sensitive operations) कर सकता है (जैसे हमारे उदाहरण में points काटना)।
यह कुछ हद तक Solidity में onlyOwner पैटर्न के समान है, सिवाय इसके कि पूरे कॉन्ट्रैक्ट पर लागू होने के बजाय, यह केवल एक ही account पर लागू होता है:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("HFmGQX4wPgPYVMFe4WrBi925NKvGySrEG2LGyRXsXJ4Z");
const STARTING_POINTS: u32 = 10;
#[program]
pub mod points {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.player.points = STARTING_POINTS;
ctx.accounts.player.authority = ctx.accounts.signer.key();
Ok(())
}
pub fn transfer_points(ctx: Context<TransferPoints>,
amount: u32) -> Result<()> {
require!(ctx.accounts.from.authority == ctx.accounts.signer.key(), Errors::SignerIsNotAuthority);
require!(ctx.accounts.from.points >= amount, Errors::InsufficientPoints);
ctx.accounts.from.points -= amount;
ctx.accounts.to.points += amount;
Ok(())
}
}
#[error_code]
pub enum Errors {
#[msg("SignerIsNotAuthority")]
SignerIsNotAuthority,
#[msg("InsufficientPoints")]
InsufficientPoints
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init,
payer = signer,
space = size_of::<Player>() + 8,
seeds = [&(signer.as_ref().key().to_bytes())],
bump)]
player: Account<'info, Player>,
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct TransferPoints<'info> {
#[account(mut)]
from: Account<'info, Player>,
#[account(mut)]
to: Account<'info, Player>,
#[account(mut)]
signer: Signer<'info>,
}
#[account]
pub struct Player {
points: u32,
authority: Pubkey
}
ध्यान दें कि हम उस account का पता (address) प्राप्त (derive) करने के लिए signer के पते (&(signer.as_ref().key().to_bytes())) का उपयोग करते हैं जहां उनके points संग्रहीत होते हैं। यह Solana में एक Solidity mapping की तरह व्यवहार करता है, जहां Solana “msg.sender / tx.origin” की (key) है।
initialize फ़ंक्शन में, प्रोग्राम शुरुआती points को 10 पर और authority को signer पर सेट करता है। उपयोगकर्ता का इन प्रारंभिक मानों (initial values) पर कोई नियंत्रण नहीं होता है।
transfer_points फ़ंक्शन Solana Anchor require macros और error code macros का उपयोग यह सुनिश्चित करने के लिए करता है कि 1) ट्रांज़ैक्शन का Signer उस account की authority है जिसका बैलेंस कट रहा है; और 2) ट्रांसफर करने के लिए account में पर्याप्त points बैलेंस है।
टेस्ट कोडबेस को समझना आसान होना चाहिए। Alice और Bob अपने accounts को initialize करते हैं, फिर Alice, Bob को 5 points ट्रांसफर करती है:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Points } from "../target/types/points";
// this airdrops sol to an address
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("points", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Points as Program<Points>;
it("Alice transfers points to Bob", async () => {
const alice = anchor.web3.Keypair.generate();
const bob = anchor.web3.Keypair.generate();
const airdrop_alice_tx = await anchor.getProvider().connection.requestAirdrop(alice.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdrop_alice_tx);
const airdrop_alice_bob = await anchor.getProvider().connection.requestAirdrop(bob.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdrop_alice_bob);
let seeds_alice = [alice.publicKey.toBytes()];
const [playerAlice, _bumpA] = anchor.web3.PublicKey.findProgramAddressSync(seeds_alice, program.programId);
let seeds_bob = [bob.publicKey.toBytes()];
const [playerBob, _bumpB] = anchor.web3.PublicKey.findProgramAddressSync(seeds_bob, program.programId);
// Alice and Bob initialize their accounts
await program.methods.initialize().accounts({
player: playerAlice,
signer: alice.publicKey,
}).signers([alice]).rpc();
await program.methods.initialize().accounts({
player: playerBob,
signer: bob.publicKey,
}).signers([bob]).rpc();
// Alice transfers 5 points to Bob. Note that this is a u32
// so we don't need a BigNum
await program.methods.transferPoints(5).accounts({
from: playerAlice,
to: playerBob,
signer: alice.publicKey,
}).signers([alice]).rpc();
console.log(`Alice has ${(await program.account.player.fetch(playerAlice)).points} points`);
console.log(`Bob has ${(await program.account.player.fetch(playerBob)).points} points`)
});
});
Exercise: एक कीपेयर (keypair) mallory बनाएं और .signers([mallory]) में mallory का signer के रूप में उपयोग करके mallory द्वारा Alice या Bob से points चुराने का प्रयास करें। आपका हमला विफल हो जाना चाहिए, लेकिन आपको फिर भी कोशिश करनी चाहिए।
require! macros को बदलने के लिए Anchor Constraints का उपयोग करना
require!(ctx.accounts.from.authority == ctx.accounts.signer.key(), Errors::SignerIsNotAuthority); लिखने का एक विकल्प Anchor constraint का उपयोग करना है। Anchor account दस्तावेज़ (docs) हमें उपलब्ध constraints की एक सूची देते हैं।
Anchor has_one constraint
has_one constraint यह मान लेता है कि #[derive(Accounts)] और #[account] के बीच “shared key” है और यह जांचता है कि उन दोनों keys का मान (value) समान है। इसे प्रदर्शित करने का सबसे अच्छा तरीका एक चित्र (picture) है:

पर्दे के पीछे (behind the scenes), Anchor ट्रांज़ैक्शन को ब्लॉक कर देगा यदि ट्रांज़ैक्शन के हिस्से के रूप में (Signer के रूप में) पास किया गया authority account, account में संग्रहीत authority के बराबर नहीं है।
हमारे ऊपर दिए गए इम्प्लीमेंटेशन में, हमने account में authority की (key) और #[derive(Accounts)] में signer का उपयोग किया था। की (key) नामों का यह बेमेल (mismatch) इस मैक्रो (macro) को काम करने से रोक देगा, इसलिए ऊपर दिया गया कोड signer की (key) को authority में बदल देता है। Authority कोई विशेष कीवर्ड नहीं है, केवल एक परंपरा है। एक व्यायाम (exercise) के रूप में, आप authority के सभी उदाहरणों को fren में बदल सकते हैं और कोड वैसे ही काम करेगा।
Anchor constraint constraint
हम require!(ctx.accounts.from.points >= amount, Errors::InsufficientPoints); मैक्रो को Anchor constraint से भी बदल सकते हैं।
constraint मैक्रो हमें ट्रांज़ैक्शन में पास किए गए accounts और account के डेटा पर मनमाने (arbitrary) प्रतिबंध लगाने की अनुमति देता है। हमारे मामले में, हम यह सुनिश्चित करना चाहते हैं कि प्रेषक (sender) के पास पर्याप्त points हों:
#[derive(Accounts)]
#[instruction(amount: u32)] // amount must be passed as an instruction
pub struct TransferPoints<'info> {
#[account(mut,
has_one = authority,
constraint = from.points >= amount)]
from: Account<'info, Player>,
#[account(mut)]
to: Account<'info, Player>,
authority: Signer<'info>,
}
#[account]
pub struct Player {
points: u32,
authority: Pubkey
}
मैक्रो यह पहचानने के लिए काफी स्मार्ट है कि from, from की (key) में पास किए गए account पर आधारित है, और उस account में एक points फ़ील्ड है। transfer_points फ़ंक्शन आर्ग्युमेंट से amount को instruction मैक्रो के माध्यम से पास किया जाना चाहिए ताकि constraint मैक्रो amount की तुलना account में point बैलेंस से कर सके।
Anchor constraints में custom error messages जोड़ना
जब constraints का उल्लंघन होता है, तो हम custom errors को जोड़कर त्रुटि संदेशों (error messages) की पठनीयता (readability) में सुधार कर सकते हैं, वही custom errors जिन्हें हमने @ नोटेशन का उपयोग करके require! मैक्रोज़ में पास किया था:
#[derive(Accounts)]
#[instruction(amount: u32)]
pub struct TransferPoints<'info> {
#[account(mut,
has_one = authority @ Errors::SignerIsNotAuthority,
constraint = from.points >= amount @ Errors::InsufficientPoints)]
from: Account<'info, Player>,
#[account(mut)]
to: Account<'info, Player>,
authority: Signer<'info>,
}
#[account]
pub struct Player {
points: u32,
authority: Pubkey
}
Errors enum को पहले Rust कोड में परिभाषित किया गया था जिसने उनका उपयोग require! मैक्रोज़ में किया था।
Exercise: has_one और constraint मैक्रो का उल्लंघन करने के लिए परीक्षणों (tests) को संशोधित करें और त्रुटि संदेशों (error messages) का निरीक्षण करें।
RareSkills के साथ और अधिक Solana सीखें
हमारे Solana tutorials में Ethereum या EVM डेवलपर के रूप में Solana सीखने का तरीका शामिल है।
मूल रूप से 5 मार्च, 2024 को प्रकाशित