2999 lines
137 KiB
Rust
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,
|
|
}
|
|
|