//! ResourceProvider service for managing nodes, slice allocation, and earnings //! Follows the established builder pattern for consistent API design use crate::models::user::{FarmNode, NodeCapacity, NodeStatus, EarningsRecord, NodeGroup, GroupStatistics, MarketplaceSLA}; use crate::models::product::Product; use crate::models::builders::FarmNodeBuilder; use crate::services::user_persistence::{UserPersistence, UserPersistentData}; use crate::services::grid::GridService; use crate::services::slice_calculator::{SliceCalculatorService, SlicePricing}; use rust_decimal::Decimal; use std::str::FromStr; use chrono::{Utc, DateTime}; use serde::{Serialize, Deserialize}; /// Staking statistics for a resource_provider #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StakingStatistics { pub total_staked_amount: Decimal, pub staked_nodes_count: u32, pub next_unlock_date: Option>, } impl Default for StakingStatistics { fn default() -> Self { Self { total_staked_amount: Decimal::ZERO, staked_nodes_count: 0, next_unlock_date: None, } } } /// Service for resource_provider-specific operations #[derive(Clone)] pub struct ResourceProviderService { auto_sync_enabled: bool, metrics_collection: bool, grid_service: GridService, slice_calculator: SliceCalculatorService, } /// Builder for ResourceProviderService #[derive(Default)] pub struct ResourceProviderServiceBuilder { auto_sync_enabled: Option, metrics_collection: Option, grid_service: Option, slice_calculator: Option, } impl ResourceProviderServiceBuilder { pub fn new() -> Self { Self::default() } pub fn auto_sync_enabled(mut self, enabled: bool) -> Self { self.auto_sync_enabled = Some(enabled); self } pub fn metrics_collection(mut self, enabled: bool) -> Self { self.metrics_collection = Some(enabled); self } pub fn grid_service(mut self, grid_service: GridService) -> Self { self.grid_service = Some(grid_service); self } pub fn slice_calculator(mut self, slice_calculator: SliceCalculatorService) -> Self { self.slice_calculator = Some(slice_calculator); self } pub fn build(self) -> Result { let grid_service = self.grid_service.unwrap_or_else(|| { GridService::builder().build().expect("Failed to create default GridService") }); let slice_calculator = self.slice_calculator.unwrap_or_else(|| { SliceCalculatorService::builder().build().expect("Failed to create default SliceCalculatorService") }); Ok(ResourceProviderService { auto_sync_enabled: self.auto_sync_enabled.unwrap_or(true), metrics_collection: self.metrics_collection.unwrap_or(true), grid_service, slice_calculator, }) } } impl ResourceProviderService { pub fn builder() -> ResourceProviderServiceBuilder { ResourceProviderServiceBuilder::new() } /// Get all nodes for a resource_provider pub fn get_resource_provider_nodes(&self, user_email: &str) -> Vec { if let Some(data) = UserPersistence::load_user_data(user_email) { // Debug: Log marketplace SLA data for all nodes for node in &data.nodes { if let Some(ref sla) = node.marketplace_sla { } else { } } data.nodes } else { Vec::new() } } /// Add a new node for a resource_provider (manual creation) pub fn add_node(&self, user_email: &str, node_data: NodeCreationData) -> Result { // Check for duplicate node names first let existing_nodes = self.get_resource_provider_nodes(user_email); if existing_nodes.iter().any(|n| n.name == node_data.name) { return Err(format!("Node '{}' is already registered", node_data.name)); } // Generate unique node ID and check for duplicates let mut node_id = format!("node_{}", &uuid::Uuid::new_v4().to_string()[..8]); while existing_nodes.iter().any(|n| n.id == node_id) { node_id = format!("node_{}", &uuid::Uuid::new_v4().to_string()[..8]); } // Validate slice formats if provided if let Some(ref slice_formats) = node_data.slice_formats { self.validate_slice_formats(slice_formats)?; } let capacity = NodeCapacity { cpu_cores: node_data.cpu_cores, memory_gb: node_data.memory_gb, storage_gb: node_data.storage_gb, bandwidth_mbps: node_data.bandwidth_mbps, ssd_storage_gb: node_data.storage_gb, // Default to all SSD for manual nodes hdd_storage_gb: 0, ram_gb: node_data.memory_gb, }; // Calculate slice data using the slice calculator let total_base_slices = self.slice_calculator.calculate_max_base_slices(&capacity); let slice_pricing = node_data.slice_pricing.unwrap_or_default(); let node_id_for_sla = node_id.clone(); let mut node = FarmNode { id: node_id, name: node_data.name.clone(), location: node_data.location.clone(), status: NodeStatus::Offline, // Starts offline until activated capacity, used_capacity: NodeCapacity { cpu_cores: 0, memory_gb: 0, storage_gb: 0, bandwidth_mbps: 0, ssd_storage_gb: 0, hdd_storage_gb: 0, ram_gb: 0, }, uptime_percentage: 0.0, farming_start_date: Utc::now(), last_updated: Utc::now(), utilization_7_day_avg: 0.0, slice_formats_supported: node_data.slice_formats.clone().unwrap_or_default(), earnings_today_usd: Decimal::ZERO, last_seen: Some(Utc::now()), health_score: 100.0, region: node_data.region.unwrap_or_else(|| "Unknown".to_string()), node_type: node_data.node_type.unwrap_or_else(|| "MyceliumNode".to_string()), slice_formats: node_data.slice_formats.clone(), rental_options: node_data.rental_options.as_ref().map(|opts| serde_json::to_value(opts).unwrap_or_default()), staking_options: None, availability_status: crate::models::user::NodeAvailabilityStatus::Available, grid_node_id: None, grid_data: None, node_group_id: None, group_assignment_date: None, group_slice_format: None, group_slice_price: None, // NEW: Marketplace SLA field with proper defaults for manual nodes marketplace_sla: Some(MarketplaceSLA { id: format!("sla-manual-{}", node_id_for_sla), name: "Manual Node SLA".to_string(), uptime_guarantee: 99.8, response_time_hours: 24, resolution_time_hours: 48, penalty_rate: 0.01, uptime_guarantee_percentage: 99.8, bandwidth_guarantee_mbps: node_data.bandwidth_mbps as f32, base_slice_price: slice_pricing.base_price_per_hour, last_updated: Utc::now(), }), // NEW: Automatic slice management fields total_base_slices: total_base_slices as i32, allocated_base_slices: 0, slice_allocations: Vec::new(), available_combinations: Vec::new(), // Will be calculated below slice_pricing: Some(serde_json::to_value(&slice_pricing).unwrap_or_default()), slice_last_calculated: Some(Utc::now()), }; // Generate initial slice combinations let combinations = self.slice_calculator.generate_slice_combinations( node.total_base_slices as u32, node.allocated_base_slices as u32, &node, user_email ); node.available_combinations = combinations.iter() .map(|c| serde_json::to_value(c).unwrap_or_default()) .collect(); // Save to persistent storage let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email); // Check for duplicates before adding let node_exists = persistent_data.nodes.iter().any(|existing_node| { existing_node.id == node.id || (existing_node.grid_node_id.is_some() && node.grid_node_id.is_some() && existing_node.grid_node_id == node.grid_node_id) }); if node_exists { return Err(format!("Node with ID '{}' or grid_node_id '{:?}' already exists", node.id, node.grid_node_id)); } persistent_data.nodes.push(node.clone()); UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; // Auto-generate marketplace products if rental options are configured self.auto_generate_marketplace_products(&node, user_email, &persistent_data.name.unwrap_or_else(|| "Unknown ResourceProvider".to_string()), node_data.slice_prices.as_ref())?; Ok(node) } /// Add a new node from ThreeFold Grid (automatic creation with real data) pub async fn add_node_from_grid(&self, user_email: &str, grid_node_id: u32) -> Result { // Load existing data and check for duplicates more thoroughly let mut persistent_data = UserPersistence::load_user_data(user_email) .unwrap_or_else(|| Self::create_default_user_data(user_email)); // ENHANCED DUPLICATE DETECTION: Check by grid_node_id, node ID pattern, AND exact ID match let node_id_pattern = format!("grid_node_{}", grid_node_id); let existing_node_index = persistent_data.nodes.iter().position(|n| { // Check by grid_node_id (most reliable) n.grid_node_id == Some(grid_node_id.to_string()) || // Check by exact ID match n.id == node_id_pattern || // Check by ID pattern (for variations like grid_node_1_abcd) n.id.starts_with(&node_id_pattern) }); if let Some(index) = existing_node_index { // Node exists - update it instead of creating duplicate // Fetch latest data from grid let grid_data = self.grid_service.fetch_node_data(grid_node_id).await?; let result_node = { let existing_node = &mut persistent_data.nodes[index]; // Update with latest grid data existing_node.capacity = grid_data.total_resources.clone(); existing_node.used_capacity = grid_data.used_resources.clone(); existing_node.grid_data = Some(serde_json::to_value(grid_data.clone()).unwrap_or_default()); existing_node.last_seen = Some(grid_data.last_updated); existing_node.uptime_percentage = 99.8; // Set clean uptime value // Recalculate slices if needed let new_total_base_slices = self.slice_calculator.calculate_max_base_slices(&existing_node.capacity); if new_total_base_slices != existing_node.total_base_slices as u32 { existing_node.total_base_slices = new_total_base_slices as i32; let combinations = self.slice_calculator.generate_slice_combinations( existing_node.total_base_slices as u32, existing_node.allocated_base_slices as u32, existing_node, user_email ); existing_node.available_combinations = combinations.iter() .map(|c| serde_json::to_value(c).unwrap_or_default()) .collect(); existing_node.slice_last_calculated = Some(Utc::now()); } // Ensure marketplace SLA is set with clean uptime value if existing_node.marketplace_sla.is_none() { existing_node.marketplace_sla = Some(MarketplaceSLA { id: format!("sla-{}", existing_node.id), name: "Standard Marketplace SLA".to_string(), uptime_guarantee: 99.8, response_time_hours: 24, resolution_time_hours: 48, penalty_rate: 0.01, uptime_guarantee_percentage: 99.8, // Clean uptime value bandwidth_guarantee_mbps: existing_node.capacity.bandwidth_mbps as f32, base_slice_price: existing_node.slice_pricing .as_ref() .and_then(|pricing| pricing.get("base_price_per_hour")) .and_then(|price| price.as_str()) .and_then(|price_str| rust_decimal::Decimal::from_str(price_str).ok()) .unwrap_or_else(|| rust_decimal::Decimal::try_from(0.50).unwrap_or_default()), last_updated: Utc::now(), }); } // Ensure rental options are set if existing_node.rental_options.is_none() { let rental_options = crate::models::user::NodeRentalOptions { full_node_available: false, slice_formats: vec!["1x1".to_string(), "2x2".to_string(), "4x4".to_string()], pricing: crate::models::user::FullNodePricing { monthly_cost: rust_decimal::Decimal::try_from(100.0).unwrap_or_default(), ..Default::default() }, slice_rental_enabled: true, full_node_rental_enabled: false, full_node_pricing: None, minimum_rental_days: 1, maximum_rental_days: Some(365), auto_renewal_enabled: true, }; existing_node.rental_options = Some(serde_json::to_value(&rental_options).unwrap_or_default()); } existing_node.clone() }; // Save updated data UserPersistence::save_user_data(&persistent_data) .map_err(|e| format!("Failed to save user data: {}", e))?; return Ok(result_node); } // 1. Fetch real node data from gridproxy let grid_data = self.grid_service.fetch_node_data(grid_node_id).await?; // 2. Calculate slice capacity from real grid data let total_base_slices = self.slice_calculator.calculate_max_base_slices(&grid_data.total_resources); // 3. Generate unique node ID let mut node_id = format!("grid_node_{}", grid_node_id); while persistent_data.nodes.iter().any(|n| n.id == node_id) { node_id = format!("grid_node_{}_{}", grid_node_id, &uuid::Uuid::new_v4().to_string()[..4]); } // 4. Create node with real grid data and calculated slices let node_id_for_sla = node_id.clone(); let mut node = FarmNode { id: node_id, location: { let city = if grid_data.city.is_empty() { "Unknown" } else { &grid_data.city }; let country = if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country }; if city == "Unknown" { country.to_string() } else { format!("{}, {}", city, country) } }, status: NodeStatus::Online, // Assume online if we can fetch from grid capacity: grid_data.total_resources.clone(), used_capacity: grid_data.used_resources.clone(), uptime_percentage: 99.8, // Clean uptime value for grid nodes farming_start_date: Utc::now() - chrono::Duration::days(30), // Default farming start last_updated: grid_data.last_updated, last_seen: Some(Utc::now()), health_score: 98.5, utilization_7_day_avg: 65.0, // Default utilization slice_formats_supported: vec!["1x1".to_string(), "2x2".to_string(), "4x4".to_string()], rental_options: None, earnings_today_usd: Decimal::ZERO, name: if grid_data.farm_name.is_empty() { format!("Grid Node {}", grid_node_id) } else { grid_data.farm_name.clone() }, region: if grid_data.country.is_empty() { "Unknown".to_string() } else { grid_data.country.clone() }, node_type: "MyceliumNode".to_string(), slice_formats: None, // Not used in new slice system staking_options: None, availability_status: crate::models::user::NodeAvailabilityStatus::Available, grid_node_id: Some(grid_node_id.to_string()), grid_data: Some(serde_json::to_value(&grid_data).unwrap_or_default()), node_group_id: None, group_assignment_date: None, group_slice_format: None, group_slice_price: None, // NEW: Marketplace SLA field with clean uptime value marketplace_sla: Some(MarketplaceSLA { id: format!("sla-{}", node_id_for_sla), name: "Standard Marketplace SLA".to_string(), uptime_guarantee: 99.8, response_time_hours: 24, resolution_time_hours: 48, penalty_rate: 0.01, uptime_guarantee_percentage: 99.8, bandwidth_guarantee_mbps: grid_data.total_resources.bandwidth_mbps as f32, base_slice_price: SlicePricing::default().base_price_per_hour, last_updated: Utc::now(), }), // NEW: Automatic slice management fields total_base_slices: total_base_slices as i32, allocated_base_slices: 0, slice_allocations: Vec::new(), available_combinations: Vec::new(), // Will be calculated below slice_pricing: Some(serde_json::to_value(&SlicePricing::default()).unwrap_or_default()), slice_last_calculated: Some(Utc::now()), }; // 5. Generate initial slice combinations let combinations = self.slice_calculator.generate_slice_combinations( node.total_base_slices as u32, node.allocated_base_slices as u32, &node, user_email ); node.available_combinations = combinations.iter() .map(|c| serde_json::to_value(c).unwrap_or_default()) .collect(); // 6. Save to persistent storage let mut persistent_data = UserPersistence::load_user_data(user_email) .unwrap_or_else(|| Self::create_default_user_data(user_email)); persistent_data.nodes.push(node.clone()); UserPersistence::save_user_data(&persistent_data) .map_err(|e| format!("Failed to save user data: {}", e))?; Ok(node) } /// Sync node capacity and slices with grid data pub async fn sync_node_with_grid(&self, user_email: &str, node_id: &str) -> Result<(), String> { let mut resource_provider_data = UserPersistence::load_user_data(user_email) .ok_or("ResourceProvider data not found")?; if let Some(node) = resource_provider_data.nodes.iter_mut().find(|n| n.id == node_id) { if let Some(grid_node_id) = &node.grid_node_id { // Fetch latest capacity from grid let grid_id: u32 = grid_node_id.parse().unwrap_or(0); let updated_grid_data = self.grid_service.fetch_node_data(grid_id).await?; // Update capacity and recalculate slices node.capacity = updated_grid_data.total_resources.clone(); node.used_capacity = updated_grid_data.used_resources.clone(); node.uptime_percentage = 99.8; // Clean uptime value for grid nodes node.status = NodeStatus::Online; // Assume online if we can fetch from grid node.grid_data = Some(serde_json::to_value(&updated_grid_data).unwrap_or_default()); // Recalculate total base slices let new_total_base_slices = self.slice_calculator.calculate_max_base_slices(&node.capacity); // Only update if capacity increased (don't reduce if slices are allocated) if new_total_base_slices > node.total_base_slices as u32 || node.allocated_base_slices == 0 { node.total_base_slices = new_total_base_slices as i32; // Recalculate available combinations let combinations = self.slice_calculator.generate_slice_combinations( node.total_base_slices as u32, node.allocated_base_slices as u32, node, user_email ); node.available_combinations = combinations.iter() .map(|c| serde_json::to_value(c).unwrap_or_default()) .collect(); node.slice_last_calculated = Some(Utc::now()); } // Save updated data UserPersistence::save_user_data(&resource_provider_data) .map_err(|e| format!("Failed to save user data: {}", e))?; } else { return Err("Node is not linked to grid".to_string()); } } else { return Err("Node not found".to_string()); } Ok(()) } /// Auto-generate marketplace products for a node based on rental options /// Products are now managed through persistent user data and aggregated by ProductService fn auto_generate_marketplace_products(&self, _node: &FarmNode, _resource_provider_email: &str, _resource_provider_name: &str, _slice_prices: Option<&std::collections::HashMap>) -> Result<(), String> { // Product generation is now handled through persistent user data // ProductService automatically aggregates products from user-owned data Ok(()) } /// Stake USD on a node pub fn stake_on_node(&self, user_email: &str, node_id: &str, staking_options: crate::models::user::NodeStakingOptions) -> Result<(), String> { // Load user data let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; // Validate wallet balance if persistent_data.wallet_balance_usd < staking_options.staked_amount { return Err(format!("Insufficient balance. Available: ${}, Required: ${}", persistent_data.wallet_balance_usd, staking_options.staked_amount)); } // Find the node and check if staking is already enabled let staked_amount = { let node = persistent_data.nodes.iter_mut() .find(|n| n.id == node_id) .ok_or("Node not found")?; // Check if node already has staking enabled if let Some(existing_staking) = &node.staking_options { if existing_staking .get("staking_enabled") .and_then(|enabled| enabled.as_bool()) .unwrap_or(false) { return Err("Node already has staking enabled. Use update_node_staking to modify.".to_string()); } } // Set staking options on node let mut final_staking_options = staking_options; final_staking_options.staking_start_date = Some(Utc::now()); let staked_amount = final_staking_options.staked_amount; node.staking_options = Some(serde_json::to_value(&final_staking_options).unwrap_or_default()); staked_amount }; // Deduct staked amount from wallet persistent_data.wallet_balance_usd -= staked_amount; // Save updated data UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(()) } /// Update staking on a node pub fn update_node_staking(&self, user_email: &str, node_id: &str, new_staking_options: crate::models::user::NodeStakingOptions) -> Result<(), String> { // Load user data let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; // Find the node let node = persistent_data.nodes.iter_mut() .find(|n| n.id == node_id) .ok_or("Node not found")?; let current_staked = if let Some(existing_staking) = &node.staking_options { existing_staking .get("staked_amount") .and_then(|amount| amount.as_str()) .and_then(|amount_str| rust_decimal::Decimal::from_str(amount_str).ok()) .unwrap_or_default() } else { Decimal::ZERO }; let new_staked = new_staking_options.staked_amount; let difference = new_staked - current_staked; // If increasing stake, check wallet balance if difference > Decimal::ZERO { if persistent_data.wallet_balance_usd < difference { return Err(format!("Insufficient balance for additional staking. Available: ${}, Required: ${}", persistent_data.wallet_balance_usd, difference)); } persistent_data.wallet_balance_usd -= difference; } else if difference < Decimal::ZERO { // If decreasing stake, check for early withdrawal penalty let withdrawal_amount = difference.abs(); let penalty = if let Some(existing_staking) = &node.staking_options { if existing_staking .get("early_withdrawal_allowed") .and_then(|allowed| allowed.as_bool()) .unwrap_or(false) { // Calculate early withdrawal penalty from JSON data existing_staking.get("staked_amount") .and_then(|amount| amount.as_str()) .and_then(|amount_str| rust_decimal::Decimal::from_str(amount_str).ok()) .and_then(|amount| { existing_staking.get("early_withdrawal_penalty_percent") .and_then(|penalty| penalty.as_f64()) .map(|percent| amount * rust_decimal::Decimal::try_from(percent / 100.0).unwrap_or_default()) }) .unwrap_or(rust_decimal::Decimal::ZERO) } else { Decimal::ZERO } } else { Decimal::ZERO }; // Return amount minus penalty to wallet persistent_data.wallet_balance_usd += withdrawal_amount - penalty; if penalty > Decimal::ZERO { } } // Update staking options let mut final_staking_options = new_staking_options; if node.staking_options.is_none() { final_staking_options.staking_start_date = Some(Utc::now()); } else { // Keep original start date if updating existing staking final_staking_options.staking_start_date = node.staking_options .as_ref() .and_then(|staking| staking.get("staking_start_date")) .and_then(|date| date.as_str()) .and_then(|date_str| chrono::DateTime::parse_from_rfc3339(date_str).ok()) .map(|dt| dt.with_timezone(&chrono::Utc)); } node.staking_options = Some(serde_json::to_value(&final_staking_options).unwrap_or_default()); // Save updated data UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(()) } /// Remove staking from a node pub fn unstake_from_node(&self, user_email: &str, node_id: &str) -> Result { // Load user data let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; // Find the node let node = persistent_data.nodes.iter_mut() .find(|n| n.id == node_id) .ok_or("Node not found")?; let staking_options = node.staking_options.as_ref() .ok_or("Node has no staking enabled")?; if !staking_options .get("staking_enabled") .and_then(|enabled| enabled.as_bool()) .unwrap_or(false) { return Err("Staking is not enabled on this node".to_string()); } let staked_amount = staking_options .get("staked_amount") .and_then(|amount| amount.as_str()) .and_then(|amount_str| rust_decimal::Decimal::from_str(amount_str).ok()) .unwrap_or_default(); let penalty = if staking_options .get("early_withdrawal_allowed") .and_then(|allowed| allowed.as_bool()) .unwrap_or(false) { // Calculate early withdrawal penalty from JSON data staking_options.get("staked_amount") .and_then(|amount| amount.as_str()) .and_then(|amount_str| rust_decimal::Decimal::from_str(amount_str).ok()) .and_then(|amount| { staking_options.get("early_withdrawal_penalty_percent") .and_then(|penalty| penalty.as_f64()) .map(|percent| amount * rust_decimal::Decimal::try_from(percent / 100.0).unwrap_or_default()) }) .unwrap_or(rust_decimal::Decimal::ZERO) } else { Decimal::ZERO }; let return_amount = staked_amount - penalty; // Return amount to wallet persistent_data.wallet_balance_usd += return_amount; // Remove staking options node.staking_options = None; // Save updated data UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(return_amount) } /// Get total staked amount for a user pub fn get_total_staked_amount(&self, user_email: &str) -> Decimal { if let Some(data) = UserPersistence::load_user_data(user_email) { data.nodes.iter() .filter_map(|node| node.staking_options.as_ref()) .filter(|staking| staking .get("staking_enabled") .and_then(|enabled| enabled.as_bool()) .unwrap_or(false)) .map(|staking| staking .get("staked_amount") .and_then(|amount| amount.as_str()) .and_then(|amount_str| rust_decimal::Decimal::from_str(amount_str).ok()) .unwrap_or_default()) .sum() } else { Decimal::ZERO } } /// Get staking statistics for a user pub fn get_staking_statistics(&self, user_email: &str) -> StakingStatistics { if let Some(data) = UserPersistence::load_user_data(user_email) { let mut total_staked = Decimal::ZERO; let mut staked_nodes = 0; let mut next_unlock_date: Option> = None; for node in &data.nodes { if let Some(staking) = &node.staking_options { if staking .get("staking_enabled") .and_then(|enabled| enabled.as_bool()) .unwrap_or(false) { let staking_amount = staking .get("staked_amount") .and_then(|amount| amount.as_str()) .and_then(|amount_str| rust_decimal::Decimal::from_str(amount_str).ok()) .unwrap_or_default(); total_staked += staking_amount; staked_nodes += 1; // Calculate unlock date if let (Some(start_date), Some(period_months)) = ( staking.get("staking_start_date") .and_then(|date| date.as_str()) .and_then(|date_str| chrono::DateTime::parse_from_rfc3339(date_str).ok()) .map(|dt| dt.with_timezone(&chrono::Utc)), staking.get("staking_period_months") .and_then(|months| months.as_i64()) ) { let unlock_date = start_date + chrono::Duration::days((period_months * 30) as i64); if next_unlock_date.is_none() || unlock_date < next_unlock_date.unwrap() { next_unlock_date = Some(unlock_date); } } } } } StakingStatistics { total_staked_amount: total_staked, staked_nodes_count: staked_nodes, next_unlock_date, } } else { StakingStatistics::default() } } /// Create a slice product from a node and slice format fn create_slice_product_from_node(&self, node: &FarmNode, slice_format_id: &str, resource_provider_email: &str, resource_provider_name: &str, custom_prices: Option<&std::collections::HashMap>) -> Result { // Get slice format details and default pricing let (cpu_cores, memory_gb, storage_gb, bandwidth_mbps, default_price) = match slice_format_id { "basic" => (2, 4, 100, 100, 25), "standard" => (4, 8, 250, 200, 50), "performance" => (8, 16, 500, 500, 100), "small" => (1, 2, 20, 100, 25), // Legacy support "medium" => (2, 4, 50, 200, 50), // Legacy support "large" => (4, 8, 100, 500, 100), // Legacy support "xlarge" => (8, 16, 200, 1000, 200), // Legacy support _ => (2, 4, 100, 100, 25), // Default to basic }; // Use custom price if provided, otherwise use default let price = if let Some(prices) = custom_prices { if let Some(custom_price) = prices.get(slice_format_id) { *custom_price } else { rust_decimal::Decimal::from(default_price) } } else { rust_decimal::Decimal::from(default_price) }; let mut product = crate::models::product::Product { id: format!("slice_{}_{}", node.id, slice_format_id), name: format!("{} Slice: {} ({})", slice_format_id.to_uppercase(), node.name, node.location), category_id: "compute".to_string(), description: format!( "{} compute slice from {} with {}vCPU, {}GB RAM, {}GB storage in {}", slice_format_id, node.name, cpu_cores, memory_gb, storage_gb, node.location ), base_price: price, base_currency: "USD".to_string(), attributes: std::collections::HashMap::new(), provider_id: resource_provider_email.to_string(), provider_name: resource_provider_name.to_string(), availability: match node.availability_status { crate::models::user::NodeAvailabilityStatus::Available => crate::models::product::ProductAvailability::Available, crate::models::user::NodeAvailabilityStatus::PartiallyRented => crate::models::product::ProductAvailability::Limited, _ => crate::models::product::ProductAvailability::Unavailable, }, metadata: crate::models::product::ProductMetadata { tags: vec!["compute".to_string(), "slice".to_string(), slice_format_id.to_string(), node.region.clone()], location: Some(node.location.clone()), rating: None, review_count: 0, featured: false, last_updated: chrono::Utc::now(), visibility: crate::models::product::ProductVisibility::Public, seo_keywords: Vec::new(), custom_fields: std::collections::HashMap::new(), }, created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), }; // Add slice-specific attributes product.add_attribute( "node_id".to_string(), serde_json::Value::String(node.id.clone()), crate::models::product::AttributeType::Text, ); product.add_attribute( "rental_type".to_string(), serde_json::Value::String("slice".to_string()), crate::models::product::AttributeType::Text, ); product.add_attribute( "slice_format".to_string(), serde_json::Value::String(slice_format_id.to_string()), crate::models::product::AttributeType::Text, ); product.add_attribute( "cpu_cores".to_string(), serde_json::Value::Number(serde_json::Number::from(cpu_cores)), crate::models::product::AttributeType::Number, ); product.add_attribute( "memory_gb".to_string(), serde_json::Value::Number(serde_json::Number::from(memory_gb)), crate::models::product::AttributeType::Number, ); product.add_attribute( "storage_gb".to_string(), serde_json::Value::Number(serde_json::Number::from(storage_gb)), crate::models::product::AttributeType::Number, ); product.add_attribute( "bandwidth_mbps".to_string(), serde_json::Value::Number(serde_json::Number::from(bandwidth_mbps)), crate::models::product::AttributeType::Number, ); product.add_attribute( "location".to_string(), serde_json::Value::String(node.location.clone()), crate::models::product::AttributeType::Text, ); Ok(product) } /// Update node status pub fn update_node_status(&self, user_email: &str, node_id: &str, status: NodeStatus) -> Result<(), String> { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; if let Some(node) = persistent_data.nodes.iter_mut().find(|n| n.id == node_id) { node.status = status; node.last_seen = Some(Utc::now()); } else { return Err("Node not found".to_string()); } UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(()) } /// Get resource_provider earnings pub fn get_resource_provider_earnings(&self, user_email: &str) -> Vec { if let Some(data) = UserPersistence::load_user_data(user_email) { data.resource_provider_earnings } else { Vec::new() } } /// Get resource_provider statistics pub fn get_resource_provider_statistics(&self, user_email: &str) -> FarmerStatistics { let nodes = self.get_resource_provider_nodes(user_email); let earnings = self.get_resource_provider_earnings(user_email); let total_nodes = nodes.len() as i32; let online_nodes = nodes.iter() .filter(|n| matches!(n.status, NodeStatus::Online)) .count() as i32; // Calculate total and used capacity let mut total_capacity = NodeCapacity { cpu_cores: 0, memory_gb: 0, storage_gb: 0, bandwidth_mbps: 0, ssd_storage_gb: 0, hdd_storage_gb: 0, ram_gb: 0, }; let mut used_capacity = NodeCapacity { cpu_cores: 0, memory_gb: 0, storage_gb: 0, bandwidth_mbps: 0, ssd_storage_gb: 0, hdd_storage_gb: 0, ram_gb: 0, }; for node in &nodes { total_capacity.cpu_cores += node.capacity.cpu_cores; total_capacity.memory_gb += node.capacity.memory_gb; total_capacity.storage_gb += node.capacity.storage_gb; total_capacity.bandwidth_mbps += node.capacity.bandwidth_mbps; total_capacity.ssd_storage_gb += node.capacity.ssd_storage_gb; total_capacity.hdd_storage_gb += node.capacity.hdd_storage_gb; used_capacity.cpu_cores += node.used_capacity.cpu_cores; used_capacity.memory_gb += node.used_capacity.memory_gb; used_capacity.storage_gb += node.used_capacity.storage_gb; used_capacity.bandwidth_mbps += node.used_capacity.bandwidth_mbps; used_capacity.ssd_storage_gb += node.used_capacity.ssd_storage_gb; used_capacity.hdd_storage_gb += node.used_capacity.hdd_storage_gb; } let monthly_earnings = earnings.iter().map(|e| e.amount).sum::(); let uptime_percentage = if nodes.is_empty() { 0.0 } else { nodes.iter().map(|n| n.uptime_percentage).sum::() / nodes.len() as f32 }; // Calculate slice statistics let mut total_base_slices = 0; let mut allocated_base_slices = 0; for node in &nodes { // Calculate total base slices for this node (minimum of CPU/1, Memory/4, Storage/200) let cpu_slices = node.capacity.cpu_cores; let memory_slices = node.capacity.memory_gb / 4; let storage_slices = node.capacity.storage_gb / 200; let node_base_slices = cpu_slices.min(memory_slices).min(storage_slices); total_base_slices += node_base_slices; // Calculate allocated base slices for this node let used_cpu_slices = node.used_capacity.cpu_cores; let used_memory_slices = node.used_capacity.memory_gb / 4; let used_storage_slices = node.used_capacity.storage_gb / 200; let node_allocated_slices = used_cpu_slices.min(used_memory_slices).min(used_storage_slices); allocated_base_slices += node_allocated_slices; } FarmerStatistics { total_nodes, online_nodes, total_capacity, used_capacity, monthly_earnings: monthly_earnings.to_string().parse::().unwrap_or(0), uptime_percentage, total_base_slices, allocated_base_slices, } } /// Check if slice fits in node capacity pub fn slice_fits_in_node(&self, slice_template: &Product, node: &FarmNode) -> Result { let slice_config = slice_template.get_slice_configuration() .ok_or("Invalid slice configuration")?; let available_cpu = node.capacity.cpu_cores - node.used_capacity.cpu_cores; let available_memory = node.capacity.memory_gb - node.used_capacity.memory_gb; let available_storage = node.capacity.storage_gb - node.used_capacity.storage_gb; let available_bandwidth = node.capacity.bandwidth_mbps - node.used_capacity.bandwidth_mbps; Ok(slice_config.cpu_cores <= available_cpu && slice_config.memory_gb <= available_memory && slice_config.storage_gb <= available_storage && slice_config.bandwidth_mbps <= available_bandwidth) } /// Get default slice formats available to all resource providers pub fn get_default_slice_formats(&self) -> Vec { vec![ DefaultSliceFormat { id: "basic".to_string(), name: "Basic".to_string(), cpu_cores: 2, memory_gb: 4, storage_gb: 100, bandwidth_mbps: 100, description: "Basic compute slice for light workloads".to_string(), price_per_hour: rust_decimal::Decimal::from(10), }, DefaultSliceFormat { id: "standard".to_string(), name: "Standard".to_string(), cpu_cores: 4, memory_gb: 8, storage_gb: 250, bandwidth_mbps: 500, description: "Standard compute slice for general workloads".to_string(), price_per_hour: rust_decimal::Decimal::from(20), }, DefaultSliceFormat { id: "performance".to_string(), name: "Performance".to_string(), cpu_cores: 8, memory_gb: 16, storage_gb: 500, bandwidth_mbps: 1000, description: "High-performance slice for demanding workloads".to_string(), price_per_hour: rust_decimal::Decimal::from(40), }, ] } /// Get a specific default slice format by ID pub fn get_default_slice_format_by_id(&self, format_id: &str) -> Option { self.get_default_slice_formats() .into_iter() .find(|format| format.id == format_id) } /// Get default slice format with user customizations applied pub fn get_default_slice_format_with_customizations(&self, user_email: &str, format_id: &str) -> Option { let mut format = self.get_default_slice_format_by_id(format_id)?; // Load user customizations from persistent data if let Some(persistent_data) = UserPersistence::load_user_data(user_email) { if let Some(resource_provider_settings) = persistent_data.resource_provider_settings { if let Some(customizations) = resource_provider_settings.default_slice_customizations { if let Some(custom_format) = customizations.get(format_id) { // Apply user customizations if let Some(cpu_cores) = custom_format.get("cpu_cores").and_then(|v| v.as_u64()).map(|v| v as u32) { format.cpu_cores = cpu_cores as i32; } if let Some(memory_gb) = custom_format.get("memory_gb").and_then(|v| v.as_u64()).map(|v| v as u32) { format.memory_gb = memory_gb as i32; } if let Some(storage_gb) = custom_format.get("storage_gb").and_then(|v| v.as_u64()).map(|v| v as u32) { format.storage_gb = storage_gb as i32; } if let Some(bandwidth_mbps) = custom_format.get("bandwidth_mbps").and_then(|v| v.as_u64()).map(|v| v as i32) { format.bandwidth_mbps = bandwidth_mbps; } if let Some(price_str) = custom_format.get("price_per_hour").and_then(|v| v.as_str()) { if let Ok(price) = rust_decimal::Decimal::from_str(price_str) { format.price_per_hour = price; } } if let Some(description) = custom_format.get("description").and_then(|v| v.as_str()) { format.description = description.to_string(); } } } } } Some(format) } /// Save default slice customizations for a user pub fn save_default_slice_customization(&self, user_email: &str, format_id: &str, customization: DefaultSliceFormat) -> Result<(), String> { let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email); // Initialize resource_provider settings if needed if persistent_data.resource_provider_settings.is_none() { persistent_data.resource_provider_settings = Some(crate::models::user::FarmerSettings { auto_accept_reserved_slices: true, maintenance_window: "02:00-04:00 UTC".to_string(), notification_email: None, slack_webhook: None, max_slice_commitment_months: Some(12), emergency_contact: None, automated_updates: Some(true), performance_notifications: Some(true), maintenance_reminders: Some(true), auto_accept_deployments: false, default_slice_customizations: Some(serde_json::to_value(std::collections::HashMap::::new()).unwrap_or_default()), minimum_deployment_duration: 24, notification_preferences: Some(serde_json::to_value(crate::models::user::NotificationSettings::default()).unwrap_or_default()), preferred_regions: Vec::new(), }); } // Initialize default slice customizations if needed if let Some(ref mut resource_provider_settings) = persistent_data.resource_provider_settings { if resource_provider_settings.default_slice_customizations.is_none() { resource_provider_settings.default_slice_customizations = Some(serde_json::to_value(std::collections::HashMap::::new()).unwrap_or_default()); } if let Some(ref mut customizations) = resource_provider_settings.default_slice_customizations { if let Some(obj) = customizations.as_object_mut() { obj.insert(format_id.to_string(), serde_json::to_value(customization).unwrap_or_default()); } } } UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(()) } /// Get a specific node by ID pub fn get_node_by_id(&self, user_email: &str, node_id: &str) -> Option { let mut persistent_data = UserPersistence::load_user_data(user_email)?; if let Some(node) = persistent_data.nodes.iter_mut().find(|n| n.id == node_id) { // Debug: Log the marketplace SLA data being retrieved if let Some(ref sla) = node.marketplace_sla { } else { } // Ensure slice calculations are current self.refresh_node_slice_calculations(node, user_email); let result = node.clone(); // Save updated data back to persistence if let Err(e) = UserPersistence::save_user_data(&persistent_data) { } Some(result) } else { None } } /// Refresh slice calculations for a specific node fn refresh_node_slice_calculations(&self, node: &mut FarmNode, user_email: &str) { // Recalculate total base slices from node capacity let new_total_base_slices = self.slice_calculator.calculate_max_base_slices(&node.capacity); // Update total base slices if different if node.total_base_slices != new_total_base_slices as i32 { node.total_base_slices = new_total_base_slices as i32; } // Regenerate slice combinations with current allocation let combinations = self.slice_calculator.generate_slice_combinations( node.total_base_slices as u32, node.allocated_base_slices as u32, node, user_email ); node.available_combinations = combinations.iter() .map(|c| serde_json::to_value(c).unwrap_or_default()) .collect(); // Ensure slice pricing is set let current_price = node.slice_pricing .as_ref() .and_then(|pricing| pricing.get("base_price_per_hour")) .and_then(|price| price.as_str()) .and_then(|price_str| rust_decimal::Decimal::from_str(price_str).ok()) .unwrap_or_else(|| rust_decimal::Decimal::ZERO); if current_price.is_zero() { if let Some(ref mut pricing) = node.slice_pricing { if let Some(pricing_obj) = pricing.as_object_mut() { pricing_obj.insert("base_price_per_hour".to_string(), serde_json::Value::String("0.50".to_string())); } } } // Update calculation timestamp node.slice_last_calculated = Some(chrono::Utc::now()); } /// Update node configuration pub fn update_node(&self, user_email: &str, node_id: &str, update_data: NodeUpdateData) -> Result<(), String> { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; if let Some(node) = persistent_data.nodes.iter_mut().find(|n| n.id == node_id) { if let Some(name) = update_data.name { node.name = name; } if let Some(location) = update_data.location { node.location = location; } if let Some(region) = update_data.region { node.region = region; } if let Some(status) = update_data.status { node.status = status; } if let Some(capacity) = update_data.capacity { node.capacity = capacity; } if let Some(marketplace_sla) = update_data.marketplace_sla { node.marketplace_sla = Some(marketplace_sla); } node.last_seen = Some(Utc::now()); } else { return Err("Node not found".to_string()); } UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(()) } /// Add slice format to node pub fn add_slice_format_to_node(&self, user_email: &str, node_id: &str, slice_format_id: &str) -> Result<(), String> { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; if let Some(node) = persistent_data.nodes.iter_mut().find(|n| n.id == node_id) { // Ensure vector is initialized with a concrete type let formats = node .slice_formats .get_or_insert_with(|| Vec::::new()); // Avoid temporary String when checking contains if !formats.iter().any(|f| f == slice_format_id) { formats.push(slice_format_id.to_string()); } } else { return Err("Node not found".to_string()); } UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(()) } /// Remove slice format from node pub fn remove_slice_format_from_node(&self, user_email: &str, node_id: &str, slice_format_id: &str) -> Result<(), String> { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; if let Some(node) = persistent_data.nodes.iter_mut().find(|n| n.id == node_id) { let formats = node .slice_formats .get_or_insert_with(|| Vec::::new()); formats.retain(|f| f != slice_format_id); } else { return Err("Node not found".to_string()); } UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(()) } /// Validate slice format selection for node creation pub fn validate_slice_formats(&self, slice_formats: &[String]) -> Result<(), String> { if slice_formats.is_empty() { return Err("At least one slice format must be selected".to_string()); } if slice_formats.len() > 3 { return Err("At most 3 slice formats can be selected".to_string()); } let default_formats = self.get_default_slice_formats(); let default_ids: Vec = default_formats.iter().map(|f| f.id.clone()).collect(); // Check if all selected formats are valid (either default or custom) for format_id in slice_formats { if !default_ids.contains(format_id) { // Check if it's a custom slice format // This would need to be enhanced to check against user's custom slice products } } Ok(()) } // ============================================================================= // GRID NODE MANAGEMENT // ============================================================================= /// Add a node from ThreeFold Grid by node ID pub async fn add_grid_node(&self, user_email: &str, grid_node_id: u32, _slice_format: Option, _slice_price: Option) -> Result { // Check for duplicate grid node IDs first let existing_nodes = self.get_resource_provider_nodes(user_email); if existing_nodes.iter().any(|n| n.grid_node_id == Some(grid_node_id.to_string())) { return Err(format!("Node {} is already registered", grid_node_id)); } // Validate node exists on grid if !self.grid_service.validate_node_exists(grid_node_id).await? { return Err(format!("Node {} does not exist on ThreeFold Grid", grid_node_id)); } // Fetch node data from grid let grid_data = self.grid_service.fetch_node_data(grid_node_id).await?; // Create FarmNode from grid data let node = FarmNodeBuilder::new() .id(format!("grid_node_{}", grid_node_id)) .name(format!("Grid Node {}", grid_node_id)) .location(format!("{}, {}", if grid_data.city.is_empty() { "Unknown City" } else { &grid_data.city }, if grid_data.country.is_empty() { "Unknown Country" } else { &grid_data.country } )) .status(NodeStatus::Online) .capacity(grid_data.total_resources.clone()) .used_capacity(grid_data.used_resources.clone()) .uptime_percentage(99.0) // Default uptime .earnings_today_usd(Decimal::ZERO) .health_score(100.0) .region(if grid_data.country.is_empty() { "Unknown".to_string() } else { grid_data.country.clone() }) .node_type("MyceliumNode".to_string()) .grid_node_id(grid_node_id) .grid_data(grid_data) .build()?; // Save to persistent storage let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email); persistent_data.nodes.push(node.clone()); UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(node) } /// Add multiple grid nodes at once pub async fn add_multiple_grid_nodes(&self, user_email: &str, grid_node_ids: Vec) -> Result, String> { // Batch approach: load once, add all nodes, save once let mut added_nodes: Vec = Vec::new(); let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email); for grid_node_id in grid_node_ids { let node_id = format!("grid_node_{}", grid_node_id); // Skip duplicates (existing or already added in this batch) let exists_in_persistent = persistent_data.nodes.iter().any(|n| n.grid_node_id == Some(grid_node_id.to_string()) || n.id == node_id ); let exists_in_batch = added_nodes.iter().any(|n: &FarmNode| n.grid_node_id == Some(grid_node_id.to_string()) || n.id == node_id ); if exists_in_persistent || exists_in_batch { continue; } // Validate and fetch grid data if !self.grid_service.validate_node_exists(grid_node_id).await.map_err(|e| e.to_string())? { return Err(format!("Node {} does not exist on ThreeFold Grid", grid_node_id)); } let grid_data = self.grid_service.fetch_node_data(grid_node_id).await.map_err(|e| e.to_string())?; // Build node let node = FarmNodeBuilder::new() .id(node_id) .name(format!("Grid Node {}", grid_node_id)) .location(format!("{}, {}", if grid_data.city.is_empty() { "Unknown City" } else { &grid_data.city }, if grid_data.country.is_empty() { "Unknown Country" } else { &grid_data.country } )) .status(NodeStatus::Online) .capacity(grid_data.total_resources.clone()) .used_capacity(grid_data.used_resources.clone()) .uptime_percentage(99.0) .earnings_today_usd(Decimal::ZERO) .health_score(100.0) .region(if grid_data.country.is_empty() { "Unknown".to_string() } else { grid_data.country.clone() }) .node_type("MyceliumNode".to_string()) .grid_node_id(grid_node_id) .grid_data(grid_data) .build() .map_err(|e| e.to_string())?; persistent_data.nodes.push(node.clone()); added_nodes.push(node); } if !added_nodes.is_empty() { UserPersistence::save_user_data(&persistent_data).map_err(|e| e.to_string())?; } Ok(added_nodes) } /// Add multiple grid nodes with slice configuration pub async fn add_multiple_grid_nodes_with_config(&self, user_email: &str, grid_node_ids: Vec, slice_formats: Vec, full_node_rental: bool) -> Result, String> { // Batch approach: load once, add all nodes, save once let mut added_nodes: Vec = Vec::new(); let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email); for grid_node_id in grid_node_ids { let node_id = format!("grid_node_{}", grid_node_id); // Skip duplicates (existing or already added in this batch) let exists_in_persistent = persistent_data.nodes.iter().any(|n| n.grid_node_id == Some(grid_node_id.to_string()) || n.id == node_id ); let exists_in_batch = added_nodes.iter().any(|n: &FarmNode| n.grid_node_id == Some(grid_node_id.to_string()) || n.id == node_id ); if exists_in_persistent || exists_in_batch { continue; } // Fetch node data from Grid let grid_data = self.grid_service.fetch_node_data(grid_node_id).await .map_err(|e| format!("Failed to fetch grid node data: {}", e))?; // Build rental options based on inputs let rental_options = if full_node_rental || !slice_formats.is_empty() { let mut builder = crate::models::builders::NodeRentalOptionsBuilder::new() .slice_rental_enabled(!slice_formats.is_empty()) .full_node_rental_enabled(full_node_rental) .minimum_rental_days(30) .auto_renewal_enabled(false); if full_node_rental { if let Ok(pricing) = crate::models::builders::FullNodePricingBuilder::new() .monthly(Decimal::from(500)) .auto_calculate(false) .build() { builder = builder.full_node_pricing(pricing); } } builder.build().ok() } else { None }; // Create the node via builder for consistency let mut node_builder = crate::models::builders::FarmNodeBuilder::new() .id(format!("grid_node_{}", grid_node_id)) .name(format!("Grid Node {}", grid_node_id)) .location(format!("{}, {}", if grid_data.city.is_empty() { "Unknown" } else { &grid_data.city }, if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country } )) .status(crate::models::user::NodeStatus::Online) .capacity(crate::models::user::NodeCapacity { cpu_cores: grid_data.total_resources.cpu_cores, memory_gb: grid_data.total_resources.memory_gb, storage_gb: grid_data.total_resources.storage_gb, hdd_storage_gb: 0, ssd_storage_gb: grid_data.total_resources.storage_gb, bandwidth_mbps: grid_data.total_resources.bandwidth_mbps, ram_gb: grid_data.total_resources.ram_gb, }) .used_capacity(crate::models::user::NodeCapacity { cpu_cores: grid_data.used_resources.cpu_cores, memory_gb: grid_data.used_resources.memory_gb, storage_gb: grid_data.used_resources.storage_gb, hdd_storage_gb: 0, ssd_storage_gb: grid_data.used_resources.storage_gb, bandwidth_mbps: grid_data.used_resources.bandwidth_mbps, ram_gb: grid_data.used_resources.ram_gb, }) .uptime_percentage(99.0) .earnings_today_usd(rust_decimal_macros::dec!(0)) .last_seen(Utc::now()) .health_score(95.0) .region(if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country }.to_string()) .node_type("MyceliumNode".to_string()) .grid_node_id(grid_node_id) .grid_data(grid_data) .availability_status(crate::models::user::NodeAvailabilityStatus::Available); if let Some(options) = rental_options { node_builder = node_builder.rental_options(options); } let mut node = node_builder.build().map_err(|e| format!("Failed to build node: {}", e))?; // Apply slice formats if provided if !slice_formats.is_empty() { node.slice_formats = Some(slice_formats.clone()); } persistent_data.nodes.push(node.clone()); added_nodes.push(node); } if !added_nodes.is_empty() { UserPersistence::save_user_data(&persistent_data).map_err(|e| format!("Failed to save user data: {}", e))?; } Ok(added_nodes) } /// Add multiple grid nodes with comprehensive rental configuration pub async fn add_multiple_grid_nodes_with_comprehensive_config(&self, user_email: &str, grid_node_ids: Vec, slice_formats: Vec, rental_options: Option) -> Result, String> { // Batch approach: load once, add all nodes, save once let mut added_nodes: Vec = Vec::new(); let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email); for grid_node_id in grid_node_ids { let node_id = format!("grid_node_{}", grid_node_id); // Skip duplicates (existing or already added in this batch) let exists_in_persistent = persistent_data.nodes.iter().any(|n| n.grid_node_id == Some(grid_node_id.to_string()) || n.id == node_id ); let exists_in_batch = added_nodes.iter().any(|n: &FarmNode| n.grid_node_id == Some(grid_node_id.to_string()) || n.id == node_id ); if exists_in_persistent || exists_in_batch { continue; } // Fetch node data from Grid let grid_data = self.grid_service.fetch_node_data(grid_node_id).await .map_err(|e| format!("Failed to fetch grid node data: {}", e))?; // Create the node using builder pattern let mut node_builder = crate::models::builders::FarmNodeBuilder::new() .id(format!("grid_node_{}", grid_node_id)) .name(format!("Grid Node {}", grid_node_id)) .location(format!("{}, {}", if grid_data.city.is_empty() { "Unknown" } else { &grid_data.city }, if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country } )) .status(crate::models::user::NodeStatus::Online) .capacity(crate::models::user::NodeCapacity { cpu_cores: grid_data.total_resources.cpu_cores, memory_gb: grid_data.total_resources.memory_gb, storage_gb: grid_data.total_resources.storage_gb, hdd_storage_gb: 0, ssd_storage_gb: grid_data.total_resources.storage_gb, bandwidth_mbps: grid_data.total_resources.bandwidth_mbps, ram_gb: grid_data.total_resources.ram_gb, }) .used_capacity(crate::models::user::NodeCapacity { cpu_cores: grid_data.used_resources.cpu_cores, memory_gb: grid_data.used_resources.memory_gb, storage_gb: grid_data.used_resources.storage_gb, hdd_storage_gb: 0, ssd_storage_gb: grid_data.used_resources.storage_gb, bandwidth_mbps: grid_data.used_resources.bandwidth_mbps, ram_gb: grid_data.used_resources.ram_gb, }) .uptime_percentage(99.0) .earnings_today_usd(rust_decimal_macros::dec!(0)) .last_seen(Utc::now()) .health_score(95.0) .region(if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country }.to_string()) .node_type("MyceliumNode".to_string()) .grid_node_id(grid_node_id) .grid_data(grid_data) .availability_status(crate::models::user::NodeAvailabilityStatus::Available); // Add rental options if provided if let Some(options) = rental_options.clone() { node_builder = node_builder.rental_options(options); } let mut node = node_builder.build() .map_err(|e| format!("Failed to build node: {}", e))?; // Set slice formats if !slice_formats.is_empty() { node.slice_formats = Some(slice_formats.clone()); } // Push to in-memory and batch list persistent_data.nodes.push(node.clone()); added_nodes.push(node); } if !added_nodes.is_empty() { UserPersistence::save_user_data(&persistent_data).map_err(|e| format!("Failed to save user data: {}", e))?; } Ok(added_nodes) } /// Add multiple grid nodes with individual pricing configuration pub async fn add_multiple_grid_nodes_with_individual_pricing(&self, user_email: &str, grid_node_ids: Vec, slice_formats: Vec, individual_pricing: std::collections::HashMap) -> Result, String> { // Batch approach: load once, add all nodes, save once let mut added_nodes: Vec = Vec::new(); let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email); for grid_node_id in grid_node_ids { let node_id = format!("grid_node_{}", grid_node_id); // Skip duplicates (existing or already added in this batch) let exists_in_persistent = persistent_data.nodes.iter().any(|n| n.grid_node_id == Some(grid_node_id.to_string()) || n.id == node_id ); let exists_in_batch = added_nodes.iter().any(|n: &FarmNode| n.grid_node_id == Some(grid_node_id.to_string()) || n.id == node_id ); if exists_in_persistent || exists_in_batch { continue; } // Create node key to match the frontend format let node_key = format!("grid_{}", grid_node_id); // Build rental options with individual pricing if provided let rental_options = if let Some(pricing) = individual_pricing.get(&node_key) { match crate::models::builders::NodeRentalOptionsBuilder::new() .slice_rental_enabled(false) .full_node_rental_enabled(true) .full_node_pricing(pricing.clone()) .minimum_rental_days(30) .auto_renewal_enabled(false) .build() { Ok(options) => Some(options), Err(e) => { return Err(format!("Failed to create rental options for node {}: {}", grid_node_id, e)); } } } else { None }; // Fetch node data from Grid let grid_data = self.grid_service.fetch_node_data(grid_node_id).await .map_err(|e| format!("Failed to fetch grid node data: {}", e))?; // Create the node let mut node_builder = crate::models::builders::FarmNodeBuilder::new() .id(format!("grid_node_{}", grid_node_id)) .name(format!("Grid Node {}", grid_node_id)) .location(format!("{}, {}", if grid_data.city.is_empty() { "Unknown" } else { &grid_data.city }, if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country } )) .status(crate::models::user::NodeStatus::Online) .capacity(crate::models::user::NodeCapacity { cpu_cores: grid_data.total_resources.cpu_cores, memory_gb: grid_data.total_resources.memory_gb, storage_gb: grid_data.total_resources.storage_gb, hdd_storage_gb: 0, ssd_storage_gb: grid_data.total_resources.storage_gb, bandwidth_mbps: grid_data.total_resources.bandwidth_mbps, ram_gb: grid_data.total_resources.ram_gb, }) .used_capacity(crate::models::user::NodeCapacity { cpu_cores: grid_data.used_resources.cpu_cores, memory_gb: grid_data.used_resources.memory_gb, storage_gb: grid_data.used_resources.storage_gb, hdd_storage_gb: 0, ssd_storage_gb: grid_data.used_resources.storage_gb, bandwidth_mbps: grid_data.used_resources.bandwidth_mbps, ram_gb: grid_data.used_resources.ram_gb, }) .uptime_percentage(99.0) .earnings_today_usd(rust_decimal_macros::dec!(0)) .last_seen(Utc::now()) .health_score(95.0) .region(if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country }.to_string()) .node_type("MyceliumNode".to_string()) .grid_node_id(grid_node_id) .grid_data(grid_data) .availability_status(crate::models::user::NodeAvailabilityStatus::Available); if let Some(options) = rental_options { node_builder = node_builder.rental_options(options); } let mut node = node_builder.build() .map_err(|e| format!("Failed to build node: {}", e))?; if !slice_formats.is_empty() { node.slice_formats = Some(slice_formats.clone()); } persistent_data.nodes.push(node.clone()); added_nodes.push(node); } if !added_nodes.is_empty() { UserPersistence::save_user_data(&persistent_data).map_err(|e| format!("Failed to save user data: {}", e))?; } Ok(added_nodes) } /// Add a single grid node with comprehensive rental configuration pub async fn add_grid_node_with_comprehensive_config(&self, user_email: &str, grid_node_id: u32, slice_formats: Vec, rental_options: Option) -> Result { // Fetch node data from Grid let grid_data = self.grid_service.fetch_node_data(grid_node_id).await .map_err(|e| format!("Failed to fetch grid node data: {}", e))?; // Create the node using builder pattern let mut node_builder = crate::models::builders::FarmNodeBuilder::new() .id(format!("grid_node_{}", grid_node_id)) .name(format!("Grid Node {}", grid_node_id)) .location(format!("{}, {}", if grid_data.city.is_empty() { "Unknown" } else { &grid_data.city }, if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country } )) .status(crate::models::user::NodeStatus::Online) .capacity(crate::models::user::NodeCapacity { cpu_cores: grid_data.total_resources.cpu_cores, memory_gb: grid_data.total_resources.memory_gb, storage_gb: grid_data.total_resources.storage_gb, hdd_storage_gb: 0, ssd_storage_gb: grid_data.total_resources.storage_gb, bandwidth_mbps: grid_data.total_resources.bandwidth_mbps, ram_gb: grid_data.total_resources.ram_gb, }) .used_capacity(crate::models::user::NodeCapacity { cpu_cores: grid_data.used_resources.cpu_cores, memory_gb: grid_data.used_resources.memory_gb, storage_gb: grid_data.used_resources.storage_gb, hdd_storage_gb: 0, ssd_storage_gb: grid_data.used_resources.storage_gb, bandwidth_mbps: grid_data.used_resources.bandwidth_mbps, ram_gb: grid_data.used_resources.ram_gb, }) .uptime_percentage(99.0) .earnings_today_usd(rust_decimal_macros::dec!(0)) .last_seen(Utc::now()) .health_score(95.0) .region(if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country }.to_string()) .node_type("MyceliumNode".to_string()) .grid_node_id(grid_node_id) .grid_data(grid_data) .availability_status(crate::models::user::NodeAvailabilityStatus::Available); // Add rental options if provided if let Some(options) = rental_options { node_builder = node_builder.rental_options(options); } let mut node = node_builder.build() .map_err(|e| format!("Failed to build node: {}", e))?; // Set slice formats if !slice_formats.is_empty() { node.slice_formats = Some(slice_formats); } // Add node to user's data let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email); // Check if node already exists if persistent_data.nodes.iter().any(|n| n.id == node.id) { return Err(format!("Node {} already exists", grid_node_id)); } persistent_data.nodes.push(node.clone()); // Save updated data UserPersistence::save_user_data(&persistent_data) .map_err(|e| format!("Failed to save user data: {}", e))?; Ok(node) } /// Add a single grid node with slice configuration (legacy method) pub async fn add_grid_node_with_slice_config(&self, user_email: &str, grid_node_id: u32, slice_formats: Vec, full_node_rental: bool) -> Result { // Fetch node data from Grid let grid_data = self.grid_service.fetch_node_data(grid_node_id).await .map_err(|e| format!("Failed to fetch grid node data: {}", e))?; // Create rental options based on configuration let rental_options = if full_node_rental || !slice_formats.is_empty() { let mut builder = crate::models::builders::NodeRentalOptionsBuilder::new() .slice_rental_enabled(!slice_formats.is_empty()) .full_node_rental_enabled(full_node_rental) .minimum_rental_days(30) .auto_renewal_enabled(false); // Add basic pricing if full node rental is enabled if full_node_rental { if let Ok(pricing) = crate::models::builders::FullNodePricingBuilder::new() .monthly(Decimal::from(500)) .auto_calculate(false) .build() { builder = builder.full_node_pricing(pricing); } } builder.build().ok() } else { None }; // Create the node let node = FarmNode { id: format!("grid_node_{}", grid_node_id), name: format!("Grid Node {}", grid_node_id), location: format!("{}, {}", if grid_data.city.is_empty() { "Unknown" } else { &grid_data.city }, if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country } ), status: crate::models::user::NodeStatus::Online, capacity: crate::models::user::NodeCapacity { cpu_cores: grid_data.total_resources.cpu_cores, memory_gb: grid_data.total_resources.memory_gb, storage_gb: grid_data.total_resources.storage_gb, bandwidth_mbps: grid_data.total_resources.bandwidth_mbps, ssd_storage_gb: grid_data.total_resources.ssd_storage_gb, hdd_storage_gb: grid_data.total_resources.hdd_storage_gb, ram_gb: grid_data.total_resources.ram_gb, }, used_capacity: crate::models::user::NodeCapacity { cpu_cores: 0, memory_gb: 0, storage_gb: 0, bandwidth_mbps: 0, ssd_storage_gb: 0, hdd_storage_gb: 0, ram_gb: 0, }, uptime_percentage: 99.0, farming_start_date: grid_data.last_updated, last_updated: grid_data.last_updated, utilization_7_day_avg: 0.0, slice_formats_supported: if slice_formats.is_empty() { Vec::new() } else { slice_formats.clone() }, earnings_today_usd: Decimal::ZERO, last_seen: Some(Utc::now()), health_score: 100.0, region: if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country }.to_string(), node_type: "MyceliumNode".to_string(), slice_formats: if slice_formats.is_empty() { None } else { Some(slice_formats.clone()) }, rental_options: rental_options.map(|ro| serde_json::to_value(&ro).unwrap_or_default()), staking_options: None, availability_status: crate::models::user::NodeAvailabilityStatus::Available, grid_node_id: Some(grid_node_id.to_string()), grid_data: Some(serde_json::to_value(&grid_data).unwrap_or_default()), node_group_id: None, group_assignment_date: None, group_slice_format: None, group_slice_price: None, // NEW: Marketplace SLA field (None for basic grid nodes) marketplace_sla: None, total_base_slices: 0, allocated_base_slices: 0, slice_allocations: Vec::new(), available_combinations: Vec::new(), slice_pricing: Some(serde_json::to_value(&crate::services::slice_calculator::SlicePricing::default()).unwrap_or_default()), slice_last_calculated: None, }; // Save to persistent storage let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email); persistent_data.nodes.push(node.clone()); UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; // Auto-generate marketplace products if rental options are configured if let Some(rental_opts) = &node.rental_options { let slice_rental_enabled = rental_opts .get("slice_rental_enabled") .and_then(|enabled| enabled.as_bool()) .unwrap_or(false); let full_node_rental_enabled = rental_opts .get("full_node_rental_enabled") .and_then(|enabled| enabled.as_bool()) .unwrap_or(false); if slice_rental_enabled || full_node_rental_enabled { let resource_provider_name = persistent_data.name.unwrap_or_else(|| "Unknown ResourceProvider".to_string()); self.auto_generate_marketplace_products(&node, user_email, &resource_provider_name, None)?; } } Ok(node) } // ============================================================================= // NODE GROUP MANAGEMENT // ============================================================================= /// Ensure default node groups exist for a resource_provider pub fn ensure_default_node_groups(&self, user_email: &str) -> Result<(), String> { let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email); let default_groups = vec![ crate::models::user::DefaultGroupType::Compute, crate::models::user::DefaultGroupType::Storage, crate::models::user::DefaultGroupType::AiGpu, ]; for default_type in default_groups { let group_id = default_type.get_id(); if !persistent_data.node_groups.iter().any(|g| g.id == group_id) { let group = crate::models::builders::NodeGroupBuilder::new() .default_group(default_type) .build()?; persistent_data.node_groups.push(group); } } UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(()) } /// Create a custom node group pub fn create_custom_node_group(&self, user_email: &str, name: String, description: Option, config: Option) -> Result { let group = crate::models::builders::NodeGroupBuilder::new() .custom_group(format!("group_{}", &uuid::Uuid::new_v4().to_string()[..8]), name) .description(description.unwrap_or_else(|| "Custom node group".to_string())) .group_config(config.unwrap_or_default()) .build()?; let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; persistent_data.node_groups.push(group.clone()); UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(group) } /// Get all node groups for a resource_provider (ensures defaults exist) pub fn get_node_groups(&self, user_email: &str) -> Vec { // Ensure default groups exist first if let Err(e) = self.ensure_default_node_groups(user_email) { } if let Some(data) = UserPersistence::load_user_data(user_email) { data.node_groups } else { Vec::new() } } /// Get node group statistics pub fn get_group_statistics(&self, user_email: &str, group_id: &str) -> Result { let persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; let group = persistent_data.node_groups.iter() .find(|g| g.id == group_id) .ok_or("Node group not found")?; // FIXED: Use both approaches to find nodes - check both node.node_group_id and group.node_ids // This handles data inconsistencies where nodes think they belong to a group but the group doesn't list them let group_nodes: Vec<&FarmNode> = persistent_data.nodes.iter() .filter(|n| { // Node is in group if either: // 1. The group lists the node in its node_ids array, OR // 2. The node has this group_id set as its node_group_id group.node_ids.contains(&n.id) || n.node_group_id.as_ref() == Some(&group_id.to_string()) }) .collect(); let total_nodes = group_nodes.len() as i32; let online_nodes = group_nodes.iter() .filter(|n| matches!(n.status, crate::models::user::NodeStatus::Online)) .count() as i32; // Calculate total resources let mut total_capacity = crate::models::user::NodeCapacity { cpu_cores: 0, memory_gb: 0, storage_gb: 0, bandwidth_mbps: 0, ssd_storage_gb: 0, hdd_storage_gb: 0, ram_gb: 0, }; for node in &group_nodes { total_capacity.cpu_cores += node.capacity.cpu_cores; total_capacity.memory_gb += node.capacity.memory_gb; total_capacity.storage_gb += node.capacity.storage_gb; total_capacity.bandwidth_mbps += node.capacity.bandwidth_mbps; total_capacity.ssd_storage_gb += node.capacity.ssd_storage_gb; total_capacity.hdd_storage_gb += node.capacity.hdd_storage_gb; } let average_uptime = if group_nodes.is_empty() { 0.0 } else { group_nodes.iter().map(|n| n.uptime_percentage).sum::() / group_nodes.len() as f32 }; Ok(GroupStatistics { group_name: group.name.clone(), group_id: group_id.to_string(), total_nodes, active_nodes: online_nodes, online_nodes, total_earnings_usd: rust_decimal::Decimal::from(0), total_capacity, average_uptime, group_type: group.group_type.clone(), }) } /// Assign node to group (or remove from group if group_id is None) pub fn assign_node_to_group(&self, user_email: &str, node_id: &str, group_id: Option) -> Result<(), String> { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; // Find the node let node = persistent_data.nodes.iter_mut() .find(|n| n.id == node_id) .ok_or("Node not found")?; // Remove node from current group if it has one if let Some(current_group_id) = &node.node_group_id { if let Some(current_group) = persistent_data.node_groups.iter_mut() .find(|g| g.id == *current_group_id) { current_group.node_ids.retain(|id| id != node_id); current_group.updated_at = Utc::now(); } } // Assign to new group if specified if let Some(new_group_id) = group_id { let group = persistent_data.node_groups.iter_mut() .find(|g| g.id == new_group_id) .ok_or("Node group not found")?; // Add node to group if !group.node_ids.contains(&node_id.to_string()) { group.node_ids.push(node_id.to_string()); } // Update node with group information node.node_group_id = Some(new_group_id.clone()); node.group_assignment_date = Some(Utc::now()); // Update group timestamp group.updated_at = Utc::now(); } else { // Remove from group (set to "Single") node.node_group_id = None; node.group_assignment_date = None; } UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(()) } /// Delete a custom node group (cannot delete default groups) pub fn delete_custom_node_group(&self, user_email: &str, group_id: &str) -> Result<(), String> { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; // Find the group and check if it's custom let group_index = persistent_data.node_groups.iter() .position(|g| g.id == group_id) .ok_or("Node group not found")?; let group = &persistent_data.node_groups[group_index]; // Check if it's a default group if matches!(group.group_type, crate::models::user::NodeGroupType::Default(_)) { return Err("Cannot delete default node groups".to_string()); } // Remove nodes from this group for node in persistent_data.nodes.iter_mut() { if node.node_group_id.as_ref() == Some(&group_id.to_string()) { node.node_group_id = None; node.group_assignment_date = None; } } // Remove the group persistent_data.node_groups.remove(group_index); UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(()) } /// Remove node from group pub fn remove_node_from_group(&self, user_email: &str, node_id: &str) -> Result<(), String> { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; // Find the node let node = persistent_data.nodes.iter_mut() .find(|n| n.id == node_id) .ok_or("Node not found")?; if let Some(group_id) = &node.node_group_id { // Find and update the group if let Some(group) = persistent_data.node_groups.iter_mut().find(|g| g.id == *group_id) { group.node_ids.retain(|id| id != node_id); group.updated_at = Utc::now(); } // Clear group information from node node.node_group_id = None; node.group_slice_format = None; node.group_slice_price = None; UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; } Ok(()) } /// Get nodes in a specific group pub fn get_nodes_in_group(&self, user_email: &str, group_id: &str) -> Vec { if let Some(data) = UserPersistence::load_user_data(user_email) { if let Some(group) = data.node_groups.iter().find(|g| g.id == group_id) { return data.nodes.into_iter() .filter(|node| group.node_ids.contains(&node.id)) .collect(); } } Vec::new() } /// Update group settings pub fn update_group_settings(&self, user_email: &str, group_id: &str, name: Option, description: Option, slice_format: Option, slice_price: Option) -> Result<(), String> { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; let group = persistent_data.node_groups.iter_mut() .find(|g| g.id == group_id) .ok_or("Node group not found")?; if let Some(name) = name { group.name = name; } if let Some(description) = description { group.description = Some(description); } if let Some(slice_format) = slice_format { // Update group config with new slice format if !group.group_config.preferred_slice_formats.contains(&slice_format) { group.group_config.preferred_slice_formats.push(slice_format.clone()); } // Update all nodes in the group (legacy support) for node_id in &group.node_ids { if let Some(node) = persistent_data.nodes.iter_mut().find(|n| n.id == *node_id) { node.group_slice_format = Some(slice_format.clone()); } } } if let Some(slice_price) = slice_price { // Update group config with new pricing if group.group_config.default_pricing.is_none() { group.group_config.default_pricing = Some(serde_json::json!({})); } if let Some(ref mut pricing) = group.group_config.default_pricing { if let Some(obj) = pricing.as_object_mut() { obj.insert("default".to_string(), serde_json::json!(slice_price)); } } // Update all nodes in the group (legacy support) for node_id in &group.node_ids { if let Some(node) = persistent_data.nodes.iter_mut().find(|n| n.id == *node_id) { node.group_slice_price = Some(slice_price); } } } group.updated_at = Utc::now(); UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(()) } /// Get the display name for a node's group (or "Single" if no group) pub fn get_node_group_display_name(&self, user_email: &str, node: &FarmNode) -> String { if let Some(group_id) = &node.node_group_id { if let Some(data) = UserPersistence::load_user_data(user_email) { if let Some(group) = data.node_groups.iter().find(|g| g.id == *group_id) { return group.name.clone(); } } } "Single".to_string() } /// Get node by name (for duplicate checking) pub fn get_node_by_name(&self, user_email: &str, node_name: &str) -> Option { let nodes = self.get_resource_provider_nodes(user_email); nodes.into_iter().find(|n| n.name == node_name) } /// Update node group assignment with proper group name display pub fn update_node_group_assignment(&self, user_email: &str, node_id: &str, group_id: Option) -> Result { self.assign_node_to_group(user_email, node_id, group_id.clone())?; // Return the display name for the group if let Some(gid) = group_id { if let Some(data) = UserPersistence::load_user_data(user_email) { if let Some(group) = data.node_groups.iter().find(|g| g.id == gid) { return Ok(group.name.clone()); } } Ok("Unknown Group".to_string()) } else { Ok("Single".to_string()) } } /// Repair data inconsistencies between nodes and groups /// This ensures that if a node has a node_group_id, the group also lists the node in its node_ids /// AND removes any phantom node references from groups that don't exist in the user's actual nodes pub fn repair_node_group_consistency(&self, user_email: &str) -> Result<(), String> { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; let mut changes_made = false; // Get the actual node IDs that exist for this user let actual_node_ids: std::collections::HashSet = persistent_data.nodes .iter() .map(|n| n.id.clone()) .collect(); // First pass: Clean up all groups - remove any node_ids that don't correspond to actual nodes // AND remove nodes that exist but are assigned to different groups for group in &mut persistent_data.node_groups { let original_node_ids = group.node_ids.clone(); let original_count = group.node_ids.len(); // Only keep node IDs that: // 1. Actually exist in the user's nodes, AND // 2. Have this group as their assigned group (node_group_id matches) group.node_ids.retain(|node_id| { // Check if node exists if !actual_node_ids.contains(node_id) { return false; } // Check if node is actually assigned to this group if let Some(node) = persistent_data.nodes.iter().find(|n| &n.id == node_id) { if let Some(node_group_id) = &node.node_group_id { if node_group_id != &group.id { return false; } } else { return false; } } true }); if group.node_ids.len() != original_count { group.updated_at = Utc::now(); changes_made = true; } } // Second pass: For each actual node with a group assignment, ensure the group lists the node for node in &persistent_data.nodes { if let Some(node_group_id) = &node.node_group_id { if let Some(group) = persistent_data.node_groups.iter_mut() .find(|g| g.id == *node_group_id) { if !group.node_ids.contains(&node.id) { group.node_ids.push(node.id.clone()); group.updated_at = Utc::now(); changes_made = true; } } else { // Note: We can't modify the node here since we're iterating over nodes // This will be handled in the third pass } } } // Third pass: Clean up nodes that reference non-existent groups for node in &mut persistent_data.nodes { if let Some(node_group_id) = &node.node_group_id { if !persistent_data.node_groups.iter().any(|g| g.id == *node_group_id) { node.node_group_id = None; node.group_assignment_date = None; changes_made = true; } } } if changes_made { UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; // Log final state for verification for group in &persistent_data.node_groups { } } else { } Ok(()) } /// Create default user data for new resource providers fn create_default_user_data(user_email: &str) -> UserPersistentData { crate::models::builders::SessionDataBuilder::new_user(user_email) } /// Refresh all slice calculations for a resource_provider pub fn refresh_all_slice_calculations(&self, user_email: &str) -> Result<(), String> { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; let mut updated_count = 0; for node in &mut persistent_data.nodes { // Recalculate slices using the slice calculator service let max_base_slices = self.slice_calculator.calculate_max_base_slices(&node.capacity); node.total_base_slices = max_base_slices as i32; // Regenerate available combinations let combinations = self.slice_calculator.generate_slice_combinations( node.total_base_slices as u32, node.allocated_base_slices as u32, node, user_email ); node.available_combinations = combinations.iter() .map(|c| serde_json::to_value(c).unwrap_or_default()) .collect(); node.slice_last_calculated = Some(Utc::now()); updated_count += 1; } UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(()) } /// Sync all nodes with ThreeFold Grid pub fn sync_all_nodes_with_grid(&self, user_email: &str) -> Result { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; let mut synced_count = 0; for node in &mut persistent_data.nodes { if let Some(ref grid_node_id) = node.grid_node_id { // Use actix-web's runtime to handle the async grid service call let rt = actix_web::rt::Runtime::new() .map_err(|e| format!("Failed to create async runtime: {}", e))?; let grid_id = grid_node_id.parse::().unwrap_or(0); match rt.block_on(self.grid_service.fetch_node_data(grid_id)) { Ok(grid_data) => { // Update node with fresh grid data node.capacity = grid_data.total_resources.clone(); node.used_capacity = grid_data.used_resources.clone(); node.grid_data = Some(serde_json::to_value(grid_data).unwrap_or_default()); // Note: last_grid_sync field doesn't exist, using slice_last_calculated as proxy node.slice_last_calculated = Some(Utc::now()); synced_count += 1; } Err(_e) => { } } } } UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(synced_count) } /// Get slices for a specific node pub fn get_node_slices(&self, user_email: &str, node_id: u32) -> Result, String> { let persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; let node = persistent_data.nodes.iter() .find(|n| n.grid_node_id == Some(node_id.to_string())) .ok_or("Node not found")?; // Calculate available slices using the slice calculator let max_base_slices = self.slice_calculator.calculate_max_base_slices(&node.capacity); let available_base_slices = max_base_slices.saturating_sub(node.allocated_base_slices as u32); let mut slices = Vec::new(); // Create slice objects with different multipliers for multiplier in &[1, 2, 4, 8] { let available_count = available_base_slices / multiplier; if available_count > 0 { let slice = serde_json::json!({ "multiplier": multiplier, "cpu_cores": 1 * multiplier, // Base slice: 1 vCPU "memory_gb": 4 * multiplier, // Base slice: 4GB RAM "storage_gb": 200 * multiplier, // Base slice: 200GB storage "available_count": available_count, "price_per_hour": node.slice_pricing .as_ref() .and_then(|pricing| pricing.get("base_price_per_hour")) .and_then(|price| price.as_str()) .and_then(|price_str| rust_decimal::Decimal::from_str(price_str).ok()) .unwrap_or_else(|| rust_decimal::Decimal::try_from(0.50).unwrap_or_default()) * rust_decimal::Decimal::from(*multiplier), "node_location": &node.location, "node_uptime": node.uptime_percentage, "node_certification": if let Some(grid_data) = &node.grid_data { grid_data.get("certification_type") .and_then(|cert| cert.as_str()) .unwrap_or("Standard") } else { "Standard" } }); slices.push(slice); } } Ok(slices) } /// Get comprehensive slice statistics for a resource_provider pub fn get_slice_statistics(&self, user_email: &str) -> Result { let persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; let mut total_base_slices = 0; let mut total_2x_slices = 0; let mut total_4x_slices = 0; let mut total_8x_slices = 0; let mut rented_slices = 0; let mut total_earnings = rust_decimal::Decimal::ZERO; let mut node_breakdown = Vec::new(); for node in &persistent_data.nodes { let max_base_slices = self.slice_calculator.calculate_max_base_slices(&node.capacity); let available_base_slices = max_base_slices.saturating_sub(node.allocated_base_slices as u32); total_base_slices += available_base_slices; total_2x_slices += available_base_slices / 2; total_4x_slices += available_base_slices / 4; total_8x_slices += available_base_slices / 8; // Calculate earnings from this node let node_earnings = node.earnings_today_usd; total_earnings += node_earnings; // Add to node breakdown let node_info = serde_json::json!({ "node_id": node.id, "node_name": node.name, "location": node.location, "total_base_slices": max_base_slices, "available_base_slices": available_base_slices, "allocated_base_slices": node.allocated_base_slices, "slice_combinations": { "1x": available_base_slices, "2x": available_base_slices / 2, "4x": available_base_slices / 4, "8x": available_base_slices / 8 }, "earnings_today": node_earnings, "uptime": node.uptime_percentage, "status": node.status }); node_breakdown.push(node_info); } // Count rented slices from active rentals rented_slices = persistent_data.slice_rentals.len() as u32; let statistics = serde_json::json!({ "total_slices": { "base_1x": total_base_slices, "combo_2x": total_2x_slices, "combo_4x": total_4x_slices, "combo_8x": total_8x_slices }, "availability": { "available_base": total_base_slices - rented_slices, "rented": rented_slices, "utilization_percentage": if total_base_slices > 0 { (rented_slices as f64 / total_base_slices as f64) * 100.0 } else { 0.0 } }, "earnings": { "today": total_earnings, "currency": "TFT" }, "nodes": { "total_count": persistent_data.nodes.len(), "online_count": persistent_data.nodes.iter() .filter(|n| matches!(n.status, NodeStatus::Online)) .count(), "breakdown": node_breakdown }, "last_updated": Utc::now() }); Ok(statistics) } /// Fetch and validate a grid node for automatic slice management pub async fn fetch_and_validate_grid_node(&self, grid_node_id: u32) -> Result { // Validate node exists on grid if !self.grid_service.validate_node_exists(grid_node_id).await? { return Err(format!("Node {} does not exist on ThreeFold Grid", grid_node_id)); } // Fetch node data from grid let grid_data = self.grid_service.fetch_node_data(grid_node_id).await?; Ok(grid_data) } /// Add multiple grid nodes with automatic slice management pub async fn add_multiple_grid_nodes_with_automatic_slices( &self, user_email: &str, grid_node_ids: Vec, base_slice_price: rust_decimal::Decimal, node_uptime_sla: f32, node_bandwidth_sla: i32, node_group: Option, enable_staking: bool, enable_full_node_rental: bool, ) -> Result, String> { let mut added_nodes = Vec::new(); // Load user data once at the beginning to maintain consistency let mut persistent_data = UserPersistence::load_user_data(user_email) .unwrap_or_else(|| Self::create_default_user_data(user_email)); for grid_node_id in grid_node_ids { // Enhanced duplicate checking: Check both existing data and nodes being added in this batch let node_id = format!("grid_node_{}", grid_node_id); // Check existing nodes in persistent data let exists_in_persistent = persistent_data.nodes.iter().any(|n| n.grid_node_id == Some(grid_node_id.to_string()) || n.id == node_id ); // Check nodes already added in this batch let exists_in_batch = added_nodes.iter().any(|n: &FarmNode| n.grid_node_id == Some(grid_node_id.to_string()) || n.id == node_id ); if exists_in_persistent || exists_in_batch { continue; } // Fetch and validate grid node match self.fetch_and_validate_grid_node(grid_node_id).await { Ok(grid_data) => { // Calculate automatic slices let total_base_slices = self.slice_calculator.calculate_max_base_slices(&grid_data.total_resources); // Create a temporary node with USER SLA values for slice combination generation let temp_node_for_slices = FarmNode { id: format!("grid_node_{}", grid_node_id), name: format!("Grid Node {}", grid_node_id), location: format!("{}, {}", if grid_data.city.is_empty() { "Unknown City" } else { &grid_data.city }, if grid_data.country.is_empty() { "Unknown Country" } else { &grid_data.country } ), status: NodeStatus::Online, capacity: NodeCapacity { cpu_cores: grid_data.total_resources.cpu_cores, memory_gb: grid_data.total_resources.memory_gb, storage_gb: grid_data.total_resources.storage_gb, bandwidth_mbps: node_bandwidth_sla as i32, // USER INPUT: Use SLA bandwidth ssd_storage_gb: grid_data.total_resources.ssd_storage_gb, hdd_storage_gb: grid_data.total_resources.hdd_storage_gb, ram_gb: grid_data.total_resources.ram_gb, }, used_capacity: NodeCapacity { cpu_cores: 0, memory_gb: 0, storage_gb: 0, bandwidth_mbps: 0, ssd_storage_gb: 0, hdd_storage_gb: 0, ram_gb: 0, }, uptime_percentage: node_uptime_sla, // USER INPUT: Use SLA uptime farming_start_date: grid_data.last_updated, last_updated: grid_data.last_updated, utilization_7_day_avg: 0.0, slice_formats_supported: Vec::new(), earnings_today_usd: rust_decimal::Decimal::ZERO, last_seen: Some(Utc::now()), health_score: 100.0, region: if grid_data.country.is_empty() { "Unknown".to_string() } else { grid_data.country.clone() }, node_type: "MyceliumNode".to_string(), slice_formats: None, rental_options: { let rental_opts = crate::models::user::NodeRentalOptions { full_node_available: enable_full_node_rental, slice_formats: vec!["1x1".to_string(), "2x2".to_string(), "4x4".to_string()], pricing: crate::models::user::FullNodePricing { monthly_cost: rust_decimal::Decimal::try_from(100.0).unwrap_or_default(), ..Default::default() }, slice_rental_enabled: true, full_node_rental_enabled: enable_full_node_rental, full_node_pricing: None, minimum_rental_days: 1, maximum_rental_days: Some(365), // 1 year auto_renewal_enabled: true, }; Some(serde_json::to_value(&rental_opts).unwrap_or_default()) }, staking_options: if enable_staking { { let staking_opts = crate::models::user::NodeStakingOptions { enabled: true, minimum_stake: rust_decimal::Decimal::new(100, 0), // $100 minimum reward_rate: 5.0, // 5% APR staking_enabled: true, staked_amount: rust_decimal::Decimal::ZERO, staking_start_date: None, staking_period_months: 12, early_withdrawal_allowed: false, early_withdrawal_penalty_percent: 10.0, }; Some(serde_json::to_value(&staking_opts).unwrap_or_default()) } } else { None }, availability_status: crate::models::user::NodeAvailabilityStatus::Available, grid_node_id: Some(grid_node_id.to_string()), grid_data: Some(serde_json::to_value(&grid_data).unwrap_or_default()), node_group_id: node_group.clone(), group_assignment_date: if node_group.is_some() { Some(Utc::now()) } else { None }, group_slice_format: None, group_slice_price: None, // NEW: Marketplace SLA field (None for temp node) marketplace_sla: None, total_base_slices: total_base_slices as i32, allocated_base_slices: 0, slice_allocations: Vec::new(), available_combinations: Vec::new(), // Will be set below slice_pricing: Some(serde_json::to_value(&SlicePricing { base_price_per_hour: base_slice_price, // USER INPUT: Use SLA pricing currency: "USD".to_string(), pricing_multiplier: rust_decimal::Decimal::from(1), }).unwrap_or_default()), slice_last_calculated: Some(Utc::now()), }; // Generate slice combinations with USER SLA values let available_combinations = self.slice_calculator.generate_slice_combinations_with_sla( total_base_slices, 0, // No allocated slices yet &temp_node_for_slices, user_email, node_uptime_sla as f64, // USER INPUT: Use SLA uptime node_bandwidth_sla as u32, // USER INPUT: Use SLA bandwidth base_slice_price // USER INPUT: Use SLA pricing ); // Create the actual node let node = FarmNode { id: format!("grid_node_{}", grid_node_id), name: format!("Grid Node {}", grid_node_id), location: format!("{}, {}", if grid_data.city.is_empty() { "Unknown City" } else { &grid_data.city }, if grid_data.country.is_empty() { "Unknown Country" } else { &grid_data.country } ), status: NodeStatus::Online, capacity: NodeCapacity { cpu_cores: grid_data.total_resources.cpu_cores, memory_gb: grid_data.total_resources.memory_gb, storage_gb: grid_data.total_resources.storage_gb, bandwidth_mbps: node_bandwidth_sla as i32, // USER INPUT: Use SLA bandwidth ssd_storage_gb: grid_data.total_resources.ssd_storage_gb, hdd_storage_gb: grid_data.total_resources.hdd_storage_gb, ram_gb: grid_data.total_resources.ram_gb, }, used_capacity: NodeCapacity { cpu_cores: 0, memory_gb: 0, storage_gb: 0, bandwidth_mbps: 0, ssd_storage_gb: 0, hdd_storage_gb: 0, ram_gb: 0, }, uptime_percentage: node_uptime_sla, farming_start_date: grid_data.last_updated, last_updated: grid_data.last_updated, utilization_7_day_avg: 0.0, slice_formats_supported: vec!["1x1".to_string(), "2x2".to_string(), "4x4".to_string()], earnings_today_usd: rust_decimal::Decimal::ZERO, last_seen: Some(Utc::now()), health_score: 100.0, region: if grid_data.country.is_empty() { "Unknown".to_string() } else { grid_data.country.clone() }, node_type: "MyceliumNode".to_string(), slice_formats: None, rental_options: Some(serde_json::to_value(&crate::models::user::NodeRentalOptions { full_node_available: enable_full_node_rental, slice_formats: vec!["1x1".to_string(), "2x2".to_string(), "4x4".to_string()], pricing: crate::models::user::FullNodePricing { monthly_cost: rust_decimal::Decimal::try_from(100.0).unwrap_or_default(), ..Default::default() }, slice_rental_enabled: true, minimum_rental_days: 1, maximum_rental_days: Some(365), full_node_rental_enabled: enable_full_node_rental, full_node_pricing: None, auto_renewal_enabled: false, }).unwrap_or_default()), staking_options: if enable_staking { { let staking_opts = crate::models::user::NodeStakingOptions { enabled: true, minimum_stake: rust_decimal::Decimal::new(100, 0), // $100 minimum reward_rate: 5.0, // 5% APR staking_enabled: true, staked_amount: rust_decimal::Decimal::ZERO, staking_start_date: None, staking_period_months: 12, early_withdrawal_allowed: false, early_withdrawal_penalty_percent: 10.0, }; Some(serde_json::to_value(&staking_opts).unwrap_or_default()) } } else { None }, // NEW: Store marketplace SLA separately from grid data marketplace_sla: { let sla = crate::models::user::MarketplaceSLA { id: format!("sla-node-{}", node_id), name: "Node SLA".to_string(), uptime_guarantee: node_uptime_sla, response_time_hours: 24, resolution_time_hours: 48, penalty_rate: 0.01, uptime_guarantee_percentage: node_uptime_sla, bandwidth_guarantee_mbps: node_bandwidth_sla as f32, base_slice_price, last_updated: Utc::now(), }; Some(sla) }, availability_status: crate::models::user::NodeAvailabilityStatus::Available, grid_node_id: Some(grid_node_id.to_string()), grid_data: Some(serde_json::to_value(&grid_data).unwrap_or_default()), node_group_id: node_group.clone(), group_assignment_date: if node_group.is_some() { Some(Utc::now()) } else { None }, group_slice_format: None, group_slice_price: None, total_base_slices: total_base_slices as i32, allocated_base_slices: 0, slice_allocations: Vec::new(), available_combinations: available_combinations.iter() .map(|c| serde_json::to_value(c).unwrap_or_default()) .collect(), slice_pricing: Some(serde_json::to_value(&SlicePricing { base_price_per_hour: base_slice_price, currency: "USD".to_string(), pricing_multiplier: rust_decimal::Decimal::from(1), }).unwrap_or_default()), slice_last_calculated: Some(Utc::now()), }; // Add node to in-memory data structure (no duplicate checking needed here since we already checked) persistent_data.nodes.push(node.clone()); added_nodes.push(node.clone()); // Debug: Log the marketplace SLA data that was saved if let Some(ref sla) = node.marketplace_sla { } else { } } Err(e) => { return Err(format!("Failed to add node {}: {}", grid_node_id, e)); } } } // Save all changes atomically at the end if !added_nodes.is_empty() { match UserPersistence::save_user_data(&persistent_data) { Ok(_) => { // Add a small delay to ensure file system consistency std::thread::sleep(std::time::Duration::from_millis(100)); // Verify the save by reloading and checking if let Some(reloaded_data) = UserPersistence::load_user_data(user_email) { let saved_node_count = reloaded_data.nodes.len(); // Check that our new nodes are actually saved let new_node_ids: Vec = added_nodes.iter().map(|n| n.id.clone()).collect(); let found_nodes: Vec = reloaded_data.nodes.iter() .filter(|n| new_node_ids.contains(&n.id)) .map(|n| n.id.clone()) .collect(); if found_nodes.len() == added_nodes.len() { } else { } } else { } } Err(e) => { return Err(format!("Failed to save nodes: {}", e)); } } } else { } Ok(added_nodes) } /// Repair nodes with missing marketplace SLA data pub fn repair_missing_marketplace_sla( &self, user_email: &str, default_uptime_sla: f32, default_bandwidth_sla: i32, default_base_price: rust_decimal::Decimal, ) -> Result { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; let mut repaired_count = 0; for node in &mut persistent_data.nodes { if node.marketplace_sla.is_none() { // Create marketplace SLA with default values node.marketplace_sla = Some(MarketplaceSLA { id: format!("sla-repair-{}", uuid::Uuid::new_v4()), name: "Repaired Node SLA".to_string(), uptime_guarantee: default_uptime_sla, response_time_hours: 24, resolution_time_hours: 48, penalty_rate: 0.01, uptime_guarantee_percentage: default_uptime_sla, bandwidth_guarantee_mbps: default_bandwidth_sla as f32, base_slice_price: default_base_price, last_updated: Utc::now(), }); repaired_count += 1; } } if repaired_count > 0 { UserPersistence::save_user_data(&persistent_data) .map_err(|e| format!("Failed to save repaired data: {}", e))?; } Ok(repaired_count) } /// Refresh all slice calculations for a resource_provider (async version) pub async fn refresh_all_slice_calculations_async(&self, user_email: &str) -> Result { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; let mut updated_count = 0; for node in &mut persistent_data.nodes { // Recalculate slices using the slice calculator service let max_base_slices = self.slice_calculator.calculate_max_base_slices(&node.capacity); node.total_base_slices = max_base_slices as i32; // Regenerate available combinations let combinations = self.slice_calculator.generate_slice_combinations( node.total_base_slices as u32, node.allocated_base_slices as u32, node, user_email ); node.available_combinations = combinations.iter() .map(|c| serde_json::to_value(c).unwrap_or_default()) .collect(); node.slice_last_calculated = Some(Utc::now()); updated_count += 1; } UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(updated_count) } /// Sync all nodes with ThreeFold Grid (async version) pub async fn sync_all_nodes_with_grid_async(&self, user_email: &str) -> Result { let mut persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; let mut synced_count = 0; for node in &mut persistent_data.nodes { if let Some(grid_node_id) = &node.grid_node_id { let grid_id: u32 = grid_node_id.parse().unwrap_or(0); match self.grid_service.fetch_node_data(grid_id).await { Ok(grid_data) => { // Update node with fresh grid data node.capacity = grid_data.total_resources.clone(); node.used_capacity = grid_data.used_resources.clone(); node.grid_data = Some(serde_json::to_value(&grid_data).unwrap_or_default()); node.slice_last_calculated = Some(Utc::now()); synced_count += 1; } Err(e) => { } } } } UserPersistence::save_user_data(&persistent_data) .map_err(|e| e.to_string())?; Ok(synced_count) } /// Get node slice details by node ID string pub fn get_node_slice_details(&self, user_email: &str, node_id: &str) -> Result { let persistent_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; let node = persistent_data.nodes.iter() .find(|n| n.id == node_id) .ok_or("Node not found")?; // Calculate available slices using the slice calculator let max_base_slices = self.slice_calculator.calculate_max_base_slices(&node.capacity); let available_base_slices = max_base_slices.saturating_sub(node.allocated_base_slices as u32); let mut slices = Vec::new(); // Create slice objects with different multipliers for multiplier in &[1, 2, 4, 8] { let available_count = available_base_slices / multiplier; if available_count > 0 { let slice = serde_json::json!({ "multiplier": multiplier, "cpu_cores": 1 * multiplier, // Base slice: 1 vCPU "memory_gb": 4 * multiplier, // Base slice: 4GB RAM "storage_gb": 200 * multiplier, // Base slice: 200GB storage "available_count": available_count, "price_per_hour": node.slice_pricing .as_ref() .and_then(|pricing| pricing.get("base_price_per_hour")) .and_then(|price| price.as_str()) .and_then(|price_str| rust_decimal::Decimal::from_str(price_str).ok()) .unwrap_or_else(|| rust_decimal::Decimal::try_from(0.50).unwrap_or_default()) * rust_decimal::Decimal::from(*multiplier), "node_location": &node.location, "node_uptime": node.uptime_percentage, "node_certification": if let Some(grid_data) = &node.grid_data { grid_data.get("certification_type") .and_then(|cert| cert.as_str()) .unwrap_or("Standard") } else { "Standard" } }); slices.push(slice); } } let result = serde_json::json!({ "node_id": node.id, "node_name": node.name, "location": node.location, "total_base_slices": max_base_slices, "available_base_slices": available_base_slices, "allocated_base_slices": node.allocated_base_slices, "slice_configurations": slices, "last_calculated": node.slice_last_calculated }); Ok(result) } } /// Data structure for creating new nodes #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeCreationData { pub name: String, pub location: String, pub cpu_cores: i32, pub memory_gb: i32, pub storage_gb: i32, pub bandwidth_mbps: i32, pub region: Option, pub node_type: Option, pub slice_formats: Option>, // Selected slice formats for this node (DEPRECATED) /// Node rental configuration options pub rental_options: Option, /// Slice pricing configuration (DEPRECATED - replaced by slice_pricing) pub slice_prices: Option>, /// NEW: Automatic slice pricing configuration pub slice_pricing: Option, } /// Data structure for updating nodes #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeUpdateData { pub name: Option, pub location: Option, pub region: Option, pub status: Option, pub capacity: Option, pub marketplace_sla: Option, } /// Default slice formats available to all resource providers #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DefaultSliceFormat { pub id: String, pub name: String, pub cpu_cores: i32, pub memory_gb: i32, pub storage_gb: i32, pub bandwidth_mbps: i32, pub description: String, pub price_per_hour: rust_decimal::Decimal, } /// ResourceProvider statistics summary #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FarmerStatistics { pub total_nodes: i32, pub online_nodes: i32, pub total_capacity: NodeCapacity, pub used_capacity: NodeCapacity, pub monthly_earnings: i32, pub uptime_percentage: f32, pub total_base_slices: i32, pub allocated_base_slices: i32, }