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
57 changes: 57 additions & 0 deletions API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,12 +624,23 @@ starforge contract inspect <CONTRACT_ID> [OPTIONS]

**Options:**
- `--network <NETWORK>` - Network to use
- `--json` - Print machine-readable JSON output

**Example:**
```bash
starforge contract inspect CCPYZFKEAXHHS5VVW5J45TOU7S2EODJ7TZNJIA5LKDVL3PESCES6FNCI
```

**JSON schema (`--json`):**
- `contract_id` (string)
- `executable` (string)
- `wasm_hash` (string|null)
- `storage_durability` (string)
- `latest_ledger` (number)
- `last_modified_ledger_seq` (number|null)
- `live_until_ledger_seq` (number|null)
- `instance_storage` (array of objects): `{ "key": string, "value": string }`

---

### `starforge deploy`
Expand All @@ -646,6 +657,7 @@ starforge deploy --wasm <FILE> [OPTIONS]
- `--network <NETWORK>` - Network to deploy to (`testnet`, `mainnet`)
- `--wallet <NAME>` - Wallet name to use for deployment
- `--yes` - Skip confirmation prompt
- `--execute` - Execute `stellar contract deploy ...` when `stellar` CLI is on PATH (default is dry-run)

**Examples:**
```bash
Expand All @@ -660,6 +672,9 @@ starforge deploy \

# Skip confirmation (for CI)
starforge deploy --wasm ./my_contract.wasm --yes

# Execute immediately (requires stellar CLI on PATH)
starforge deploy --wasm ./my_contract.wasm --execute
```

---
Expand Down Expand Up @@ -868,6 +883,8 @@ starforge shell --contract <WASM>

**Options:**
- `--contract <WASM>` - Path to compiled contract
- `--no-history` - Disable persistent history for this session
- `--history-max-lines <N>` - Max lines to keep in `~/.starforge/repl_history` (default: 1000)

**Example:**
```bash
Expand Down Expand Up @@ -955,6 +972,46 @@ starforge gas optimize --target <INPUT> --output <OUTPUT>
- `--target <INPUT>` - Input wasm file (required)
- `--output <OUTPUT>` - Output wasm file (required)

#### `starforge gas diff`

Compare two wasm builds side-by-side and diff estimated simulation cost.

**Usage:**
```bash
starforge gas diff <OLD_WASM> <NEW_WASM>
```

**Arguments:**
- `<OLD_WASM>` - Baseline wasm file
- `<NEW_WASM>` - Candidate wasm file

**Output includes:**
- Old/new wasm size
- Old/new estimated simulation cost
- Delta and percentage change
- Profiling timings per analysis step

---

### `starforge inspect storage`

List decoded storage entries for a contract scope.

**Usage:**
```bash
starforge inspect storage <CONTRACT_ID> [OPTIONS]
```

**Options:**
- `--scope <SCOPE>` - `instance`, `persistent`, or `temporary`
- `--network <NETWORK>` - Network to use (`testnet`, `mainnet`)
- `--json` - Print machine-readable JSON output

**JSON schema (`--json`):**
- `contract_id` (string)
- `scope` (string)
- `entries` (array of objects): `{ "key": string, "value": string }`

---

### `starforge benchmark`
Expand Down
15 changes: 13 additions & 2 deletions src/commands/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ pub struct InspectArgs {
/// Network to use; defaults to the global config network
#[arg(long, value_parser = ["testnet", "mainnet"])]
pub network: Option<String>,
/// Output as JSON
#[arg(long)]
pub json: bool,
}

#[derive(Args)]
Expand Down Expand Up @@ -85,6 +88,11 @@ fn handle_inspect(args: InspectArgs) -> Result<()> {
p::step(1, 1, "Querying contract instance from Soroban RPC…");
let inspect = soroban::inspect_contract(&args.contract_id, &network)?;

if args.json {
println!("{}", serde_json::to_string_pretty(&inspect)?);
return Ok(());
}

println!();
p::kv_accent("Contract ID", &inspect.contract_id);
p::kv("Executable", &inspect.executable);
Expand Down Expand Up @@ -247,10 +255,13 @@ fn handle_invoke(args: InvokeArgs) -> Result<()> {
if args.submit {
if let Some(mut wallet) = wallet {
println!();

if let Some(sk) = &wallet.secret_key {
if sk.contains(':') {
let pwd = crypto::prompt_password(&format!("Enter password to decrypt wallet '{}'", wallet.name), false)?;
let pwd = crypto::prompt_password(
&format!("Enter password to decrypt wallet '{}'", wallet.name),
false,
)?;
let plain_sk = crypto::decrypt_secret(&pwd, sk)?;
wallet.secret_key = Some(plain_sk);
}
Expand Down
73 changes: 70 additions & 3 deletions src/commands/deploy.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::utils::{config, horizon, print as p, soroban};
use crate::commands::info;
use crate::utils::{config, horizon, print as p};
use anyhow::Result;
use clap::Args;
use colored::*;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

const SOROBAN_WASM_LIMIT_KB: f64 = 128.0;

Expand All @@ -21,9 +23,9 @@ pub struct DeployArgs {
/// Skip confirmation prompt
#[arg(long, default_value = "false")]
pub yes: bool,
/// Simulate the deploy transaction first and show estimated Soroban fee / errors
/// Execute deployment immediately if Stellar CLI is installed
#[arg(long, default_value = "false")]
pub simulate: bool,
pub execute: bool,
}

fn is_wasm_above_size_limit(wasm_size_kb: f64) -> bool {
Expand All @@ -46,6 +48,19 @@ fn build_stellar_deploy_command(wasm: &std::path::Path, source: &str, network: &
)
}

fn build_stellar_deploy_args(wasm: &std::path::Path, source: &str, network: &str) -> Vec<String> {
vec![
"contract".to_string(),
"deploy".to_string(),
"--wasm".to_string(),
wasm.display().to_string(),
"--source".to_string(),
source.to_string(),
"--network".to_string(),
network.to_string(),
]
}

pub fn handle(args: DeployArgs) -> Result<()> {
p::header("Deploy Soroban Contract");

Expand Down Expand Up @@ -183,6 +198,36 @@ pub fn handle(args: DeployArgs) -> Result<()> {
println!(" {}", line.cyan());
}
println!();
if args.execute {
let stellar_path = info::detect_stellar_cli().ok_or_else(|| {
anyhow::anyhow!(
"Cannot execute deploy: Stellar CLI not found on PATH.\nInstall it from https://developers.stellar.org/docs/tools/stellar-cli"
)
})?;

p::info(&format!(
"Executing with Stellar CLI at {}",
stellar_path.display()
));
let cmd_args = build_stellar_deploy_args(&args.wasm, &wallet.public_key, &args.network);
let output = Command::new(stellar_path).args(&cmd_args).output()?;
if output.status.success() {
p::success("Deployment command executed successfully.");
let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.trim().is_empty() {
println!("{}", stdout.trim());
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"Stellar CLI deployment failed (exit: {}). {}",
output.status,
stderr.trim()
);
}
} else {
p::info("Dry-run mode (default): command not executed. Use --execute to run it.");
}
p::info("Install the Stellar CLI: https://developers.stellar.org/docs/tools/stellar-cli");
p::separator();

Expand Down Expand Up @@ -224,6 +269,28 @@ mod tests {
assert!(command.contains("--network testnet"));
}

#[test]
fn builds_expected_deploy_args() {
let args = build_stellar_deploy_args(
std::path::Path::new("target/release/token.wasm"),
"GABCDEF1234567890",
"testnet",
);
assert_eq!(
args,
vec![
"contract",
"deploy",
"--wasm",
"target/release/token.wasm",
"--source",
"GABCDEF1234567890",
"--network",
"testnet"
]
);
}

#[test]
fn flags_large_wasm_sizes() {
assert!(!is_wasm_above_size_limit(127.9));
Expand Down
75 changes: 75 additions & 0 deletions src/commands/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,20 @@ pub enum GasCommands {
#[arg(long)]
output: PathBuf,
},
/// Compare two wasm builds and diff estimated simulation costs
Diff {
/// Path to the baseline wasm
old_wasm: PathBuf,
/// Path to the candidate wasm
new_wasm: PathBuf,
},
}

pub fn handle(cmd: GasCommands) -> Result<()> {
match cmd {
GasCommands::Analyze { wasm, network } => analyze(wasm, network),
GasCommands::Optimize { target, output } => optimize(target, output),
GasCommands::Diff { old_wasm, new_wasm } => diff(old_wasm, new_wasm),
}
}

Expand Down Expand Up @@ -81,3 +89,70 @@ fn optimize(target: PathBuf, output: PathBuf) -> Result<()> {
p::kv("Duration", &format!("{:?}", elapsed));
Ok(())
}

fn diff(old_wasm: PathBuf, new_wasm: PathBuf) -> Result<()> {
config::validate_file_path(&old_wasm, Some("wasm"))?;
config::validate_file_path(&new_wasm, Some("wasm"))?;

p::header("Gas Diff");
p::kv("Old wasm", &old_wasm.display().to_string());
p::kv("New wasm", &new_wasm.display().to_string());

let mut profile = profiler::Profiler::start();
let old_report = optimizer::analyze_wasm(&old_wasm)?;
profile.mark("analyze_old");
let new_report = optimizer::analyze_wasm(&new_wasm)?;
profile.mark("analyze_new");

let old_est = estimate_simulation_cost(old_report.size_bytes);
let new_est = estimate_simulation_cost(new_report.size_bytes);
let delta = new_est as i64 - old_est as i64;
let pct = if old_est == 0 {
0.0
} else {
(delta as f64 / old_est as f64) * 100.0
};

println!();
p::separator();
p::kv("Old size (bytes)", &old_report.size_bytes.to_string());
p::kv("New size (bytes)", &new_report.size_bytes.to_string());
p::kv("Old est. sim cost", &old_est.to_string());
p::kv("New est. sim cost", &new_est.to_string());
p::kv(
"Estimated delta",
&format!(
"{} ({:+.2}%)",
if delta >= 0 {
format!("+{}", delta)
} else {
delta.to_string()
},
pct
),
);
p::kv(
"Result",
if delta < 0 {
"Improved (lower estimated cost)"
} else if delta > 0 {
"Regressed (higher estimated cost)"
} else {
"No change"
},
);
for point in profile.points() {
p::kv(
&format!("Step {}", point.label),
&format!("{:?}", point.elapsed),
);
}
p::kv("Total profile", &format!("{:?}", profile.total_elapsed()));
p::separator();

Ok(())
}

fn estimate_simulation_cost(size_bytes: usize) -> u64 {
2_000 + (size_bytes as u64 / 8)
}
Loading