Understanding Arch Programs
This comprehensive guide walks you through building Arch Network programs by examining a complete, working example. We’ll build a “Hello World” program that demonstrates all the essential concepts you need to start developing on Arch Network.
What You’ll Learn
By the end of this guide, you’ll understand:
- Program structure and architecture
- Account management and state handling
- Bitcoin transaction integration
- Error handling best practices
- Testing and deployment patterns
Complete Example: Hello World Program
Let’s build a complete program that stores personalized greetings and demonstrates key Arch Network concepts.
1. Project Setup
First, create your program with the correct dependencies:
Cargo.toml
[package]
name = "hello_world_program"
version = "0.1.0"
edition = "2021"
[dependencies]
arch_program = { path = "../../program" }
borsh = { version = "1.5.1", features = ["derive"] }
[lib]
crate-type = ["cdylib", "lib"]
[workspace]
2. Program Dependencies and Imports
src/lib.rs
#![allow(unused)] fn main() { use arch_program::{ account::AccountInfo, bitcoin::{self, absolute::LockTime, transaction::Version, Transaction}, entrypoint, helper::add_state_transition, input_to_sign::InputToSign, msg, program::{next_account_info, set_transaction_to_sign}, program_error::ProgramError, pubkey::Pubkey, transaction_to_sign::TransactionToSign, }; use borsh::{BorshDeserialize, BorshSerialize}; }
Key Dependencies Explained:
AccountInfo
: Access to account data and metadatabitcoin
: Bitcoin transaction types and functionalityentrypoint
: Macro for registering program entry pointhelper::add_state_transition
: Manages Bitcoin state transitionsmsg
: Logging for debugging and monitoringborsh
: Efficient serialization for program data
3. Program Data Structures
#![allow(unused)] fn main() { /// Parameters sent to our Hello World program #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] pub struct HelloWorldParams { /// The name to include in the greeting pub name: String, /// Bitcoin transaction for paying fees pub tx_hex: Vec<u8>, } /// State stored in the account #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] pub struct GreetingAccount { /// The greeting message pub message: String, /// Who was greeted pub name: String, /// When this greeting was created (block height) pub created_at: u64, /// How many times this account has been updated pub update_count: u32, } impl GreetingAccount { pub const MAX_SIZE: usize = 4 + 50 + 4 + 50 + 8 + 4; // Borsh overhead + data pub fn new(name: String, message: String, block_height: u64) -> Self { Self { message, name, created_at: block_height, update_count: 1, } } } }
4. Custom Error Handling
#![allow(unused)] fn main() { /// Custom errors for our Hello World program #[derive(Debug, Clone, PartialEq, Eq)] pub enum HelloWorldError { /// The provided name is too long (max 50 chars) NameTooLong, /// The provided name is empty NameEmpty, /// The provided name contains invalid characters InvalidCharacters, /// Account data is corrupted InvalidAccountData, /// Insufficient fees provided InsufficientFees, } impl From<HelloWorldError> for ProgramError { fn from(e: HelloWorldError) -> Self { ProgramError::Custom(match e { HelloWorldError::NameTooLong => 1001, HelloWorldError::NameEmpty => 1002, HelloWorldError::InvalidCharacters => 1003, HelloWorldError::InvalidAccountData => 1004, HelloWorldError::InsufficientFees => 1005, }) } } /// Validates the provided name fn validate_name(name: &str) -> Result<(), HelloWorldError> { if name.is_empty() { return Err(HelloWorldError::NameEmpty); } if name.len() > 50 { return Err(HelloWorldError::NameTooLong); } if !name.chars().all(|c| c.is_alphanumeric() || c.is_whitespace() || c == '-' || c == '_') { return Err(HelloWorldError::InvalidCharacters); } Ok(()) } }
5. Program Entry Point and Logic
#![allow(unused)] fn main() { // Register our program's entry point entrypoint!(process_instruction); /// Main program entry point pub fn process_instruction( _program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<(), ProgramError> { msg!("Hello World program invoked"); // Parse instruction data let params: HelloWorldParams = borsh::from_slice(instruction_data) .map_err(|_| ProgramError::InvalidInstructionData)?; // Validate input validate_name(¶ms.name)?; msg!("Processing greeting for: {}", params.name); // Validate accounts if accounts.len() != 1 { msg!("Expected 1 account, got {}", accounts.len()); return Err(ProgramError::NotEnoughAccountKeys); } let account_iter = &mut accounts.iter(); let greeting_account = next_account_info(account_iter)?; // Verify account permissions if !greeting_account.is_writable { msg!("Account must be writable"); return Err(ProgramError::InvalidAccountData); } if !greeting_account.is_signer { msg!("Account must be a signer"); return Err(ProgramError::MissingRequiredSignature); } // Process the greeting process_greeting(greeting_account, ¶ms)?; // Handle Bitcoin transaction handle_bitcoin_transaction(greeting_account, ¶ms.tx_hex)?; msg!("Hello World program completed successfully"); Ok(()) } /// Processes the greeting and updates account state fn process_greeting( account: &AccountInfo, params: &HelloWorldParams, ) -> Result<(), ProgramError> { let current_data_len = account.data.borrow().len(); // Check if account is initialized let mut greeting_data = if current_data_len == 0 { msg!("Initializing new greeting account"); GreetingAccount::new( params.name.clone(), format!("Hello, {}! Welcome to Arch Network!", params.name), 0, // We'll get actual block height in a real implementation ) } else { // Update existing account let existing_data = GreetingAccount::try_from_slice(&account.data.borrow()) .map_err(|_| HelloWorldError::InvalidAccountData)?; msg!("Updating greeting for existing account"); GreetingAccount { message: format!("Hello again, {}! Visit count: {}", params.name, existing_data.update_count + 1), name: params.name.clone(), created_at: existing_data.created_at, update_count: existing_data.update_count + 1, } }; // Serialize the new data let serialized_data = borsh::to_vec(&greeting_data) .map_err(|_| ProgramError::InvalidAccountData)?; // Ensure account has enough space if serialized_data.len() > current_data_len { msg!("Reallocating account space from {} to {} bytes", current_data_len, serialized_data.len()); account.realloc(serialized_data.len(), true)?; } // Write the data account.data.borrow_mut().copy_from_slice(&serialized_data); msg!("Greeting stored: {}", greeting_data.message); Ok(()) } /// Handles Bitcoin transaction for state transition fn handle_bitcoin_transaction( account: &AccountInfo, tx_hex: &[u8], ) -> Result<(), ProgramError> { if tx_hex.is_empty() { return Err(HelloWorldError::InsufficientFees.into()); } // Deserialize the fee transaction let fees_tx: Transaction = bitcoin::consensus::deserialize(tx_hex) .map_err(|_| HelloWorldError::InsufficientFees)?; msg!("Processing Bitcoin transaction with {} inputs", fees_tx.input.len()); // Create state transition transaction let mut tx = Transaction { version: Version::TWO, lock_time: LockTime::ZERO, input: vec![], output: vec![], }; // Add state transition for our account add_state_transition(&mut tx, account); // Add fee input if !fees_tx.input.is_empty() { tx.input.push(fees_tx.input[0].clone()); } // Prepare transaction for signing let tx_to_sign = TransactionToSign { tx_bytes: &bitcoin::consensus::serialize(&tx), inputs_to_sign: &[InputToSign { index: 0, signer: account.key.clone(), }], }; msg!("Submitting transaction for signing"); set_transaction_to_sign(&[account.clone()], tx_to_sign)?; Ok(()) } }
Program Architecture Breakdown
1. Entrypoint Pattern
#![allow(unused)] fn main() { entrypoint!(process_instruction); }
Every Arch program needs exactly one entry point. The entrypoint!
macro registers your process_instruction
function as the program’s main entry point.
2. Function Signature
#![allow(unused)] fn main() { pub fn process_instruction( _program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<(), ProgramError> }
Parameters explained:
program_id
: Your program’s public key (often unused in simple programs)accounts
: Array of accounts this instruction will read/writeinstruction_data
: Serialized parameters for your specific instruction
3. Account Validation
Always validate accounts before use:
#![allow(unused)] fn main() { // Check account count if accounts.len() != expected_count { return Err(ProgramError::NotEnoughAccountKeys); } // Check permissions if !account.is_writable { return Err(ProgramError::InvalidAccountData); } if !account.is_signer { return Err(ProgramError::MissingRequiredSignature); } }
4. State Management
#![allow(unused)] fn main() { // Read existing state let data = MyState::try_from_slice(&account.data.borrow())?; // Modify state let new_data = MyState { /* updated fields */ }; // Serialize and store let serialized = borsh::to_vec(&new_data)?; account.data.borrow_mut().copy_from_slice(&serialized); }
5. Bitcoin Integration
Every state change must be committed to Bitcoin:
#![allow(unused)] fn main() { // Create Bitcoin transaction let mut tx = Transaction { /* ... */ }; // Add state transition add_state_transition(&mut tx, account); // Submit for signing set_transaction_to_sign(accounts, TransactionToSign { /* ... */ })?; }
Testing Your Program
Create comprehensive tests for your program:
tests/integration_test.rs
#![allow(unused)] fn main() { use arch_sdk::helper::sign_and_send_instruction; use arch_test_sdk::{ get_balance_bitcoin, initialize_client, Account, Balance }; #[test] fn test_hello_world_basic() { let (client, _boot_info) = initialize_client(); let program_pubkey = deploy_program(); let user_account = Account::new(); // Test initial greeting let params = HelloWorldParams { name: "Alice".to_string(), tx_hex: create_fee_transaction(), }; let result = send_hello_instruction(&client, &program_pubkey, &user_account, params); assert!(result.is_ok()); // Verify state was stored correctly let account_data = client.read_account_info(user_account.pubkey()).unwrap(); let greeting = GreetingAccount::try_from_slice(&account_data.data).unwrap(); assert_eq!(greeting.name, "Alice"); assert!(greeting.message.contains("Hello, Alice")); assert_eq!(greeting.update_count, 1); } #[test] fn test_error_handling() { // Test name too long let params = HelloWorldParams { name: "A".repeat(100), // Too long tx_hex: create_fee_transaction(), }; let result = send_hello_instruction(&client, &program_pubkey, &user_account, params); assert!(result.is_err()); // Test empty name let params = HelloWorldParams { name: "".to_string(), tx_hex: create_fee_transaction(), }; let result = send_hello_instruction(&client, &program_pubkey, &user_account, params); assert!(result.is_err()); } }
Best Practices
1. Error Handling
- Define custom error types for better debugging
- Use descriptive error messages with
msg!
- Validate all inputs before processing
- Handle both program logic and Bitcoin transaction errors
2. Account Management
- Always check account permissions (
is_signer
,is_writable
) - Validate account ownership when needed
- Use
realloc
when data size changes - Consider account rent and minimum balances
3. State Design
- Keep state structures simple and well-defined
- Use Borsh for efficient serialization
- Consider data size limits
- Plan for state evolution
4. Bitcoin Integration
- Always include fee transactions
- Validate transaction structure
- Use proper input/output management
- Handle signing requirements correctly
5. Security
- Validate all input parameters
- Check account ownership and permissions
- Prevent reentrancy attacks
- Use safe arithmetic operations
Common Patterns
Program-Derived Addresses (PDAs)
#![allow(unused)] fn main() { let (pda, bump) = Pubkey::find_program_address( &[b"greeting", user.key.as_ref()], program_id ); }
Cross-Program Invocation (CPI)
#![allow(unused)] fn main() { let instruction = system_instruction::create_account(/* ... */); invoke(&instruction, &[account1, account2, system_program])?; }
Multiple Instructions
#![allow(unused)] fn main() { match MyInstruction::try_from_slice(instruction_data)? { MyInstruction::Initialize { .. } => process_initialize(accounts)?, MyInstruction::Update { .. } => process_update(accounts)?, MyInstruction::Close => process_close(accounts)?, } }
Next Steps
Now that you understand the fundamentals:
- Explore Advanced Examples: Check out the token program and oracle implementation
- Learn Testing: Set up comprehensive test suites for your programs
- Understand PDAs: Master program-derived addresses for complex state management
- Study CPI: Learn cross-program invocation for composable programs
- Deploy and Monitor: Learn deployment and monitoring best practices
Additional Resources
The complete code for this example is available in the Hello World example.