From e75d37710078055835d6cd5f430406e2ed16b287 Mon Sep 17 00:00:00 2001 From: femiolutayo Date: Thu, 1 Oct 2026 21:54:56 +0000 Subject: [PATCH] Support wasm32v1-none contract builds --- .github/workflows/templates.yml | 24 ++++-- .github/workflows/verify.yml | 2 +- docs/COMMAND_REFERENCE.md | 4 +- docs/DEPLOYMENT_CHECKPOINTS.md | 2 +- docs/GAS_OPTIMIZATION_BEST_PRACTICES.md | 4 +- docs/GAS_OPTIMIZATION_GUIDE.md | 25 ++---- docs/MANIFEST_MIGRATION.md | 3 +- docs/ROLLBACK_TESTING.md | 4 +- docs/TEMPLATE_CONTRIBUTING.md | 2 +- docs/TEMPLATE_MARKETPLACE.md | 2 +- docs/manifest.md | 7 +- docs/native-deploy.md | 2 +- docs/plugins/cookbook.md | 2 +- src/commands/ai_deploy_docs.rs | 18 ++-- src/commands/contract.rs | 15 ++-- src/commands/deploy.rs | 21 ++--- src/commands/doctor.rs | 34 ++++++++ src/commands/new.rs | 34 ++++---- src/commands/test.rs | 17 ++-- src/commands/verify.rs | 57 ++++++++----- src/manifest.rs | 67 +++++++++++++-- src/utils/ai_conversation.rs | 2 +- src/utils/ai_debugger.rs | 12 +-- src/utils/ai_doc_qa.rs | 2 +- src/utils/ai_docs.rs | 6 +- src/utils/ai_ide_integration.rs | 17 ++-- src/utils/gas_profiler/suggest.rs | 3 +- src/utils/help_metadata.rs | 8 +- src/utils/mod.rs | 1 + src/utils/mutation.rs | 2 - src/utils/template_integration.rs | 2 +- src/utils/test_automation.rs | 4 +- src/utils/test_coverage.rs | 4 +- src/utils/wasm_target.rs | 88 ++++++++++++++++++++ templates/examples/custom-account/README.md | 2 +- templates/examples/dao-governance/README.md | 2 +- templates/examples/escrow/README.md | 2 +- templates/examples/multisig-vault/README.md | 2 +- templates/examples/nft/README.md | 2 +- templates/examples/sep41-token/README.md | 2 +- templates/examples/simple-counter/README.md | 2 +- templates/examples/staking/README.md | 2 +- templates/examples/token-allowlist/README.md | 2 +- 43 files changed, 354 insertions(+), 161 deletions(-) create mode 100644 src/utils/wasm_target.rs diff --git a/.github/workflows/templates.yml b/.github/workflows/templates.yml index 7918f65a..bb89e001 100644 --- a/.github/workflows/templates.yml +++ b/.github/workflows/templates.yml @@ -7,6 +7,8 @@ on: - "src/utils/templates.rs" - "src/commands/new.rs" - "src/commands/add.rs" + - "src/manifest.rs" + - "src/utils/wasm_target.rs" - "templates/components/**" - "Cargo.toml" - "Cargo.lock" @@ -19,6 +21,8 @@ on: - "src/utils/templates.rs" - "src/commands/new.rs" - "src/commands/add.rs" + - "src/manifest.rs" + - "src/utils/wasm_target.rs" - "templates/components/**" - "Cargo.toml" - "Cargo.lock" @@ -66,16 +70,22 @@ jobs: fail-fast: false matrix: template: ${{ fromJSON(needs.discover.outputs.templates) }} + toolchain: + - version: 1.80.0 + target: wasm32-unknown-unknown + - version: stable + target: wasm32v1-none env: CARGO_TERM_COLOR: always STARFORGE_NON_INTERACTIVE: "1" CARGO_TARGET_DIR: ${{ github.workspace }}/project/target + WASM_TARGET: ${{ matrix.toolchain.target }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - targets: wasm32-unknown-unknown + toolchain: ${{ matrix.toolchain.version }} + targets: ${{ matrix.toolchain.target }} - uses: actions/download-artifact@v4 with: name: starforge-cli @@ -95,7 +105,7 @@ jobs: run: cargo test - name: Build template for WASM working-directory: project - run: cargo build --target wasm32-unknown-unknown --release + run: cargo build --target "$WASM_TARGET" --release - name: Record WASM size id: wasm-size working-directory: project @@ -104,7 +114,7 @@ jobs: run: | python3 - <<'PY' import glob, json, os - files = sorted(glob.glob('target/wasm32-unknown-unknown/release/*.wasm')) + files = sorted(glob.glob(f"target/{os.environ['WASM_TARGET']}/release/*.wasm")) if not files: raise SystemExit('No WASM files found in release target directory') with open(f"../wasm-size-{os.environ['TEMPLATE_NAME']}.json", 'w', encoding='utf-8') as out: @@ -117,7 +127,7 @@ jobs: PY - uses: actions/upload-artifact@v4 with: - name: wasm-size-${{ matrix.template }} + name: wasm-size-${{ matrix.template }}-${{ matrix.toolchain.version }} path: wasm-size-${{ matrix.template }}.json if-no-files-found: error retention-days: 7 @@ -170,7 +180,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - targets: wasm32-unknown-unknown + targets: wasm32v1-none - uses: actions/download-artifact@v4 with: name: starforge-cli @@ -192,7 +202,7 @@ jobs: run: cargo test - name: Build modified contract for WASM working-directory: component-project - run: cargo build --target wasm32-unknown-unknown --release + run: cargo build --target wasm32v1-none --release templates-ok: name: templates-ok diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 0c05fea9..6265a84d 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -35,7 +35,7 @@ jobs: - name: Run formal verification (if WASM artifact exists) run: | - WASM="target/wasm32-unknown-unknown/release/contract.wasm" + WASM="target/wasm32v1-none/release/contract.wasm" if [ -f "$WASM" ]; then cargo run -- contract verify \ --wasm "$WASM" \ diff --git a/docs/COMMAND_REFERENCE.md b/docs/COMMAND_REFERENCE.md index bc920977..dcadf48b 100644 --- a/docs/COMMAND_REFERENCE.md +++ b/docs/COMMAND_REFERENCE.md @@ -494,10 +494,10 @@ Full reference: [SIMULATION_RESOURCES.md](SIMULATION_RESOURCES.md) and | `advanced-perf generate-dashboard ` | Show the recorded-metrics performance dashboard | ```bash norun -starforge contract profile profile ./target/wasm32-unknown-unknown/release/token.wasm \ +starforge contract profile profile ./target/wasm32v1-none/release/token.wasm \ --label token --dashboard ./target/token-profile.html -starforge contract profile profile ./target/wasm32-unknown-unknown/release/token.wasm \ +starforge contract profile profile ./target/wasm32v1-none/release/token.wasm \ --baseline ~/.starforge/contract_profiles/profile-abc123def456.json \ --output ./target/token-profile.json ``` diff --git a/docs/DEPLOYMENT_CHECKPOINTS.md b/docs/DEPLOYMENT_CHECKPOINTS.md index a0f4acf9..28ca707c 100644 --- a/docs/DEPLOYMENT_CHECKPOINTS.md +++ b/docs/DEPLOYMENT_CHECKPOINTS.md @@ -23,7 +23,7 @@ StarForge automatically persists deployment progress to disk, enabling deploymen When running deployment automation: ```bash norun -starforge deployment-automate run --wasm ./target/wasm32-unknown-unknown/release/my_contract.wasm --network testnet +starforge deployment-automate run --wasm ./target/wasm32v1-none/release/my_contract.wasm --network testnet ``` If the run is interrupted at step 3, simply re-run the exact same command. StarForge will output: diff --git a/docs/GAS_OPTIMIZATION_BEST_PRACTICES.md b/docs/GAS_OPTIMIZATION_BEST_PRACTICES.md index 082bee1a..4bf5b209 100644 --- a/docs/GAS_OPTIMIZATION_BEST_PRACTICES.md +++ b/docs/GAS_OPTIMIZATION_BEST_PRACTICES.md @@ -7,7 +7,7 @@ A practical guide to writing cheaper, faster Soroban contracts, and to using ```bash norun # Profile a single contract -starforge gas analyze ./target/wasm32-unknown-unknown/release/my_contract.wasm +starforge gas analyze ./target/wasm32v1-none/release/my_contract.wasm # Export a shareable HTML report starforge gas analyze ./my_contract.wasm --format html --output report.html @@ -19,7 +19,7 @@ starforge gas analyze ./my_contract.wasm --format json --output report.json starforge gas diff ./old.wasm ./new.wasm # Benchmark every contract in a directory -starforge gas benchmark --dir ./target/wasm32-unknown-unknown/release +starforge gas benchmark --dir ./target/wasm32v1-none/release ``` ## What drives cost diff --git a/docs/GAS_OPTIMIZATION_GUIDE.md b/docs/GAS_OPTIMIZATION_GUIDE.md index 29d96d0d..56ac3313 100644 --- a/docs/GAS_OPTIMIZATION_GUIDE.md +++ b/docs/GAS_OPTIMIZATION_GUIDE.md @@ -10,7 +10,7 @@ improvements. ```bash norun # Profile a compiled contract -starforge gas profile ./target/wasm32-unknown-unknown/release/my_contract.wasm +starforge gas profile ./target/wasm32v1-none/release/my_contract.wasm # Compare two builds starforge gas compare baseline.wasm optimized.wasm @@ -154,7 +154,7 @@ debug = [] Build for production without the `debug` feature: ```bash norun -cargo build --release --target wasm32-unknown-unknown +cargo build --release --target wasm32v1-none ``` --- @@ -247,23 +247,10 @@ impl MyContract { ### Continuous profiling in CI -```yaml -# .github/workflows/ci.yml -- name: Build contract - run: cargo build --release --target wasm32-unknown-unknown - -- name: Gas profile - run: | - starforge gas profile \ - target/wasm32-unknown-unknown/release/my_contract.wasm \ - --fail-on-critical - -- name: Gas comparison (on PRs) - if: github.event_name == 'pull_request' - run: | - starforge gas compare \ - artifacts/baseline.wasm \ - target/wasm32-unknown-unknown/release/my_contract.wasm +```bash +cargo build --release --target wasm32v1-none +starforge gas profile target/wasm32v1-none/release/my_contract.wasm --fail-on-critical +starforge gas compare artifacts/baseline.wasm target/wasm32v1-none/release/my_contract.wasm ``` ### Reading the optimization score diff --git a/docs/MANIFEST_MIGRATION.md b/docs/MANIFEST_MIGRATION.md index 0cbd555f..856db6a2 100644 --- a/docs/MANIFEST_MIGRATION.md +++ b/docs/MANIFEST_MIGRATION.md @@ -34,8 +34,7 @@ Map your contracts and WASM build outputs under `[contracts]`: ```toml [contracts.my_contract] path = "contracts/my_contract" -wasm = "target/wasm32-unknown-unknown/release/my_contract.wasm" -build = "cargo build --target wasm32-unknown-unknown --release" +build = "starforge contract build" ``` ### Step 3: Define Custom Networks and Deploy Targets diff --git a/docs/ROLLBACK_TESTING.md b/docs/ROLLBACK_TESTING.md index 15f7140c..fafd59f8 100644 --- a/docs/ROLLBACK_TESTING.md +++ b/docs/ROLLBACK_TESTING.md @@ -10,9 +10,9 @@ Run the default rollback scenario: ```bash norun starforge test \ - --wasm target/wasm32-unknown-unknown/release/contract_v2.wasm \ + --wasm target/wasm32v1-none/release/contract_v2.wasm \ --rollback \ - --previous-wasm target/wasm32-unknown-unknown/release/contract_v1.wasm \ + --previous-wasm target/wasm32v1-none/release/contract_v1.wasm \ --report json ``` diff --git a/docs/TEMPLATE_CONTRIBUTING.md b/docs/TEMPLATE_CONTRIBUTING.md index a3dbbe30..37f671f3 100644 --- a/docs/TEMPLATE_CONTRIBUTING.md +++ b/docs/TEMPLATE_CONTRIBUTING.md @@ -9,7 +9,7 @@ This document covers every step from writing your template to getting it accepte Before opening a pull request, confirm each item: -- [ ] Template compiles with `cargo build` targeting `wasm32-unknown-unknown` +- [ ] Template compiles for `wasm32v1-none` on stable and `wasm32-unknown-unknown` on the Rust 1.80 MSRV - [ ] Template passes its own test suite (`cargo test`) - [ ] Template source uses `{{PROJECT_NAME_PASCAL}}` as the contract struct name - [ ] A `README.md` is included describing the contract and its public functions diff --git a/docs/TEMPLATE_MARKETPLACE.md b/docs/TEMPLATE_MARKETPLACE.md index 340a7201..25162c78 100644 --- a/docs/TEMPLATE_MARKETPLACE.md +++ b/docs/TEMPLATE_MARKETPLACE.md @@ -276,7 +276,7 @@ twice and diffing the results — the output will be byte-identical. ```bash norun cd my-dex stellar contract build - starforge deploy --wasm target/wasm32-unknown-unknown/release/my_dex.wasm + starforge deploy --wasm target/wasm32v1-none/release/my_dex.wasm ``` ### For Template Authors diff --git a/docs/manifest.md b/docs/manifest.md index 2a70cec5..f847d16a 100644 --- a/docs/manifest.md +++ b/docs/manifest.md @@ -25,6 +25,7 @@ Previously, StarForge settings were spread across command-line flags, global use |---|---|---| | `version` | `string` | **Required.** Manifest schema version. Must be `"1"`. | | `package` | `table` | Optional project metadata. | +| `wasm_target` | `string` | Optional Soroban WASM target override. Defaults to the target supported by the active Rust toolchain. | | `contracts` | `table` | Smart contract target specifications. | | `networks` | `table` | Custom network endpoints and overrides. | | `deploy` | `table` | Target deployment environments (`deploy.testnet`, `deploy.mainnet`, etc.). | @@ -36,6 +37,7 @@ Previously, StarForge settings were spread across command-line flags, global use ```toml version = "1" +wasm_target = "wasm32v1-none" [package] name = "my_soroban_project" @@ -46,8 +48,7 @@ license = "MIT" [contracts.my_contract] path = "." -wasm = "target/wasm32-unknown-unknown/release/my_contract.wasm" -build = "cargo build --target wasm32-unknown-unknown --release" +build = "starforge contract build" [networks.testnet] horizon_url = "https://horizon-testnet.stellar.org" @@ -62,7 +63,7 @@ fee = 100 source_wallet = "alice" [scripts] -build = "cargo build --target wasm32-unknown-unknown --release" +build = "starforge contract build" test = "cargo test" deploy = "starforge deploy" ``` diff --git a/docs/native-deploy.md b/docs/native-deploy.md index 13964785..85c417fd 100644 --- a/docs/native-deploy.md +++ b/docs/native-deploy.md @@ -10,7 +10,7 @@ entirely over Soroban RPC: 5. `CreateContract` / `CreateContractV2` (with `--constructor-arg` when needed) ```bash -starforge deploy run --wasm target/wasm32-unknown-unknown/release/hello.wasm \ +starforge deploy run --wasm target/wasm32v1-none/release/hello.wasm \ --wallet deployer --network testnet --execute # Machine-readable contract id diff --git a/docs/plugins/cookbook.md b/docs/plugins/cookbook.md index 6993812f..32499baf 100644 --- a/docs/plugins/cookbook.md +++ b/docs/plugins/cookbook.md @@ -264,7 +264,7 @@ Security implications for authors: summarises a compiled contract: ```bash norun -starforge wasm-inspect target/wasm32-unknown-unknown/release/my_contract.wasm +starforge wasm-inspect target/wasm32v1-none/release/my_contract.wasm ``` It reports the module size, WebAssembly version, every section, and whether diff --git a/src/commands/ai_deploy_docs.rs b/src/commands/ai_deploy_docs.rs index da679447..d4672907 100644 --- a/src/commands/ai_deploy_docs.rs +++ b/src/commands/ai_deploy_docs.rs @@ -231,6 +231,8 @@ fn generate_deployment_guide( let h1 = if markdown { "#" } else { "" }; let h2 = if markdown { "##" } else { "──" }; let h3 = if markdown { "###" } else { " >" }; + let wasm_target = crate::utils::wasm_target::resolve_target(None) + .unwrap_or_else(|_| "".to_string()); format!( r#"{h1} Deployment Guide — {contract} @@ -239,23 +241,23 @@ Generated by starforge ai-deploy-docs · Network: {network} {h2} Prerequisites -- Rust toolchain with `wasm32-unknown-unknown` target installed +- Rust toolchain with the selected Soroban WASM target installed - Stellar CLI (`stellar`) ≥ 21.0 on your PATH - A funded {network} account (create with `starforge wallet create deployer --fund`) -- The compiled contract WASM: `target/wasm32-unknown-unknown/release/{contract_snake}.wasm` +- The compiled contract WASM: `target/{wasm_target}/release/{contract_snake}.wasm` {h2} Build ```bash stellar contract build -# Output: target/wasm32-unknown-unknown/release/{contract_snake}.wasm +# Output: target/{wasm_target}/release/{contract_snake}.wasm ``` {h2} Pre-deployment Checks 1. Verify the WASM size is within Soroban limits (< 64 KB recommended): ```bash - wc -c target/wasm32-unknown-unknown/release/{contract_snake}.wasm + wc -c target/{wasm_target}/release/{contract_snake}.wasm ``` 2. Check your deployer account balance: ```bash @@ -271,7 +273,7 @@ stellar contract build ```bash starforge deploy \ - --wasm target/wasm32-unknown-unknown/release/{contract_snake}.wasm \ + --wasm target/{wasm_target}/release/{contract_snake}.wasm \ --network {network} \ --wallet deployer ``` @@ -310,6 +312,7 @@ production deployer accounts and store the passphrase in a secrets manager. contract_snake = contract.replace('-', "_"), network = network, fn_list = fn_list, + wasm_target = wasm_target, ) } @@ -327,6 +330,8 @@ fn generate_runbook(contract: &str, network: &str, source: &str, markdown: bool) let h1 = if markdown { "#" } else { "" }; let h2 = if markdown { "##" } else { "──" }; + let wasm_target = crate::utils::wasm_target::resolve_target(None) + .unwrap_or_else(|_| "".to_string()); format!( r#"{h1} Operational Runbook — {contract} @@ -351,7 +356,7 @@ Generated by starforge ai-deploy-docs · Network: {network} 2. **Deploy the contract** ```bash starforge deploy \ - --wasm target/wasm32-unknown-unknown/release/{contract_snake}.wasm \ + --wasm target/{wasm_target}/release/{contract_snake}.wasm \ --network {network} \ --wallet deployer ``` @@ -404,6 +409,7 @@ starforge analytics show --contract contract_snake = contract.replace('-', "_"), network = network, fn_checks = fn_checks, + wasm_target = wasm_target, ) } diff --git a/src/commands/contract.rs b/src/commands/contract.rs index 35879eea..27e9c806 100644 --- a/src/commands/contract.rs +++ b/src/commands/contract.rs @@ -873,15 +873,15 @@ fn handle_build(args: BuildArgs) -> Result<()> { .map(Path::to_path_buf) .unwrap_or_else(|| current_dir.clone()); + let manifest_target = crate::manifest::discover_manifest(&project_dir) + .map(|path| crate::manifest::load_manifest(&path)) + .transpose()? + .and_then(|manifest| manifest.wasm_target); + let wasm_target = crate::utils::wasm_target::resolve_target(manifest_target.as_deref())?; + let mut command = Command::new("stellar"); command.args(["contract", "build"]); - - // For stellar-cli >= 22.0.0, `--workspace` can be used to build the workspace. - // Wait, does stellar contract build support --workspace? Actually, `cargo build --workspace` does. - // Wait, we can just pass `--workspace` or `--all` or maybe just do it. I'll just pass `--workspace` if `--all` is set or just let cargo handle it. Wait, the prompt says "Build and deploy commands understand workspaces". Let's pass `--workspace` or just `cargo build --target wasm32-unknown-unknown --release`... actually I'll pass `--workspace`. - // Wait, `stellar contract build` might not accept `--workspace` directly in older versions? Actually, it accepts `--all` or `--workspace`? Let's assume it accepts `--workspace` if it's delegating to cargo, or maybe we just don't pass anything and cargo detects the workspace? Let's check. - // Wait! StarForge wraps `stellar contract build`. I will pass `--workspace`. - // Wait, passing `--workspace` to stellar contract build might fail if it's not supported. I'll just skip it for a moment, wait, I'll pass `--workspace` if `args.all` is true. Wait, `cargo check` task logs might tell me. Let me just pass `--workspace`. Wait, I will just do it. + command.env("CARGO_BUILD_TARGET", &wasm_target); if args.all { command.arg("--workspace"); @@ -929,6 +929,7 @@ fn handle_build(args: BuildArgs) -> Result<()> { } else { p::info("Embedding StarForge build provenance metadata."); } + p::info(&format!("Using Rust target {wasm_target}.")); p::separator(); diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 6f1f0f3e..f8165307 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -534,23 +534,20 @@ fn resolve_deploy_target(args: &DeployArgs) -> Result<(PathBuf, String, Option." diff --git a/src/commands/doctor.rs b/src/commands/doctor.rs index e6b486f3..0792b1f8 100644 --- a/src/commands/doctor.rs +++ b/src/commands/doctor.rs @@ -27,6 +27,40 @@ pub async fn run() -> Result<()> { let cfg = config::load()?; findings.extend(config::validate_config_integrity(&cfg)); + let target_override = match crate::manifest::find_and_load_manifest( + &std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), + ) { + Ok(Some((_, manifest))) => manifest.wasm_target, + Ok(None) => None, + Err(error) => { + findings.push(config::DoctorFinding::fail( + "wasm_target", + format!("could not read starforge.toml: {error:#}"), + )); + None + } + }; + + match crate::utils::wasm_target::resolve_target(target_override.as_deref()) { + Ok(target) => match crate::utils::wasm_target::installed_target(&target) { + Ok(true) => findings.push(config::DoctorFinding::pass( + "wasm_target", + format!("Rust target '{target}' is installed"), + )), + Ok(false) => findings.push(config::DoctorFinding::fail( + "wasm_target", + format!( + "Rust target '{target}' is not installed; run `rustup target add {target}`" + ), + )), + Err(error) => findings.push(config::DoctorFinding::fail( + "wasm_target", + format!("could not check Rust target '{target}': {error:#}"), + )), + }, + Err(error) => findings.push(config::DoctorFinding::fail("wasm_target", error.to_string())), + } + let network = cfg.network.clone(); if horizon::check_network(&network).await { findings.push(config::DoctorFinding::pass( diff --git a/src/commands/new.rs b/src/commands/new.rs index c6b7fdd0..ea791b9a 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -180,8 +180,9 @@ async fn scaffold_contract_interactive(default_name: String) -> Result<()> { println!( " Tests : {}", if opts.include_tests { + let wasm_target = crate::utils::wasm_target::resolve_target(None)?; "yes".green() - } else { + " starforge deploy --wasm target/{wasm_target}/release/{}.wasm", "no".yellow() } ); @@ -291,6 +292,7 @@ async fn scaffold_contract( // Roll back the partially-created directory if any step below fails. let mut target_guard = PathCleanup::new(dir.to_path_buf()); + let wasm_target = crate::utils::wasm_target::resolve_target(None)?; p::step(1, 4, "Creating directory structure…"); fs::create_dir_all(dir.join("src"))?; @@ -298,7 +300,7 @@ async fn scaffold_contract( p::step(2, 4, "Writing Cargo.toml & starforge.toml…"); fs::write(dir.join("Cargo.toml"), cargo_toml(&name, license, author))?; - fs::write(dir.join(".cargo/config.toml"), cargo_config())?; + fs::write(dir.join(".cargo/config.toml"), cargo_config(&wasm_target))?; fs::write(dir.join(".gitignore"), "target/\n.soroban/\n")?; let starter_manifest = crate::manifest::ProjectManifest::default_starter(&name); let manifest_toml = toml::to_string_pretty(&starter_manifest)?; @@ -326,7 +328,10 @@ async fn scaffold_contract( fs::write(dir.join("src/lib.rs"), src)?; p::step(4, 4, "Writing README.md…"); - fs::write(dir.join("README.md"), readme(&name, &template, source))?; + fs::write( + dir.join("README.md"), + readme(&name, &template, source, &wasm_target), + )?; // Scaffolding completed: keep the directory. target_guard.commit(); @@ -345,9 +350,9 @@ async fn scaffold_contract( println!(); println!(" Next steps:"); p::info(&format!(" cd {}", name)); - p::info(" stellar contract build"); + p::info(" starforge contract build"); p::info(&format!( - " starforge deploy --wasm target/wasm32v1-none/release/{}.wasm", + " starforge deploy --wasm target/{wasm_target}/release/{}.wasm", name.replace('-', "_") )); println!(); @@ -444,10 +449,10 @@ lto = true ) } -fn cargo_config() -> &'static str { - r#"[target.wasm32-unknown-unknown] -rustflags = ["-C", "target-feature=+multivalue,+sign-ext"] -"# +fn cargo_config(target: &str) -> String { + format!( + "[target.{target}]\nrustflags = [\"-C\", \"target-feature=+multivalue,+sign-ext\"]\n" + ) } // ── Contract templates ──────────────────────────────────────────────────────── @@ -990,7 +995,7 @@ npm run dev ) } -fn readme(name: &str, template: &str, source: &str) -> String { +fn readme(name: &str, template: &str, source: &str, wasm_target: &str) -> String { format!( r#"# {name} @@ -999,7 +1004,7 @@ A Soroban smart contract scaffolded with [starforge](https://github.com/Nanle-co ## Build ```bash -stellar contract build +starforge contract build ``` ## Test @@ -1012,7 +1017,7 @@ cargo test ```bash starforge deploy \ - --wasm target/wasm32v1-none/release/{snake}.wasm \ + --wasm target/{wasm_target}/release/{snake}.wasm \ --network testnet ``` @@ -1021,6 +1026,7 @@ Source: `{source}` "#, name = name, snake = name.replace('-', "_"), + wasm_target = wasm_target, template = template, source = source ) @@ -1272,9 +1278,9 @@ async fn scaffold_from_marketplace(name: String, template_name: String) -> Resul println!(); println!(" Next steps:"); p::info(&format!(" cd {}", name)); - p::info(" stellar contract build"); + p::info(" starforge contract build"); p::info(&format!( - " starforge deploy --wasm target/wasm32v1-none/release/{}.wasm", + " starforge deploy --wasm target/{wasm_target}/release/{}.wasm", name.replace('-', "_") )); println!(); diff --git a/src/commands/test.rs b/src/commands/test.rs index c2b42961..d78dedbf 100644 --- a/src/commands/test.rs +++ b/src/commands/test.rs @@ -160,16 +160,13 @@ fn resolve_test_wasm(args: &TestArgs) -> Result { } else { let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); if let Some((manifest_path, manifest)) = crate::manifest::find_and_load_manifest(&cwd)? { - let contract_wasm = manifest - .contracts - .values() - .find_map(|c| c.wasm.clone()); - if let Some(wasm_rel) = contract_wasm { - let base_dir = manifest_path.parent().unwrap_or_else(|| std::path::Path::new(".")); - Ok(base_dir.join(wasm_rel)) - } else { - anyhow::bail!("No contract WASM specified in starforge.toml. Please pass --wasm ."); - } + let contract_name = manifest.contracts.keys().next().ok_or_else(|| { + anyhow::anyhow!("No contract WASM specified in starforge.toml. Please pass --wasm .") + })?; + let base_dir = manifest_path.parent().unwrap_or_else(|| std::path::Path::new(".")); + manifest + .wasm_path_for_contract(contract_name, base_dir)? + .ok_or_else(|| anyhow::anyhow!("No WASM artifact configured for '{contract_name}'")) } else { anyhow::bail!("Missing --wasm argument and no starforge.toml project manifest found."); } diff --git a/src/commands/verify.rs b/src/commands/verify.rs index 8c9cd894..d772c60d 100644 --- a/src/commands/verify.rs +++ b/src/commands/verify.rs @@ -129,11 +129,8 @@ pub struct CiArgs { #[arg(long, default_value = "github", value_parser = ["github", "gitlab", "circleci"])] pub platform: String, /// WASM path to embed in the snippet - #[arg( - long, - default_value = "target/wasm32-unknown-unknown/release/contract.wasm" - )] - pub wasm: String, + #[arg(long)] + pub wasm: Option, /// Contract label to embed in the snippet #[arg(long, default_value = "my-contract")] pub contract: String, @@ -348,6 +345,8 @@ fn generate_harness_content(wasm_path: &Path, properties: &[PropertySpec]) -> St }) .collect::>() .join("\n"); + let wasm_target = crate::utils::wasm_target::resolve_target(None) + .unwrap_or_else(|_| "".to_string()); format!( r#"//! Formal verification harness for: {wasm_name} @@ -370,7 +369,7 @@ mod property_tests {{ fn wasm_exists() {{ assert!( std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("target/wasm32-unknown-unknown/release/{wasm_name}.wasm") + .join("target/{wasm_target}/release/{wasm_name}.wasm") .exists() || true, // path may differ; update accordingly "WASM artifact not found" @@ -380,6 +379,7 @@ mod property_tests {{ "#, wasm_name = wasm_name, prop_stubs = prop_stubs, + wasm_target = wasm_target, ) } @@ -806,6 +806,11 @@ fn handle_reports(args: ReportsArgs) -> Result<()> { fn handle_ci(args: CiArgs) -> Result<()> { p::header("CI Configuration for Continuous Verification"); + let target = crate::utils::wasm_target::resolve_target(None)?; + let wasm = match args.wasm { + Some(wasm) => wasm, + None => format!("target/{target}/release/contract.wasm"), + }; let snippet = match args.platform.as_str() { "github" => format!( @@ -820,12 +825,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - - name: Build contract WASM - run: cargo build --target wasm32-unknown-unknown --release + - uses: dtolnay/rust-toolchain@stable + with: + targets: {target} + - name: Build contract WASM + run: cargo build --target {target} --release - name: Install starforge run: cargo install --path . - name: Add verification properties @@ -845,8 +849,9 @@ jobs: if: always() run: starforge verify report --contract {contract} "#, - wasm = args.wasm, - contract = args.contract + wasm = wasm, + contract = args.contract, + target = target ), "gitlab" => format!( r#"# .gitlab-ci.yml (verification job) @@ -854,16 +859,17 @@ verify-contract: image: rust:latest stage: test before_script: - - rustup target add wasm32-unknown-unknown + - rustup target add {target} - cargo install --path . script: - - cargo build --target wasm32-unknown-unknown --release + - cargo build --target {target} --release - starforge verify property add --contract {contract} --name no-overflow --spec no_overflow --severity critical - starforge verify run --wasm {wasm} --contract {contract} --fail-on-critical true - starforge verify report --contract {contract} "#, - wasm = args.wasm, - contract = args.contract + wasm = wasm, + contract = args.contract, + target = target ), "circleci" => format!( r#"# .circleci/config.yml (verification job) @@ -877,8 +883,8 @@ jobs: - run: name: Build WASM command: | - rustup target add wasm32-unknown-unknown - cargo build --target wasm32-unknown-unknown --release + rustup target add {target} + cargo build --target {target} --release - run: name: Run formal verification command: | @@ -886,11 +892,18 @@ jobs: starforge verify property add --contract {contract} --name no-overflow --spec no_overflow --severity critical starforge verify run --wasm {wasm} --contract {contract} --fail-on-critical true "#, - wasm = args.wasm, - contract = args.contract + wasm = wasm, + contract = args.contract, + target = target ), _ => unreachable!(), }; + let snippet = snippet + .replace(" - uses: actions/checkout", " - uses: actions/checkout") + .replace(" with:", " with:") + .replace(" targets:", " targets:") + .replace(" - name: Build contract WASM", " - name: Build contract WASM") + .replace(" run: cargo build", " run: cargo build"); p::separator(); println!("{}", snippet.bright_white()); diff --git a/src/manifest.rs b/src/manifest.rs index 57501006..7137aaf2 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -26,6 +26,10 @@ pub struct ProjectManifest { #[serde(default, skip_serializing_if = "Option::is_none")] pub package: Option, + /// Soroban WASM target override; defaults according to the active Rust toolchain. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub wasm_target: Option, + /// Contract definitions mapped by contract name. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub contracts: BTreeMap, @@ -113,12 +117,9 @@ impl ProjectManifest { project_name.to_string(), ContractConfig { path: Some(".".to_string()), - wasm: Some(format!( - "target/wasm32-unknown-unknown/release/{}.wasm", - project_name - )), + wasm: None, init_args: None, - build: Some("cargo build --target wasm32-unknown-unknown --release".to_string()), + build: Some("starforge contract build".to_string()), }, ); @@ -148,7 +149,7 @@ impl ProjectManifest { let mut scripts = BTreeMap::new(); scripts.insert( "build".to_string(), - "cargo build --target wasm32-unknown-unknown --release".to_string(), + "starforge contract build".to_string(), ); scripts.insert("test".to_string(), "cargo test".to_string()); @@ -161,6 +162,7 @@ impl ProjectManifest { authors: Some(vec![]), license: Some("MIT".to_string()), }), + wasm_target: None, contracts, networks, deploy, @@ -178,6 +180,10 @@ impl ProjectManifest { ); } + if let Some(target) = &self.wasm_target { + crate::utils::wasm_target::resolve_target(Some(target))?; + } + if let Some(ref pkg) = self.package { if pkg.name.trim().is_empty() { bail!("Package name in starforge.toml cannot be empty."); @@ -228,6 +234,22 @@ impl ProjectManifest { Ok(()) } + /// Resolve a contract's configured or toolchain-default WASM output path. + pub fn wasm_path_for_contract(&self, name: &str, project_dir: &Path) -> Result> { + let Some(contract) = self.contracts.get(name) else { + return Ok(None); + }; + if let Some(wasm) = &contract.wasm { + return Ok(Some(project_dir.join(wasm))); + } + let target = crate::utils::wasm_target::resolve_target(self.wasm_target.as_deref())?; + Ok(Some(crate::utils::wasm_target::artifact_path( + project_dir, + name, + &target, + ))) + } + /// Generates the JSON Schema for `starforge.toml`. pub fn json_schema() -> serde_json::Value { serde_json::json!({ @@ -258,6 +280,14 @@ impl ProjectManifest { }, "additionalProperties": false }, + "wasm_target": { + "type": "string", + "enum": [ + crate::utils::wasm_target::LEGACY_WASM_TARGET, + crate::utils::wasm_target::V1_WASM_TARGET + ], + "description": "Optional Soroban WASM target override" + }, "contracts": { "type": "object", "description": "Smart contract target specifications", @@ -366,6 +396,30 @@ mod tests { fn starter_manifest_validates_cleanly() { let manifest = ProjectManifest::default_starter("my_contract"); assert!(manifest.validate().is_ok()); + assert!(manifest + .wasm_path_for_contract("my_contract", Path::new("project")) + .unwrap() + .unwrap() + .to_string_lossy() + .contains("/release/my_contract.wasm")); + } + + #[test] + fn manifest_target_override_controls_wasm_path() { + let mut manifest = ProjectManifest::default_starter("my_contract"); + manifest.wasm_target = Some(crate::utils::wasm_target::LEGACY_WASM_TARGET.to_string()); + assert_eq!( + manifest + .wasm_path_for_contract("my_contract", Path::new("project")) + .unwrap(), + Some( + PathBuf::from("project") + .join("target") + .join(crate::utils::wasm_target::LEGACY_WASM_TARGET) + .join("release") + .join("my_contract.wasm") + ) + ); } #[test] @@ -379,7 +433,6 @@ version = "0.1.0" [contracts.token_contract] path = "contracts/token" -wasm = "target/wasm32-unknown-unknown/release/token.wasm" [networks.testnet] horizon_url = "https://horizon-testnet.stellar.org" diff --git a/src/utils/ai_conversation.rs b/src/utils/ai_conversation.rs index 257c1d93..3f0e33fc 100644 --- a/src/utils/ai_conversation.rs +++ b/src/utils/ai_conversation.rs @@ -372,7 +372,7 @@ impl ConversationManager { title: "Compile contract".to_string(), description: "Build the WASM file".to_string(), action_type: SuggestionAction::Command( - "cargo build --target wasm32-unknown-unknown --release".to_string(), + "starforge contract build".to_string(), ), confidence: 0.95, }); diff --git a/src/utils/ai_debugger.rs b/src/utils/ai_debugger.rs index d1ce9fb8..0e914069 100644 --- a/src/utils/ai_debugger.rs +++ b/src/utils/ai_debugger.rs @@ -242,7 +242,7 @@ fn pattern_wasm_invalid() -> DebugFinding { This can happen if the file is truncated, built with incompatible settings, \ or is not a Soroban contract at all." .into(), - root_cause: "The binary was not compiled with `--target wasm32-unknown-unknown`, \ + root_cause: "The binary was not compiled for a Soroban-compatible WASM target, \ or build artifacts are stale after a `cargo clean`." .into(), fix_suggestion: "Rebuild from source: `stellar contract build`. Verify the output \ @@ -448,7 +448,7 @@ fn pattern_deployment_wasm_size_exceeded() -> DebugFinding { was not optimized before deployment. Debug builds are significantly larger \ than release builds." .into(), - fix_suggestion: "Build with `--release` flag: `cargo build --release --target wasm32-unknown-unknown`. \ + fix_suggestion: "Build with `starforge contract build`. \ Use starforge's built-in optimizer: `starforge deploy --optimize`. \ Remove unused dependencies from Cargo.toml. Consider splitting large contracts \ into multiple smaller contracts." @@ -460,7 +460,7 @@ fn pattern_deployment_wasm_size_exceeded() -> DebugFinding { "Apply optimization and retry.".into(), ], breakpoint_hints: vec![ - "Check WASM file size: ls -lh target/wasm32-unknown-unknown/release/*.wasm".into(), + "Check WASM file size under target//release/*.wasm".into(), "Run wasm-opt with --optimize flag for maximum size reduction.".into(), ], references: vec![ @@ -562,7 +562,7 @@ fn pattern_deployment_wasm_hash_mismatch() -> DebugFinding { "Compare expected vs actual WASM hash.".into(), ], breakpoint_hints: vec![ - "Compute local WASM hash: sha256sum target/wasm32-unknown-unknown/release/*.wasm".into(), + "Compute the local WASM hash from target//release/*.wasm".into(), "Fetch on-chain WASM hash from contract metadata.".into(), ], references: vec![ @@ -588,7 +588,7 @@ fn pattern_compilation_error() -> DebugFinding { to an existing module/item (E0433), a missing `derive` or trait bound required by \ the soroban_sdk macros, or moving a value that is used again afterwards." .into(), - fix_suggestion: "Run `cargo build --target wasm32-unknown-unknown 2>&1 | head -50` \ + fix_suggestion: "Run `starforge contract build` \ and read the first reported error — later errors are often just consequences of \ the first one. For E0308, check the exact types on both sides of the mismatch. \ For E0433/E0432, verify the `use` path and that the target module is declared \ @@ -596,7 +596,7 @@ fn pattern_compilation_error() -> DebugFinding { value or restructuring so it is moved only once." .into(), reproduction_steps: vec![ - "Run `cargo build --target wasm32-unknown-unknown`.".into(), + "Run `starforge contract build`.".into(), "Copy the first `error[EXXXX]` block reported.".into(), "Apply the compiler's suggested fix, then rebuild.".into(), ], diff --git a/src/utils/ai_doc_qa.rs b/src/utils/ai_doc_qa.rs index aee3b382..11314903 100644 --- a/src/utils/ai_doc_qa.rs +++ b/src/utils/ai_doc_qa.rs @@ -744,7 +744,7 @@ Projects are generated as Rust crates using the Soroban SDK.", title: "StarForge Deployment", url: "https://github.com/Nanle-code/StarForge", content: "To deploy a Soroban contract with StarForge, first compile the contract to WASM \ -(cargo build --target wasm32-unknown-unknown --release), then run starforge deploy with the \ +(starforge contract build), then run starforge deploy with the \ compiled .wasm file. You must have a funded wallet configured (starforge wallet create) and select \ a network (starforge network or --network testnet). Deployment uploads the WASM and creates a \ contract instance. Deployment history, verification, and rollback are available through \ diff --git a/src/utils/ai_docs.rs b/src/utils/ai_docs.rs index f1c7a450..ba25093f 100644 --- a/src/utils/ai_docs.rs +++ b/src/utils/ai_docs.rs @@ -344,7 +344,7 @@ fn build_sections( sections.push(DocSection { title: "Getting Started".to_string(), content: format!( - "1. Build the contract WASM with `cargo build --target wasm32v1-none --release`.\n\ + "1. Build the contract WASM with `starforge contract build`.\n\ 2. Deploy with `starforge deploy --wasm --network {network}`.\n\ 3. Call public entrypoints via `starforge contract invoke` or generated bindings.\n\ 4. Keep rustdoc comments (`///`, `//!`) in sync — re-run `starforge docs generate --source` after API changes." @@ -449,8 +449,8 @@ fn build_troubleshooting( .to_string(), ); tips.push( - "**Build fails on `wasm32v1-none`** — ensure the target is installed with \ - `rustup target add wasm32v1-none` and that `#![no_std]` is present for on-chain builds." + "**Build fails for the Soroban WASM target** — run `starforge config doctor` to check \ + the selected target and install it, and ensure `#![no_std]` is present for on-chain builds." .to_string(), ); diff --git a/src/utils/ai_ide_integration.rs b/src/utils/ai_ide_integration.rs index 03f8a2d2..35c3202b 100644 --- a/src/utils/ai_ide_integration.rs +++ b/src/utils/ai_ide_integration.rs @@ -442,11 +442,13 @@ pub fn handle_request(request: &IdeRequest) -> IdeResponse { /// Builds the integration files for `ide`. pub fn build_integration(ide: Ide) -> IdeIntegration { + let wasm_target = crate::utils::wasm_target::resolve_target(None) + .unwrap_or_else(|_| crate::utils::wasm_target::V1_WASM_TARGET.to_string()); let files = match ide { Ide::VsCode => vec![ GeneratedFile { relative_path: ".vscode/tasks.json".to_string(), - contents: VSCODE_TASKS.to_string(), + contents: VSCODE_TASKS.replace("__WASM_TARGET__", &wasm_target), purpose: "Build, test, audit, and profile tasks".to_string(), }, GeneratedFile { @@ -468,7 +470,7 @@ pub fn build_integration(ide: Ide) -> IdeIntegration { }, GeneratedFile { relative_path: ".idea/runConfigurations/Starforge_Profile.xml".to_string(), - contents: INTELLIJ_PROFILE_RUN_CONFIG.to_string(), + contents: INTELLIJ_PROFILE_RUN_CONFIG.replace("__WASM_TARGET__", &wasm_target), purpose: "Run configuration for `starforge ai profiling run`".to_string(), }, ], @@ -479,7 +481,7 @@ pub fn build_integration(ide: Ide) -> IdeIntegration { }], Ide::Zed => vec![GeneratedFile { relative_path: ".zed/tasks.json".to_string(), - contents: ZED_TASKS.to_string(), + contents: ZED_TASKS.replace("__WASM_TARGET__", &wasm_target), purpose: "Zed task definitions for audit and profiling".to_string(), }], }; @@ -567,7 +569,7 @@ const VSCODE_TASKS: &str = r#"{ { "label": "starforge: build contract", "type": "shell", - "command": "stellar contract build", + "command": "starforge contract build", "group": "build", "problemMatcher": ["$rustc"] }, @@ -580,7 +582,7 @@ const VSCODE_TASKS: &str = r#"{ { "label": "starforge: AI profile", "type": "shell", - "command": "starforge ai profiling run --wasm ${workspaceFolder}/target/wasm32-unknown-unknown/release/contract.wasm", + "command": "starforge ai profiling run --wasm ${workspaceFolder}/target/__WASM_TARGET__/release/contract.wasm", "problemMatcher": [] }, { @@ -594,7 +596,6 @@ const VSCODE_TASKS: &str = r#"{ "#; const VSCODE_SETTINGS: &str = r#"{ - "rust-analyzer.cargo.target": "wasm32-unknown-unknown", "rust-analyzer.check.command": "clippy", "starforge.ai.enabled": true, "starforge.ai.diagnosticsOnSave": true, @@ -614,7 +615,7 @@ const INTELLIJ_AUDIT_RUN_CONFIG: &str = r#" -