diff --git a/README.md b/README.md index e419963..2320d6a 100644 --- a/README.md +++ b/README.md @@ -243,10 +243,13 @@ cargo test --workspace > The commands below match `crates/tools/src/main.rs` and the canonical > status table in > [`docs/deployment.md`](docs/deployment.md#known-limitations--cli-status). -> `deploy` and `invoke` are currently stubs in the CLI binary; -> use the native `stellar contract …` commands or `make deploy-testnet` -> instead. `account` is deprecated but still functional — it delegates to -> `keypair` commands with a deprecation warning. `config init`, +> `deploy` is currently a stub in the CLI binary; +> use `make deploy-testnet` or `bash scripts/deploy.sh testnet` instead. +> `invoke` is now fully wired — set `CONTRACT_ID` (or `SOROBAN_CONTRACT_ID`) +> and `SOROBAN_SOURCE` (or `STELLAR_SECRET_KEY`), then run +> `milestonex-cli invoke [args...]` directly. +> `account` is deprecated but still functional — it delegates to +> `keypair` commands with a deprecation banner. `config init`, > `contract-id`, `build-donation-tx`, `submit-tx`, `verify-tx`, > `prepare-wallet-signing`, and `complete-wallet-signing` shown in older > docs are **not implemented** — see issue @@ -361,14 +364,20 @@ Expected output: ### Step 4: Invoke the ping Method -> The in-CLI `invoke` command is also a stub. Use `stellar contract invoke` -> natively against your deployed contract ID. - ```bash # Read the contract ID that Step 3 wrote out CONTRACT_ID=$(cat .milestonex_contract_id) -# Invoke a contract method (replace `version` with any contract method such as `ping`) +# Invoke a contract method using the CLI (invoke is now fully wired) +milestonex-cli invoke version + +# Or invoke with additional args (e.g., get_dashboard_metrics for campaign ID 1) +milestonex-cli invoke get_dashboard_metrics 1 +``` + +Or call `stellar contract invoke` natively if you prefer: + +```bash stellar contract invoke \ --id "$CONTRACT_ID" \ --source test_account \ @@ -423,7 +432,7 @@ stellar contract invoke \ - **"WASM file not found"**: Run `make build-wasm` to build the contracts first. - **"Unknown command" or "coming soon"**: You ran an `milestonex-cli` command - that is still a stub (`deploy`, `invoke`). Run + that is still a stub (`deploy`). Run `cargo run -p milestonex-tools` with no arguments to see which commands are actually implemented, and follow [`docs/deployment.md`](docs/deployment.md#known-limitations--cli-status). diff --git a/crates/tools/src/main.rs b/crates/tools/src/main.rs index 41e4591..9cc67e1 100644 --- a/crates/tools/src/main.rs +++ b/crates/tools/src/main.rs @@ -88,13 +88,16 @@ fn print_available_commands() { println!(" keypair - Master/distribution keypair lifecycle"); println!(" signing - Build donation/campaign/custom signing requests"); println!(" response - Process/validate/save signed wallet responses"); + println!(" invoke [args] - Invoke a method on the deployed campaign contract"); + println!(" Requires: CONTRACT_ID (or SOROBAN_CONTRACT_ID) and"); + println!(" SOROBAN_SOURCE (or STELLAR_SECRET_KEY) env vars."); println!(); println!("Stubs (no-op placeholders, do not rely on in production):"); println!(" deploy - Stub. Use `stellar contract deploy` or `make deploy-testnet`."); - println!(" invoke - Stub. Use `stellar contract invoke` natively."); println!(); - println!("Deprecated (still functional, but will be removed):"); - println!(" account - Deprecated. Use `keypair generate-master|fund` instead."); + println!("Deprecated (still functional via delegation, will be removed):"); + println!(" account create - Deprecated alias for `keypair generate-master`."); + println!(" account fund - Deprecated alias for `keypair fund`."); println!(); println!("Run `milestonex-cli ` (no subcommand) for usage details."); println!("Full status of every command mentioned in docs: docs/deployment.md."); @@ -178,70 +181,203 @@ fn handle_deploy() -> Result<()> { } fn handle_invoke(args: &[String]) -> Result<()> { - println!("🔄 The 'invoke' command is a stub and is NOT yet implemented in this binary."); + use std::process::Command; + if args.is_empty() { - println!("💡 Invoke natively with:"); - println!(" stellar contract invoke \\"); - println!(" --id \\"); - println!(" --source \\"); - println!(" --network testnet \\"); - println!(" -- [args...]"); + println!("🔄 Invoke a method on the deployed MilestoneX campaign contract."); + println!(); + println!("Usage: milestonex-cli invoke [--arg ]..."); + println!(); + println!("Required environment variables:"); + println!(" CONTRACT_ID — the deployed contract address (or set SOROBAN_CONTRACT_ID)"); + println!(" SOROBAN_SOURCE — source keypair name / secret key (or set STELLAR_SECRET_KEY)"); + println!(" SOROBAN_NETWORK — testnet | mainnet (default: testnet)"); + println!(); + println!("Examples:"); + println!(" CONTRACT_ID=CB7... SOROBAN_SOURCE=alice \\"); + println!(" milestonex-cli invoke version"); + println!(); + println!(" CONTRACT_ID=CB7... SOROBAN_SOURCE=alice \\"); + println!(" milestonex-cli invoke get_dashboard_metrics --arg '1'"); + println!(); + println!("Canonical contract methods (milestonex-campaign):"); + println!(" version — return contract version"); + println!(" get_dashboard_metrics — aggregate metrics for a campaign"); + println!(" get_campaign_report — full campaign report"); + println!(" get_platform_summary — platform-wide statistics"); + println!(" get_donation_count — number of donations for a campaign"); + println!(" get_donor_count — unique donor count"); + println!(" get_release_count — milestone release count"); + println!(" get_total_tx_count — total transaction count"); + return Ok(()); + } + + let method = &args[0]; + + // Resolve contract ID — prefer explicit env var, then .milestonex_contract_id file. + let contract_id = env::var("CONTRACT_ID") + .or_else(|_| env::var("SOROBAN_CONTRACT_ID")) + .or_else(|_| { + std::fs::read_to_string(".milestonex_contract_id") + .map(|s| s.trim().to_string()) + .map_err(|_| env::VarError::NotPresent) + }) + .context( + "CONTRACT_ID not set. Set the CONTRACT_ID env var, or run `make deploy-testnet` \ + first — it writes the deployed address to .milestonex_contract_id automatically.", + )?; + + // Resolve source key — prefer SOROBAN_SOURCE, then STELLAR_SECRET_KEY. + let source = env::var("SOROBAN_SOURCE") + .or_else(|_| env::var("STELLAR_SECRET_KEY")) + .context( + "SOROBAN_SOURCE not set. Set SOROBAN_SOURCE (keypair name) or STELLAR_SECRET_KEY \ + (secret key) so stellar contract invoke can sign the transaction.", + )?; + + let network = env::var("SOROBAN_NETWORK").unwrap_or_else(|_| "testnet".to_string()); + + // --- Build the stellar contract invoke argument list --- + // Positional args after the method name are passed as-is after `--`. + // They can be raw JSON scalars (numbers, strings, booleans) or JSON objects. + // stellar-cli already speaks JSON on the command line, so we pass them through. + let method_args: Vec = args[1..].to_vec(); + + // Validate JSON args so we surface mistakes before shelling out. + for (i, arg) in method_args.iter().enumerate() { + // Skip flag-style args like "--arg" that precede the value. + if arg.starts_with('-') { + continue; + } + // Try to parse as JSON; fall back gracefully (stellar-cli handles plain strings too). + if !arg.is_empty() { + if let Err(_) = serde_json::from_str::(arg) { + // Not valid JSON — treat as a plain string literal (stellar-cli accepts this). + // Only warn if it looks like the user meant JSON (starts with { or [). + if arg.starts_with('{') || arg.starts_with('[') { + println!( + "⚠️ Argument {} '{}' looks like JSON but failed to parse. \ + Passing as-is — stellar-cli may reject it.", + i + 1, + arg + ); + } + } + } + } + + println!("🔄 Invoking '{}' on network: {}", method, network); + println!("📝 Contract ID: {}", contract_id); + println!("🔑 Source: {}", source); + if !method_args.is_empty() { + println!("📥 Args: {}", method_args.join(" ")); + } + println!(); + + // Build the command: + // stellar contract invoke \ + // --id \ + // --source \ + // --network \ + // -- [method_args...] + let mut cmd = Command::new("stellar"); + cmd.arg("contract") + .arg("invoke") + .arg("--id") + .arg(&contract_id) + .arg("--source") + .arg(&source) + .arg("--network") + .arg(&network) + .arg("--") + .arg(method); + + for arg in &method_args { + cmd.arg(arg); + } + + let output = cmd.output().context( + "Failed to execute `stellar contract invoke`. \ + Is stellar-cli installed? Run: cargo install --locked stellar-cli --features opt", + )?; + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + println!("✅ Invocation successful!"); + if !stdout.trim().is_empty() { + println!("📤 Result: {}", stdout.trim()); + } } else { - println!("💡 You asked to invoke '{}'. Run it natively for now:", args[0]); - println!(" stellar contract invoke \\"); - println!(" --id \\"); - println!(" --source \\"); - println!(" --network testnet \\"); - println!(" -- {} [args...]", args[0]); + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + println!("❌ Invocation failed (exit code: {})", output.status); + if !stdout.trim().is_empty() { + println!("stdout: {}", stdout.trim()); + } + if !stderr.trim().is_empty() { + println!("stderr: {}", stderr.trim()); + } + anyhow::bail!("stellar contract invoke exited with error"); } - println!("🔗 Tracked in: https://github.com/MillestoneX/MilestoneX-Contracts/issues/37"); + Ok(()) } fn handle_account(args: &[String]) -> Result<()> { - println!("⚠️ DEPRECATION WARNING: The 'account' namespace is deprecated and will be removed in a future release."); - println!("💡 Please use the 'keypair' namespace instead. See below for mapping."); + // Always show the deprecation banner — this namespace is a thin alias kept + // only for backward compatibility with scripts written before Issue #31 + // introduced the `keypair` sub-command. + println!("╔══════════════════════════════════════════════════════════════╗"); + println!("║ ⚠️ DEPRECATION WARNING ║"); + println!("║ The 'account' sub-command is deprecated and will be removed ║"); + println!("║ in a future release. Please migrate to the 'keypair' ║"); + println!("║ namespace: ║"); + println!("║ ║"); + println!("║ account create → keypair generate-master ║"); + println!("║ account fund → keypair fund ║"); + println!("╚══════════════════════════════════════════════════════════════╝"); println!(); if args.is_empty() { - println!("👤 Account Management Commands (Deprecated)"); - println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - println!("Usage: milestonex-cli account "); + println!("Usage: milestonex-cli account [args...]"); println!(); - println!("Commands (deprecated - use 'keypair' instead):"); - println!(" create - Create master keypair (use 'keypair generate-master')"); - println!(" fund - Fund account on testnet (use 'keypair fund')"); + println!("Commands (deprecated — use 'keypair' instead):"); + println!(" create — alias for 'keypair generate-master'"); + println!(" fund — alias for 'keypair fund'"); println!(); - println!("💡 Migrate to:"); - println!(" milestonex-cli keypair generate-master"); - println!(" milestonex-cli keypair fund "); + println!("Migrate now:"); + println!(" milestonex-cli keypair generate-master"); + println!(" milestonex-cli keypair fund "); return Ok(()); } - // Map account commands to keypair commands for backward compatibility + // Transparently delegate to the canonical keypair handlers. match args[0].as_str() { "create" => { - println!("🔄 'account create' is deprecated. Delegating to 'keypair generate-master'..."); + println!("🔄 Delegating 'account create' → 'keypair generate-master'"); println!(); handle_keypair(&["generate-master".to_string()])?; } "fund" => { if args.len() < 3 { println!("Usage: milestonex-cli account fund "); - println!("💡 Please use: milestonex-cli keypair fund "); + println!(); + println!("Preferred form:"); + println!(" milestonex-cli keypair fund "); return Ok(()); } - println!("🔄 'account fund' is deprecated. Delegating to 'keypair fund'..."); + println!("🔄 Delegating 'account fund' → 'keypair fund'"); println!(); handle_keypair(&["fund".to_string(), args[1].clone(), args[2].clone()])?; } _ => { - println!("❌ Unknown account command: {}", args[0]); - println!("💡 The 'account' namespace is deprecated. Available commands:"); - println!(" account create (use 'keypair generate-master')"); - println!(" account fund (use 'keypair fund')"); + println!("❌ Unknown account sub-command: '{}'", args[0]); + println!(); + println!("The 'account' namespace only aliases:"); + println!(" account create → keypair generate-master"); + println!(" account fund → keypair fund "); println!(); - println!("🔗 See https://github.com/MillestoneX/MilestoneX-Contracts/issues/37 for details"); + println!("Run 'milestonex-cli keypair' for the full keypair command list."); } } diff --git a/docs/deployment.md b/docs/deployment.md index 2fd278b..74283c3 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -157,10 +157,10 @@ to. Tracker: [issue #37](https://github.com/MillestoneX/MilestoneX-Contracts/iss | `milestonex-cli signing …` | ✅ Implemented | `handle_signing` (5 sub-commands) | Use as-is | | `milestonex-cli response …` | ✅ Implemented | `handle_response` (5 sub-commands) | Use as-is | | `milestonex-cli deploy` | ⚠️ **Stub** | `handle_deploy` prints an "NOT yet implemented" banner | Use `make deploy-testnet` or `bash scripts/deploy.sh testnet` | -| `milestonex-cli invoke ` | ⚠️ **Stub** | `handle_invoke` prints an "NOT yet implemented" banner | Use `stellar contract invoke --id $CONTRACT_ID --source --network testnet -- [args…]` | -| `milestonex-cli account` | ⚠️ **Deprecated** | `handle_account` delegates to `keypair` with deprecation warning | Use `milestonex-cli keypair generate-master` (creation) or `keypair fund` (testnet funding) | -| `milestonex-cli account create` | ⚠️ **Deprecated** | Delegates to `keypair generate-master` with deprecation warning | Use `milestonex-cli keypair generate-master` | -| `milestonex-cli account fund` | ⚠️ **Deprecated** | Delegates to `keypair fund` with deprecation warning | Use `milestonex-cli keypair fund ` | +| `milestonex-cli invoke ` | ✅ **Implemented** | `handle_invoke` shells out to `stellar contract invoke`; reads `CONTRACT_ID` / `SOROBAN_CONTRACT_ID` / `.milestonex_contract_id` and `SOROBAN_SOURCE` / `STELLAR_SECRET_KEY` from env | Set `CONTRACT_ID` and `SOROBAN_SOURCE`, then run `milestonex-cli invoke version` or `milestonex-cli invoke get_dashboard_metrics 1` | +| `milestonex-cli account` | ⚠️ **Deprecated** | `handle_account` delegates to `keypair` with a prominent deprecation banner | Use `milestonex-cli keypair generate-master` (creation) or `keypair fund` (testnet funding) | +| `milestonex-cli account create` | ⚠️ **Deprecated** | Transparently delegates to `keypair generate-master` with deprecation banner | Use `milestonex-cli keypair generate-master` | +| `milestonex-cli account fund` | ⚠️ **Deprecated** | Transparently delegates to `keypair fund` with deprecation banner | Use `milestonex-cli keypair fund ` | | `milestonex-cli config init` | ❌ **Missing** | not in the dispatcher → `Unknown command` | Run `cp .env.example .env` and edit it manually | | `milestonex-cli config check` | ❌ **Missing** (subcommand) | top-level `config` prints + validates everything already | Use `milestonex-cli config` | | `milestonex-cli contract-id` | ❌ **Missing** | not in the dispatcher → `Unknown command` | Use `cat .milestonex_contract_id` or `cat deployments/.json` |