بنية برنامج Rust
تتميز برامج سولانا المكتوبة بلغة Rust بمتطلبات هيكلية بسيطة، مما يتيح المرونة في
تنظيم الشيفرة. المتطلب الوحيد هو أن البرنامج يجب أن يحتوي على entrypoint
، الذي
يحدد نقطة بداية تنفيذ البرنامج.
بنية البرنامج
على الرغم من عدم وجود قواعد صارمة لهيكل الملفات، فإن برامج سولانا عادةً تتبع نمطًا شائعًا:
entrypoint.rs
: يحدد نقطة الدخول التي توجه التعليمات الواردة.state.rs
: يحدد حالة البرنامج (بيانات الحساب).instructions.rs
: يحدد التعليمات التي يمكن للبرنامج تنفيذها.processor.rs
: يحدد معالجات التعليمات (الوظائف) التي تنفذ المنطق التجاري لكل تعليمة.error.rs
: يحدد الأخطاء المخصصة التي يمكن للبرنامج إرجاعها.
على سبيل المثال، انظر إلى برنامج الرمز المميز.
مثال على البرنامج
لتوضيح كيفية بناء برنامج Rust أصلي مع تعليمات متعددة، سنشرح برنامج عداد بسيط ينفذ تعليمتين:
InitializeCounter
: ينشئ ويهيئ حسابًا جديدًا بقيمة أولية.IncrementCounter
: يزيد القيمة المخزنة في حساب موجود.
للتبسيط، سيتم تنفيذ البرنامج في ملف lib.rs
واحد، على الرغم من أنه في الممارسة
العملية قد ترغب في تقسيم البرامج الأكبر إلى ملفات متعددة.
الجزء الأول: كتابة البرنامج
لنبدأ ببناء برنامج العداد. سننشئ برنامجًا يمكنه تهيئة عداد بقيمة بدء وزيادتها.
إنشاء برنامج جديد
أولاً، دعنا ننشئ مشروع Rust جديد لبرنامج سولانا الخاص بنا.
$cargo new counter_program --lib$cd counter_program
يجب أن ترى ملفات src/lib.rs
و Cargo.toml
الافتراضية.
قم بتحديث حقل edition
في Cargo.toml
إلى 2021. وإلا، قد تواجه خطأ عند بناء
البرنامج.
إضافة التبعيات
الآن دعنا نضيف التبعيات الضرورية لبناء برنامج سولانا. نحتاج إلى solana-program
للحصول على SDK الأساسي و borsh
للتسلسل.
$cargo add solana-program@2.2.0$cargo add borsh
لا يوجد شرط لاستخدام Borsh. ومع ذلك، فهي مكتبة تسلسل شائعة الاستخدام لبرامج سولانا.
تكوين crate-type
يجب تجميع برامج سولانا كمكتبات ديناميكية. أضف قسم [lib]
لتكوين كيفية بناء
Cargo للبرنامج.
[lib]crate-type = ["cdylib", "lib"]
إذا لم تقم بتضمين هذا التكوين، فلن يتم إنشاء دليل target/deploy عند بناء البرنامج.
إعداد نقطة دخول البرنامج
كل برنامج سولانا له نقطة دخول، وهي الوظيفة التي يتم استدعاؤها عند تشغيل البرنامج. دعنا نبدأ بإضافة الاستيرادات التي سنحتاجها للبرنامج وإعداد نقطة الدخول.
أضف الكود التالي إلى lib.rs
:
use borsh::{BorshDeserialize, BorshSerialize};use solana_program::{account_info::{next_account_info, AccountInfo},entrypoint,entrypoint::ProgramResult,msg,program::invoke,program_error::ProgramError,pubkey::Pubkey,system_instruction,sysvar::{rent::Rent, Sysvar},};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {Ok(())}
يتعامل ماكرو
entrypoint
مع إلغاء تسلسل بيانات input
إلى معلمات وظيفة process_instruction
.
يحتوي entrypoint
لبرنامج سولانا على توقيع الوظيفة التالي. المطورون أحرار في
إنشاء تنفيذهم الخاص لوظيفة entrypoint
.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
تحديد حالة البرنامج
الآن دعنا نحدد بنية البيانات التي سيتم تخزينها في حسابات العداد الخاصة بنا. هذه
هي البيانات التي سيتم تخزينها في حقل data
الخاص بالحساب.
أضف الكود التالي إلى lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {pub count: u64,}
تعريف تعداد التعليمات
دعنا نحدد التعليمات التي يمكن لبرنامجنا تنفيذها. سنستخدم تعداداً (enum) حيث يمثل كل متغير تعليمة مختلفة.
أضف الكود التالي إلى lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 },IncrementCounter,}
تنفيذ فك تشفير التعليمات
الآن نحتاج إلى فك تشفير instruction_data
(البايتات الخام) إلى أحد متغيرات
التعداد CounterInstruction
. تتعامل طريقة Borsh try_from_slice
مع هذا التحويل
تلقائياً.
قم بتحديث وظيفة process_instruction
لاستخدام فك تشفير Borsh:
pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {let instruction = CounterInstruction::try_from_slice(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;Ok(())}
توجيه التعليمات إلى المعالجات
الآن دعنا نحدث وظيفة process_instruction
الرئيسية لتوجيه التعليمات إلى وظائف
المعالجة المناسبة لها.
هذا النمط من التوجيه شائع في برامج سولانا. يتم فك تشفير instruction_data
إلى
متغير من تعداد يمثل التعليمة، ثم يتم استدعاء وظيفة المعالجة المناسبة. تتضمن كل
وظيفة معالجة التنفيذ الخاص بتلك التعليمة.
أضف الكود التالي إلى lib.rs
لتحديث وظيفة process_instruction
وإضافة معالجات
لتعليمات InitializeCounter
و IncrementCounter
:
pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {let instruction = CounterInstruction::try_from_slice(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;match instruction {CounterInstruction::InitializeCounter { initial_value } => {process_initialize_counter(program_id, accounts, initial_value)?}CounterInstruction::IncrementCounter => {process_increment_counter(program_id, accounts)?}};Ok(())}fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {Ok(())}fn process_increment_counter(program_id: &Pubkey,accounts: &[AccountInfo],) -> ProgramResult {Ok(())}
تنفيذ معالج التهيئة
دعنا ننفذ المعالج لإنشاء وتهيئة حساب عداد جديد. بما أن System Program فقط هو الذي يمكنه إنشاء حسابات على سولانا، سنستخدم استدعاء عبر البرامج (Cross Program Invocation - CPI)، وهو في الأساس استدعاء برنامج آخر من برنامجنا.
يقوم برنامجنا بإجراء CPI لاستدعاء تعليمة create_account
الخاصة بـ System
Program. يتم إنشاء الحساب الجديد مع برنامجنا كمالك، مما يمنح برنامجنا القدرة على
الكتابة في الحساب وتهيئة البيانات.
أضف الكود التالي إلى lib.rs
لتحديث الدالة process_initialize_counter
:
fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;let payer_account = next_account_info(accounts_iter)?;let system_program = next_account_info(accounts_iter)?;let account_space = 8;let rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);invoke(&system_instruction::create_account(payer_account.key,counter_account.key,required_lamports,account_space as u64,program_id,),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;let counter_data = CounterAccount {count: initial_value,};let mut account_data = &mut counter_account.data.borrow_mut()[..];counter_data.serialize(&mut account_data)?;msg!("Counter initialized with value: {}", initial_value);Ok(())}
هذه التعليمات لأغراض التوضيح فقط. لا تتضمن فحوصات الأمان والتحقق المطلوبة للبرامج الإنتاجية.
تنفيذ معالج الزيادة
الآن دعنا ننفذ المعالج الذي يزيد العداد الموجود. هذه التعليمات:
- تقرأ حقل
data
للحسابcounter_account
- تحول البيانات إلى بنية
CounterAccount
- تزيد حقل
count
بمقدار 1 - تعيد تحويل بنية
CounterAccount
مرة أخرى إلى حقلdata
للحساب
أضف الكود التالي إلى lib.rs
لتحديث الدالة process_increment_counter
:
fn process_increment_counter(program_id: &Pubkey,accounts: &[AccountInfo],) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;if counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}let mut data = counter_account.data.borrow_mut();let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;counter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;counter_data.serialize(&mut &mut data[..])?;msg!("Counter incremented to: {}", counter_data.count);Ok(())}
هذه التعليمات لأغراض التوضيح فقط. لا تتضمن فحوصات الأمان والتحقق المطلوبة للبرامج الإنتاجية.
البرنامج المكتمل
تهانينا! لقد قمت ببناء برنامج سولانا كامل يوضح الهيكل الأساسي المشترك بين جميع برامج سولانا:
- نقطة الدخول: تحدد أين يبدأ تنفيذ البرنامج وتوجه جميع الطلبات الواردة إلى معالجات التعليمات المناسبة
- معالجة التعليمات: تحدد التعليمات ودوال المعالجة المرتبطة بها
- إدارة الحالة: تحدد هياكل بيانات الحساب وتدير حالتها في الحسابات المملوكة للبرنامج
- استدعاء البرامج المتقاطع (Cross Program Invocation): يستدعي System Program لإنشاء حسابات جديدة مملوكة للبرنامج
الخطوة التالية هي اختبار البرنامج للتأكد من أن كل شيء يعمل بشكل صحيح.
الجزء 2: اختبار البرنامج
الآن دعنا نختبر برنامج العداد الخاص بنا. سنستخدم LiteSVM، وهو إطار اختبار يتيح لنا اختبار البرامج دون نشرها على مجموعة.
إضافة تبعيات الاختبار
أولاً، دعنا نضيف التبعيات اللازمة للاختبار. سنستخدم litesvm
للاختبار و
solana-sdk
.
$cargo add litesvm@0.6.1 --dev$cargo add solana-sdk@2.2.0 --dev
إنشاء وحدة اختبار
الآن دعنا نضيف وحدة اختبار إلى برنامجنا. سنبدأ بالهيكل الأساسي والاستيرادات.
أضف الكود التالي إلى lib.rs
، مباشرة أسفل كود البرنامج:
#[cfg(test)]mod test {use super::*;use litesvm::LiteSVM;use solana_sdk::{account::ReadableAccount,instruction::{AccountMeta, Instruction},message::Message,signature::{Keypair, Signer},system_program,transaction::Transaction,};#[test]fn test_counter_program() {// Test implementation will go here}}
السمة #[cfg(test)]
تضمن أن هذا الكود يتم تجميعه فقط عند تشغيل الاختبارات.
تهيئة بيئة الاختبار
دعنا نقوم بإعداد بيئة الاختبار باستخدام LiteSVM وتمويل حساب الدافع.
يحاكي LiteSVM بيئة تشغيل سولانا، مما يسمح لنا باختبار برنامجنا دون النشر على مجموعة حقيقية.
أضف الكود التالي إلى lib.rs
مع تحديث الدالة test_counter_program
:
let mut svm = LiteSVM::new();let payer = Keypair::new();svm.airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to airdrop");
تحميل البرنامج
الآن نحتاج إلى بناء وتحميل برنامجنا في بيئة الاختبار. قم بتشغيل الأمر
cargo build-sbf
لبناء البرنامج. سيؤدي هذا إلى إنشاء ملف counter_program.so
في دليل target/deploy
.
$cargo build-sbf
تأكد من أن edition
في Cargo.toml
مضبوط على 2021
.
بعد البناء، يمكننا تحميل البرنامج.
قم بتحديث الدالة test_counter_program
لتحميل البرنامج في بيئة الاختبار.
let program_keypair = Keypair::new();let program_id = program_keypair.pubkey();svm.add_program_from_file(program_id,"target/deploy/counter_program.so").expect("Failed to load program");
يجب عليك تشغيل cargo build-sbf
قبل تشغيل الاختبارات لإنشاء ملف .so
. يقوم
الاختبار بتحميل البرنامج المُجمّع.
اختبار تعليمة التهيئة
دعنا نختبر تعليمة التهيئة من خلال إنشاء حساب عداد جديد بقيمة ابتدائية.
أضف الكود التالي إلى lib.rs
لتحديث الدالة test_counter_program
:
let counter_keypair = Keypair::new();let initial_value: u64 = 42;println!("Testing counter initialization...");let init_instruction_data =borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value }).expect("Failed to serialize instruction");let initialize_instruction = Instruction::new_with_bytes(program_id,&init_instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true),AccountMeta::new(payer.pubkey(), true),AccountMeta::new_readonly(system_program::id(), false),],);let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));let transaction = Transaction::new(&[&payer, &counter_keypair],message,svm.latest_blockhash());let result = svm.send_transaction(transaction);assert!(result.is_ok(), "Initialize transaction should succeed");let logs = result.unwrap().logs;println!("Transaction logs:\n{:#?}", logs);
التحقق من التهيئة
بعد التهيئة، دعنا نتحقق من إنشاء حساب العداد بشكل صحيح بالقيمة المتوقعة.
أضف الكود التالي إلى lib.rs
لتحديث الدالة test_counter_program
:
let account = svm.get_account(&counter_keypair.pubkey()).expect("Failed to get counter account");let counter: CounterAccount = CounterAccount::try_from_slice(account.data()).expect("Failed to deserialize counter data");assert_eq!(counter.count, 42);println!("Counter initialized successfully with value: {}", counter.count);
اختبار تعليمة الزيادة
الآن دعنا نختبر تعليمة الزيادة للتأكد من أنها تحدث قيمة العداد بشكل صحيح.
أضف الكود التالي إلى lib.rs
لتحديث الدالة test_counter_program
:
println!("Testing counter increment...");let increment_instruction_data =borsh::to_vec(&CounterInstruction::IncrementCounter).expect("Failed to serialize instruction");let increment_instruction = Instruction::new_with_bytes(program_id,&increment_instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true)],);let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));let transaction = Transaction::new(&[&payer, &counter_keypair],message,svm.latest_blockhash());let result = svm.send_transaction(transaction);assert!(result.is_ok(), "Increment transaction should succeed");let logs = result.unwrap().logs;println!("Transaction logs:\n{:#?}", logs);
التحقق من النتائج النهائية
أخيرًا، دعنا نتحقق من أن الزيادة عملت بشكل صحيح من خلال فحص قيمة العداد المحدثة.
أضف الكود التالي إلى lib.rs
لتحديث الدالة test_counter_program
:
let account = svm.get_account(&counter_keypair.pubkey()).expect("Failed to get counter account");let counter: CounterAccount = CounterAccount::try_from_slice(account.data()).expect("Failed to deserialize counter data");assert_eq!(counter.count, 43);println!("Counter incremented successfully to: {}", counter.count);
قم بتشغيل الاختبارات باستخدام الأمر التالي. العلامة --nocapture
تطبع مخرجات
الاختبار.
$cargo test -- --nocapture
المخرجات المتوقعة:
Testing counter initialization...Transaction logs:["Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]","Program 11111111111111111111111111111111 invoke [2]","Program 11111111111111111111111111111111 success","Program log: Counter initialized with value: 42","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",]Counter initialized successfully with value: 42Testing counter increment...Transaction logs:["Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]","Program log: Counter incremented to: 43","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",]Counter incremented successfully to: 43
الجزء 3: استدعاء البرنامج
الآن دعنا نضيف نصًا برمجيًا للعميل لاستدعاء البرنامج.
إنشاء مثال للعميل
دعنا ننشئ عميل Rust للتفاعل مع برنامجنا المنشور.
$mkdir examples$touch examples/client.rs
أضف التكوين التالي إلى Cargo.toml
:
[[example]]name = "client"path = "examples/client.rs"
قم بتثبيت تبعيات العميل:
$cargo add solana-client@2.2.0 --dev$cargo add tokio --dev
تنفيذ كود العميل
الآن دعنا ننفذ العميل الذي سيستدعي برنامجنا المنشور.
قم بتشغيل الأمر التالي للحصول على معرف البرنامج الخاص بك من ملف keypair:
$solana address -k ./target/deploy/counter_program-keypair.json
أضف كود العميل إلى examples/client.rs
واستبدل program_id
بمخرجات الأمر
السابق:
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");
use solana_client::rpc_client::RpcClient;use solana_sdk::{commitment_config::CommitmentConfig,instruction::{AccountMeta, Instruction},pubkey::Pubkey,signature::{Keypair, Signer},system_program,transaction::Transaction,};use std::str::FromStr;use counter_program::CounterInstruction;#[tokio::main]async fn main() {// Replace with your actual program ID from deploymentlet program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");// Connect to local clusterlet rpc_url = String::from("http://localhost:8899");let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());// Generate a new keypair for paying feeslet payer = Keypair::new();// Request airdrop of 1 SOL for transaction feesprintln!("Requesting airdrop...");let airdrop_signature = client.request_airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to request airdrop");// Wait for airdrop confirmationloop {if client.confirm_transaction(&airdrop_signature).unwrap_or(false){break;}std::thread::sleep(std::time::Duration::from_millis(500));}println!("Airdrop confirmed");println!("\nInitializing counter...");let counter_keypair = Keypair::new();let initial_value = 100u64;// Serialize the initialize instruction datalet instruction_data = borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value }).expect("Failed to serialize instruction");let initialize_instruction = Instruction::new_with_bytes(program_id,&instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true),AccountMeta::new(payer.pubkey(), true),AccountMeta::new_readonly(system_program::id(), false),],);let mut transaction =Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));let blockhash = client.get_latest_blockhash().expect("Failed to get blockhash");transaction.sign(&[&payer, &counter_keypair], blockhash);match client.send_and_confirm_transaction(&transaction) {Ok(signature) => {println!("Counter initialized!");println!("Transaction: {}", signature);println!("Counter address: {}", counter_keypair.pubkey());}Err(err) => {eprintln!("Failed to initialize counter: {}", err);return;}}println!("\nIncrementing counter...");// Serialize the increment instruction datalet increment_data = borsh::to_vec(&CounterInstruction::IncrementCounter).expect("Failed to serialize instruction");let increment_instruction = Instruction::new_with_bytes(program_id,&increment_data,vec![AccountMeta::new(counter_keypair.pubkey(), true)],);let mut transaction =Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], blockhash);match client.send_and_confirm_transaction(&transaction) {Ok(signature) => {println!("Counter incremented!");println!("Transaction: {}", signature);}Err(err) => {eprintln!("Failed to increment counter: {}", err);}}}
الجزء 4: نشر البرنامج
الآن بعد أن أصبح برنامجنا والعميل جاهزين، دعنا نقوم ببناء ونشر واستدعاء البرنامج.
بناء البرنامج
أولاً، دعنا نبني برنامجنا.
$cargo build-sbf
يقوم هذا الأمر بتجميع برنامجك وإنشاء ملفين مهمين في target/deploy/
:
counter_program.so # The compiled programcounter_program-keypair.json # Keypair for the program ID
يمكنك عرض معرّف برنامجك عن طريق تشغيل الأمر التالي:
$solana address -k ./target/deploy/counter_program-keypair.json
مثال للمخرجات:
HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
بدء validator محلي
لأغراض التطوير، سنستخدم validator اختبار محلي.
أولاً، قم بتكوين واجهة سطر أوامر سولانا لاستخدام localhost:
$solana config set -ul
مثال للمخرجات:
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed
الآن قم بتشغيل validator الاختبار في نافذة طرفية منفصلة:
$solana-test-validator
نشر البرنامج
مع تشغيل validator، قم بنشر برنامجك على المجموعة المحلية:
$solana program deploy ./target/deploy/counter_program.so
مثال للمخرجات:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
يمكنك التحقق من النشر باستخدام أمر solana program show
مع معرّف برنامجك:
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
مثال على النتائج:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
تشغيل العميل
مع استمرار تشغيل validator المحلي، قم بتنفيذ العميل:
$cargo run --example client
النتائج المتوقعة:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
مع تشغيل validator المحلي، يمكنك عرض المعاملات على
مستكشف سولانا باستخدام توقيعات
المعاملات الناتجة. لاحظ أنه يجب تعيين المجموعة في مستكشف سولانا إلى "عنوان URL
مخصص لـ RPC"، والذي يكون افتراضيًا http://localhost:8899
الذي يعمل عليه
solana-test-validator
.
Is this page helpful?