rialo-cdk 0.2.0-alpha.0

Rialo CDK - A comprehensive toolkit for building with the Rialo blockchain
Documentation
// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Simple configuration API for basic use cases
//!
//! This module provides the original simple configuration types that were replaced
//! by the more complex layered configuration system. These types are particularly
//! useful for:
//! - WASM environments where simplicity is preferred
//! - Applications that need straightforward config management
//! - Compatibility with existing code that used the original API

use std::collections::HashMap;

use async_trait::async_trait;

use crate::constants::{
    NETWORK_DEVNET, NETWORK_LOCALNET, NETWORK_MAINNET, NETWORK_TESTNET, URL_DEVNET, URL_LOCALNET,
    URL_MAINNET, URL_TESTNET,
};

/// Simple configuration struct with basic settings
#[derive(Debug, Clone)]
pub struct Config {
    /// Current RPC endpoint URL
    pub rpc_url: String,
    /// Default wallet name (if set)
    pub default_wallet: Option<String>,
    /// Current network name
    pub network: String,
    /// Default unit for displaying balances
    pub default_balance_unit: String,
    /// Whether to show verbose output
    pub verbose_mode: bool,
    /// Custom RPC URLs for specific networks
    pub custom_rpc_urls: HashMap<String, String>,
    /// Default account indices for specific wallets
    pub wallet_defaults: HashMap<String, usize>,
}

impl Default for Config {
    fn default() -> Self {
        Config {
            rpc_url: URL_LOCALNET.to_string(),
            default_wallet: None,
            network: NETWORK_LOCALNET.to_string(),
            default_balance_unit: "rlo".to_string(),
            verbose_mode: false,
            custom_rpc_urls: HashMap::new(),
            wallet_defaults: HashMap::new(),
        }
    }
}

impl Config {
    /// Set the network and update the RPC URL accordingly
    pub fn set_network(&mut self, network: &str) {
        self.network = network.to_string();

        // Update RPC URL based on network using constants
        match network {
            NETWORK_LOCALNET => self.rpc_url = URL_LOCALNET.to_string(),
            NETWORK_DEVNET => self.rpc_url = URL_DEVNET.to_string(),
            NETWORK_TESTNET => self.rpc_url = URL_TESTNET.to_string(),
            NETWORK_MAINNET => self.rpc_url = URL_MAINNET.to_string(),
            _ => {} // Keep current RPC URL for unknown networks
        }

        // Override with custom RPC URL if available
        if let Some(custom_url) = self.custom_rpc_urls.get(network) {
            self.rpc_url = custom_url.clone();
        }
    }

    /// Set a custom RPC URL for a specific network
    pub fn set_custom_rpc_url(&mut self, network: &str, url: &str) {
        self.custom_rpc_urls
            .insert(network.to_string(), url.to_string());
        // If this is the current network, update the RPC URL
        if self.network == network {
            self.rpc_url = url.to_string();
        }
    }

    /// Set the default account index for a wallet
    pub fn set_wallet_default_account(&mut self, wallet_name: &str, account_index: usize) {
        self.wallet_defaults
            .insert(wallet_name.to_string(), account_index);
    }

    /// Get the default account index for a wallet (returns 0 if not set)
    pub fn get_wallet_default_account(&self, wallet_name: &str) -> usize {
        self.wallet_defaults.get(wallet_name).copied().unwrap_or(0)
    }

    /// Get the RPC URL for the current network
    pub fn get_rpc_url(&self) -> &str {
        &self.rpc_url
    }

    /// Get the RPC URL for a specific network
    pub fn get_rpc_url_for_network(&self, network: &str) -> String {
        // Check for custom RPC URL first
        if let Some(custom_url) = self.custom_rpc_urls.get(network) {
            return custom_url.clone();
        }

        // Use default URLs with constants
        match network {
            NETWORK_LOCALNET => URL_LOCALNET.to_string(),
            NETWORK_DEVNET => URL_DEVNET.to_string(),
            NETWORK_TESTNET => URL_TESTNET.to_string(),
            NETWORK_MAINNET => URL_MAINNET.to_string(),
            _ => URL_LOCALNET.to_string(), // Default fallback
        }
    }
}

/// Simple configuration provider trait
#[async_trait]
pub trait ConfigProvider {
    /// Error type for configuration operations
    type Error;

    /// Load configuration
    async fn load(&self) -> Result<Config, Self::Error>;

    /// Save configuration
    async fn save(&self, config: &Config) -> Result<(), Self::Error>;
}

/// In-memory configuration provider
///
/// This provider stores configuration in memory and is useful for:
/// - WASM environments where file access is limited
/// - Testing scenarios
/// - Applications that don't need persistence
#[derive(Debug)]
pub struct InMemoryConfigProvider {
    config: std::sync::Arc<std::sync::RwLock<Config>>,
}

impl InMemoryConfigProvider {
    /// Create a new in-memory provider with the given config
    pub fn new(config: Config) -> Self {
        InMemoryConfigProvider {
            config: std::sync::Arc::new(std::sync::RwLock::new(config)),
        }
    }
}

impl Default for InMemoryConfigProvider {
    fn default() -> Self {
        Self::new(Config::default())
    }
}

#[async_trait]
impl ConfigProvider for InMemoryConfigProvider {
    type Error = String;

    async fn load(&self) -> Result<Config, Self::Error> {
        Ok(self
            .config
            .read()
            .map_err(|e| format!("Failed to read config: {}", e))?
            .clone())
    }

    async fn save(&self, config: &Config) -> Result<(), Self::Error> {
        let mut guard = self
            .config
            .write()
            .map_err(|e| format!("Failed to write config: {}", e))?;
        *guard = config.clone();
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_config_default() {
        let config = Config::default();
        assert_eq!(config.network, NETWORK_LOCALNET);
        assert_eq!(config.rpc_url, URL_LOCALNET);
        assert_eq!(config.default_balance_unit, "rlo");
        assert!(!config.verbose_mode);
    }

    #[test]
    fn test_set_network() {
        let mut config = Config::default();

        config.set_network(NETWORK_DEVNET);
        assert_eq!(config.network, NETWORK_DEVNET);
        assert_eq!(config.rpc_url, URL_DEVNET);

        config.set_network(NETWORK_MAINNET);
        assert_eq!(config.network, NETWORK_MAINNET);
        assert_eq!(config.rpc_url, URL_MAINNET);
    }

    #[test]
    fn test_custom_rpc_url() {
        let mut config = Config::default();
        config.set_custom_rpc_url(NETWORK_LOCALNET, "http://custom:8080");

        // Should not change RPC URL yet since we're still on localnet
        config.set_network(NETWORK_LOCALNET);
        assert_eq!(config.rpc_url, "http://custom:8080");
    }

    #[test]
    fn test_wallet_defaults() {
        let mut config = Config::default();
        config.set_wallet_default_account("test_wallet", 5);

        assert_eq!(config.get_wallet_default_account("test_wallet"), 5);
        assert_eq!(config.get_wallet_default_account("nonexistent"), 0);
    }

    #[tokio::test]
    async fn test_in_memory_provider() {
        let config = Config {
            verbose_mode: true,
            ..Default::default()
        };

        let provider = InMemoryConfigProvider::new(config.clone());

        // Test load
        let loaded = provider.load().await.unwrap();
        assert!(loaded.verbose_mode);

        // Test save
        let mut new_config = loaded;
        new_config.network = NETWORK_DEVNET.to_string();
        provider.save(&new_config).await.unwrap();

        // Verify save worked
        let reloaded = provider.load().await.unwrap();
        assert_eq!(reloaded.network, NETWORK_DEVNET);
    }
}