برامج سولانا المكتوبة بلغة Rust لها متطلبات هيكلية بسيطة، مما يسمح بالمرونة في
كيفية تنظيم الكود. المتطلب الوحيد هو أن يحتوي البرنامج على entrypoint، والذي
يحدد مكان بدء تنفيذ البرنامج.
بنية البرنامج
على الرغم من عدم وجود قواعد صارمة لبنية الملفات، عادةً ما تتبع برامج سولانا نمطاً شائعاً:
entrypoint.rs: يحدد نقطة الدخول التي توجه التعليمات الواردة.state.rs: يحدد حالة البرنامج (بيانات الحساب).instructions.rs: يحدد التعليمات التي يمكن للبرنامج تنفيذها.processor.rs: يحدد معالجات التعليمات (الدوال) التي تنفذ منطق العمل لكل تعليمة.error.rs: يحدد الأخطاء المخصصة التي يمكن للبرنامج إرجاعها.
على سبيل المثال، راجع Token Program.
مثال على برنامج
لتوضيح كيفية بناء برنامج 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. ومع ذلك، فهي مكتبة تسلسل شائعة الاستخدام لبرامج سولانا.
تكوين نوع الحزمة
يجب تجميع برامج سولانا كمكتبات ديناميكية. أضف قسم [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,}
تعريف تعداد التعليمات
لنحدد التعليمات التي يمكن لبرنامجنا تنفيذها. سنستخدم تعداداً حيث يمثل كل متغير تعليمة مختلفة.
أضف الكود التالي إلى lib.rs:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 },IncrementCounter,}
تنفيذ إلغاء تسلسل التعليمات
الآن نحتاج إلى إلغاء تسلسل instruction_data (البايتات الخام) إلى أحد متغيرات
تعداد CounterInstruction الخاصة بنا. تتعامل طريقة try_from_slice في Borsh مع
هذا التحويل تلقائياً.
حدّث دالة 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 فقط هو من يمكنه إنشاء حسابات على سولانا، سنستخدم استدعاء برنامج متقاطع (CPI)، أي استدعاء برنامج آخر من برنامجنا.
يقوم برنامجنا بإجراء CPI لاستدعاء تعليمة create_account الخاصة ببرنامج النظام.
يتم إنشاء الحساب الجديد مع برنامجنا كمالك، مما يمنح برنامجنا القدرة على الكتابة
إلى الحساب وتهيئة البيانات.
أضف الكود التالي إلى 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(())}
هذه التعليمة لأغراض توضيحية فقط. لا تتضمن فحوصات الأمان والتحقق المطلوبة لبرامج الإنتاج.
البرنامج المكتمل
تهانينا! لقد قمت ببناء برنامج سولانا كامل يوضح البنية الأساسية المشتركة بين جميع برامج سولانا:
- نقطة الدخول: تحدد مكان بدء تنفيذ البرنامج وتوجه جميع الطلبات الواردة إلى معالجات التعليمات المناسبة
- معالجة التعليمات: تحدد التعليمات ودوال المعالجات المرتبطة بها
- إدارة الحالة: تحدد بنى بيانات الحسابات وتدير حالتها في الحسابات المملوكة للبرنامج
- الاستدعاء عبر البرامج (CPI): يستدعي برنامج النظام لإنشاء حسابات جديدة مملوكة للبرنامج
الخطوة التالية هي اختبار البرنامج للتأكد من أن كل شيء يعمل بشكل صحيح.
الجزء الثاني: اختبار البرنامج
الآن لنختبر برنامج العداد الخاص بنا. سنستخدم 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
تشغيل المدقق المحلي
للتطوير، سنستخدم مدقق اختبار محلي.
أولاً، قم بتكوين واجهة سطر أوامر سولانا لاستخدام 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
الآن قم بتشغيل مدقق الاختبار في نافذة طرفية منفصلة:
$solana-test-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
تشغيل العميل
مع استمرار تشغيل المدقق المحلي، قم بتنفيذ العميل:
$cargo run --example client
المخرجات المتوقعة:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
مع تشغيل المدقق المحلي، يمكنك عرض المعاملات على
Solana Explorer باستخدام توقيعات
المعاملات الناتجة. لاحظ أنه يجب تعيين المجموعة على Solana Explorer إلى "Custom
RPC URL"، والذي يكون افتراضيًا http://localhost:8899 الذي يعمل عليه
solana-test-validator.
Is this page helpful?