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
16 changes: 16 additions & 0 deletions docs/COMMAND_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ starforge contract generate-bindings ./token.wasm --lang rust
| `--fixture <FILE>` | JSON/TOML contract test suite with fixtures, mocks, and assertions |
| `--source <FILE>` | Contract source used for generated tests or coverage |
| `--coverage` | Include source coverage summary |
| `--coverage-out <FILE>` | Write a dedicated coverage report |
| `--coverage-format html\|json\|markdown\|text` | Format for `--coverage-out` |
| `--coverage-goal <PCT>` | Minimum overall coverage percentage |
| `--function-coverage-goal <PCT>` | Minimum function coverage percentage |
| `--line-coverage-goal <PCT>` | Minimum line coverage percentage |
| `--branch-coverage-goal <PCT>` | Minimum branch coverage percentage |
| `--coverage-ci` | Fail when configured coverage goals are missed |
| `--coverage-ci-workflow-out <FILE>` | Generate a GitHub Actions coverage workflow |
| `--report html\|json\|junit` | Write a test report (`junit` is available for fixture suites) |
| `--testnet` | Validate Soroban testnet integration for the run |
| `--testnet-dry-run` | Validate testnet configuration without probing RPC health |
Expand All @@ -123,11 +131,19 @@ starforge contract generate-bindings ./token.wasm --lang rust
starforge test --wasm ./target/contract.wasm \
--fixture ./contract-tests.json --coverage --source ./src/lib.rs --report html

starforge test --wasm ./target/contract.wasm --source ./src/lib.rs \
--coverage --coverage-out coverage.html --coverage-format html \
--coverage-ci --coverage-goal 85 --branch-coverage-goal 70

starforge test --wasm ./target/contract.wasm --source ./src/lib.rs \
--coverage-ci-workflow-out .github/workflows/starforge-coverage.yml

starforge test --wasm ./target/contract.wasm \
--fixture ./contract-tests.toml --testnet --testnet-dry-run
```

Fixture suites support named storage fixtures, mocked contract calls, and assertions such as `state_equals`, `state_exists`, `return_equals`, `event_emitted`, `fee_at_most`, and `mock_called`.
Coverage analysis tracks Soroban contract functions, line spans, branch paths, uncovered functions, threshold goals, and HTML/JSON/Markdown/text reports.

---

Expand Down
5 changes: 4 additions & 1 deletion src/commands/backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,10 @@ async fn handle_contract_state(args: ContractStateArgs) -> Result<()> {
p::kv("Contract", &manifest.contract_id);
p::kv("Network", &manifest.source_network);
p::kv("Latest ledger", &manifest.latest_ledger.to_string());
p::kv("State entries", &manifest.instance_storage.len().to_string());
p::kv(
"State entries",
&manifest.instance_storage.len().to_string(),
);
p::kv("Checksum", &manifest.checksum);
p::success("Contract state backup created and verified");
Ok(())
Expand Down
36 changes: 26 additions & 10 deletions src/commands/bridge.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
use crate::utils::bridge::{
load_config, load_transfers, providers::{self, BridgeTransferRequest, TransferStatus},
record_transfer, routes::RouteRegistry, save_config, security::SecurityVerifier,
state::StateSynchronizer, monitoring::BridgeMonitor, BridgeConfig, BridgeTransferRecord,
load_config, load_transfers,
monitoring::BridgeMonitor,
providers::{self, BridgeTransferRequest, TransferStatus},
record_transfer,
routes::RouteRegistry,
save_config,
security::SecurityVerifier,
state::StateSynchronizer,
BridgeConfig, BridgeTransferRecord,
};
use crate::utils::print as p;
use anyhow::Result;
Expand Down Expand Up @@ -255,7 +261,10 @@ fn handle_status(args: StatusArgs) -> Result<()> {

p::kv("Transfer ID", &record.id);
p::kv("Status", &status.to_string());
p::kv("Source", &format!("{} → {}", record.source_network, record.asset));
p::kv(
"Source",
&format!("{} → {}", record.source_network, record.asset),
);
p::kv("Dest", &record.dest_network);
p::kv("Amount", &record.amount.to_string());
if let Some(ref tx) = record.tx_hash_source {
Expand Down Expand Up @@ -315,10 +324,11 @@ fn handle_routes(args: RoutesArgs) -> Result<()> {
fn handle_configure(args: ConfigureArgs) -> Result<()> {
let mut config = load_config()?;

if args.show || (!args.enable.is_some()
&& args.default_provider.is_none()
&& args.max_amount.is_none()
&& args.require_proof.is_none())
if args.show
|| (args.enable.is_none()
&& args.default_provider.is_none()
&& args.max_amount.is_none()
&& args.require_proof.is_none())
{
p::header("Bridge Configuration");
p::kv("Enabled", &config.enabled.to_string());
Expand Down Expand Up @@ -371,7 +381,10 @@ fn handle_sync(args: SyncArgs) -> Result<()> {
p::kv("Dest ledger", &args.dest_ledger.to_string());
p::kv("In sync", &sync.is_in_sync(1000).to_string());
p::kv("Pending", &sync.state().pending_transfers.len().to_string());
p::kv("Completed", &sync.state().completed_transfers.len().to_string());
p::kv(
"Completed",
&sync.state().completed_transfers.len().to_string(),
);
p::success("State synchronized");
Ok(())
}
Expand Down Expand Up @@ -431,7 +444,10 @@ fn handle_monitor(args: MonitorArgs) -> Result<()> {
}

p::header("Bridge Monitoring");
p::kv("Unacknowledged alerts", &monitor.unacknowledged_count().to_string());
p::kv(
"Unacknowledged alerts",
&monitor.unacknowledged_count().to_string(),
);

if args.json {
println!("{}", serde_json::to_string_pretty(monitor.alerts())?);
Expand Down
48 changes: 32 additions & 16 deletions src/commands/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,7 @@ async fn handle_start(args: StartArgs) -> Result<()> {
}
sess.set_variables(vars);

let mut frames = Vec::new();
frames.push(debugger::StackFrame {
let frames = vec![debugger::StackFrame {
function: "contract_init".to_string(),
contract_id: Some(cid.clone()),
source_location: None,
Expand All @@ -173,7 +172,7 @@ async fn handle_start(args: StartArgs) -> Result<()> {
value: e.value.clone(),
})
.collect(),
});
}];
sess.set_call_stack(frames);
sess.add_step_history("Debug session started".to_string());
}
Expand Down Expand Up @@ -283,21 +282,33 @@ async fn handle_step(args: StepArgs) -> Result<()> {
debugger.step_out();
p::success("Stepping out of current function…");
}
_ => anyhow::bail!("Invalid step direction '{}'. Use 'into', 'over', or 'out'.", args.direction),
_ => anyhow::bail!(
"Invalid step direction '{}'. Use 'into', 'over', or 'out'.",
args.direction
),
}

debugger.session.add_step_history(format!("step {}", args.direction));
debugger
.session
.add_step_history(format!("step {}", args.direction));

simulate_step(debugger).await?;
simulate_step(debugger)?;

Ok(())
}

async fn simulate_step(debugger: &mut Debugger) -> Result<()> {
let current_fn = debugger.session.current_function.clone().unwrap_or_default();
fn simulate_step(debugger: &mut Debugger) -> Result<()> {
let current_fn = debugger
.session
.current_function
.clone()
.unwrap_or_default();

p::separator();
p::kv_accent("Current Depth", &debugger.session.call_stack.len().to_string());
p::kv_accent(
"Current Depth",
&debugger.session.call_stack.len().to_string(),
);
p::kv_accent("Step Count", &debugger.session.step_count.to_string());

if !current_fn.is_empty() {
Expand Down Expand Up @@ -449,7 +460,11 @@ async fn handle_stack() -> Result<()> {
if frames.is_empty() {
p::info("Call stack is empty.");
} else {
p::header(&format!("Call Stack ({} frame{})", frames.len(), if frames.len() == 1 { "" } else { "s" }));
p::header(&format!(
"Call Stack ({} frame{})",
frames.len(),
if frames.len() == 1 { "" } else { "s" }
));
p::separator();
for (i, frame) in frames.iter().enumerate() {
let depth = frames.len() - i;
Expand Down Expand Up @@ -587,7 +602,12 @@ async fn handle_ui(args: UiArgs) -> Result<()> {
async fn manage_breakpoints_interactive() -> Result<()> {
let selection = Select::new()
.with_prompt("Breakpoint Action")
.items(&["List Breakpoints", "Add Breakpoint", "Remove Breakpoint", "Go Back"])
.items(&[
"List Breakpoints",
"Add Breakpoint",
"Remove Breakpoint",
"Go Back",
])
.default(0)
.interact()?;

Expand All @@ -596,9 +616,7 @@ async fn manage_breakpoints_interactive() -> Result<()> {
handle_breakpoint(BreakpointCommands::List).await?;
}
1 => {
let function: String = Input::new()
.with_prompt("Function name")
.interact_text()?;
let function: String = Input::new().with_prompt("Function name").interact_text()?;
let contract_id: String = Input::new()
.with_prompt("Contract ID (optional)")
.allow_empty(true)
Expand Down Expand Up @@ -635,5 +653,3 @@ async fn manage_breakpoints_interactive() -> Result<()> {
}
Ok(())
}


40 changes: 27 additions & 13 deletions src/commands/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,8 @@ pub async fn handle(args: DeployArgs) -> Result<()> {
wasm_size_kb,
wallet,
&args.network,
).await;
)
.await;
}

if args.simulate {
Expand Down Expand Up @@ -422,15 +423,17 @@ pub async fn handle(args: DeployArgs) -> Result<()> {
let pb = p::progress_bar(3, "Starting deployment steps...");

pb.set_message("Verifying account on-chain...");
let account = horizon::fetch_account(&wallet.public_key, &args.network).await.map_err(|e| {
pb.abandon();
anyhow::anyhow!(
"Account not active on {}: {}\nFund it with: starforge wallet fund {}",
args.network,
e,
wallet.name
)
})?;
let account = horizon::fetch_account(&wallet.public_key, &args.network)
.await
.map_err(|e| {
pb.abandon();
anyhow::anyhow!(
"Account not active on {}: {}\nFund it with: starforge wallet fund {}",
args.network,
e,
wallet.name
)
})?;

let xlm = account
.balances
Expand Down Expand Up @@ -495,7 +498,12 @@ pub async fn handle(args: DeployArgs) -> Result<()> {
p::error(&format!("Stellar CLI deployment failed: {}", stderr));

// Automatic rollback safety net: revert to the last good deployment.
handle_failed_deploy_rollback(args.no_auto_rollback, previous, &wallet.name, &args.network)?;
handle_failed_deploy_rollback(
args.no_auto_rollback,
previous,
&wallet.name,
&args.network,
)?;

anyhow::bail!("Stellar CLI deployment failed: {}", stderr);
}
Expand Down Expand Up @@ -567,12 +575,18 @@ mod tests {
let id = format!("C{}", "A".repeat(55));
assert_eq!(id.len(), 56);
let stdout = format!("ℹ️ Simulating deploy...\nℹ️ Submitting...\n{}\n", id);
assert_eq!(parse_contract_id_from_stdout(&stdout).as_deref(), Some(id.as_str()));
assert_eq!(
parse_contract_id_from_stdout(&stdout).as_deref(),
Some(id.as_str())
);
}

#[test]
fn returns_none_when_no_contract_id_present() {
assert_eq!(parse_contract_id_from_stdout("deploy failed: timeout"), None);
assert_eq!(
parse_contract_id_from_stdout("deploy failed: timeout"),
None
);
// A 56-char wallet public key (G...) must not be mistaken for a contract id.
let gkey = format!("G{}", "A".repeat(55));
assert_eq!(gkey.len(), 56);
Expand Down
33 changes: 20 additions & 13 deletions src/commands/deployments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,20 +305,24 @@ async fn handle_verify(args: VerifyArgs) -> Result<()> {
if let Some(wallet) = cfg.wallets.iter().find(|w| w.name == record.wallet) {
match horizon::fetch_account(&wallet.public_key, &record.network).await {
Ok(_) => {
report.checks.push(crate::utils::deployment_verify::VerificationCheck {
name: "wallet_active".to_string(),
category: "functionality".to_string(),
status: crate::utils::deployment_verify::CheckStatus::Passed,
detail: format!("Wallet '{}' is active on-chain", record.wallet),
});
report
.checks
.push(crate::utils::deployment_verify::VerificationCheck {
name: "wallet_active".to_string(),
category: "functionality".to_string(),
status: crate::utils::deployment_verify::CheckStatus::Passed,
detail: format!("Wallet '{}' is active on-chain", record.wallet),
});
}
Err(e) => {
report.checks.push(crate::utils::deployment_verify::VerificationCheck {
name: "wallet_active".to_string(),
category: "functionality".to_string(),
status: crate::utils::deployment_verify::CheckStatus::Warning,
detail: format!("Could not verify wallet: {}", e),
});
report
.checks
.push(crate::utils::deployment_verify::VerificationCheck {
name: "wallet_active".to_string(),
category: "functionality".to_string(),
status: crate::utils::deployment_verify::CheckStatus::Warning,
detail: format!("Could not verify wallet: {}", e),
});
}
}
}
Expand Down Expand Up @@ -349,7 +353,10 @@ async fn handle_verify(args: VerifyArgs) -> Result<()> {
.iter()
.filter(|c| c.status == crate::utils::deployment_verify::CheckStatus::Passed)
.count();
p::kv("Checks passed", &format!("{}/{}", passed_count, report.checks.len()));
p::kv(
"Checks passed",
&format!("{}/{}", passed_count, report.checks.len()),
);
}

if args.report || args.save {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
pub mod analytics;
pub mod audit;
pub mod backup;
pub mod bridge;
pub mod benchmark;
pub mod bridge;
pub mod command_tree;
pub mod completions;
pub mod config;
Expand Down
40 changes: 23 additions & 17 deletions src/commands/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,29 @@ pub async fn handle(args: MonitorArgs) -> Result<()> {
println!();

match (&args.contract, &args.wallet) {
(Some(contract_id), None) => monitor_contract(
contract_id,
args.events.as_deref(),
args.event_type.as_deref(),
args.topic.as_deref(),
args.value.as_deref(),
network,
args.interval,
args.follow,
).await,
(None, Some(wallet_name)) => monitor_wallet(
wallet_name,
args.threshold,
args.balance_alert,
network,
args.interval,
).await,
(Some(contract_id), None) => {
monitor_contract(
contract_id,
args.events.as_deref(),
args.event_type.as_deref(),
args.topic.as_deref(),
args.value.as_deref(),
network,
args.interval,
args.follow,
)
.await
}
(None, Some(wallet_name)) => {
monitor_wallet(
wallet_name,
args.threshold,
args.balance_alert,
network,
args.interval,
)
.await
}
_ => anyhow::bail!("Specify either --contract or --wallet (but not both)"),
}
}
Expand Down
Loading
Loading