Skip to content
Open
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
132 changes: 125 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>
```

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
Expand Down Expand Up @@ -97,7 +107,9 @@ Where:

Simplex CLI provides the following commands:

- `simplex init` - Initializes a new Simplex project.
- `simplex new <name>` - Creates a new Simplex project in a new `<name>` directory.
- `simplex example <name>` - 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.
Expand All @@ -110,17 +122,123 @@ 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

We are open to any mind-blowing ideas! Please take a look at our [contributing guidelines](CONTRIBUTING.md) to get involved.

## 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.
Expand Down
4 changes: 4 additions & 0 deletions crates/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()?;

Expand Down
18 changes: 17 additions & 1 deletion crates/cli/src/commands/core.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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
Expand Down
100 changes: 100 additions & 0 deletions crates/cli/src/commands/example.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}
Loading
Loading