Files
projectmycelium/src/services/resource_provider.rs

2999 lines
137 KiB
Rust

//! 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<DateTime<Utc>>,
}
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<bool>,
metrics_collection: Option<bool>,
grid_service: Option<GridService>,
slice_calculator: Option<SliceCalculatorService>,
}
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<ResourceProviderService, String> {
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<FarmNode> {
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<FarmNode, String> {
// 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 Mycelium Grid (automatic creation with real data)
pub async fn add_node_from_grid(&self, user_email: &str, grid_node_id: u32) -> Result<FarmNode, String> {
// 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<String, rust_decimal::Decimal>>) -> 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<Decimal, 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 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<DateTime<Utc>> = 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<String, rust_decimal::Decimal>>) -> Result<crate::models::product::Product, String> {
// 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<EarningsRecord> {
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) -> ResourceProviderStatistics {
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::<rust_decimal::Decimal>();
let uptime_percentage = if nodes.is_empty() {
0.0
} else {
nodes.iter().map(|n| n.uptime_percentage).sum::<f32>() / 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;
}
ResourceProviderStatistics {
total_nodes,
online_nodes,
total_capacity,
used_capacity,
monthly_earnings: monthly_earnings.to_string().parse::<i32>().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<bool, String> {
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<DefaultSliceFormat> {
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<DefaultSliceFormat> {
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<DefaultSliceFormat> {
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::ResourceProviderSettings {
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::<String, serde_json::Value>::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::<String, serde_json::Value>::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<FarmNode> {
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::<String>::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::<String>::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<String> = 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 Mycelium Grid by node ID
pub async fn add_grid_node(&self, user_email: &str, grid_node_id: u32, _slice_format: Option<String>, _slice_price: Option<Decimal>) -> Result<FarmNode, String> {
// 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 Mycelium 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<u32>) -> Result<Vec<FarmNode>, String> {
// Batch approach: load once, add all nodes, save once
let mut added_nodes: Vec<FarmNode> = 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 Mycelium 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<u32>, slice_formats: Vec<String>, full_node_rental: bool) -> Result<Vec<FarmNode>, String> {
// Batch approach: load once, add all nodes, save once
let mut added_nodes: Vec<FarmNode> = 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<u32>, slice_formats: Vec<String>, rental_options: Option<crate::models::user::NodeRentalOptions>) -> Result<Vec<FarmNode>, String> {
// Batch approach: load once, add all nodes, save once
let mut added_nodes: Vec<FarmNode> = 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<u32>, slice_formats: Vec<String>, individual_pricing: std::collections::HashMap<String, crate::models::user::FullNodePricing>) -> Result<Vec<FarmNode>, String> {
// Batch approach: load once, add all nodes, save once
let mut added_nodes: Vec<FarmNode> = 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<String>, rental_options: Option<crate::models::user::NodeRentalOptions>) -> Result<FarmNode, String> {
// 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<String>, full_node_rental: bool) -> Result<FarmNode, String> {
// 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<String>, config: Option<crate::models::user::NodeGroupConfig>) -> Result<NodeGroup, String> {
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<NodeGroup> {
// 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<GroupStatistics, String> {
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::<f32>() / 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<String>) -> 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<FarmNode> {
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<String>, description: Option<String>, slice_format: Option<String>, slice_price: Option<Decimal>) -> 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<FarmNode> {
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<String>) -> Result<String, String> {
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<String> = 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 Mycelium Grid
pub fn sync_all_nodes_with_grid(&self, user_email: &str) -> Result<u32, String> {
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::<u32>().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<Vec<serde_json::Value>, 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<serde_json::Value, String> {
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<crate::models::user::GridNodeData, String> {
// Validate node exists on grid
if !self.grid_service.validate_node_exists(grid_node_id).await? {
return Err(format!("Node {} does not exist on Mycelium 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<u32>,
base_slice_price: rust_decimal::Decimal,
node_uptime_sla: f32,
node_bandwidth_sla: i32,
node_group: Option<String>,
enable_staking: bool,
enable_full_node_rental: bool,
) -> Result<Vec<FarmNode>, 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<String> = added_nodes.iter().map(|n| n.id.clone()).collect();
let found_nodes: Vec<String> = 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<u32, String> {
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<u32, 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(updated_count)
}
/// Sync all nodes with Mycelium Grid (async version)
pub async fn sync_all_nodes_with_grid_async(&self, user_email: &str) -> Result<u32, String> {
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<serde_json::Value, 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.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<String>,
pub node_type: Option<String>,
pub slice_formats: Option<Vec<String>>, // Selected slice formats for this node (DEPRECATED)
/// Node rental configuration options
pub rental_options: Option<crate::models::user::NodeRentalOptions>,
/// Slice pricing configuration (DEPRECATED - replaced by slice_pricing)
pub slice_prices: Option<std::collections::HashMap<String, rust_decimal::Decimal>>,
/// NEW: Automatic slice pricing configuration
pub slice_pricing: Option<SlicePricing>,
}
/// Data structure for updating nodes
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeUpdateData {
pub name: Option<String>,
pub location: Option<String>,
pub region: Option<String>,
pub status: Option<NodeStatus>,
pub capacity: Option<NodeCapacity>,
pub marketplace_sla: Option<MarketplaceSLA>,
}
/// 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 ResourceProviderStatistics {
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,
}