learn.sol
Rust Foundations • Enums And Pattern Matching Practice

Enums and Pattern Matching Practice

Practice enums and pattern matching through a small wallet manager exercise that models account state, transaction state, and fallible actions explicitly.

This practice page has one job.

It should prove that you can model state with enums instead of hiding it in booleans and optional fields.

If the previous lesson made sense, this page is where that understanding becomes real.

Do not try to make this bigger than it needs to be.

The goal is not to build a complete wallet product.

The goal is to make enums carry real meaning and then handle them cleanly with match.

What You Are Building

Build a tiny wallet manager with three ideas:

  • an account can be in one of several states
  • a transaction can be in one of several states
  • wallet operations can succeed or fail for explicit reasons

That is enough.

You do not need NFTs, staking, network simulation, or recovery flows.

Those would only distract from the real practice target.

What This Practice Should Prove

By the end, you should be able to explain:

  • why AccountStatus is better as an enum than as a struct of flags
  • why TransactionStatus should carry different data in different variants
  • when to use match instead of if let
  • how Result<T, E> makes wallet operations honest about failure

Step 1: Define The Core Enums

Start here.

Do not write methods yet.

Just define the state model.

#[derive(Debug, Clone, PartialEq)]
enum AccountType {
    System,
    Token { mint: String },
}

#[derive(Debug, Clone, PartialEq)]
enum AccountStatus {
    Active,
    Frozen { reason: String },
    Closed,
}

#[derive(Debug, Clone, PartialEq)]
enum TransactionStatus {
    Pending,
    Confirmed { signature: String },
    Failed { error: String },
}

#[derive(Debug)]
enum WalletError {
    AccountNotFound(String),
    AccountFrozen(String),
    AccountClosed(String),
    InsufficientBalance { requested: u64, available: u64 },
}

Why this is the right starting point

Each enum represents a different kind of decision:

  • AccountType answers what kind of account this is
  • AccountStatus answers whether the account can still be used
  • TransactionStatus answers what happened to the transaction
  • WalletError answers why an operation failed

That separation matters.

Do not merge them into one vague status type.

Step 2: Add The Main Structs

Now define one account type and one transaction type.

#[derive(Debug, Clone)]
struct Account {
    address: String,
    balance: u64,
    account_type: AccountType,
    status: AccountStatus,
}

#[derive(Debug, Clone)]
struct Transaction {
    from: String,
    to: String,
    amount: u64,
    status: TransactionStatus,
}

#[derive(Debug)]
struct WalletManager {
    accounts: Vec<Account>,
    transactions: Vec<Transaction>,
}

Keep the shape small.

This is practice.

You are not trying to model the entire Solana runtime.

Step 3: Write The First Useful match

Start with a method that only reads state.

That keeps the first step simple.

impl Account {
    fn status_label(&self) -> &str {
        match &self.status {
            AccountStatus::Active => "active",
            AccountStatus::Frozen { .. } => "frozen",
            AccountStatus::Closed => "closed",
        }
    }
}

This method is small, but it proves the most important habit:

  • inspect the enum
  • handle every variant
  • keep each branch specific

Step 4: Make State Affect Behavior

Now write a method that decides whether an account can send funds.

impl Account {
    fn can_send(&self, amount: u64) -> Result<(), WalletError> {
        match &self.status {
            AccountStatus::Frozen { reason } => {
                return Err(WalletError::AccountFrozen(reason.clone()));
            }
            AccountStatus::Closed => {
                return Err(WalletError::AccountClosed(self.address.clone()));
            }
            AccountStatus::Active => {}
        }

        if amount > self.balance {
            return Err(WalletError::InsufficientBalance {
                requested: amount,
                available: self.balance,
            });
        }

        Ok(())
    }
}

What this step teaches

  • enums do not just label state
  • enums control legal behavior
  • Result makes failure reasons explicit

That is the whole point of this exercise.

Step 5: Model Transaction Outcomes Properly

Now write a helper on Transaction.

impl Transaction {
    fn summary(&self) -> String {
        match &self.status {
            TransactionStatus::Pending => {
                format!("{} -> {} | {} lamports | pending", self.from, self.to, self.amount)
            }
            TransactionStatus::Confirmed { signature } => {
                format!(
                    "{} -> {} | {} lamports | confirmed: {}",
                    self.from,
                    self.to,
                    self.amount,
                    signature
                )
            }
            TransactionStatus::Failed { error } => {
                format!(
                    "{} -> {} | {} lamports | failed: {}",
                    self.from,
                    self.to,
                    self.amount,
                    error
                )
            }
        }
    }
}

This is where the earlier lesson should click.

Each transaction state carries different data because each state means something different.

If all states shared the same fields, the enum would not be needed.

Step 6: Build One Real Wallet Operation

Now implement one transfer method on WalletManager.

This is the main practice target.

impl WalletManager {
    fn transfer(&mut self, from: &str, to: &str, amount: u64) -> Result<(), WalletError> {
        let sender_index = self
            .accounts
            .iter()
            .position(|account| account.address == from)
            .ok_or_else(|| WalletError::AccountNotFound(from.to_string()))?;

        let receiver_index = self
            .accounts
            .iter()
            .position(|account| account.address == to)
            .ok_or_else(|| WalletError::AccountNotFound(to.to_string()))?;

        self.accounts[sender_index].can_send(amount)?;

        self.accounts[sender_index].balance -= amount;
        self.accounts[receiver_index].balance += amount;

        self.transactions.push(Transaction {
            from: from.to_string(),
            to: to.to_string(),
            amount,
            status: TransactionStatus::Confirmed {
                signature: String::from("demo-signature-123"),
            },
        });

        Ok(())
    }
}

Why this is enough

You do not need minting, burning, staking, and freezing logic all at once.

One good transfer method already tests:

  • account lookup
  • status validation
  • error propagation
  • transaction state modeling

That is enough for this stage.

Step 7: Add One Failure Path On Purpose

A practice page is weak if everything only succeeds.

Create one frozen account and prove the transfer fails for the correct reason.

fn main() {
    let mut manager = WalletManager {
        accounts: vec![
            Account {
                address: String::from("alice"),
                balance: 1_000,
                account_type: AccountType::System,
                status: AccountStatus::Frozen {
                    reason: String::from("manual review"),
                },
            },
            Account {
                address: String::from("bob"),
                balance: 500,
                account_type: AccountType::System,
                status: AccountStatus::Active,
            },
        ],
        transactions: Vec::new(),
    };

    let result = manager.transfer("alice", "bob", 200);

    match result {
        Ok(()) => println!("unexpected success"),
        Err(error) => println!("expected failure: {:?}", error),
    }
}

This matters more than adding more features.

It proves your enum state is actually controlling program behavior.

Suggested Build Order

If you want to work cleanly, build in this exact order:

  1. write the enums
  2. write the structs
  3. implement status_label
  4. implement can_send
  5. implement summary
  6. implement transfer
  7. test one success case
  8. test one frozen-account failure
  9. test one insufficient-balance failure

Do not skip straight to the full manager and then debug everything at once.

Common Mistakes

Mistake 1: using booleans instead of enums

This is weak:

struct Account {
    is_active: bool,
    is_frozen: bool,
    is_closed: bool,
}

It allows nonsense combinations.

Use one enum instead.

Mistake 2: storing error strings everywhere

If a wallet operation can fail for known reasons, model those reasons with WalletError.

Do not just return random strings from every branch.

Mistake 3: using if let for every case

If you need to handle all states, use match.

That is what gives you complete coverage.

Mistake 4: making the practice page too ambitious

If your file turns into a fake wallet protocol with ten features, you are no longer practicing enums.

You are hiding the lesson inside too much surface area.

What To Hand Yourself At The End

When you finish, you should have a program where:

  • one account is active
  • one account is frozen or closed
  • one transfer succeeds
  • one transfer fails
  • at least one transaction summary prints correctly

If you can explain why each failure happened by pointing to enum variants, the practice worked.

Optional Extension

Only do this if the main version is already clean.

Add one token account variant:

AccountType::Token { mint: String }

Then update one summary method so token accounts print differently from system accounts.

That is a good extension because it reinforces the same lesson instead of introducing a new one.

What Comes Next

The next lesson moves to collections and strings.

That is the right next step because once your state model is clear, you need better tools for storing, grouping, and transforming more data.

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px
    Enums and Pattern Matching Practice | learn.sol