diff --git a/CHANGELOG.md b/CHANGELOG.md index cb7548c..85db4c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,31 @@ # Changelog -## [Unreleased] +## [0.0.5] +### Simplex + +- Added some documentation to the public simplex functions. +- Implemented "global configuration" singleton in the SDK. +- Added `TxReceipt` object that gets returned upon transaction broadcasting for blocks confirmation convenience. +- Fixed `FinalTransaction` issuance inputs that didn't work correctly with inflation tokens. +- Renamed `with_pub_key` program method to `with_taproot_pubkey`. +- Added `asset` and `amount` methods to the `UTXO` struct. +- Refactored `simplex test` command: + - Added compatibility for custom test name filters. + - Added `--target` flag that isolates tests to a specific integration test module. + - Added `--no-fail-fast` flag to allow tests to keep running even if one fails. + - Added `--quiet` flag to suppress some simplex logging. +- Refactored `simplex new` command to set up a new project by accepting a directory name. +- Added `NetworkUtils` to `TestContext` for some "network cheatcodes": + - The `mine_until_height` function is provided. +- Added a `-v` flag to `simplex test` that logs simplicity pruning traces. +- Fixed `rustfmt` warning on generated artifacts. Now they are skipped. - Added fixtures for simplex integration tests. +### Simplexup + +- Added ability to fetch specific simplex commits. + ## [0.0.4] - Sped up regtest setup 3x times by mining a block after tokens sweep. diff --git a/Cargo.lock b/Cargo.lock index c0ce516..153444d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -897,12 +897,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - [[package]] name = "pin-utils" version = "0.1.0" @@ -1273,8 +1267,9 @@ dependencies = [ [[package]] name = "simplicity-sys" -version = "0.6.1" -source = "git+https://github.com/BlockstreamResearch/rust-simplicity?tag=simplicity-sys-0.6.1#7f42b532fdcad8b88e65af467a238af28204bc8b" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3401ee7331f183a5458c0f5a4b3d5d00bde0fd12e2e03728c537df34efae289" dependencies = [ "bitcoin_hashes", "cc", @@ -1282,9 +1277,9 @@ dependencies = [ [[package]] name = "simplicityhl" -version = "0.5.0-rc.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8fb4ae422c730ec6e274c1438013ce6d7b41b97152e2dec63735e8daad1f1c4" +checksum = "25de8990174fe3e1a843df138cacc4265d05839ebd2550c18b9196f567d55e81" dependencies = [ "base64 0.21.7", "chumsky", @@ -1300,7 +1295,7 @@ dependencies = [ [[package]] name = "smplx-build" -version = "0.0.4" +version = "0.0.5" dependencies = [ "glob", "globwalk", @@ -1317,7 +1312,7 @@ dependencies = [ [[package]] name = "smplx-cli" -version = "0.0.4" +version = "0.0.5" dependencies = [ "anyhow", "clap", @@ -1331,14 +1326,13 @@ dependencies = [ "smplx-sdk", "smplx-test", "thiserror", - "tokio", "toml 0.9.12+spec-1.1.0", "toml_edit", ] [[package]] name = "smplx-macros" -version = "0.0.4" +version = "0.0.5" dependencies = [ "smplx-build", "smplx-test", @@ -1347,7 +1341,7 @@ dependencies = [ [[package]] name = "smplx-regtest" -version = "0.0.4" +version = "0.0.5" dependencies = [ "electrsd", "hex", @@ -1361,7 +1355,7 @@ dependencies = [ [[package]] name = "smplx-sdk" -version = "0.0.4" +version = "0.0.5" dependencies = [ "bip39", "bitcoin_hashes", @@ -1379,7 +1373,7 @@ dependencies = [ [[package]] name = "smplx-std" -version = "0.0.4" +version = "0.0.5" dependencies = [ "either", "serde", @@ -1392,7 +1386,7 @@ dependencies = [ [[package]] name = "smplx-test" -version = "0.0.4" +version = "0.0.5" dependencies = [ "electrsd", "proc-macro2", @@ -1505,27 +1499,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tokio" -version = "1.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" -dependencies = [ - "pin-project-lite", - "tokio-macros", -] - -[[package]] -name = "tokio-macros" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "toml" version = "0.9.12+spec-1.1.0" diff --git a/Cargo.toml b/Cargo.toml index cbc5032..9136de6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,35 +1,36 @@ [workspace] resolver = "3" -members = [ - "crates/*", -] +members = ["crates/*"] exclude = ["examples/basic", "fixtures"] [workspace.package] license = "MIT" edition = "2024" -[workspace.lints.clippy] -multiple_crate_versions = "allow" - [workspace.dependencies] -smplx-macros = { path = "./crates/macros", version = "0.0.4" } -smplx-build = { path = "./crates/build", version = "0.0.4" } -smplx-test = { path = "./crates/test", version = "0.0.4" } -smplx-regtest = { path = "./crates/regtest", version = "0.0.4" } -smplx-sdk = { path = "./crates/sdk", version = "0.0.4" } -smplx-std = { path = "./crates/simplex", version = "0.0.4" } +smplx-macros = { path = "./crates/macros", version = "0.0.5" } +smplx-build = { path = "./crates/build", version = "0.0.5" } +smplx-test = { path = "./crates/test", version = "0.0.5" } +smplx-regtest = { path = "./crates/regtest", version = "0.0.5" } +smplx-sdk = { path = "./crates/sdk", version = "0.0.5" } +smplx-std = { path = "./crates/simplex", version = "0.0.5" } -serde = { version = "1.0.228", features = ["derive"]} +serde = { version = "1.0.228", features = ["derive"] } hex = { version = "0.4.3" } hmac = { version = "0.12.1" } sha2 = { version = "0.10.9", features = ["compress"] } thiserror = { version = "2.0.18" } toml = { version = "0.9.8" } minreq = { version = "2.14.1", features = ["https", "json-using-serde"] } + electrsd = { version = "0.29.0", features = ["legacy"] } +simplicityhl = { version = "0.5.0" } -simplicityhl = { version = "0.5.0-rc.0" } +[workspace.lints.rust] +rust_2018_idioms = "warn" +unused_lifetimes = "warn" +unreachable_pub = "warn" +deprecated_in_future = "warn" -[patch.crates-io] -simplicity-sys = { git = "https://github.com/BlockstreamResearch/rust-simplicity", tag = "simplicity-sys-0.6.1" } +[workspace.lints.clippy] +multiple_crate_versions = "allow" diff --git a/README.md b/README.md index f12e31a..234ae56 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ ![](https://github.com/user-attachments/assets/c4661df7-6101-4c46-9376-dedaeef8056b) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Tests](https://github.com/BlockstreamResearch/smplx/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/BlockstreamResearch/smplx/workflows/ci.yml) +[![Tests](https://github.com/BlockstreamResearch/smplx/actions/workflows/crates.yml/badge.svg?branch=master)](https://github.com/BlockstreamResearch/smplx/workflows/crates.yml) +[![Integration](https://github.com/BlockstreamResearch/smplx/actions/workflows/fixtures.yml/badge.svg?branch=master)](https://github.com/BlockstreamResearch/smplx/workflows/fixtures.yml) [![Community](https://img.shields.io/endpoint?color=neon&logo=telegram&label=Chat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fsimplicity_community)](https://t.me/simplicity_community) # Smplx @@ -40,7 +41,7 @@ cargo add --dev smplx-std Optionally, initialize a new project: ```bash -simplex init +simplex init ``` ## Usage @@ -66,6 +67,7 @@ rpc_password = "password" [test] mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" bitcoins = 10_000_000 +verbosity = 3 # 1 - none, 2 - warning, 3 - debug, 4 - trace [test.esplora] url = "" @@ -93,6 +95,7 @@ Where: - `test` (`simplex test` config) - `mnemonic` - The signer's mnemonic internal regtest will send initial funds to. - `bitcoins` - Initial coins available to the signer. + - `verbosity` - Simplicity pruning log level. - `esplora` - `url` - Esplora API endpoint url. - `network` - Esplora network type (`Liquid`, `LiquidTestnet`, `ElementsRegtest`). diff --git a/crates/build/Cargo.toml b/crates/build/Cargo.toml index b070e90..988dc6d 100644 --- a/crates/build/Cargo.toml +++ b/crates/build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "smplx-build" -version = "0.0.4" +version = "0.0.5" description = "Simplex build command internal implementation" license.workspace = true edition.workspace = true diff --git a/crates/build/src/generator.rs b/crates/build/src/generator.rs index 7260bd5..670bbff 100644 --- a/crates/build/src/generator.rs +++ b/crates/build/src/generator.rs @@ -175,44 +175,53 @@ impl ArtifactsGenerator { impl #program_name { pub const SOURCE: &'static str = #include_simf_module::#include_simf_source_const; + #[must_use] pub fn new(arguments: impl ArgumentsTrait + 'static) -> Self { Self { program: Program::new(Self::SOURCE, Box::new(arguments)), } } - pub fn with_pub_key(mut self, pub_key: XOnlyPublicKey) -> Self { - self.program = self.program.with_pub_key(pub_key); + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); self } + #[must_use] pub fn with_storage_capacity(mut self, capacity: usize) -> Self { self.program = self.program.with_storage_capacity(capacity); self } + #[must_use] pub fn set_storage_at(&mut self, index: usize, new_value: [u8; 32]) { self.program.set_storage_at(index, new_value); } + #[must_use] pub fn get_storage_len(&self) -> usize { self.program.get_storage_len() } + #[must_use] pub fn get_storage(&self) -> &[[u8; 32]] { self.program.get_storage() } + #[must_use] pub fn get_storage_at(&self, index: usize) -> [u8; 32] { self.program.get_storage_at(index) } + #[must_use] pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { self.program.get_script_pubkey(network) } + #[must_use] pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { self.program.get_script_hash(network) } @@ -242,7 +251,10 @@ impl ArtifactsGenerator { let code = quote! { #![allow(clippy::all)] - #(pub mod #mod_names);*; + #( + #[rustfmt::skip] + pub mod #mod_names; + )* }; Ok(code) diff --git a/crates/build/src/macros/parse.rs b/crates/build/src/macros/parse.rs index 3cdd34d..ca508ff 100644 --- a/crates/build/src/macros/parse.rs +++ b/crates/build/src/macros/parse.rs @@ -14,7 +14,7 @@ pub struct SynFilePath { } impl Parse for SynFilePath { - fn parse(input: ParseStream) -> syn::Result { + fn parse(input: ParseStream<'_>) -> syn::Result { let expr = input.parse::()?; let span_file = expr.span().file(); diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 08475bd..a558313 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "smplx-cli" -version = "0.0.4" +version = "0.0.5" description = "Simplex cli with various utilities to manage a simplicity project" license.workspace = true edition.workspace = true @@ -28,7 +28,6 @@ minreq = { workspace = true } anyhow = "1" dotenvy = "0.15" clap = { version = "4", features = ["derive", "env"] } -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } toml_edit = { version = "0.23.9" } ctrlc = { version = "3.5.2", features = ["termination"] } serde_json = { version = "1.0.149" } diff --git a/crates/cli/assets/Simplex.default.toml b/crates/cli/assets/Simplex.default.toml index 979e816..3d71376 100644 --- a/crates/cli/assets/Simplex.default.toml +++ b/crates/cli/assets/Simplex.default.toml @@ -16,6 +16,7 @@ # [test] # mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" # bitcoins = 10_000_000 +# verbosity = 3 # [test.esplora] # url = "" diff --git a/crates/cli/src/bin/main.rs b/crates/cli/src/bin/main.rs index 465f356..a573c45 100644 --- a/crates/cli/src/bin/main.rs +++ b/crates/cli/src/bin/main.rs @@ -1,10 +1,9 @@ use clap::Parser; -#[tokio::main] -async fn main() -> anyhow::Result<()> { +fn main() -> anyhow::Result<()> { let _ = dotenvy::dotenv(); - Box::pin(smplx_cli::Cli::parse().run()).await?; + smplx_cli::Cli::parse().run()?; Ok(()) } diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 2186fd3..69edbbb 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -22,12 +22,29 @@ pub struct Cli { } impl Cli { - pub async fn run(&self) -> Result<(), CliError> { + /// Executes the parsed command and routes it to the corresponding sub-handler. + /// + /// # Errors + /// Returns a `CliError` if loading the configuration fails, an underlying command execution encounters an error, + /// or if there are file system I/O errors (for example, attempting to initialize over an existing project directory). + pub fn run(&self) -> Result<(), CliError> { match &self.command { - Command::Init { additional_flags } => { - let simplex_conf_path = Config::get_default_path()?; + Command::Init { name } => { + let simplex_conf_path = match name { + Some(name) => { + let dir = std::env::current_dir()?.join(name); - Ok(Init::run(simplex_conf_path, additional_flags)?) + if dir.exists() { + return Err(CliError::Io(std::io::Error::from(std::io::ErrorKind::AlreadyExists))); + } + + std::fs::create_dir_all(&dir)?; + dir.join("Simplex.toml") + } + None => Config::get_default_path()?, + }; + + Ok(Init::run(simplex_conf_path)?) } Command::Config => { let config_path = Config::get_default_path()?; @@ -37,31 +54,29 @@ impl Cli { Ok(()) } - Command::Test { name, additional_flags } => { + Command::Test { args, flags } => { let config_path = Config::get_default_path()?; let loaded_config = Config::load(config_path)?; - let filter = name.clone().unwrap_or_default(); - - Ok(Test::run(loaded_config.test, filter, additional_flags)?) + Ok(Test::run(loaded_config.test, args, flags)?) } Command::Regtest => { let config_path = Config::get_default_path()?; let loaded_config = Config::load(config_path)?; - Ok(Regtest::run(loaded_config.regtest)?) + Ok(Regtest::run(&loaded_config.regtest)?) } Command::Build => { let config_path = Config::get_default_path()?; let loaded_config = Config::load(config_path)?; - Ok(Build::run(loaded_config.build)?) + Ok(Build::run(&loaded_config.build)?) } Command::Clean => { let config_path = Config::get_default_path()?; let loaded_config = Config::load(&config_path)?; - Ok(Clean::run(loaded_config.build)?) + Ok(Clean::run(&loaded_config.build)?) } } } diff --git a/crates/cli/src/commands/build.rs b/crates/cli/src/commands/build.rs index 337e7f2..d6d6977 100644 --- a/crates/cli/src/commands/build.rs +++ b/crates/cli/src/commands/build.rs @@ -5,7 +5,11 @@ use super::error::CommandError; pub struct Build {} impl Build { - pub fn run(config: BuildConfig) -> Result<(), CommandError> { + /// Builds the project and generates artifacts based on the provided configuration. + /// + /// # Errors + /// Returns a `CommandError` if it fails to resolve directories or files, or if artifact generation encounters an error. + pub fn run(config: &BuildConfig) -> Result<(), CommandError> { let output_dir = ArtifactsResolver::resolve_local_dir(&config.out_dir)?; let src_dir = ArtifactsResolver::resolve_local_dir(&config.src_dir)?; let files_to_build = ArtifactsResolver::resolve_files_to_build(&config.src_dir, &config.simf_files)?; diff --git a/crates/cli/src/commands/clean.rs b/crates/cli/src/commands/clean.rs index 586def0..4d3664c 100644 --- a/crates/cli/src/commands/clean.rs +++ b/crates/cli/src/commands/clean.rs @@ -10,7 +10,13 @@ pub struct Clean; pub struct DeletedItems(Vec); impl Clean { - pub fn run(config: BuildConfig) -> Result<(), CommandError> { + /// Cleans up generated artifacts from the project. + /// + /// This method removes compiled files and output directories based on the provided configuration. + /// + /// # Errors + /// Returns a `CommandError` if it fails to resolve the artifacts directory or if an error occurs while removing the directories. + pub fn run(config: &BuildConfig) -> Result<(), CommandError> { let deleted_files = Self::delete_files(config)?; println!("Deleted files: {deleted_files}"); @@ -18,7 +24,7 @@ impl Clean { Ok(()) } - fn delete_files(config: BuildConfig) -> Result { + fn delete_files(config: &BuildConfig) -> Result { let mut deleted_items = Vec::with_capacity(1); let generated_artifacts = Self::remove_artifacts(config)?; @@ -29,12 +35,12 @@ impl Clean { Ok(DeletedItems(deleted_items)) } - fn remove_artifacts(config: BuildConfig) -> Result, CleanError> { + 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()))?; + fs::remove_dir_all(&output_dir).map_err(|e| CleanError::RemoveOutDir(e, output_dir.clone()))?; Some(output_dir) } else { None @@ -46,11 +52,13 @@ impl Clean { impl Display for DeletedItems { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::fmt::Write; + let paths_len = self.0.len(); let mut result = String::from("["); for (index, path) in self.0.iter().enumerate() { - result.push_str(&format!("\n {}", path.display())); + let _ = write!(result, "\n {}", path.display()); if index < paths_len - 1 { result.push(','); @@ -61,6 +69,6 @@ impl Display for DeletedItems { result.push(']'); - write!(f, "{}", result) + write!(f, "{result}") } } diff --git a/crates/cli/src/commands/core.rs b/crates/cli/src/commands/core.rs index c5a667e..c579aea 100644 --- a/crates/cli/src/commands/core.rs +++ b/crates/cli/src/commands/core.rs @@ -4,8 +4,8 @@ use clap::{Args, Subcommand}; pub enum Command { /// Initializes Simplex project Init { - #[command(flatten)] - additional_flags: InitFlags, + /// Name of the new project + name: Option, }, /// Prints current Simplex config in use Config, @@ -13,12 +13,11 @@ pub enum Command { Regtest, /// Runs Simplex tests Test { - /// Name or a substring of the tests to run - #[arg()] - name: Option, + #[command(flatten)] + args: TestArguments, #[command(flatten)] - additional_flags: TestFlags, + flags: TestFlags, }, /// Generates the simplicity contracts artifacts Build, @@ -26,14 +25,19 @@ pub enum Command { Clean, } -#[derive(Debug, Args, Copy, Clone)] -pub struct InitFlags { - /// Generate a draft Rust library instead of just `Simplex.toml` - #[arg(long)] - pub lib: bool, +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Args, Clone)] +pub struct TestArguments { + /// Space-separated test name filters + #[arg(value_name = "FILTER", num_args = 0..)] + pub filters: Vec, + /// Integration test target to run + #[arg(long = "target")] + pub target: Option, } -#[derive(Debug, Args, Copy, Clone)] +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Args, Clone)] pub struct TestFlags { /// Show output from successful tests #[arg(long)] @@ -44,4 +48,13 @@ pub struct TestFlags { /// Run ignored tests #[arg(long)] pub ignored: bool, + /// Run tests regardless of failure + #[arg(long = "no-fail-fast")] + pub no_fail_fast: bool, + /// Log simplicity pruning stack trace + #[arg(short = 'v', long)] + pub verbose: bool, + /// Display one character per test instead of one line + #[arg(short = 'q', long)] + pub quiet: bool, } diff --git a/crates/cli/src/commands/init.rs b/crates/cli/src/commands/init.rs index 207e196..c2176e9 100644 --- a/crates/cli/src/commands/init.rs +++ b/crates/cli/src/commands/init.rs @@ -1,7 +1,7 @@ use std::{fs, fs::OpenOptions, io::Write, path::Path}; use crate::commands::error::CommandError; -use crate::commands::{InitFlags, error::InitError}; +use crate::commands::error::InitError; use crate::config::INIT_CONFIG; pub const SIMPLEX_CRATE_NAME: &str = "smplx-std"; @@ -9,10 +9,15 @@ pub const SIMPLEX_CRATE_NAME: &str = "smplx-std"; pub struct Init; impl Init { - pub fn run(smplx_conf_path: impl AsRef, flags: &InitFlags) -> Result<(), CommandError> { - if flags.lib { - Self::generate_lib_inplace(&smplx_conf_path)? - } + /// Initializes a new Simplex project at the specified configuration path. + /// + /// This method generates the necessary project files and directories (including + /// `Cargo.toml`, source files, test templates, and configuration files) in place. + /// + /// # Errors + /// Returns a `CommandError` if creating directories, writing project files, or fetching the latest crate version from `crates.io` fails. + pub fn run(smplx_conf_path: impl AsRef) -> Result<(), CommandError> { + Self::generate_lib_inplace(&smplx_conf_path)?; Self::fill_simplex_toml(smplx_conf_path)?; @@ -55,6 +60,8 @@ impl Init { let default_lib_rs_file_content: &[u8] = { b"pub mod artifacts;" }; let default_test_file_content: &[u8] = { b"\ +/// For a complete working example, browse the source at: +/// #[simplex::test] fn dummy_test(context: simplex::TestContext) { // your test code here @@ -93,25 +100,25 @@ fn main() { let file_name = file_name .to_str() - .ok_or_else(|| InitError::NonUnicodeName(format!("{file_name:?}")))?; + .ok_or_else(|| InitError::NonUnicodeName(format!("{}", file_name.display())))?; - Ok(format!("simplex_{}", file_name)) + Ok(format!("simplex_{file_name}")) } fn get_smplx_max_version() -> Result { - let url = format!("https://crates.io/api/v1/crates/{}", SIMPLEX_CRATE_NAME); + 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)))?; + .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)))?; + .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)))?; + 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() @@ -143,6 +150,7 @@ fn main() { Ok(()) } + #[allow(clippy::unnecessary_wraps)] fn execute_cargo_fmt(file: impl AsRef) -> Result<(), InitError> { let mut cargo_test_command = std::process::Command::new("sh"); diff --git a/crates/cli/src/commands/regtest.rs b/crates/cli/src/commands/regtest.rs index 5cb705c..fb7f35c 100644 --- a/crates/cli/src/commands/regtest.rs +++ b/crates/cli/src/commands/regtest.rs @@ -9,7 +9,14 @@ use crate::commands::error::CommandError; pub struct Regtest {} impl Regtest { - pub fn run(config: RegtestConfig) -> Result<(), CommandError> { + /// Starts the regtest environment and blocks until terminated via Ctrl-C. + /// + /// # Errors + /// Returns a `CommandError` if initializing the environment from the config fails, or if shutting down the client fails. + /// + /// # Panics + /// Panics if setting the Ctrl-C handler fails, or if required RPC authentication credentials cannot be unwrapped. + pub fn run(config: &RegtestConfig) -> Result<(), CommandError> { let (mut client, signer) = RegtestRunner::from_config(config)?; let running = Arc::new(AtomicBool::new(true)); diff --git a/crates/cli/src/commands/test.rs b/crates/cli/src/commands/test.rs index 2eeaa53..0e4ca98 100644 --- a/crates/cli/src/commands/test.rs +++ b/crates/cli/src/commands/test.rs @@ -1,25 +1,38 @@ use std::path::PathBuf; use std::process::Stdio; +use smplx_test::config::Verbosity; use smplx_test::{SMPLX_TEST_MARKER, TestConfig}; -use super::core::TestFlags; +use super::core::{TestArguments, TestFlags}; use super::error::CommandError; pub struct Test {} impl Test { - pub fn run(config: TestConfig, filter: String, flags: &TestFlags) -> Result<(), CommandError> { + /// Runs tests based on the given configuration, filter, and flags. + /// + /// # Errors + /// Returns a `CommandError` if building the cache filename fails, writing the config to file fails, or running the system process fails. + /// + /// # Panics + /// Panics if the output of the cargo test command is not valid UTF-8. + pub fn run(mut config: TestConfig, args: &TestArguments, flags: &TestFlags) -> Result<(), CommandError> { let cache_path = Self::get_test_config_cache_name()?; + + if flags.verbose { + config.verbosity = Some(Verbosity(4)); + } + config.to_file(&cache_path)?; - let mut cargo_test_command = Self::build_cargo_test_command(&cache_path, filter, flags); + let mut cargo_test_command = Self::build_cargo_test_command(&cache_path, args, flags); let output = cargo_test_command.output()?; match output.status.code() { Some(code) => { - println!("Exit Status: {}", code); + println!("Exit Status: {code}"); if code == 0 { println!("{}", String::from_utf8(output.stdout).unwrap()); @@ -33,10 +46,16 @@ impl Test { Ok(()) } - fn build_cargo_test_command(cache_path: &PathBuf, filter: String, flags: &TestFlags) -> std::process::Command { - let mut cargo_test_command = std::process::Command::new("sh"); + fn build_cargo_test_command( + cache_path: &PathBuf, + args: &TestArguments, + flags: &TestFlags, + ) -> std::process::Command { + let mut cargo_test_command = std::process::Command::new("cargo"); + cargo_test_command.arg("test"); - cargo_test_command.args(["-c".to_string(), Self::build_test_command(filter, flags)]); + cargo_test_command.args(Self::build_cargo_test_args(args, flags)); + cargo_test_command.args(Self::build_test_bin_args(args, flags)); cargo_test_command .env(smplx_test::TEST_ENV_NAME, cache_path) @@ -47,37 +66,55 @@ impl Test { cargo_test_command } - fn build_test_command(filter: String, flags: &TestFlags) -> String { - let mut command_as_arg = String::new(); + fn build_cargo_test_args(args: &TestArguments, flags: &TestFlags) -> Vec { + let mut cargo_test_args = Vec::new(); - command_as_arg.push_str(&format!("cargo test {filter}_{SMPLX_TEST_MARKER}")); + if let Some(target) = &args.target { + cargo_test_args.push("--test".into()); + cargo_test_args.push(target.clone()); + } - let flag_args = Self::build_test_flags(flags); + if flags.no_fail_fast { + cargo_test_args.push("--no-fail-fast".into()); + } + + cargo_test_args + } - if !flag_args.is_empty() { - command_as_arg.push_str(" --"); - command_as_arg.push_str(&flag_args); + fn build_test_bin_args(args: &TestArguments, flags: &TestFlags) -> Vec { + let mut test_bin_args = Vec::new(); + + test_bin_args.push("--".into()); + + // TODO: custom filters may run non-simplex tests due to cargo limitations. Figure out how to fix this + if args.filters.is_empty() { + test_bin_args.push(SMPLX_TEST_MARKER.to_string()); + } else { + test_bin_args.extend(args.filters.iter().cloned()); } - command_as_arg + test_bin_args.extend(Self::build_test_bin_flags(flags)); + + test_bin_args } - fn build_test_flags(flags: &TestFlags) -> String { - let mut opt_params = String::new(); + fn build_test_bin_flags(flags: &TestFlags) -> Vec { + let mut test_bin_args = Vec::new(); if flags.nocapture { - opt_params.push_str(" --nocapture"); + test_bin_args.push("--nocapture".into()); } - if flags.show_output { - opt_params.push_str(" --show-output"); + test_bin_args.push("--show-output".into()); } - if flags.ignored { - opt_params.push_str(" --ignored"); + test_bin_args.push("--ignored".into()); + } + if flags.quiet { + test_bin_args.push("--quiet".into()); } - opt_params + test_bin_args } fn get_test_config_cache_name() -> Result { diff --git a/crates/cli/src/config/core.rs b/crates/cli/src/config/core.rs index 14caf75..616f4a5 100644 --- a/crates/cli/src/config/core.rs +++ b/crates/cli/src/config/core.rs @@ -3,7 +3,8 @@ use std::path::{Path, PathBuf}; use smplx_build::BuildConfig; use smplx_regtest::RegtestConfig; -use smplx_test::TestConfig; +use smplx_sdk::program::TrackerLogLevel; +use smplx_test::{TestConfig, config::Verbosity}; use super::error::ConfigError; @@ -19,14 +20,33 @@ pub struct Config { } impl Config { + /// Retrieves the default path for the configuration by using `std::env::current_dir()` + /// + /// # Errors + /// This function can return a `ConfigError` in the following cases: + /// - If the current working directory cannot be determined. + /// - If the `get_path` function encounters an error, processing the retrieved path. pub fn get_default_path() -> Result { Self::get_path(std::env::current_dir()?) } + /// Constructs a complete configuration file path by joining the provided path with the + /// predefined configuration file name `CONFIG_FILENAME`. + /// + /// # Errors + /// This function will return an error if the provided `path` cannot be resolved for any reason + /// that would result in a failure when interacting with path-related operations. pub fn get_path(path: impl AsRef) -> Result { Ok(path.as_ref().join(CONFIG_FILENAME)) } + /// Loads a configuration file from a given path and deserializes its contents into a `Config` object. + /// + /// # Errors + /// - `ConfigError::PathIsNotFile`: If the given path is not a file. + /// - `ConfigError::PathNotExists`: If the given path does not exist. + /// - `ConfigError::UnableToDeserialize`: If the file's contents cannot be parsed as valid TOML. + /// - Any other I/O errors that may occur when reading the file. pub fn load(path_buf: impl AsRef) -> Result { let path = path_buf.as_ref().to_path_buf(); @@ -47,6 +67,11 @@ impl Config { } fn validate(config: &Config) -> Result<(), ConfigError> { + match config.test.verbosity { + Some(verbosity) => Self::validate_verbosity(verbosity), + None => Ok(()), + }?; + match config.test.esplora.clone() { Some(esplora_config) => { Self::validate_network(&esplora_config.network)?; @@ -61,6 +86,13 @@ impl Config { } } + fn validate_verbosity(verbosity: Verbosity) -> Result<(), ConfigError> { + match TryInto::::try_into(verbosity) { + Ok(_) => Ok(()), + Err(val) => Err(ConfigError::BadVersbosityMode(val.0)), + } + } + fn validate_network(network: &String) -> Result<(), ConfigError> { if network != "Liquid" && network != "LiquidTestnet" && network != "ElementsRegtest" { return Err(ConfigError::BadNetworkName(network.clone())); diff --git a/crates/cli/src/config/error.rs b/crates/cli/src/config/error.rs index d76ca7b..7c362b1 100644 --- a/crates/cli/src/config/error.rs +++ b/crates/cli/src/config/error.rs @@ -25,4 +25,7 @@ pub enum ConfigError { #[error("Path doesn't exist: '{0}'")] PathNotExists(PathBuf), + + #[error("Verbosity level should be either 1, 2, 3, 4, got: {0}")] + BadVersbosityMode(u64), } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index ffa77c5..230fd4b 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,3 +1,5 @@ +#![warn(clippy::all, clippy::pedantic)] + pub mod cli; pub mod commands; pub mod config; diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index ae16913..7fcb770 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "smplx-macros" -version = "0.0.4" +version = "0.0.5" description = "Simplex macros re-export package" license.workspace = true edition.workspace = true diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index c80bed3..1513604 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1,3 +1,5 @@ +#![warn(clippy::all, clippy::pedantic)] + use proc_macro::TokenStream; #[proc_macro] diff --git a/crates/regtest/Cargo.toml b/crates/regtest/Cargo.toml index f924c6f..6048dc4 100644 --- a/crates/regtest/Cargo.toml +++ b/crates/regtest/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "smplx-regtest" -version = "0.0.4" +version = "0.0.5" description = "Simplex regtest command internal implementation" license.workspace = true edition.workspace = true diff --git a/crates/regtest/src/args.rs b/crates/regtest/src/args.rs index f3db53f..04a8b81 100644 --- a/crates/regtest/src/args.rs +++ b/crates/regtest/src/args.rs @@ -5,7 +5,7 @@ use super::RegtestConfig; type HmacSha256 = Hmac; -pub fn get_elementsd_bin_args(config: &RegtestConfig) -> Vec { +pub(super) fn get_elementsd_bin_args(config: &RegtestConfig) -> Vec { let mut args = vec![ "-fallbackfee=0.0001".to_string(), "-dustrelayfee=0.00000001".to_string(), @@ -38,7 +38,7 @@ pub fn get_elementsd_bin_args(config: &RegtestConfig) -> Vec { args } -pub fn get_electrs_bin_args(config: &RegtestConfig) -> Vec { +pub(super) fn get_electrs_bin_args(config: &RegtestConfig) -> Vec { let mut args = vec![]; if let Some(port) = config.esplora_port { diff --git a/crates/regtest/src/client.rs b/crates/regtest/src/client.rs index a093311..dced9cb 100644 --- a/crates/regtest/src/client.rs +++ b/crates/regtest/src/client.rs @@ -10,6 +10,7 @@ use super::RegtestConfig; use super::error::RegtestError; use crate::args::{get_electrs_bin_args, get_elementsd_bin_args}; +/// Client for managing background `elementsd` and `electrs` nodes for Regtest environments. pub struct RegtestClient { pub electrs: ElectrsD, pub elements: BitcoinD, @@ -17,6 +18,11 @@ pub struct RegtestClient { } impl RegtestClient { + /// Creates a new `RegtestClient` by starting local instances of `elementsd` and `electrs`. + /// + /// # Panics + /// Panics if binding to a local ZMQ port fails, or if starting the node executables fails. + #[must_use] pub fn new(config: &RegtestConfig) -> Self { let (electrs_path, elementsd_path) = Self::default_bin_paths(); let zmq_addr = Self::get_zmq_addr(); @@ -30,6 +36,8 @@ impl RegtestClient { } } + /// Returns the default binary paths for `electrs` and `elementsd`. + #[must_use] pub fn default_bin_paths() -> (PathBuf, PathBuf) { const ELECTRS_BIN_PATH: &str = "electrs"; const ELEMENTSD_BIN_PATH: &str = "elementsd"; @@ -40,6 +48,7 @@ impl RegtestClient { ) } + /// Returns the configured or default RPC URL for the `elementsd` node. pub fn rpc_url(&self) -> String { if let Some(port) = self.config.rpc_port { return format!("http://127.0.0.1:{port}"); @@ -48,17 +57,25 @@ impl RegtestClient { self.elements.rpc_url() } + /// Returns the configured or default Esplora REST API URL for the `electrs` instance. + /// + /// # Panics + /// Panics if an automatically assigned URL cannot be properly parsed. pub fn esplora_url(&self) -> String { if let Some(port) = self.config.esplora_port { return format!("http://127.0.0.1:{port}"); } let url = self.electrs.esplora_url.clone().unwrap(); - let port = url.split_once(":").unwrap().1; + let port = url.split_once(':').unwrap().1; - format!("http://127.0.0.1:{}", port) + format!("http://127.0.0.1:{port}") } + /// Returns the RPC authentication credentials. + /// + /// # Panics + /// Panics if no explicit credentials are provided and reading the generated cookie file fails. pub fn auth(&self) -> Auth { if let (Some(user), Some(password)) = (&self.config.rpc_user, &self.config.rpc_password) { return Auth::UserPass(user.clone(), password.clone()); @@ -69,6 +86,12 @@ impl RegtestClient { Auth::UserPass(cookie.user, cookie.password) } + /// Terminates the running processes associated with this client. + /// + /// Note that killing the `electrs` process will typically stop the associated `elementsd` node automatically. + /// + /// # Errors + /// Returns a `RegtestError` if terminating the `electrs` daemon fails. pub fn kill(&mut self) -> Result<(), RegtestError> { // electrs stops elements automatically self.electrs.kill().map_err(|_| RegtestError::ElectrsTermination())?; @@ -94,7 +117,7 @@ impl RegtestClient { bin_args.push(format!("-zmqpubhashblock=tcp://{zmq_addr}")); bin_args.push(format!("-zmqpubsequence=tcp://{zmq_addr}")); - conf.args = bin_args.iter().map(|x| x.as_ref()).collect::>(); + conf.args = bin_args.iter().map(std::convert::AsRef::as_ref).collect::>(); conf.network = "liquidregtest"; conf.p2p = bitcoind::P2P::Yes; @@ -112,7 +135,7 @@ impl RegtestClient { bin_args.push(format!("--zmq-addr={zmq_addr}")); - conf.args = bin_args.iter().map(|x| x.as_ref()).collect::>(); + conf.args = bin_args.iter().map(std::convert::AsRef::as_ref).collect::>(); conf.http_enabled = config.esplora_port.is_none(); conf.network = "liquidregtest"; diff --git a/crates/regtest/src/config.rs b/crates/regtest/src/config.rs index 2fe2aec..02ae844 100644 --- a/crates/regtest/src/config.rs +++ b/crates/regtest/src/config.rs @@ -21,6 +21,10 @@ pub struct RegtestConfig { } impl RegtestConfig { + /// Loads a `RegtestConfig` from a specified TOML file. + /// + /// # Errors + /// Returns a `RegtestError` if the file cannot be opened, read, or if the contents are not valid TOML. pub fn from_file(path: impl AsRef) -> Result { let mut content = String::new(); let mut file = OpenOptions::new().read(true).open(path)?; diff --git a/crates/regtest/src/lib.rs b/crates/regtest/src/lib.rs index 5275698..3679031 100644 --- a/crates/regtest/src/lib.rs +++ b/crates/regtest/src/lib.rs @@ -1,3 +1,5 @@ +#![warn(clippy::all, clippy::pedantic)] + mod args; pub mod client; pub mod config; diff --git a/crates/regtest/src/regtest.rs b/crates/regtest/src/regtest.rs index a777ab3..b4c2757 100644 --- a/crates/regtest/src/regtest.rs +++ b/crates/regtest/src/regtest.rs @@ -13,8 +13,18 @@ use super::error::RegtestError; pub struct Regtest {} impl Regtest { - pub fn from_config(config: RegtestConfig) -> Result<(RegtestClient, Signer), RegtestError> { - let client = RegtestClient::new(&config); + /// Initializes a Regtest environment and returns a configured client and funded signer. + /// + /// This method establishes a connection to the backend, sets up the provider, + /// and prepares the `Signer` by generating initial blocks and sweeping funds based on the configuration. + /// + /// # Errors + /// Returns a `RegtestError` if node initialization, block generation, or RPC calls fail. + /// + /// # Panics + /// Panics if the background indexer (`electrs`) fails to index the unspent outputs within the timeout window (10 seconds). + pub fn from_config(config: &RegtestConfig) -> Result<(RegtestClient, Signer), RegtestError> { + let client = RegtestClient::new(config); let provider = Box::new(SimplexProvider::new( client.esplora_url(), @@ -51,9 +61,7 @@ impl Regtest { attempts += 1; - if attempts > 100 { - panic!("Electrs failed to index the sweep after 10 seconds"); - } + assert!(attempts <= 100, "Electrs failed to index the sweep after 10 seconds"); std::thread::sleep(Duration::from_millis(100)); } diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 9eea17b..323559e 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "smplx-sdk" -version = "0.0.4" +version = "0.0.5" description = "Simplex sdk to simplify the development with simplicity" license.workspace = true edition.workspace = true diff --git a/crates/sdk/README.md b/crates/sdk/README.md new file mode 100644 index 0000000..3fd2b11 --- /dev/null +++ b/crates/sdk/README.md @@ -0,0 +1,22 @@ +# smplx-sdk + +The `smplx-sdk` crate is a standalone set of modules of a larger [Smplx](https://github.com/BlockstreamResearch/smplx) framework that can be used separately to interact with Simplicity smart contracts. + +It also streamlines building, signing, and broadcasting transactions on Liquid. + +## Features + +- `signer` - Securely parse BIP39 mnemonics, manage keys, sign transactions, and work with confidential addresses. +- `provider` - Connect to existing Elements nodes via RPC or Esplora APIs to query UTXOs and broadcast transactions. +- `transaction` - High-level builder abstractions over `FinalTransaction`, `TxReceipt`, `UTXO`, `PartialInput`, and `PartialOutput`. +- `program` - Load and interact with Simplicity (`.simf`) smart contracts. + +The `smplx-sdk` can be used as a standalone SDK, however, check out [Smplx](https://github.com/BlockstreamResearch/smplx) for a complete Simplicity development experience. + +## Quick Start + +Read [simplex/README.md](https://github.com/BlockstreamResearch/smplx/blob/master/README.md). + +## Disclaimer + +Secure DeFi. On Bitcoin. diff --git a/crates/sdk/src/constants.rs b/crates/sdk/src/constants.rs index b9c3b45..add3f2d 100644 --- a/crates/sdk/src/constants.rs +++ b/crates/sdk/src/constants.rs @@ -1,16 +1,19 @@ +/// General public blinder key to use pub const PUBLIC_SECRET_BLINDER_KEY: [u8; 32] = [1; 32]; +/// Dummy signature, which is used for fee estimation pub const DUMMY_SIGNATURE: [u8; 64] = [1; 64]; +/// Minimal acceptable fee for nodes to send a transaction pub const MIN_FEE: u64 = 10; -/// Policy asset id (hex, BE) for Liquid mainnet. +/// Policy `AssetId` (hex, BE) for Liquid mainnet. pub const LIQUID_POLICY_ASSET_STR: &str = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"; -/// Policy asset id (hex, BE) for Liquid testnet. +/// Policy `AssetId` (hex, BE) for Liquid testnet. pub const LIQUID_TESTNET_POLICY_ASSET_STR: &str = "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; -/// Policy asset id (hex, BE) for Elements regtest. +/// Policy `AssetId` (hex, BE) for Elements regtest. pub const LIQUID_DEFAULT_REGTEST_ASSET_STR: &str = "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"; -/// Example test asset id (hex, BE) on Liquid testnet. +/// Example test `AssetId` (hex, BE) on Liquid testnet. pub const LIQUID_TESTNET_TEST_ASSET_ID_STR: &str = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; diff --git a/crates/sdk/src/global.rs b/crates/sdk/src/global.rs new file mode 100644 index 0000000..b4a6e2e --- /dev/null +++ b/crates/sdk/src/global.rs @@ -0,0 +1,38 @@ +use std::sync::OnceLock; + +use crate::program::TrackerLogLevel; + +/// A structure to represent the global configuration settings for the application. +#[derive(Clone, Copy, Debug)] +pub struct GlobalConfig { + log_level: TrackerLogLevel, +} + +impl Default for GlobalConfig { + fn default() -> Self { + Self { + log_level: TrackerLogLevel::Debug, + } + } +} + +static GLOBAL_CONFIG: OnceLock = OnceLock::new(); + +/// Sets the global configuration for the SDK. +/// +/// This function allows setting a global configuration which includes +/// the logging level for `simplicity` contracts execution. +/// It must be called exactly once during the application's lifetime. +/// +/// # Errors +/// Returns an error containing the newly created `GlobalConfig` if the global configuration has already been initialized. +pub fn set_global_config(log_level: TrackerLogLevel) -> Result<(), GlobalConfig> { + GLOBAL_CONFIG.set(GlobalConfig { log_level }) +} + +/// Returns the default log level if `GLOBAL_CONFIG` is not initialized +pub fn get_log_level() -> TrackerLogLevel { + GLOBAL_CONFIG + .get() + .map_or(GlobalConfig::default().log_level, |config| config.log_level) +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 0849165..c6e8dfc 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1,6 +1,20 @@ +#![doc(html_logo_url = "https://raw.githubusercontent.com/BlockstreamResearch/smplx/master/docs/simplex_logo.png")] +#![doc(html_root_url = "https://docs.rs/smplx-sdk/latest/simplex/")] +#![cfg_attr(doc, doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR" ), "/", "README.md")))] +#![cfg_attr(not(doc), doc = "Simplex SDK")] +#![warn(clippy::all, clippy::pedantic, missing_docs)] + +/// Common constants and identifiers used across the Simplex SDK. pub mod constants; +/// Global state, configuration, and shared context used throughout the SDK. +pub mod global; +/// Core abstractions, definitions, and errors for compiling and evaluating Simplicity programs. pub mod program; +/// Interfaces and implementations for interacting with blockchain nodes and APIs. pub mod provider; +/// Traits and mechanisms for signing transactions and satisfying witness requirements. pub mod signer; +/// Constructs and builders for assembling, tracking, and managing Elements transactions. pub mod transaction; +/// General utility functions, conversions, and helper tools. pub mod utils; diff --git a/crates/sdk/src/program/arguments.rs b/crates/sdk/src/program/arguments.rs index 60799cd..61d734d 100644 --- a/crates/sdk/src/program/arguments.rs +++ b/crates/sdk/src/program/arguments.rs @@ -1,7 +1,10 @@ use dyn_clone::DynClone; use simplicityhl::Arguments; +/// An interface for structs capable of generating static argument mapping for Simplicity programs. +/// See the `include_simc!()` macro, which generates automatic `ArgumentsTrait` implementation. pub trait ArgumentsTrait: DynClone { + /// Compiles and returns the bound `Arguments` dict required to instantiate a program. fn build_arguments(&self) -> Arguments; } diff --git a/crates/sdk/src/program/core.rs b/crates/sdk/src/program/core.rs index 87a647e..5dd47f9 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -10,20 +10,37 @@ use simplicityhl::simplicity::bitcoin::{XOnlyPublicKey, secp256k1}; use simplicityhl::simplicity::jet::Elements; use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; use simplicityhl::simplicity::{BitMachine, RedeemNode, Value, leaf_version}; -use simplicityhl::tracker::{DefaultTracker, TrackerLogLevel}; +use simplicityhl::tracker::DefaultTracker; use simplicityhl::{Parameters, WitnessTypes, WitnessValues}; +use crate::global::get_log_level; + use super::arguments::ArgumentsTrait; use super::error::ProgramError; use crate::provider::SimplicityNetwork; use crate::utils::{hash_script, tap_data_hash, tr_unspendable_key}; +/// Executes `simplicity` programs at runtime. +/// +/// This trait defines a core behavior related to testing and execution. pub trait ProgramTrait: DynClone { + /// Retrieves the types of arguments required by a `simplicity` program. + /// + /// # Errors + /// Returns a `ProgramError` if parsing or generating ABI metadata fails. fn get_argument_types(&self) -> Result; + /// Retrieves the witness types required by a `simplicity` program. + /// + /// # Errors + /// Returns a `ProgramError` if parsing or generating ABI metadata fails. fn get_witness_types(&self) -> Result; + /// Constructs the Elements environment for a specified input index, PST, and network for further program execution. + /// + /// # Errors + /// Returns a `ProgramError` if the input index is out of bounds or if the script pubkey of the UTXO mismatches the expected program script. fn get_env( &self, pst: &PartiallySignedTransaction, @@ -31,6 +48,14 @@ pub trait ProgramTrait: DynClone { network: &SimplicityNetwork, ) -> Result>, ProgramError>; + /// Executes a Simplicity program for the given input index of a partially signed transaction. + /// + /// This function evaluates a Simplicity script associated with a specific transaction input + /// in a given network, producing the result of the computation along with the redeem node + /// used during execution. + /// + /// # Errors + /// Returns a `ProgramError` if loading the program, satisfying the witness, retrieving the environment, or executing the `BitMachine` fails. fn execute( &self, pst: &PartiallySignedTransaction, @@ -39,6 +64,10 @@ pub trait ProgramTrait: DynClone { network: &SimplicityNetwork, ) -> Result<(Arc>, Value), ProgramError>; + /// Finalizes and returns `pruned_witness` as output after executing the program on certain parameters. + /// + /// # Errors + /// Returns a `ProgramError` if program execution or constructing the control block fails. fn finalize( &self, pst: &PartiallySignedTransaction, @@ -48,6 +77,9 @@ pub trait ProgramTrait: DynClone { ) -> Result>, ProgramError>; } +/// Represents a program structure containing its source, a public key, arguments, and associated storage. +/// +/// Abstraction giving the power to execute Simplicity contracts without specifying any additional parameters. #[derive(Clone)] pub struct Program { source: &'static str, @@ -124,8 +156,7 @@ impl ProgramTrait for Program { .satisfy(witness.clone()) .map_err(ProgramError::WitnessSatisfaction)?; - // TODO: global config for TrackerLogLevel - let mut tracker = DefaultTracker::new(satisfied.debug_symbols()).with_log_level(TrackerLogLevel::Debug); + let mut tracker = DefaultTracker::new(satisfied.debug_symbols()).with_log_level(get_log_level()); let env = self.get_env(pst, input_index, network)?; @@ -159,6 +190,8 @@ impl ProgramTrait for Program { } impl Program { + /// Creates a new instance of the struct with the provided source string and arguments. + #[must_use] pub fn new(source: &'static str, arguments: Box) -> Self { Self { source, @@ -168,36 +201,59 @@ impl Program { } } - pub fn with_pub_key(mut self, pub_key: XOnlyPublicKey) -> Self { + /// Sets the `pub_key` field of the struct to the provided `XOnlyPublicKey` value and returns the updated builder instance. + /// This is used to set the taproot public key for the program. + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { self.pub_key = pub_key; self } + /// Sets storage capacity for further usage. + #[must_use] pub fn with_storage_capacity(mut self, capacity: usize) -> Self { self.storage = vec![[0u8; 32]; capacity]; self } + /// Sets a 32-byte value at the specified index in the storage. + /// + /// # Panics + /// Panics if the `index` is out of bounds for the initiasized storage. pub fn set_storage_at(&mut self, index: usize, new_value: [u8; 32]) { let slot = self.storage.get_mut(index).expect("Index out of bounds"); *slot = new_value; } + /// Returns the number of storage chunks for a program. + #[must_use] pub fn get_storage_len(&self) -> usize { self.storage.len() } + /// Returns storage as a whole array of 32-byte chunks. + #[must_use] pub fn get_storage(&self) -> &[[u8; 32]] { &self.storage } + /// Returns storage value at a certain index. + /// + /// # Panics + /// Panics if the `index` is out of bounds for the initiated storage. + #[must_use] pub fn get_storage_at(&self, index: usize) -> [u8; 32] { self.storage[index] } + /// Returns a taproot address for a defined `SimplicityNetwork`. + /// + /// # Panics + /// Panics if generating the taproot spending information fails. + #[must_use] pub fn get_tr_address(&self, network: &SimplicityNetwork) -> Address { let spend_info = self.taproot_spending_info().unwrap(); @@ -210,14 +266,22 @@ impl Program { ) } + /// Retrieves the `ScriptPubKey` associated with the Simplicity address for the specified network. + #[must_use] pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { self.get_tr_address(network).script_pubkey() } + /// Retrieves the 32-byte `ScriptPubKey` hash associated with the Simplicity address for the specified network. + #[must_use] pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { hash_script(&self.get_script_pubkey(network)) } + /// Retrieves program ABI metadata for argument types. + /// + /// # Errors + /// Returns a `ProgramError` if compilation fails or generating ABI metadata fails. pub fn get_argument_types(&self) -> Result { let compiled = self.load()?; let abi_meta = compiled.generate_abi_meta().map_err(ProgramError::ProgramGenAbiMeta)?; @@ -225,6 +289,10 @@ impl Program { Ok(abi_meta.param_types) } + /// Retrieves the witness types from the compiled program's ABI metadata. + /// + /// # Errors + /// Returns a `ProgramError` if compilation fails or generating ABI metadata fails. pub fn get_witness_types(&self) -> Result { let compiled = self.load()?; let abi_meta = compiled.generate_abi_meta().map_err(ProgramError::ProgramGenAbiMeta)?; @@ -302,7 +370,7 @@ mod tests { use super::*; // simplicityhl/examples/cat.simf - const DUMMY_PROGRAM: &str = r#" + const DUMMY_PROGRAM: &str = r" fn main() { let ab: u16 = <(u8, u8)>::into((0x10, 0x01)); let c: u16 = 0x1001; @@ -311,7 +379,7 @@ mod tests { let c: u8 = 0b10111101; assert!(jet::eq_8(ab, c)); } - "#; + "; #[derive(Clone)] struct EmptyArguments; diff --git a/crates/sdk/src/program/error.rs b/crates/sdk/src/program/error.rs index f36d750..344fa2e 100644 --- a/crates/sdk/src/program/error.rs +++ b/crates/sdk/src/program/error.rs @@ -1,32 +1,53 @@ +/// Errors that can occur when compiling, preparing, and executing Simplicity programs. #[derive(Debug, thiserror::Error)] pub enum ProgramError { + /// Error thrown when compiling the raw Simplicity program source fails. #[error("Failed to compile Simplicity program: {0}")] Compilation(String), + /// Error indicating failure while matching or satisfying witness values to the program requirements. #[error("Failed to satisfy witness: {0}")] WitnessSatisfaction(String), + /// Error indicating pruning the node tree during execution failed safely. #[error("Failed to prune program: {0}")] Pruning(#[from] simplicityhl::simplicity::bit_machine::ExecutionError), + /// Error thrown when the bit machine cannot be initialized due to complexity or limit restrictions. #[error("Failed to construct a Bit Machine with enough space: {0}")] BitMachineCreation(#[from] simplicityhl::simplicity::bit_machine::LimitError), + /// Error thrown during bit machine execution due to underlying logical or environment validation errors. #[error("Failed to execute program on the Bit Machine: {0}")] Execution(simplicityhl::simplicity::bit_machine::ExecutionError), + /// Error indicating an input index points past the bounds of available inputs/UTXOs. #[error("UTXO index {input_index} out of bounds (have {utxo_count} UTXOs)")] - UtxoIndexOutOfBounds { input_index: usize, utxo_count: usize }, - + UtxoIndexOutOfBounds { + /// The requested input index to spend. + input_index: usize, + /// The actual total number of available UTXOs. + utxo_count: usize, + }, + + /// Error indicating the script pubkey present on the targeted UTXO differs from the expectation. #[error("Script pubkey mismatch: expected hash {expected_hash}, got {actual_hash}")] - ScriptPubkeyMismatch { expected_hash: String, actual_hash: String }, - + ScriptPubkeyMismatch { + /// The expected CMR (Commitment Merkle Root) hash. + expected_hash: String, + /// The actual CMR hash present in the script pubkey. + actual_hash: String, + }, + + /// Error thrown when an underlying Elements transaction fails to extract from the PSET wrapper. #[error("Failed to extract tx from pst: {0}")] TxExtraction(#[from] simplicityhl::elements::pset::Error), + /// Error indicating an array size overflow if the target index exceeds limits. #[error("Input index exceeds u32 maximum: {0}")] InputIndexOverflow(#[from] std::num::TryFromIntError), + /// Error thrown if the compiled program fails to export or generate valid ABI metadata. #[error("Failed to obtain program witness types: {0}")] ProgramGenAbiMeta(String), } diff --git a/crates/sdk/src/program/mod.rs b/crates/sdk/src/program/mod.rs index 6982d16..1285d55 100644 --- a/crates/sdk/src/program/mod.rs +++ b/crates/sdk/src/program/mod.rs @@ -1,9 +1,14 @@ +/// Definitions and traits for handling program arguments in Simplicity programs. pub mod arguments; +/// Core definitions, features, and abstractions for working with Simplicity programs. pub mod core; +/// Error types and definitions for program compilation, manipulation, and execution failures. pub mod error; +/// Definitions and traits for resolving and satisfying execution witnesses for Simplicity programs. pub mod witness; pub use arguments::ArgumentsTrait; pub use core::{Program, ProgramTrait}; pub use error::ProgramError; +pub use simplicityhl::tracker::TrackerLogLevel; pub use witness::WitnessTrait; diff --git a/crates/sdk/src/program/witness.rs b/crates/sdk/src/program/witness.rs index 374f8f6..7bf4777 100644 --- a/crates/sdk/src/program/witness.rs +++ b/crates/sdk/src/program/witness.rs @@ -1,7 +1,10 @@ use dyn_clone::DynClone; use simplicityhl::WitnessValues; +/// An interface for structs capable of generating Simplicity program witness mappings. +/// See the ` include_simc!()` macro, which generates an automatic `WitnessTrait` implementation. pub trait WitnessTrait: DynClone { + /// Compiles and generates the fully populated `WitnessValues` map for execution. fn build_witness(&self) -> WitnessValues; } diff --git a/crates/sdk/src/provider/core.rs b/crates/sdk/src/provider/core.rs index a5c76da..e918b9e 100644 --- a/crates/sdk/src/provider/core.rs +++ b/crates/sdk/src/provider/core.rs @@ -5,39 +5,84 @@ use electrsd::bitcoind::bitcoincore_rpc::Auth; use simplicityhl::elements::{Address, Script, Transaction, Txid}; use crate::provider::SimplicityNetwork; -use crate::transaction::UTXO; +use crate::transaction::{TxReceipt, UTXO}; use super::error::ProviderError; +/// The fallback default fee rate (in sats/kvb) to use when dynamic estimates fail. pub const DEFAULT_FEE_RATE: f32 = 100.0; +/// The standard timeout duration (in seconds) applied to Esplora REST API requests. pub const DEFAULT_ESPLORA_TIMEOUT_SECS: u64 = 10; +/// Contains foundational configuration elements required for initializing a generic blockchain provider. #[derive(Debug, Clone)] pub struct ProviderInfo { + /// URL of the target Esplora REST service. pub esplora_url: String, + /// URL of the target direct `elementsd` or `bitcoind` RPC interface. pub elements_url: Option, + /// Authentication settings (e.g. cookie or username/password) for the RPC backend. pub auth: Option, } +/// Baseline traits detailing required interaction methods between the SDK client and the underlying blockchain node or API. pub trait ProviderTrait { + /// Retrieves the network configured for this provider. fn get_network(&self) -> &SimplicityNetwork; - fn broadcast_transaction(&self, tx: &Transaction) -> Result; + /// Attempts to broadcast a fully compiled transaction to the configured backend. + /// + /// # Errors + /// Returns a `ProviderError` if network transmission fails, or if the backend explicitly rejects the transaction. + fn broadcast_transaction(&self, tx: &Transaction) -> Result, ProviderError>; + /// Blocks and repeatedly polls the network until the specified transaction receives its first confirmation. + /// + /// # Errors + /// Returns a `ProviderError` if the network fails or the designated timeout elapses without confirmation. fn wait(&self, txid: &Txid) -> Result<(), ProviderError>; + /// Retrieves the current block height of the network tip. + /// + /// # Errors + /// Returns a `ProviderError` if the backend request fails or the returned height cannot be parsed. fn fetch_tip_height(&self) -> Result; + /// Retrieves the block timestamp representing network consensus clock tip. + /// + /// # Errors + /// Returns a `ProviderError` if the hash query, subsequent block query, or block parsing fails. fn fetch_tip_timestamp(&self) -> Result; + /// Retrieves the serialized transaction payload given its hex transaction ID. + /// + /// # Errors + /// Returns a `ProviderError` if the node fails to locate the transaction or serialization fails. fn fetch_transaction(&self, txid: &Txid) -> Result; + /// Fetches all active unspent transaction outputs correlated to a particular public `Address`. + /// + /// # Errors + /// Returns a `ProviderError` if the backend request fails or if UTXO parsing fails. fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError>; + /// Fetches all active unspent transaction outputs correlated to a given custom `Script` mapping. + /// + /// # Errors + /// Returns a `ProviderError` if the backend request fails or if UTXO parsing fails. fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError>; + /// Fetches network fee estimation models based on varying target confirmation block delays. + /// + /// # Errors + /// Returns a `ProviderError` if the REST request fails or the resulting mappings fail to parse cleanly. fn fetch_fee_estimates(&self) -> Result, ProviderError>; + /// Attempts to extract the specific fee rate (in sats/kvb) necessary for the transaction to be confirmed within `target_blocks`. + /// + /// # Errors + /// Passes along `ProviderError` if `fetch_fee_estimates` fails. + #[allow(clippy::cast_possible_truncation)] fn fetch_fee_rate(&self, target_blocks: u32) -> Result { let estimates = self.fetch_fee_estimates()?; let target_str = target_blocks.to_string(); diff --git a/crates/sdk/src/provider/error.rs b/crates/sdk/src/provider/error.rs index 40a994b..ca11a61 100644 --- a/crates/sdk/src/provider/error.rs +++ b/crates/sdk/src/provider/error.rs @@ -1,22 +1,36 @@ use crate::provider::rpc::error::RpcError; +/// Defines standard errors possible when using a blockchain interaction provider. #[derive(Debug, thiserror::Error)] pub enum ProviderError { + /// Wrapper around an RPC-level error representing transport or network-level connectivity failures to the inner node. #[error(transparent)] Rpc(#[from] RpcError), + /// Error indicating that a standard HTTP request to a provider (such as an Esplora REST instance) encountered a failure. #[error("HTTP request failed: {0}")] Request(String), + /// Error indicating the configured timeout to wait for transaction confirmation elapsed without confirmation. #[error("Couldn't wait for the transaction to be confirmed")] Confirmation(), + /// Error indicating a provider returned a non-success response explicitly rejecting a broadcasted transaction payload. #[error("Broadcast failed with HTTP {status} for {url}: {message}")] - BroadcastRejected { status: u16, url: String, message: String }, + BroadcastRejected { + /// The HTTP status code indicating the failure. + status: u16, + /// The URL that the broadcast was sent to. + url: String, + /// The error message returned by the provider. + message: String, + }, + /// Error indicating that a provider's raw response body was unable to be correctly deserialized into native structs or types. #[error("Failed to deserialize response: {0}")] Deserialize(String), + /// Error indicating an incorrectly formatted transaction ID string was encountered. #[error("Invalid txid format: {0}")] InvalidTxid(String), } diff --git a/crates/sdk/src/provider/esplora.rs b/crates/sdk/src/provider/esplora.rs index 886c51e..c41f41c 100644 --- a/crates/sdk/src/provider/esplora.rs +++ b/crates/sdk/src/provider/esplora.rs @@ -11,14 +11,19 @@ use simplicityhl::elements::{Address, OutPoint, Script, Transaction, Txid}; use serde::Deserialize; use crate::provider::SimplicityNetwork; -use crate::transaction::UTXO; +use crate::transaction::{TxReceipt, UTXO}; use super::core::{DEFAULT_ESPLORA_TIMEOUT_SECS, ProviderTrait}; use super::error::ProviderError; +/// A provider implementation that interacts with the Esplora REST API backend. +#[derive(Debug)] pub struct EsploraProvider { + /// The base URL of the Esplora REST API. pub esplora_url: String, + /// The currently configured Simplicity network (e.g. Liquid, Testnet, Regtest). pub network: SimplicityNetwork, + /// Timeout duration used in underlying HTTP requests. pub timeout: Duration, } @@ -60,6 +65,8 @@ struct EsploraUtxo { } impl EsploraProvider { + /// Creates a new `EsploraProvider` connected to the provided endpoint targeting the specific network. + #[must_use] pub fn new(url: String, network: SimplicityNetwork) -> Self { Self { esplora_url: url, @@ -68,7 +75,7 @@ impl EsploraProvider { } } - fn esplora_utxo_to_outpoint(&self, utxo: &EsploraUtxo) -> Result { + fn esplora_utxo_to_outpoint(utxo: &EsploraUtxo) -> Result { let txid = Txid::from_str(&utxo.txid).map_err(|e| ProviderError::InvalidTxid(e.to_string()))?; Ok(OutPoint::new(txid, utxo.vout)) @@ -101,7 +108,8 @@ impl ProviderTrait for EsploraProvider { &self.network } - fn broadcast_transaction(&self, tx: &Transaction) -> Result { + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + fn broadcast_transaction(&self, tx: &Transaction) -> Result, ProviderError> { let tx_hex = encode::serialize_hex(tx); let url = format!("{}/tx", self.esplora_url); let timeout_secs = self.timeout.as_secs(); @@ -123,7 +131,9 @@ impl ProviderTrait for EsploraProvider { }); } - Txid::from_str(&body).map_err(|e| ProviderError::InvalidTxid(e.to_string())) + Txid::from_str(&body) + .map_err(|e| ProviderError::InvalidTxid(e.to_string())) + .map(|tx_id| TxReceipt::new(self, tx_id)) } fn wait(&self, txid: &Txid) -> Result<(), ProviderError> { @@ -269,7 +279,7 @@ impl ProviderTrait for EsploraProvider { let utxos: Vec = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?; let outpoints = utxos .iter() - .map(|utxo| self.esplora_utxo_to_outpoint(utxo)) + .map(Self::esplora_utxo_to_outpoint) .collect::, ProviderError>>()?; self.populate_txouts_from_outpoints(&outpoints) @@ -298,7 +308,7 @@ impl ProviderTrait for EsploraProvider { let utxos: Vec = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?; let outpoints = utxos .iter() - .map(|utxo| self.esplora_utxo_to_outpoint(utxo)) + .map(Self::esplora_utxo_to_outpoint) .collect::, ProviderError>>()?; self.populate_txouts_from_outpoints(&outpoints) diff --git a/crates/sdk/src/provider/mod.rs b/crates/sdk/src/provider/mod.rs index 1873ed7..a476aa9 100644 --- a/crates/sdk/src/provider/mod.rs +++ b/crates/sdk/src/provider/mod.rs @@ -1,8 +1,14 @@ +/// Core provider traits and information structs used to define general blockchain interaction interfaces. pub mod core; +/// Provider-specific error enumerations for handling transmission, retrieval, or interpretation issues. pub mod error; +/// Types and definitions for interacting specifically with an Esplora REST API provider backend. pub mod esplora; +/// Definitions distinguishing blockchain network states (e.g. mainnet, testnet, regtest) and related configurations. pub mod network; +/// Submodules and definitions handling direct JSON-RPC interfacing with backing Bitcoin/Elements core nodes. pub mod rpc; +/// Abstractions and composite providers intended for general usage in the Simplex SDK. pub mod simplex; pub use core::{ProviderInfo, ProviderTrait}; diff --git a/crates/sdk/src/provider/network.rs b/crates/sdk/src/provider/network.rs index b1326ef..1b9ffeb 100644 --- a/crates/sdk/src/provider/network.rs +++ b/crates/sdk/src/provider/network.rs @@ -5,6 +5,7 @@ use simplicityhl::simplicity::hashes::{Hash, sha256}; use crate::constants::{LIQUID_DEFAULT_REGTEST_ASSET_STR, LIQUID_POLICY_ASSET_STR, LIQUID_TESTNET_POLICY_ASSET_STR}; +/// The default Bitcoin `AssetId` used on Liquid testnet. pub static LIQUID_TESTNET_BITCOIN_ASSET: std::sync::LazyLock = std::sync::LazyLock::new(|| { elements::AssetId::from_inner(sha256::Midstate([ 0x49, 0x9a, 0x81, 0x85, 0x45, 0xf6, 0xba, 0xe3, 0x9f, 0xc0, 0x3b, 0x63, 0x7f, 0x2a, 0x4e, 0x1e, 0x64, 0xe5, @@ -12,6 +13,7 @@ pub static LIQUID_TESTNET_BITCOIN_ASSET: std::sync::LazyLock ])) }); +/// The genesis block hash for Liquid mainnet. pub static LIQUID_MAINNET_GENESIS: std::sync::LazyLock = std::sync::LazyLock::new(|| { elements::BlockHash::from_byte_array([ 0x03, 0x60, 0x20, 0x8a, 0x88, 0x96, 0x92, 0x37, 0x2c, 0x8d, 0x68, 0xb0, 0x84, 0xa6, 0x2e, 0xfd, 0xf6, 0x0e, @@ -19,6 +21,7 @@ pub static LIQUID_MAINNET_GENESIS: std::sync::LazyLock = st ]) }); +/// The genesis block hash for Liquid testnet. pub static LIQUID_TESTNET_GENESIS: std::sync::LazyLock = std::sync::LazyLock::new(|| { elements::BlockHash::from_byte_array([ 0xc1, 0xb1, 0x6a, 0xe2, 0x4f, 0x24, 0x23, 0xae, 0xa2, 0xea, 0x34, 0x55, 0x22, 0x92, 0x79, 0x3b, 0x5b, 0x5e, @@ -26,6 +29,7 @@ pub static LIQUID_TESTNET_GENESIS: std::sync::LazyLock = st ]) }); +/// The genesis block hash for Elements regtest environments. pub static LIQUID_REGTEST_GENESIS: std::sync::LazyLock = std::sync::LazyLock::new(|| { elements::BlockHash::from_byte_array([ 0x21, 0xca, 0xb1, 0xe5, 0xda, 0x47, 0x18, 0xea, 0x14, 0x0d, 0x97, 0x16, 0x93, 0x17, 0x02, 0x42, 0x2f, 0x0e, @@ -33,19 +37,36 @@ pub static LIQUID_REGTEST_GENESIS: std::sync::LazyLock = st ]) }); +/// Represents the target network configuration for Simplicity interactions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SimplicityNetwork { + /// Liquid mainnet. Liquid, + /// Liquid testnet. LiquidTestnet, - ElementsRegtest { policy_asset: elements::AssetId }, + /// Local Elements Regtest environment. + ElementsRegtest { + /// Regtest mode `AssetId`, which is used as a default policy asset locally. + policy_asset: elements::AssetId, + }, } impl SimplicityNetwork { + /// Creates a default Elements Regtest configuration. + /// + /// # Panics + /// This function will panic if the provided `LIQUID_DEFAULT_REGTEST_ASSET_STR` cannot be parsed. + #[must_use] pub fn default_regtest() -> Self { let policy_asset = elements::AssetId::from_str(LIQUID_DEFAULT_REGTEST_ASSET_STR).unwrap(); Self::ElementsRegtest { policy_asset } } + /// Returns the policy `AssetId` associated with the current network. + /// + /// # Panics + /// This function will panic if the provided `LIQUID_DEFAULT_REGTEST_ASSET_STR` cannot be parsed. + #[must_use] pub fn policy_asset(&self) -> elements::AssetId { match self { Self::Liquid => elements::AssetId::from_str(LIQUID_POLICY_ASSET_STR).unwrap(), @@ -54,6 +75,8 @@ impl SimplicityNetwork { } } + /// Returns the genesis block hash for the network variant. + #[must_use] pub fn genesis_block_hash(&self) -> elements::BlockHash { match self { Self::Liquid => *LIQUID_MAINNET_GENESIS, @@ -62,10 +85,14 @@ impl SimplicityNetwork { } } + /// Determines if the current network is the mainnet (Liquid). + #[must_use] pub fn is_mainnet(&self) -> bool { self == &Self::Liquid } + /// Returns the address parameters associated with the current enum variant. + #[must_use] pub const fn address_params(&self) -> &'static elements::AddressParams { match self { Self::Liquid => &elements::AddressParams::LIQUID, diff --git a/crates/sdk/src/provider/rpc/elements.rs b/crates/sdk/src/provider/rpc/elements.rs index 9f9d6fd..5371dc6 100644 --- a/crates/sdk/src/provider/rpc/elements.rs +++ b/crates/sdk/src/provider/rpc/elements.rs @@ -11,13 +11,22 @@ use super::error::RpcError; use crate::utils::sat2btc; +/// A lightweight wrapper around the standard `bitcoincore_rpc` `Client` providing Elements-specific functionality. +#[derive(Debug)] pub struct ElementsRpc { + /// The underlying JSON-RPC client connected to the Elements node. pub inner: Client, + /// The authentication credentials used. pub auth: Auth, + /// The URL endpoint of the node. pub url: String, } impl ElementsRpc { + /// Creates a new `ElementsRpc` client. + /// + /// # Errors + /// Returns an `RpcError` if it fails to initialize the connection or if a liveness `ping` fails. pub fn new(url: String, auth: Auth) -> Result { let inner = Client::new(url.as_str(), auth.clone())?; inner.ping()?; @@ -25,6 +34,13 @@ impl ElementsRpc { Ok(Self { inner, auth, url }) } + /// Requests a new wallet address from the node, mapped to the provided label. + /// + /// # Errors + /// Returns an `RpcError` if the node fails to generate an address or yields an invalid string payload. + /// + /// # Panics + /// Panics if the returned JSON value is not a string, or if the string cannot be parsed into a valid `Address`. pub fn get_new_address(&self, label: &str) -> Result { const METHOD: &str = "getnewaddress"; @@ -33,6 +49,14 @@ impl ElementsRpc { Ok(Address::from_str(addr.as_str().unwrap()).unwrap()) } + /// Instructs the node to transfer funds directly to the target address. + /// + /// # Errors + /// Returns an `RpcError` if the node returns an error or returns an invalid `Txid` payload. + /// + /// # Panics + /// Panics if the satoshi amount cannot be converted to a valid BTC amount, if the returned + /// JSON value is not a string, or if the string cannot be parsed into a valid `Txid`. pub fn send_to_address(&self, address: &Address, satoshi: u64, asset: Option) -> Result { const METHOD: &str = "sendtoaddress"; @@ -65,6 +89,10 @@ impl ElementsRpc { Ok(Txid::from_str(r.as_str().unwrap()).unwrap()) } + /// Instructs the node to rescan the block chain for missed wallet transactions. + /// + /// # Errors + /// Returns an `RpcError` if the node fails the RPC call. pub fn rescan_blockchain(&self, start: Option, stop: Option) -> Result<(), RpcError> { const METHOD: &str = "rescanblockchain"; @@ -83,7 +111,11 @@ impl ElementsRpc { Ok(()) } - pub fn generate_blocks(&self, block_num: u32) -> Result<(), RpcError> { + /// Mines a specified number of new blocks mapping generated rewards to a newly generated wallet address. + /// + /// # Errors + /// Returns an `RpcError` if generating the address or calling the `generatetoaddress` RPC command fails. + pub fn generate_blocks(&self, block_num: u64) -> Result<(), RpcError> { const METHOD: &str = "generatetoaddress"; let address = self.get_new_address("")?.to_string(); @@ -92,6 +124,10 @@ impl ElementsRpc { Ok(()) } + /// Instructs the node to sweep the `initialfreecoins` balance generated by standard regtest genesis blocks. + /// + /// # Errors + /// Returns an `RpcError` if the `getnewaddress` or `sendtoaddress` RPC commands fail. pub fn sweep_initialfreecoins(&self) -> Result<(), RpcError> { const METHOD: &str = "sendtoaddress"; @@ -109,4 +145,17 @@ impl ElementsRpc { Ok(()) } + + /// Retrieves the current block chain tip height. + /// + /// # Errors + /// Returns an `RpcError` if the node call fails or yields a JSON structure that does not map successfully to a `u64`. + pub fn height(&self) -> Result { + const METHOD: &str = "getblockcount"; + + self.inner + .call::(METHOD, &[])? + .as_u64() + .ok_or_else(|| RpcError::ElementsRpcUnexpectedReturn(METHOD.into())) + } } diff --git a/crates/sdk/src/provider/rpc/error.rs b/crates/sdk/src/provider/rpc/error.rs index 0d76c06..46cc351 100644 --- a/crates/sdk/src/provider/rpc/error.rs +++ b/crates/sdk/src/provider/rpc/error.rs @@ -1,11 +1,15 @@ +/// Errors that can occur when calling JSON-RPC methods on a Bitcoin Core or Elements node. #[derive(thiserror::Error, Debug)] pub enum RpcError { + /// Transparent wrapper mapping underlying errors generated directly from the Core/Elements RPC node client. #[error(transparent)] ElementsRpcError(#[from] electrsd::bitcoind::bitcoincore_rpc::Error), + /// Error indicating the requested Elements RPC call succeeded but the resulting JSON data payload did not map to the expected type or structure. #[error("Elements RPC returned an unexpected value for call {0}")] ElementsRpcUnexpectedReturn(String), + /// Error thrown when an invalid hex string fails to parse back into an exact byte array sequence. #[error("Failed to decode hex value to array, {0}")] BitcoinHashesHex(#[from] bitcoin_hashes::hex::HexToArrayError), } diff --git a/crates/sdk/src/provider/rpc/mod.rs b/crates/sdk/src/provider/rpc/mod.rs index 9201cc8..1460870 100644 --- a/crates/sdk/src/provider/rpc/mod.rs +++ b/crates/sdk/src/provider/rpc/mod.rs @@ -1,2 +1,4 @@ +/// A lightweight JSON-RPC client wrapper and utility methods for interacting with an Elements node. pub mod elements; +/// Error definitions mapping native framework RPC issues and parsing exceptions. pub mod error; diff --git a/crates/sdk/src/provider/simplex.rs b/crates/sdk/src/provider/simplex.rs index c34f25b..e1e018d 100644 --- a/crates/sdk/src/provider/simplex.rs +++ b/crates/sdk/src/provider/simplex.rs @@ -5,18 +5,28 @@ use electrsd::bitcoind::bitcoincore_rpc::Auth; use simplicityhl::elements::{Address, Script, Transaction, Txid}; use crate::provider::SimplicityNetwork; -use crate::transaction::UTXO; +use crate::transaction::{TxReceipt, UTXO}; use super::core::ProviderTrait; use super::error::ProviderError; use super::{ElementsRpc, EsploraProvider}; +/// A local provider used during Regtest or local development. +/// It wraps an `EsploraProvider` for REST API queries and an `ElementsRpc` for direct node interactions. +#[derive(Debug)] pub struct SimplexProvider { + /// The Esplora provider for handling REST API queries. pub esplora: EsploraProvider, + /// The Elements RPC provider for direct node operations and wallet interaction. pub elements: ElementsRpc, } impl SimplexProvider { + /// Creates a new `SimplexProvider` with the given URLs, authentication, and network. + /// + /// # Panics + /// Panics if the `ElementsRpc` client fails to initialize. + #[must_use] pub fn new(esplora_url: String, elements_url: String, auth: Auth, network: SimplicityNetwork) -> Self { Self { esplora: EsploraProvider::new(esplora_url, network), @@ -30,12 +40,12 @@ impl ProviderTrait for SimplexProvider { self.esplora.get_network() } - fn broadcast_transaction(&self, tx: &Transaction) -> Result { - let txid = self.esplora.broadcast_transaction(tx)?; + fn broadcast_transaction(&self, tx: &Transaction) -> Result, ProviderError> { + let tx_receipt = self.esplora.broadcast_transaction(tx)?; self.elements.generate_blocks(1)?; - Ok(txid) + Ok(tx_receipt) } fn wait(&self, txid: &Txid) -> Result<(), ProviderError> { diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index 7a4f95c..ef7e2c4 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -33,13 +33,19 @@ use crate::program::ProgramTrait; use crate::provider::ProviderTrait; use crate::provider::SimplicityNetwork; use crate::signer::wtns_injector::WtnsInjector; -use crate::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO}; +use crate::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, TxReceipt, UTXO}; use super::error::SignerError; +/// A placeholder dummy fee amount used during transaction estimation. pub const PLACEHOLDER_FEE: u64 = 1; +/// Common signing interface spanning over standard explicit inputs and Simplicity programs. pub trait SignerTrait { + /// Generates a Schnorr signature to satisfy a target Simplicity program input. + /// + /// # Errors + /// Returns a `SignerError` if the elements environment fails to build or if the message digest fails to construct. fn sign_program( &self, pst: &PartiallySignedTransaction, @@ -48,6 +54,10 @@ pub trait SignerTrait { network: &SimplicityNetwork, ) -> Result; + /// Generates an ECDSA signature to spend a standard transaction input. + /// + /// # Errors + /// Returns a `SignerError` if the transaction formatting or sighash msg extraction fails. fn sign_input( &self, pst: &PartiallySignedTransaction, @@ -55,6 +65,8 @@ pub trait SignerTrait { ) -> Result<(PublicKey, ecdsa::Signature), SignerError>; } +/// Core interface responsible for managing keys, interfacing with the blockchain provider, +/// assembling descriptors, estimating fees, and finalizing/signing transactions. pub struct Signer { mnemonic: Mnemonic, xprv: Xpriv, @@ -109,6 +121,11 @@ enum Estimate { } impl Signer { + /// Creates a new `Signer` instance seeded from the provided mnemonic and paired with the specified provider. + /// + /// # Panics + /// Panics if the mnemonic fails to parse, or if deriving the master private key fails. + #[must_use] pub fn new(mnemonic: &str, provider: Box) -> Self { let secp = Secp256k1::new(); let mnemonic: Mnemonic = mnemonic @@ -129,8 +146,12 @@ impl Signer { } } + /// Composes, funds, and broadcasts a standard network transaction sending the specified value of the primary policy asset. + /// + /// # Errors + /// Returns a `SignerError` if compiling the inputs fails, there are insufficient funds/fees, or broadcast is rejected. // TODO: add an ability to send arbitrary assets - pub fn send(&self, to: Script, amount: u64) -> Result { + pub fn send(&self, to: Script, amount: u64) -> Result, SignerError> { let mut ft = FinalTransaction::new(); ft.add_output(PartialOutput::new(to, amount, self.network.policy_asset())); @@ -140,12 +161,20 @@ impl Signer { Ok(self.provider.broadcast_transaction(&tx)?) } - pub fn broadcast(&self, tx: &FinalTransaction) -> Result { + /// Evaluates, funds, and broadcasts an already assembled `FinalTransaction`. + /// + /// # Errors + /// Returns a `SignerError` if finalizing the payload fails or if the network rejects the broadcast. + pub fn broadcast(&self, tx: &FinalTransaction) -> Result, SignerError> { let (tx, _fee) = self.finalize(tx)?; Ok(self.provider.broadcast_transaction(&tx)?) } + /// Evaluates the input components of a `FinalTransaction`, iteratively selecting available wallet UTXOs to cover outputs and estimated fees. + /// + /// # Errors + /// Returns a `SignerError` if the wallet contains insufficient funds to satisfy output values and target fee rates. pub fn finalize(&self, tx: &FinalTransaction) -> Result<(Transaction, u64), SignerError> { let mut signer_utxos = self.get_utxos_asset(self.network.policy_asset())?; let mut set = HashSet::new(); @@ -160,18 +189,7 @@ impl Signer { signer_utxos.retain(|utxo| !set.contains(&utxo.outpoint)); // descending sort of both confidential and explicit utxos - signer_utxos.sort_by(|a, b| { - let a_value = match a.secrets { - Some(secrets) => secrets.value, - None => a.explicit_amount(), - }; - let b_value = match b.secrets { - Some(secrets) => secrets.value, - None => b.explicit_amount(), - }; - - b_value.cmp(&a_value) - }); + signer_utxos.sort_by_key(|utxo| std::cmp::Reverse(utxo.amount())); let mut fee_tx = tx.clone(); let mut curr_fee = MIN_FEE; @@ -180,8 +198,8 @@ impl Signer { for utxo in signer_utxos { let policy_amount_delta = fee_tx.calculate_fee_delta(&self.network); - if policy_amount_delta >= curr_fee as i64 { - match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta as u64)? { + if policy_amount_delta >= curr_fee.cast_signed() { + match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta.cast_unsigned())? { Estimate::Success(tx, fee) => return Ok((tx, fee)), Estimate::Failure(required_fee) => curr_fee = required_fee, } @@ -193,8 +211,8 @@ impl Signer { // need to try one more time after the loop let policy_amount_delta = fee_tx.calculate_fee_delta(&self.network); - if policy_amount_delta >= curr_fee as i64 { - match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta as u64)? { + if policy_amount_delta >= curr_fee.cast_signed() { + match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta.cast_unsigned())? { Estimate::Success(tx, fee) => return Ok((tx, fee)), Estimate::Failure(required_fee) => curr_fee = required_fee, } @@ -203,6 +221,12 @@ impl Signer { Err(SignerError::NotEnoughFunds(curr_fee)) } + /// Verifies and finalizes a transaction against a strict target confirmation window (in blocks). + /// This function also assumes that the transaction already includes the coin selection. + /// + /// # Errors + /// Returns a `SignerError` if the assembled inputs do not meet dust limits or fail to cover the + /// dynamically estimated required fee. pub fn finalize_strict( &self, tx: &FinalTransaction, @@ -210,23 +234,30 @@ impl Signer { ) -> Result<(Transaction, u64), SignerError> { let policy_amount_delta = tx.calculate_fee_delta(&self.network); - if policy_amount_delta < MIN_FEE as i64 { + if policy_amount_delta < MIN_FEE.cast_signed() { return Err(SignerError::DustAmount(policy_amount_delta)); } let fee_rate = self.provider.fetch_fee_rate(target_blocks)?; // policy_amount_delta will be > 0 - match self.estimate_tx(tx.clone(), fee_rate, policy_amount_delta as u64)? { + match self.estimate_tx(tx.clone(), fee_rate, policy_amount_delta.cast_unsigned())? { Estimate::Success(tx, fee) => Ok((tx, fee)), Estimate::Failure(required_fee) => Err(SignerError::NotEnoughFeeAmount(policy_amount_delta, required_fee)), } } + /// Returns a reference to the active configured network provider. + #[must_use] pub fn get_provider(&self) -> &dyn ProviderTrait { self.provider.as_ref() } + /// Returns the confidential elements address matching the local wallet logic. + /// + /// # Panics + /// Panics if the SLIP77 descriptor cannot be generated or parsed, or if address derivation fails. + #[must_use] pub fn get_confidential_address(&self) -> Address { let mut descriptor = ConfidentialDescriptor::::from_str(&self.get_slip77_descriptor().unwrap()) @@ -243,6 +274,11 @@ impl Signer { .unwrap() } + /// Returns the standard unblinded address matching the local wallet logic. + /// + /// # Panics + /// Panics if the WPKH descriptor cannot be generated or parsed, or if address derivation fails. + #[must_use] pub fn get_address(&self) -> Address { let descriptor = Descriptor::::from_str(&self.get_wpkh_descriptor().unwrap()) .map_err(|e| SignerError::WpkhDescriptor(e.to_string())) @@ -255,21 +291,36 @@ impl Signer { .unwrap() } + /// Iterates against the network provider to select and unblind all known UTXOs. + /// + /// # Errors + /// Returns a `SignerError` if querying the network or unblinding operations fail. pub fn get_utxos(&self) -> Result, SignerError> { self.get_utxos_filter(&|_| true, &|_| true) } + /// Finds all known UTXOs belonging to the specific `AssetId`. + /// + /// # Errors + /// Returns a `SignerError` if network interaction or confidential output decryption fails. pub fn get_utxos_asset(&self, asset: AssetId) -> Result, SignerError> { - self.get_utxos_filter(&|utxo| utxo.explicit_asset() == asset, &|utxo| { - utxo.unblinded_asset() == asset - }) + self.get_utxos_filter(&|utxo| utxo.asset() == asset, &|utxo| utxo.asset() == asset) } + /// Finds all known UTXOs deriving from a targeted `Txid`. + /// + /// # Errors + /// Returns a `SignerError` if querying the network fails. // TODO: can this be optimized to not populate TxOuts that are filtered out? pub fn get_utxos_txid(&self, txid: Txid) -> Result, SignerError> { self.get_utxos_filter(&|utxo| utxo.outpoint.txid == txid, &|utxo| utxo.outpoint.txid == txid) } + /// Maps UTXOs retrieved from the provider through arbitrary functional filters. + /// Separate filtering criteria apply explicitly vs confidentially. + /// + /// # Errors + /// Returns a `SignerError` if retrieving remote outputs or executing confidential node unblinding throws an error. pub fn get_utxos_filter( &self, explicit_filter: &dyn Fn(&UTXO) -> bool, @@ -298,6 +349,8 @@ impl Signer { Ok(all_utxos) } + /// Derives the X-Only public key specifically used for Schnorr and Taproot structures. + #[must_use] pub fn get_schnorr_public_key(&self) -> XOnlyPublicKey { let private_key = self.get_private_key(); let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner); @@ -305,14 +358,23 @@ impl Signer { keypair.x_only_public_key().0 } + /// Resolves the standard format ECDSA public key. + #[must_use] pub fn get_ecdsa_public_key(&self) -> PublicKey { self.get_private_key().public_key(&self.secp) } + /// Resolves the corresponding blinding public key. + #[must_use] pub fn get_blinding_public_key(&self) -> PublicKey { self.get_blinding_private_key().public_key(&self.secp) } + /// Internally derives and exposes the wallet's signing active private key. + /// + /// # Panics + /// Panics if the master private key or derivation path cannot be derived. + #[must_use] pub fn get_private_key(&self) -> PrivateKey { let master_xprv = self.master_xpriv().unwrap(); let full_path = self.get_derivation_path().unwrap(); @@ -328,6 +390,14 @@ impl Signer { PrivateKey::new(ext_derived.private_key, NetworkKind::Test) } + /// Generates the private key linked to confidential payload blinding. + /// + /// The generated `PrivateKey` is associated with the `Test` (non-Bitcoin-mainnet) network kind. + /// Retrieves the blinding private key derived from the master SLIP77 key and the script public key of the address. + /// + /// # Panics + /// Panics if the master SLIP77 key cannot be derived. + #[must_use] pub fn get_blinding_private_key(&self) -> PrivateKey { let blinding_key = self .master_slip77() @@ -471,7 +541,9 @@ impl Signer { let signature = self.sign_program(pst, program, index, &self.network)?; // inject the signature into the wtns name directly if the path is not provided - let sig_val = if !sig_path.is_empty() { + let sig_val = if sig_path.is_empty() { + Value::byte_array(signature.serialize()) + } else { let witness_types = program.get_witness_types()?; let witness_type = witness_types .get(&WitnessName::from_str_unchecked(witness_name)) @@ -490,8 +562,6 @@ impl Signer { sig_path, Value::byte_array(signature.serialize()), )? - } else { - Value::byte_array(signature.serialize()) }; let mut hm = HashMap::new(); @@ -505,6 +575,7 @@ impl Signer { Ok(WitnessValues::from(hm)) } + #[allow(clippy::unnecessary_wraps)] fn master_slip77(&self) -> Result { let seed = self.mnemonic.to_seed(""); diff --git a/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs index 59fd0d3..d41c707 100644 --- a/crates/sdk/src/signer/error.rs +++ b/crates/sdk/src/signer/error.rs @@ -1,80 +1,106 @@ use crate::program::ProgramError; use crate::provider::ProviderError; +/// Core error types for the Signer component. #[derive(Debug, thiserror::Error)] pub enum SignerError { + /// Errors originating from Simplicity program evaluation and state. #[error(transparent)] Program(#[from] ProgramError), + /// Errors originating from provider network interactions. #[error(transparent)] Provider(#[from] ProviderError), + /// Errors encountered when attempting to inject or wrap witness fields. #[error(transparent)] WtnsInjectError(#[from] WtnsWrappingError), + /// Error indicating an incorrectly formatted mnemonic phrase. #[error("Failed to parse a mnemonic: {0}")] Mnemonic(String), + /// Error thrown when PSET transaction extraction fails. #[error("Failed to extract tx from pst: {0}")] TxExtraction(#[from] simplicityhl::elements::pset::Error), + /// Error indicating failure to unblind a confidential transaction output. #[error("Failed to unblind txout: {0}")] Unblind(#[from] simplicityhl::elements::UnblindError), + /// Error thrown when PSET blinding fails. #[error("Failed to blind a PST: {0}")] PsetBlind(#[from] simplicityhl::elements::pset::PsetBlindError), + /// Error indicating failure to construct sighash for input spending. #[error("Failed to construct a message for the input spending: {0}")] SighashConstruction(#[from] elements_miniscript::psbt::SighashError), + /// Error indicating the transaction inputs cover an amount that is lower than the dust limit. #[error("Fee amount is too low: {0}")] DustAmount(i64), + /// Error indicating the defined fee amount cannot cover the calculated transaction costs. #[error("Not enough fee amount {0} to cover transaction costs: {1}")] NotEnoughFeeAmount(i64, u64), + /// Error indicating that the available UTXO funds are not enough to cover total costs. #[error("Not enough funds on account to cover transaction costs: {0}")] NotEnoughFunds(u64), + /// Error indicating an invalid upstream `secp256k1` secret key. #[error("Invalid secret key")] InvalidSecretKey(#[from] simplicityhl::elements::secp256k1_zkp::UpstreamError), + /// Error thrown when HD wallet private key derivation fails. #[error("Failed to derive a private key: {0}")] PrivateKeyDerivation(#[from] elements_miniscript::bitcoin::bip32::Error), + /// Error thrown when constructing a derivation path string fails. #[error("Failed to construct a derivation path: {0}")] DerivationPath(String), + /// Error indicating failure to construct a valid WPKH (Witness Public Key Hash) descriptor. #[error("Failed to construct a wpkh descriptor: {0}")] WpkhDescriptor(String), + /// Error indicating failure to construct a valid SLIP77 blinding key descriptor. #[error("Failed to construct a slip77 descriptor: {0}")] Slip77Descriptor(String), + /// Error thrown if there's a problem during descriptor conversion. #[error("Failed to convert a descriptor: {0}")] DescriptorConversion(#[from] elements_miniscript::descriptor::ConversionError), + /// Error thrown when WPKH address creation fails. #[error("Failed to construct a wpkh address: {0}")] WpkhAddressConstruction(#[from] elements_miniscript::Error), + /// Error indicating an expected witness field could not be found. #[error("Missing such witness field: {0}")] WtnsFieldNotFound(String), } +/// Errors originating from manipulating witness paths and injecting values. #[derive(Debug, thiserror::Error)] pub enum WtnsWrappingError { + /// Error indicating a failure while parsing the provided witness path string. #[error("Failed to parse path")] ParsingError, + /// Error pointing to the use of a path type that is currently not supported. #[error("Unsupported path type: {0}")] UnsupportedPathType(String), + /// Error thrown during path traversal when an index exceeds the inner array lengths. #[error("Path index out of bounds: len is {0}, got {1}")] IdxOutOfBounds(usize, usize), + /// Error indicating that the runtime type at the path root expected one type but encountered another. #[error("Root type mismatch: expected {0}, got {1}")] RootTypeMismatch(String, String), + /// Error indicating that a path traversal attempted to reach an undefined or mismatched Either branch. #[error("Path reached undefined branch of Either")] EitherBranchMismatch, } diff --git a/crates/sdk/src/signer/mod.rs b/crates/sdk/src/signer/mod.rs index 9cdcfcd..dc8007e 100644 --- a/crates/sdk/src/signer/mod.rs +++ b/crates/sdk/src/signer/mod.rs @@ -1,5 +1,8 @@ +/// Core implementations and abstractions for transaction signing mechanisms in the Simplex SDK. pub mod core; +/// Signer-specific error enumerations capturing execution constraints and mapping internal failure types. pub mod error; +/// Utilities for injecting witness data bindings into Simplicity environments. mod wtns_injector; pub use core::{Signer, SignerTrait}; diff --git a/crates/sdk/src/signer/wtns_injector.rs b/crates/sdk/src/signer/wtns_injector.rs index 5accf52..1084167 100644 --- a/crates/sdk/src/signer/wtns_injector.rs +++ b/crates/sdk/src/signer/wtns_injector.rs @@ -9,23 +9,27 @@ use simplicityhl::{ use crate::signer::error::WtnsWrappingError; +/// Represents an index-based route for array or tuple witness types. #[derive(Clone, Copy, Debug)] -pub struct EnumerableRoute(usize); +pub(super) struct EnumerableRoute(usize); +/// Represents a branch route for `Either` witness type. #[derive(Clone, Copy, Debug)] -pub enum EitherRoute { +pub(super) enum EitherRoute { Left, Right, } +/// Represents a single step in a witness path, either following a branch or an index. #[derive(Clone, Copy, Debug)] -pub enum WtnsPathRoute { +pub(super) enum WtnsPathRoute { Either(EitherRoute), Enumerable(EnumerableRoute), } +/// Exposes utilities to safely inject values into specific locations within a Simplicity witness structure. #[derive(Clone)] -pub struct WtnsInjector {} +pub(super) struct WtnsInjector {} enum StackItem { Either(EitherRoute, Arc), @@ -34,9 +38,16 @@ enum StackItem { } impl WtnsInjector { - /// Constructs new value by injecting given value into witness at the position described by `path`. + /// Constructs a new value by injecting a given value into the witness at the position described by `path`. + /// /// Consistency between `witness` and `witness_types` should be guaranteed by the caller. - pub fn inject_value( + /// + /// # Errors + /// Returns a `WtnsWrappingError` if the path contains invalid segments, attempts to access an out-of-bounds index, navigates into an incorrect type layout, or expects a different branch representation. + /// + /// # Panics + /// Panics if internal type validations or downcasts fail after safety checks have passed. + pub(super) fn inject_value( witness: &Arc, witness_types: &ResolvedType, path: I, @@ -52,12 +63,13 @@ impl WtnsInjector { let mut current_val = Arc::clone(witness); let mut current_ty = witness_types; - for route in parsed_path.iter() { + for route in &parsed_path { if !matches!( (route, current_ty.as_inner()), - (WtnsPathRoute::Enumerable(_), TypeInner::Array(_, _)) - | (WtnsPathRoute::Enumerable(_), TypeInner::Tuple(_)) - | (WtnsPathRoute::Either(_), TypeInner::Either(_, _)) + ( + WtnsPathRoute::Enumerable(_), + TypeInner::Array(_, _) | TypeInner::Tuple(_) + ) | (WtnsPathRoute::Either(_), TypeInner::Either(_, _)) ) { return Err(WtnsWrappingError::UnsupportedPathType(current_ty.to_string())); } @@ -71,12 +83,12 @@ impl WtnsInjector { (EitherRoute::Left, false) => { stack.push(StackItem::Either(direction, Arc::clone(right_ty))); current_ty = left_ty; - current_val = Arc::clone(either_val.as_ref().unwrap_left()) + current_val = Arc::clone(either_val.as_ref().unwrap_left()); } (EitherRoute::Right, true) => { stack.push(StackItem::Either(direction, Arc::clone(left_ty))); current_ty = right_ty; - current_val = Arc::clone(either_val.as_ref().unwrap_right()) + current_val = Arc::clone(either_val.as_ref().unwrap_right()); } _ => return Err(WtnsWrappingError::EitherBranchMismatch), } @@ -189,7 +201,7 @@ impl TryInto for WtnsPathRoute { fn try_into(self) -> Result { match self { Self::Either(direction) => Ok(direction), - _ => Err(self), + Self::Enumerable(_) => Err(self), } } } @@ -200,7 +212,7 @@ impl TryInto for WtnsPathRoute { fn try_into(self) -> Result { match self { Self::Enumerable(tuple) => Ok(tuple), - _ => Err(self), + Self::Either(_) => Err(self), } } } diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index 0fcc90e..0742a51 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -1,27 +1,144 @@ use std::collections::HashMap; -use simplicityhl::elements::pset::PartiallySignedTransaction; +use bitcoin_hashes::sha256; + +use simplicityhl::elements::pset::{Input, PartiallySignedTransaction}; use simplicityhl::elements::{ AssetId, TxOutSecrets, confidential::{AssetBlindingFactor, ValueBlindingFactor}, }; use crate::provider::SimplicityNetwork; -use crate::utils::asset_entropy; +use crate::utils; use super::partial_input::{IssuanceInput, PartialInput, ProgramInput, RequiredSignature}; use super::partial_output::PartialOutput; +/// Constant is defined for fee calculation on transaction sending. pub const WITNESS_SCALE_FACTOR: usize = 4; +/// A structure representing the details of token issuance and related metadata. +#[derive(Debug, Clone)] +pub struct IssuanceDetails { + /// The unique `AssetId` generated from the provided entropy, representing the issued tokens struct. + pub asset_id: AssetId, + /// The `AssetId` corresponding to the reissuance (inflation) token, used for minting new tokens. + pub inflation_asset_id: AssetId, + /// The entropy value (`sha256::Midstate`) that was used to derive both the `asset_id` and `inflation_asset_id`. + pub asset_entropy: sha256::Midstate, +} + +/// Represents the final input structure put into a `FinalTransaction` for processing. #[derive(Clone)] pub struct FinalInput { + /// Holds the base input data required for the operation. pub partial_input: PartialInput, + /// Holds program inputs, which are used for program witness finalization. pub program_input: Option, + /// Contains optional issuance-related information. pub issuance_input: Option, + /// Required signature for finalizing the transaction. pub required_sig: RequiredSignature, } +impl FinalInput { + /// Creates a new instance of the type with the specified `partial_input` and `required_sig`. + #[must_use] + pub fn new(partial_input: PartialInput, required_sig: RequiredSignature) -> Self { + Self { + partial_input, + required_sig, + program_input: None, + issuance_input: None, + } + } + + /// Sets the `program_input` field with the given `ProgramInput` and returns the modified `FinalInput`. + #[must_use] + pub fn with_program(mut self, program_input: ProgramInput) -> Self { + self.program_input = Some(program_input); + + self + } + + /// Sets the `issuance_input` field of the current instance and returns the updated `FinalInput`. + #[must_use] + pub fn with_issuance(mut self, issuance_input: IssuanceInput) -> Self { + self.issuance_input = Some(issuance_input); + + self + } + + /// Retrieves the issuance details associated with the current instance. + /// + /// # Errors + /// + /// This method does not explicitly return errors but returns `None` if no issuance + /// input is available. + #[must_use] + pub fn get_issuance_details(&self) -> Option { + match &self.issuance_input { + Some(issuance_input) => { + let asset_entropy = match issuance_input { + IssuanceInput::Issuance { asset_entropy, .. } => { + utils::asset_entropy(&self.partial_input.outpoint(), *asset_entropy) + } + IssuanceInput::Reissuance { asset_entropy, .. } => { + sha256::Midstate::from_byte_array(*asset_entropy) + } + }; + + let asset_id = AssetId::from_entropy(asset_entropy); + let inflation_asset_id = AssetId::reissuance_token_from_entropy(asset_entropy, false); + + Some(IssuanceDetails { + asset_id, + inflation_asset_id, + asset_entropy, + }) + } + None => None, + } + } + + /// Converts the current object into an `Input` representation, including any + /// issuance input and partial input details. + /// + /// # Panics + /// + /// This function will panic if the `issuance_input` is of type `Reissuance` + /// and the `partial_input.secrets` field is `None` or does not contain the necessary + /// confidential information. Specifically, a panic occurs when attempting to unwrap the `asset_bf` value. + #[must_use] + pub fn to_input(&self) -> Input { + let mut pst_input = self.partial_input.to_input(); + + // populate the input manually since `input.merge` is private + if let Some(issuance_input) = &self.issuance_input { + let issue = issuance_input.to_input(); + + pst_input.issuance_value_amount = issue.issuance_value_amount; + pst_input.issuance_asset_entropy = issue.issuance_asset_entropy; + pst_input.issuance_inflation_keys = issue.issuance_inflation_keys; + pst_input.blinded_issuance = issue.blinded_issuance; + + if matches!(issuance_input, IssuanceInput::Reissuance { .. }) { + let issuance_blinding_nonce = self + .partial_input + .secrets + .expect("Reissuance input must be confidential") + .asset_bf + .into_inner(); + + pst_input.issuance_blinding_nonce = Some(issuance_blinding_nonce); + } + } + + pst_input + } +} + +/// A struct representing a final (but not yet signed) transaction. #[derive(Clone)] pub struct FinalTransaction { inputs: Vec, @@ -29,6 +146,8 @@ pub struct FinalTransaction { } impl FinalTransaction { + /// Creates a new instance of the final transaction with default values. + #[must_use] #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { @@ -37,22 +156,27 @@ impl FinalTransaction { } } + /// Adds a new input to the transaction. + /// + /// # Panics + /// Panics if the requested signature is not `NativeEcdsa` or `None`. + /// (i.e. if `required_sig` is `RequiredSignature::Witness` or `RequiredSignature::WitnessWithPath`) pub fn add_input(&mut self, partial_input: PartialInput, required_sig: RequiredSignature) { match required_sig { RequiredSignature::Witness(_) | RequiredSignature::WitnessWithPath(_, _) => { panic!("Requested signature is not NativeEcdsa or None") } _ => {} - }; + } - self.inputs.push(FinalInput { - partial_input, - program_input: None, - issuance_input: None, - required_sig, - }); + self.push_new_input(FinalInput::new(partial_input, required_sig)); } + /// Adds a new program input to the transaction. + /// + /// # Panics + /// The function will panic if the `required_sig` parameter is of type `RequiredSignature::NativeEcdsa`, + /// as this type of signature is not applicable for program inputs. pub fn add_program_input( &mut self, partial_input: PartialInput, @@ -63,68 +187,56 @@ impl FinalTransaction { panic!("Requested signature is not Witness or None"); } - self.inputs.push(FinalInput { - partial_input, - program_input: Some(program_input), - issuance_input: None, - required_sig, - }); + self.push_new_input(FinalInput::new(partial_input, required_sig).with_program(program_input)); } + /// Adds an issuance (or reissuance) input to the transaction. + /// + /// # Panics + /// This function panics if the `required_sig` is of type `Witness` or + /// `WitnessWithPath`, as these signature types are not allowed in the current context. pub fn add_issuance_input( &mut self, partial_input: PartialInput, issuance_input: IssuanceInput, required_sig: RequiredSignature, - ) -> (AssetId, AssetId) { + ) -> IssuanceDetails { match required_sig { RequiredSignature::Witness(_) | RequiredSignature::WitnessWithPath(_, _) => { panic!("Requested signature is not NativeEcdsa or None") } _ => {} - }; - - let entropy = asset_entropy(&partial_input.outpoint(), issuance_input.asset_entropy); - - let issuance_asset_id = AssetId::from_entropy(entropy); - let reissuance_asset_id = AssetId::reissuance_token_from_entropy(entropy, false); - - self.inputs.push(FinalInput { - partial_input, - program_input: None, - issuance_input: Some(issuance_input), - required_sig, - }); + } - (issuance_asset_id, reissuance_asset_id) + self.push_new_input(FinalInput::new(partial_input, required_sig).with_issuance(issuance_input)) + .unwrap() } + /// Adds an issuance program input to the transaction with the specified parameters. + /// + /// # Panics + /// Panics if the `required_sig` parameter is of type `RequiredSignature::NativeEcdsa`. + /// Also panics if the populated input fails to return valid issuance details. pub fn add_program_issuance_input( &mut self, partial_input: PartialInput, program_input: ProgramInput, issuance_input: IssuanceInput, required_sig: RequiredSignature, - ) -> (AssetId, AssetId) { + ) -> IssuanceDetails { if let RequiredSignature::NativeEcdsa = required_sig { panic!("Requested signature is not Witness or None"); } - let entropy = asset_entropy(&partial_input.outpoint(), issuance_input.asset_entropy); - - let issuance_asset_id = AssetId::from_entropy(entropy); - let reissuance_asset_id = AssetId::reissuance_token_from_entropy(entropy, false); - - self.inputs.push(FinalInput { - partial_input, - program_input: Some(program_input), - issuance_input: Some(issuance_input), - required_sig, - }); - - (issuance_asset_id, reissuance_asset_id) + self.push_new_input( + FinalInput::new(partial_input, required_sig) + .with_program(program_input) + .with_issuance(issuance_input), + ) + .unwrap() } + /// Removes an input from the list of inputs at the specified index. pub fn remove_input(&mut self, index: usize) -> Option { if self.inputs.get(index).is_some() { return Some(self.inputs.remove(index)); @@ -133,10 +245,15 @@ impl FinalTransaction { None } + /// Adds a partial output to the list of outputs. pub fn add_output(&mut self, partial_output: PartialOutput) { self.outputs.push(partial_output); } + /// Removes an output from the `outputs` list at the specified index. + /// + /// # Panics + /// This function does not panic. If the `index` is invalid, it will return `None` instead of causing a panic. pub fn remove_output(&mut self, index: usize) -> Option { if self.outputs.get(index).is_some() { return Some(self.outputs.remove(index)); @@ -145,34 +262,59 @@ impl FinalTransaction { None } + /// Provides a slice reference to the collection of `FinalInput` elements. + #[must_use] pub fn inputs(&self) -> &[FinalInput] { &self.inputs } + /// Provides mutable access to the `inputs` field. + /// + /// This method returns a mutable slice of `FinalInput` elements, + /// allowing the caller to modify the elements in the `inputs` field. pub fn inputs_mut(&mut self) -> &mut [FinalInput] { &mut self.inputs } + /// Returns a reference to the slice of `PartialOutput` elements contained within the struct. + #[must_use] pub fn outputs(&self) -> &[PartialOutput] { &self.outputs } + /// Provides mutable access to the `outputs` field of the current struct. pub fn outputs_mut(&mut self) -> &mut [PartialOutput] { &mut self.outputs } + /// Returns the number of inputs associated with the current instance. + #[must_use] pub fn n_inputs(&self) -> usize { self.inputs.len() } + /// Returns the number of outputs associated with the object. + #[must_use] pub fn n_outputs(&self) -> usize { self.outputs.len() } + /// Checks if any of the outputs require blinding, determines if at least one of them has a `blinding_key` specified. + #[must_use] pub fn needs_blinding(&self) -> bool { self.outputs.iter().any(|el| el.blinding_key.is_some()) } + /// Calculates the fee delta for a transaction based on the inputs and outputs. + /// + /// The fee delta represents the net difference between the available asset amount + /// from the transaction's inputs and the consumed asset amount by its outputs. + /// The function considers the network's policy asset to determine which inputs + /// and outputs contribute to the calculation. + /// + /// # Panics + /// Function will panic if the asset doesn't be unblinded correctly, and PST input asset and amount is confidential. + #[must_use] pub fn calculate_fee_delta(&self, network: &SimplicityNetwork) -> i64 { let mut available_amount = 0; @@ -199,32 +341,41 @@ impl FinalTransaction { .filter(|output| output.asset == network.policy_asset()) .fold(0_u64, |acc, output| acc + output.amount); - available_amount as i64 - consumed_amount as i64 + available_amount.cast_signed() - consumed_amount.cast_signed() } + /// Computes the transaction fee based on the provided weight and fee rate. + /// + /// Overall, the function calculates the virtual size (vsize) of the transaction as: + /// `weight / WITNESS_SCALE_FACTOR`, rounded up to the nearest whole number. + /// Then, the fee is computed as `(vsize * fee_rate / 1000.0)`, also rounded up. + /// + /// # Returns + /// The transaction fee in satoshis, rounded up to the nearest whole number. + #[allow( + clippy::cast_possible_truncation, + clippy::cast_precision_loss, + clippy::cast_sign_loss + )] + #[must_use] pub fn calculate_fee(&self, weight: usize, fee_rate: f32) -> u64 { let vsize = weight.div_ceil(WITNESS_SCALE_FACTOR); (vsize as f32 * fee_rate / 1000.0).ceil() as u64 } + /// Extracts a partially signed transaction (PST) and a mapping of input secrets from the current state. + /// + /// # Panics + /// Function will panic if the pst input is a confidential issuance. + #[must_use] pub fn extract_pst(&self) -> (PartiallySignedTransaction, HashMap) { let mut input_secrets = HashMap::new(); let mut pst = PartiallySignedTransaction::new_v2(); for i in 0..self.inputs.len() { let final_input = &self.inputs[i]; - let mut pst_input = final_input.partial_input.to_input(); - - // populate the input manually since `input.merge` is private - if final_input.issuance_input.is_some() { - let issue = final_input.issuance_input.clone().unwrap().to_input(); - - pst_input.issuance_value_amount = issue.issuance_value_amount; - pst_input.issuance_asset_entropy = issue.issuance_asset_entropy; - pst_input.issuance_inflation_keys = issue.issuance_inflation_keys; - pst_input.blinded_issuance = issue.blinded_issuance; - } + let pst_input = final_input.to_input(); match final_input.partial_input.secrets { // insert input secrets if present @@ -250,6 +401,14 @@ impl FinalTransaction { (pst, input_secrets) } + + fn push_new_input(&mut self, new_input: FinalInput) -> Option { + let issuance_details = new_input.get_issuance_details(); + + self.inputs.push(new_input); + + issuance_details + } } #[cfg(test)] @@ -397,7 +556,7 @@ mod tests { let utxo = explicit_utxo(0x01, 0, 5000, policy); let partial_input = PartialInput::new(utxo); - let issuance = IssuanceInput::new(issuance_amount, entropy); + let issuance = IssuanceInput::new_issuance(issuance_amount, 0, entropy); let partial_output = PartialOutput::new(Script::new(), 4000, policy); let mut ft = FinalTransaction::new(); @@ -411,6 +570,7 @@ mod tests { expected_input.issuance_value_amount = issuance_input.issuance_value_amount; expected_input.issuance_asset_entropy = issuance_input.issuance_asset_entropy; expected_input.issuance_inflation_keys = issuance_input.issuance_inflation_keys; + expected_input.issuance_blinding_nonce = None; expected_input.blinded_issuance = issuance_input.blinded_issuance; expected_pst.add_input(expected_input); expected_pst.add_output(partial_output.to_output()); @@ -425,4 +585,42 @@ mod tests { assert_eq!(pst, expected_pst); assert_eq!(secrets, expected_secrets); } + + #[test] + fn extract_pst_with_reissuance_input() { + let policy = dummy_asset_id(0xAA); + let entropy = [0x42u8; 32]; + let issuance_amount = 1_000_000u64; + + let conf_utxo = confidential_utxo(0x02, 0, policy, 1000); + let partial_input = PartialInput::new(conf_utxo); + let reissuance_input = IssuanceInput::new_reissuance(issuance_amount, entropy); + let partial_output = PartialOutput::new(Script::new(), 1000, policy); + + let mut ft = FinalTransaction::new(); + ft.add_issuance_input(partial_input.clone(), reissuance_input.clone(), RequiredSignature::None); + ft.add_output(partial_output.clone()); + + // build expected pst, merge partial_input and issuance manually + let mut expected_pst = PartiallySignedTransaction::new_v2(); + let mut expected_input = partial_input.to_input(); + let issuance_input = reissuance_input.to_input(); + expected_input.issuance_value_amount = issuance_input.issuance_value_amount; + expected_input.issuance_asset_entropy = issuance_input.issuance_asset_entropy; + expected_input.issuance_inflation_keys = None; + expected_input.issuance_blinding_nonce = Some(partial_input.secrets.unwrap().asset_bf.into_inner()); + expected_input.blinded_issuance = issuance_input.blinded_issuance; + expected_pst.add_input(expected_input); + expected_pst.add_output(partial_output.to_output()); + + let expected_secrets = HashMap::from([( + 0, + TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 1000, ValueBlindingFactor::zero()), + )]); + + let (pst, secrets) = ft.extract_pst(); + + assert_eq!(pst, expected_pst); + assert_eq!(secrets, expected_secrets); + } } diff --git a/crates/sdk/src/transaction/mod.rs b/crates/sdk/src/transaction/mod.rs index 3bf0c5d..c4031da 100644 --- a/crates/sdk/src/transaction/mod.rs +++ b/crates/sdk/src/transaction/mod.rs @@ -1,9 +1,16 @@ +/// Represents a fully finalized target transaction schema ready for signing and broadcasting. pub mod final_transaction; +/// Represents inputs under construction before transaction finalization. pub mod partial_input; +/// Represents outputs under construction before transaction finalization. pub mod partial_output; +/// Contains data representing the submission status of a broadcast transaction. +pub mod tx_receipt; +/// Common representation of unspent transaction outputs used as funding sources. pub mod utxo; -pub use final_transaction::{FinalInput, FinalTransaction}; +pub use final_transaction::{FinalInput, FinalTransaction, IssuanceDetails}; pub use partial_input::{PartialInput, ProgramInput, RequiredSignature}; pub use partial_output::PartialOutput; +pub use tx_receipt::TxReceipt; pub use utxo::UTXO; diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index d664b04..88b6235 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -1,6 +1,5 @@ use simplicityhl::elements::confidential::{Asset, Value}; use simplicityhl::elements::pset::Input; -use simplicityhl::elements::secp256k1_zkp::Tweak; use simplicityhl::elements::{AssetId, LockTime, OutPoint, Sequence, TxOut, TxOutSecrets, Txid}; use crate::program::ProgramTrait; @@ -8,15 +7,21 @@ use crate::program::WitnessTrait; use super::UTXO; +/// Defines the type of signature required for an input. #[derive(Debug, Clone)] pub enum RequiredSignature { + /// No signature is required. None, + /// A standard Native ECDSA (WPKH) signature is required. NativeEcdsa, + /// A generic witness payload associated with an external name. Witness(String), + /// A witness payload requiring traversal through a specified path hierarchy. WitnessWithPath(String, Vec), } impl RequiredSignature { + /// Creates a `WitnessWithPath` requirement using an iterator of path segments. pub fn witness_with_path(name: &str, path: I) -> Self where I: IntoIterator, @@ -29,35 +34,63 @@ impl RequiredSignature { } } +/// Represents partially prepared input data for Elements transactions. #[derive(Debug, Clone)] pub struct PartialInput { + /// The transaction ID containing the target UTXO being spent. pub witness_txid: Txid, + /// The output index of the UTXO within the transaction being spent. pub witness_output_index: u32, + /// The native transaction output corresponding to the targeted UTXO. pub witness_utxo: TxOut, + /// The sequence number indicating transaction replaceability or relative timelocking. pub sequence: Sequence, + /// Absolute timelock criteria enforced against the input. pub locktime: LockTime, - // if utxo is explicit, amount and asset are Some + /// The explicit amount value in Satoshis for the input, if available. + /// Note: if UTXO is explicit, `amount` and `asset` are `Some`. pub amount: Option, + /// The explicit `AssetId` being spent by the input, if available. pub asset: Option, - // if utxo is confidential, secrets are Some + /// Optional blinding secrets mapping values and asset states into confidential outputs. + /// Note: if UTXO is confidential, `secrets` are `Some`. pub secrets: Option, } +/// Represents an input that runs a specific Simplicity program with an associated witness. #[derive(Clone)] pub struct ProgramInput { + /// The compiled program interface associated with the input. pub program: Box, + /// The witness values required to satisfy the program. pub witness: Box, } -#[derive(Clone)] -pub struct IssuanceInput { - pub issuance_amount: u64, - pub asset_entropy: [u8; 32], - pub reissuance_amount: Option, - pub blinding_nonce: Option, +/// Represents an input designated for asset issuance or reissuance. +#[derive(Clone, Debug)] +pub enum IssuanceInput { + /// Represents a completely new asset issuance. + Issuance { + /// The initial issuance amount for the asset. + issuance_amount: u64, + /// The initial issuance amount for the inflation key. + inflation_amount: u64, + /// The contract hash or entropy used to derive the generated `AssetId`. + asset_entropy: [u8; 32], + }, + /// Represents a reissuance of an existing asset. + Reissuance { + /// The amount of the generated asset to issue. + issuance_amount: u64, + /// The original asset's entropy used to tie this reissuance back to the parent issuance. + asset_entropy: [u8; 32], + }, } impl PartialInput { + /// Creates a new `PartialInput` from an existing `UTXO`. + /// Extracts explicit value and asset amounts if available. + #[must_use] pub fn new(utxo: UTXO) -> Self { let amount = match utxo.txout.value { Value::Explicit(value) => Some(value), @@ -80,18 +113,24 @@ impl PartialInput { } } + /// Sets a specific `Sequence` for the input. + #[must_use] pub fn with_sequence(mut self, sequence: Sequence) -> Self { self.sequence = sequence; self } + /// Sets a specific `LockTime` for the input. + #[must_use] pub fn with_locktime(mut self, locktime: LockTime) -> Self { self.locktime = locktime; self } + /// Returns the `OutPoint` corresponding to this input. + #[must_use] pub fn outpoint(&self) -> OutPoint { OutPoint { txid: self.witness_txid, @@ -99,15 +138,17 @@ impl PartialInput { } } + /// Converts this `PartialInput` into a fully formed PSET `Input`. + #[must_use] pub fn to_input(&self) -> Input { let time_locktime = match self.locktime { LockTime::Seconds(value) => Some(value), - _ => None, + LockTime::Blocks(_) => None, }; // zero height locktime is essentially ignored let height_locktime = match self.locktime { LockTime::Blocks(value) => Some(value), - _ => None, + LockTime::Seconds(_) => None, }; Input { @@ -125,39 +166,56 @@ impl PartialInput { } impl ProgramInput { + /// Creates a new `ProgramInput` from a `ProgramTrait` and its associated `WitnessTrait`. + #[must_use] pub fn new(program: Box, witness: Box) -> Self { Self { program, witness } } } impl IssuanceInput { - pub fn new(issuance_amount: u64, asset_entropy: [u8; 32]) -> Self { - Self { + /// Creates a new `IssuanceInput` for creating a new asset issuance. + #[must_use] + pub fn new_issuance(issuance_amount: u64, inflation_amount: u64, asset_entropy: [u8; 32]) -> Self { + Self::Issuance { issuance_amount, + inflation_amount, asset_entropy, - reissuance_amount: None, - blinding_nonce: None, } } - pub fn with_reissuance(mut self, reissuance_amount: u64) -> Self { - self.reissuance_amount = Some(reissuance_amount); - - self - } - - pub fn with_blinding_nonce(mut self, blinding_nonce: [u8; 32]) -> Self { - self.blinding_nonce = Some(Tweak::from_inner(blinding_nonce).expect("valid blinding_nonce")); - - self + /// Creates a new `IssuanceInput` for reissuing an existing asset. + #[must_use] + pub fn new_reissuance(issuance_amount: u64, asset_entropy: [u8; 32]) -> Self { + Self::Reissuance { + issuance_amount, + asset_entropy, + } } + /// Converts this `IssuanceInput` into a partial PSET `Input` configured for issuance or reissuance. + #[must_use] pub fn to_input(&self) -> Input { + let (issuance_amount, asset_entropy, inflation_amount) = match self { + Self::Issuance { + issuance_amount, + inflation_amount, + asset_entropy, + } => { + let inflation_amount = (*inflation_amount > 0).then_some(*inflation_amount); + + (*issuance_amount, *asset_entropy, inflation_amount) + } + Self::Reissuance { + issuance_amount, + asset_entropy, + } => (*issuance_amount, *asset_entropy, None), + }; + Input { - issuance_value_amount: Some(self.issuance_amount), - issuance_asset_entropy: Some(self.asset_entropy), - issuance_inflation_keys: self.reissuance_amount, - issuance_blinding_nonce: self.blinding_nonce, + issuance_value_amount: Some(issuance_amount), + issuance_asset_entropy: Some(asset_entropy), + issuance_inflation_keys: inflation_amount, blinded_issuance: Some(0x00), ..Default::default() } diff --git a/crates/sdk/src/transaction/partial_output.rs b/crates/sdk/src/transaction/partial_output.rs index 0b9ddfe..221f39a 100644 --- a/crates/sdk/src/transaction/partial_output.rs +++ b/crates/sdk/src/transaction/partial_output.rs @@ -3,15 +3,22 @@ use elements_miniscript::bitcoin::PublicKey; use simplicityhl::elements::pset::Output; use simplicityhl::elements::{AssetId, Script}; +/// Represents partially prepared output data for Elements transactions. #[derive(Debug, Clone)] pub struct PartialOutput { + /// Represents a bound `ScriptPubKey` for arbitrary output. pub script_pubkey: Script, + /// Amount of a certain transaction output pub amount: u64, + /// Amount of a certain transaction output pub asset: AssetId, + /// Public key of a blinding key pub blinding_key: Option, } impl PartialOutput { + /// Creates a new `PartialOutput` assigning a base script, amount, and `AssetId`. + #[must_use] pub fn new(script: Script, amount: u64, asset: AssetId) -> Self { Self { script_pubkey: script, @@ -21,6 +28,8 @@ impl PartialOutput { } } + /// Creates a new `PartialOutput` representing an `OP_RETURN` data metadata output. + #[must_use] pub fn new_metadata(data: &[u8]) -> Self { Self { script_pubkey: Script::new_op_return(data), @@ -30,12 +39,16 @@ impl PartialOutput { } } + /// Attaches an optional blinding public key to the partial output. + #[must_use] pub fn with_blinding_key(mut self, blinding_key: PublicKey) -> Self { self.blinding_key = Some(blinding_key); self } + /// Converts this `PartialOutput` into a fully formed PSET `Output`. + #[must_use] pub fn to_output(&self) -> Output { let mut output = Output::new_explicit(self.script_pubkey.clone(), self.amount, self.asset, self.blinding_key); diff --git a/crates/sdk/src/transaction/tx_receipt.rs b/crates/sdk/src/transaction/tx_receipt.rs new file mode 100644 index 0000000..9b9b270 --- /dev/null +++ b/crates/sdk/src/transaction/tx_receipt.rs @@ -0,0 +1,53 @@ +use std::fmt; +use std::fmt::{Debug, Display, Formatter}; + +use simplicityhl::elements::Txid; + +use crate::provider::{ProviderError, ProviderTrait}; + +/// A receipt for a broadcast transaction, containing the provider context and the transaction ID. +#[derive(Clone, Copy)] +pub struct TxReceipt<'a> { + provider: &'a dyn ProviderTrait, + tx_id: Txid, +} + +impl Display for TxReceipt<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.tx_id, f) + } +} + +impl Debug for TxReceipt<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.tx_id, f) + } +} + +impl AsRef for TxReceipt<'_> { + fn as_ref(&self) -> &Txid { + &self.tx_id + } +} + +impl<'a> TxReceipt<'a> { + /// Creates a new `TxReceipt` associated with a specific provider and transaction ID. + pub fn new(provider: &'a dyn ProviderTrait, tx_id: Txid) -> Self { + Self { provider, tx_id } + } + + /// Returns the ID of the broadcasted transaction. + #[must_use] + pub fn txid(self) -> Txid { + self.tx_id + } + + /// Blocks and waits for the transaction to be confirmed by the provider. + /// + /// # Errors + /// Returns a `ProviderError` if the provider encounters an error while tracking the transaction state. + #[inline] + pub fn wait(&self) -> Result<(), ProviderError> { + self.provider.wait(&self.tx_id) + } +} diff --git a/crates/sdk/src/transaction/utxo.rs b/crates/sdk/src/transaction/utxo.rs index 3dad2bb..d4423bc 100644 --- a/crates/sdk/src/transaction/utxo.rs +++ b/crates/sdk/src/transaction/utxo.rs @@ -1,26 +1,70 @@ use simplicityhl::elements::{AssetId, OutPoint, TxOut, TxOutSecrets}; +/// Represents an Unspent Transaction Output (UTXO). #[derive(Debug, Clone)] pub struct UTXO { + /// Bounded outpoint for this object pub outpoint: OutPoint, + /// Transaction output characteristics pub txout: TxOut, + /// Already unblinded transaction output secrets pub secrets: Option, } impl UTXO { + /// Retrieves the explicit `AssetId` from the transaction output (`txout`). + /// + /// # Panics + /// This function will panic if the UTXO's asset is confidential. + #[must_use] pub fn explicit_asset(&self) -> AssetId { self.txout.asset.explicit().expect("The UTXO's asset is not explicit") } + /// Retrieves the explicit amount contained within the transaction output (UTXO). + /// + /// # Panics + /// This function will panic if the UTXO's amount is confidential. + #[must_use] pub fn explicit_amount(&self) -> u64 { self.txout.value.explicit().expect("The UTXO's amount is not explicit") } + /// Retrieves the unblinded `AssetId` of the current UTXO. + /// + /// # Panics + /// + /// This function will panic if the UTXO is not blinded. The panic occurs when + /// `self.secrets` is `None`, as it expects the UTXO to be in an unblinded state to retrieve the `AssetId`. + #[must_use] pub fn unblinded_asset(&self) -> AssetId { self.secrets.expect("The UTXO is not unblinded").asset } + /// Retrieves the unblinded amount from the UTXO. + /// + /// # Panics + /// This function will panic if the UTXO is not confidential. + #[must_use] pub fn unblinded_amount(&self) -> u64 { self.secrets.expect("The UTXO is not unblinded").value } + + /// Retrieves the `AssetId` associated with the instance. + #[must_use] + pub fn asset(&self) -> AssetId { + self.secrets + .map_or_else(|| self.explicit_asset(), |secrets| secrets.asset) + } + + /// Retrieves the amount associated with the current instance. + /// + /// This function returns the `value` from the `secrets` field if it exists. + /// If no `secrets` are present, it falls back to calculating and returning + /// the explicitly defined amount using the `explicit_amount()` method. + #[must_use] + pub fn amount(&self) -> u64 { + self.secrets + .map_or_else(|| self.explicit_amount(), |secrets| secrets.value) + } } diff --git a/crates/sdk/src/utils.rs b/crates/sdk/src/utils.rs index cf110b0..ad596e1 100644 --- a/crates/sdk/src/utils.rs +++ b/crates/sdk/src/utils.rs @@ -8,12 +8,22 @@ use simplicityhl::simplicity::bitcoin; use simplicityhl::simplicity::bitcoin::secp256k1; use simplicityhl::simplicity::hashes::{Hash, sha256}; +/// Generates a radom menemonic with 12 words. +/// +/// # Panics +/// Panics if the underlying mnemonic generation fails (e.g. invalid word count configuration). +#[must_use] pub fn random_mnemonic() -> String { let mnemonic = Mnemonic::generate(12).expect("word count should be valid"); - format!("{}", mnemonic) + mnemonic.to_string() } +/// Generates a hardcoded "unspendable" Taproot public key. +/// +/// # Panics +/// Panics if the hardcoded byte slice is not exactly 32 bytes or cannot be parsed into a valid `XOnlyPublicKey` (which should never happen statically). +#[must_use] pub fn tr_unspendable_key() -> secp256k1::XOnlyPublicKey { secp256k1::XOnlyPublicKey::from_slice(&[ 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e, 0x07, 0x8a, @@ -22,11 +32,15 @@ pub fn tr_unspendable_key() -> secp256k1::XOnlyPublicKey { .expect("key should be valid") } +/// Calculates new sha256 midstate and binds generated entropy to the specific outpoint. +#[must_use] pub fn asset_entropy(outpoint: &OutPoint, entropy: [u8; 32]) -> sha256::Midstate { let contract_hash = ContractHash::from_byte_array(entropy); AssetId::generate_asset_entropy(*outpoint, contract_hash) } +/// Hashes arbitrary data with and additional `TapData` and tags. +#[must_use] pub fn tap_data_hash(data: &[u8]) -> sha256::Hash { let tag = sha256::Hash::hash(b"TapData"); @@ -38,6 +52,8 @@ pub fn tap_data_hash(data: &[u8]) -> sha256::Hash { sha256::Hash::from_engine(eng) } +/// Computes the SHA-256 hash of a given script and returns the resulting 32-byte array. +#[must_use] pub fn hash_script(script: &Script) -> [u8; 32] { let mut hasher = Sha256::new(); @@ -45,10 +61,14 @@ pub fn hash_script(script: &Script) -> [u8; 32] { hasher.finalize().into() } +/// Converts Satoshi amount into BTC. +#[must_use] pub fn sat2btc(sat: u64) -> f64 { bitcoin::Amount::from_sat(sat).to_btc() } +/// Converts BTC amount into Satoshi. +#[must_use] pub fn btc2sat(btc: u64) -> u64 { bitcoin::Amount::from_int_btc(btc).to_sat() } @@ -59,6 +79,6 @@ mod tests { #[test] fn generates_mnemonic() { - random_mnemonic(); + let _ = random_mnemonic(); } } diff --git a/crates/simplex/Cargo.toml b/crates/simplex/Cargo.toml index f4b7730..88e2014 100644 --- a/crates/simplex/Cargo.toml +++ b/crates/simplex/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "smplx-std" -version = "0.0.4" +version = "0.0.5" description = "A blazingly-fast, ux-first simplicity development framework" rust-version = "1.91.0" license.workspace = true diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index f968e44..2b6e21d 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "smplx-test" -version = "0.0.4" +version = "0.0.5" description = "Simplex test command internal implementation" license.workspace = true edition.workspace = true diff --git a/crates/test/src/config.rs b/crates/test/src/config.rs index 1cc8a03..f410c87 100644 --- a/crates/test/src/config.rs +++ b/crates/test/src/config.rs @@ -7,6 +7,7 @@ use std::path::Path; use serde::{Deserialize, Serialize}; use smplx_regtest::RegtestConfig; +use smplx_sdk::program::TrackerLogLevel; use super::error::TestError; @@ -21,6 +22,7 @@ pub struct TestConfig { pub bitcoins: u64, pub esplora: Option, pub rpc: Option, + pub verbosity: Option, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -36,6 +38,23 @@ pub struct RpcConfig { pub password: String, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Verbosity(pub u64); + +impl TryInto for Verbosity { + type Error = Self; + + fn try_into(self) -> Result { + match self { + Verbosity(1) => Ok(TrackerLogLevel::None), + Verbosity(2) => Ok(TrackerLogLevel::Warning), + Verbosity(3) => Ok(TrackerLogLevel::Debug), + Verbosity(4) => Ok(TrackerLogLevel::Trace), + _ => Err(self), + } + } +} + impl TestConfig { pub fn from_file(path: impl AsRef) -> Result { let mut content = String::new(); @@ -78,6 +97,7 @@ impl Default for TestConfig { bitcoins: DEFAULT_BITCOINS, esplora: None, rpc: None, + verbosity: Some(Verbosity(3)), } } } diff --git a/crates/test/src/context.rs b/crates/test/src/context.rs index 9d288e5..1f3bacb 100644 --- a/crates/test/src/context.rs +++ b/crates/test/src/context.rs @@ -5,12 +5,16 @@ use electrsd::bitcoind::bitcoincore_rpc::Auth; use smplx_regtest::Regtest; use smplx_regtest::client::RegtestClient; -use smplx_sdk::provider::{EsploraProvider, ProviderInfo, ProviderTrait, SimplexProvider, SimplicityNetwork}; +use smplx_sdk::global::set_global_config; +use smplx_sdk::provider::{ + ElementsRpc, EsploraProvider, ProviderInfo, ProviderTrait, SimplexProvider, SimplicityNetwork, +}; use smplx_sdk::signer::Signer; use smplx_sdk::utils::random_mnemonic; use crate::config::TestConfig; use crate::error::TestError; +use crate::network_utils::NetworkUtils; #[allow(dead_code)] pub struct TestContext { @@ -25,6 +29,15 @@ impl TestContext { pub fn new(config_path: PathBuf) -> Result { let config = TestConfig::from_file(&config_path)?; + // error is ignored because we assume that all tests use the same verbosity + let _ = set_global_config( + config + .verbosity + .expect("This will be set") + .try_into() + .expect("Validated in CLI"), + ); + let (signer, provider_info, client) = Self::setup(&config)?; Ok(Self { @@ -75,6 +88,24 @@ impl TestContext { self.signer.get_provider().get_network() } + pub fn get_network_utils(&self) -> NetworkUtils { + assert!( + self._client.is_some(), + "Network utils only available in Regtest network" + ); + + let regtest_rpc = ElementsRpc::new( + self._provider_info.elements_url.clone().unwrap(), + self._provider_info.auth.clone().unwrap(), + ) + .expect("Failed to create rpc client for network utils"); + + let network = self.get_network(); + let esplora = EsploraProvider::new(self._provider_info.esplora_url.clone(), *network); + + NetworkUtils::new(regtest_rpc, esplora) + } + fn setup(config: &TestConfig) -> Result<(Signer, ProviderInfo, Option), TestError> { let client: Option; let provider_info: ProviderInfo; @@ -121,7 +152,7 @@ impl TestContext { }, None => { // simplex inner network - let (regtest_client, regtest_signer) = Regtest::from_config(config.to_regtest_config())?; + let (regtest_client, regtest_signer) = Regtest::from_config(&config.to_regtest_config())?; provider_info = ProviderInfo { esplora_url: regtest_client.esplora_url(), diff --git a/crates/test/src/error.rs b/crates/test/src/error.rs index 23ea154..dcb5214 100644 --- a/crates/test/src/error.rs +++ b/crates/test/src/error.rs @@ -20,4 +20,16 @@ pub enum TestError { #[error("Network name should either be `Liquid`, `LiquidTestnet` or `ElementsRegtest`, got: {0}")] BadNetworkName(String), + + #[error("Occurred a network utils execution error: '{0}'")] + NetworkUtilsExecution(#[from] NetworkUtilsError), +} + +#[derive(thiserror::Error, Debug)] +pub enum NetworkUtilsError { + #[error(transparent)] + Provider(#[from] ProviderError), + + #[error("Unsuccessful action completion, err: '{0}'")] + UnsuccessfulSync(String), } diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index d071b51..2725aa8 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -2,6 +2,8 @@ pub mod config; pub mod context; pub mod error; pub mod macros; +pub mod network_utils; pub use config::{RpcConfig, TEST_ENV_NAME, TestConfig}; pub use macros::core::SMPLX_TEST_MARKER; +pub use network_utils::NetworkUtils; diff --git a/crates/test/src/macros/core.rs b/crates/test/src/macros/core.rs index 81c56d3..8541145 100644 --- a/crates/test/src/macros/core.rs +++ b/crates/test/src/macros/core.rs @@ -36,7 +36,7 @@ fn expand_inner(input: &syn::ItemFn, _args: AttributeArgs) -> syn::Result { + Err(_) => { panic!("Failed to run this test, required to use `simplex test`"); }, Ok(path) => { diff --git a/crates/test/src/network_utils.rs b/crates/test/src/network_utils.rs new file mode 100644 index 0000000..da48498 --- /dev/null +++ b/crates/test/src/network_utils.rs @@ -0,0 +1,41 @@ +use smplx_sdk::provider::{ElementsRpc, EsploraProvider, ProviderError, ProviderTrait}; + +use crate::error::NetworkUtilsError; + +pub struct NetworkUtils { + rpc: ElementsRpc, + esplora: EsploraProvider, +} + +impl NetworkUtils { + pub fn new(rpc: ElementsRpc, esplora: EsploraProvider) -> Self { + Self { rpc, esplora } + } + + pub fn mine_until_height(&self, target_height: u64) -> Result<(), NetworkUtilsError> { + let current_height = self.rpc.height().map_err(ProviderError::from)?; + + if current_height < target_height { + let blocks_to_mine = target_height - current_height; + + self.rpc.generate_blocks(blocks_to_mine).map_err(ProviderError::from)?; + + let mut h = 0; + for _ in 0..50 { + h = self.esplora.fetch_tip_height()? as u64; + + if h >= target_height { + return Ok(()); + } + + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + return Err(NetworkUtilsError::UnsuccessfulSync(format!( + "Failed to complete mining until height, got: '{h}', desired height: '{current_height}'", + ))); + } + + Ok(()) + } +} diff --git a/deny.toml b/deny.toml index 36b404b..8960e0f 100644 --- a/deny.toml +++ b/deny.toml @@ -102,6 +102,7 @@ allow = [ "Zlib", "MITNFA", "BSD-3-Clause", + "CDLA-Permissive-2.0", ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the diff --git a/docs/simplex_logo.png b/docs/simplex_logo.png new file mode 100644 index 0000000..643e539 Binary files /dev/null and b/docs/simplex_logo.png differ diff --git a/examples/basic/Cargo.lock b/examples/basic/Cargo.lock index 9e65784..924343a 100644 --- a/examples/basic/Cargo.lock +++ b/examples/basic/Cargo.lock @@ -1201,9 +1201,9 @@ dependencies = [ [[package]] name = "simplicityhl" -version = "0.5.0-rc.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8fb4ae422c730ec6e274c1438013ce6d7b41b97152e2dec63735e8daad1f1c4" +checksum = "25de8990174fe3e1a843df138cacc4265d05839ebd2550c18b9196f567d55e81" dependencies = [ "base64 0.21.7", "chumsky", @@ -1219,7 +1219,7 @@ dependencies = [ [[package]] name = "smplx-build" -version = "0.0.4" +version = "0.0.5" dependencies = [ "glob", "globwalk", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "smplx-macros" -version = "0.0.4" +version = "0.0.5" dependencies = [ "smplx-build", "smplx-test", @@ -1245,7 +1245,7 @@ dependencies = [ [[package]] name = "smplx-regtest" -version = "0.0.4" +version = "0.0.5" dependencies = [ "electrsd", "hex", @@ -1259,7 +1259,7 @@ dependencies = [ [[package]] name = "smplx-sdk" -version = "0.0.4" +version = "0.0.5" dependencies = [ "bip39", "bitcoin_hashes", @@ -1277,7 +1277,7 @@ dependencies = [ [[package]] name = "smplx-std" -version = "0.0.4" +version = "0.0.5" dependencies = [ "either", "serde", @@ -1289,7 +1289,7 @@ dependencies = [ [[package]] name = "smplx-test" -version = "0.0.4" +version = "0.0.5" dependencies = [ "electrsd", "proc-macro2", diff --git a/examples/basic/Simplex.toml b/examples/basic/Simplex.toml index b6e701e..3d71376 100644 --- a/examples/basic/Simplex.toml +++ b/examples/basic/Simplex.toml @@ -7,9 +7,16 @@ # [regtest] # mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" +# bitcoins = 10_000_000 +# rpc_port = 18443 +# esplora_port = 3000 +# rpc_user = "user" +# rpc_password = "password" # [test] # mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" +# bitcoins = 10_000_000 +# verbosity = 3 # [test.esplora] # url = "" diff --git a/examples/basic/src/lib.rs b/examples/basic/src/lib.rs index 91946c7..b5efd88 100644 --- a/examples/basic/src/lib.rs +++ b/examples/basic/src/lib.rs @@ -1 +1,3 @@ +#![warn(clippy::all, clippy::pedantic)] + pub mod artifacts; diff --git a/examples/basic/tests/basic_test.rs b/examples/basic/tests/basic_test.rs index bf73a75..9110dc2 100644 --- a/examples/basic/tests/basic_test.rs +++ b/examples/basic/tests/basic_test.rs @@ -1,7 +1,7 @@ -use simplex::simplicityhl::elements::{Script, Txid}; +use simplex::simplicityhl::elements::Script; use simplex::constants::DUMMY_SIGNATURE; -use simplex::transaction::{FinalTransaction, PartialInput, ProgramInput, RequiredSignature}; +use simplex::transaction::{FinalTransaction, PartialInput, ProgramInput, RequiredSignature, TxReceipt}; use simplex_example::artifacts::p2pk::P2pkProgram; use simplex_example::artifacts::p2pk::derived_p2pk::{P2pkArguments, P2pkWitness}; @@ -19,26 +19,24 @@ fn get_p2pk(context: &simplex::TestContext) -> (P2pkProgram, Script) { (p2pk, p2pk_script) } -fn spend_p2wpkh(context: &simplex::TestContext) -> anyhow::Result { +fn spend_p2wpkh(context: &simplex::TestContext) -> anyhow::Result> { let signer = context.get_default_signer(); let (_, p2pk_script) = get_p2pk(context); - let txid = signer.send(p2pk_script.clone(), 50)?; - println!("Broadcast: {}", txid); + let tx_receipt = signer.send(p2pk_script.clone(), 50)?; + println!("Broadcast: {}", tx_receipt); - Ok(txid) + Ok(tx_receipt) } -fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result { +fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result> { let signer = context.get_default_signer(); let provider = context.get_default_provider(); let (p2pk, p2pk_script) = get_p2pk(context); - let mut p2pk_utxos = provider.fetch_scripthash_utxos(&p2pk_script)?; - - p2pk_utxos.retain(|utxo| utxo.explicit_asset() == context.get_network().policy_asset()); + let p2pk_utxos = provider.fetch_scripthash_utxos(&p2pk_script)?; let mut ft = FinalTransaction::new(); @@ -52,24 +50,22 @@ fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result { RequiredSignature::Witness("SIGNATURE".to_string()), ); - let txid = signer.broadcast(&ft)?; - println!("Broadcast: {}", txid); + let tx_receipt = signer.broadcast(&ft)?; + println!("Broadcast: {}", tx_receipt); - Ok(txid) + Ok(tx_receipt) } #[simplex::test] fn basic_test(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - - let txid = spend_p2wpkh(&context)?; + let tx_receipt = spend_p2wpkh(&context)?; - provider.wait(&txid)?; + tx_receipt.wait()?; println!("Confirmed"); - let txid = spend_p2pk(&context)?; + let tx_receipt = spend_p2pk(&context)?; - provider.wait(&txid)?; + tx_receipt.wait()?; println!("Confirmed"); Ok(()) diff --git a/examples/basic/tests/confidential_test.rs b/examples/basic/tests/confidential_test.rs index ab6c68a..f5bbab2 100644 --- a/examples/basic/tests/confidential_test.rs +++ b/examples/basic/tests/confidential_test.rs @@ -1,10 +1,10 @@ -use simplex::simplicityhl::elements::{AssetId, Txid}; +use simplex::simplicityhl::elements::AssetId; use simplex::signer::Signer; use simplex::transaction::partial_input::IssuanceInput; -use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature}; +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, TxReceipt}; -fn make_confidential_to_bob(alice: &Signer, bob: &Signer, asset: AssetId) -> anyhow::Result { +fn make_confidential_to_bob<'a>(alice: &'a Signer, bob: &Signer, asset: AssetId) -> anyhow::Result> { let mut ft = FinalTransaction::new(); ft.add_output( @@ -12,38 +12,40 @@ fn make_confidential_to_bob(alice: &Signer, bob: &Signer, asset: AssetId) -> any .with_blinding_key(bob.get_blinding_public_key()), ); - let txid = alice.broadcast(&ft)?; - println!("Broadcast: {}", txid); + let tx_receipt = alice.broadcast(&ft)?; + println!("Broadcast: {}", tx_receipt); - Ok(txid) + Ok(tx_receipt) } -fn issue_confidential_to_alice(alice: &Signer, bob: &Signer) -> anyhow::Result { +fn issue_confidential_to_alice<'a>(alice: &Signer, bob: &'a Signer) -> anyhow::Result> { let utxos = bob.get_utxos()?; let mut ft = FinalTransaction::new(); - let (issuance_id, reissuance_id) = ft.add_issuance_input( + let issuance_details = ft.add_issuance_input( PartialInput::new(utxos[0].clone()), - IssuanceInput::new(1000, [1u8; 32]) - .with_reissuance(100) - .with_blinding_nonce([1u8; 32]), + IssuanceInput::new_issuance(1000, 100, [1u8; 32]), RequiredSignature::NativeEcdsa, ); ft.add_output( - PartialOutput::new(alice.get_address().script_pubkey(), 1000, issuance_id) + PartialOutput::new(alice.get_address().script_pubkey(), 1000, issuance_details.asset_id) .with_blinding_key(alice.get_blinding_public_key()), ); ft.add_output( - PartialOutput::new(alice.get_address().script_pubkey(), 100, reissuance_id) - .with_blinding_key(alice.get_blinding_public_key()), + PartialOutput::new( + alice.get_address().script_pubkey(), + 100, + issuance_details.inflation_asset_id, + ) + .with_blinding_key(alice.get_blinding_public_key()), ); - let txid = bob.broadcast(&ft)?; - println!("Broadcast: {}", txid); + let tx_receipt = bob.broadcast(&ft)?; + println!("Broadcast: {}", tx_receipt); - Ok(txid) + Ok(tx_receipt) } #[simplex::test] @@ -52,21 +54,21 @@ fn confidential_test(context: simplex::TestContext) -> anyhow::Result<()> { let alice = context.get_default_signer(); let bob = context.random_signer(); - let txid = make_confidential_to_bob(alice, &bob, provider.get_network().policy_asset())?; + let tx_receipt = make_confidential_to_bob(alice, &bob, provider.get_network().policy_asset())?; - provider.wait(&txid)?; + tx_receipt.wait()?; println!("Confirmed"); - let txid = issue_confidential_to_alice(alice, &bob)?; + let tx_receipt = issue_confidential_to_alice(alice, &bob)?; - provider.wait(&txid)?; + tx_receipt.wait()?; println!("Confirmed"); // spend confidential - let txid = bob.send(alice.get_address().script_pubkey(), 50)?; - println!("Broadcast: {}", txid); + let tx_receipt = bob.send(alice.get_address().script_pubkey(), 50)?; + println!("Broadcast: {}", tx_receipt); - provider.wait(&txid)?; + tx_receipt.wait()?; println!("Confirmed"); Ok(()) diff --git a/fixtures/Cargo.lock b/fixtures/Cargo.lock index f7dbe39..6c622b2 100644 --- a/fixtures/Cargo.lock +++ b/fixtures/Cargo.lock @@ -1201,9 +1201,9 @@ dependencies = [ [[package]] name = "simplicityhl" -version = "0.5.0-rc.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8fb4ae422c730ec6e274c1438013ce6d7b41b97152e2dec63735e8daad1f1c4" +checksum = "25de8990174fe3e1a843df138cacc4265d05839ebd2550c18b9196f567d55e81" dependencies = [ "base64 0.21.7", "chumsky", @@ -1219,7 +1219,7 @@ dependencies = [ [[package]] name = "smplx-build" -version = "0.0.4" +version = "0.0.5" dependencies = [ "glob", "globwalk", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "smplx-macros" -version = "0.0.4" +version = "0.0.5" dependencies = [ "smplx-build", "smplx-test", @@ -1245,7 +1245,7 @@ dependencies = [ [[package]] name = "smplx-regtest" -version = "0.0.4" +version = "0.0.5" dependencies = [ "electrsd", "hex", @@ -1259,7 +1259,7 @@ dependencies = [ [[package]] name = "smplx-sdk" -version = "0.0.4" +version = "0.0.5" dependencies = [ "bip39", "bitcoin_hashes", @@ -1277,7 +1277,7 @@ dependencies = [ [[package]] name = "smplx-std" -version = "0.0.4" +version = "0.0.5" dependencies = [ "either", "serde", @@ -1289,7 +1289,7 @@ dependencies = [ [[package]] name = "smplx-test" -version = "0.0.4" +version = "0.0.5" dependencies = [ "electrsd", "proc-macro2", diff --git a/fixtures/Cargo.toml b/fixtures/Cargo.toml index 17afae3..3f81a15 100644 --- a/fixtures/Cargo.toml +++ b/fixtures/Cargo.toml @@ -5,6 +5,6 @@ rust-version = "1.91.0" version = "0.1.0" [dependencies] -smplx-std = { path = "./../crates/simplex" } +smplx-std = { path = "../crates/simplex" } anyhow = { version = "1.0.101" } diff --git a/fixtures/Simplex.toml b/fixtures/Simplex.toml index b6e701e..3d71376 100644 --- a/fixtures/Simplex.toml +++ b/fixtures/Simplex.toml @@ -7,9 +7,16 @@ # [regtest] # mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" +# bitcoins = 10_000_000 +# rpc_port = 18443 +# esplora_port = 3000 +# rpc_user = "user" +# rpc_password = "password" # [test] # mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" +# bitcoins = 10_000_000 +# verbosity = 3 # [test.esplora] # url = "" diff --git a/fixtures/simf/dummy_panic.simf b/fixtures/simf/dummy_panic.simf new file mode 100644 index 0000000..f86e0b5 --- /dev/null +++ b/fixtures/simf/dummy_panic.simf @@ -0,0 +1,3 @@ +fn main() { + assert!(false); +} \ No newline at end of file diff --git a/fixtures/src/lib.rs b/fixtures/src/lib.rs index 91946c7..b5efd88 100644 --- a/fixtures/src/lib.rs +++ b/fixtures/src/lib.rs @@ -1 +1,3 @@ +#![warn(clippy::all, clippy::pedantic)] + pub mod artifacts; diff --git a/fixtures/tests/basic_test.rs b/fixtures/tests/basic_test.rs index fbbebf2..76f897b 100644 --- a/fixtures/tests/basic_test.rs +++ b/fixtures/tests/basic_test.rs @@ -1,4 +1,4 @@ -use simplex::simplicityhl::elements::{Script, Txid}; +use simplex::simplicityhl::elements::Script; use simplex::constants::DUMMY_SIGNATURE; use simplex::transaction::{FinalTransaction, PartialInput, ProgramInput, RequiredSignature}; @@ -19,26 +19,24 @@ fn get_p2pk(context: &simplex::TestContext) -> (P2pkProgram, Script) { (p2pk, p2pk_script) } -fn spend_p2wpkh(context: &simplex::TestContext) -> anyhow::Result { +fn spend_p2wpkh(context: &simplex::TestContext) -> anyhow::Result<()> { let signer = context.get_default_signer(); let (_, p2pk_script) = get_p2pk(context); - let txid = signer.send(p2pk_script.clone(), 50)?; - println!("Broadcast: {}", txid); + let tx_receipt = signer.send(p2pk_script.clone(), 50)?; + println!("Broadcast: {}", tx_receipt); - Ok(txid) + Ok(()) } -fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result { +fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result<()> { let signer = context.get_default_signer(); let provider = context.get_default_provider(); let (p2pk, p2pk_script) = get_p2pk(context); - let mut p2pk_utxos = provider.fetch_scripthash_utxos(&p2pk_script)?; - - p2pk_utxos.retain(|utxo| utxo.explicit_asset() == context.get_network().policy_asset()); + let p2pk_utxos = provider.fetch_scripthash_utxos(&p2pk_script)?; let mut ft = FinalTransaction::new(); @@ -52,25 +50,16 @@ fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result { RequiredSignature::Witness("SIGNATURE".to_string()), ); - let txid = signer.broadcast(&ft)?; - println!("Broadcast: {}", txid); + let tx_receipt = signer.broadcast(&ft)?; + println!("Broadcast: {}", tx_receipt); - Ok(txid) + Ok(()) } #[simplex::test] fn basic_test(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - - let txid = spend_p2wpkh(&context)?; - - provider.wait(&txid)?; - println!("Confirmed"); - - let txid = spend_p2pk(&context)?; - - provider.wait(&txid)?; - println!("Confirmed"); + spend_p2wpkh(&context)?; + spend_p2pk(&context)?; Ok(()) } diff --git a/fixtures/tests/confidential_test.rs b/fixtures/tests/confidential_test.rs index ab6c68a..5e1784d 100644 --- a/fixtures/tests/confidential_test.rs +++ b/fixtures/tests/confidential_test.rs @@ -1,10 +1,10 @@ -use simplex::simplicityhl::elements::{AssetId, Txid}; +use simplex::simplicityhl::elements::AssetId; use simplex::signer::Signer; use simplex::transaction::partial_input::IssuanceInput; use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature}; -fn make_confidential_to_bob(alice: &Signer, bob: &Signer, asset: AssetId) -> anyhow::Result { +fn make_confidential_to_bob(alice: &Signer, bob: &Signer, asset: AssetId) -> anyhow::Result<()> { let mut ft = FinalTransaction::new(); ft.add_output( @@ -12,38 +12,40 @@ fn make_confidential_to_bob(alice: &Signer, bob: &Signer, asset: AssetId) -> any .with_blinding_key(bob.get_blinding_public_key()), ); - let txid = alice.broadcast(&ft)?; - println!("Broadcast: {}", txid); + let tx_receipt = alice.broadcast(&ft)?; + println!("Broadcast: {}", tx_receipt); - Ok(txid) + Ok(()) } -fn issue_confidential_to_alice(alice: &Signer, bob: &Signer) -> anyhow::Result { +fn issue_confidential_to_alice(alice: &Signer, bob: &Signer) -> anyhow::Result<()> { let utxos = bob.get_utxos()?; let mut ft = FinalTransaction::new(); - let (issuance_id, reissuance_id) = ft.add_issuance_input( + let issuance_details = ft.add_issuance_input( PartialInput::new(utxos[0].clone()), - IssuanceInput::new(1000, [1u8; 32]) - .with_reissuance(100) - .with_blinding_nonce([1u8; 32]), + IssuanceInput::new_issuance(1000, 100, [1u8; 32]), RequiredSignature::NativeEcdsa, ); ft.add_output( - PartialOutput::new(alice.get_address().script_pubkey(), 1000, issuance_id) + PartialOutput::new(alice.get_address().script_pubkey(), 1000, issuance_details.asset_id) .with_blinding_key(alice.get_blinding_public_key()), ); ft.add_output( - PartialOutput::new(alice.get_address().script_pubkey(), 100, reissuance_id) - .with_blinding_key(alice.get_blinding_public_key()), + PartialOutput::new( + alice.get_address().script_pubkey(), + 100, + issuance_details.inflation_asset_id, + ) + .with_blinding_key(alice.get_blinding_public_key()), ); - let txid = bob.broadcast(&ft)?; - println!("Broadcast: {}", txid); + let tx_receipt = bob.broadcast(&ft)?; + println!("Broadcast: {}", tx_receipt); - Ok(txid) + Ok(()) } #[simplex::test] @@ -52,22 +54,12 @@ fn confidential_test(context: simplex::TestContext) -> anyhow::Result<()> { let alice = context.get_default_signer(); let bob = context.random_signer(); - let txid = make_confidential_to_bob(alice, &bob, provider.get_network().policy_asset())?; - - provider.wait(&txid)?; - println!("Confirmed"); - - let txid = issue_confidential_to_alice(alice, &bob)?; - - provider.wait(&txid)?; - println!("Confirmed"); + make_confidential_to_bob(alice, &bob, provider.get_network().policy_asset())?; + issue_confidential_to_alice(alice, &bob)?; // spend confidential - let txid = bob.send(alice.get_address().script_pubkey(), 50)?; - println!("Broadcast: {}", txid); - - provider.wait(&txid)?; - println!("Confirmed"); + let tx_receipt = bob.send(alice.get_address().script_pubkey(), 50)?; + println!("Broadcast: {}", tx_receipt); Ok(()) } diff --git a/fixtures/tests/ignore_default_tests.rs b/fixtures/tests/ignore_default_tests.rs index cea22d8..f600787 100644 --- a/fixtures/tests/ignore_default_tests.rs +++ b/fixtures/tests/ignore_default_tests.rs @@ -2,7 +2,6 @@ #[cfg(test)] mod test { - #[simplex::test] fn smplx_test_invoked(_: simplex::TestContext) -> anyhow::Result<()> { Ok(()) diff --git a/fixtures/tests/log_level.rs b/fixtures/tests/log_level.rs new file mode 100644 index 0000000..ed174f1 --- /dev/null +++ b/fixtures/tests/log_level.rs @@ -0,0 +1,42 @@ +use simplex::transaction::{FinalTransaction, PartialInput, ProgramInput, RequiredSignature}; + +use simplex_fixtures::artifacts::dummy_panic::DummyPanicProgram; +use simplex_fixtures::artifacts::dummy_panic::derived_dummy_panic::{DummyPanicArguments, DummyPanicWitness}; + +fn setup_dummy(context: &simplex::TestContext) -> (DummyPanicProgram, simplex::simplicityhl::elements::Script) { + let signer = context.get_default_signer(); + + let dummy = DummyPanicProgram::new(DummyPanicArguments {}).with_taproot_pubkey(signer.get_schnorr_public_key()); + + let script = dummy.get_script_pubkey(context.get_network()); + + (dummy, script) +} + +#[simplex::test] +fn dummy_log_level(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let (dummy, script) = setup_dummy(&context); + + let tx_receipt = signer.send(script.clone(), 50)?; + println!("Funded dummy script: {}", tx_receipt); + + let utxos = provider.fetch_scripthash_utxos(&script)?; + + let mut ft = FinalTransaction::new(); + + ft.add_program_input( + PartialInput::new(utxos[0].clone()), + ProgramInput::new(Box::new(dummy.as_ref().clone()), Box::new(DummyPanicWitness {})), + RequiredSignature::None, + ); + + let result = signer.broadcast(&ft); + + assert!(result.is_err(), "expected assert!(false) program to fail execution"); + println!("{}", result.err().unwrap()); + + Ok(()) +} diff --git a/fixtures/tests/nested_sig.rs b/fixtures/tests/nested_sig.rs index 7ce792a..d3f6e0b 100644 --- a/fixtures/tests/nested_sig.rs +++ b/fixtures/tests/nested_sig.rs @@ -1,5 +1,5 @@ use simplex::constants::DUMMY_SIGNATURE; -use simplex::simplicityhl::elements::{Script, Txid}; +use simplex::simplicityhl::elements::Script; use simplex::transaction::{FinalTransaction, PartialInput, ProgramInput, RequiredSignature}; use simplex_fixtures::artifacts::nested_sig::NestedSigProgram; @@ -18,21 +18,21 @@ fn get_nested_sig(context: &simplex::TestContext) -> (NestedSigProgram, Script) (program, script) } -fn fund_nested_sig(context: &simplex::TestContext) -> anyhow::Result { +fn fund_nested_sig(context: &simplex::TestContext) -> anyhow::Result<()> { let signer = context.get_default_signer(); let (_, script) = get_nested_sig(context); - let txid = signer.send(script, 50_000)?; - println!("Funded: {}", txid); + let tx_receipt = signer.send(script, 50_000)?; + println!("Funded: {}", tx_receipt); - Ok(txid) + Ok(()) } fn spend_nested_sig( context: &simplex::TestContext, witness: NestedSigWitness, sig_path: &[&str], -) -> anyhow::Result { +) -> anyhow::Result<()> { let signer = context.get_default_signer(); let provider = context.get_default_provider(); @@ -48,65 +48,50 @@ fn spend_nested_sig( RequiredSignature::witness_with_path("INHERIT_OR_NOT", sig_path), ); - let txid = signer.broadcast(&ft)?; - println!("Broadcast: {}", txid); + let tx_receipt = signer.broadcast(&ft)?; + println!("Broadcast: {}", tx_receipt); - Ok(txid) + Ok(()) } #[simplex::test] fn test_inherit_spend(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - - let fund_tx = fund_nested_sig(&context)?; - provider.wait(&fund_tx)?; + fund_nested_sig(&context)?; // Left - inheritor sig injected by signer at path L let witness = NestedSigWitness { inherit_or_not: simplex::either::Either::Left((DUMMY_SIGNATURE, [0; 32])), }; - let spend_tx = spend_nested_sig(&context, witness, &["Left", "0"])?; - provider.wait(&spend_tx)?; - println!("Inherit spend confirmed"); + spend_nested_sig(&context, witness, &["Left", "0"])?; Ok(()) } #[simplex::test] fn test_cold_spend(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - - let fund_tx = fund_nested_sig(&context)?; - provider.wait(&fund_tx)?; + fund_nested_sig(&context)?; // Right Left - cold sig injected by signer at path R L let witness = NestedSigWitness { inherit_or_not: simplex::either::Either::Right(simplex::either::Either::Left(DUMMY_SIGNATURE)), }; - let spend_tx = spend_nested_sig(&context, witness, &["Right", "Left"])?; - provider.wait(&spend_tx)?; - println!("Cold spend confirmed"); + spend_nested_sig(&context, witness, &["Right", "Left"])?; Ok(()) } #[simplex::test] fn test_hot_spend(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - - let fund_tx = fund_nested_sig(&context)?; - provider.wait(&fund_tx)?; + fund_nested_sig(&context)?; // Right Right - hot sig injected by signer at path R R let witness = NestedSigWitness { inherit_or_not: simplex::either::Either::Right(simplex::either::Either::Right([DUMMY_SIGNATURE, [0; 64]])), }; - let spend_tx = spend_nested_sig(&context, witness, &["Right", "Right", "0"])?; - provider.wait(&spend_tx)?; - println!("Hot spend confirmed"); + spend_nested_sig(&context, witness, &["Right", "Right", "0"])?; Ok(()) } diff --git a/fixtures/tests/network_utils.rs b/fixtures/tests/network_utils.rs new file mode 100644 index 0000000..e88dd89 --- /dev/null +++ b/fixtures/tests/network_utils.rs @@ -0,0 +1,14 @@ +#[simplex::test] +fn test_blocks_mining(context: simplex::TestContext) -> anyhow::Result<()> { + const DESIRED_HEIGHT: u64 = 1_234; + + let network_utils = context.get_network_utils(); + network_utils.mine_until_height(DESIRED_HEIGHT)?; + + assert_eq!( + DESIRED_HEIGHT, + context.get_default_provider().fetch_tip_height()? as u64 + ); + + Ok(()) +} diff --git a/fixtures/tests/reissuance_test.rs b/fixtures/tests/reissuance_test.rs new file mode 100644 index 0000000..ea79b90 --- /dev/null +++ b/fixtures/tests/reissuance_test.rs @@ -0,0 +1,122 @@ +use simplex::simplicityhl::elements::AssetId; + +use simplex::signer::Signer; +use simplex::transaction::partial_input::IssuanceInput; +use simplex::transaction::{ + FinalTransaction, IssuanceDetails, PartialInput, PartialOutput, RequiredSignature, TxReceipt, +}; + +fn make_confidential_to_bob<'a>(alice: &'a Signer, bob: &Signer, asset: AssetId) -> anyhow::Result> { + let mut ft = FinalTransaction::new(); + + ft.add_output( + PartialOutput::new(bob.get_address().script_pubkey(), 1000, asset) + .with_blinding_key(bob.get_blinding_public_key()), + ); + + let tx_receipt = alice.broadcast(&ft)?; + println!("Broadcast: {}", tx_receipt); + + Ok(tx_receipt) +} + +fn issue_explicit_to_alice_with_reissuance<'a>( + alice: &Signer, + bob: &'a Signer, +) -> anyhow::Result<(TxReceipt<'a>, IssuanceDetails)> { + let utxos = bob.get_utxos()?; + + let mut ft = FinalTransaction::new(); + + let issuance_details = ft.add_issuance_input( + PartialInput::new(utxos[0].clone()), + IssuanceInput::new_issuance(1000, 100, [1u8; 32]), + RequiredSignature::NativeEcdsa, + ); + + ft.add_output(PartialOutput::new( + alice.get_address().script_pubkey(), + 1000, + issuance_details.asset_id, + )); + ft.add_output( + PartialOutput::new( + bob.get_address().script_pubkey(), + 100, + issuance_details.inflation_asset_id, + ) + .with_blinding_key(bob.get_blinding_public_key()), + ); + + let tx_receipt = bob.broadcast(&ft)?; + println!("Broadcast: {}", tx_receipt); + + Ok((tx_receipt, issuance_details)) +} + +fn reissue_tokens_to_bob<'a>( + bob: &'a Signer, + issuance_details: &IssuanceDetails, + reissuance_amount: u64, +) -> anyhow::Result> { + let reissuance_token_utxo = bob.get_utxos_asset(issuance_details.inflation_asset_id)?[0].clone(); + + let mut ft = FinalTransaction::new(); + + ft.add_output( + PartialOutput::new( + bob.get_address().script_pubkey(), + reissuance_token_utxo.unblinded_amount(), + reissuance_token_utxo.unblinded_asset(), + ) + .with_blinding_key(bob.get_blinding_public_key()), + ); + + ft.add_issuance_input( + PartialInput::new(reissuance_token_utxo), + IssuanceInput::new_reissuance(reissuance_amount, issuance_details.asset_entropy.0), + RequiredSignature::NativeEcdsa, + ); + + ft.add_output(PartialOutput::new( + bob.get_address().script_pubkey(), + reissuance_amount, + issuance_details.asset_id, + )); + + let tx_receipt = bob.broadcast(&ft)?; + println!("Broadcast: {}", tx_receipt); + + Ok(tx_receipt) +} + +#[simplex::test] +fn reissuance_test(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let alice = context.get_default_signer(); + let bob = context.random_signer(); + + let tx_receipt = make_confidential_to_bob(alice, &bob, provider.get_network().policy_asset())?; + + tx_receipt.wait()?; + println!("Confirmed"); + + let (tx_receipt, issuance_details) = issue_explicit_to_alice_with_reissuance(alice, &bob)?; + + tx_receipt.wait()?; + println!("Confirmed"); + + let reissuance_amount = 5000; + let tx_receipt = reissue_tokens_to_bob(&bob, &issuance_details, reissuance_amount)?; + println!("Broadcast: {}", tx_receipt); + + tx_receipt.wait()?; + println!("Confirmed"); + + let bob_asset_utxos = bob.get_utxos_asset(issuance_details.asset_id)?; + + assert!(bob_asset_utxos.len() == 1); + assert!(bob_asset_utxos[0].explicit_amount() == reissuance_amount); + + Ok(()) +}