Writing Your First Arch Program
A comprehensive guide to creating your first Arch program from scratch with a complete counter example
This comprehensive guide walks you through creating your first Arch program from scratch. We'll build a feature-rich counter program that demonstrates the complete development workflow and all essential concepts you need for building production-ready Arch Network applications.
What You'll Build
By the end of this guide, you'll have created a complete counter program that:
- Manages state in program accounts
- Handles multiple instruction types
- Integrates with Bitcoin transactions
- Includes comprehensive error handling
- Provides extensive testing coverage
- Follows security best practices
Prerequisites
Before starting, ensure you have:
- Rust 1.70+ and Cargo installed (Install Rust)
- Solana CLI 2.0+ - Install Guide
- Arch Network CLI - Download Latest
- Running validator (see Validator Setup Guide)
- Basic Rust knowledge and understanding of Arch concepts
Step 1: Project Setup
1.1 Create Project Structure
# Create project directory
mkdir my-counter-program
cd my-counter-program
# Create program directory
mkdir program
cd program
# Initialize Rust library
cargo init --lib
1.2 Configure Dependencies
Create a proper Cargo.toml
:
program/Cargo.toml
[package]
name = "my_counter_program"
version = "0.1.0"
edition = "2021"
[dependencies]
arch_program = "0.5.4"
borsh = { version = "1.5.1", features = ["derive"] }
[lib]
crate-type = ["cdylib", "lib"]
[workspace]
1.3 Project Structure
Your project should look like this:
my-counter-program/
├── program/
│ ├── src/
│ │ └── lib.rs
│ └── Cargo.toml
├── client/ # We'll add this later
└── tests/ # We'll add this later
Step 2: Define Program Data Structures
Create comprehensive data structures for your program:
program/src/lib.rs
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};
/// Counter program state stored in accounts
#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq)]
pub struct CounterAccount {
/// Current counter value
pub count: i64,
/// Who created this counter
pub owner: Pubkey,
/// When this counter was created
pub created_at: i64,
/// Last time the counter was updated
pub last_updated: i64,
/// Whether this counter is active
pub is_active: bool,
}
/// Instructions that this program can handle
#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
pub enum CounterInstruction {
/// Initialize a new counter
Initialize {
initial_value: i64,
},
/// Increment the counter by a specified amount
Increment {
amount: i64,
},
/// Decrement the counter by a specified amount
Decrement {
amount: i64,
},
/// Reset the counter to zero
Reset,
/// Transfer ownership of the counter
TransferOwnership {
new_owner: Pubkey,
},
/// Deactivate the counter
Deactivate,
/// Reactivate the counter
Reactivate,
}
/// Program errors
#[derive(Debug)]
pub enum CounterError {
InvalidInstruction,
InvalidAccount,
Unauthorized,
CounterOverflow,
CounterUnderflow,
CounterInactive,
InvalidOwner,
}
impl From<CounterError> for ProgramError {
fn from(e: CounterError) -> Self {
ProgramError::Custom(e as u32)
}
}
Step 3: Implement Program Logic
Add the core program implementation:
/// Process a single instruction
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), ProgramError> {
msg!("Counter program entry point");
// Parse the instruction
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| CounterError::InvalidInstruction)?;
// Get account iterator
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
let owner_account = next_account_info(accounts_iter)?;
// Verify the account is owned by this program
if counter_account.owner != program_id {
return Err(CounterError::InvalidAccount.into());
}
// Process the instruction
match instruction {
CounterInstruction::Initialize { initial_value } => {
process_initialize(counter_account, owner_account, initial_value)
}
CounterInstruction::Increment { amount } => {
process_increment(counter_account, owner_account, amount)
}
CounterInstruction::Decrement { amount } => {
process_decrement(counter_account, owner_account, amount)
}
CounterInstruction::Reset => {
process_reset(counter_account, owner_account)
}
CounterInstruction::TransferOwnership { new_owner } => {
process_transfer_ownership(counter_account, owner_account, &new_owner)
}
CounterInstruction::Deactivate => {
process_deactivate(counter_account, owner_account)
}
CounterInstruction::Reactivate => {
process_reactivate(counter_account, owner_account)
}
}
}
/// Initialize a new counter
fn process_initialize(
counter_account: &AccountInfo,
owner_account: &AccountInfo,
initial_value: i64,
) -> Result<(), ProgramError> {
msg!("Initializing counter with value: {}", initial_value);
// Check if account is already initialized
if counter_account.data_len() > 0 {
return Err(CounterError::InvalidAccount.into());
}
// Create new counter account
let counter = CounterAccount {
count: initial_value,
owner: *owner_account.key,
created_at: get_current_timestamp(),
last_updated: get_current_timestamp(),
is_active: true,
};
// Serialize and store the counter
let serialized = counter.try_to_vec()
.map_err(|_| ProgramError::InvalidAccountData)?;
counter_account.try_borrow_mut_data()?[..serialized.len()].copy_from_slice(&serialized);
msg!("Counter initialized successfully");
Ok(())
}
/// Increment the counter
fn process_increment(
counter_account: &AccountInfo,
owner_account: &AccountInfo,
amount: i64,
) -> Result<(), ProgramError> {
msg!("Incrementing counter by: {}", amount);
// Load and verify counter
let mut counter = load_counter(counter_account)?;
verify_owner(&counter, owner_account)?;
verify_active(&counter)?;
// Check for overflow
if counter.count > i64::MAX - amount {
return Err(CounterError::CounterOverflow.into());
}
// Update counter
counter.count += amount;
counter.last_updated = get_current_timestamp();
// Save updated counter
save_counter(counter_account, &counter)?;
msg!("Counter incremented to: {}", counter.count);
Ok(())
}
/// Decrement the counter
fn process_decrement(
counter_account: &AccountInfo,
owner_account: &AccountInfo,
amount: i64,
) -> Result<(), ProgramError> {
msg!("Decrementing counter by: {}", amount);
// Load and verify counter
let mut counter = load_counter(counter_account)?;
verify_owner(&counter, owner_account)?;
verify_active(&counter)?;
// Check for underflow
if counter.count < amount {
return Err(CounterError::CounterUnderflow.into());
}
// Update counter
counter.count -= amount;
counter.last_updated = get_current_timestamp();
// Save updated counter
save_counter(counter_account, &counter)?;
msg!("Counter decremented to: {}", counter.count);
Ok(())
}
/// Reset the counter to zero
fn process_reset(
counter_account: &AccountInfo,
owner_account: &AccountInfo,
) -> Result<(), ProgramError> {
msg!("Resetting counter");
// Load and verify counter
let mut counter = load_counter(counter_account)?;
verify_owner(&counter, owner_account)?;
verify_active(&counter)?;
// Reset counter
counter.count = 0;
counter.last_updated = get_current_timestamp();
// Save updated counter
save_counter(counter_account, &counter)?;
msg!("Counter reset to: {}", counter.count);
Ok(())
}
/// Transfer ownership of the counter
fn process_transfer_ownership(
counter_account: &AccountInfo,
owner_account: &AccountInfo,
new_owner: &Pubkey,
) -> Result<(), ProgramError> {
msg!("Transferring ownership to: {}", new_owner);
// Load and verify counter
let mut counter = load_counter(counter_account)?;
verify_owner(&counter, owner_account)?;
// Update owner
counter.owner = *new_owner;
counter.last_updated = get_current_timestamp();
// Save updated counter
save_counter(counter_account, &counter)?;
msg!("Ownership transferred successfully");
Ok(())
}
/// Deactivate the counter
fn process_deactivate(
counter_account: &AccountInfo,
owner_account: &AccountInfo,
) -> Result<(), ProgramError> {
msg!("Deactivating counter");
// Load and verify counter
let mut counter = load_counter(counter_account)?;
verify_owner(&counter, owner_account)?;
// Deactivate counter
counter.is_active = false;
counter.last_updated = get_current_timestamp();
// Save updated counter
save_counter(counter_account, &counter)?;
msg!("Counter deactivated");
Ok(())
}
/// Reactivate the counter
fn process_reactivate(
counter_account: &AccountInfo,
owner_account: &AccountInfo,
) -> Result<(), ProgramError> {
msg!("Reactivating counter");
// Load and verify counter
let mut counter = load_counter(counter_account)?;
verify_owner(&counter, owner_account)?;
// Reactivate counter
counter.is_active = true;
counter.last_updated = get_current_timestamp();
// Save updated counter
save_counter(counter_account, &counter)?;
msg!("Counter reactivated");
Ok(())
}
Step 4: Helper Functions
Add utility functions for common operations:
/// Load a counter from account data
fn load_counter(account: &AccountInfo) -> Result<CounterAccount, ProgramError> {
let data = account.try_borrow_data()?;
CounterAccount::try_from_slice(&data)
.map_err(|_| ProgramError::InvalidAccountData)
}
/// Save a counter to account data
fn save_counter(account: &AccountInfo, counter: &CounterAccount) -> Result<(), ProgramError> {
let serialized = counter.try_to_vec()
.map_err(|_| ProgramError::InvalidAccountData)?;
let mut data = account.try_borrow_mut_data()?;
data[..serialized.len()].copy_from_slice(&serialized);
Ok(())
}
/// Verify that the account is the owner
fn verify_owner(counter: &CounterAccount, owner_account: &AccountInfo) -> Result<(), ProgramError> {
if counter.owner != *owner_account.key {
return Err(CounterError::Unauthorized.into());
}
Ok(())
}
/// Verify that the counter is active
fn verify_active(counter: &CounterAccount) -> Result<(), ProgramError> {
if !counter.is_active {
return Err(CounterError::CounterInactive.into());
}
Ok(())
}
/// Get current timestamp (simplified for demo)
fn get_current_timestamp() -> i64 {
// In a real implementation, you'd get this from the system
// For now, we'll use a placeholder
1234567890
}
Step 5: Program Entry Point
Add the program entry point:
// Declare the program's entry point
entrypoint!(process_instruction);
Step 6: Build and Deploy
6.1 Build the Program
# Build the program
cargo build-sbf
# Check the build output
ls -la target/deploy/
6.2 Deploy to Local Network
# Deploy the program
arch-cli deploy target/deploy/my_counter_program.so
# Note the program ID for later use
Step 7: Create a Client
Create a client to interact with your program:
client/Cargo.toml
[package]
name = "counter-client"
version = "0.1.0"
edition = "2021"
[dependencies]
arch_program = "0.5.4"
borsh = { version = "1.5.1", features = ["derive"] }
solana-client = "1.17"
solana-sdk = "1.17"
client/src/main.rs
use arch_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
system_instruction,
};
use borsh::{BorshDeserialize, BorshSerialize};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
signature::{Keypair, Signer},
transaction::Transaction,
};
// Import your program types
use my_counter_program::{CounterAccount, CounterInstruction};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to local validator
let client = RpcClient::new("http://localhost:9002".to_string());
// Create keypairs
let payer = Keypair::new();
let counter_keypair = Keypair::new();
// Airdrop some lamports to the payer
client.request_airdrop(&payer.pubkey(), 1_000_000_000)?;
// Create counter account
let space = std::mem::size_of::<CounterAccount>();
let create_account_ix = system_instruction::create_account(
&payer.pubkey(),
&counter_keypair.pubkey(),
1_000_000, // rent
space as u64,
&program_id(), // Your program ID
);
// Initialize counter instruction
let init_ix = Instruction {
program_id: program_id(),
accounts: vec![
AccountMeta::new(counter_keypair.pubkey(), false),
AccountMeta::new_readonly(payer.pubkey(), true),
],
data: CounterInstruction::Initialize { initial_value: 42 }
.try_to_vec()?,
};
// Create and send transaction
let mut transaction = Transaction::new_with_payer(
&[create_account_ix, init_ix],
Some(&payer.pubkey()),
);
let recent_blockhash = client.get_latest_blockhash()?;
transaction.sign(&[&payer, &counter_keypair], recent_blockhash);
client.send_and_confirm_transaction(&transaction)?;
println!("Counter initialized successfully!");
// Read the counter state
let account_data = client.get_account_data(&counter_keypair.pubkey())?;
let counter: CounterAccount = CounterAccount::try_from_slice(&account_data)?;
println!("Counter value: {}", counter.count);
println!("Owner: {}", counter.owner);
println!("Created at: {}", counter.created_at);
Ok(())
}
fn program_id() -> Pubkey {
// Replace with your actual program ID
"YourProgramIdHere".parse().unwrap()
}
Step 8: Testing
Create comprehensive tests for your program:
tests/integration_tests.rs
use arch_program::{
account::AccountInfo,
program_error::ProgramError,
pubkey::Pubkey,
};
use my_counter_program::{
process_instruction, CounterAccount, CounterInstruction, CounterError,
};
#[test]
fn test_initialize_counter() {
// Test initialization logic
let program_id = Pubkey::new_unique();
let owner = Pubkey::new_unique();
// Create mock accounts
let mut counter_data = vec![0u8; 1000];
let counter_account = AccountInfo::new(
&Pubkey::new_unique(),
false,
true,
&mut counter_data,
&program_id,
&Pubkey::new_unique(),
false,
0,
);
let owner_account = AccountInfo::new(
&owner,
true,
false,
&mut vec![],
&Pubkey::new_unique(),
&Pubkey::new_unique(),
false,
0,
);
let accounts = vec![counter_account, owner_account];
let instruction = CounterInstruction::Initialize { initial_value: 100 };
let instruction_data = instruction.try_to_vec().unwrap();
// Process instruction
let result = process_instruction(&program_id, &accounts, &instruction_data);
assert!(result.is_ok());
// Verify counter was initialized
let counter: CounterAccount = CounterAccount::try_from_slice(&counter_data[..100]).unwrap();
assert_eq!(counter.count, 100);
assert_eq!(counter.owner, owner);
assert!(counter.is_active);
}
#[test]
fn test_increment_counter() {
// Test increment logic
// Similar structure to above test
}
#[test]
fn test_unauthorized_access() {
// Test that only the owner can modify the counter
}
#[test]
fn test_overflow_protection() {
// Test overflow protection
}
#[test]
fn test_inactive_counter() {
// Test that inactive counters can't be modified
}
Step 9: Advanced Features
9.1 Bitcoin Integration
Add Bitcoin transaction support:
use arch_program::{
bitcoin::{Transaction, TxIn, TxOut},
helper::add_state_transition,
input_to_sign::InputToSign,
transaction_to_sign::TransactionToSign,
};
/// Process a Bitcoin transaction
fn process_bitcoin_transaction(
counter_account: &AccountInfo,
bitcoin_tx: &Transaction,
) -> Result<(), ProgramError> {
msg!("Processing Bitcoin transaction");
// Verify Bitcoin transaction
verify_bitcoin_transaction(bitcoin_tx)?;
// Create state transition
let state_transition = create_state_transition(counter_account, bitcoin_tx)?;
// Add to transaction
add_state_transition(state_transition)?;
Ok(())
}
fn verify_bitcoin_transaction(tx: &Transaction) -> Result<(), ProgramError> {
// Implement Bitcoin transaction verification
// Check signatures, inputs, outputs, etc.
Ok(())
}
fn create_state_transition(
counter_account: &AccountInfo,
bitcoin_tx: &Transaction,
) -> Result<TransactionToSign, ProgramError> {
// Create a Bitcoin transaction that reflects the state change
let mut tx = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![],
output: vec![],
};
// Add inputs and outputs based on the counter state
// This is a simplified example
Ok(TransactionToSign::new(tx))
}
9.2 Error Handling and Logging
Enhance error handling:
use arch_program::msg;
/// Enhanced error handling with detailed logging
fn process_instruction_with_logging(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), ProgramError> {
msg!("=== Counter Program Execution ===");
msg!("Program ID: {}", program_id);
msg!("Accounts: {}", accounts.len());
msg!("Instruction data length: {}", instruction_data.len());
match process_instruction(program_id, accounts, instruction_data) {
Ok(()) => {
msg!("Instruction processed successfully");
Ok(())
}
Err(e) => {
msg!("Instruction failed with error: {:?}", e);
Err(e)
}
}
}
Step 10: Security Best Practices
10.1 Input Validation
/// Validate instruction inputs
fn validate_instruction(instruction: &CounterInstruction) -> Result<(), ProgramError> {
match instruction {
CounterInstruction::Initialize { initial_value } => {
if *initial_value < 0 {
return Err(CounterError::InvalidInstruction.into());
}
}
CounterInstruction::Increment { amount } => {
if *amount <= 0 {
return Err(CounterError::InvalidInstruction.into());
}
}
CounterInstruction::Decrement { amount } => {
if *amount <= 0 {
return Err(CounterError::InvalidInstruction.into());
}
}
_ => {} // Other instructions don't need validation
}
Ok(())
}
10.2 Access Control
/// Enhanced access control
fn verify_access_control(
counter: &CounterAccount,
owner_account: &AccountInfo,
instruction: &CounterInstruction,
) -> Result<(), ProgramError> {
// Check ownership
if counter.owner != *owner_account.key {
return Err(CounterError::Unauthorized.into());
}
// Check if counter is active for state-changing operations
match instruction {
CounterInstruction::Increment { .. } |
CounterInstruction::Decrement { .. } |
CounterInstruction::Reset |
CounterInstruction::Deactivate => {
if !counter.is_active {
return Err(CounterError::CounterInactive.into());
}
}
_ => {} // Read-only operations don't need active check
}
Ok(())
}
Next Steps
Congratulations! You've successfully created your first Arch program. Here's what you can do next:
Explore More Examples
Learn how to create fungible tokens
Build an Oracle Program
Create a price oracle for external data
Advanced Testing
Learn comprehensive testing strategies
Program Architecture
Deep dive into Arch program concepts
Key Takeaways
- State Management: Arch programs store state in accounts using Borsh serialization
- Instruction Processing: Programs handle different instruction types through enums
- Security: Always verify ownership and validate inputs
- Bitcoin Integration: Programs can interact with Bitcoin transactions
- Testing: Comprehensive testing is essential for production programs
- Error Handling: Proper error handling improves user experience and security
Resources
- Arch Program Library - Pre-built programs and utilities
- SDK Documentation - Client libraries and tools
- API Reference - Complete API documentation
- Community Discord - Get help and share your projects