Skip to content

Commit 4e42fa4

Browse files
authored
Merge pull request #67 from xqcxx/feat/multisig-gas-plugins-testing
Add multisig, gas analyzer, plugin registry, and contract test command
2 parents 1008c49 + 96b1a27 commit 4e42fa4

16 files changed

Lines changed: 802 additions & 221 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ libloading = "0.8.1"
3636
uuid = { version = "1.6.1", features = ["v4"] }
3737
hidapi = { version = "2.6.5", optional = true }
3838
hex = "0.4.3"
39+
sha2 = "0.10"
3940

4041
[features]
4142
hardware-wallet = ["dep:hidapi"]

src/commands/gas.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use crate::utils::{config, optimizer, print as p, profiler};
2+
use anyhow::Result;
3+
use clap::Subcommand;
4+
use std::path::PathBuf;
5+
6+
#[derive(Subcommand)]
7+
pub enum GasCommands {
8+
/// Analyze a compiled Soroban contract for gas/cpu opportunities
9+
Analyze {
10+
/// Path to the compiled wasm
11+
wasm: PathBuf,
12+
/// Network context (used for fee heuristics)
13+
#[arg(long)]
14+
network: Option<String>,
15+
},
16+
/// Emit an "optimized" wasm (lightweight, heuristic-based)
17+
Optimize {
18+
/// Path to the input wasm
19+
#[arg(long)]
20+
target: PathBuf,
21+
/// Output path for optimized wasm
22+
#[arg(long)]
23+
output: PathBuf,
24+
},
25+
}
26+
27+
pub fn handle(cmd: GasCommands) -> Result<()> {
28+
match cmd {
29+
GasCommands::Analyze { wasm, network } => analyze(wasm, network),
30+
GasCommands::Optimize { target, output } => optimize(target, output),
31+
}
32+
}
33+
34+
fn analyze(wasm: PathBuf, network: Option<String>) -> Result<()> {
35+
config::validate_file_path(&wasm, Some("wasm"))?;
36+
37+
let cfg = config::load()?;
38+
let network = network.unwrap_or(cfg.network);
39+
config::validate_network(&network)?;
40+
41+
p::header("Gas Analyzer");
42+
p::kv("Network", &network);
43+
p::kv("Wasm", &wasm.display().to_string());
44+
45+
let t = profiler::Timer::start();
46+
let report = optimizer::analyze_wasm(&wasm)?;
47+
let elapsed = t.elapsed();
48+
49+
println!();
50+
p::separator();
51+
p::kv_accent("Size (bytes)", &report.size_bytes.to_string());
52+
p::kv("SHA256", &report.sha256);
53+
p::kv("Heuristic score", &report.score.to_string());
54+
if !report.suggestions.is_empty() {
55+
println!();
56+
p::info("Suggestions:");
57+
for s in &report.suggestions {
58+
println!(" - {}", s);
59+
}
60+
}
61+
p::separator();
62+
p::kv("Duration", &format!("{:?}", elapsed));
63+
Ok(())
64+
}
65+
66+
fn optimize(target: PathBuf, output: PathBuf) -> Result<()> {
67+
config::validate_file_path(&target, Some("wasm"))?;
68+
69+
p::header("Gas Optimizer");
70+
p::kv("Input", &target.display().to_string());
71+
p::kv("Output", &output.display().to_string());
72+
73+
let t = profiler::Timer::start();
74+
let result = optimizer::optimize_wasm(&target, &output)?;
75+
let elapsed = t.elapsed();
76+
77+
println!();
78+
p::success("Optimization output written");
79+
p::kv("Bytes in", &result.input_size_bytes.to_string());
80+
p::kv("Bytes out", &result.output_size_bytes.to_string());
81+
p::kv("Duration", &format!("{:?}", elapsed));
82+
Ok(())
83+
}

src/commands/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
pub mod completions;
22
pub mod contract;
33
pub mod deploy;
4+
pub mod gas;
45
pub mod info;
56
pub mod invoke;
67
pub mod monitor;
78
pub mod network;
89
pub mod new;
10+
pub mod plugin;
911
pub mod shell;
12+
pub mod test;
1013
pub mod tx;
1114
pub mod wallet;
1215
pub mod tutorial;

src/commands/plugin.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use crate::plugins::{registry, PluginManager};
2+
use crate::utils::print as p;
3+
use anyhow::{Context, Result};
4+
use clap::Subcommand;
5+
use std::path::PathBuf;
6+
7+
#[derive(Subcommand)]
8+
pub enum PluginCommands {
9+
/// Register a plugin shared library for StarForge to load
10+
Install {
11+
/// Plugin name (used as the command name)
12+
name: String,
13+
/// Path to the plugin shared library (.so/.dylib/.dll)
14+
#[arg(long)]
15+
path: Option<PathBuf>,
16+
},
17+
/// List installed plugins from the local registry
18+
List,
19+
/// Load installed plugins and show those successfully loaded
20+
Load,
21+
}
22+
23+
pub fn handle(cmd: PluginCommands) -> Result<()> {
24+
match cmd {
25+
PluginCommands::Install { name, path } => install(name, path),
26+
PluginCommands::List => list(),
27+
PluginCommands::Load => load(),
28+
}
29+
}
30+
31+
fn install(name: String, path: Option<PathBuf>) -> Result<()> {
32+
let lib_path = registry::resolve_plugin_library_path(&name, path)?;
33+
registry::install_plugin(&name, &lib_path)?;
34+
35+
p::header("Plugin Install");
36+
p::success("Plugin registered");
37+
p::kv_accent("Name", &name);
38+
p::kv("Library", &lib_path.display().to_string());
39+
p::info("Load plugins with: starforge plugin load");
40+
Ok(())
41+
}
42+
43+
fn list() -> Result<()> {
44+
p::header("Installed Plugins");
45+
let reg = registry::load_registry().unwrap_or_default();
46+
if reg.plugins.is_empty() {
47+
p::info("No plugins installed. Use: starforge plugin install <name> --path <lib>");
48+
return Ok(());
49+
}
50+
51+
p::separator();
52+
for (i, pl) in reg.plugins.iter().enumerate() {
53+
println!(" {:>2}. {}", i + 1, pl.name);
54+
p::kv("Path", &pl.path);
55+
if i < reg.plugins.len() - 1 {
56+
println!();
57+
}
58+
}
59+
p::separator();
60+
Ok(())
61+
}
62+
63+
fn load() -> Result<()> {
64+
p::header("Plugin Loader");
65+
66+
let reg = registry::load_registry().unwrap_or_default();
67+
if reg.plugins.is_empty() {
68+
p::info("No plugins installed. Use: starforge plugin install <name> --path <lib>");
69+
return Ok(());
70+
}
71+
72+
let mut pm = PluginManager::new();
73+
for pl in &reg.plugins {
74+
unsafe {
75+
pm.load_plugin(&pl.path)
76+
.with_context(|| format!("Failed to load plugin '{}' from {}", pl.name, pl.path))?;
77+
}
78+
}
79+
80+
let loaded = pm.list_plugins();
81+
if loaded.is_empty() {
82+
p::warn("No plugins loaded.");
83+
return Ok(());
84+
}
85+
86+
p::separator();
87+
for (name, desc) in loaded {
88+
p::kv_accent(name, desc);
89+
}
90+
p::separator();
91+
Ok(())
92+
}

src/commands/test.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use crate::utils::{config, print as p, test_runner};
2+
use anyhow::Result;
3+
use clap::Args;
4+
use std::path::PathBuf;
5+
6+
#[derive(Args)]
7+
pub struct TestArgs {
8+
/// Path to the compiled wasm
9+
#[arg(long)]
10+
pub wasm: PathBuf,
11+
12+
/// Collect a lightweight coverage report (heuristic)
13+
#[arg(long, default_value = "false")]
14+
pub coverage: bool,
15+
16+
/// Output report format (e.g. html, json)
17+
#[arg(long)]
18+
pub report: Option<String>,
19+
}
20+
21+
pub fn handle(args: TestArgs) -> Result<()> {
22+
config::validate_file_path(&args.wasm, Some("wasm"))?;
23+
24+
p::header("Contract Test Runner");
25+
p::kv("Wasm", &args.wasm.display().to_string());
26+
p::kv("Coverage", if args.coverage { "yes" } else { "no" });
27+
if let Some(r) = &args.report {
28+
p::kv("Report", r);
29+
}
30+
31+
let result = test_runner::run_contract_tests(&args.wasm, test_runner::TestOptions {
32+
coverage: args.coverage,
33+
report_format: args.report.clone(),
34+
})?;
35+
36+
println!();
37+
p::separator();
38+
p::kv_accent("SHA256", &result.sha256);
39+
p::kv("Wasm bytes", &result.size_bytes.to_string());
40+
p::kv("Cases executed", &result.cases_executed.to_string());
41+
p::kv("Failures", &result.failures.to_string());
42+
if let Some(path) = &result.report_path {
43+
p::kv("Report path", &path.display().to_string());
44+
}
45+
p::separator();
46+
47+
if result.failures > 0 {
48+
anyhow::bail!("Some contract tests failed");
49+
}
50+
51+
p::success("All contract tests passed");
52+
Ok(())
53+
}
54+

0 commit comments

Comments
 (0)