इस दो-भाग वाले ट्यूटोरियल में, हम सीखेंगे कि native Solana प्रोग्राम्स में डेटा स्टोर करने के लिए अकाउंट्स कैसे बनाए जाते हैं। इसके लिए हम दो तरीकों का उपयोग करेंगे: keypairs (यह भाग) और Program Derived Addresses या PDAs (भाग 2)। हमारा लक्ष्य low level पर अकाउंट allocation, initialization और डेटा serialization को समझना है — वह लॉजिक जिसे Anchor का #[account(init)] मैक्रो आपसे छिपा देता है (abstract कर देता है)।
दोनों तरीकों के लिए, हम एक प्रोग्राम बनाएंगे जो एक अकाउंट बनाता है और उसमें डेटा लिखता है, फिर यह verify करने के लिए कि डेटा सही ढंग से लिखा गया था, हम एक TypeScript क्लाइंट के साथ इसका परीक्षण (test) करेंगे।
Keypair स्टोरेज प्रोग्राम सेटअप करना
डायरेक्टरी बनाने और Cargo के साथ एक Rust प्रोजेक्ट initialize करने के लिए निम्नलिखित कमांड्स रन करें:
mkdir solana-storage-write
cd solana-storage-write
cargo init --lib
अपने Cargo.toml को नीचे दिए गए कॉन्फ़िगरेशन के अनुसार अपडेट करें, जो crate टाइप सेट करता है और Solana प्रोग्राम dependencies जोड़ता है:
[package]
name = "solana-storage-write"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
solana-program = "1.18"
borsh = "0.10"
ध्यान दें कि हमने serialization के लिए borsh = "0.10" जोड़ा है, जिसकी हमें अपना अकाउंट बनाते समय आवश्यकता होगी। हमने पिछले ट्यूटोरियल में Borsh serialization को विस्तार से कवर किया था।
अब अपना प्रोग्राम बनाते हैं।
Native Solana प्रोग्राम्स में keypairs के साथ अकाउंट्स बनाना
डेटा स्टोरेज के लिए अकाउंट स्ट्रक्चर
कोड में गहराई से जाने से पहले, आइए समझते हैं कि डेटा स्टोर करने वाला एक Solana अकाउंट कैसा दिखता है:

data फील्ड वह जगह है जहाँ हम अपना serialized स्ट्रक्चर (struct) स्टोर करते हैं (हम इस पर बाद में आएंगे)। जब हम कोई अकाउंट बनाते हैं, तो हम specify करते हैं कि इस फील्ड में कितने बाइट्स होने चाहिए, और System Program उस स्पेस को allocate करता है।
Keypair-आधारित स्टोरेज अकाउंट बनाने के चरण
नीचे दिए गए चरण दिखाते हैं कि डेटा स्टोरेज के लिए keypair-आधारित अकाउंट कैसे बनाया जाता है:
- क्लाइंट (जिसने keypair जनरेट किया है और ट्रांजेक्शन को साइन करेगा) से स्टोरेज अकाउंट की public key प्राप्त करना।
- वह डेटा स्ट्रक्चर बनाना जिसे हम स्टोर करना चाहते हैं (एक
u64count फील्ड के साथ एकCounterDatastruct) और इसे Borsh के साथ serialize करना। - आवश्यक स्पेस निर्धारित करना (serialized डेटा की बाइट लेंथ जो अकाउंट के डेटा फील्ड का आकार सेट करती है)।
- उस स्पेस के लिए आवश्यक rent-exempt lamports की गणना करना (अकाउंट को garbage collect होने से बचाने और जीवित रखने के लिए आवश्यक न्यूनतम SOL बैलेंस)।
- कैलकुलेट किए गए स्पेस और lamports के साथ अकाउंट बनाने के लिए System Program का उपयोग करना।
- Serialized डेटा को सीधे अकाउंट के डेटा फील्ड में लिखना।
अब आइए इसे कोड में देखते हैं। src/lib.rs फ़ाइल के कोड को निम्नलिखित कोड से बदलें। हमने यह दिखाने के लिए कोड कमेंट्स जोड़े हैं कि हम ऊपर सूचीबद्ध प्रत्येक चरण को कहाँ लागू (implement) करते हैं:
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
rent::Rent,
system_instruction, system_program,
};
entrypoint!(process_instruction);
// This represents the data we'll store in our account
// We've added Borsh derive macros for serialization and deserialization
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterData {
pub count: u64,
}
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
msg!("Storage Write Program: Creating storage account and writing data");
let accounts_iter = &mut accounts.iter();
// STEP 1: Get the accounts we need
// next_account_info() extracts the next AccountInfo from the iterator
let storage_account = next_account_info(accounts_iter)?;
let signer = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
// Verify the system program
if system_program.key != &system_program::ID {
msg!("Invalid system program");
return Err(ProgramError::IncorrectProgramId);
}
// Verify the signer account is actually a signer
if !signer.is_signer {
msg!("Signer must be a signer");
return Err(ProgramError::MissingRequiredSignature);
}
// STEP 2: Create our counter data
let counter_data = CounterData { count: 42 };
// STEP 2: Serialize the data with Borsh (u64 becomes 8 bytes in little-endian format)
let serialized_data = counter_data.try_to_vec()?; // [42, 0, 0, 0, 0, 0, 0, 0]
// STEP 3: Determine the space needed for our data
let space = serialized_data.len();
msg!("Creating storage account with {} bytes", space);
msg!("Serialized data: {:?}", serialized_data);
// STEP 4: Calculate lamports needed for rent exemption
let lamports = Rent::default().minimum_balance(space);
// STEP 5: Create the account using system program
let create_account_ix = system_instruction::create_account(
signer.key,
storage_account.key,
lamports,
space as u64,
program_id,
);
// STEP 5 (continued): Execute the create_account instruction
invoke(
&create_account_ix,
&[
signer.clone(),
storage_account.clone(),
system_program.clone(),
],
)?;
msg!("Storage account created successfully");
// STEP 6: Write our serialized data to the account
let mut account_data = storage_account.try_borrow_mut_data()?;
account_data.copy_from_slice(&serialized_data);
msg!("Data written to storage account");
Ok(())
}
अब हम अकाउंट बनाने के पीछे के चरणों पर नज़र डालेंगे।
हमारे Rust प्रोग्राम में keypair अकाउंट निर्माण कैसे काम करता है
हमारे प्रोग्राम में अकाउंट बनाने की प्रक्रिया दो चरणों में होती है। सबसे पहले, हम system_instruction::create_account() के साथ instruction बनाते हैं — यह एक हेल्पर फ़ंक्शन है जो अकाउंट बनाने के लिए सही अकाउंट्स और डेटा के साथ Instruction struct का निर्माण करता है (CPI ट्यूटोरियल में, हमने इस struct को मैन्युअल रूप से बनाया था)।

दूसरा, हम invoke() का उपयोग करके इस instruction को execute करते हैं, जो System Program को एक Cross-Program Invocation (CPI) करता है। इसके बाद System Program वास्तव में हमारे द्वारा specify किए गए पैरामीटर्स के साथ ऑन-चेन अकाउंट बनाता है।
create_account Instruction
System Program का create_account instruction lamports ट्रांसफर करके, डेटा स्पेस allocate करके, और एक owner असाइन करके एक नया ऑन-चेन अकाउंट बनाता है। यह पाँच पैरामीटर्स लेता है:
- Payer: वह अकाउंट जो नए अकाउंट के रेंट (rent) की फंडिंग करता है
- New account address: वह एड्रेस जहाँ अकाउंट बनाया जाना है (वह अकाउंट जो initialize हो रहा है)
- Lamports: नए अकाउंट में ट्रांसफर की जाने वाली SOL की मात्रा (यह rent-exemption को कवर करना चाहिए)
- Space: अकाउंट के डेटा के लिए allocate किए जाने वाले बाइट्स की संख्या
- Owner: प्रोग्राम ID (public key) जो नए अकाउंट का owner होगा
चूंकि System Program को create_account instruction में नए अकाउंट एड्रेस का एक signer होना आवश्यक होता है, इसलिए एक keypair अकाउंट उम्मीद के मुताबिक काम करता है: ट्रांजेक्शन में keypair की प्राइवेट की (private key) का सिग्नेचर शामिल होता है। एक PDA की कोई प्राइवेट की नहीं होती है, इसलिए जो प्रोग्राम इसे बनाता है उसे invoke_signed() के माध्यम से सीड्स (seeds) प्रदान करने होते हैं, जिसका उपयोग रनटाइम (runtime) PDA को फिर से प्राप्त (re-derive) करने और verify करने के लिए करता है, जिससे उसे साइन करने का अधिकार मिलता है (हम इसे भाग 2 में देखेंगे)।
Solana को कैसे पता चलता है कि keypair एड्रेस अकाउंट बनाने के लिए उपलब्ध है?
क्लाइंट एक नया keypair जनरेट करता है और ट्रांजेक्शन में अपनी public key पास करता है। हमारा प्रोग्राम इस एड्रेस को let storage_account = next_account_info(accounts_iter)?; के माध्यम से प्राप्त करता है। जब System Program create_account instruction को प्रोसेस करता है, तो यह चेक करता है कि क्या उस एड्रेस पर पहले से कोई अकाउंट मौजूद है। यदि वहां कोई अकाउंट मौजूद नहीं है, तो System Program इसे बनाता है। यदि कोई अकाउंट पहले से मौजूद है, तो instruction एक एरर के साथ फेल हो जाता है।
Solana अकाउंट में डेटा कैसे स्टोर होता है
अब जब हमने कोड देख लिया है, तो आइए समझते हैं कि जब हम अपने Solana अकाउंट में डेटा स्टोर करते हैं तो वास्तव में क्या होता है।
सबसे पहले, हम अपने struct से शुरुआत करते हैं:

यह मेमोरी में रहने वाला एक साधारण Rust struct है। लेकिन Solana अकाउंट्स सीधे Rust structs को स्टोर नहीं कर सकते, वे केवल रॉ बाइट्स (raw bytes) को समझते हैं। इसलिए हमें Borsh का उपयोग करके अपने struct को बाइट्स में बदलना होगा:

यह try_to_vec() मेथड इसलिए उपलब्ध है क्योंकि हमने पहले अपने CounterData struct में #[derive(BorshSerialize, BorshDeserialize)] एट्रिब्यूट जोड़ा था।

ये derive मैक्रोज़ हमारे struct को बाइट्स में और बाइट्स से struct में बदलने के लिए कोड जनरेट करते हैं। Borsh हमारे count: 42 (एक u64) को लेता है और इसे little-endian फॉर्मेट में 8 बाइट्स में बदल देता है। 42 की वैल्यू [42, 0, 0, 0, 0, 0, 0, 0] बन जाती है, पहला बाइट 42 है, और बाकी शून्य हैं क्योंकि u64 हमेशा ठीक 8 बाइट्स लेता है (जैसा कि हमने Borsh serialization ट्यूटोरियल में चर्चा की थी)। यह आवश्यक है क्योंकि Solana अकाउंट्स सीधे Rust structs को नहीं, बल्कि केवल रॉ बाइट्स को ही स्टोर कर सकते हैं।
इसके बाद, हम System Program के create_account instruction के साथ अकाउंट बनाते हैं।

create_account instruction ये पैरामीटर्स लेता है:
signer.key: नए अकाउंट बनाने के लिए भुगतान (pay) करने वाले अकाउंट का एड्रेसstorage_account.key: वह एड्रेस जहाँ नया अकाउंट बनाया जाएगाlamports: नए अकाउंट को फंड करने के लिए SOL की मात्रा (rent exemption के लिए)space as u64: बाइट्स में नए अकाउंट के डेटा फील्ड का आकारprogram_id: कौन सा प्रोग्राम नए बनाए गए अकाउंट का मालिक होगा (इस मामले में हमारा प्रोग्राम)
फिर हम invoke (जो solana_program crate से इम्पोर्ट किया गया है) के साथ अकाउंट बनाते हैं:

इस अकाउंट को बनाने के बाद, हम Borsh serialized बाइट्स को सीधे इसके डेटा फील्ड में लिखते हैं:

यह स्टोरेज में कैसे persist (स्थायी) होता है?
storage_account.try_borrow_mut_data()? हमें सिर्फ एक कॉपी नहीं देता है। यह हमें Solana ब्लॉकचेन पर मौजूद वास्तविक अकाउंट के डेटा फील्ड का एक mutable reference देता है। इसलिए जब हम account_data में लिखते हैं, तो हम सीधे अकाउंट के persistent स्टोरेज में लिख रहे होते हैं।
इसे इस तरह से समझें:
storage_accountSolana पर मौजूद एक वास्तविक अकाउंट का हैंडल हैtry_borrow_mut_data()आपको उस अकाउंट के डेटा बाइट्स का सीधा एक्सेस देता है- जब आप
account_dataको modify करते हैं, तो आपcopy_from_sliceका उपयोग करके वास्तविक ऑन-चेन अकाउंट डेटा को modify कर रहे होते हैं (serialized_dataसे बाइट्स कोaccount_dataमें कॉपी करता है) - जब आपका प्रोग्राम सफलतापूर्वक समाप्त हो जाता है तो Solana रनटाइम स्वचालित रूप से इन बदलावों को persist (सेव) कर देता है
इस बिंदु पर, हमारे अकाउंट के डेटा फील्ड में ठीक Borsh serialized 8 बाइट्स होते हैं: [42, 0, 0, 0, 0, 0, 0, 0]। बस हो गया, हमारा struct अब अकाउंट में “स्टोर” हो गया है, और यह हमारे प्रोग्राम के निष्पादन (execution) समाप्त होने के बाद भी ब्लॉकचेन पर बना रहेगा।
अब प्रोग्राम को बिल्ड और डिप्लॉय करें:
cargo build-sbf
solana-test-validator # in another terminal
solana program deploy target/deploy/solana_storage_write.so
डिप्लॉय आउटपुट से प्रोग्राम ID को कॉपी करें, हम प्रोग्राम का परीक्षण करते समय इसका उपयोग करेंगे।
क्लाइंट के साथ प्रोग्राम की टेस्टिंग करना
अब अपने स्टोरेज प्रोग्राम का परीक्षण करने के लिए एक TypeScript क्लाइंट बनाते हैं।
पिछले ट्यूटोरियल्स की तरह ही, हम प्रोजेक्ट रूट डायरेक्टरी से क्लाइंट एनवायरनमेंट सेट अप करते हैं:
mkdir client && cd client
npm init -y
npm install @solana/web3.js typescript ts-node @types/node
टेस्ट स्क्रिप्ट जोड़ने के लिए client/package.json को अपडेट करें:
{
"scripts": {
"test": "ts-node client.ts"
}
}
TypeScript कम्पाइलेशन सेटिंग्स को कॉन्फ़िगर करने के लिए client/tsconfig.json बनाएं:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["*.ts"]
}
अब client/client.ts बनाएं और निम्नलिखित कोड जोड़ें। इस क्लाइंट में, हम:
- अपने
CounterDatastruct के लिए एक signer keypair और एक स्टोरेज keypair अकाउंट बनाते हैं - Signer अकाउंट को SOL से फंड करते हैं
- स्टोरेज बनाने के लिए अपने प्रोग्राम में आवश्यक अकाउंट्स पास करते हैं (स्टोरेज अकाउंट, signer अकाउंट, System Program)
- अकाउंट बनाने और डेटा लिखने के लिए ट्रांजेक्शन execute करते हैं
- अकाउंट डेटा को वापस पढ़ते हैं और verify करते हैं कि यह सही ढंग से लिखा गया था (कि count की वैल्यू ठीक 42 है)
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
sendAndConfirmTransaction,
} from '@solana/web3.js';
const PROGRAM_ID = new PublicKey('YOUR_PROGRAM_ID_HERE'); // Replace with your actual program ID
const connection = new Connection('http://localhost:8899', 'confirmed');
async function testStorageWrite() {
console.log('Testing Storage Creation and Writing\n');
// Create a signer keypair and a storage keypair account
const signer = Keypair.generate();
const storageAccount = Keypair.generate();
// Fund the signer keypair account
console.log('Funding signer account...');
await connection.requestAirdrop(signer.publicKey, LAMPORTS_PER_SOL);
await new Promise(resolve => setTimeout(resolve, 2000));
console.log(`Signer: ${signer.publicKey.toString()}`);
console.log(`Storage Account: ${storageAccount.publicKey.toString()}\n`);
// Create instruction with required accounts (storage, signer & system program account)
const instruction = new TransactionInstruction({
keys: [
{ pubkey: storageAccount.publicKey, isSigner: true, isWritable: true },
{ pubkey: signer.publicKey, isSigner: true, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
programId: PROGRAM_ID,
data: Buffer.alloc(0), // No instruction data needed
});
const transaction = new Transaction().add(instruction);
console.log('Creating storage account and writing data...');
const signature = await sendAndConfirmTransaction(connection, transaction, [signer, storageAccount]);
console.log(`Transaction confirmed: ${signature}`);
// Verify the data was written correctly by reading it back
console.log('\nVerifying data was written correctly...');
const accountInfo = await connection.getAccountInfo(storageAccount.publicKey);
if (accountInfo && accountInfo.data.length > 0) {
console.log('Account data length:', accountInfo.data.length, 'bytes');
console.log('Raw account data:', Array.from(accountInfo.data));
// Deserialize the data back to verify
const deserializedData = new Uint8Array(accountInfo.data);
// DataView lets us read binary data as specific types (u64 in this case)
// getBigUint64(0, true) reads 8 bytes starting at offset 0, little-endian
const count = new DataView(deserializedData.buffer).getBigUint64(0, true);
console.log('Deserialized count value:', count.toString());
// the `n` makes 42 a BigInt literal. This is required because getBigUint64 returns a BigInt
if (count === 42n) {
console.log('Data was written correctly to the storage account.');
} else {
console.log('Error: Expected count 42, got', count.toString());
}
} else {
console.log('Error: Could not read account data');
}
}
testStorageWrite().catch(console.error);
सुनिश्चित करें कि PROGRAM_ID वेरिएबल आपके प्रोग्राम ID पर सेट है।
हमारे क्लाइंट में keypair अकाउंट क्रिएशन को समझना
सबसे पहले हम एक keypair जनरेट करते हैं जो हमारा नया अकाउंट बनेगा:

अगला महत्वपूर्ण कदम instruction बनाते समय अकाउंट को signer के रूप में सेट करना है:

isSigner: true महत्वपूर्ण है क्योंकि System Program को उसी सटीक एड्रेस से सिग्नेचर की आवश्यकता होती है जहाँ अकाउंट बनाया जाएगा।
फिर हम ट्रांजेक्शन भेजते समय keypairs प्रदान करते हैं:

Keypair ऑब्जेक्ट्स में वे प्राइवेट कीज़ होती हैं जो आवश्यक सिग्नेचर जनरेट करने के लिए ज़रूरी हैं, जो उस विशिष्ट एड्रेस पर अकाउंट बनाने को अधिकृत (authorize) करती हैं।
टेस्ट रन करने से पहले, यह सुनिश्चित करें कि आपका लोकल Solana वैलिडेटर अभी भी चल रहा है और प्रोग्राम उस पर डिप्लॉय कर दिया गया है।
अब टेस्ट रन करें:
cd client
npm run test
आप देखेंगे कि ट्रांजेक्शन सफलतापूर्वक execute हो गया है।

हमारा प्रोग्राम सफलतापूर्वक execute हुआ और इसने 42 काउंटर वैल्यू के साथ एक अकाउंट बनाया। हम [42, 0, 0, 0, 0, 0, 0, 0] (8 बाइट्स) का serialized CounterData भी देखते हैं।
इस ट्यूटोरियल के अगले भाग में, हम keypair के बजाय PDA का उपयोग करके एक स्टोरेज अकाउंट बनाएंगे। मूलभूत बातें समान रहती हैं—केवल साइन करने का तंत्र (signing mechanism) अलग होता है।
यह लेख Solana डेवलपमेंट पर एक ट्यूटोरियल सीरीज़ का हिस्सा है।