Skip to content
Draft
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add the `dusk-forge` CLI with `new`, `build`, `test`, and `check` commands for contract project scaffolding and workflows.
- Add `expand`, `clean`, and `completions` commands to the `dusk-forge` CLI.
- Add `schema`, `call`, and `verify` commands to the `dusk-forge` CLI.

### Changed

- Make local forge path overrides opt-in for release builds and harden CLI template/path handling across platforms.
- Move the forge workspace and template project to Rust edition 2024 and bump published Dusk dependencies to their latest released versions.

## [0.2.2] - 2026-02-02

Expand Down
19 changes: 8 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "dusk-forge"
version = "0.2.2"
edition = "2021"
edition = "2024"
description = "A smart contract development framework for Dusk"
license = "MPL-2.0"
repository = "https://github.com/dusk-network/forge"
Expand All @@ -24,13 +24,13 @@ members = ["contract-macro", "tests/types", "tests/test-bridge", "cli"]
exclude = ["contract-template"]

[workspace.package]
edition = "2021"
edition = "2024"

[workspace.dependencies]
# Workspace internal dependencies
dusk-forge-contract = { version = "0.1.1", path = "./contract-macro/" }

dusk-core = "1.4"
dusk-core = "1.4.1"
bytecheck = { version = "0.6.12", default-features = false }
dusk-bytes = "0.1.7"
rkyv = { version = "0.7", default-features = false, features = [
Expand All @@ -48,20 +48,17 @@ syn = { version = "2", features = ["full", "visit"] }
assert_cmd = "2"
cargo_metadata = "0.19"
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
colored = "3"
blake3 = "=1.5.4"
predicates = "3"
tempfile = "=3.10.1"
thiserror = "2"
toml = "0.8"
wasmtime = "25"

# Pin to match L1Contracts versions
dusk-vm = { version = "=1.4.0", default-features = false }
# Pin to avoid edition2024 issues
blake2b_simd = "=1.0.2"
blake3 = "=1.5.4"
constant_time_eq = "=0.3.1"
time-core = "=0.1.6"
uuid = "=1.20.0"
# Pin to match the latest published Dusk runtime crates used by generated contracts.
dusk-vm = { version = "=1.4.3", default-features = false }

# Enable overflow checks in release builds for contract safety
[profile.release]
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,13 @@ All runtime dependencies go in the WASM-only section because contracts are gated

```toml
[target.'cfg(target_family = "wasm")'.dependencies]
dusk-core = "1.4"
dusk-data-driver = { version = "0.3", optional = true } # Only for data-driver
dusk-forge = "0.1"
dusk-core = "1.4.1"
dusk-data-driver = { version = "0.3.1", optional = true } # Only for data-driver
dusk-forge = "0.2.2"

[dev-dependencies]
dusk-core = "1.4" # Same types, but for host-side tests
dusk-vm = "0.1" # To run contract in tests
dusk-core = "1.4.1" # Same types, but for host-side tests
dusk-vm = "1.4.3" # To run contract in tests
```

### Features
Expand Down
10 changes: 9 additions & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "dusk-forge-cli"
version = "0.1.0"
edition = "2021"
edition = "2024"
description = "CLI for scaffolding and building Dusk Forge contracts"
license = "MPL-2.0"
repository = "https://github.com/dusk-network/forge"
Expand All @@ -13,10 +13,18 @@ path = "src/main.rs"
[dependencies]
cargo_metadata = { workspace = true }
clap = { workspace = true, features = ["derive"] }
clap_complete = { workspace = true }
colored = { workspace = true }
blake3 = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
wasmtime = { workspace = true, optional = true }

[features]
default = ["schema"]
schema = ["dep:wasmtime"]

[dev-dependencies]
assert_cmd = { workspace = true }
Expand Down
27 changes: 26 additions & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ cargo run -p dusk-forge-cli -- --help
- `dusk-forge build [target]`: build WASM artifacts. Targets: `all` (default), `contract`, `data-driver`.
- `dusk-forge test [-- <cargo-test-args>]`: build contract WASM and run `cargo test --release`.
- `dusk-forge check`: validate project structure and toolchain.
- `dusk-forge expand [--data-driver]`: show macro expansion with `cargo-expand`.
- `dusk-forge clean`: remove `target/contract` and `target/data-driver`.
- `dusk-forge schema [--pretty]`: build data-driver WASM and print `CONTRACT_SCHEMA` JSON.
- `dusk-forge call <function> [--input <json>]`: encode call bytes using the data-driver export `encode_input_fn`.
- `dusk-forge verify [--expected-blake3 <hash>] [--skip-build]`: validate artifacts, schema loading, and optional contract hash match.
- `dusk-forge completions <shell>`: generate shell completions.

## Common Options

Expand All @@ -64,6 +70,24 @@ dusk-forge build contract
dusk-forge test
```

Print schema JSON:

```bash
dusk-forge schema --pretty
```

Encode input bytes for a function call:

```bash
dusk-forge call set_count --input '42'
```

Verify artifacts and hash:

```bash
dusk-forge verify --expected-blake3 <hash>
```

## Toolchain Requirements

Contract builds require:
Expand All @@ -81,6 +105,7 @@ Data-driver builds require:
Optional tools:

- `wasm-opt` for smaller WASM artifacts
- `cargo-expand` for the `expand` command

## Template Notes

Expand All @@ -96,4 +121,4 @@ Scaffolded projects include:
- `rust-toolchain.toml` for deterministic toolchain selection
- `Cargo.lock` generated at scaffold time

Build/test commands run Cargo with `--locked`.
Build/test/schema commands run Cargo with `--locked`.
148 changes: 148 additions & 0 deletions cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::path::PathBuf;

use clap::{Args, Parser, Subcommand, ValueEnum};
use clap_complete::Shell;

use crate::build_runner::BuildTarget;

Expand Down Expand Up @@ -49,6 +50,18 @@ pub enum Commands {
Test(TestArgs),
/// Validate project structure and toolchain.
Check(ProjectOptions),
/// Show macro-expanded code using cargo-expand.
Expand(ExpandArgs),
/// Remove contract-specific build artifact directories.
Clean(ProjectOptions),
/// Build data-driver WASM and print CONTRACT_SCHEMA as JSON.
Schema(SchemaArgs),
/// Encode call input bytes through the data-driver.
Call(CallArgs),
/// Verify contract and data-driver artifacts.
Verify(VerifyArgs),
/// Generate shell completion scripts.
Completions(CompletionsArgs),
}

#[derive(Debug, Clone, Copy, ValueEnum)]
Expand Down Expand Up @@ -109,3 +122,138 @@ pub struct TestArgs {
/// Extra args passed through to `cargo test --release`.
pub cargo_test_args: Vec<String>,
}

#[derive(Debug, Args)]
pub struct ExpandArgs {
#[command(flatten)]
pub project: ProjectOptions,

/// Expand with the data-driver feature.
#[arg(long)]
pub data_driver: bool,
}

#[derive(Debug, Args)]
pub struct SchemaArgs {
#[command(flatten)]
pub project: ProjectOptions,

/// Pretty-print JSON output.
#[arg(long)]
pub pretty: bool,
}

#[derive(Debug, Args)]
pub struct CallArgs {
#[command(flatten)]
pub project: ProjectOptions,

/// Contract function name to encode.
pub function: String,

/// JSON input payload for the function (use `null` for no input).
#[arg(long, default_value = "null")]
pub input: String,
}

#[derive(Debug, Args)]
pub struct VerifyArgs {
#[command(flatten)]
pub project: ProjectOptions,

/// Optional expected BLAKE3 hash of the contract WASM.
#[arg(long)]
pub expected_blake3: Option<String>,

/// Skip rebuilding artifacts and verify existing files only.
#[arg(long)]
pub skip_build: bool,
}

#[derive(Debug, Args)]
pub struct CompletionsArgs {
/// Shell to generate completions for.
#[arg(value_enum)]
pub shell: Shell,
}

#[cfg(test)]
mod tests {
use std::path::PathBuf;

use clap::Parser;

use super::{Cli, Commands};

#[test]
fn parses_expand_command() {
let cli = Cli::parse_from(["dusk-forge", "expand", "--data-driver"]);

match cli.command {
Commands::Expand(args) => assert!(args.data_driver),
other => panic!("expected expand command, got {other:?}"),
}
}

#[test]
fn parses_clean_command() {
let cli = Cli::parse_from(["dusk-forge", "clean", "--path", "demo"]);

match cli.command {
Commands::Clean(args) => assert_eq!(args.path, PathBuf::from("demo")),
other => panic!("expected clean command, got {other:?}"),
}
}

#[test]
fn parses_completions_command() {
let cli = Cli::parse_from(["dusk-forge", "completions", "bash"]);

match cli.command {
Commands::Completions(_) => {}
other => panic!("expected completions command, got {other:?}"),
}
}

#[test]
fn parses_schema_command() {
let cli = Cli::parse_from(["dusk-forge", "schema", "--pretty"]);

match cli.command {
Commands::Schema(args) => assert!(args.pretty),
other => panic!("expected schema command, got {other:?}"),
}
}

#[test]
fn parses_call_command() {
let cli = Cli::parse_from(["dusk-forge", "call", "transfer", "--input", "{\"foo\":1}"]);

match cli.command {
Commands::Call(args) => {
assert_eq!(args.function, "transfer");
assert_eq!(args.input, "{\"foo\":1}");
}
other => panic!("expected call command, got {other:?}"),
}
}

#[test]
fn parses_verify_command() {
let cli = Cli::parse_from([
"dusk-forge",
"verify",
"--expected-blake3",
"deadbeef",
"--skip-build",
]);

match cli.command {
Commands::Verify(args) => {
assert_eq!(args.expected_blake3.as_deref(), Some("deadbeef"));
assert!(args.skip_build);
}
other => panic!("expected verify command, got {other:?}"),
}
}
}
64 changes: 64 additions & 0 deletions cli/src/commands/call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use crate::{cli::CallArgs, error::Result};

#[cfg(feature = "schema")]
use crate::{
build_runner::{self, BuildTarget},
data_driver_wasm::DataDriverWasm,
project::{detect, metadata},
toolchain, ui,
};

#[cfg(feature = "schema")]
pub fn run(args: CallArgs) -> Result<()> {
let project = metadata::load(&args.project.path)?;
detect::ensure_forge_project(&project.project_dir)?;

toolchain::ensure_build(&project.project_dir, false)?;

ui::status(format!(
"Building data-driver WASM for function '{}'",
args.function
));

let wasm_path = build_runner::build(&project, BuildTarget::DataDriver, args.project.verbose)?;
let optimized =
build_runner::wasm_opt::optimize_if_available(&wasm_path, args.project.verbose)?;
if !optimized {
ui::warn("wasm-opt not found, skipping optimization");
}

let mut driver = DataDriverWasm::load(&wasm_path)?;
let encoded = driver.encode_input(&args.function, &args.input)?;

if args.project.verbose {
ui::status(format!(
"Encoded {} bytes for '{}'",
encoded.len(),
args.function
));
}

println!("{}", to_hex_prefixed(&encoded));
ui::success("Call payload encoded");
Ok(())
}

#[cfg(not(feature = "schema"))]
pub fn run(_args: CallArgs) -> Result<()> {
Err(crate::error::CliError::Message(
"call command is disabled (build with --features schema)".to_string(),
))
}

#[cfg(feature = "schema")]
fn to_hex_prefixed(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2 + 2);
out.push_str("0x");

for byte in bytes {
use std::fmt::Write;
let _ = write!(&mut out, "{byte:02x}");
}

out
}
Loading
Loading