learn.sol

Rust Mid-Course Projects: Portfolio Tracker and Blockchain Simulator

Apply Rust concepts from the first half with complete projects: build a crypto portfolio tracker or a simple blockchain simulator to reinforce ownership and structs.

🎮 Mid-Course Challenge Break

Time to Build Something Real!

You've mastered Rust fundamentals, ownership, and structs. Now let's combine everything into practical projects that demonstrate real-world patterns used in Solana development.

🎯 Project Selection

Choose one primary project to focus on, or tackle both if you're feeling ambitious:

Build a comprehensive portfolio management system

  • Beginner Friendly - Clear structure
  • Time: 1-2 hours
  • Focus: Structs, methods, ownership

Build a CLI application that manages cryptocurrency portfolios with real-world patterns.

Create a basic blockchain with blocks and validation

  • More Challenging - Complex interactions
  • Time: 1-2 hours
  • Focus: References, validation, chains

Create a basic blockchain that demonstrates fundamental concepts used in Solana.


📈 Project 1: Crypto Portfolio Tracker

Build a CLI application that manages cryptocurrency portfolios with real-world patterns.

🏗️ Architecture Overview

📋 Implementation Steps

Step 1: Define the Asset Struct

// src/asset.rs
#[derive(Debug, Clone)]
pub struct Asset {
    pub symbol: String,
    pub quantity: f64,
    pub purchase_price: f64,
    pub current_price: f64,
    pub purchase_date: String,
}

impl Asset {
    pub fn new(symbol: String, quantity: f64, purchase_price: f64) -> Self {
        Self {
            symbol,
            quantity,
            purchase_price,
            current_price: purchase_price, // Initially same as purchase price
            purchase_date: "2024-01-01".to_string(), // Simplified for now
        }
    }
    
    pub fn market_value(&self) -> f64 {
        self.quantity * self.current_price
    }
    
    pub fn profit_loss(&self) -> f64 {
        (self.current_price - self.purchase_price) * self.quantity
    }
    
    pub fn profit_loss_percentage(&self) -> f64 {
        ((self.current_price - self.purchase_price) / self.purchase_price) * 100.0
    }
    
    pub fn update_price(&mut self, new_price: f64) {
        self.current_price = new_price;
    }
}

Step 2: Create the Portfolio Manager

// src/portfolio.rs
use crate::asset::Asset;

#[derive(Debug)]
pub struct Portfolio {
    assets: Vec<Asset>,
    name: String,
}

impl Portfolio {
    pub fn new(name: String) -> Self {
        Self {
            assets: Vec::new(),
            name,
        }
    }
    
    pub fn add_asset(&mut self, asset: Asset) {
        // Check if asset already exists
        if let Some(existing) = self.assets.iter_mut()
            .find(|a| a.symbol == asset.symbol) {
            // Update existing asset (average cost basis)
            let total_value = existing.market_value() + asset.market_value();
            existing.quantity += asset.quantity;
            existing.purchase_price = total_value / existing.quantity;
        } else {
            self.assets.push(asset);
        }
    }
    
    pub fn remove_asset(&mut self, symbol: &str) -> Option<Asset> {
        if let Some(pos) = self.assets.iter().position(|a| a.symbol == symbol) {
            Some(self.assets.remove(pos))
        } else {
            None
        }
    }
    
    pub fn total_value(&self) -> f64 {
        self.assets.iter().map(|asset| asset.market_value()).sum()
    }
    
    pub fn total_profit_loss(&self) -> f64 {
        self.assets.iter().map(|asset| asset.profit_loss()).sum()
    }
    
    pub fn get_asset(&self, symbol: &str) -> Option<&Asset> {
        self.assets.iter().find(|asset| asset.symbol == symbol)
    }
    
    pub fn get_asset_mut(&mut self, symbol: &str) -> Option<&mut Asset> {
        self.assets.iter_mut().find(|asset| asset.symbol == symbol)
    }
    
    pub fn list_assets(&self) -> &Vec<Asset> {
        &self.assets
    }
    
    pub fn update_all_prices(&mut self, price_updates: &[(String, f64)]) {
        for (symbol, new_price) in price_updates {
            if let Some(asset) = self.get_asset_mut(symbol) {
                asset.update_price(*new_price);
            }
        }
    }
}

Step 3: Build the CLI Interface

// src/main.rs
mod asset;
mod portfolio;

use asset::Asset;
use portfolio::Portfolio;
use std::io::{self, Write};

fn main() {
    let mut portfolio = Portfolio::new("My Crypto Portfolio".to_string());
    
    loop {
        println!("\n=== Crypto Portfolio Tracker ===");
        println!("1. Add Asset");
        println!("2. Remove Asset");
        println!("3. Update Prices");
        println!("4. View Portfolio");
        println!("5. View Asset Details");
        println!("6. Exit");
        print!("Choose an option: ");
        io::stdout().flush().unwrap();
        
        let mut input = String::new();
        io::stdin().read_line(&mut input).unwrap();
        
        match input.trim() {
            "1" => add_asset(&mut portfolio),
            "2" => remove_asset(&mut portfolio),
            "3" => update_prices(&mut portfolio),
            "4" => view_portfolio(&portfolio),
            "5" => view_asset_details(&portfolio),
            "6" => {
                println!("Thanks for using Portfolio Tracker!");
                break;
            }
            _ => println!("Invalid option, please try again."),
        }
    }
}

fn add_asset(portfolio: &mut Portfolio) {
    println!("\n--- Add New Asset ---");
    
    print!("Symbol (e.g., BTC, ETH): ");
    io::stdout().flush().unwrap();
    let mut symbol = String::new();
    io::stdin().read_line(&mut symbol).unwrap();
    let symbol = symbol.trim().to_uppercase();
    
    print!("Quantity: ");
    io::stdout().flush().unwrap();
    let mut quantity_str = String::new();
    io::stdin().read_line(&mut quantity_str).unwrap();
    let quantity: f64 = match quantity_str.trim().parse() {
        Ok(q) => q,
        Err(_) => {
            println!("Invalid quantity!");
            return;
        }
    };
    
    print!("Purchase price: $");
    io::stdout().flush().unwrap();
    let mut price_str = String::new();
    io::stdin().read_line(&mut price_str).unwrap();
    let price: f64 = match price_str.trim().parse() {
        Ok(p) => p,
        Err(_) => {
            println!("Invalid price!");
            return;
        }
    };
    
    let asset = Asset::new(symbol, quantity, price);
    portfolio.add_asset(asset);
    println!("Asset added successfully!");
}

fn view_portfolio(portfolio: &Portfolio) {
    println!("\n--- Portfolio Overview ---");
    
    let assets = portfolio.list_assets();
    if assets.is_empty() {
        println!("No assets in portfolio.");
        return;
    }
    
    println!("{:<8} {:<12} {:<12} {:<12} {:<12}", 
             "Symbol", "Quantity", "Avg Cost", "Current", "Value");
    println!("{}", "-".repeat(60));
    
    for asset in assets {
        println!("{:<8} {:<12.4} ${:<11.2} ${:<11.2} ${:<11.2}",
                 asset.symbol,
                 asset.quantity,
                 asset.purchase_price,
                 asset.current_price,
                 asset.market_value());
    }
    
    println!("{}", "-".repeat(60));
    println!("Total Portfolio Value: ${:.2}", portfolio.total_value());
    
    let total_pl = portfolio.total_profit_loss();
    let pl_indicator = if total_pl >= 0.0 { "+" } else { "" };
    println!("Total Profit/Loss: {}{:.2}", pl_indicator, total_pl);
}

// Additional helper functions...
fn remove_asset(portfolio: &mut Portfolio) {
    print!("Enter symbol to remove: ");
    io::stdout().flush().unwrap();
    let mut symbol = String::new();
    io::stdin().read_line(&mut symbol).unwrap();
    let symbol = symbol.trim().to_uppercase();
    
    match portfolio.remove_asset(&symbol) {
        Some(asset) => println!("Removed {} from portfolio", asset.symbol),
        None => println!("Asset {} not found", symbol),
    }
}

fn update_prices(portfolio: &mut Portfolio) {
    // Simulate price updates
    let price_updates = vec![
        ("BTC".to_string(), 45000.0),
        ("ETH".to_string(), 3200.0),
        ("SOL".to_string(), 120.0),
    ];
    
    portfolio.update_all_prices(&price_updates);
    println!("Prices updated!");
}

fn view_asset_details(portfolio: &Portfolio) {
    print!("Enter symbol: ");
    io::stdout().flush().unwrap();
    let mut symbol = String::new();
    io::stdin().read_line(&mut symbol).unwrap();
    let symbol = symbol.trim().to_uppercase();
    
    match portfolio.get_asset(&symbol) {
        Some(asset) => {
            println!("\n--- {} Details ---", asset.symbol);
            println!("Quantity: {:.4}", asset.quantity);
            println!("Average Cost: ${:.2}", asset.purchase_price);
            println!("Current Price: ${:.2}", asset.current_price);
            println!("Market Value: ${:.2}", asset.market_value());
            println!("Profit/Loss: ${:.2} ({:.1}%)", 
                     asset.profit_loss(), 
                     asset.profit_loss_percentage());
        }
        None => println!("Asset {} not found", symbol),
    }
}

🎯 Key Learning Objectives

  • Ownership: Passing references vs moving values
  • Borrowing: Mutable and immutable references
  • Structs: Complex data organization
  • Methods: Associated functions and self methods
  • Control Flow: Menu-driven applications
  • Error Handling: Basic input validation

⛓️ Project 2: Simple Blockchain Simulator

Create a basic blockchain that demonstrates fundamental concepts used in Solana.

🏗️ Architecture Overview

📋 Implementation Steps

Step 1: Create the Block Structure

// src/block.rs
use std::time::{SystemTime, UNIX_EPOCH};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

#[derive(Debug, Clone)]
pub struct Block {
    pub index: u64,
    pub timestamp: u64,
    pub data: String,
    pub previous_hash: String,
    pub hash: String,
    pub nonce: u64, // For simple proof of work
}

impl Block {
    pub fn new(index: u64, data: String, previous_hash: String) -> Self {
        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();
        
        let mut block = Self {
            index,
            timestamp,
            data,
            previous_hash,
            hash: String::new(),
            nonce: 0,
        };
        
        block.hash = block.calculate_hash();
        block
    }
    
    pub fn calculate_hash(&self) -> String {
        let mut hasher = DefaultHasher::new();
        self.index.hash(&mut hasher);
        self.timestamp.hash(&mut hasher);
        self.data.hash(&mut hasher);
        self.previous_hash.hash(&mut hasher);
        self.nonce.hash(&mut hasher);
        format!("{:x}", hasher.finish())
    }
    
    pub fn mine_block(&mut self, difficulty: usize) {
        let target = "0".repeat(difficulty);
        
        while !self.hash.starts_with(&target) {
            self.nonce += 1;
            self.hash = self.calculate_hash();
        }
        
        println!("Block mined: {}", self.hash);
    }
    
    pub fn is_valid(&self, previous_block: Option<&Block>) -> bool {
        // Check if hash is correct
        if self.hash != self.calculate_hash() {
            return false;
        }
        
        // Check if previous hash matches
        if let Some(prev) = previous_block {
            if self.previous_hash != prev.hash {
                return false;
            }
            
            // Check if index is sequential
            if self.index != prev.index + 1 {
                return false;
            }
        }
        
        true
    }
}

Step 2: Build the Blockchain

// src/blockchain.rs
use crate::block::Block;

#[derive(Debug)]
pub struct Blockchain {
    pub blocks: Vec<Block>,
    pub difficulty: usize,
}

impl Blockchain {
    pub fn new() -> Self {
        let mut blockchain = Self {
            blocks: Vec::new(),
            difficulty: 2, // Number of leading zeros required
        };
        
        blockchain.create_genesis_block();
        blockchain
    }
    
    fn create_genesis_block(&mut self) {
        let mut genesis = Block::new(
            0,
            "Genesis Block".to_string(),
            "0".to_string(),
        );
        
        genesis.mine_block(self.difficulty);
        self.blocks.push(genesis);
    }
    
    pub fn get_latest_block(&self) -> &Block {
        self.blocks.last().unwrap()
    }
    
    pub fn add_block(&mut self, data: String) {
        let previous_block = self.get_latest_block();
        let mut new_block = Block::new(
            previous_block.index + 1,
            data,
            previous_block.hash.clone(),
        );
        
        new_block.mine_block(self.difficulty);
        self.blocks.push(new_block);
    }
    
    pub fn is_chain_valid(&self) -> bool {
        for i in 1..self.blocks.len() {
            let current_block = &self.blocks[i];
            let previous_block = &self.blocks[i - 1];
            
            if !current_block.is_valid(Some(previous_block)) {
                return false;
            }
        }
        
        // Check genesis block separately
        if !self.blocks[0].is_valid(None) {
            return false;
        }
        
        true
    }
    
    pub fn get_block(&self, index: u64) -> Option<&Block> {
        self.blocks.iter().find(|block| block.index == index)
    }
    
    pub fn get_chain_info(&self) -> ChainInfo {
        ChainInfo {
            length: self.blocks.len(),
            latest_hash: self.get_latest_block().hash.clone(),
            is_valid: self.is_chain_valid(),
            difficulty: self.difficulty,
        }
    }
}

#[derive(Debug)]
pub struct ChainInfo {
    pub length: usize,
    pub latest_hash: String,
    pub is_valid: bool,
    pub difficulty: usize,
}

Step 3: CLI Interface

// src/main.rs
mod block;
mod blockchain;

use blockchain::Blockchain;
use std::io::{self, Write};

fn main() {
    let mut blockchain = Blockchain::new();
    println!("🔗 Simple Blockchain Simulator");
    println!("Genesis block created!");
    
    loop {
        println!("\n=== Blockchain Menu ===");
        println!("1. Add Block");
        println!("2. View Blockchain");
        println!("3. Validate Chain");
        println!("4. View Block Details");
        println!("5. Chain Statistics");
        println!("6. Exit");
        print!("Choose an option: ");
        io::stdout().flush().unwrap();
        
        let mut input = String::new();
        io::stdin().read_line(&mut input).unwrap();
        
        match input.trim() {
            "1" => add_block(&mut blockchain),
            "2" => view_blockchain(&blockchain),
            "3" => validate_chain(&blockchain),
            "4" => view_block_details(&blockchain),
            "5" => show_statistics(&blockchain),
            "6" => {
                println!("Thanks for using Blockchain Simulator!");
                break;
            }
            _ => println!("Invalid option, please try again."),
        }
    }
}

fn add_block(blockchain: &mut Blockchain) {
    print!("Enter block data: ");
    io::stdout().flush().unwrap();
    let mut data = String::new();
    io::stdin().read_line(&mut data).unwrap();
    let data = data.trim().to_string();
    
    if data.is_empty() {
        println!("Block data cannot be empty!");
        return;
    }
    
    println!("Mining block...");
    blockchain.add_block(data);
    println!("Block added successfully!");
}

fn view_blockchain(blockchain: &Blockchain) {
    println!("\n--- Blockchain Overview ---");
    
    for block in &blockchain.blocks {
        println!("Block #{}", block.index);
        println!("  Timestamp: {}", block.timestamp);
        println!("  Data: {}", block.data);
        println!("  Hash: {}", block.hash);
        println!("  Previous Hash: {}", block.previous_hash);
        println!("  Nonce: {}", block.nonce);
        println!();
    }
}

fn validate_chain(blockchain: &Blockchain) {
    println!("\n--- Chain Validation ---");
    if blockchain.is_chain_valid() {
        println!("✅ Blockchain is valid!");
    } else {
        println!("❌ Blockchain is invalid!");
    }
}

fn view_block_details(blockchain: &Blockchain) {
    print!("Enter block index: ");
    io::stdout().flush().unwrap();
    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();
    
    let index: u64 = match input.trim().parse() {
        Ok(i) => i,
        Err(_) => {
            println!("Invalid index!");
            return;
        }
    };
    
    match blockchain.get_block(index) {
        Some(block) => {
            println!("\n--- Block #{} Details ---", block.index);
            println!("Timestamp: {}", block.timestamp);
            println!("Data: {}", block.data);
            println!("Hash: {}", block.hash);
            println!("Previous Hash: {}", block.previous_hash);
            println!("Nonce: {}", block.nonce);
            
            // Validate this specific block
            let previous_block = if index > 0 {
                blockchain.get_block(index - 1)
            } else {
                None
            };
            
            if block.is_valid(previous_block) {
                println!("Status: ✅ Valid");
            } else {
                println!("Status: ❌ Invalid");
            }
        }
        None => println!("Block #{} not found!", index),
    }
}

fn show_statistics(blockchain: &Blockchain) {
    let info = blockchain.get_chain_info();
    
    println!("\n--- Blockchain Statistics ---");
    println!("Chain Length: {} blocks", info.length);
    println!("Latest Hash: {}", info.latest_hash);
    println!("Mining Difficulty: {} leading zeros", info.difficulty);
    println!("Chain Valid: {}", if info.is_valid { "✅ Yes" } else { "❌ No" });
    
    // Calculate total nonce work
    let total_nonce: u64 = blockchain.blocks.iter().map(|b| b.nonce).sum();
    println!("Total Mining Work: {} nonce calculations", total_nonce);
}

🎯 Key Learning Objectives

  • References: Borrowing blocks for validation
  • Ownership: Moving vs borrowing in collections
  • Method Design: Associated functions vs methods
  • Data Validation: Chain integrity checks
  • Memory Management: Efficient vector operations

🚀 Extension Challenges

If you complete your chosen project early, try these enhancements:

For Portfolio Tracker:

  • Add transaction history tracking
  • Implement portfolio rebalancing suggestions
  • Add support for different asset types (stocks, crypto, commodities)
  • Create portfolio comparison features

For Blockchain:

  • Implement transaction pools
  • Add digital signature verification
  • Create a network simulation with multiple nodes
  • Add smart contract execution simulation

📚 What's Next?

After completing your project, you should feel confident with:

  • Struct design and implementation
  • Method syntax and associated functions
  • Ownership and borrowing patterns
  • Basic error handling and validation
  • CLI application structure

These foundations prepare you perfectly for the advanced concepts coming in Days 4-7: enums, pattern matching, collections, and error handling!

Pro Tip: Document Your Journey

Keep notes about what you learned and what challenged you. These patterns will appear constantly in Solana development, so building muscle memory now pays huge dividends later!

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px
    Rust Mid-Course Projects: Portfolio Tracker and Blockchain Simulator | learn.sol