यह ट्यूटोरियल उस मैकेनिज्म को पेश करेगा जिसके द्वारा Solana Anchor प्रोग्राम्स ट्रांजैक्शन के हिस्से के रूप में SOL को ट्रांसफर कर सकते हैं।
Ethereum के विपरीत, जहां वॉलेट्स ट्रांजैक्शन के हिस्से के रूप में msg.value को निर्दिष्ट करते हैं और कॉन्ट्रैक्ट में ETH को “push” करते हैं, Solana प्रोग्राम्स वॉलेट से SOL को “pull” करते हैं।
इसलिए, इसमें “payable” फंक्शंस या “msg.value” जैसी कोई चीज नहीं होती है।
नीचे हमने sol_splitter नामक एक नया anchor प्रोजेक्ट बनाया है और SOL को सेंडर से प्राप्तकर्ता को ट्रांसफर करने के लिए Rust कोड डाला है।
बेशक, यह अधिक कुशल होगा यदि सेंडर किसी प्रोग्राम के माध्यम से ऐसा करने के बजाय सीधे SOL भेजे, लेकिन हम यह दिखाना चाहते हैं कि यह कैसे किया जाता है:
use anchor_lang::prelude::*;
use anchor_lang::system_program;
declare_id!("9qnGx9FgLensJQy1hSB4b8TaRae6oWuNDveUrxoYatr7");
#[program]
pub mod sol_splitter {
use super::*;
pub fn send_sol(ctx: Context<SendSol>, amount: u64) -> Result<()> {
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.signer.to_account_info(),
to: ctx.accounts.recipient.to_account_info(),
}
);
let res = system_program::transfer(cpi_context, amount);
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::TransferFailed);
}
}
}
#[error_code]
pub enum Errors {
#[msg("transfer failed")]
TransferFailed,
}
#[derive(Accounts)]
pub struct SendSol<'info> {
/// CHECK: we do not read or write the data of this account
#[account(mut)]
recipient: UncheckedAccount<'info>,
system_program: Program<'info, System>,
#[account(mut)]
signer: Signer<'info>,
}
यहां समझाने के लिए बहुत सी चीजें हैं।
CPI का परिचय: Cross Program Invocation
Ethereum में, ETH को ट्रांसफर करना केवल msg.value फील्ड में एक वैल्यू निर्दिष्ट करके किया जाता है। Solana में, system program नामक एक अंतर्निहित प्रोग्राम एक अकाउंट से दूसरे अकाउंट में SOL ट्रांसफर करता है। यही कारण है कि जब हमने अकाउंट्स को इनिशियलाइज़ किया था और उन्हें इनिशियलाइज़ करने के लिए फीस चुकानी पड़ी थी, तो यह बार-बार दिखाई दे रहा था।
आप मोटे तौर पर system program को Ethereum में एक precompile के रूप में सोच सकते हैं। कल्पना करें कि यह प्रोटोकॉल में बने एक ERC-20 टोकन की तरह व्यवहार करता है जिसका उपयोग नेटिव करेंसी के रूप में किया जाता है। और इसमें transfer नामक एक पब्लिक फंक्शन होता है।
CPI ट्रांजैक्शन्स के लिए Context
जब भी किसी Solana प्रोग्राम फंक्शन को कॉल किया जाता है, तो एक Context प्रदान किया जाना चाहिए। वह Context उन सभी अकाउंट्स को होल्ड करता है जिनके साथ प्रोग्राम इंटरैक्ट करेगा।
system program को कॉल करना भी कुछ अलग नहीं है। system program को एक Context की आवश्यकता होती है जो from और to अकाउंट्स को होल्ड करता है। जो amount ट्रांसफर किया जाता है उसे एक “regular” आर्गुमेंट के रूप में पास किया जाता है — यह Context का हिस्सा नहीं है (क्योंकि “amount” कोई अकाउंट नहीं है, यह सिर्फ एक वैल्यू है)।
अब हम नीचे दिए गए कोड स्निपेट को समझा सकते हैं:

हम एक नया CpiContext बना रहे हैं जो यह होल्ड करता है कि हम पहले आर्गुमेंट (green box) के रूप में किस प्रोग्राम को कॉल करने जा रहे हैं, और उन अकाउंट्स को जो उस ट्रांजैक्शन (yellow box) के हिस्से के रूप में शामिल होंगे। यहां amount आर्गुमेंट की आपूर्ति नहीं की गई है क्योंकि amount एक अकाउंट नहीं है।
अब जब हमारा cpi_context बन गया है, तो हम amount निर्दिष्ट करते हुए system program (orange box) में एक cross program invocation कर सकते हैं।
यह एक Result<()> टाइप रिटर्न करता है, ठीक वैसे ही जैसे हमारे Anchor प्रोग्राम्स पर पब्लिक फंक्शंस करते हैं।
Cross program invocations की रिटर्न वैल्यूज को अनदेखा न करें।
यह जांचने के लिए कि क्या cross program invocation सफल रहा, हमें बस यह जांचना होगा कि रिटर्न की गई वैल्यू एक Ok है। Rust इसे is_ok() मेथड के साथ सीधा बनाता है:
let res = system_program::transfer(cpi_context, amount);
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::TransferFailed);
}
}
}
#[error_code]
pub enum Errors {
#[msg("transfer failed")]
TransferFailed,
}
केवल signer ही “from” हो सकता है
यदि आप system program को ऐसे from अकाउंट के साथ कॉल करते हैं जो Signer नहीं है, तो system program कॉल को रिजेक्ट कर देगा। सिग्नेचर के बिना, system program यह नहीं जान सकता कि आपने कॉल को ऑथराइज़ किया है या नहीं।
Typescript कोड:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { SolSplitter } from "../target/types/sol_splitter";
describe("sol_splitter", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.SolSplitter as Program<SolSplitter>;
async function printAccountBalance(account) {
const balance = await anchor.getProvider().connection.getBalance(account);
console.log(`${account} has ${balance / anchor.web3.LAMPORTS_PER_SOL} SOL`);
}
it("Transmit SOL", async () => {
// generate a new wallet
const recipient = anchor.web3.Keypair.generate();
await printAccountBalance(recipient.publicKey);
// send the account 1 SOL via the program
let amount = new anchor.BN(1 * anchor.web3.LAMPORTS_PER_SOL);
await program.methods.sendSol(amount)
.accounts({recipient: recipient.publicKey})
.rpc();
await printAccountBalance(recipient.publicKey);
});
});
ध्यान देने योग्य कुछ बातें:
- हमने पहले और बाद का बैलेंस दिखाने के लिए एक हेल्पर फंक्शन
printAccountBalanceबनाया है - हमने
anchor.web3.Keypair.generate()का उपयोग करके प्राप्तकर्ता का वॉलेट जनरेट किया - हमने नए अकाउंट में एक SOL ट्रांसफर किया
जब हम कोड रन करते हैं, तो अपेक्षित परिणाम इस प्रकार होता है। प्रिंट स्टेटमेंट्स प्राप्तकर्ता के एड्रेस का पहले और बाद का बैलेंस हैं:

Exercise: एक Solana प्रोग्राम बनाएं जो आने वाले SOL को दो प्राप्तकर्ताओं के बीच समान रूप से विभाजित करता है। आप इसे फंक्शन आर्गुमेंट्स के माध्यम से पूरा नहीं कर पाएंगे, अकाउंट्स को Context स्ट्रक्ट में होना चाहिए।
एक पेमेंट स्प्लिटर बनाना: remaining_accounts के साथ अनियंत्रित संख्या में अकाउंट्स का उपयोग करना।
हम देख सकते हैं कि यदि हम SOL को कई अकाउंट्स में स्प्लिट करना चाहते हैं, तो इस तरह का एक Context स्ट्रक्ट निर्दिष्ट करना काफी अटपटा होगा:
#[derive(Accounts)]
pub struct SendSol<'info> {
/// CHECK: we do not read or write the data of this account
#[account(mut)]
recipient1: UncheckedAccount<'info>,
/// CHECK: we do not read or write the data of this account
#[account(mut)]
recipient2: UncheckedAccount<'info>,
/// CHECK: we do not read or write the data of this account
#[account(mut)]
recipient3: UncheckedAccount<'info>,
// ...
/// CHECK: we do not read or write the data of this account
#[account(mut)]
recipientn: UncheckedAccount<'info>,
system_program: Program<'info, System>,
#[account(mut)]
signer: Signer<'info>,
}
इसे हल करने के लिए, Anchor Context स्ट्रक्ट्स में एक remaining_accounts फील्ड जोड़ता है।
नीचे दिया गया कोड स्पष्ट करता है कि उस फीचर का उपयोग कैसे करें:
use anchor_lang::prelude::*;
use anchor_lang::system_program;
declare_id!("9qnGx9FgLensJQy1hSB4b8TaRae6oWuNDveUrxoYatr7");
#[program]
pub mod sol_splitter {
use super::*;
// 'a, 'b, 'c are Rust lifetimes, ignore them for now
pub fn split_sol<'a, 'b, 'c, 'info>(
ctx: Context<'a, 'b, 'c, 'info, SplitSol<'info>>,
amount: u64,
) -> Result<()> {
let amount_each_gets = amount / ctx.remaining_accounts.len() as u64;
let system_program = &ctx.accounts.system_program;
// note the keyword `remaining_accounts`
for recipient in ctx.remaining_accounts {
let cpi_accounts = system_program::Transfer {
from: ctx.accounts.signer.to_account_info(),
to: recipient.to_account_info(),
};
let cpi_program = system_program.to_account_info();
let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
let res = system_program::transfer(cpi_context, amount_each_gets);
if !res.is_ok() {
return err!(Errors::TransferFailed);
}
}
Ok(())
}
}
#[error_code]
pub enum Errors {
#[msg("transfer failed")]
TransferFailed,
}
#[derive(Accounts)]
pub struct SplitSol<'info> {
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
}
और यहाँ Typescript कोड है:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { SolSplitter } from "../target/types/sol_splitter";
describe("sol_splitter", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.SolSplitter as Program<SolSplitter>;
async function printAccountBalance(account) {
const balance = await anchor.getProvider().connection.getBalance(account);
console.log(`${account} has ${balance / anchor.web3.LAMPORTS_PER_SOL} SOL`);
}
it("Split SOL", async () => {
const recipient1 = anchor.web3.Keypair.generate();
const recipient2 = anchor.web3.Keypair.generate();
const recipient3 = anchor.web3.Keypair.generate();
await printAccountBalance(recipient1.publicKey);
await printAccountBalance(recipient2.publicKey);
await printAccountBalance(recipient3.publicKey);
const accountMeta1 = {pubkey: recipient1.publicKey, isWritable: true, isSigner: false};
const accountMeta2 = {pubkey: recipient2.publicKey, isWritable: true, isSigner: false};
const accountMeta3 = {pubkey: recipient3.publicKey, isWritable: true, isSigner: false};
let amount = new anchor.BN(1 * anchor.web3.LAMPORTS_PER_SOL);
await program.methods.splitSol(amount)
.remainingAccounts([accountMeta1, accountMeta2, accountMeta3])
.rpc();
await printAccountBalance(recipient1.publicKey);
await printAccountBalance(recipient2.publicKey);
await printAccountBalance(recipient3.publicKey);
});
});
टेस्ट्स रन करने पर पहले और बाद का बैलेंस दिखाई देता है:

यहाँ Rust कोड पर कुछ टिप्पणी दी गई है:
Rust Lifetimes
split_sol के फंक्शन डिक्लेरेशन में कुछ अजीब सिंटैक्स पेश किया गया है:
pub fn split_sol<'a, 'b, 'c, 'info>(
ctx: Context<'a, 'b, 'c, 'info, SplitSol<'info>>,
amount: u64,
) -> Result<()>
'a, 'b, और 'c Rust lifetimes हैं। Rust lifetimes एक जटिल विषय है जिससे हम अभी बचना चाहेंगे। लेकिन एक उच्च-स्तरीय व्याख्या यह है कि Rust कोड को इस बात के आश्वासन की आवश्यकता होती है कि for recipient in ctx.remaining_accounts लूप में पास किए गए रिसोर्सेज़ लूप की पूरी अवधि के लिए मौजूद रहेंगे।
ctx.remaining_accounts
लूप recipient in ctx.remaining_accounts के लिए लूप करता है। remaining_accounts कीवर्ड, Context स्ट्रक्ट में बहुत सारी कीज़ बनाए बिना, अनियंत्रित संख्या में अकाउंट्स पास करने का एक Anchor मैकेनिज्म है।
Typescript टेस्ट्स में, हम ट्रांजैक्शन में remaining_accounts को इस प्रकार जोड़ सकते हैं:
await program.methods.splitSol(amount)
.remainingAccounts([accountMeta1, accountMeta2, accountMeta3])
.rpc();
RareSkills के साथ और जानें
बाकी Solana ट्यूटोरियल्स के लिए हमारा Solana कोर्स देखें।
मूल रूप से 2 मार्च, 2024 को प्रकाशित