diff --git a/src/commands/contract.rs b/src/commands/contract.rs index 2877aad3..cd34fe1b 100644 --- a/src/commands/contract.rs +++ b/src/commands/contract.rs @@ -9,6 +9,10 @@ pub enum ContractCommands { Invoke(InvokeArgs), /// Inspect a deployed Soroban contract instance Inspect(InspectArgs), + /// Upload a WASM binary to the Stellar network (upload-only step) + /// + /// See: https://developers.stellar.org/docs/build/smart-contracts/getting-started/deploy-increment-contract + Upload(UploadArgs), } #[derive(Args)] @@ -43,10 +47,24 @@ pub struct InspectArgs { pub network: Option, } +#[derive(Args)] +pub struct UploadArgs { + /// Path to the compiled WASM file + #[arg(long)] + pub wasm: String, + /// Network to use + #[arg(long, default_value = "testnet", value_parser = ["testnet", "mainnet"])] + pub network: String, + /// Wallet name to use for signing + #[arg(long)] + pub wallet: Option, +} + pub fn handle(cmd: ContractCommands) -> Result<()> { match cmd { ContractCommands::Invoke(args) => handle_invoke(args), ContractCommands::Inspect(args) => handle_inspect(args), + ContractCommands::Upload(args) => handle_upload(args), } } @@ -262,6 +280,60 @@ fn handle_invoke(args: InvokeArgs) -> Result<()> { Ok(()) } +fn handle_upload(args: UploadArgs) -> Result<()> { + config::validate_network(&args.network)?; + + p::header("Upload WASM to Stellar Network"); + p::separator(); + p::kv("WASM", &args.wasm); + p::kv("Network", &args.network); + + if args.network == "mainnet" { + p::warn("You are uploading on MAINNET. This will cost real XLM."); + } + + let cfg = config::load()?; + let wallet = if let Some(ref name) = args.wallet { + cfg.wallets + .iter() + .find(|w| &w.name == name) + .ok_or_else(|| { + anyhow::anyhow!("Wallet '{}' not found. Run `starforge wallet list`", name) + })? + .clone() + } else if !cfg.wallets.is_empty() { + p::info(&format!( + "No --wallet specified. Using: {}", + cfg.wallets[0].name.cyan() + )); + cfg.wallets[0].clone() + } else { + anyhow::bail!( + "No wallets found. Create one first:\n starforge wallet create deployer --fund" + ); + }; + + p::kv("Wallet", &wallet.name); + p::separator(); + + println!(); + p::step(1, 1, "Uploading WASM binary…"); + + let wasm_hash = soroban::upload_wasm(&args.wasm, &args.network, &wallet)?; + + println!(); + p::kv_accent("WASM Hash", &wasm_hash); + p::success("WASM uploaded successfully."); + println!(); + p::info("Next step — create the contract instance:"); + p::info(&format!( + " stellar contract deploy --wasm-hash {} --network {} --source {}", + wasm_hash, args.network, wallet.name + )); + println!(); + Ok(()) +} + fn resolve_network(network_override: Option) -> Result { let network = network_override.unwrap_or(config::load()?.network); match network.as_str() { diff --git a/src/utils/soroban.rs b/src/utils/soroban.rs index e1c3c07c..cd8dfb3a 100644 --- a/src/utils/soroban.rs +++ b/src/utils/soroban.rs @@ -150,6 +150,44 @@ pub fn submit_transaction( Ok(TransactionResult { hash, return_value }) } +pub fn upload_wasm( + wasm_path: &str, + network: &str, + wallet: &crate::utils::config::WalletEntry, +) -> Result { + use std::process::Command; + + let rpc_url = get_rpc_url(network); + + let output = Command::new("stellar") + .args([ + "contract", + "upload", + "--wasm", + wasm_path, + "--rpc-url", + &rpc_url, + "--source", + &wallet.name, + "--network-passphrase", + if network == "mainnet" { + "Public Global Stellar Network ; September 2015" + } else { + "Test SDF Network ; September 2015" + }, + ]) + .output() + .context("Failed to run `stellar contract upload`. Is the Stellar CLI installed?")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("WASM upload failed: {}", stderr.trim()); + } + + let wasm_hash = String::from_utf8_lossy(&output.stdout).trim().to_string(); + Ok(wasm_hash) +} + pub fn inspect_contract(contract_id: &str, network: &str) -> Result { let ledger_key = build_contract_instance_key(contract_id)?; let ledger_key_xdr = ledger_key_to_xdr_base64(&ledger_key)?;