From 039b2d66ffe8cc6db315c1b393366d6e1f16a035 Mon Sep 17 00:00:00 2001 From: stringhandler Date: Fri, 27 Mar 2026 12:54:47 +0200 Subject: [PATCH] Add `simplex new` and `simplex example` commands, improve generated test template, and expand README docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New commands ------------ simplex new Creates a complete Simplex project scaffold in a new / directory. Equivalent to running `simplex init --lib` in a freshly created directory, but with a better UX — errors immediately if the target directory already exists rather than clobbering existing files. simplex example Scaffolds a full example project into a new directory. Currently supports `simplex example basic`, which writes all files from the examples/basic/ template (including the options, option_offer, array_tr_storage, bytes32_tr_storage, and dual_currency_deposit contracts) using include_str! so the content is embedded in the binary at compile time. The Cargo.toml is generated dynamically to pin to the latest published smplx-std version from crates.io, consistent with how `simplex new` works. Improved generated test template --------------------------------- The default test file produced by `simplex new` and `simplex init --lib` was previously a non-compiling todo!() stub. It is now a working end-to-end p2pk integration test that demonstrates: - Instantiating a program with typed arguments (P2pkArguments) - Deriving a script pubkey from the program - Funding the script via FinalTransaction + PartialOutput - Spending the script by supplying a P2pkWitness with DUMMY_SIGNATURE and RequiredSignature::Witness so the signer fills in the real signature The crate name (simplex_) is substituted at generation time so the artifact imports resolve correctly for any project name. anyhow is now also added to the generated Cargo.toml to support the -> anyhow::Result<()> return type. README ------ - Replaced the `cargo add smplx-std` getting-started flow with `simplex new` as the primary entry point - Added `simplex new` and `simplex example` to the CLI command reference - Added a "Typical workflow" section showing the new -> build -> test flow - Added a "Using smplx-std as a library" section covering: dependency setup, how generated artifacts are structured, instantiating a program with arguments, funding and spending scripts with FinalTransaction, a key types reference table, and the #[simplex::test] macro - Updated the future work checklist to mark `simplex new` and `simplex example` as complete --- README.md | 132 +++++++++++++++++++++++++++-- crates/cli/src/cli.rs | 4 + crates/cli/src/commands/core.rs | 18 +++- crates/cli/src/commands/example.rs | 100 ++++++++++++++++++++++ crates/cli/src/commands/init.rs | 64 +++++++++++--- crates/cli/src/commands/mod.rs | 2 + crates/cli/src/commands/new.rs | 28 ++++++ 7 files changed, 329 insertions(+), 19 deletions(-) create mode 100644 crates/cli/src/commands/example.rs create mode 100644 crates/cli/src/commands/new.rs diff --git a/README.md b/README.md index 5ef0d6d..2d4adae 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,23 @@ See the [simplexup manual](simplexup/README.md) for more details. ## Getting started -Add `smplx-std` dependency to cargo: +Create a new Simplex project in a new directory: ```bash -cargo add --dev smplx-std +simplex new ``` -Optionally, initialize a new project: +This scaffolds a complete project with a `Simplex.toml`, `Cargo.toml`, a p2pk contract in `simf/p2pk.simf`, and a working integration test in `tests/p2pk_test.rs`. + +To scaffold a full working example instead: + +```bash +simplex example basic +``` + +This creates a `basic/` directory containing the complete basic example project, including several contract examples and an integration test you can run immediately after building. + +Alternatively, initialize a Simplex project in the current directory: ```bash simplex init @@ -97,7 +107,9 @@ Where: Simplex CLI provides the following commands: -- `simplex init` - Initializes a new Simplex project. +- `simplex new ` - Creates a new Simplex project in a new `` directory. +- `simplex example ` - Scaffolds a complete example project into a new directory (e.g. `simplex example basic`). +- `simplex init` - Initializes a Simplex project in the current directory. - `simplex config` - Prints the current config. - `simplex build` - Generates simplicity artifacts. - `simplex regtest` - Spins up local Electrs + Elements nodes. @@ -110,9 +122,115 @@ To view the available options, run the help command: simplex -h ``` -### Example +### Typical workflow + +```bash +# Create a new project +simplex new mycontract +cd mycontract + +# Build artifacts from .simf contracts +simplex build + +# Start a local regtest node and run integration tests +simplex test integration +``` + +### Using `smplx-std` as a library + +`smplx-std` is the Rust library that backs your Simplex project. Add it to `Cargo.toml`: + +```toml +[dependencies] +smplx-std = "x.y.z" +``` + +Everything is re-exported from the `simplex` crate name: + +```rust +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature}; +use simplex::utils::tr_unspendable_key; +use simplex::constants::DUMMY_SIGNATURE; +``` + +#### Building and spending a program + +The generated artifacts for each `.simf` contract live in `src/artifacts/` after running `simplex build`. Each contract exposes a typed program struct, an arguments struct, and a witness struct: + +```rust +// Generated from simf/p2pk.simf +use my_project::artifacts::p2pk::P2pkProgram; +use my_project::artifacts::p2pk::derived_p2pk::{P2pkArguments, P2pkWitness}; +``` + +Instantiate the program by passing a Taproot internal key and the typed arguments: + +```rust +let arguments = P2pkArguments { + public_key: signer.get_schnorr_public_key().unwrap().serialize(), +}; +let program = P2pkProgram::new(tr_unspendable_key(), arguments); +let script = program.get_program().get_script_pubkey(context.get_network()).unwrap(); +``` + +Fund the script by adding it as an output to a `FinalTransaction`: + +```rust +let mut ft = FinalTransaction::new(*context.get_network()); +ft.add_output(PartialOutput::new(script.clone(), 50, context.get_network().policy_asset())); +let (tx, _) = signer.finalize(&ft).unwrap(); +provider.broadcast_transaction(&tx).unwrap(); +``` + +Spend from the script by constructing the witness and calling `add_program_input`. Use `DUMMY_SIGNATURE` as a placeholder — the signer replaces it with a real signature identified by the `RequiredSignature::Witness` name: + +```rust +let witness = P2pkWitness { signature: DUMMY_SIGNATURE }; +let mut ft = FinalTransaction::new(*context.get_network()); +ft.add_program_input( + PartialInput::new(utxo_outpoint, utxo_txout), + ProgramInput::new(Box::new(program.get_program().clone()), Box::new(witness)), + RequiredSignature::Witness("SIGNATURE".to_string()), +).unwrap(); +let (tx, _) = signer.finalize(&ft).unwrap(); +provider.broadcast_transaction(&tx).unwrap(); +``` + +#### Key types + +| Type | Description | +|---|---| +| `FinalTransaction` | Transaction builder — holds inputs and outputs | +| `PartialInput` | A UTXO to spend, identified by outpoint and `TxOut` | +| `PartialOutput` | An output with script, amount, and asset | +| `ProgramInput` | Pairs a compiled Simplicity program with its witness | +| `RequiredSignature` | Tells the signer which witness field to fill (`Witness("NAME")`) | +| `tr_unspendable_key()` | Returns the standard unspendable Taproot internal key used for Simplicity outputs | +| `DUMMY_SIGNATURE` | 64-byte placeholder replaced by the signer at finalization time | + +#### Test macro + +Annotate integration test functions with `#[simplex::test]` to get an injected `TestContext` wired to the configured regtest or remote network: + +```rust +#[simplex::test] +fn my_test(context: simplex::TestContext) -> anyhow::Result<()> { + let signer = context.get_signer(); + let provider = context.get_provider(); + // ... + Ok(()) +} +``` + +Run tests with: + +```bash +simplex test integration +``` + +### Examples -Check out the complete project examples in the `examples` directory to learn more. +Check out the complete project examples in the `examples` directory, or scaffold one locally with `simplex example basic`. ## Contributing @@ -120,7 +238,7 @@ We are open to any mind-blowing ideas! Please take a look at our [contributing g ## Future work -- [x] Complete `simplex init` and `simplex clean` tasks. +- [x] Complete `simplex init`, `simplex new`, `simplex example`, and `simplex clean` commands. - [ ] SDK support for confidential assets, taproot signer, and custom witness signatures. - [ ] Local regtest 10x speedup. - [ ] Regtest cheat codes. diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index ff44693..a2779d3 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -5,7 +5,9 @@ use clap::Parser; use crate::commands::Command; use crate::commands::build::Build; use crate::commands::clean::Clean; +use crate::commands::example::Example; use crate::commands::init::Init; +use crate::commands::new::New; use crate::commands::regtest::Regtest; use crate::commands::test::Test; use crate::config::Config; @@ -24,6 +26,8 @@ pub struct Cli { impl Cli { pub async fn run(&self) -> Result<(), CliError> { match &self.command { + Command::New { name } => Ok(New::run(name)?), + Command::Example { example } => Ok(Example::run(example)?), Command::Init { additional_flags } => { let simplex_conf_path = Config::get_default_path()?; diff --git a/crates/cli/src/commands/core.rs b/crates/cli/src/commands/core.rs index 1d84676..b65525c 100644 --- a/crates/cli/src/commands/core.rs +++ b/crates/cli/src/commands/core.rs @@ -1,7 +1,17 @@ -use clap::{Args, Subcommand}; +use clap::{Args, Subcommand, ValueEnum}; #[derive(Debug, Subcommand)] pub enum Command { + /// Creates a new Simplex project in a new directory + New { + /// Name of the new project + name: String, + }, + /// Scaffolds an example Simplex project into a new directory + Example { + /// Name of the example to scaffold + example: ExampleName, + }, /// Initializes Simplex project Init { #[command(flatten)] @@ -22,6 +32,12 @@ pub enum Command { Clean, } +#[derive(Debug, Clone, ValueEnum)] +pub enum ExampleName { + /// Basic p2pk example with contract scaffolding + Basic, +} + #[derive(Debug, Subcommand)] pub enum TestCommand { /// Runs integration tests diff --git a/crates/cli/src/commands/example.rs b/crates/cli/src/commands/example.rs new file mode 100644 index 0000000..de7bd0c --- /dev/null +++ b/crates/cli/src/commands/example.rs @@ -0,0 +1,100 @@ +use std::path::PathBuf; + +use crate::commands::ExampleName; +use crate::commands::error::{CommandError, InitError}; +use crate::commands::init::{Init, SIMPLEX_CRATE_NAME}; + +pub struct Example; + +impl Example { + pub fn run(example: &ExampleName) -> Result<(), CommandError> { + match example { + ExampleName::Basic => Self::create_basic()?, + } + Ok(()) + } + + fn create_basic() -> Result<(), InitError> { + let dir: PathBuf = std::env::current_dir().map_err(InitError::FmtError)?.join("basic"); + + if dir.exists() { + return Err(InitError::CreateDirs( + std::io::Error::new(std::io::ErrorKind::AlreadyExists, "destination 'basic' already exists"), + dir, + )); + } + + let smplx_version = Init::get_smplx_max_version()?; + + // Generate Cargo.toml dynamically with the latest smplx-std version + let manifest = { + let mut manifest = toml_edit::DocumentMut::new(); + manifest["package"] = toml_edit::Item::Table(toml_edit::Table::new()); + manifest["package"]["name"] = toml_edit::value("simplex_example"); + manifest["package"]["version"] = toml_edit::value("0.1.0"); + manifest["package"]["edition"] = toml_edit::value("2024"); + manifest["package"]["rust-version"] = toml_edit::value("1.91.0"); + + let mut dep_table = toml_edit::Table::default(); + dep_table.insert( + SIMPLEX_CRATE_NAME, + toml_edit::Item::Value(toml_edit::Value::String(toml_edit::Formatted::new(smplx_version))), + ); + dep_table.insert( + "anyhow", + toml_edit::Item::Value(toml_edit::Value::String(toml_edit::Formatted::new("1".to_string()))), + ); + manifest["dependencies"] = toml_edit::Item::Table(dep_table); + manifest + }; + + Init::write_to_file(dir.join("Cargo.toml"), manifest.to_string())?; + Init::write_to_file( + dir.join("Simplex.toml"), + include_str!("../../../../examples/basic/Simplex.toml"), + )?; + Init::write_to_file( + dir.join(".gitignore"), + include_str!("../../../../examples/basic/.gitignore"), + )?; + Init::write_to_file( + dir.join("src/lib.rs"), + include_str!("../../../../examples/basic/src/lib.rs"), + )?; + Init::write_to_file( + dir.join("tests/example_test.rs"), + include_str!("../../../../examples/basic/tests/example_test.rs"), + )?; + Init::write_to_file( + dir.join("simf/p2pk.simf"), + include_str!("../../../../examples/basic/simf/p2pk.simf"), + )?; + Init::write_to_file( + dir.join("simf/options.simf"), + include_str!("../../../../examples/basic/simf/options.simf"), + )?; + Init::write_to_file( + dir.join("simf/module/option_offer.simf"), + include_str!("../../../../examples/basic/simf/module/option_offer.simf"), + )?; + Init::write_to_file( + dir.join("simf/another_dir/array_tr_storage.simf"), + include_str!("../../../../examples/basic/simf/another_dir/array_tr_storage.simf"), + )?; + Init::write_to_file( + dir.join("simf/another_dir/another_module/bytes32_tr_storage.simf"), + include_str!("../../../../examples/basic/simf/another_dir/another_module/bytes32_tr_storage.simf"), + )?; + Init::write_to_file( + dir.join("simf/another_dir/another_module/dual_currency_deposit.simf"), + include_str!("../../../../examples/basic/simf/another_dir/another_module/dual_currency_deposit.simf"), + )?; + + println!("Created example project 'basic'"); + println!( + "Run `simplex build` inside 'basic/' to generate artifacts, then `simplex test integration` to run the tests." + ); + + Ok(()) + } +} diff --git a/crates/cli/src/commands/init.rs b/crates/cli/src/commands/init.rs index fc8fa1e..c18f51a 100644 --- a/crates/cli/src/commands/init.rs +++ b/crates/cli/src/commands/init.rs @@ -36,7 +36,7 @@ impl Init { let manifest = { let mut manifest = toml_edit::DocumentMut::new(); manifest["package"] = toml_edit::Item::Table(toml_edit::Table::new()); - manifest["package"]["name"] = toml_edit::value(name); + manifest["package"]["name"] = toml_edit::value(&name); manifest["package"]["version"] = toml_edit::value("0.1.0"); manifest["package"]["edition"] = toml_edit::value("2024"); @@ -47,20 +47,62 @@ impl Init { Self::get_smplx_max_version()?, ))), ); + dep_table.insert( + "anyhow", + toml_edit::Item::Value(toml_edit::Value::String(toml_edit::Formatted::new("1".to_string()))), + ); manifest["dependencies"] = toml_edit::Item::Table(dep_table); manifest }; let default_lib_rs_file_content: &[u8] = { b"pub mod artifacts;" }; - let default_test_file_content: &[u8] = { - b"\ + let default_test_file_content = format!( + "\ +use {name}::artifacts::p2pk::P2pkProgram; +use {name}::artifacts::p2pk::derived_p2pk::{{P2pkArguments, P2pkWitness}}; + +use simplex::constants::DUMMY_SIGNATURE; +use simplex::transaction::{{FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature}}; +use simplex::utils::tr_unspendable_key; + #[simplex::test] -fn dummy_test(context: simplex::TestContext) { - // your test code here - todo!() -}" - }; +fn p2pk_test(context: simplex::TestContext) -> anyhow::Result<()> {{ + let signer = context.get_signer(); + let provider = context.get_provider(); + + // Build the p2pk program with its arguments + let arguments = P2pkArguments {{ + public_key: signer.get_schnorr_public_key().unwrap().serialize(), + }}; + let program = P2pkProgram::new(tr_unspendable_key(), arguments); + let script = program.get_program().get_script_pubkey(context.get_network()).unwrap(); + + // Fund the p2pk script output + let mut ft = FinalTransaction::new(*context.get_network()); + ft.add_output(PartialOutput::new(script.clone(), 50, context.get_network().policy_asset())); + let (tx, _) = signer.finalize(&ft).unwrap(); + let txid = provider.broadcast_transaction(&tx).unwrap(); + provider.wait(&txid)?; + + // Spend the p2pk output by providing the witness signature + let mut utxos = provider.fetch_scripthash_utxos(&script).unwrap(); + utxos.retain(|el| el.1.asset.explicit().unwrap() == context.get_network().policy_asset()); + + let witness = P2pkWitness {{ signature: DUMMY_SIGNATURE }}; + let mut ft = FinalTransaction::new(*context.get_network()); + ft.add_program_input( + PartialInput::new(utxos[0].0, utxos[0].1.clone()), + ProgramInput::new(Box::new(program.get_program().clone()), Box::new(witness)), + RequiredSignature::Witness(\"SIGNATURE\".to_string()), + ).unwrap(); + let (tx, _) = signer.finalize(&ft).unwrap(); + let txid = provider.broadcast_transaction(&tx).unwrap(); + provider.wait(&txid)?; + + Ok(()) +}}" + ); let default_p2pk_simf_file_content: &[u8] = { b"\ fn main() { @@ -77,7 +119,7 @@ fn main() { Self::write_to_file(manifest_path, manifest.to_string())?; Self::write_to_file(&lib_rs_path, default_lib_rs_file_content)?; - Self::write_to_file(&test_rs_path, default_test_file_content)?; + Self::write_to_file(&test_rs_path, default_test_file_content.as_bytes())?; Self::write_to_file(&p2pk_simf_content, default_p2pk_simf_file_content)?; Self::write_to_file(&gitignore_path, default_gitignore_file_content)?; @@ -98,7 +140,7 @@ fn main() { Ok(format!("simplex_{}", file_name)) } - fn get_smplx_max_version() -> Result { + pub(crate) fn get_smplx_max_version() -> Result { let url = format!("https://crates.io/api/v1/crates/{}", SIMPLEX_CRATE_NAME); let response = minreq::get(&url) @@ -120,7 +162,7 @@ fn main() { Ok(latest_version.to_string()) } - fn write_to_file(path: impl AsRef, content: impl AsRef<[u8]>) -> Result<(), InitError> { + pub(crate) fn write_to_file(path: impl AsRef, content: impl AsRef<[u8]>) -> Result<(), InitError> { let path = path.as_ref(); fs::create_dir_all( diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 50cb5fb..11df736 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -2,7 +2,9 @@ pub mod build; pub mod clean; pub mod core; pub mod error; +pub mod example; pub mod init; +pub mod new; pub mod regtest; pub mod test; diff --git a/crates/cli/src/commands/new.rs b/crates/cli/src/commands/new.rs new file mode 100644 index 0000000..90d433d --- /dev/null +++ b/crates/cli/src/commands/new.rs @@ -0,0 +1,28 @@ +use std::path::PathBuf; + +use crate::commands::InitFlags; +use crate::commands::error::CommandError; +use crate::commands::init::Init; + +pub struct New; + +impl New { + pub fn run(name: &str) -> Result<(), CommandError> { + let project_dir: PathBuf = std::env::current_dir()?.join(name); + + if project_dir.exists() { + return Err(CommandError::Io(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + format!("destination '{}' already exists", project_dir.display()), + ))); + } + + let simplex_conf_path = project_dir.join("Simplex.toml"); + + Init::run(InitFlags { lib: true }, simplex_conf_path)?; + + println!("Created new Simplex project '{}'", name); + + Ok(()) + } +}