diff --git a/crates/tools/src/secure_vault.rs b/crates/tools/src/secure_vault.rs index c3974d5..ce33dcc 100644 --- a/crates/tools/src/secure_vault.rs +++ b/crates/tools/src/secure_vault.rs @@ -33,30 +33,25 @@ impl SecureVault { } } - /// Validate that required keys are present for mainnet operations. + /// Validate that required keys are present for mainnet operations. #[must_use] pub fn validate_for_mainnet(&self) -> Result<()> { if self.admin_secret_key.is_none() { anyhow::bail!("SOROBAN_ADMIN_SECRET_KEY is required for mainnet operations"); } - if self.admin_public_key.is_none() { anyhow::bail!("SOROBAN_ADMIN_PUBLIC_KEY is required for mainnet operations"); } - - // Validate key format (basic check) if let Some(secret) = &self.admin_secret_key { if !secret.starts_with('S') { anyhow::bail!("Admin secret key must start with 'S'"); } } - if let Some(public) = &self.admin_public_key { if !public.starts_with('G') { anyhow::bail!("Admin public key must start with 'G'"); } } - Ok(()) } @@ -68,13 +63,11 @@ impl SecureVault { anyhow::bail!("Admin secret key must start with 'S' or be empty"); } } - if let Some(public) = &self.admin_public_key { if !public.is_empty() && !public.starts_with('G') { anyhow::bail!("Admin public key must start with 'G' or be empty"); } } - Ok(()) } @@ -82,12 +75,10 @@ impl SecureVault { pub fn display_safe(&self) { println!("🔐 Secure Vault Status"); println!("━━━━━━━━━━━━━━━━━━━━"); - match &self.admin_public_key { Some(key) => println!("Admin Public Key: {}", key), None => println!("Admin Public Key: ⚠️ Not set"), } - match &self.admin_secret_key { Some(key) if key.len() > 10 => { println!( @@ -99,12 +90,10 @@ impl SecureVault { Some(_) => println!("Admin Secret Key: ***"), None => println!("Admin Secret Key: ⚠️ Not set"), } - match &self.issuing_public_key { Some(key) => println!("Issuing Public Key: {}", key), None => println!("Issuing Public Key: ⚠️ Not set"), } - match &self.issuing_secret_key { Some(key) if key.len() > 10 => { println!( @@ -118,8 +107,109 @@ impl SecureVault { } } - /// Save vault to encrypted file (placeholder for future encryption) - /// Save vault to encrypted file (placeholder for future encryption) +impl Default for SecureVault { + fn default() -> Self { + Self { + admin_secret_key: None, + admin_public_key: None, + issuing_secret_key: None, + issuing_public_key: None, + } + } +} + + pub fn save_to_file(&self, _path: &str) -> Result<()> { + eprintln!("🚨 ERROR: SecureVault::save_to_file() stores keys in PLAINTEXT."); + eprintln!(" Use EncryptedVault::save_to_file() instead."); + eprintln!(" Example: orbitchain-cli keymanager vault-save "); + anyhow::bail!("Plaintext vault save disabled for security. Use EncryptedVault."); + } + + /// Load vault from file + pub fn load_from_file(path: &str) -> Result { + let content = fs::read_to_string(path).context("Failed to read vault file")?; + + let mut vault = Self { + admin_secret_key: None, + admin_public_key: None, + issuing_secret_key: None, + issuing_public_key: None, + }; + + for line in content.lines() { + if line.starts_with('#') || line.trim().is_empty() { + continue; + } + + let parts: Vec<&str> = line.splitn(2, '=').collect(); + if parts.len() == 2 { + match parts[0] { + "SOROBAN_ADMIN_SECRET_KEY" => { + vault.admin_secret_key = Some(parts[1].to_string()) + } + "SOROBAN_ADMIN_PUBLIC_KEY" => { + vault.admin_public_key = Some(parts[1].to_string()) + } + "SOROBAN_ISSUING_SECRET_KEY" => { + vault.issuing_secret_key = Some(parts[1].to_string()) + } + "SOROBAN_ISSUING_PUBLIC_KEY" => { + vault.issuing_public_key = Some(parts[1].to_string()) + } + _ => {} + } + } + } + + Ok(vault) + } +} + +/// Check mainnet configuration readiness +pub fn check_mainnet_readiness() -> Result<()> { + let vault = SecureVault::from_env(); + + println!("🔒 Mainnet Configuration Check"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + + // Validate vault + if let Err(e) = vault.validate_for_mainnet() { + println!("❌ Mainnet validation failed: {}", e); + println!(); + println!("💡 To configure mainnet:"); + println!(" 1. Set SOROBAN_NETWORK=mainnet in .env"); + println!(" 2. Set SOROBAN_ADMIN_SECRET_KEY="); + println!(" 3. Set SOROBAN_ADMIN_PUBLIC_KEY="); + println!(" 4. Ensure you have sufficient XLM for transaction fees"); + return Err(e); + } + + println!("✅ Admin keys configured"); + vault.display_safe(); + + println!(); + println!("✅ Mainnet configuration is ready"); + println!("⚠️ WARNING: Mainnet transactions use real XLM!"); + + Ok(()) +} + +/// Toggle between testnet and mainnet configurations +pub fn toggle_network(network: &str) -> Result<()> { + match network { + "testnet" => { + println!("🔄 Switching to TESTNET..."); + println!("✅ Network: testnet"); + println!("💡 Use testnet for development and testing"); + } + "mainnet" => { + println!("🔄 Switching to MAINNET..."); + check_mainnet_readiness()?; + } + _ => anyhow::bail!("Unknown network: {}. Use 'testnet' or 'mainnet'", network), + } + + /// # Deprecated /// This method stores keys in plaintext. Use `EncryptedVault::save_to_file()` instead. pub fn save_to_file(&self, _path: &str) -> Result<()> { @@ -220,24 +310,62 @@ pub fn toggle_network(network: &str) -> Result<()> { mod tests { use super::*; + // Test loading from environment and shape checks. #[test] fn test_vault_from_env() { let vault = SecureVault::from_env(); - // Should not panic even if keys are not set - assert!(vault.admin_secret_key.is_none() || vault.admin_secret_key.is_some()); + if let Some(secret) = &vault.admin_secret_key { + assert!(secret.is_empty() || secret.starts_with('S'), "admin secret key must start with 'S' (got {:?})", secret); + } + if let Some(public) = &vault.admin_public_key { + assert!(public.is_empty() || public.starts_with('G'), "admin public key must start with 'G' (got {:?})", public); + } } + // Positive test for testnet validation. #[test] - fn test_validate_for_testnet() { - let vault = SecureVault::from_env(); - // Testnet validation should pass even without keys + fn test_validate_for_testnet_positive() { + let mut vault = SecureVault::default(); + // Empty keys allowed. + assert!(vault.validate_for_testnet().is_ok()); + + // Valid keys should also pass. + vault.admin_secret_key = Some("SSECRET".to_string()); + vault.admin_public_key = Some("GPUBLIC".to_string()); assert!(vault.validate_for_testnet().is_ok()); } + // Negative test for testnet validation. + #[test] + fn test_validate_for_testnet_rejects_bad_secret() { + let mut vault = SecureVault::default(); + vault.admin_secret_key = Some("invalid_secret".to_string()); + assert!(vault.validate_for_testnet().is_err()); + } + + // Positive test for mainnet validation. + #[test] + fn test_validate_for_mainnet_positive() { + let mut vault = SecureVault::default(); + vault.admin_secret_key = Some("SSECRET".to_string()); + vault.admin_public_key = Some("GPUBLIC".to_string()); + assert!(vault.validate_for_mainnet().is_ok()); + } + + // Negative test for mainnet validation (missing admin keys). + #[test] + fn test_validate_for_mainnet_rejects_missing_admin() { + let vault = SecureVault::default(); + assert!(vault.validate_for_mainnet().is_err()); + } + #[test] fn test_display_safe() { let vault = SecureVault::from_env(); vault.display_safe(); // Should not panic } + + // Reference to issue #32 for context. + // See: https://github.com/your-repo/OrbitChain-Contracts/issues/32 } diff --git a/crates/tools/src/withdrawal_limits.rs b/crates/tools/src/withdrawal_limits.rs index 95872f4..7781c75 100644 --- a/crates/tools/src/withdrawal_limits.rs +++ b/crates/tools/src/withdrawal_limits.rs @@ -64,10 +64,13 @@ impl WithdrawalLimits { )); } if let Some(cap) = self.max_total { - if already_withdrawn + amount > cap { + let new_total = already_withdrawn + .checked_add(amount) + .ok_or_else(|| anyhow!("Withdrawal arithmetic overflow: already_withdrawn + amount exceeds i128"))?; + if new_total > cap { return Err(anyhow!( "Total withdrawn {} would exceed the campaign cap of {}", - already_withdrawn + amount, cap + new_total, cap, )); } } @@ -114,5 +117,13 @@ mod tests { fn constructor_rejects_invalid_range() { assert!(WithdrawalLimits::new(1000, 500, None).is_err()); assert!(WithdrawalLimits::new(0, 500, None).is_err()); + + #[test] + fn overflow_error_when_sum_exceeds_i128() { + // Create limits with a very large max_per_withdrawal to allow the amount + let limits = WithdrawalLimits::new(1, i128::MAX, None).unwrap(); + // This should cause checked_add overflow (already_withdrawn + amount > i128::MAX) + assert!(limits.validate(i128::MAX, 1).is_err()); + } } }