Skip to content
Merged
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
55 changes: 37 additions & 18 deletions src/commands/contract.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::utils::{bindings, call_graph, config, crypto, print as p, soroban};
use crate::utils::{bindings, call_graph, config, print as p, soroban, wallet_signer};
use anyhow::Result;
use clap::{Args, Subcommand, ValueEnum};
use colored::*;
use crate::utils::hardware_wallet::HardwareWalletKind;
use std::path::PathBuf;

#[derive(Subcommand)]
Expand Down Expand Up @@ -68,6 +69,12 @@ pub struct InvokeArgs {
/// Submit the transaction after simulation
#[arg(long, default_value = "false")]
pub submit: bool,
/// Sign with a hardware wallet instead of a local secret key
#[arg(long, value_enum)]
pub hardware: Option<HardwareWalletKind>,
/// HD derivation path for hardware wallet signing
#[arg(long, default_value = crate::utils::hardware_wallet::STELLAR_HD_PATH)]
pub hd_path: String,
}

#[derive(Args)]
Expand All @@ -93,6 +100,12 @@ pub struct UploadArgs {
/// Wallet name to use for signing
#[arg(long)]
pub wallet: Option<String>,
/// Sign with a hardware wallet instead of a local secret key
#[arg(long, value_enum)]
pub hardware: Option<HardwareWalletKind>,
/// HD derivation path for hardware wallet signing
#[arg(long, default_value = crate::utils::hardware_wallet::STELLAR_HD_PATH)]
pub hd_path: String,
}

#[derive(Debug, Clone, Copy, ValueEnum)]
Expand Down Expand Up @@ -250,10 +263,10 @@ async fn handle_invoke(args: InvokeArgs) -> Result<()> {
p::warn("You are invoking on MAINNET. This may cost real XLM if submitted.");
}

// Load and optionally decrypt wallet for submission
let submit_wallet: Option<crate::utils::config::WalletEntry> = if args.submit {
// Load wallet and signing configuration for submission
let (submit_wallet, signing_request) = if args.submit {
let cfg = config::load()?;
let mut w = if let Some(ref wallet_name) = args.wallet {
let wallet = if let Some(ref wallet_name) = args.wallet {
cfg.wallets
.iter()
.find(|w| &w.name == wallet_name)
Expand All @@ -263,31 +276,35 @@ async fn handle_invoke(args: InvokeArgs) -> Result<()> {
wallet_name
)
})?
.clone()
} else if !cfg.wallets.is_empty() {
p::info(&format!(
"No --wallet specified. Using: {}",
cfg.wallets[0].name.cyan()
));
cfg.wallets[0].clone()
&cfg.wallets[0]
} else {
anyhow::bail!(
"No wallets found for submission. Create one first:\n starforge wallet create deployer --fund"
);
};
p::kv("Wallet", &w.name);
if let Some(sk) = &w.secret_key.clone() {
if sk.contains(':') {
let pwd = crypto::prompt_password(
&format!("Enter password to decrypt wallet '{}'", w.name),
false,
)?;
w.secret_key = Some(crypto::decrypt_secret(&pwd, sk)?);
}
p::kv("Wallet", &wallet.name);
if wallet.secret_key.is_none() && args.hardware.is_none() {
anyhow::bail!(
"Wallet '{}' has no local secret key. Use --hardware ledger or --hardware trezor.",
wallet.name
);
}
Some(w)
let signing = wallet_signer::SigningRequest::from_options(
Some(wallet),
args.hardware,
Some(&args.hd_path),
&args.network,
false,
"contract invocation",
)?;
(Some(wallet.clone()), Some(signing))
} else {
None
(None, None)
};

p::separator();
Expand All @@ -307,7 +324,9 @@ async fn handle_invoke(args: InvokeArgs) -> Result<()> {
&arg_types,
&args.network,
submit_wallet.as_ref(),
).await?;
signing_request.as_ref(),
)
.await?;

let simulation_result = outcome.simulation;
p::kv_accent("Simulation", "✓ Success");
Expand Down
60 changes: 36 additions & 24 deletions src/commands/deploy.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
use crate::utils::deploy_history::{
self, last_successful, record_deployment, set_contract_id, update_status, DeployRecord,
DeployStatus,
};
use crate::utils::{config, confirmation, horizon, optimizer, print as p, soroban};
use crate::utils::{config, confirmation, horizon, optimizer, print as p, soroban, wallet_signer};
use anyhow::Result;
use clap::Args;
use colored::*;
use crate::utils::hardware_wallet::HardwareWalletKind;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::PathBuf;
Expand Down Expand Up @@ -47,24 +44,12 @@ pub struct DeployArgs {
/// deployment plan and exits. Implies --simulate.
#[arg(long, default_value = "false")]
pub dry_run: bool,
/// Disable automatic rollback. By default, if an executed deployment fails
/// and a previous successful deployment exists on this network, StarForge
/// records a rollback to that version as a safety net.
#[arg(long, default_value = "false")]
pub no_auto_rollback: bool,
}

/// Extract the deployed contract id (a `C...` strkey) from the Stellar CLI's
/// stdout. `stellar contract deploy` prints the 56-char contract id, typically
/// on its own final line. Returns the first plausible contract id found.
fn parse_contract_id_from_stdout(stdout: &str) -> Option<String> {
stdout
.split(|c: char| c.is_whitespace())
.map(|t| t.trim())
.find(|t| {
t.len() == 56 && t.starts_with('C') && t.chars().all(|c| c.is_ascii_alphanumeric())
})
.map(|t| t.to_string())
/// Sign deployment with a hardware wallet (Ledger/Trezor)
#[arg(long, value_enum)]
pub hardware: Option<HardwareWalletKind>,
/// HD derivation path for hardware wallet signing
#[arg(long, default_value = crate::utils::hardware_wallet::STELLAR_HD_PATH)]
pub hd_path: String,
}

fn is_wasm_above_size_limit(wasm_size_kb: f64) -> bool {
Expand Down Expand Up @@ -390,7 +375,14 @@ pub async fn handle(args: DeployArgs) -> Result<()> {
.add("Wallet", &wallet.name)
.add("Public Key", &wallet.public_key)
.add("Optimized", if args.optimize { "Yes" } else { "No" })
.add("Execute", if args.execute { "Yes" } else { "No (dry-run)" });
.add("Execute", if args.execute { "Yes" } else { "No (dry-run)" })
.add(
"Signer",
&match args.hardware {
Some(device) => format!("hardware ({})", device),
None => format!("local ({})", wallet.name),
},
);

let confirm_config = confirmation::ConfirmationConfig {
risk_level,
Expand All @@ -405,6 +397,26 @@ pub async fn handle(args: DeployArgs) -> Result<()> {
return Ok(());
}

if args.execute {
if let Some(device) = args.hardware {
let signing_request = wallet_signer::SigningRequest::from_options(
Some(wallet),
Some(device),
Some(&args.hd_path),
&args.network,
args.yes,
"contract deployment",
)?;
soroban::sign_deploy_transaction(&wasm_hash, wallet, &args.network, &signing_request)?;
p::success(&format!("Deployment transaction signed on {}", device));
} else if wallet.secret_key.is_none() {
anyhow::bail!(
"Wallet '{}' has no local secret key. Use --hardware ledger or --hardware trezor for deployment.",
wallet.name
);
}
}

println!();
println!();
let pb = p::progress_bar(3, "Starting deployment steps...");
Expand Down
4 changes: 3 additions & 1 deletion src/commands/invoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ pub async fn handle(args: InvokeArgs) -> Result<()> {
&arg_type_list,
network,
submit_wallet.map(|w| w as &crate::utils::config::WalletEntry),
).await?;
None,
)
.await?;

println!();
p::success("Simulation successful!");
Expand Down
75 changes: 48 additions & 27 deletions src/commands/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use clap::{Args, Subcommand};
use colored::*;

use crate::utils::confirmation;
use crate::utils::hardware_wallet::HardwareWalletKind;
use crate::utils::horizon::FeeStats;
use crate::utils::{config, crypto, horizon, print as p, tx_batch}; // Import FeeStats
use crate::utils::{config, horizon, print as p, tx_batch, wallet_signer};

#[derive(Args)]
pub struct TxArgs {
Expand Down Expand Up @@ -42,6 +43,12 @@ pub struct BatchArgs {
/// Skip confirmation prompt
#[arg(long, default_value = "false")]
pub yes: bool,
/// Sign with a hardware wallet instead of a local secret key
#[arg(long, value_enum)]
pub hardware: Option<HardwareWalletKind>,
/// HD derivation path for hardware wallet signing
#[arg(long, default_value = crate::utils::hardware_wallet::STELLAR_HD_PATH)]
pub hd_path: String,
}

#[derive(Args)]
Expand All @@ -64,6 +71,12 @@ pub struct SendArgs {
/// Skip confirmation prompt
#[arg(long, default_value = "false")]
pub yes: bool,
/// Sign with a hardware wallet instead of a local secret key
#[arg(long, value_enum)]
pub hardware: Option<HardwareWalletKind>,
/// HD derivation path for hardware wallet signing
#[arg(long, default_value = crate::utils::hardware_wallet::STELLAR_HD_PATH)]
pub hd_path: String,
}

#[derive(Args)]
Expand Down Expand Up @@ -123,8 +136,11 @@ async fn handle_batch(args: BatchArgs) -> Result<()> {
)
})?;

if wallet.secret_key.is_none() {
anyhow::bail!("Wallet '{}' has no secret key stored", args.from);
if wallet.secret_key.is_none() && args.hardware.is_none() {
anyhow::bail!(
"Wallet '{}' has no secret key stored. Use --hardware ledger or --hardware trezor.",
args.from
);
}

let payment_ops: Vec<horizon::BatchPaymentOp> = doc
Expand Down Expand Up @@ -239,21 +255,22 @@ async fn handle_batch(args: BatchArgs) -> Result<()> {

println!();

let mut secret_key = wallet.secret_key.as_ref().unwrap().clone();
if secret_key.contains(':') {
let pwd = crypto::prompt_password(
&format!("Enter password to decrypt wallet '{}'", wallet.name),
false,
)?;
secret_key = crypto::decrypt_secret(&pwd, &secret_key)?;
}
let signing_request = wallet_signer::SigningRequest::from_options(
Some(wallet),
args.hardware,
Some(&args.hd_path),
&args.network,
args.yes,
"batch transaction",
)?;

p::info("Submitting batch transaction…");
let submit_result = horizon::submit_payment_transaction(
let submit_result = horizon::submit_payment_with_signing(
&tx_result.transaction_xdr,
&secret_key,
&signing_request,
&args.network,
).await?;
)
.await?;

println!();
p::separator();
Expand Down Expand Up @@ -316,8 +333,11 @@ async fn handle_send(args: SendArgs) -> Result<()> {
})?;

// Validate wallet has secret key
if wallet.secret_key.is_none() {
anyhow::bail!("Wallet '{}' has no secret key stored", args.from);
if wallet.secret_key.is_none() && args.hardware.is_none() {
anyhow::bail!(
"Wallet '{}' has no secret key stored. Use --hardware ledger or --hardware trezor.",
args.from
);
}

// Parse asset
Expand Down Expand Up @@ -453,21 +473,22 @@ async fn handle_send(args: SendArgs) -> Result<()> {
// Submit transaction
println!();

let mut secret_key = wallet.secret_key.as_ref().unwrap().clone();
if secret_key.contains(':') {
let pwd = crypto::prompt_password(
&format!("Enter password to decrypt wallet '{}'", wallet.name),
false,
)?;
secret_key = crypto::decrypt_secret(&pwd, &secret_key)?;
}
let signing_request = wallet_signer::SigningRequest::from_options(
Some(wallet),
args.hardware,
Some(&args.hd_path),
&args.network,
args.yes,
"payment transaction",
)?;

p::info("Submitting transaction…");
let submit_result = horizon::submit_payment_transaction(
let submit_result = horizon::submit_payment_with_signing(
&tx_result.transaction_xdr,
&secret_key,
&signing_request,
&args.network,
).await?;
)
.await?;

println!();
p::separator();
Expand Down
Loading
Loading