diff --git a/Cargo.lock b/Cargo.lock index 3af41ae..6f459ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,9 +1313,9 @@ dependencies = [ "clap", "ctrlc", "dotenvy", - "electrsd", + "minreq", "serde", - "simplicityhl", + "serde_json", "smplx-build", "smplx-regtest", "smplx-sdk", @@ -1323,6 +1323,7 @@ dependencies = [ "thiserror", "tokio", "toml 0.9.12+spec-1.1.0", + "toml_edit", ] [[package]] @@ -1554,6 +1555,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_parser" version = "1.0.9+spec-1.1.0" @@ -1877,6 +1891,9 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] [[package]] name = "wit-bindgen" diff --git a/README.md b/README.md index ef111d3..5ef0d6d 100644 --- a/README.md +++ b/README.md @@ -97,11 +97,12 @@ Where: Simplex CLI provides the following commands: -- `simplex init` - Initializes a Simplex project. +- `simplex init` - Initializes a new Simplex project. - `simplex config` - Prints the current config. - `simplex build` - Generates simplicity artifacts. - `simplex regtest` - Spins up local Electrs + Elements nodes. - `simplex test` - Runs Simplex tests. +- `simplex clean` - Cleans up generated artifacts. To view the available options, run the help command: @@ -119,7 +120,7 @@ We are open to any mind-blowing ideas! Please take a look at our [contributing g ## Future work -- [ ] Complete `simplex init` and `simplex clean` tasks. +- [x] Complete `simplex init` and `simplex clean` tasks. - [ ] SDK support for confidential assets, taproot signer, and custom witness signatures. - [ ] Local regtest 10x speedup. - [ ] Regtest cheat codes. diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index eda9790..a96e5f8 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -17,17 +17,18 @@ workspace = true [dependencies] smplx-regtest = { workspace = true } smplx-test = { workspace = true } -smplx-build = { workspace = true} +smplx-build = { workspace = true } smplx-sdk = { workspace = true } -simplicityhl = { workspace = true } -electrsd = { workspace = true } thiserror = { workspace = true } serde = { workspace = true } toml = { workspace = true } +minreq = { workspace = true } anyhow = "1" dotenvy = "0.15" clap = { version = "4", features = ["derive", "env"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -ctrlc = { version = "3.5.2", features = ["termination"] } +toml_edit = { version = "0.23.9" } +ctrlc = { version = "3.5.2", features = ["termination"] } +serde_json = { version = "1.0.149" } diff --git a/crates/cli/Simplex.default.toml b/crates/cli/assets/Simplex.default.toml similarity index 100% rename from crates/cli/Simplex.default.toml rename to crates/cli/assets/Simplex.default.toml diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 09a98a5..ad0d15d 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -4,9 +4,11 @@ use clap::Parser; use crate::commands::Command; use crate::commands::build::Build; +use crate::commands::clean::Clean; +use crate::commands::init::Init; use crate::commands::regtest::Regtest; use crate::commands::test::Test; -use crate::config::{Config, INIT_CONFIG}; +use crate::config::Config; use crate::error::CliError; #[derive(Debug, Parser)] @@ -22,13 +24,10 @@ pub struct Cli { impl Cli { pub async fn run(&self) -> Result<(), CliError> { match &self.command { - Command::Init => { - let config_path = Config::get_default_path()?; - std::fs::write(&config_path, INIT_CONFIG)?; - - println!("Config written to: '{}'", config_path.display()); + Command::Init { additional_flags } => { + let simplex_conf_path = Config::get_default_path()?; - Ok(()) + Ok(Init::run(*additional_flags, simplex_conf_path)?) } Command::Config => { let config_path = Config::get_default_path()?; @@ -56,6 +55,12 @@ impl Cli { Ok(Build::run(loaded_config.build)?) } + Command::Clean { additional_flags } => { + let config_path = Config::get_default_path()?; + let loaded_config = Config::load(&config_path)?; + + Ok(Clean::run(loaded_config.build, *additional_flags, config_path)?) + } } } } diff --git a/crates/cli/src/commands/clean.rs b/crates/cli/src/commands/clean.rs new file mode 100644 index 0000000..220b246 --- /dev/null +++ b/crates/cli/src/commands/clean.rs @@ -0,0 +1,97 @@ +use std::{ + fmt::Display, + fs, + path::{Path, PathBuf}, +}; + +use smplx_build::{ArtifactsResolver, BuildConfig}; + +use crate::commands::error::CommandError; +use crate::commands::{CleanFlags, error::CleanError}; + +pub struct Clean; + +pub struct DeletedItems(Vec); + +impl Clean { + pub fn run( + config: BuildConfig, + additional_flags: CleanFlags, + config_path: impl AsRef, + ) -> Result<(), CommandError> { + let deleted_files = Self::delete_files(config, additional_flags, config_path)?; + + println!("Deleted files: {deleted_files}"); + + Ok(()) + } + + fn delete_files( + config: BuildConfig, + additional_flags: CleanFlags, + smplx_toml_path: impl AsRef, + ) -> Result { + let mut deleted_items = Vec::with_capacity(2); + let generated_artifacts = Self::remove_artifacts(config)?; + + if let Some(artifacts_dir) = generated_artifacts { + deleted_items.push(artifacts_dir); + } + + if additional_flags.all { + let simplex_toml_path = Self::remove_config(smplx_toml_path)?; + + if let Some(simplex_toml) = simplex_toml_path { + deleted_items.push(simplex_toml); + } + } + + Ok(DeletedItems(deleted_items)) + } + + fn remove_artifacts(config: BuildConfig) -> Result, CleanError> { + let output_dir = ArtifactsResolver::resolve_local_dir(&config.out_dir) + .map_err(|e| CleanError::ResolveOutDir(e.to_string()))?; + + let res = if output_dir.exists() { + fs::remove_dir_all(&output_dir).map_err(|e| CleanError::RemoveOutDir(e, output_dir.to_path_buf()))?; + Some(output_dir) + } else { + None + }; + + Ok(res) + } + + fn remove_config(config_path: impl AsRef) -> Result, CleanError> { + let config_path = config_path.as_ref().to_path_buf(); + + if config_path.exists() && config_path.is_file() { + fs::remove_file(&config_path).map_err(|e| CleanError::RemoveFile(e, config_path.to_path_buf()))?; + + Ok(Some(config_path)) + } else { + Ok(None) + } + } +} + +impl Display for DeletedItems { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let paths_len = self.0.len(); + let mut result = String::from("["); + + for (index, path) in self.0.iter().enumerate() { + result.push_str(&format!("\n\t{}", path.display())); + + if index < paths_len - 1 { + result.push(','); + } + result.push('\n'); + } + + result.push(']'); + + write!(f, "{}", result) + } +} diff --git a/crates/cli/src/commands/core.rs b/crates/cli/src/commands/core.rs index 1e59c70..2ff8f51 100644 --- a/crates/cli/src/commands/core.rs +++ b/crates/cli/src/commands/core.rs @@ -2,19 +2,27 @@ use clap::{Args, Subcommand}; #[derive(Debug, Subcommand)] pub enum Command { - /// Initializes the Simplex project (TODO) - Init, - /// Prints the current Simplex config in use + /// Initializes Simplex project + Init { + #[command(flatten)] + additional_flags: InitFlags, + }, + /// Prints current Simplex config in use Config, /// Spins up the local Electrs + Elements regtest Regtest, - /// Runs the Simplex tests + /// Runs Simplex tests Test { #[command(subcommand)] command: TestCommand, }, /// Generates the simplicity contracts artifacts Build, + /// Clean Simplex artifacts in the current directory + Clean { + #[command(flatten)] + additional_flags: CleanFlags, + }, } #[derive(Debug, Subcommand)] @@ -46,3 +54,17 @@ pub struct TestFlags { #[arg(long)] pub ignored: bool, } + +#[derive(Debug, Args, Copy, Clone)] +pub struct InitFlags { + /// Generate a draft Rust library instead of just `Simplex.toml` + #[arg(long)] + pub lib: bool, +} + +#[derive(Debug, Args, Copy, Clone)] +pub struct CleanFlags { + /// Remove `Simplex.toml` as well + #[arg(long)] + pub all: bool, +} diff --git a/crates/cli/src/commands/error.rs b/crates/cli/src/commands/error.rs index 6cadf5f..38e27c1 100644 --- a/crates/cli/src/commands/error.rs +++ b/crates/cli/src/commands/error.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + #[derive(thiserror::Error, Debug)] pub enum CommandError { #[error(transparent)] @@ -12,6 +14,51 @@ pub enum CommandError { #[error(transparent)] Build(#[from] smplx_build::error::BuildError), + #[error(transparent)] + Init(#[from] InitError), + + #[error(transparent)] + Clean(#[from] CleanError), + #[error("IO error: {0}")] Io(#[from] std::io::Error), } + +#[derive(thiserror::Error, Debug)] +pub enum InitError { + #[error("Failed to open file '{1}': {0}")] + OpenFile(std::io::Error, PathBuf), + + #[error("Failed to write to file '{1}': {0}")] + WriteToFile(std::io::Error, PathBuf), + + #[error("Failed to format file with rustfmt: {0}")] + FmtError(std::io::Error), + + #[error("Failed to resolve parent directory for: {0}")] + ResolveParent(PathBuf), + + #[error("Failed to create directories at '{1}': {0}")] + CreateDirs(std::io::Error, PathBuf), + + #[error("Failed to fetch crate info from crates.io: {0}")] + CratesIoFetch(String), + + #[error("Cannot auto-detect package name from path: {0}")] + PackageName(PathBuf), + + #[error("Cannot create package with a non-unicode name: '{0}'")] + NonUnicodeName(String), +} + +#[derive(thiserror::Error, Debug)] +pub enum CleanError { + #[error("Failed to resolve out_dir from config, err: '{0}'")] + ResolveOutDir(String), + + #[error("Failed to remove output directory '{1}': {0}")] + RemoveOutDir(std::io::Error, PathBuf), + + #[error("Failed to remove file '{1}': {0}")] + RemoveFile(std::io::Error, PathBuf), +} diff --git a/crates/cli/src/commands/init.rs b/crates/cli/src/commands/init.rs new file mode 100644 index 0000000..97865e2 --- /dev/null +++ b/crates/cli/src/commands/init.rs @@ -0,0 +1,153 @@ +use std::{fs, fs::OpenOptions, io::Write, path::Path}; + +use crate::commands::error::CommandError; +use crate::commands::{InitFlags, error::InitError}; +use crate::config::INIT_CONFIG; + +pub const SIMPLEX_CRATE_NAME: &str = "smplx-std"; + +pub struct Init; + +impl Init { + pub fn run(conf: InitFlags, smplx_conf_path: impl AsRef) -> Result<(), CommandError> { + if conf.lib { + Self::generate_lib_inplace(&smplx_conf_path)? + } + + Self::fill_simplex_toml(smplx_conf_path)?; + + Ok(()) + } + + fn fill_simplex_toml(config_path: impl AsRef) -> Result<(), InitError> { + let path_to_write = config_path.as_ref(); + Self::write_to_file(path_to_write, INIT_CONFIG)?; + + println!("Config written to: '{}'", path_to_write.display()); + + Ok(()) + } + + fn generate_lib_inplace(config_path: impl AsRef) -> Result<(), InitError> { + let pwd = config_path.as_ref().parent().unwrap(); + let name = Self::get_project_name(pwd)?; + + // Create `Cargo.toml` file + 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"]["version"] = toml_edit::value("0.1.0"); + manifest["package"]["edition"] = toml_edit::value("2024"); + + 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( + Self::get_smplx_max_version()?, + ))), + ); + + manifest["dependencies"] = toml_edit::Item::Table(dep_table); + manifest + }; + + let default_lib_rs_file_content: &[u8] = { b"mod artifacts;" }; + let default_test_file_content: &[u8] = { + b"\ +#[simplex::test] +fn dummy_test(context: simplex::TestContext) { + // your test code here + todo!() +}" + }; + let default_p2pk_simf_file_content: &[u8] = { + b"\ +fn main() { + jet::bip_0340_verify((param::PUBLIC_KEY, jet::sig_all_hash()), witness::SIGNATURE) +}" + }; + let default_gitignore_file_content: &[u8] = { b"src/artifacts" }; + + let manifest_path = pwd.join("Cargo.toml"); + let lib_rs_path = pwd.join("src/lib.rs"); + let p2pk_simf_content = pwd.join("simf/p2pk.simf"); + let test_rs_path = pwd.join("tests/p2pk_test.rs"); + let gitignore_path = pwd.join(".gitignore"); + + 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(&p2pk_simf_content, default_p2pk_simf_file_content)?; + Self::write_to_file(&gitignore_path, default_gitignore_file_content)?; + + Self::execute_cargo_fmt(lib_rs_path)?; + + Ok(()) + } + + fn get_project_name(path: &Path) -> Result<&str, InitError> { + let file_name = path + .file_name() + .ok_or_else(|| InitError::PackageName(path.to_path_buf()))?; + + file_name + .to_str() + .ok_or_else(|| InitError::NonUnicodeName(format!("{file_name:?}"))) + } + + fn get_smplx_max_version() -> Result { + let url = format!("https://crates.io/api/v1/crates/{}", SIMPLEX_CRATE_NAME); + + let response = minreq::get(&url) + .with_header("User-Agent", "simplex_generator") + .send() + .map_err(|e| InitError::CratesIoFetch(format!("Failed to fetch crate info: {}", e)))?; + + let body = response + .as_str() + .map_err(|e| InitError::CratesIoFetch(format!("Invalid response body: {}", e)))?; + + let json: serde_json::Value = + serde_json::from_str(body).map_err(|e| InitError::CratesIoFetch(format!("Failed to parse JSON: {}", e)))?; + + let latest_version = json["crate"]["max_stable_version"] + .as_str() + .ok_or_else(|| InitError::CratesIoFetch("Could not find max_version in response".to_string()))?; + + Ok(latest_version.to_string()) + } + + fn write_to_file(path: impl AsRef, content: impl AsRef<[u8]>) -> Result<(), InitError> { + let path = path.as_ref(); + + fs::create_dir_all( + path.parent() + .ok_or_else(|| InitError::ResolveParent(path.to_path_buf()))?, + ) + .map_err(|e| InitError::CreateDirs(e, path.to_path_buf()))?; + + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path) + .map_err(|e| InitError::OpenFile(e, path.to_path_buf()))?; + file.write_all(content.as_ref()) + .map_err(|e| InitError::WriteToFile(e, path.to_path_buf()))?; + file.flush() + .map_err(|e| InitError::WriteToFile(e, path.to_path_buf()))?; + + Ok(()) + } + + fn execute_cargo_fmt(file: impl AsRef) -> Result<(), InitError> { + let mut cargo_test_command = std::process::Command::new("sh"); + + cargo_test_command.args(["-c".to_string(), format!("rustfmt {}", file.as_ref().display())]); + + let _output = cargo_test_command.output().map_err(InitError::FmtError); + + Ok(()) + } +} diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 81faac2..50cb5fb 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -1,6 +1,8 @@ pub mod build; +pub mod clean; pub mod core; pub mod error; +pub mod init; pub mod regtest; pub mod test; diff --git a/crates/cli/src/config/core.rs b/crates/cli/src/config/core.rs index e03b6a3..14caf75 100644 --- a/crates/cli/src/config/core.rs +++ b/crates/cli/src/config/core.rs @@ -7,7 +7,7 @@ use smplx_test::TestConfig; use super::error::ConfigError; -pub const INIT_CONFIG: &str = include_str!("../../Simplex.default.toml"); +pub const INIT_CONFIG: &str = include_str!("../../assets/Simplex.default.toml"); pub const CONFIG_FILENAME: &str = "Simplex.toml"; #[derive(Debug, Default, Clone, Deserialize)] @@ -20,9 +20,11 @@ pub struct Config { impl Config { pub fn get_default_path() -> Result { - let cwd = std::env::current_dir()?; + Self::get_path(std::env::current_dir()?) + } - Ok(cwd.join(CONFIG_FILENAME)) + pub fn get_path(path: impl AsRef) -> Result { + Ok(path.as_ref().join(CONFIG_FILENAME)) } pub fn load(path_buf: impl AsRef) -> Result {