Getting Started with the Rust SDK
This guide will walk you through setting up and using the native Arch Network Rust SDK to build high-performance applications and on-chain programs.
Prerequisites
- Rust 1.70+ with Cargo
- Basic understanding of Rust and blockchain concepts
- Arch Network node running locally or access to a remote node
- Rust development environment set up
Installation
Create a New Rust Project
# Create a new binary project
cargo new my-arch-app --bin
cd my-arch-app
# Or create a library for on-chain programs
cargo new my-arch-program --lib
cd my-arch-program
Add the SDK Dependency
Edit your Cargo.toml
:
[dependencies]
arch_sdk = "0.5.4"
arch_program = "0.5.4" # For on-chain program development
# Required for async operations
tokio = { version = "1.38", features = ["full"] }
# Optional dependencies commonly used
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Your First Connection
Create src/main.rs
:
use arch_sdk::Connection; use anyhow::Result; #[tokio::main] async fn main() -> Result<()> { // Connect to local validator let connection = Connection::new("http://localhost:9002"); // Check if node is ready let is_ready = connection.is_node_ready().await?; println!("Node ready: {}", is_ready); // Get current block count let block_count = connection.get_block_count().await?; println!("Current block count: {}", block_count); Ok(()) }
Run the program:
cargo run
Working with Keypairs
Generate a New Keypair
#![allow(unused)] fn main() { use arch_sdk::Keypair; use arch_program::pubkey::Pubkey; fn create_keypair() { // Generate a new keypair let keypair = Keypair::new(); // Get the public key let pubkey: Pubkey = keypair.pubkey(); println!("Public key: {}", pubkey); // Get the secret key bytes let secret_key_bytes = keypair.secret_key(); println!("Secret key length: {} bytes", secret_key_bytes.len()); // Save keypair to file (be careful with security!) keypair.save_to_file("keypair.json").expect("Failed to save keypair"); // Load keypair from file let loaded_keypair = Keypair::load_from_file("keypair.json") .expect("Failed to load keypair"); } }
Create Keypair from Seed
#![allow(unused)] fn main() { use arch_sdk::Keypair; fn create_from_seed() { // Create from seed phrase or bytes let seed_bytes = b"your-seed-phrase-here-32-bytes!!"; // Must be 32 bytes let keypair = Keypair::from_seed(seed_bytes); println!("Pubkey from seed: {}", keypair.pubkey()); } }
Reading Account Information
Get Account Info
#![allow(unused)] fn main() { use arch_sdk::{Connection, Account}; use arch_program::pubkey::Pubkey; use std::str::FromStr; async fn read_account(connection: &Connection) -> Result<()> { // Parse a public key from string let account_pubkey = Pubkey::from_str("YourAccountAddress...")?; // Get account info match connection.get_account(&account_pubkey).await? { Some(account) => { println!("Owner: {}", account.owner); println!("Lamports: {}", account.lamports); println!("Data length: {}", account.data.len()); println!("Executable: {}", account.executable); } None => { println!("Account not found"); } } Ok(()) } }
Get Multiple Accounts
#![allow(unused)] fn main() { async fn read_multiple_accounts(connection: &Connection) -> Result<()> { let pubkeys = vec![ Pubkey::from_str("Address1...")?, Pubkey::from_str("Address2...")?, Pubkey::from_str("Address3...")?, ]; let accounts = connection.get_multiple_accounts(&pubkeys).await?; for (i, account) in accounts.iter().enumerate() { match account { Some(acc) => println!("Account {}: {} lamports", i, acc.lamports), None => println!("Account {}: Not found", i), } } Ok(()) } }
Building and Sending Transactions
Simple Transfer
#![allow(unused)] fn main() { use arch_sdk::{Connection, Keypair, Transaction}; use arch_program::{ instruction::Instruction, system_instruction, pubkey::Pubkey, }; async fn transfer_lamports(connection: &Connection) -> Result<()> { // Create or load keypairs let sender = Keypair::new(); let recipient = Keypair::new(); // Create transfer instruction let transfer_ix = system_instruction::transfer( &sender.pubkey(), &recipient.pubkey(), 1_000_000, // 1 SOL equivalent in lamports ); // Build transaction let mut transaction = Transaction::new_with_payer( &[transfer_ix], Some(&sender.pubkey()), ); // Get recent blockhash let recent_blockhash = connection.get_latest_blockhash().await?; transaction.message.recent_blockhash = recent_blockhash; // Sign transaction transaction.sign(&[&sender], recent_blockhash); // Send and confirm let signature = connection.send_and_confirm_transaction(&transaction).await?; println!("Transaction signature: {}", signature); Ok(()) } }
Create Account
#![allow(unused)] fn main() { use arch_program::system_instruction; async fn create_account( connection: &Connection, payer: &Keypair, new_account: &Keypair, space: u64, owner: &Pubkey, ) -> Result<()> { // Calculate rent-exempt amount let rent = connection.get_minimum_balance_for_rent_exemption(space).await?; // Create account instruction let create_account_ix = system_instruction::create_account( &payer.pubkey(), &new_account.pubkey(), rent, space, owner, ); // Build and send transaction let mut transaction = Transaction::new_with_payer( &[create_account_ix], Some(&payer.pubkey()), ); let recent_blockhash = connection.get_latest_blockhash().await?; transaction.sign(&[payer, new_account], recent_blockhash); let signature = connection.send_and_confirm_transaction(&transaction).await?; println!("Account created: {}", new_account.pubkey()); println!("Transaction: {}", signature); Ok(()) } }
Developing On-Chain Programs
Basic Program Structure
Create src/lib.rs
for your on-chain program:
#![allow(unused)] fn main() { use arch_program::{ account_info::{next_account_info, AccountInfo}, entrypoint, entrypoint::ProgramResult, msg, pubkey::Pubkey, }; // Declare the program's entrypoint entrypoint!(process_instruction); // Program entrypoint implementation pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { msg!("Hello from Arch program!"); // Parse accounts let accounts_iter = &mut accounts.iter(); let account = next_account_info(accounts_iter)?; // Check account ownership if account.owner != program_id { msg!("Account does not have the correct program id"); return Err(ProgramError::IncorrectProgramId); } // Process instruction data match instruction_data.get(0) { Some(0) => process_initialize(account, instruction_data), Some(1) => process_update(account, instruction_data), _ => Err(ProgramError::InvalidInstructionData), } } fn process_initialize(account: &AccountInfo, data: &[u8]) -> ProgramResult { msg!("Initializing account"); // Implementation here Ok(()) } fn process_update(account: &AccountInfo, data: &[u8]) -> ProgramResult { msg!("Updating account"); // Implementation here Ok(()) } }
Building Programs
Add to Cargo.toml
:
[lib]
crate-type = ["cdylib", "lib"]
[features]
no-entrypoint = []
[dependencies]
arch_program = "0.5.4"
Build the program:
cargo build-bpf
Error Handling
Using Result Types
#![allow(unused)] fn main() { use arch_sdk::ArchError; use anyhow::{Result, Context}; async fn robust_operation(connection: &Connection) -> Result<()> { // Use ? operator for automatic error propagation let block_count = connection.get_block_count().await .context("Failed to get block count")?; // Pattern match on specific errors match connection.get_account(&some_pubkey).await { Ok(Some(account)) => { println!("Account found: {} lamports", account.lamports); } Ok(None) => { println!("Account not found"); } Err(e) => { eprintln!("Error fetching account: {}", e); return Err(e.into()); } } Ok(()) } }
Custom Error Types
#![allow(unused)] fn main() { use thiserror::Error; #[derive(Error, Debug)] pub enum MyProgramError { #[error("Invalid instruction data")] InvalidInstruction, #[error("Insufficient funds: needed {needed}, available {available}")] InsufficientFunds { needed: u64, available: u64 }, #[error("Account not initialized")] UninitializedAccount, } // Use in your program fn validate_account(account: &AccountInfo) -> Result<(), MyProgramError> { if account.data_is_empty() { return Err(MyProgramError::UninitializedAccount); } Ok(()) } }
Advanced Features
Parallel Account Processing
#![allow(unused)] fn main() { use futures::future::join_all; async fn process_accounts_parallel(connection: &Connection, pubkeys: Vec<Pubkey>) -> Result<()> { // Create futures for all account fetches let futures: Vec<_> = pubkeys .iter() .map(|pubkey| connection.get_account(pubkey)) .collect(); // Execute all fetches in parallel let results = join_all(futures).await; // Process results for (i, result) in results.into_iter().enumerate() { match result { Ok(Some(account)) => { println!("Account {}: {} lamports", i, account.lamports); } Ok(None) => { println!("Account {}: Not found", i); } Err(e) => { eprintln!("Error fetching account {}: {}", i, e); } } } Ok(()) } }
Custom Serialization
#![allow(unused)] fn main() { use borsh::{BorshDeserialize, BorshSerialize}; #[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct MyAccountData { pub counter: u64, pub owner: Pubkey, pub timestamp: i64, } impl MyAccountData { pub fn save(&self, account: &AccountInfo) -> ProgramResult { self.serialize(&mut &mut account.data.borrow_mut()[..])?; Ok(()) } pub fn load(account: &AccountInfo) -> Result<Self, ProgramError> { Self::try_from_slice(&account.data.borrow()) .map_err(|_| ProgramError::InvalidAccountData) } } }
Testing
Unit Tests
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use arch_program::clock::Epoch; #[test] fn test_keypair_generation() { let keypair = Keypair::new(); assert_eq!(keypair.secret_key().len(), 64); let pubkey = keypair.pubkey(); assert_eq!(pubkey.to_bytes().len(), 32); } #[tokio::test] async fn test_connection() { let connection = Connection::new("http://localhost:9002"); // This will fail if no local node is running match connection.is_node_ready().await { Ok(ready) => assert!(ready), Err(_) => println!("No local node available for testing"), } } } }
Integration Tests
Create tests/integration_test.rs
:
#![allow(unused)] fn main() { use arch_sdk::{Connection, Keypair}; use anyhow::Result; #[tokio::test] async fn test_full_transaction_flow() -> Result<()> { let connection = Connection::new("http://localhost:9002"); // Only run if node is available if !connection.is_node_ready().await.unwrap_or(false) { println!("Skipping integration test - no node available"); return Ok(()); } // Your integration test logic here Ok(()) } }
Best Practices
Performance Optimization
#![allow(unused)] fn main() { // 1. Reuse connections lazy_static::lazy_static! { static ref CONNECTION: Connection = Connection::new("http://localhost:9002"); } // 2. Batch operations when possible pub async fn get_all_token_accounts(mint: &Pubkey) -> Result<Vec<Account>> { CONNECTION.get_program_accounts_with_config( &token_program_id(), RpcProgramAccountsConfig { filters: Some(vec![ RpcFilterType::Memcmp(Memcmp { offset: 0, bytes: MemcmpEncodedBytes::Base58(mint.to_string()), encoding: None, }), ]), ..Default::default() }, ).await } // 3. Use appropriate data structures use std::collections::HashMap; use dashmap::DashMap; // For concurrent access type AccountCache = DashMap<Pubkey, Account>; }
Security Considerations
#![allow(unused)] fn main() { // 1. Always validate inputs pub fn validate_pubkey(input: &str) -> Result<Pubkey> { Pubkey::from_str(input) .map_err(|e| anyhow::anyhow!("Invalid public key: {}", e)) } // 2. Check account ownership pub fn verify_owner(account: &AccountInfo, expected_owner: &Pubkey) -> ProgramResult { if account.owner != expected_owner { msg!("Account has wrong owner"); return Err(ProgramError::IncorrectProgramId); } Ok(()) } // 3. Prevent arithmetic overflows pub fn safe_add(a: u64, b: u64) -> Option<u64> { a.checked_add(b) } }
Complete Example
Here’s a complete example combining multiple concepts:
use arch_sdk::{Connection, Keypair, Transaction}; use arch_program::{ instruction::Instruction, pubkey::Pubkey, system_instruction, }; use anyhow::Result; use std::str::FromStr; const LAMPORTS_PER_SOL: u64 = 1_000_000_000; #[tokio::main] async fn main() -> Result<()> { // 1. Setup connection let connection = Connection::new("http://localhost:9002"); println!("Connecting to Arch Network..."); // 2. Create or load keypairs let payer = Keypair::new(); let recipient = Keypair::new(); println!("Payer: {}", payer.pubkey()); println!("Recipient: {}", recipient.pubkey()); // 3. Check initial balances let payer_balance = connection.get_balance(&payer.pubkey()).await?; println!("Payer balance: {} SOL", payer_balance as f64 / LAMPORTS_PER_SOL as f64); // 4. Create transfer instruction let transfer_amount = LAMPORTS_PER_SOL / 10; // 0.1 SOL let transfer_ix = system_instruction::transfer( &payer.pubkey(), &recipient.pubkey(), transfer_amount, ); // 5. Build transaction let mut transaction = Transaction::new_with_payer( &[transfer_ix], Some(&payer.pubkey()), ); // 6. Get recent blockhash and sign let recent_blockhash = connection.get_latest_blockhash().await?; transaction.sign(&[&payer], recent_blockhash); // 7. Send transaction println!("Sending transaction..."); match connection.send_and_confirm_transaction(&transaction).await { Ok(signature) => { println!("Transaction successful!"); println!("Signature: {}", signature); // 8. Verify the transfer let recipient_balance = connection.get_balance(&recipient.pubkey()).await?; println!("Recipient balance: {} SOL", recipient_balance as f64 / LAMPORTS_PER_SOL as f64); } Err(e) => { eprintln!("Transaction failed: {}", e); } } Ok(()) }
Next Steps
Now that you understand the basics of the Rust SDK:
- Program Development Guide - Build on-chain programs
- Rust API Reference - Complete API documentation
- Advanced Examples - Complex use cases
- Program Development - General program concepts
- System Calls - System-level operations
Resources
- Crate: arch_sdk on crates.io
- Documentation: docs.rs/arch_sdk
- GitHub: arch-network/arch-network
- Examples: Arch Network Examples
- Discord: Arch Network Discord