Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src/commands/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use crate::utils::{config, crypto, print as p};
use anyhow::Result;
use clap::Subcommand;

#[derive(Subcommand)]
pub enum ConfigCommands {
/// Show current global configuration
Show,
/// Set global wallet encryption parameters (Argon2id)
SetEncryption {
/// Argon2 memory cost in KiB (e.g. 65536)
#[arg(long)]
mem: Option<u32>,
/// Argon2 iteration count (e.g. 3)
#[arg(long)]
iterations: Option<u32>,
/// Argon2 parallelism factor (e.g. 4)
#[arg(long)]
parallelism: Option<u32>,
/// Reset to library defaults
#[arg(long, default_value = "false")]
reset: bool,
},
}

pub fn handle(cmd: ConfigCommands) -> Result<()> {
match cmd {
ConfigCommands::Show => show(),
ConfigCommands::SetEncryption {
mem,
iterations,
parallelism,
reset,
} => set_encryption(mem, iterations, parallelism, reset),
}
}

fn show() -> Result<()> {
let cfg = config::load()?;
p::header("StarForge Configuration");
p::separator();

p::kv("Config file", &config::config_path().display().to_string());
p::kv("Active network", &cfg.network);
p::kv("Telemetry", if cfg.telemetry_enabled.unwrap_or(false) { "enabled" } else { "disabled" });

println!();
p::header("Wallet Encryption (Argon2id)");
if let Some(kdf) = &cfg.wallet_encryption {
p::kv("Memory cost", &format!("{} KiB", kdf.mem.unwrap_or(32768)));
p::kv("Iterations", &kdf.iterations.unwrap_or(3).to_string());
p::kv("Parallelism", &kdf.parallelism.unwrap_or(1).to_string());
} else {
p::info("Using default Argon2id parameters:");
p::kv("Memory cost", "32768 KiB (default)");
p::kv("Iterations", "3 (default)");
p::kv("Parallelism", "1 (default)");
}

p::separator();
Ok(())
}

fn set_encryption(
mem: Option<u32>,
iterations: Option<u32>,
parallelism: Option<u32>,
reset: bool,
) -> Result<()> {
let mut cfg = config::load()?;

if reset {
cfg.wallet_encryption = None;
config::save(&cfg)?;
p::success("Wallet encryption parameters reset to defaults.");
return Ok(());
}

if mem.is_none() && iterations.is_none() && parallelism.is_none() {
anyhow::bail!("Provide at least one parameter to set (e.g. --mem 65536) or use --reset");
}

let mut kdf = cfg.wallet_encryption.unwrap_or_default();
if let Some(m) = mem { kdf.mem = Some(m); }
if let Some(i) = iterations { kdf.iterations = Some(i); }
if let Some(p) = parallelism { kdf.parallelism = Some(p); }

cfg.wallet_encryption = Some(kdf);
config::save(&cfg)?;

p::success("Global wallet encryption parameters updated.");
show()?;
Ok(())
}
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod benchmark;
pub mod completions;
pub mod config;
pub mod contract;
pub mod deploy;
pub mod gas;
Expand Down
10 changes: 9 additions & 1 deletion src/commands/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,13 @@ fn update(name: Option<String>, yes: bool) -> Result<()> {

match status {
Ok(s) if s.success() => {
registry::install_plugin(&pl.name, std::path::Path::new(&pl.path), &pl.source)?;
registry::install_plugin(
&pl.name,
std::path::Path::new(&pl.path),
&pl.source,
&pl.starforge_version,
&pl.plugin_version,
)?;
p::success(&format!(" '{}' updated via cargo install", pl.name));
updated += 1;
}
Expand Down Expand Up @@ -401,6 +407,8 @@ fn update(name: Option<String>, yes: bool) -> Result<()> {
&pl.name,
std::path::Path::new(&pl.path),
&pl.source,
&pl.starforge_version,
&pl.plugin_version,
)?;
p::success(&format!(
" '{}' library on disk is newer — registry refreshed.",
Expand Down
4 changes: 4 additions & 0 deletions src/commands/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ fn install(
version,
cli_version_min,
cli_version_max,
None,
None,
None,
None,
)?;
p::header("Template Install");
p::info("Template package installed into the local registry.");
Expand Down
74 changes: 64 additions & 10 deletions src/commands/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,27 @@ use stellar_strkey::ed25519::{PrivateKey as StellarPrivateKey, PublicKey as Stel

const WALLET_BACKUP_VERSION: &str = "1";

fn kdf_options(mem: Option<u32>, iterations: Option<u32>) -> Option<crypto::KdfOptions> {
if mem.is_none() && iterations.is_none() {
None
} else {
Some(crypto::KdfOptions { mem, iterations })
fn kdf_options(
mem: Option<u32>,
iterations: Option<u32>,
parallelism: Option<u32>,
config_default: Option<&crypto::KdfOptions>,
) -> Option<crypto::KdfOptions> {
if mem.is_none() && iterations.is_none() && parallelism.is_none() && config_default.is_none() {
return None;
}

let mut options = config_default.cloned().unwrap_or_default();
if let Some(m) = mem {
options.mem = Some(m);
}
if let Some(i) = iterations {
options.iterations = Some(i);
}
if let Some(p) = parallelism {
options.parallelism = Some(p);
}
Some(options)
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -79,6 +94,15 @@ pub enum WalletCommands {
/// Account index for SEP-0005 path m/44'/148'/index' (requires --mnemonic)
#[arg(long, default_value = "0", requires = "mnemonic")]
account_index: u32,
/// Argon2 memory cost in KiB (requires --encrypt)
#[arg(long, requires = "encrypt")]
mem: Option<u32>,
/// Argon2 iteration count (requires --encrypt)
#[arg(long, requires = "encrypt")]
iterations: Option<u32>,
/// Argon2 parallelism factor (requires --encrypt)
#[arg(long, requires = "encrypt")]
parallelism: Option<u32>,
},
/// List all saved wallets
List,
Expand Down Expand Up @@ -139,6 +163,9 @@ pub enum WalletCommands {
/// Argon2 iteration count (requires --encrypt)
#[arg(long, requires = "encrypt")]
iterations: Option<u32>,
/// Argon2 parallelism factor (requires --encrypt)
#[arg(long, requires = "encrypt")]
parallelism: Option<u32>,
},
/// Export a wallet to a JSON backup file
Export {
Expand Down Expand Up @@ -277,6 +304,9 @@ pub fn handle(cmd: WalletCommands) -> Result<()> {
mnemonic: use_mnemonic,
words,
account_index,
mem,
iterations,
parallelism,
} => create(
name,
fund,
Expand All @@ -286,6 +316,9 @@ pub fn handle(cmd: WalletCommands) -> Result<()> {
use_mnemonic,
words,
account_index,
mem,
iterations,
parallelism,
),
WalletCommands::List => list(),
WalletCommands::Show { name, reveal } => show(name, reveal),
Expand All @@ -306,7 +339,8 @@ pub fn handle(cmd: WalletCommands) -> Result<()> {
encrypt,
mem,
iterations,
} => rotate_wallet(name, fund, network, encrypt, mem, iterations),
parallelism,
} => rotate_wallet(name, fund, network, encrypt, mem, iterations, parallelism),
WalletCommands::Export { name, all, output } => export_wallet(name, all, output),
WalletCommands::Import {
name,
Expand Down Expand Up @@ -467,6 +501,9 @@ fn create(
use_mnemonic: bool,
words: String,
account_index: u32,
mem: Option<u32>,
iterations: Option<u32>,
parallelism: Option<u32>,
) -> Result<()> {
let mut cfg = config::load()?;

Expand Down Expand Up @@ -518,7 +555,11 @@ fn create(
}
println!();
let pwd = crypto::prompt_passphrase("Set a passphrase to encrypt this wallet", strict)?;
crypto::encrypt_secret(&pwd, &secret_key, None)?
crypto::encrypt_secret(
&pwd,
&secret_key,
kdf_options(mem, iterations, parallelism, cfg.wallet_encryption.as_ref()).as_ref(),
)?
} else {
secret_key.clone()
};
Expand Down Expand Up @@ -978,6 +1019,7 @@ fn rotate_wallet(
encrypt: bool,
mem: Option<u32>,
iterations: Option<u32>,
parallelism: Option<u32>,
) -> Result<()> {
config::validate_wallet_name(&name)?;
let mut cfg = config::load()?;
Expand Down Expand Up @@ -1005,7 +1047,11 @@ fn rotate_wallet(
"Set a secure passphrase to encrypt the rotated wallet",
true,
)?;
crypto::encrypt_secret(&pwd, &secret_key, kdf_options(mem, iterations).as_ref())?
crypto::encrypt_secret(
&pwd,
&secret_key,
kdf_options(mem, iterations, parallelism, cfg.wallet_encryption.as_ref()).as_ref(),
)?
} else {
secret_key.clone()
};
Expand Down Expand Up @@ -1100,7 +1146,11 @@ fn export_wallet(name_opt: Option<String>, all: bool, output: PathBuf) -> Result
let json = serde_json::to_string_pretty(&backup)
.with_context(|| "Failed to serialize wallet backup")?;
let passphrase = crypto::prompt_passphrase("Enter passphrase to encrypt backup", false)?;
let encrypted = crypto::encrypt_secret(&passphrase, &json, None)?;
let encrypted = crypto::encrypt_secret(
&passphrase,
&json,
kdf_options(None, None, None, cfg.wallet_encryption.as_ref()).as_ref(),
)?;
fs::write(&output, encrypted)
.with_context(|| format!("Failed to write {}", output.display()))?;

Expand Down Expand Up @@ -1161,7 +1211,11 @@ fn import_from_mnemonic(
let secret_to_store = if encrypt {
println!();
let pwd = crypto::prompt_passphrase("Set a passphrase to encrypt this wallet", false)?;
crypto::encrypt_secret(&pwd, &secret_key, None)?
crypto::encrypt_secret(
&pwd,
&secret_key,
kdf_options(None, None, None, cfg.wallet_encryption.as_ref()).as_ref(),
)?
} else {
secret_key
};
Expand Down
6 changes: 6 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ enum Commands {
/// Show starforge config and environment info
Info,

/// Manage global configuration
#[command(subcommand)]
Config(commands::config::ConfigCommands),

Tx(commands::tx::TxArgs), // fetch transaction for the account

/// View or switch the active network (testnet/mainnet)
Expand Down Expand Up @@ -129,6 +133,7 @@ fn main() {
Commands::Inspect(_) => "inspect",
Commands::Deploy(_) => "deploy",
Commands::Info => "info",
Commands::Config(_) => "config",
Commands::Tx(_) => "tx",
Commands::Network(_) => "network",
Commands::Node(_) => "node",
Expand All @@ -155,6 +160,7 @@ fn main() {
Commands::Inspect(cmd) => commands::inspect::handle(cmd),
Commands::Deploy(args) => commands::deploy::handle(args),
Commands::Info => commands::info::handle(),
Commands::Config(cmd) => commands::config::handle(cmd),
Commands::Tx(args) => commands::tx::handle(args),
Commands::Network(cmd) => commands::network::handle(cmd),
Commands::Node(cmd) => commands::node::handle(cmd),
Expand Down
21 changes: 14 additions & 7 deletions src/plugins/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ pub struct InstalledPlugin {
/// Plugin version from manifest.
#[serde(default)]
pub plugin_version: String,
/// RFC3339 timestamp of when the plugin was installed.
#[serde(default)]
pub installed_at: Option<String>,
}

fn registry_path() -> Result<PathBuf> {
Expand Down Expand Up @@ -158,15 +161,9 @@ pub fn install_plugin(
}

let trust = classify_source(source);
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
let now = chrono::Utc::now().to_rfc3339();

let mut reg = load_registry().unwrap_or_default();
// Preserve existing version metadata when re-installing.
let existing_version = reg
.plugins
.iter()
.find(|p| p.name == name)
.and_then(|p| p.version.clone());
reg.plugins.retain(|p| p.name != name);
reg.plugins.push(InstalledPlugin {
name: name.to_string(),
Expand All @@ -175,6 +172,7 @@ pub fn install_plugin(
trust,
starforge_version: starforge_version.to_string(),
plugin_version: plugin_version.to_string(),
installed_at: Some(now),
});
reg.plugins.sort_by(|a, b| a.name.cmp(&b.name));
save_registry(&reg)?;
Expand Down Expand Up @@ -284,6 +282,15 @@ fn candidate_library_names(name: &str) -> Vec<String> {
}
}

pub fn get_installed_plugin_version(name: &str) -> Option<String> {
load_registry()
.ok()?
.plugins
.iter()
.find(|p| p.name == name)
.map(|p| p.plugin_version.clone())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading