Các chương trình Solana được viết bằng Rust có yêu cầu cấu trúc tối thiểu, cho
phép tính linh hoạt trong cách tổ chức mã nguồn. Yêu cầu duy nhất là chương
trình phải có một entrypoint, xác định nơi bắt đầu thực thi của chương trình.
Cấu trúc chương trình
Mặc dù không có quy tắc nghiêm ngặt về cấu trúc tệp, các chương trình Solana thường tuân theo một mẫu chung:
entrypoint.rs: Định nghĩa entrypoint để định tuyến các lệnh đến.state.rs: Định nghĩa trạng thái chương trình (dữ liệu tài khoản).instructions.rs: Định nghĩa các lệnh mà chương trình có thể thực thi.processor.rs: Định nghĩa các trình xử lý lệnh (hàm) triển khai logic nghiệp vụ cho từng lệnh.error.rs: Định nghĩa các lỗi tùy chỉnh mà chương trình có thể trả về.
Ví dụ, xem Token Program.
Chương trình ví dụ
Để minh họa cách xây dựng một chương trình Rust native với nhiều lệnh, chúng ta sẽ tìm hiểu một chương trình đếm đơn giản triển khai hai lệnh:
InitializeCounter: Tạo và khởi tạo một tài khoản mới với giá trị ban đầu.IncrementCounter: Tăng giá trị được lưu trữ trong một tài khoản hiện có.
Để đơn giản, chương trình sẽ được triển khai trong một tệp lib.rs duy nhất,
mặc dù trong thực tế bạn có thể muốn chia các chương trình lớn hơn thành nhiều
tệp.
Phần 1: Viết chương trình
Hãy bắt đầu bằng cách xây dựng chương trình đếm. Chúng ta sẽ tạo một chương trình có thể khởi tạo bộ đếm với giá trị bắt đầu và tăng giá trị đó.
Tạo chương trình mới
Đầu tiên, hãy tạo một dự án Rust mới cho chương trình Solana của chúng ta.
$cargo new counter_program --lib$cd counter_program
Bạn sẽ thấy các tệp src/lib.rs và Cargo.toml mặc định.
Cập nhật trường edition trong Cargo.toml thành 2021. Nếu không, bạn có thể
gặp lỗi khi build chương trình.
Thêm các phụ thuộc
Bây giờ hãy thêm các phụ thuộc cần thiết để build chương trình Solana. Chúng ta
cần solana-program cho core SDK và borsh cho serialization.
$cargo add solana-program@2.2.0$cargo add borsh
Không bắt buộc phải sử dụng Borsh. Tuy nhiên, đây là thư viện serialization được sử dụng phổ biến cho các chương trình Solana.
Cấu hình crate-type
Các chương trình Solana phải được biên dịch dưới dạng dynamic library. Thêm phần
[lib] để cấu hình cách Cargo build chương trình.
[lib]crate-type = ["cdylib", "lib"]
Nếu bạn không thêm cấu hình này, thư mục target/deploy sẽ không được tạo khi bạn build chương trình.
Thiết lập entrypoint của chương trình
Mọi chương trình Solana đều có một entrypoint, đó là hàm được gọi khi chương trình được thực thi. Hãy bắt đầu bằng cách thêm các import cần thiết cho chương trình và thiết lập entrypoint.
Thêm đoạn mã sau vào 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(())}
Macro
entrypoint
xử lý việc deserialization dữ liệu input thành các tham số của hàm
process_instruction.
Một entrypoint của chương trình Solana có function signature như sau. Các
developer hoàn toàn có thể tự tạo implementation riêng của hàm entrypoint.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Định nghĩa trạng thái chương trình
Bây giờ hãy định nghĩa cấu trúc dữ liệu sẽ được lưu trữ trong các tài khoản bộ
đếm của chúng ta. Đây là dữ liệu sẽ được lưu trữ trong trường data của tài
khoản.
Thêm đoạn mã sau vào lib.rs:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {pub count: u64,}
Định nghĩa enum lệnh
Hãy định nghĩa các lệnh mà chương trình của chúng ta có thể thực thi. Chúng ta sẽ sử dụng một enum trong đó mỗi biến thể đại diện cho một lệnh khác nhau.
Thêm đoạn mã sau vào lib.rs:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 },IncrementCounter,}
Triển khai giải mã hóa lệnh
Bây giờ chúng ta cần giải mã hóa instruction_data (các byte thô) thành một
trong các biến thể enum CounterInstruction của chúng ta. Phương thức
try_from_slice của Borsh xử lý việc chuyển đổi này một cách tự động.
Cập nhật hàm process_instruction để sử dụng giải mã hóa 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(())}
Định tuyến lệnh đến các trình xử lý
Bây giờ hãy cập nhật hàm process_instruction chính để định tuyến các lệnh đến
các hàm xử lý thích hợp của chúng.
Mẫu định tuyến này phổ biến trong các chương trình Solana. instruction_data
được giải mã hóa thành một biến thể của enum đại diện cho lệnh, sau đó hàm xử lý
thích hợp được gọi. Mỗi hàm xử lý bao gồm triển khai cho lệnh đó.
Thêm đoạn mã sau vào lib.rs cập nhật hàm process_instruction và thêm các
trình xử lý cho các lệnh InitializeCounter và 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(())}
Triển khai trình xử lý khởi tạo
Hãy triển khai trình xử lý để tạo và khởi tạo một tài khoản bộ đếm mới. Vì chỉ có System Program mới có thể tạo tài khoản trên Solana, chúng ta sẽ sử dụng Cross Program Invocation (CPI), về cơ bản là gọi một chương trình khác từ chương trình của chúng ta.
Chương trình của chúng ta thực hiện CPI để gọi instruction create_account của
System Program. Tài khoản mới được tạo với chương trình của chúng ta làm chủ sở
hữu, cho phép chương trình có khả năng ghi vào tài khoản và khởi tạo dữ liệu.
Thêm đoạn mã sau vào lib.rs cập nhật hàm 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(())}
Instruction này chỉ nhằm mục đích minh họa. Nó không bao gồm các kiểm tra bảo mật và xác thực cần thiết cho các chương trình production.
Triển khai trình xử lý tăng
Bây giờ hãy triển khai trình xử lý tăng giá trị bộ đếm hiện có. Instruction này:
- Đọc trường
datacủa tài khoản chocounter_account - Deserialize nó thành struct
CounterAccount - Tăng trường
countlên 1 - Serialize struct
CounterAccounttrở lại vào trườngdatacủa tài khoản
Thêm đoạn mã sau vào lib.rs cập nhật hàm 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(())}
Instruction này chỉ nhằm mục đích minh họa. Nó không bao gồm các kiểm tra bảo mật và xác thực cần thiết cho các chương trình production.
Chương trình hoàn chỉnh
Chúc mừng! Bạn đã xây dựng một chương trình Solana hoàn chỉnh minh họa cấu trúc cơ bản được chia sẻ bởi tất cả các chương trình Solana:
- Entrypoint: Xác định nơi bắt đầu thực thi chương trình và định tuyến tất cả các yêu cầu đến đến các trình xử lý instruction phù hợp
- Xử lý instruction: Xác định các instruction và các hàm xử lý liên quan của chúng
- Quản lý trạng thái: Xác định cấu trúc dữ liệu tài khoản và quản lý trạng thái của chúng trong các tài khoản thuộc sở hữu chương trình
- Cross Program Invocation (CPI): Gọi System Program để tạo các tài khoản thuộc sở hữu chương trình mới
Bước tiếp theo là kiểm thử chương trình để đảm bảo mọi thứ hoạt động chính xác.
Phần 2: Kiểm thử chương trình
Bây giờ hãy kiểm thử chương trình counter của chúng ta. Chúng ta sẽ sử dụng LiteSVM, một framework kiểm thử cho phép kiểm thử các chương trình mà không cần deploy lên cluster.
Thêm các phụ thuộc kiểm thử
Đầu tiên, hãy thêm các phụ thuộc cần thiết cho việc kiểm thử. Chúng ta sẽ sử
dụng litesvm để kiểm thử và solana-sdk.
$cargo add litesvm@0.6.1 --dev$cargo add solana-sdk@2.2.0 --dev
Tạo module kiểm thử
Bây giờ hãy thêm một module kiểm thử vào chương trình của chúng ta. Chúng ta sẽ bắt đầu với cấu trúc cơ bản và các import.
Thêm đoạn mã sau vào lib.rs, ngay bên dưới mã chương trình:
#[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}}
Thuộc tính #[cfg(test)] đảm bảo mã này chỉ được biên dịch khi chạy các bài
kiểm thử.
Khởi tạo môi trường kiểm thử
Hãy thiết lập môi trường kiểm thử với LiteSVM và nạp tiền cho tài khoản payer.
LiteSVM mô phỏng môi trường runtime của Solana, cho phép chúng ta kiểm thử chương trình mà không cần deploy lên cluster thực.
Thêm đoạn mã sau vào lib.rs cập nhật hàm test_counter_program:
let mut svm = LiteSVM::new();let payer = Keypair::new();svm.airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to airdrop");
Tải chương trình
Bây giờ chúng ta cần build và tải chương trình vào môi trường kiểm thử. Chạy
lệnh cargo build-sbf để build chương trình. Lệnh này sẽ tạo ra file
counter_program.so trong thư mục target/deploy.
$cargo build-sbf
Đảm bảo edition trong Cargo.toml được đặt thành 2021.
Sau khi build, chúng ta có thể load chương trình.
Cập nhật hàm test_counter_program để load chương trình vào môi trường kiểm
thử.
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");
Bạn phải chạy cargo build-sbf trước khi chạy test để tạo file .so. Test sẽ
load chương trình đã được compile.
Kiểm thử instruction khởi tạo
Hãy kiểm thử instruction khởi tạo bằng cách tạo một counter account mới với giá trị ban đầu.
Thêm đoạn mã sau vào lib.rs cập nhật hàm 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);
Xác minh khởi tạo
Sau khi khởi tạo, hãy xác minh counter account đã được tạo đúng cách với giá trị mong đợi.
Thêm đoạn mã sau vào lib.rs cập nhật hàm 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);
Kiểm thử instruction tăng giá trị
Bây giờ hãy kiểm thử instruction tăng giá trị để đảm bảo nó cập nhật giá trị counter một cách chính xác.
Thêm đoạn mã sau vào lib.rs cập nhật hàm 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);
Xác minh kết quả cuối cùng
Cuối cùng, hãy xác minh việc tăng giá trị đã hoạt động đúng bằng cách kiểm tra giá trị counter đã được cập nhật.
Thêm đoạn mã sau vào lib.rs cập nhật hàm 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);
Chạy các bài kiểm tra với lệnh sau. Cờ --nocapture in ra kết quả của bài kiểm
tra.
$cargo test -- --nocapture
Đầu ra dự kiến:
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
Phần 3: Gọi chương trình
Bây giờ hãy thêm một script client để gọi chương trình.
Tạo ví dụ client
Hãy tạo một client Rust để tương tác với chương trình đã triển khai của chúng ta.
$mkdir examples$touch examples/client.rs
Thêm cấu hình sau vào Cargo.toml:
[[example]]name = "client"path = "examples/client.rs"
Cài đặt các dependency của client:
$cargo add solana-client@2.2.0 --dev$cargo add tokio --dev
Triển khai mã client
Bây giờ hãy triển khai client sẽ gọi chương trình đã triển khai của chúng ta.
Chạy lệnh sau để lấy program ID từ file keypair:
$solana address -k ./target/deploy/counter_program-keypair.json
Thêm mã client vào examples/client.rs và thay thế program_id bằng kết quả
của lệnh trước:
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);}}}
Phần 4: Triển khai chương trình
Bây giờ chúng ta đã có chương trình và client sẵn sàng, hãy build, triển khai và gọi chương trình.
Build chương trình
Đầu tiên, hãy build chương trình của chúng ta.
$cargo build-sbf
Lệnh này biên dịch chương trình của bạn và tạo ra hai file quan trọng trong
target/deploy/:
counter_program.so # The compiled programcounter_program-keypair.json # Keypair for the program ID
Bạn có thể xem ID chương trình của mình bằng cách chạy lệnh sau:
$solana address -k ./target/deploy/counter_program-keypair.json
Kết quả ví dụ:
HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Khởi động validator cục bộ
Để phát triển, chúng ta sẽ sử dụng test validator cục bộ.
Đầu tiên, cấu hình Solana CLI để sử dụng localhost:
$solana config set -ul
Kết quả ví dụ:
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed
Bây giờ khởi động test validator trong một terminal riêng:
$solana-test-validator
Triển khai chương trình
Với validator đang chạy, triển khai chương trình của bạn lên cluster cục bộ:
$solana program deploy ./target/deploy/counter_program.so
Kết quả ví dụ:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
Bạn có thể xác minh việc triển khai bằng lệnh solana program show với program
ID của bạn:
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Kết quả ví dụ:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
Chạy client
Với validator cục bộ vẫn đang chạy, thực thi client:
$cargo run --example client
Kết quả mong đợi:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
Với validator cục bộ đang chạy, bạn có thể xem các giao dịch trên
Solana Explorer bằng cách sử dụng
chữ ký giao dịch đầu ra. Lưu ý rằng cluster trên Solana Explorer phải được đặt
thành "Custom RPC URL", mặc định là http://localhost:8899 mà
solana-test-validator đang chạy.
Is this page helpful?