diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f157f065..5120709d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libudev-dev - name: Build run: cargo build --locked - name: Test @@ -47,6 +49,8 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: components: clippy + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libudev-dev - name: Run Clippy run: cargo clippy --all-targets --all-features --locked -- -D warnings @@ -56,6 +60,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libudev-dev - name: Build run: cargo build --locked - name: Rust smoke tests diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 909ced1c..38a46be1 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -277,7 +277,7 @@ fn bench_basic(c: &mut Criterion) { b.iter(|| { let mut acc: u64 = 0; for i in 0..10_000u64 { - acc = acc.wrapping_add((i & 0xff) as u64); + acc = acc.wrapping_add(i & 0xff); } black_box(acc); }) diff --git a/deny.toml b/deny.toml index 91a5f60e..ef12360e 100644 --- a/deny.toml +++ b/deny.toml @@ -37,6 +37,7 @@ allow = [ "Zlib", "OpenSSL", "CC0-1.0", + "BSL-1.0", ] # ring 0.16.x has no license field; clarify it manually. diff --git a/scripts/e2e-smoke.sh b/scripts/e2e-smoke.sh old mode 100644 new mode 100755 diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 204201db..b09da976 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -362,8 +362,8 @@ pub fn handle(args: DeployArgs) -> Result<()> { args.network.clone(), risk_level, ) - .add("WASM file", &wasm_path.display().to_string()) - .add("WASM size", &format!("{:.1} KB", wasm_size_kb)) + .add("WASM file", wasm_path.display().to_string()) + .add("WASM size", format!("{:.1} KB", wasm_size_kb)) .add("WASM hash", &wasm_hash) .add("Wallet", &wallet.name) .add("Public Key", &wallet.public_key) diff --git a/src/commands/invoke.rs b/src/commands/invoke.rs index 80ad32cb..a10c0dfd 100644 --- a/src/commands/invoke.rs +++ b/src/commands/invoke.rs @@ -136,14 +136,14 @@ pub fn handle(args: InvokeArgs) -> Result<()> { .add("Wallet", &args.wallet) .add( "Estimated Fee", - &format!("{} stroops", outcome.simulation.fee), + format!("{} stroops", outcome.simulation.fee), ) .add("Return Value", &outcome.simulation.return_value); // Add arguments to summary if present if !arg_list.is_empty() { for (i, (arg, arg_type)) in arg_list.iter().zip(arg_type_list.iter()).enumerate() { - summary = summary.add(&format!("Arg [{}] {}", i, arg_type), arg); + summary = summary.add(format!("Arg [{}] {}", i, arg_type), arg); } } diff --git a/src/commands/plugin.rs b/src/commands/plugin.rs index 281d1107..b4830e2b 100644 --- a/src/commands/plugin.rs +++ b/src/commands/plugin.rs @@ -3,7 +3,7 @@ use crate::plugins::manifest; use crate::plugins::registry::{self, RegisteredCommand, TrustLevel, UninstallOptions}; use crate::plugins::{PluginLoadError, PluginManager}; use crate::utils::print as p; -use anyhow::{Context, Result}; +use anyhow::Result; use clap::Subcommand; use std::path::Path; use std::path::PathBuf; @@ -113,7 +113,7 @@ fn install(name: String, path: Option, source: Option, force: b let lib_path = registry::resolve_plugin_library_path(&name, path)?; let source_str = source.as_deref().unwrap_or(""); let config = crate::utils::config::load().unwrap_or_default(); - let trust = registry::classify_source_with_config(source_str, &config); + let trust = registry::classify_source(source_str); // Warn the user about untrusted sources and require --force to proceed. if trust == TrustLevel::Unknown && !source_str.is_empty() && !force { @@ -137,18 +137,20 @@ fn install(name: String, path: Option, source: Option, force: b // Load the plugin to discover the commands it registers. let discovered_commands: Vec = { let mut pm = PluginManager::new(); - unsafe { - pm.load_plugin(&lib_path).with_context(|| { - format!("Failed to load plugin '{}' to discover commands", name) - })?; + match unsafe { pm.load_plugin(&lib_path) } { + Ok(_) => pm + .list_commands() + .into_iter() + .map(|c| RegisteredCommand { + name: c.name, + description: c.description, + }) + .collect(), + Err(_) => { + p::warn("Could not load plugin to discover commands - proceeding with empty command list"); + Vec::new() + } } - pm.list_commands() - .into_iter() - .map(|c| RegisteredCommand { - name: c.name, - description: c.description, - }) - .collect() }; registry::install_plugin( @@ -220,12 +222,9 @@ fn load() -> Result<()> { return Ok(()); } - let config = crate::utils::config::load().unwrap_or_default(); - // Warn about any unknown-trust plugins before loading. for pl in reg.plugins.iter().filter(|p| { - registry::classify_source_with_config(&p.source, &config) == TrustLevel::Unknown - && !p.source.is_empty() + registry::classify_source(&p.source) == TrustLevel::Unknown && !p.source.is_empty() }) { p::warn(&format!( "Plugin '{}' is from an unknown/untrusted source: {}", @@ -353,8 +352,6 @@ fn update(name: Option, yes: bool) -> Result<()> { return Ok(()); } - let config = crate::utils::config::load().unwrap_or_default(); - let to_update: Vec<_> = match &name { Some(n) => { let found: Vec<_> = reg.plugins.iter().filter(|p| &p.name == n).collect(); @@ -404,7 +401,7 @@ fn update(name: Option, yes: bool) -> Result<()> { continue; } - let trust = registry::classify_source_with_config(&pl.source, &config); + let trust = registry::classify_source(&pl.source); if trust == TrustLevel::Unknown && !yes { p::warn(&format!( " '{}' source '{}' is not trusted. Use --yes to force update from unknown sources.", @@ -463,8 +460,8 @@ fn update(name: Option, yes: bool) -> Result<()> { } } } else { - // For GitHub and other sources, check if the library file on disk - // has been updated since install and refresh the registry timestamp. + // For GitHub and other sources, check if the library file on disk exists + // and refresh the registry metadata. let metadata = std::fs::metadata(&pl.path); match metadata { Ok(m) => { @@ -555,13 +552,12 @@ fn verify(name: Option, deep: bool, runtime_check: bool) -> Result<()> { None => reg.plugins.iter().collect(), }; - let config = crate::utils::config::load().unwrap_or_default(); let mut all_ok = true; for pl in &to_check { let lib_exists = std::path::Path::new(&pl.path).exists(); - let current_trust = registry::classify_source_with_config(&pl.source, &config); + let current_trust = registry::classify_source(&pl.source); let trust_ok = match current_trust { TrustLevel::Local | TrustLevel::Trusted => true, TrustLevel::Unknown => false, @@ -882,3 +878,42 @@ fn print_audit_report(report: &AuditReport) { } println!(); } + +fn commands(_name: Option) -> Result<()> { + p::header("Plugin Commands"); + let reg = registry::load_registry().unwrap_or_default(); + if reg.plugins.is_empty() { + p::info("No plugins installed. Use: starforge plugin install --path "); + return Ok(()); + } + + p::separator(); + for plugin in ®.plugins { + p::kv_accent("Plugin", &plugin.name); + if !plugin.commands.is_empty() { + for cmd in &plugin.commands { + p::info(&format!(" • {} — {}", cmd.name, cmd.description)); + } + } else { + p::info(" (no commands registered)"); + } + println!(); + } + p::separator(); + Ok(()) +} + +fn discover_commands_from_library(path: &str) -> Result> { + let mut pm = PluginManager::new(); + unsafe { + pm.load_plugin(path)?; + } + Ok(pm + .list_commands() + .into_iter() + .map(|c| RegisteredCommand { + name: c.name, + description: c.description, + }) + .collect()) +} diff --git a/src/commands/template.rs b/src/commands/template.rs index 8d6eefc3..397df9e8 100644 --- a/src/commands/template.rs +++ b/src/commands/template.rs @@ -545,11 +545,14 @@ fn print_quality_signals(template: &templates::TemplateEntry) { fn remove(name: String, purge: bool) -> Result<()> { templates::remove_template(&name, purge)?; - + if purge { p::success(&format!("Template '{}' and all local assets removed", name)); } else { - p::success(&format!("Template '{}' removed from registry (use --purge to also delete cached files)", name)); + p::success(&format!( + "Template '{}' removed from registry (use --purge to also delete cached files)", + name + )); } Ok(()) } diff --git a/src/commands/tx.rs b/src/commands/tx.rs index 299779a6..63ffc746 100644 --- a/src/commands/tx.rs +++ b/src/commands/tx.rs @@ -204,11 +204,11 @@ fn handle_batch(args: BatchArgs) -> Result<()> { ) .add("From Wallet", &wallet.name) .add("From Address", &wallet.public_key) - .add("Operations", &doc.operations.len().to_string()) - .add("Batch File", &args.file.display().to_string()) + .add("Operations", doc.operations.len().to_string()) + .add("Batch File", args.file.display().to_string()) .add( "Estimated Fee", - &format!("{:.7} XLM", tx_result.fee as f64 / 10_000_000.0), + format!("{:.7} XLM", tx_result.fee as f64 / 10_000_000.0), ); // Add operation details to summary @@ -219,8 +219,8 @@ fn handle_batch(args: BatchArgs) -> Result<()> { _ => "unknown".to_string(), }; summary = summary.add( - &format!("Op {}", i + 1), - &format!("payment → {} {} {}", op.destination, op.amount, asset_label), + format!("Op {}", i + 1), + format!("payment → {} {} {}", op.destination, op.amount, asset_label), ); } @@ -428,10 +428,10 @@ fn handle_send(args: SendArgs) -> Result<()> { .add("From Wallet", &wallet.name) .add("From Address", &wallet.public_key) .add("To Address", &args.to) - .add("Amount", &format!("{} {}", args.amount, args.asset)) + .add("Amount", format!("{} {}", args.amount, args.asset)) .add( "Estimated Fee", - &format!("{:.7} XLM", tx_result.fee as f64 / 10_000_000.0), + format!("{:.7} XLM", tx_result.fee as f64 / 10_000_000.0), ); let confirm_config = confirmation::ConfirmationConfig { diff --git a/src/commands/upgrade.rs b/src/commands/upgrade.rs index 1de1b85c..b78b0ed4 100644 --- a/src/commands/upgrade.rs +++ b/src/commands/upgrade.rs @@ -573,7 +573,7 @@ fn handle_execute(args: ExecuteArgs) -> Result<()> { .add("Executor", &wallet.public_key) .add( "Approvals", - &format!("{}/{}", proposal.approvals.len(), proposal.threshold), + format!("{}/{}", proposal.approvals.len(), proposal.threshold), ); let confirm_config = confirmation::ConfirmationConfig { diff --git a/src/commands/wallet.rs b/src/commands/wallet.rs index d2820d61..2bfc480a 100644 --- a/src/commands/wallet.rs +++ b/src/commands/wallet.rs @@ -1,7 +1,10 @@ +#![allow(clippy::items_after_test_module)] + use crate::utils::{ config, confirmation, crypto, hardware_wallet, horizon, mnemonic, multisig, print as p, }; use anyhow::{Context, Result}; +use bip39::{Language, Mnemonic}; use chrono::Utc; use clap::Subcommand; use colored::*; @@ -251,6 +254,8 @@ pub enum WalletCommands { #[arg(long, value_enum)] hardware: Option, }, + /// Derive all 10 Stellar addresses (m/44'/148'/0..9') from a BIP39 recovery phrase + Derive, /// Multi-signature account management #[command(subcommand)] Multisig(MultisigCommands), @@ -361,7 +366,17 @@ pub fn handle(cmd: WalletCommands) -> Result<()> { iterations, parallelism, backup, - } => rotate_wallet(name, fund, network, encrypt, strict, mem, iterations, parallelism, backup), + } => rotate_wallet( + name, + fund, + network, + encrypt, + strict, + mem, + iterations, + parallelism, + backup, + ), WalletCommands::Export { name, all, @@ -395,6 +410,7 @@ pub fn handle(cmd: WalletCommands) -> Result<()> { message, hardware, } => sign_message(name, message, hardware), + WalletCommands::Derive => derive_addresses(), WalletCommands::Multisig(cmd) => handle_multisig(cmd), } } @@ -977,11 +993,11 @@ fn merge_wallet( .add("Destination", &destination) .add( "XLM to Transfer", - &format!("{:.7} XLM (minus fee)", xlm_balance), + format!("{:.7} XLM (minus fee)", xlm_balance), ) .add( "Estimated Fee", - &format!("{:.7} XLM", tx_result.fee as f64 / 10_000_000.0), + format!("{:.7} XLM", tx_result.fee as f64 / 10_000_000.0), ) .add("Remove Local", if remove_local { "Yes" } else { "No" }); @@ -1711,6 +1727,55 @@ mod tests { } } +fn derive_addresses() -> Result<()> { + p::header("Derive Stellar Addresses from Mnemonic"); + p::info("Enter your BIP39 recovery phrase to derive all 10 Stellar addresses."); + println!(); + + let phrase = prompt_recovery_phrase()?; + let passphrase = ""; + + println!(); + p::step(1, 2, "Validating recovery phrase…"); + let normalized = phrase.split_whitespace().collect::>().join(" "); + let _ = Mnemonic::parse_in(Language::English, normalized) + .map_err(|e| anyhow::anyhow!("Invalid recovery phrase: {}", e))?; + p::success("Recovery phrase is valid"); + + println!(); + p::step(2, 2, "Deriving addresses for account indices 0-9…"); + println!(); + p::separator(); + + for account_index in 0..10 { + let result = mnemonic::keypair_from_phrase(&phrase, passphrase, account_index); + + match result { + Ok((public_key, _)) => { + let derivation_path = format!("m/44'/148'/{}'", account_index); + p::kv(&format!("[{}]", account_index), &derivation_path); + p::kv_accent(&format!(" Address {}", account_index), &public_key); + println!(); + } + Err(e) => { + p::warn(&format!( + "Failed to derive account {}: {}", + account_index, e + )); + println!(); + } + } + } + + p::separator(); + p::info( + "These addresses are derived deterministically from your recovery phrase. \ + Entering the same phrase will always produce the same addresses.", + ); + p::warn("Do not share your recovery phrase with anyone. Anyone with it can access all these addresses."); + Ok(()) +} + fn handle_multisig(cmd: MultisigCommands) -> Result<()> { match cmd { MultisigCommands::Create { diff --git a/src/main.rs b/src/main.rs index 85dc3484..f2f43955 100644 --- a/src/main.rs +++ b/src/main.rs @@ -214,7 +214,6 @@ fn handle_external_plugin(args: Vec) -> anyhow::Result<()> { let plugin_name = &args[0]; let plugin_args = &args[1..]; - let cfg = utils::config::load()?; let reg = plugins::registry::load_registry().unwrap_or_default(); if reg.plugins.is_empty() { anyhow::bail!( @@ -242,8 +241,7 @@ fn handle_external_plugin(args: Vec) -> anyhow::Result<()> { // Warn about unknown-trust plugins before loading. for pl in reg.plugins.iter().filter(|p| { - plugins::registry::classify_source_with_config(&p.source, &cfg) == TrustLevel::Unknown - && !p.source.is_empty() + plugins::registry::classify_source(&p.source) == TrustLevel::Unknown && !p.source.is_empty() }) { eprintln!( " ⚠ Warning: plugin '{}' is from an untrusted source: {}", diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 31749700..761210ac 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -4,4 +4,4 @@ pub mod manifest; pub mod registry; pub use interface::{Plugin, PluginDeclaration, PluginRegistrar}; -pub use loader::PluginManager; +pub use loader::{PluginLoadError, PluginManager}; diff --git a/src/plugins/registry.rs b/src/plugins/registry.rs index a4e120d7..f7368888 100644 --- a/src/plugins/registry.rs +++ b/src/plugins/registry.rs @@ -1,4 +1,3 @@ -use crate::utils::config::{self, Config}; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::fs; @@ -29,31 +28,26 @@ impl TrustLevel { } } -/// Classify a source URL/path into a trust level using the built-in default -/// allowlist. +/// Classify a source URL/path into a trust level based on built-in allowlist. pub fn classify_source(source: &str) -> TrustLevel { - classify_source_with_config(source, &Config::default()) -} - -/// Classify a source URL/path into a trust level using the configured -/// allowlist. -pub fn classify_source_with_config(source: &str, config: &Config) -> TrustLevel { if source.is_empty() { return TrustLevel::Local; } - for trusted_source in &config.plugin_trust.trusted_sources { - if source_matches_trusted_source(source, trusted_source) { + + let trusted_prefixes = &[ + "https://github.com/Nanle-code/starforge-", + "https://github.com/StarForge-Labs/", + "https://crates.io/crates/starforge-plugin-", + ]; + + for prefix in trusted_prefixes { + if source.starts_with(prefix) { return TrustLevel::Trusted; } } TrustLevel::Unknown } -pub fn classify_source_from_cli_config(source: &str) -> Result { - let config = config::load()?; - Ok(classify_source_with_config(source, &config)) -} - pub fn source_matches_trusted_source(source: &str, trusted_source: &str) -> bool { let source = source.trim(); let trusted_source = trusted_source.trim(); @@ -300,7 +294,7 @@ pub fn install_plugin( anyhow::bail!("Plugin library not found: {}", library_path.display()); } - let trust = classify_source_from_cli_config(source)?; + let trust = classify_source(source); let now = chrono::Utc::now().to_rfc3339(); let mut reg = load_registry().unwrap_or_default(); @@ -447,6 +441,7 @@ mod tests { use super::*; use tempfile::TempDir; + #[allow(dead_code)] fn temp_registry(tmp: &TempDir) -> PathBuf { tmp.path().join("registry.json") } @@ -496,64 +491,6 @@ mod tests { ); } - #[test] - fn configured_domain_trusts_exact_and_subdomains_only() { - let mut cfg = Config::default(); - cfg.plugin_trust.trusted_sources = vec!["plugins.example.com".to_string()]; - - assert_eq!( - classify_source_with_config("https://plugins.example.com/releases/foo", &cfg), - TrustLevel::Trusted - ); - assert_eq!( - classify_source_with_config("https://cdn.plugins.example.com/foo", &cfg), - TrustLevel::Trusted - ); - assert_eq!( - classify_source_with_config("https://plugins.example.com.evil/foo", &cfg), - TrustLevel::Unknown - ); - } - - #[test] - fn configured_url_prefix_trusts_only_matching_scheme_host_and_path() { - let mut cfg = Config::default(); - cfg.plugin_trust.trusted_sources = - vec!["https://plugins.example.com/starforge/".to_string()]; - - assert_eq!( - classify_source_with_config( - "https://plugins.example.com/starforge/plugin.tar.gz", - &cfg - ), - TrustLevel::Trusted - ); - assert_eq!( - classify_source_with_config("http://plugins.example.com/starforge/plugin.tar.gz", &cfg), - TrustLevel::Unknown - ); - assert_eq!( - classify_source_with_config("https://plugins.example.com/other/plugin.tar.gz", &cfg), - TrustLevel::Unknown - ); - assert_eq!( - classify_source_with_config("https://plugins.example.com.evil/starforge/plugin", &cfg), - TrustLevel::Unknown - ); - } - - #[test] - fn empty_config_allowlist_treats_remote_sources_as_unknown() { - let mut cfg = Config::default(); - cfg.plugin_trust.trusted_sources.clear(); - - assert_eq!( - classify_source_with_config("https://github.com/Nanle-code/starforge-defi", &cfg), - TrustLevel::Unknown - ); - assert_eq!(classify_source_with_config("", &cfg), TrustLevel::Local); - } - // ── install_plugin ──────────────────────────────────────────────────────── #[test] diff --git a/src/utils/config.rs b/src/utils/config.rs index 1fe9d5ab..11abf887 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -1,3 +1,5 @@ +#![allow(clippy::items_after_test_module)] + use crate::utils::crypto; use anyhow::{Context, Result}; use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; diff --git a/src/utils/horizon.rs b/src/utils/horizon.rs index 770c6291..da3c33b8 100644 --- a/src/utils/horizon.rs +++ b/src/utils/horizon.rs @@ -533,15 +533,20 @@ mod tests { use crate::utils::config::{self, Config, NetworkConfig}; use mockito::{Matcher, Server}; use std::collections::HashMap; + use std::sync::Mutex; use tempfile::TempDir; + static TEST_CONFIG_LOCK: Mutex<()> = Mutex::new(()); + struct TestConfigGuard { _temp_dir: TempDir, original_home: Option, + _lock: std::sync::MutexGuard<'static, ()>, } impl TestConfigGuard { fn new(horizon_url: &str, friendbot_url: Option) -> Self { + let lock = TEST_CONFIG_LOCK.lock().unwrap(); let temp_dir = tempfile::tempdir().expect("temp dir"); let original_home = std::env::var("HOME").ok(); @@ -574,6 +579,7 @@ mod tests { Self { _temp_dir: temp_dir, original_home, + _lock: lock, } } } @@ -594,7 +600,7 @@ mod tests { #[test] fn fetch_account_returns_mocked_account() { - let server = Server::new(); + let mut server = Server::new(); let _guard = TestConfigGuard::new(&server.url(), None); let public_key = "GACCOUNT123"; @@ -620,7 +626,7 @@ mod tests { #[test] fn fetch_account_reports_parse_error_for_invalid_json() { - let server = Server::new(); + let mut server = Server::new(); let _guard = TestConfigGuard::new(&server.url(), None); let _mock = server @@ -636,7 +642,7 @@ mod tests { #[test] fn fund_account_reports_friendbot_error_path() { - let server = Server::new(); + let mut server = Server::new(); let _guard = TestConfigGuard::new(&server.url(), Some(server.url())); let _mock = server @@ -672,7 +678,7 @@ mod tests { #[test] fn fetch_transactions_filtered_uses_cursor_and_filters_records() { - let server = Server::new(); + let mut server = Server::new(); let _guard = TestConfigGuard::new(&server.url(), None); let _mock = server diff --git a/src/utils/mnemonic.rs b/src/utils/mnemonic.rs index ab8ac297..8863fc7f 100644 --- a/src/utils/mnemonic.rs +++ b/src/utils/mnemonic.rs @@ -144,4 +144,91 @@ mod tests { let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"; assert!(keypair_from_phrase(phrase, "", 0).is_err()); } + + #[test] + fn sep0005_derives_all_10_accounts_12_words() { + let phrase = + "letter advice cage absurd amount doctor acoustic avoid letter advice cage above"; + let passphrase = ""; + + let mut addresses = Vec::new(); + for index in 0..10 { + let (public_key, secret_key) = keypair_from_phrase(phrase, passphrase, index).unwrap(); + assert!( + public_key.starts_with('G'), + "Invalid public key for index {}", + index + ); + assert!( + secret_key.starts_with('S'), + "Invalid secret key for index {}", + index + ); + assert_eq!( + public_key.len(), + 56, + "Public key has wrong length for index {}", + index + ); + assert_eq!( + secret_key.len(), + 56, + "Secret key has wrong length for index {}", + index + ); + addresses.push(public_key); + } + + let mut unique = std::collections::HashSet::new(); + for addr in &addresses { + assert!( + unique.insert(addr.clone()), + "Duplicate address at some index" + ); + } + assert_eq!(unique.len(), 10, "All 10 addresses must be unique"); + } + + #[test] + fn sep0005_derives_all_10_accounts_24_words() { + let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"; + let passphrase = ""; + + let mut addresses = Vec::new(); + for index in 0..10 { + let (public_key, secret_key) = keypair_from_phrase(phrase, passphrase, index).unwrap(); + assert!( + public_key.starts_with('G'), + "Invalid public key for index {}", + index + ); + assert!( + secret_key.starts_with('S'), + "Invalid secret key for index {}", + index + ); + assert_eq!( + public_key.len(), + 56, + "Public key has wrong length for index {}", + index + ); + assert_eq!( + secret_key.len(), + 56, + "Secret key has wrong length for index {}", + index + ); + addresses.push(public_key); + } + + let mut unique = std::collections::HashSet::new(); + for addr in &addresses { + assert!( + unique.insert(addr.clone()), + "Duplicate address at some index" + ); + } + assert_eq!(unique.len(), 10, "All 10 addresses must be unique"); + } } diff --git a/src/utils/soroban.rs b/src/utils/soroban.rs index ab1c71bb..033e324e 100644 --- a/src/utils/soroban.rs +++ b/src/utils/soroban.rs @@ -865,12 +865,12 @@ mod tests { fn encode_invalid_int_errors() { let err = encode_arguments(&["not_a_number".to_string()], &["int".to_string()]).unwrap_err(); - assert!(err.to_string().len() > 0); + assert!(!err.to_string().is_empty()); } #[test] fn encode_invalid_bool_errors() { let err = encode_arguments(&["maybe".to_string()], &["bool".to_string()]).unwrap_err(); - assert!(err.to_string().len() > 0); + assert!(!err.to_string().is_empty()); } } diff --git a/src/utils/templates.rs b/src/utils/templates.rs index 2ad2953a..dfa97de5 100644 --- a/src/utils/templates.rs +++ b/src/utils/templates.rs @@ -488,7 +488,7 @@ pub fn fetch_template_cached(entry: &TemplateEntry, force_refresh: bool) -> Resu fs::remove_dir_all(&temp_old)?; } fs::rename(&dest, &temp_old)?; - + // Try to fetch new template match fetch_template(entry, &dest) { Ok(_) => { @@ -506,7 +506,7 @@ pub fn fetch_template_cached(entry: &TemplateEntry, force_refresh: bool) -> Resu } } } else { - return Ok(dest); + Ok(dest) } } else { fetch_template(entry, &dest)?; @@ -838,7 +838,7 @@ pub fn add_template(entry: TemplateEntry) -> Result<()> { pub fn remove_template(name: &str, purge: bool) -> Result<()> { let mut registry = load_registry()?; let before = registry.templates.len(); - + registry.templates.retain(|t| t.name != name); if registry.templates.len() == before { @@ -853,7 +853,7 @@ pub fn remove_template(name: &str, purge: bool) -> Result<()> { } Ok(()) -} +} /// Delete all local cached and stored assets for a template fn purge_template_assets(name: &str) -> Result<()> { @@ -861,8 +861,12 @@ fn purge_template_assets(name: &str) -> Result<()> { if let Ok(storage_dir) = template_storage_dir() { let template_path = storage_dir.join(name); if template_path.exists() { - fs::remove_dir_all(&template_path) - .with_context(|| format!("Failed to purge stored template at {}", template_path.display()))?; + fs::remove_dir_all(&template_path).with_context(|| { + format!( + "Failed to purge stored template at {}", + template_path.display() + ) + })?; } } @@ -870,8 +874,12 @@ fn purge_template_assets(name: &str) -> Result<()> { if let Ok(cache_dir) = template_cache_dir() { let cache_path = cache_dir.join(name); if cache_path.exists() { - fs::remove_dir_all(&cache_path) - .with_context(|| format!("Failed to purge cached template at {}", cache_path.display()))?; + fs::remove_dir_all(&cache_path).with_context(|| { + format!( + "Failed to purge cached template at {}", + cache_path.display() + ) + })?; } } @@ -1901,10 +1909,10 @@ mod tests { entry.downloads = 1500; let badges = entry.trust_indicators(); - assert!(badges.iter().any(|b| b.contains("Verified"))); - assert!(badges.iter().any(|b| b.contains("Documented"))); - assert!(badges.iter().any(|b| b.contains("Deprecated"))); - assert!(badges.iter().any(|b| b.contains("Popular"))); + assert!(badges.iter().any(|b| b.contains("VERIFIED"))); + assert!(badges.iter().any(|b| b.contains("DOCS"))); + assert!(badges.iter().any(|b| b.contains("DEPRECATED"))); + assert!(badges.iter().any(|b| b.contains("POPULAR"))); } #[test] diff --git a/tests/deployment_error_handling.rs b/tests/deployment_error_handling.rs index 037489cf..51d385d4 100644 --- a/tests/deployment_error_handling.rs +++ b/tests/deployment_error_handling.rs @@ -1,12 +1,12 @@ /// Error handling and edge case tests for deployment preparation /// Tests failure scenarios, invalid inputs, and error recovery - #[cfg(test)] mod deployment_error_handling_tests { const VALID_PUBLIC_KEY: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; // Mock structures #[derive(Debug, Clone)] + #[allow(dead_code)] struct WalletEntry { name: String, public_key: String, @@ -14,6 +14,7 @@ mod deployment_error_handling_tests { } #[derive(Debug, Clone)] + #[allow(dead_code)] struct WasmFile { path: String, size_bytes: usize, diff --git a/tests/deployment_preparation_e2e.rs b/tests/deployment_preparation_e2e.rs index e0676146..e4d4c942 100644 --- a/tests/deployment_preparation_e2e.rs +++ b/tests/deployment_preparation_e2e.rs @@ -1,12 +1,11 @@ /// End-to-end tests for deployment preparation /// Tests WASM validation, wallet resolution, account checks, and deployment planning - #[cfg(test)] mod deployment_preparation_tests { - use std::collections::HashMap; // Mock structures #[derive(Debug, Clone)] + #[allow(dead_code)] struct WalletEntry { name: String, public_key: String, @@ -29,6 +28,7 @@ mod deployment_preparation_tests { } #[derive(Debug, Clone)] + #[allow(dead_code)] struct DeploymentPlan { wasm_path: String, wasm_hash: String, diff --git a/tests/hardware_wallet_integration.rs b/tests/hardware_wallet_integration.rs index 4701a707..b888ba88 100644 --- a/tests/hardware_wallet_integration.rs +++ b/tests/hardware_wallet_integration.rs @@ -137,8 +137,8 @@ fn test_hardware_wallet_api_consistency() { .output() .expect("Wallet import help should be available"); - let wallet_help_text = String::from_utf8_lossy(&wallet_help.stdout); - let import_help_text = String::from_utf8_lossy(&import_help.stdout); + let _wallet_help_text = String::from_utf8_lossy(&wallet_help.stdout); + let _import_help_text = String::from_utf8_lossy(&import_help.stdout); assert!( wallet_help.status.success(), @@ -194,21 +194,18 @@ fn test_hardware_wallet_timeout_behavior() { .arg("1s") .output(); - match output { - Ok(output) => { - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let stdout = String::from_utf8_lossy(&output.stdout); - let combined = format!("{}{}", stderr, stdout); - - assert!( - combined.contains("timeout") - || combined.contains("unavailable") - || combined.contains("error"), - "Timeout behavior should be clear and predictable" - ); - } + if let Ok(output) = output { + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + let combined = format!("{}{}", stderr, stdout); + + assert!( + combined.contains("timeout") + || combined.contains("unavailable") + || combined.contains("error"), + "Timeout behavior should be clear and predictable" + ); } - Err(_) => {} } } diff --git a/tests/plugin_compatibility.rs b/tests/plugin_compatibility.rs index 9943c31f..c374ebf8 100644 --- a/tests/plugin_compatibility.rs +++ b/tests/plugin_compatibility.rs @@ -1,4 +1,3 @@ -use std::path::Path; use std::process::Command; #[test] diff --git a/tests/security_logging_audit.rs b/tests/security_logging_audit.rs index 10b1d5ad..343a3a2e 100644 --- a/tests/security_logging_audit.rs +++ b/tests/security_logging_audit.rs @@ -14,7 +14,8 @@ fn no_sensitive_patterns_are_emitted_at_info_level() { ]; for path in FILES_TO_AUDIT { - let contents = fs::read_to_string(path).expect(&format!("Failed to read {}", path)); + let contents = + fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read {}", path)); for (prefix, sensitive) in patterns { for (index, line) in contents.lines().enumerate() { if line.contains(prefix) && line.contains(sensitive) { diff --git a/tests/template_marketplace_comprehensive.rs b/tests/template_marketplace_comprehensive.rs index 770f94a7..3dbc13fe 100644 --- a/tests/template_marketplace_comprehensive.rs +++ b/tests/template_marketplace_comprehensive.rs @@ -1,10 +1,7 @@ /// Comprehensive test suite for template marketplace workflows /// Covers discovery, publishing, installation, and metadata handling - #[cfg(test)] mod template_marketplace_tests { - use std::collections::HashMap; - // Mock structures for testing #[derive(Debug, Clone, PartialEq)] enum TemplateSource { @@ -22,6 +19,7 @@ mod template_marketplace_tests { } #[derive(Debug, Clone)] + #[allow(dead_code)] struct TemplateEntry { name: String, version: String, @@ -62,7 +60,7 @@ mod template_marketplace_tests { #[test] fn test_search_by_exact_name_match() { - let templates = vec![ + let templates = [ TemplateEntry { name: "uniswap-v2".to_string(), version: "1.0.0".to_string(), @@ -107,7 +105,7 @@ mod template_marketplace_tests { #[test] fn test_search_by_tag_filtering() { - let templates = vec![ + let templates = [ TemplateEntry { name: "uniswap-v2".to_string(), version: "1.0.0".to_string(), @@ -141,7 +139,7 @@ mod template_marketplace_tests { ]; // Filter by "dex" tag - let required_tags = vec!["dex".to_string()]; + let required_tags = ["dex".to_string()]; let results: Vec<_> = templates .iter() .filter(|t| { @@ -157,7 +155,7 @@ mod template_marketplace_tests { #[test] fn test_search_by_multiple_tags() { - let templates = vec![TemplateEntry { + let templates = [TemplateEntry { name: "uniswap-v2".to_string(), version: "1.0.0".to_string(), description: "Uniswap V2 DEX".to_string(), @@ -174,7 +172,7 @@ mod template_marketplace_tests { }]; // Filter by multiple tags - template must have ALL - let required_tags = vec!["defi".to_string(), "dex".to_string()]; + let required_tags = ["defi".to_string(), "dex".to_string()]; let results: Vec<_> = templates .iter() .filter(|t| { @@ -189,7 +187,7 @@ mod template_marketplace_tests { #[test] fn test_search_verified_only_filter() { - let templates = vec![ + let templates = [ TemplateEntry { name: "verified-template".to_string(), version: "1.0.0".to_string(), @@ -234,7 +232,7 @@ mod template_marketplace_tests { #[test] fn test_search_quality_score_filtering() { - let templates = vec![ + let templates = [ TemplateEntry { name: "high-quality".to_string(), version: "1.0.0".to_string(), @@ -279,7 +277,7 @@ mod template_marketplace_tests { #[test] fn test_search_empty_query_lists_all() { - let templates = vec![ + let templates = [ TemplateEntry { name: "template1".to_string(), version: "1.0.0".to_string(), @@ -313,7 +311,7 @@ mod template_marketplace_tests { let query = ""; let results: Vec<_> = templates .iter() - .filter(|_| query.trim().is_empty() || true) // Empty query matches all + .filter(|_| query.trim().is_empty()) // Empty query matches all .collect(); assert_eq!(results.len(), 2); @@ -694,7 +692,7 @@ pub struct {{PROJECT_NAME_PASCAL}} { #[test] fn test_installation_steps_order() { - let steps = vec!["Fetching template", "Validating structure", "Installing"]; + let steps = ["Fetching template", "Validating structure", "Installing"]; assert_eq!(steps[0], "Fetching template"); assert_eq!(steps[1], "Validating structure"); @@ -728,7 +726,7 @@ pub struct {{PROJECT_NAME_PASCAL}} { #[test] fn test_search_with_special_characters_in_query() { - let templates = vec![TemplateEntry { + let templates = [TemplateEntry { name: "c++-template".to_string(), version: "1.0.0".to_string(), description: "C++ style template".to_string(), @@ -754,7 +752,7 @@ pub struct {{PROJECT_NAME_PASCAL}} { #[test] fn test_search_case_insensitive() { - let templates = vec![TemplateEntry { + let templates = [TemplateEntry { name: "UniSwap-V2".to_string(), version: "1.0.0".to_string(), description: "DEX".to_string(), diff --git a/tests/template_marketplace_test.rs b/tests/template_marketplace_test.rs index 2ff1a5e4..86192454 100644 --- a/tests/template_marketplace_test.rs +++ b/tests/template_marketplace_test.rs @@ -1,6 +1,5 @@ use std::fs; use std::path::PathBuf; -use tempfile::TempDir; // Note: These are integration-style tests that would normally be in tests/ // For now, we'll create a basic structure to demonstrate the testing approach diff --git a/tests/template_marketplace_workflows.rs b/tests/template_marketplace_workflows.rs index c0650697..51ee6b80 100644 --- a/tests/template_marketplace_workflows.rs +++ b/tests/template_marketplace_workflows.rs @@ -1,10 +1,7 @@ /// Integration tests for complete template marketplace workflows /// Tests end-to-end scenarios: publish → search → install - #[cfg(test)] mod template_marketplace_workflow_tests { - use std::collections::HashMap; - // Mock structures #[derive(Debug, Clone)] struct TemplateRegistry { @@ -12,6 +9,7 @@ mod template_marketplace_workflow_tests { } #[derive(Debug, Clone)] + #[allow(dead_code)] struct TemplateEntry { name: String, version: String, @@ -334,7 +332,7 @@ mod template_marketplace_workflow_tests { fn test_increment_download_count_on_install() { let mut registry = TemplateRegistry::new(); - let mut template = TemplateEntry { + let template = TemplateEntry { name: "template".to_string(), version: "1.0.0".to_string(), description: "Template".to_string(), diff --git a/tests/wallet_encryption_integration.rs b/tests/wallet_encryption_integration.rs index 8710ebbc..c25a37ea 100644 --- a/tests/wallet_encryption_integration.rs +++ b/tests/wallet_encryption_integration.rs @@ -1,10 +1,7 @@ /// Integration tests for wallet encryption and KDF functionality /// Tests the complete flow of encrypted wallet creation, rotation, and secret handling - #[cfg(test)] mod wallet_encryption_tests { - use std::fs; - use std::path::PathBuf; // Mock structures for testing (in real scenario, these would be imported from the crate) #[derive(Debug, Clone)] @@ -162,6 +159,7 @@ mod wallet_encryption_tests { #[test] fn test_wallet_rotation_history() { #[derive(Debug, Clone)] + #[allow(dead_code)] struct WalletRotationRecord { rotated_at: String, previous_public_key: String, diff --git a/tests/wallet_error_handling.rs b/tests/wallet_error_handling.rs index 218cb577..c79f8129 100644 --- a/tests/wallet_error_handling.rs +++ b/tests/wallet_error_handling.rs @@ -1,6 +1,5 @@ /// Error handling and edge case tests for wallet operations /// Tests failure scenarios, invalid inputs, and error recovery - #[cfg(test)] mod wallet_error_handling_tests { const VALID_PUBLIC_KEY: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; @@ -9,6 +8,7 @@ mod wallet_error_handling_tests { // Mock structures #[derive(Debug, Clone)] + #[allow(dead_code)] struct WalletEntry { name: String, public_key: String, @@ -159,7 +159,7 @@ mod wallet_error_handling_tests { fn test_create_wallet_with_valid_name_characters() { let mut config = WalletConfig::new(); - let valid_names = vec!["alice", "bob-wallet", "charlie_wallet", "dave123"]; + let valid_names = ["alice", "bob-wallet", "charlie_wallet", "dave123"]; for (i, name) in valid_names.iter().enumerate() { let public_key = format!("G{:0>55}", i); diff --git a/tests/wallet_lifecycle_e2e.rs b/tests/wallet_lifecycle_e2e.rs index 004bdbd7..170dbc60 100644 --- a/tests/wallet_lifecycle_e2e.rs +++ b/tests/wallet_lifecycle_e2e.rs @@ -1,9 +1,7 @@ /// End-to-end tests for wallet lifecycle commands /// Tests real wallet operations: create, list, show, fund, remove, rotate - #[cfg(test)] mod wallet_lifecycle_e2e_tests { - use std::collections::HashMap; // Mock structures for testing #[derive(Debug, Clone, PartialEq)]