Skip to content

Commit 987e638

Browse files
authored
Merge pull request #142 from CMI-James-OD/codex/issue-33-13-30-16
feat: add gas diff, deploy execute, repl history, and inspect json
2 parents 4084e54 + 677bf3a commit 987e638

9 files changed

Lines changed: 354 additions & 25 deletions

File tree

API_REFERENCE.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,12 +624,23 @@ starforge contract inspect <CONTRACT_ID> [OPTIONS]
624624

625625
**Options:**
626626
- `--network <NETWORK>` - Network to use
627+
- `--json` - Print machine-readable JSON output
627628

628629
**Example:**
629630
```bash
630631
starforge contract inspect CCPYZFKEAXHHS5VVW5J45TOU7S2EODJ7TZNJIA5LKDVL3PESCES6FNCI
631632
```
632633

634+
**JSON schema (`--json`):**
635+
- `contract_id` (string)
636+
- `executable` (string)
637+
- `wasm_hash` (string|null)
638+
- `storage_durability` (string)
639+
- `latest_ledger` (number)
640+
- `last_modified_ledger_seq` (number|null)
641+
- `live_until_ledger_seq` (number|null)
642+
- `instance_storage` (array of objects): `{ "key": string, "value": string }`
643+
633644
---
634645

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

650662
**Examples:**
651663
```bash
@@ -660,6 +672,9 @@ starforge deploy \
660672

661673
# Skip confirmation (for CI)
662674
starforge deploy --wasm ./my_contract.wasm --yes
675+
676+
# Execute immediately (requires stellar CLI on PATH)
677+
starforge deploy --wasm ./my_contract.wasm --execute
663678
```
664679

665680
---
@@ -868,6 +883,8 @@ starforge shell --contract <WASM>
868883

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

872889
**Example:**
873890
```bash
@@ -955,6 +972,46 @@ starforge gas optimize --target <INPUT> --output <OUTPUT>
955972
- `--target <INPUT>` - Input wasm file (required)
956973
- `--output <OUTPUT>` - Output wasm file (required)
957974

975+
#### `starforge gas diff`
976+
977+
Compare two wasm builds side-by-side and diff estimated simulation cost.
978+
979+
**Usage:**
980+
```bash
981+
starforge gas diff <OLD_WASM> <NEW_WASM>
982+
```
983+
984+
**Arguments:**
985+
- `<OLD_WASM>` - Baseline wasm file
986+
- `<NEW_WASM>` - Candidate wasm file
987+
988+
**Output includes:**
989+
- Old/new wasm size
990+
- Old/new estimated simulation cost
991+
- Delta and percentage change
992+
- Profiling timings per analysis step
993+
994+
---
995+
996+
### `starforge inspect storage`
997+
998+
List decoded storage entries for a contract scope.
999+
1000+
**Usage:**
1001+
```bash
1002+
starforge inspect storage <CONTRACT_ID> [OPTIONS]
1003+
```
1004+
1005+
**Options:**
1006+
- `--scope <SCOPE>` - `instance`, `persistent`, or `temporary`
1007+
- `--network <NETWORK>` - Network to use (`testnet`, `mainnet`)
1008+
- `--json` - Print machine-readable JSON output
1009+
1010+
**JSON schema (`--json`):**
1011+
- `contract_id` (string)
1012+
- `scope` (string)
1013+
- `entries` (array of objects): `{ "key": string, "value": string }`
1014+
9581015
---
9591016

9601017
### `starforge benchmark`

src/commands/contract.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ pub struct InspectArgs {
4545
/// Network to use; defaults to the global config network
4646
#[arg(long, value_parser = ["testnet", "mainnet"])]
4747
pub network: Option<String>,
48+
/// Output as JSON
49+
#[arg(long)]
50+
pub json: bool,
4851
}
4952

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

91+
if args.json {
92+
println!("{}", serde_json::to_string_pretty(&inspect)?);
93+
return Ok(());
94+
}
95+
8896
println!();
8997
p::kv_accent("Contract ID", &inspect.contract_id);
9098
p::kv("Executable", &inspect.executable);
@@ -247,10 +255,13 @@ fn handle_invoke(args: InvokeArgs) -> Result<()> {
247255
if args.submit {
248256
if let Some(mut wallet) = wallet {
249257
println!();
250-
258+
251259
if let Some(sk) = &wallet.secret_key {
252260
if sk.contains(':') {
253-
let pwd = crypto::prompt_password(&format!("Enter password to decrypt wallet '{}'", wallet.name), false)?;
261+
let pwd = crypto::prompt_password(
262+
&format!("Enter password to decrypt wallet '{}'", wallet.name),
263+
false,
264+
)?;
254265
let plain_sk = crypto::decrypt_secret(&pwd, sk)?;
255266
wallet.secret_key = Some(plain_sk);
256267
}

src/commands/deploy.rs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
use crate::utils::{config, horizon, print as p, soroban};
1+
use crate::commands::info;
2+
use crate::utils::{config, horizon, print as p};
23
use anyhow::Result;
34
use clap::Args;
45
use colored::*;
56
use std::fs;
67
use std::path::PathBuf;
8+
use std::process::Command;
79

810
const SOROBAN_WASM_LIMIT_KB: f64 = 128.0;
911

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

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

51+
fn build_stellar_deploy_args(wasm: &std::path::Path, source: &str, network: &str) -> Vec<String> {
52+
vec![
53+
"contract".to_string(),
54+
"deploy".to_string(),
55+
"--wasm".to_string(),
56+
wasm.display().to_string(),
57+
"--source".to_string(),
58+
source.to_string(),
59+
"--network".to_string(),
60+
network.to_string(),
61+
]
62+
}
63+
4964
pub fn handle(args: DeployArgs) -> Result<()> {
5065
p::header("Deploy Soroban Contract");
5166

@@ -183,6 +198,36 @@ pub fn handle(args: DeployArgs) -> Result<()> {
183198
println!(" {}", line.cyan());
184199
}
185200
println!();
201+
if args.execute {
202+
let stellar_path = info::detect_stellar_cli().ok_or_else(|| {
203+
anyhow::anyhow!(
204+
"Cannot execute deploy: Stellar CLI not found on PATH.\nInstall it from https://developers.stellar.org/docs/tools/stellar-cli"
205+
)
206+
})?;
207+
208+
p::info(&format!(
209+
"Executing with Stellar CLI at {}",
210+
stellar_path.display()
211+
));
212+
let cmd_args = build_stellar_deploy_args(&args.wasm, &wallet.public_key, &args.network);
213+
let output = Command::new(stellar_path).args(&cmd_args).output()?;
214+
if output.status.success() {
215+
p::success("Deployment command executed successfully.");
216+
let stdout = String::from_utf8_lossy(&output.stdout);
217+
if !stdout.trim().is_empty() {
218+
println!("{}", stdout.trim());
219+
}
220+
} else {
221+
let stderr = String::from_utf8_lossy(&output.stderr);
222+
anyhow::bail!(
223+
"Stellar CLI deployment failed (exit: {}). {}",
224+
output.status,
225+
stderr.trim()
226+
);
227+
}
228+
} else {
229+
p::info("Dry-run mode (default): command not executed. Use --execute to run it.");
230+
}
186231
p::info("Install the Stellar CLI: https://developers.stellar.org/docs/tools/stellar-cli");
187232
p::separator();
188233

@@ -224,6 +269,28 @@ mod tests {
224269
assert!(command.contains("--network testnet"));
225270
}
226271

272+
#[test]
273+
fn builds_expected_deploy_args() {
274+
let args = build_stellar_deploy_args(
275+
std::path::Path::new("target/release/token.wasm"),
276+
"GABCDEF1234567890",
277+
"testnet",
278+
);
279+
assert_eq!(
280+
args,
281+
vec![
282+
"contract",
283+
"deploy",
284+
"--wasm",
285+
"target/release/token.wasm",
286+
"--source",
287+
"GABCDEF1234567890",
288+
"--network",
289+
"testnet"
290+
]
291+
);
292+
}
293+
227294
#[test]
228295
fn flags_large_wasm_sizes() {
229296
assert!(!is_wasm_above_size_limit(127.9));

src/commands/gas.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,20 @@ pub enum GasCommands {
2222
#[arg(long)]
2323
output: PathBuf,
2424
},
25+
/// Compare two wasm builds and diff estimated simulation costs
26+
Diff {
27+
/// Path to the baseline wasm
28+
old_wasm: PathBuf,
29+
/// Path to the candidate wasm
30+
new_wasm: PathBuf,
31+
},
2532
}
2633

2734
pub fn handle(cmd: GasCommands) -> Result<()> {
2835
match cmd {
2936
GasCommands::Analyze { wasm, network } => analyze(wasm, network),
3037
GasCommands::Optimize { target, output } => optimize(target, output),
38+
GasCommands::Diff { old_wasm, new_wasm } => diff(old_wasm, new_wasm),
3139
}
3240
}
3341

@@ -81,3 +89,70 @@ fn optimize(target: PathBuf, output: PathBuf) -> Result<()> {
8189
p::kv("Duration", &format!("{:?}", elapsed));
8290
Ok(())
8391
}
92+
93+
fn diff(old_wasm: PathBuf, new_wasm: PathBuf) -> Result<()> {
94+
config::validate_file_path(&old_wasm, Some("wasm"))?;
95+
config::validate_file_path(&new_wasm, Some("wasm"))?;
96+
97+
p::header("Gas Diff");
98+
p::kv("Old wasm", &old_wasm.display().to_string());
99+
p::kv("New wasm", &new_wasm.display().to_string());
100+
101+
let mut profile = profiler::Profiler::start();
102+
let old_report = optimizer::analyze_wasm(&old_wasm)?;
103+
profile.mark("analyze_old");
104+
let new_report = optimizer::analyze_wasm(&new_wasm)?;
105+
profile.mark("analyze_new");
106+
107+
let old_est = estimate_simulation_cost(old_report.size_bytes);
108+
let new_est = estimate_simulation_cost(new_report.size_bytes);
109+
let delta = new_est as i64 - old_est as i64;
110+
let pct = if old_est == 0 {
111+
0.0
112+
} else {
113+
(delta as f64 / old_est as f64) * 100.0
114+
};
115+
116+
println!();
117+
p::separator();
118+
p::kv("Old size (bytes)", &old_report.size_bytes.to_string());
119+
p::kv("New size (bytes)", &new_report.size_bytes.to_string());
120+
p::kv("Old est. sim cost", &old_est.to_string());
121+
p::kv("New est. sim cost", &new_est.to_string());
122+
p::kv(
123+
"Estimated delta",
124+
&format!(
125+
"{} ({:+.2}%)",
126+
if delta >= 0 {
127+
format!("+{}", delta)
128+
} else {
129+
delta.to_string()
130+
},
131+
pct
132+
),
133+
);
134+
p::kv(
135+
"Result",
136+
if delta < 0 {
137+
"Improved (lower estimated cost)"
138+
} else if delta > 0 {
139+
"Regressed (higher estimated cost)"
140+
} else {
141+
"No change"
142+
},
143+
);
144+
for point in profile.points() {
145+
p::kv(
146+
&format!("Step {}", point.label),
147+
&format!("{:?}", point.elapsed),
148+
);
149+
}
150+
p::kv("Total profile", &format!("{:?}", profile.total_elapsed()));
151+
p::separator();
152+
153+
Ok(())
154+
}
155+
156+
fn estimate_simulation_cost(size_bytes: usize) -> u64 {
157+
2_000 + (size_bytes as u64 / 8)
158+
}

0 commit comments

Comments
 (0)