From a5295b7fc7b5c8be1647985da34e2c45ba9af554 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Fri, 24 Apr 2026 18:01:21 +0300 Subject: [PATCH 01/15] fix badge --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f12e31a..f905477 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 From b677a738f81f1d9f619ea848d7e72b3b4612a8e3 Mon Sep 17 00:00:00 2001 From: Illia Kripaka <30872146+ikripaka@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:58:03 +0300 Subject: [PATCH 02/15] smplx_generator: add rustfmt::skip (#63) --- crates/build/src/generator.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/build/src/generator.rs b/crates/build/src/generator.rs index 7260bd5..f5d31fd 100644 --- a/crates/build/src/generator.rs +++ b/crates/build/src/generator.rs @@ -241,6 +241,7 @@ impl ArtifactsGenerator { let code = quote! { #![allow(clippy::all)] + #[rustfmt::skip] #(pub mod #mod_names);*; }; From 2bc3851baefdff31509c5e9de8822d79c9ba757a Mon Sep 17 00:00:00 2001 From: Vitalii Volovyk <161724671+topologoanatom@users.noreply.github.com> Date: Tue, 28 Apr 2026 20:36:50 +0300 Subject: [PATCH 03/15] Log level attribute for `simplex::test` (#61) * impl log level attribute for test macro * add global log level loaded from config * control global logging level via cli command * some refactor * refactor and clean up changes from prev commits * cleanup * readme * fix build --------- Co-authored-by: Artem Chystiakov --- README.md | 2 ++ crates/build/src/generator.rs | 6 ++-- crates/cli/assets/Simplex.default.toml | 1 + crates/cli/src/commands/core.rs | 5 ++- crates/cli/src/commands/test.rs | 8 ++++- crates/cli/src/config/core.rs | 15 ++++++++- crates/cli/src/config/error.rs | 3 ++ crates/sdk/src/global.rs | 30 ++++++++++++++++++ crates/sdk/src/lib.rs | 1 + crates/sdk/src/program/core.rs | 7 +++-- crates/sdk/src/program/mod.rs | 1 + crates/test/src/config.rs | 20 ++++++++++++ crates/test/src/context.rs | 10 ++++++ crates/test/src/macros/core.rs | 2 +- examples/basic/Simplex.toml | 7 +++++ fixtures/Cargo.toml | 2 +- fixtures/Simplex.toml | 7 +++++ fixtures/simf/dummy_panic.simf | 3 ++ fixtures/tests/basic_test.rs | 27 +++++------------ fixtures/tests/confidential_test.rs | 24 +++++---------- fixtures/tests/ignore_default_tests.rs | 1 - fixtures/tests/log_level.rs | 42 ++++++++++++++++++++++++++ fixtures/tests/nested_sig.rs | 37 +++++++---------------- 23 files changed, 188 insertions(+), 73 deletions(-) create mode 100644 crates/sdk/src/global.rs create mode 100644 fixtures/simf/dummy_panic.simf create mode 100644 fixtures/tests/log_level.rs diff --git a/README.md b/README.md index f905477..cfa5772 100644 --- a/README.md +++ b/README.md @@ -67,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 = "" @@ -94,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/src/generator.rs b/crates/build/src/generator.rs index f5d31fd..d044fcf 100644 --- a/crates/build/src/generator.rs +++ b/crates/build/src/generator.rs @@ -241,9 +241,11 @@ impl ArtifactsGenerator { let code = quote! { #![allow(clippy::all)] - #[rustfmt::skip] - #(pub mod #mod_names);*; + #( + #[rustfmt::skip] + pub mod #mod_names; + )* }; Ok(code) 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/commands/core.rs b/crates/cli/src/commands/core.rs index c5a667e..fc3c3ba 100644 --- a/crates/cli/src/commands/core.rs +++ b/crates/cli/src/commands/core.rs @@ -33,7 +33,7 @@ pub struct InitFlags { pub lib: bool, } -#[derive(Debug, Args, Copy, Clone)] +#[derive(Debug, Args, Clone)] pub struct TestFlags { /// Show output from successful tests #[arg(long)] @@ -44,4 +44,7 @@ pub struct TestFlags { /// Run ignored tests #[arg(long)] pub ignored: bool, + /// Log simplicity pruning stack trace + #[arg(short = 'v', long)] + pub verbose: bool, } diff --git a/crates/cli/src/commands/test.rs b/crates/cli/src/commands/test.rs index 2eeaa53..a1499bf 100644 --- a/crates/cli/src/commands/test.rs +++ b/crates/cli/src/commands/test.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use std::process::Stdio; +use smplx_test::config::Verbosity; use smplx_test::{SMPLX_TEST_MARKER, TestConfig}; use super::core::TestFlags; @@ -9,8 +10,13 @@ use super::error::CommandError; pub struct Test {} impl Test { - pub fn run(config: TestConfig, filter: String, flags: &TestFlags) -> Result<(), CommandError> { + pub fn run(mut config: TestConfig, filter: String, 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); diff --git a/crates/cli/src/config/core.rs b/crates/cli/src/config/core.rs index 14caf75..b40e2c5 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; @@ -47,6 +48,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 +67,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/sdk/src/global.rs b/crates/sdk/src/global.rs new file mode 100644 index 0000000..7b5102d --- /dev/null +++ b/crates/sdk/src/global.rs @@ -0,0 +1,30 @@ +use std::sync::OnceLock; + +use crate::program::TrackerLogLevel; + +#[derive(Clone, Copy)] +pub struct GlobalConfig { + log_level: TrackerLogLevel, +} + +impl Default for GlobalConfig { + fn default() -> Self { + Self { + log_level: TrackerLogLevel::Debug, + } + } +} + +static GLOBAL_CONFIG: OnceLock = OnceLock::new(); + +pub fn set_global_config(log_level: TrackerLogLevel) -> Result<(), GlobalConfig> { + GLOBAL_CONFIG.set(GlobalConfig { log_level }) +} + +/// Returns default log level if `GLOBAL_CONFIG` is not initialized +pub fn get_log_level() -> TrackerLogLevel { + GLOBAL_CONFIG + .get() + .map(|config| config.log_level) + .unwrap_or(GlobalConfig::default().log_level) +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 0849165..a0ae82f 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1,4 +1,5 @@ pub mod constants; +pub mod global; pub mod program; pub mod provider; pub mod signer; diff --git a/crates/sdk/src/program/core.rs b/crates/sdk/src/program/core.rs index 87a647e..6815e01 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -10,9 +10,11 @@ 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; @@ -124,8 +126,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)?; diff --git a/crates/sdk/src/program/mod.rs b/crates/sdk/src/program/mod.rs index 6982d16..4cf75c1 100644 --- a/crates/sdk/src/program/mod.rs +++ b/crates/sdk/src/program/mod.rs @@ -6,4 +6,5 @@ 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/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..34fdd55 100644 --- a/crates/test/src/context.rs +++ b/crates/test/src/context.rs @@ -5,6 +5,7 @@ use electrsd::bitcoind::bitcoincore_rpc::Auth; use smplx_regtest::Regtest; use smplx_regtest::client::RegtestClient; +use smplx_sdk::global::set_global_config; use smplx_sdk::provider::{EsploraProvider, ProviderInfo, ProviderTrait, SimplexProvider, SimplicityNetwork}; use smplx_sdk::signer::Signer; use smplx_sdk::utils::random_mnemonic; @@ -25,6 +26,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 { 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/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/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/tests/basic_test.rs b/fixtures/tests/basic_test.rs index fbbebf2..cd6aabc 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,7 +19,7 @@ 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); @@ -27,18 +27,16 @@ fn spend_p2wpkh(context: &simplex::TestContext) -> anyhow::Result { let txid = signer.send(p2pk_script.clone(), 50)?; println!("Broadcast: {}", txid); - 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(); @@ -55,22 +53,13 @@ fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result { let txid = signer.broadcast(&ft)?; println!("Broadcast: {}", txid); - 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..0de010e 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( @@ -15,10 +15,10 @@ fn make_confidential_to_bob(alice: &Signer, bob: &Signer, asset: AssetId) -> any let txid = alice.broadcast(&ft)?; println!("Broadcast: {}", txid); - 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(); @@ -43,7 +43,7 @@ fn issue_confidential_to_alice(alice: &Signer, bob: &Signer) -> anyhow::Result 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"); - 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..322d881 --- /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_pub_key(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 txid = signer.send(script.clone(), 50)?; + println!("Funded dummy script: {}", txid); + + 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..ba0056f 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); - 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(); @@ -51,62 +51,47 @@ fn spend_nested_sig( let txid = signer.broadcast(&ft)?; println!("Broadcast: {}", txid); - 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(()) } From 40990fc07fccce999cf0ea58f848d0aeb0224a56 Mon Sep 17 00:00:00 2001 From: Kyrylo Riabov Date: Wed, 29 Apr 2026 12:41:09 +0300 Subject: [PATCH 04/15] Minor refactoring (#65) --- crates/sdk/src/signer/core.rs | 17 ++--------------- crates/sdk/src/transaction/utxo.rs | 10 ++++++++++ deny.toml | 1 + 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index 7a4f95c..6c263fa 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -160,18 +160,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; @@ -260,9 +249,7 @@ impl Signer { } 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) } // TODO: can this be optimized to not populate TxOuts that are filtered out? diff --git a/crates/sdk/src/transaction/utxo.rs b/crates/sdk/src/transaction/utxo.rs index 3dad2bb..e301a43 100644 --- a/crates/sdk/src/transaction/utxo.rs +++ b/crates/sdk/src/transaction/utxo.rs @@ -23,4 +23,14 @@ impl UTXO { pub fn unblinded_amount(&self) -> u64 { self.secrets.expect("The UTXO is not unblinded").value } + + pub fn asset(&self) -> AssetId { + self.secrets + .map_or_else(|| self.explicit_asset(), |secrets| secrets.asset) + } + + pub fn amount(&self) -> u64 { + self.secrets + .map_or_else(|| self.explicit_amount(), |secrets| secrets.value) + } } 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 From 6a6bef4d8b14aa7bdfdfb35ef2174cc6a370ad45 Mon Sep 17 00:00:00 2001 From: Oleh Komendant <44612825+Hrom131@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:44:32 +0300 Subject: [PATCH 05/15] Fix/reissuance (#66) * Add ReissuanceInput and update FinalTransaction functions * Refactor FinalInput struct * Refactor FinalTransaction; add reissuance test * Move ReissuanceInput logic to the IssuanceInput * Convert IssuanceInput to enum * Change IssuanceInput fields order --- .../sdk/src/transaction/final_transaction.rs | 201 +++++++++++++----- crates/sdk/src/transaction/mod.rs | 2 +- crates/sdk/src/transaction/partial_input.rs | 61 +++--- examples/basic/tests/confidential_test.rs | 16 +- fixtures/tests/confidential_test.rs | 16 +- fixtures/tests/reissuance_test.rs | 117 ++++++++++ 6 files changed, 320 insertions(+), 93 deletions(-) create mode 100644 fixtures/tests/reissuance_test.rs diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index 0fcc90e..0264718 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -1,19 +1,28 @@ 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; pub const WITNESS_SCALE_FACTOR: usize = 4; +#[derive(Debug, Clone)] +pub struct IssuanceDetails { + pub asset_id: AssetId, + pub inflation_asset_id: AssetId, + pub asset_entropy: sha256::Midstate, +} + #[derive(Clone)] pub struct FinalInput { pub partial_input: PartialInput, @@ -22,6 +31,81 @@ pub struct FinalInput { pub required_sig: RequiredSignature, } +impl FinalInput { + pub fn new(partial_input: PartialInput, required_sig: RequiredSignature) -> Self { + Self { + partial_input, + required_sig, + program_input: None, + issuance_input: None, + } + } + + pub fn with_program(mut self, program_input: ProgramInput) -> Self { + self.program_input = Some(program_input); + + self + } + + pub fn with_issuance(mut self, issuance_input: IssuanceInput) -> Self { + self.issuance_input = Some(issuance_input); + + self + } + + 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_entropy, + asset_id, + inflation_asset_id, + }) + } + None => None, + } + } + + 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 + } +} + #[derive(Clone)] pub struct FinalTransaction { inputs: Vec, @@ -45,12 +129,7 @@ impl FinalTransaction { _ => {} }; - self.inputs.push(FinalInput { - partial_input, - program_input: None, - issuance_input: None, - required_sig, - }); + self.push_new_input(FinalInput::new(partial_input, required_sig)); } pub fn add_program_input( @@ -63,12 +142,7 @@ 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)); } pub fn add_issuance_input( @@ -76,7 +150,7 @@ impl FinalTransaction { 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") @@ -84,19 +158,8 @@ impl FinalTransaction { _ => {} }; - 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() } pub fn add_program_issuance_input( @@ -105,24 +168,17 @@ impl FinalTransaction { 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() } pub fn remove_input(&mut self, index: usize) -> Option { @@ -214,17 +270,7 @@ impl FinalTransaction { 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 +296,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 +451,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 +465,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 +480,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..a3916dc 100644 --- a/crates/sdk/src/transaction/mod.rs +++ b/crates/sdk/src/transaction/mod.rs @@ -3,7 +3,7 @@ pub mod partial_input; pub mod partial_output; 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 utxo::UTXO; diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index d664b04..9daa35b 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; @@ -50,11 +49,16 @@ pub struct ProgramInput { } #[derive(Clone)] -pub struct IssuanceInput { - pub issuance_amount: u64, - pub asset_entropy: [u8; 32], - pub reissuance_amount: Option, - pub blinding_nonce: Option, +pub enum IssuanceInput { + Issuance { + issuance_amount: u64, + inflation_amount: u64, + asset_entropy: [u8; 32], + }, + Reissuance { + issuance_amount: u64, + asset_entropy: [u8; 32], + }, } impl PartialInput { @@ -131,33 +135,42 @@ impl ProgramInput { } impl IssuanceInput { - pub fn new(issuance_amount: u64, asset_entropy: [u8; 32]) -> Self { - Self { + 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 + pub fn new_reissuance(issuance_amount: u64, asset_entropy: [u8; 32]) -> Self { + Self::Reissuance { + issuance_amount, + asset_entropy, + } } 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/examples/basic/tests/confidential_test.rs b/examples/basic/tests/confidential_test.rs index ab6c68a..d757577 100644 --- a/examples/basic/tests/confidential_test.rs +++ b/examples/basic/tests/confidential_test.rs @@ -23,21 +23,23 @@ fn issue_confidential_to_alice(alice: &Signer, bob: &Signer) -> anyhow::Result anyhow::Result<( 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)?; diff --git a/fixtures/tests/reissuance_test.rs b/fixtures/tests/reissuance_test.rs new file mode 100644 index 0000000..8b4c5bf --- /dev/null +++ b/fixtures/tests/reissuance_test.rs @@ -0,0 +1,117 @@ +use simplex::simplicityhl::elements::{AssetId, Txid}; + +use simplex::signer::Signer; +use simplex::transaction::partial_input::IssuanceInput; +use simplex::transaction::{FinalTransaction, IssuanceDetails, PartialInput, PartialOutput, RequiredSignature}; + +fn make_confidential_to_bob(alice: &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 txid = alice.broadcast(&ft)?; + println!("Broadcast: {}", txid); + + Ok(txid) +} + +fn issue_explicit_to_alice_with_reissuance(alice: &Signer, bob: &Signer) -> anyhow::Result<(Txid, 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 txid = bob.broadcast(&ft)?; + println!("Broadcast: {}", txid); + + Ok((txid, issuance_details)) +} + +fn reissue_tokens_to_bob( + bob: &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 txid = bob.broadcast(&ft)?; + println!("Broadcast: {}", txid); + + Ok(txid) +} + +#[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 txid = make_confidential_to_bob(alice, &bob, provider.get_network().policy_asset())?; + + provider.wait(&txid)?; + println!("Confirmed"); + + let (txid, issuance_details) = issue_explicit_to_alice_with_reissuance(alice, &bob)?; + + provider.wait(&txid)?; + println!("Confirmed"); + + let reissuance_amount = 5000; + let txid = reissue_tokens_to_bob(&bob, &issuance_details, reissuance_amount)?; + println!("Broadcast: {}", txid); + + provider.wait(&txid)?; + 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(()) +} From c833e3de229ddb3233eb611167cf0e7e6340d4a4 Mon Sep 17 00:00:00 2001 From: Illia Kripaka <30872146+ikripaka@users.noreply.github.com> Date: Wed, 29 Apr 2026 22:47:37 +0300 Subject: [PATCH 06/15] smplx_sdk: add wrapper over Txid for wait behaviour (TxReceipt) (#62) * smplx_sdk: add wrapper over Txid for wait behaviour - add simplex::transaction::TxReceipt type to wrap behavior over Txid. In this way we need to wait Txid for completing instead of invoking provider - function to wait elements::Txid remained inplace - align tests # Conflicts: # fixtures/tests/basic_test.rs # fixtures/tests/confidential_test.rs # fixtures/tests/nested_sig.rs * Apply suggestion from @Arvolear --------- Co-authored-by: Artem Chystiakov <47551140+Arvolear@users.noreply.github.com> --- crates/sdk/src/provider/core.rs | 4 +- crates/sdk/src/provider/esplora.rs | 8 ++-- crates/sdk/src/provider/simplex.rs | 8 ++-- crates/sdk/src/signer/core.rs | 6 +-- crates/sdk/src/transaction/mod.rs | 2 + crates/sdk/src/transaction/tx_receipt.rs | 45 ++++++++++++++++++++ examples/basic/tests/basic_test.rs | 30 +++++++------ examples/basic/tests/confidential_test.rs | 34 +++++++-------- fixtures/tests/basic_test.rs | 8 ++-- fixtures/tests/confidential_test.rs | 12 +++--- fixtures/tests/log_level.rs | 4 +- fixtures/tests/nested_sig.rs | 8 ++-- fixtures/tests/reissuance_test.rs | 51 +++++++++++++---------- 13 files changed, 136 insertions(+), 84 deletions(-) create mode 100644 crates/sdk/src/transaction/tx_receipt.rs diff --git a/crates/sdk/src/provider/core.rs b/crates/sdk/src/provider/core.rs index a5c76da..6875a91 100644 --- a/crates/sdk/src/provider/core.rs +++ b/crates/sdk/src/provider/core.rs @@ -5,7 +5,7 @@ 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; @@ -22,7 +22,7 @@ pub struct ProviderInfo { pub trait ProviderTrait { fn get_network(&self) -> &SimplicityNetwork; - fn broadcast_transaction(&self, tx: &Transaction) -> Result; + fn broadcast_transaction(&self, tx: &Transaction) -> Result, ProviderError>; fn wait(&self, txid: &Txid) -> Result<(), ProviderError>; diff --git a/crates/sdk/src/provider/esplora.rs b/crates/sdk/src/provider/esplora.rs index 886c51e..0ca803f 100644 --- a/crates/sdk/src/provider/esplora.rs +++ b/crates/sdk/src/provider/esplora.rs @@ -11,7 +11,7 @@ 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; @@ -101,7 +101,7 @@ impl ProviderTrait for EsploraProvider { &self.network } - fn broadcast_transaction(&self, tx: &Transaction) -> Result { + 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 +123,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> { diff --git a/crates/sdk/src/provider/simplex.rs b/crates/sdk/src/provider/simplex.rs index c34f25b..bf81c91 100644 --- a/crates/sdk/src/provider/simplex.rs +++ b/crates/sdk/src/provider/simplex.rs @@ -5,7 +5,7 @@ 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; @@ -30,12 +30,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 6c263fa..eeeecd8 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -33,7 +33,7 @@ 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; @@ -130,7 +130,7 @@ impl Signer { } // 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,7 +140,7 @@ impl Signer { Ok(self.provider.broadcast_transaction(&tx)?) } - pub fn broadcast(&self, tx: &FinalTransaction) -> Result { + pub fn broadcast(&self, tx: &FinalTransaction) -> Result, SignerError> { let (tx, _fee) = self.finalize(tx)?; Ok(self.provider.broadcast_transaction(&tx)?) diff --git a/crates/sdk/src/transaction/mod.rs b/crates/sdk/src/transaction/mod.rs index a3916dc..902ec25 100644 --- a/crates/sdk/src/transaction/mod.rs +++ b/crates/sdk/src/transaction/mod.rs @@ -1,9 +1,11 @@ pub mod final_transaction; pub mod partial_input; pub mod partial_output; +pub mod tx_receipt; pub mod utxo; 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/tx_receipt.rs b/crates/sdk/src/transaction/tx_receipt.rs new file mode 100644 index 0000000..4e7aba5 --- /dev/null +++ b/crates/sdk/src/transaction/tx_receipt.rs @@ -0,0 +1,45 @@ +use std::fmt; +use std::fmt::{Debug, Display, Formatter}; + +use simplicityhl::elements::Txid; + +use crate::provider::{ProviderError, ProviderTrait}; + +#[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> { + pub fn new(provider: &'a dyn ProviderTrait, tx_id: Txid) -> Self { + Self { provider, tx_id } + } + + pub fn txid(self) -> Txid { + self.tx_id + } + + #[inline] + pub fn wait(&self) -> Result<(), ProviderError> { + self.provider.wait(&self.tx_id) + } +} diff --git a/examples/basic/tests/basic_test.rs b/examples/basic/tests/basic_test.rs index bf73a75..030fc09 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,18 +19,18 @@ 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(); @@ -52,24 +52,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 d757577..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,13 +12,13 @@ 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(); @@ -42,10 +42,10 @@ fn issue_confidential_to_alice(alice: &Signer, bob: &Signer) -> anyhow::Result 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/tests/basic_test.rs b/fixtures/tests/basic_test.rs index cd6aabc..76f897b 100644 --- a/fixtures/tests/basic_test.rs +++ b/fixtures/tests/basic_test.rs @@ -24,8 +24,8 @@ fn spend_p2wpkh(context: &simplex::TestContext) -> anyhow::Result<()> { 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(()) } @@ -50,8 +50,8 @@ 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(()) } diff --git a/fixtures/tests/confidential_test.rs b/fixtures/tests/confidential_test.rs index 744cb69..5e1784d 100644 --- a/fixtures/tests/confidential_test.rs +++ b/fixtures/tests/confidential_test.rs @@ -12,8 +12,8 @@ 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(()) } @@ -42,8 +42,8 @@ fn issue_confidential_to_alice(alice: &Signer, bob: &Signer) -> anyhow::Result<( .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(()) } @@ -58,8 +58,8 @@ fn confidential_test(context: simplex::TestContext) -> anyhow::Result<()> { issue_confidential_to_alice(alice, &bob)?; // 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); Ok(()) } diff --git a/fixtures/tests/log_level.rs b/fixtures/tests/log_level.rs index 322d881..05a38e0 100644 --- a/fixtures/tests/log_level.rs +++ b/fixtures/tests/log_level.rs @@ -20,8 +20,8 @@ fn dummy_log_level(context: simplex::TestContext) -> anyhow::Result<()> { let (dummy, script) = setup_dummy(&context); - let txid = signer.send(script.clone(), 50)?; - println!("Funded dummy script: {}", txid); + let tx_receipt = signer.send(script.clone(), 50)?; + println!("Funded dummy script: {}", tx_receipt); let utxos = provider.fetch_scripthash_utxos(&script)?; diff --git a/fixtures/tests/nested_sig.rs b/fixtures/tests/nested_sig.rs index ba0056f..d3f6e0b 100644 --- a/fixtures/tests/nested_sig.rs +++ b/fixtures/tests/nested_sig.rs @@ -22,8 +22,8 @@ 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(()) } @@ -48,8 +48,8 @@ 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(()) } diff --git a/fixtures/tests/reissuance_test.rs b/fixtures/tests/reissuance_test.rs index 8b4c5bf..ea79b90 100644 --- a/fixtures/tests/reissuance_test.rs +++ b/fixtures/tests/reissuance_test.rs @@ -1,10 +1,12 @@ -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, IssuanceDetails, PartialInput, PartialOutput, RequiredSignature}; +use simplex::transaction::{ + FinalTransaction, IssuanceDetails, 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,13 +14,16 @@ 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_explicit_to_alice_with_reissuance(alice: &Signer, bob: &Signer) -> anyhow::Result<(Txid, IssuanceDetails)> { +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(); @@ -43,17 +48,17 @@ fn issue_explicit_to_alice_with_reissuance(alice: &Signer, bob: &Signer) -> anyh .with_blinding_key(bob.get_blinding_public_key()), ); - let txid = bob.broadcast(&ft)?; - println!("Broadcast: {}", txid); + let tx_receipt = bob.broadcast(&ft)?; + println!("Broadcast: {}", tx_receipt); - Ok((txid, issuance_details)) + Ok((tx_receipt, issuance_details)) } -fn reissue_tokens_to_bob( - bob: &Signer, +fn reissue_tokens_to_bob<'a>( + bob: &'a Signer, issuance_details: &IssuanceDetails, reissuance_amount: u64, -) -> anyhow::Result { +) -> anyhow::Result> { let reissuance_token_utxo = bob.get_utxos_asset(issuance_details.inflation_asset_id)?[0].clone(); let mut ft = FinalTransaction::new(); @@ -79,10 +84,10 @@ fn reissue_tokens_to_bob( issuance_details.asset_id, )); - 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] @@ -91,21 +96,21 @@ fn reissuance_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, issuance_details) = issue_explicit_to_alice_with_reissuance(alice, &bob)?; + let (tx_receipt, issuance_details) = issue_explicit_to_alice_with_reissuance(alice, &bob)?; - provider.wait(&txid)?; + tx_receipt.wait()?; println!("Confirmed"); let reissuance_amount = 5000; - let txid = reissue_tokens_to_bob(&bob, &issuance_details, reissuance_amount)?; - println!("Broadcast: {}", txid); + let tx_receipt = reissue_tokens_to_bob(&bob, &issuance_details, reissuance_amount)?; + println!("Broadcast: {}", tx_receipt); - provider.wait(&txid)?; + tx_receipt.wait()?; println!("Confirmed"); let bob_asset_utxos = bob.get_utxos_asset(issuance_details.asset_id)?; From 98e21ea39711d748e0436e09d0a100e3cbb6f99e Mon Sep 17 00:00:00 2001 From: Illia Kripaka <30872146+ikripaka@users.noreply.github.com> Date: Fri, 1 May 2026 18:02:23 +0300 Subject: [PATCH 07/15] Add possibility to use NetworkUtils on tests (#68) * smplx_test: add possibility to run network utils on tests - add generate_blocks endpoint to ProviderTrait - add function to generate desired block height in test - add test for it * smplx_test: fix lints * Apply suggestions from code review Co-authored-by: Artem Chystiakov <47551140+Arvolear@users.noreply.github.com> --------- Co-authored-by: Artem Chystiakov <47551140+Arvolear@users.noreply.github.com> --- crates/sdk/src/provider/rpc/elements.rs | 11 ++++++- crates/test/src/context.rs | 23 +++++++++++++- crates/test/src/error.rs | 12 ++++++++ crates/test/src/lib.rs | 2 ++ crates/test/src/network_utils.rs | 41 +++++++++++++++++++++++++ fixtures/tests/network_utils.rs | 14 +++++++++ 6 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 crates/test/src/network_utils.rs create mode 100644 fixtures/tests/network_utils.rs diff --git a/crates/sdk/src/provider/rpc/elements.rs b/crates/sdk/src/provider/rpc/elements.rs index 9f9d6fd..b3270ed 100644 --- a/crates/sdk/src/provider/rpc/elements.rs +++ b/crates/sdk/src/provider/rpc/elements.rs @@ -83,7 +83,7 @@ impl ElementsRpc { Ok(()) } - pub fn generate_blocks(&self, block_num: u32) -> Result<(), RpcError> { + pub fn generate_blocks(&self, block_num: u64) -> Result<(), RpcError> { const METHOD: &str = "generatetoaddress"; let address = self.get_new_address("")?.to_string(); @@ -109,4 +109,13 @@ impl ElementsRpc { Ok(()) } + + 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/test/src/context.rs b/crates/test/src/context.rs index 34fdd55..663e8df 100644 --- a/crates/test/src/context.rs +++ b/crates/test/src/context.rs @@ -6,12 +6,15 @@ use smplx_regtest::Regtest; use smplx_regtest::client::RegtestClient; use smplx_sdk::global::set_global_config; -use smplx_sdk::provider::{EsploraProvider, ProviderInfo, ProviderTrait, SimplexProvider, SimplicityNetwork}; +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 { @@ -85,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; 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/network_utils.rs b/crates/test/src/network_utils.rs new file mode 100644 index 0000000..a573ec3 --- /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 { + break; + } + + 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/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(()) +} From 088f5a6bc7943bc93cdd170b6c369fc2ba664af4 Mon Sep 17 00:00:00 2001 From: Vitalii Volovyk <161724671+topologoanatom@users.noreply.github.com> Date: Fri, 1 May 2026 18:10:25 +0300 Subject: [PATCH 08/15] [refactor] Add `simplex new` and `simplex example` commands (#67) * add new and example commands * comment out test exec and add reference to example * remove `example` command and move `new` to `init` * initialize rust lib with config by default * Apply suggestion --------- Co-authored-by: stringhandler Co-authored-by: Artem Chystiakov <47551140+Arvolear@users.noreply.github.com> --- Cargo.toml | 6 ++---- README.md | 2 +- crates/cli/src/cli.rs | 18 +++++++++++++++--- crates/cli/src/commands/core.rs | 11 ++--------- crates/cli/src/commands/init.rs | 10 +++++----- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cbc5032..a7dee43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,6 @@ [workspace] resolver = "3" -members = [ - "crates/*", -] +members = ["crates/*"] exclude = ["examples/basic", "fixtures"] [workspace.package] @@ -20,7 +18,7 @@ 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" } -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"] } diff --git a/README.md b/README.md index cfa5772..80db48f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ cargo add --dev smplx-std Optionally, initialize a new project: ```bash -simplex init +simplex init ``` ## Usage diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 2186fd3..5d53238 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -24,10 +24,22 @@ pub struct Cli { impl Cli { pub async 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()?; diff --git a/crates/cli/src/commands/core.rs b/crates/cli/src/commands/core.rs index fc3c3ba..2885cf0 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, @@ -26,13 +26,6 @@ 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, -} - #[derive(Debug, Args, Clone)] pub struct TestFlags { /// Show output from successful tests diff --git a/crates/cli/src/commands/init.rs b/crates/cli/src/commands/init.rs index 207e196..303fbde 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,8 @@ 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)? - } + 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 +53,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 From 8beb0de9461124b4547e463e4b401a4ddcfc53e8 Mon Sep 17 00:00:00 2001 From: Oleh Komendant <44612825+Hrom131@users.noreply.github.com> Date: Fri, 8 May 2026 14:28:26 +0300 Subject: [PATCH 09/15] Add filters logic to the CLI test command (#72) * Add more arguments to the CLI test command * Add TODO --- crates/cli/src/cli.rs | 6 +-- crates/cli/src/commands/core.rs | 23 +++++++++-- crates/cli/src/commands/test.rs | 68 ++++++++++++++++++++++----------- 3 files changed, 67 insertions(+), 30 deletions(-) diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 5d53238..9a6092b 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -49,13 +49,11 @@ 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()?; diff --git a/crates/cli/src/commands/core.rs b/crates/cli/src/commands/core.rs index 2885cf0..1bb05b1 100644 --- a/crates/cli/src/commands/core.rs +++ b/crates/cli/src/commands/core.rs @@ -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,6 +25,16 @@ pub enum Command { Clean, } +#[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, Clone)] pub struct TestFlags { /// Show output from successful tests @@ -37,7 +46,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/test.rs b/crates/cli/src/commands/test.rs index a1499bf..2f9b131 100644 --- a/crates/cli/src/commands/test.rs +++ b/crates/cli/src/commands/test.rs @@ -4,13 +4,13 @@ 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(mut config: TestConfig, filter: String, flags: &TestFlags) -> Result<(), CommandError> { + pub fn run(mut config: TestConfig, args: &TestArguments, flags: &TestFlags) -> Result<(), CommandError> { let cache_path = Self::get_test_config_cache_name()?; if flags.verbose { @@ -19,7 +19,7 @@ impl Test { 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()?; @@ -39,10 +39,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) @@ -53,37 +59,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()); + } + + if flags.no_fail_fast { + cargo_test_args.push("--no-fail-fast".into()); + } + + cargo_test_args + } - let flag_args = Self::build_test_flags(flags); + fn build_test_bin_args(args: &TestArguments, flags: &TestFlags) -> Vec { + let mut test_bin_args = Vec::new(); - if !flag_args.is_empty() { - command_as_arg.push_str(" --"); - command_as_arg.push_str(&flag_args); + 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.extend(args.filters.iter().cloned()); + } else { + test_bin_args.push(SMPLX_TEST_MARKER.to_string()); } - 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 { From 3330f15249234ab91e3243443de81fc1a5b19ae7 Mon Sep 17 00:00:00 2001 From: Illia Kripaka <30872146+ikripaka@users.noreply.github.com> Date: Fri, 8 May 2026 18:02:37 +0300 Subject: [PATCH 10/15] Docs (#70) * assets: add logo imgs * smplx: add extended clippy linting * add readme for smplx-sdk * smplx: add extended clippy linting * fix lints, which aren't related to docs # Conflicts: # crates/cli/src/commands/core.rs # crates/cli/src/commands/init.rs # Conflicts: # crates/cli/src/commands/test.rs * smplx_test: fix typo in mine_until_height when returning Err value on success * docs: fix clippy doc lints # Conflicts: # crates/cli/src/cli.rs # crates/cli/src/commands/test.rs * docs: change "asset id" on `AssetId` * docs: remove custom lints from smplx-std Cargo.toml * add additional lint to crates::sdk::lib.rs * add additional lints to root Cargo.toml * generator: add must_use attribute * smplx-sdk readme * Apply suggestions from code review Co-authored-by: Artem Chystiakov <47551140+Arvolear@users.noreply.github.com> --------- Co-authored-by: Artem Chystiakov Co-authored-by: Artem Chystiakov <47551140+Arvolear@users.noreply.github.com> --- Cargo.lock | 28 ----- Cargo.toml | 12 +- crates/build/src/generator.rs | 9 ++ crates/build/src/macros/parse.rs | 2 +- crates/cli/Cargo.toml | 1 - crates/cli/src/bin/main.rs | 5 +- crates/cli/src/cli.rs | 13 +- crates/cli/src/commands/build.rs | 6 +- crates/cli/src/commands/clean.rs | 20 ++- crates/cli/src/commands/core.rs | 2 + crates/cli/src/commands/init.rs | 20 ++- crates/cli/src/commands/regtest.rs | 9 +- crates/cli/src/commands/test.rs | 17 ++- crates/cli/src/config/core.rs | 19 +++ crates/cli/src/lib.rs | 2 + crates/macros/src/lib.rs | 2 + crates/regtest/src/args.rs | 4 +- crates/regtest/src/client.rs | 31 ++++- crates/regtest/src/config.rs | 4 + crates/regtest/src/lib.rs | 2 + crates/regtest/src/regtest.rs | 18 ++- crates/sdk/README.md | 22 ++++ crates/sdk/src/constants.rs | 11 +- crates/sdk/src/global.rs | 16 ++- crates/sdk/src/lib.rs | 13 ++ crates/sdk/src/program/arguments.rs | 3 + crates/sdk/src/program/core.rs | 72 ++++++++++- crates/sdk/src/program/error.rs | 29 ++++- crates/sdk/src/program/mod.rs | 4 + crates/sdk/src/program/witness.rs | 3 + crates/sdk/src/provider/core.rs | 45 +++++++ crates/sdk/src/provider/error.rs | 16 ++- crates/sdk/src/provider/esplora.rs | 14 ++- crates/sdk/src/provider/mod.rs | 6 + crates/sdk/src/provider/network.rs | 29 ++++- crates/sdk/src/provider/rpc/elements.rs | 40 ++++++ crates/sdk/src/provider/rpc/error.rs | 4 + crates/sdk/src/provider/rpc/mod.rs | 2 + crates/sdk/src/provider/simplex.rs | 10 ++ crates/sdk/src/signer/core.rs | 102 +++++++++++++-- crates/sdk/src/signer/error.rs | 26 ++++ crates/sdk/src/signer/mod.rs | 3 + crates/sdk/src/signer/wtns_injector.rs | 40 +++--- .../sdk/src/transaction/final_transaction.rs | 117 +++++++++++++++++- crates/sdk/src/transaction/mod.rs | 5 + crates/sdk/src/transaction/partial_input.rs | 55 +++++++- crates/sdk/src/transaction/partial_output.rs | 13 ++ crates/sdk/src/transaction/tx_receipt.rs | 8 ++ crates/sdk/src/transaction/utxo.rs | 34 +++++ crates/sdk/src/utils.rs | 24 +++- crates/test/src/context.rs | 2 +- crates/test/src/network_utils.rs | 2 +- docs/simplex_logo.png | Bin 0 -> 53550 bytes examples/basic/src/lib.rs | 2 + fixtures/src/lib.rs | 2 + 55 files changed, 873 insertions(+), 127 deletions(-) create mode 100644 crates/sdk/README.md create mode 100644 docs/simplex_logo.png diff --git a/Cargo.lock b/Cargo.lock index c0ce516..21f0d13 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" @@ -1331,7 +1325,6 @@ dependencies = [ "smplx-sdk", "smplx-test", "thiserror", - "tokio", "toml 0.9.12+spec-1.1.0", "toml_edit", ] @@ -1505,27 +1498,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 a7dee43..93fe05f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,6 @@ exclude = ["examples/basic", "fixtures"] 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" } @@ -31,3 +28,12 @@ simplicityhl = { version = "0.5.0-rc.0" } [patch.crates-io] simplicity-sys = { git = "https://github.com/BlockstreamResearch/rust-simplicity", tag = "simplicity-sys-0.6.1" } + +[workspace.lints.rust] +rust_2018_idioms = "warn" +unused_lifetimes = "warn" +unreachable_pub = "warn" +deprecated_in_future = "warn" + +[workspace.lints.clippy] +multiple_crate_versions = "allow" diff --git a/crates/build/src/generator.rs b/crates/build/src/generator.rs index d044fcf..8537df5 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)), } } + #[must_use] pub fn with_pub_key(mut self, pub_key: XOnlyPublicKey) -> Self { self.program = self.program.with_pub_key(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) } 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..b990aaf 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -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/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 9a6092b..69edbbb 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -22,7 +22,12 @@ 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 { name } => { let simplex_conf_path = match name { @@ -59,19 +64,19 @@ impl Cli { 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 1bb05b1..c579aea 100644 --- a/crates/cli/src/commands/core.rs +++ b/crates/cli/src/commands/core.rs @@ -25,6 +25,7 @@ pub enum Command { Clean, } +#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Args, Clone)] pub struct TestArguments { /// Space-separated test name filters @@ -35,6 +36,7 @@ pub struct TestArguments { pub target: Option, } +#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Args, Clone)] pub struct TestFlags { /// Show output from successful tests diff --git a/crates/cli/src/commands/init.rs b/crates/cli/src/commands/init.rs index 303fbde..15c0aa9 100644 --- a/crates/cli/src/commands/init.rs +++ b/crates/cli/src/commands/init.rs @@ -9,6 +9,13 @@ pub const SIMPLEX_CRATE_NAME: &str = "smplx-std"; pub struct Init; impl Init { + /// Initialises 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)?; @@ -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 2f9b131..0e4ca98 100644 --- a/crates/cli/src/commands/test.rs +++ b/crates/cli/src/commands/test.rs @@ -10,11 +10,18 @@ use super::error::CommandError; pub struct Test {} impl Test { + /// 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.verbosity = Some(Verbosity(4)); } config.to_file(&cache_path)?; @@ -25,7 +32,7 @@ impl Test { match output.status.code() { Some(code) => { - println!("Exit Status: {}", code); + println!("Exit Status: {code}"); if code == 0 { println!("{}", String::from_utf8(output.stdout).unwrap()); @@ -80,10 +87,10 @@ impl Test { 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.extend(args.filters.iter().cloned()); - } else { + if args.filters.is_empty() { test_bin_args.push(SMPLX_TEST_MARKER.to_string()); + } else { + test_bin_args.extend(args.filters.iter().cloned()); } test_bin_args.extend(Self::build_test_bin_flags(flags)); diff --git a/crates/cli/src/config/core.rs b/crates/cli/src/config/core.rs index b40e2c5..616f4a5 100644 --- a/crates/cli/src/config/core.rs +++ b/crates/cli/src/config/core.rs @@ -20,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(); 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/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/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..cedf472 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); + /// Initialises 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 initialisation, 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/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 index 7b5102d..72ec6b5 100644 --- a/crates/sdk/src/global.rs +++ b/crates/sdk/src/global.rs @@ -2,7 +2,8 @@ use std::sync::OnceLock; use crate::program::TrackerLogLevel; -#[derive(Clone, Copy)] +/// A structure to represent the global configuration settings for the application. +#[derive(Clone, Copy, Debug)] pub struct GlobalConfig { log_level: TrackerLogLevel, } @@ -17,14 +18,21 @@ impl Default for GlobalConfig { 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 initialised. pub fn set_global_config(log_level: TrackerLogLevel) -> Result<(), GlobalConfig> { GLOBAL_CONFIG.set(GlobalConfig { log_level }) } -/// Returns default log level if `GLOBAL_CONFIG` is not initialized +/// Returns the default log level if `GLOBAL_CONFIG` is not initialized pub fn get_log_level() -> TrackerLogLevel { GLOBAL_CONFIG .get() - .map(|config| config.log_level) - .unwrap_or(GlobalConfig::default().log_level) + .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 a0ae82f..c6e8dfc 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1,7 +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 6815e01..5df3367 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -21,11 +21,28 @@ 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 behaviour related to testing and execution. +/// Drastically simplifies the usage of `simplicity` programs by generating +/// an implementation with `include_simf!()` macro. 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, @@ -33,6 +50,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, @@ -41,6 +66,10 @@ pub trait ProgramTrait: DynClone { network: &SimplicityNetwork, ) -> Result<(Arc>, Value), ProgramError>; + /// Finalises 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, @@ -50,6 +79,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, @@ -160,6 +192,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, @@ -169,36 +203,58 @@ impl Program { } } + /// Sets the `pub_key` field of the struct to the provided `XOnlyPublicKey` value and returns the updated builder instance. + #[must_use] pub fn with_pub_key(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(); @@ -211,14 +267,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)?; @@ -226,6 +290,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)?; @@ -303,7 +371,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; @@ -312,7 +380,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 4cf75c1..1285d55 100644 --- a/crates/sdk/src/program/mod.rs +++ b/crates/sdk/src/program/mod.rs @@ -1,6 +1,10 @@ +/// 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; 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 6875a91..e918b9e 100644 --- a/crates/sdk/src/provider/core.rs +++ b/crates/sdk/src/provider/core.rs @@ -9,35 +9,80 @@ 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; + /// 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 0ca803f..c41f41c 100644 --- a/crates/sdk/src/provider/esplora.rs +++ b/crates/sdk/src/provider/esplora.rs @@ -16,9 +16,14 @@ 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,6 +108,7 @@ impl ProviderTrait for EsploraProvider { &self.network } + #[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); @@ -271,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) @@ -300,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 b3270ed..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,6 +111,10 @@ impl ElementsRpc { Ok(()) } + /// 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"; @@ -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"; @@ -110,6 +146,10 @@ 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"; 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 bf81c91..6f8345a 100644 --- a/crates/sdk/src/provider/simplex.rs +++ b/crates/sdk/src/provider/simplex.rs @@ -11,12 +11,22 @@ 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 initialise. + #[must_use] pub fn new(esplora_url: String, elements_url: String, auth: Auth, network: SimplicityNetwork) -> Self { Self { esplora: EsploraProvider::new(esplora_url, network), diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index eeeecd8..c001a94 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -37,9 +37,15 @@ use crate::transaction::{FinalTransaction, PartialInput, PartialOutput, Required 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,6 +146,10 @@ 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, SignerError> { let mut ft = FinalTransaction::new(); @@ -140,12 +161,20 @@ impl Signer { Ok(self.provider.broadcast_transaction(&tx)?) } + /// Evaluates, funds, and broadcasts an already assembled `FinalTransaction`. + /// + /// # Errors + /// Returns a `SignerError` if finalising 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(); @@ -169,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, } @@ -182,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, } @@ -192,6 +221,12 @@ impl Signer { Err(SignerError::NotEnoughFunds(curr_fee)) } + /// Verifies and finalises 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, @@ -199,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()) @@ -232,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())) @@ -244,19 +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.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, @@ -285,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); @@ -292,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(); @@ -315,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() @@ -458,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)) @@ -477,8 +562,6 @@ impl Signer { sig_path, Value::byte_array(signature.serialize()), )? - } else { - Value::byte_array(signature.serialize()) }; let mut hm = HashMap::new(); @@ -492,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 0264718..6967ad5 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -14,24 +14,36 @@ 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 finalisation. pub program_input: Option, + /// Contains optional issuance-related information. pub issuance_input: Option, + /// Required signature for finalising 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, @@ -41,18 +53,29 @@ impl FinalInput { } } + /// 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) => { @@ -69,15 +92,24 @@ impl FinalInput { let inflation_asset_id = AssetId::reissuance_token_from_entropy(asset_entropy, false); Some(IssuanceDetails { - asset_entropy, 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(); @@ -106,6 +138,7 @@ impl FinalInput { } } +/// A struct representing a final (but not yet signed) transaction. #[derive(Clone)] pub struct FinalTransaction { inputs: Vec, @@ -113,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 { @@ -121,17 +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.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, @@ -145,6 +190,11 @@ impl FinalTransaction { 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, @@ -156,12 +206,17 @@ impl FinalTransaction { panic!("Requested signature is not NativeEcdsa or None") } _ => {} - }; + } 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, @@ -181,6 +236,7 @@ impl FinalTransaction { .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)); @@ -189,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)); @@ -201,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; @@ -255,15 +341,34 @@ 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(); diff --git a/crates/sdk/src/transaction/mod.rs b/crates/sdk/src/transaction/mod.rs index 902ec25..1fa8f62 100644 --- a/crates/sdk/src/transaction/mod.rs +++ b/crates/sdk/src/transaction/mod.rs @@ -1,7 +1,12 @@ +/// Represents a fully finalised target transaction schema ready for signing and broadcasting. pub mod final_transaction; +/// Represents inputs under construction before transaction finalisation. pub mod partial_input; +/// Represents outputs under construction before transaction finalisation. 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, IssuanceDetails}; diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index 9daa35b..88b6235 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -7,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, @@ -28,40 +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)] +/// 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), @@ -84,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, @@ -103,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 { @@ -129,12 +166,16 @@ 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 { + /// 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, @@ -143,6 +184,8 @@ impl IssuanceInput { } } + /// 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, @@ -150,6 +193,8 @@ impl IssuanceInput { } } + /// 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 { 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 index 4e7aba5..9b9b270 100644 --- a/crates/sdk/src/transaction/tx_receipt.rs +++ b/crates/sdk/src/transaction/tx_receipt.rs @@ -5,6 +5,7 @@ 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, @@ -30,14 +31,21 @@ impl AsRef for TxReceipt<'_> { } 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 e301a43..d4423bc 100644 --- a/crates/sdk/src/transaction/utxo.rs +++ b/crates/sdk/src/transaction/utxo.rs @@ -1,34 +1,68 @@ 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/test/src/context.rs b/crates/test/src/context.rs index 663e8df..1f3bacb 100644 --- a/crates/test/src/context.rs +++ b/crates/test/src/context.rs @@ -152,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/network_utils.rs b/crates/test/src/network_utils.rs index a573ec3..da48498 100644 --- a/crates/test/src/network_utils.rs +++ b/crates/test/src/network_utils.rs @@ -25,7 +25,7 @@ impl NetworkUtils { h = self.esplora.fetch_tip_height()? as u64; if h >= target_height { - break; + return Ok(()); } std::thread::sleep(std::time::Duration::from_millis(100)); diff --git a/docs/simplex_logo.png b/docs/simplex_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..643e5391039a57fb4d0730a2df9162751fbf39d0 GIT binary patch literal 53550 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 Date: Mon, 11 May 2026 21:52:18 +0300 Subject: [PATCH 11/15] rename a program function (#74) --- crates/build/src/generator.rs | 4 ++-- crates/sdk/src/program/core.rs | 5 +++-- fixtures/tests/log_level.rs | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/build/src/generator.rs b/crates/build/src/generator.rs index 8537df5..670bbff 100644 --- a/crates/build/src/generator.rs +++ b/crates/build/src/generator.rs @@ -183,8 +183,8 @@ impl ArtifactsGenerator { } #[must_use] - pub fn with_pub_key(mut self, pub_key: XOnlyPublicKey) -> Self { - self.program = self.program.with_pub_key(pub_key); + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); self } diff --git a/crates/sdk/src/program/core.rs b/crates/sdk/src/program/core.rs index 5df3367..183343d 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -25,7 +25,7 @@ use crate::utils::{hash_script, tap_data_hash, tr_unspendable_key}; /// /// This trait defines a core behaviour related to testing and execution. /// Drastically simplifies the usage of `simplicity` programs by generating -/// an implementation with `include_simf!()` macro. +/// an implementation with `include_simf!()` macro. pub trait ProgramTrait: DynClone { /// Retrieves the types of arguments required by a `simplicity` program. /// @@ -204,8 +204,9 @@ impl Program { } /// 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_pub_key(mut self, pub_key: XOnlyPublicKey) -> Self { + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { self.pub_key = pub_key; self diff --git a/fixtures/tests/log_level.rs b/fixtures/tests/log_level.rs index 05a38e0..ed174f1 100644 --- a/fixtures/tests/log_level.rs +++ b/fixtures/tests/log_level.rs @@ -6,7 +6,7 @@ use simplex_fixtures::artifacts::dummy_panic::derived_dummy_panic::{DummyPanicAr fn setup_dummy(context: &simplex::TestContext) -> (DummyPanicProgram, simplex::simplicityhl::elements::Script) { let signer = context.get_default_signer(); - let dummy = DummyPanicProgram::new(DummyPanicArguments {}).with_pub_key(signer.get_schnorr_public_key()); + let dummy = DummyPanicProgram::new(DummyPanicArguments {}).with_taproot_pubkey(signer.get_schnorr_public_key()); let script = dummy.get_script_pubkey(context.get_network()); From 459ff412aaf7b11ebfeab7b657f4e6c75af56b61 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Tue, 12 May 2026 13:19:03 +0300 Subject: [PATCH 12/15] quick doc fix --- crates/sdk/src/program/core.rs | 6 ++---- crates/sdk/src/signer/core.rs | 4 ++-- crates/sdk/src/transaction/final_transaction.rs | 4 ++-- crates/sdk/src/transaction/mod.rs | 6 +++--- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/crates/sdk/src/program/core.rs b/crates/sdk/src/program/core.rs index 183343d..5dd47f9 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -23,9 +23,7 @@ use crate::utils::{hash_script, tap_data_hash, tr_unspendable_key}; /// Executes `simplicity` programs at runtime. /// -/// This trait defines a core behaviour related to testing and execution. -/// Drastically simplifies the usage of `simplicity` programs by generating -/// an implementation with `include_simf!()` macro. +/// 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. /// @@ -66,7 +64,7 @@ pub trait ProgramTrait: DynClone { network: &SimplicityNetwork, ) -> Result<(Arc>, Value), ProgramError>; - /// Finalises and returns `pruned_witness` as output after executing the program on certain parameters. + /// 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. diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index c001a94..ef7e2c4 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -164,7 +164,7 @@ impl Signer { /// Evaluates, funds, and broadcasts an already assembled `FinalTransaction`. /// /// # Errors - /// Returns a `SignerError` if finalising the payload fails or if the network rejects the broadcast. + /// 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)?; @@ -221,7 +221,7 @@ impl Signer { Err(SignerError::NotEnoughFunds(curr_fee)) } - /// Verifies and finalises a transaction against a strict target confirmation window (in blocks). + /// 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 diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index 6967ad5..0742a51 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -33,11 +33,11 @@ pub struct IssuanceDetails { 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 finalisation. + /// 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 finalising the transaction. + /// Required signature for finalizing the transaction. pub required_sig: RequiredSignature, } diff --git a/crates/sdk/src/transaction/mod.rs b/crates/sdk/src/transaction/mod.rs index 1fa8f62..c4031da 100644 --- a/crates/sdk/src/transaction/mod.rs +++ b/crates/sdk/src/transaction/mod.rs @@ -1,8 +1,8 @@ -/// Represents a fully finalised target transaction schema ready for signing and broadcasting. +/// Represents a fully finalized target transaction schema ready for signing and broadcasting. pub mod final_transaction; -/// Represents inputs under construction before transaction finalisation. +/// Represents inputs under construction before transaction finalization. pub mod partial_input; -/// Represents outputs under construction before transaction finalisation. +/// 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; From c05568385dc4139b118c686d7fe1761b782a7501 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Tue, 12 May 2026 20:00:08 +0300 Subject: [PATCH 13/15] changelog --- CHANGELOG.md | 20 +++++++++++++++++++- README.md | 2 +- crates/cli/src/commands/init.rs | 2 +- crates/regtest/src/regtest.rs | 4 ++-- crates/sdk/src/global.rs | 2 +- crates/sdk/src/provider/simplex.rs | 2 +- examples/basic/tests/basic_test.rs | 4 +--- 7 files changed, 26 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb7548c..c3dd205 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,27 @@ # Changelog -## [Unreleased] +## [0.0.5] +### Simplex + +- Renamed `with_pub_key` program function to `with_taproot_pubkey`. +- Added some documentation to the public functions. +- Refactored `simplex test` command: + - Added compatibility for custom test name filters. + - Added `--target` flag that isolates the build 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 its 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/README.md b/README.md index 80db48f..234ae56 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ cargo add --dev smplx-std Optionally, initialize a new project: ```bash -simplex init +simplex init ``` ## Usage diff --git a/crates/cli/src/commands/init.rs b/crates/cli/src/commands/init.rs index 15c0aa9..c2176e9 100644 --- a/crates/cli/src/commands/init.rs +++ b/crates/cli/src/commands/init.rs @@ -9,7 +9,7 @@ pub const SIMPLEX_CRATE_NAME: &str = "smplx-std"; pub struct Init; impl Init { - /// Initialises a new Simplex project at the specified configuration 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. diff --git a/crates/regtest/src/regtest.rs b/crates/regtest/src/regtest.rs index cedf472..b4c2757 100644 --- a/crates/regtest/src/regtest.rs +++ b/crates/regtest/src/regtest.rs @@ -13,13 +13,13 @@ use super::error::RegtestError; pub struct Regtest {} impl Regtest { - /// Initialises a Regtest environment and returns a configured client and funded signer. + /// 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 initialisation, block generation, or RPC calls fail. + /// 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). diff --git a/crates/sdk/src/global.rs b/crates/sdk/src/global.rs index 72ec6b5..b4a6e2e 100644 --- a/crates/sdk/src/global.rs +++ b/crates/sdk/src/global.rs @@ -25,7 +25,7 @@ static GLOBAL_CONFIG: OnceLock = OnceLock::new(); /// 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 initialised. +/// 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 }) } diff --git a/crates/sdk/src/provider/simplex.rs b/crates/sdk/src/provider/simplex.rs index 6f8345a..e1e018d 100644 --- a/crates/sdk/src/provider/simplex.rs +++ b/crates/sdk/src/provider/simplex.rs @@ -25,7 +25,7 @@ impl SimplexProvider { /// Creates a new `SimplexProvider` with the given URLs, authentication, and network. /// /// # Panics - /// Panics if the `ElementsRpc` client fails to initialise. + /// 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 { diff --git a/examples/basic/tests/basic_test.rs b/examples/basic/tests/basic_test.rs index 030fc09..9110dc2 100644 --- a/examples/basic/tests/basic_test.rs +++ b/examples/basic/tests/basic_test.rs @@ -36,9 +36,7 @@ fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result> { 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(); From 019b33ba5ec0a605ecc422628b973b090d9444a8 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Tue, 12 May 2026 20:33:52 +0300 Subject: [PATCH 14/15] version --- CHANGELOG.md | 9 +++++---- Cargo.lock | 23 ++++++++++++----------- Cargo.toml | 19 ++++++++----------- crates/build/Cargo.toml | 2 +- crates/cli/Cargo.toml | 2 +- crates/macros/Cargo.toml | 2 +- crates/regtest/Cargo.toml | 2 +- crates/sdk/Cargo.toml | 2 +- crates/simplex/Cargo.toml | 2 +- crates/test/Cargo.toml | 2 +- examples/basic/Cargo.lock | 16 ++++++++-------- fixtures/Cargo.lock | 16 ++++++++-------- 12 files changed, 48 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3dd205..4e95cb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,19 @@ ### Simplex -- Renamed `with_pub_key` program function to `with_taproot_pubkey`. -- Added some documentation to the public functions. +- Added some documentation to the public simplex functions. +- Implemented "global configuration" singleton in the SDK. - Refactored `simplex test` command: - Added compatibility for custom test name filters. - - Added `--target` flag that isolates the build to a specific integration test module. + - 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 its name. +- 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. +- Renamed `with_pub_key` program method to `with_taproot_pubkey`. - Added fixtures for simplex integration tests. ### Simplexup diff --git a/Cargo.lock b/Cargo.lock index 21f0d13..153444d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1267,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", @@ -1276,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", @@ -1294,7 +1295,7 @@ dependencies = [ [[package]] name = "smplx-build" -version = "0.0.4" +version = "0.0.5" dependencies = [ "glob", "globwalk", @@ -1311,7 +1312,7 @@ dependencies = [ [[package]] name = "smplx-cli" -version = "0.0.4" +version = "0.0.5" dependencies = [ "anyhow", "clap", @@ -1331,7 +1332,7 @@ dependencies = [ [[package]] name = "smplx-macros" -version = "0.0.4" +version = "0.0.5" dependencies = [ "smplx-build", "smplx-test", @@ -1340,7 +1341,7 @@ dependencies = [ [[package]] name = "smplx-regtest" -version = "0.0.4" +version = "0.0.5" dependencies = [ "electrsd", "hex", @@ -1354,7 +1355,7 @@ dependencies = [ [[package]] name = "smplx-sdk" -version = "0.0.4" +version = "0.0.5" dependencies = [ "bip39", "bitcoin_hashes", @@ -1372,7 +1373,7 @@ dependencies = [ [[package]] name = "smplx-std" -version = "0.0.4" +version = "0.0.5" dependencies = [ "either", "serde", @@ -1385,7 +1386,7 @@ dependencies = [ [[package]] name = "smplx-test" -version = "0.0.4" +version = "0.0.5" dependencies = [ "electrsd", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 93fe05f..9136de6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,12 +8,12 @@ license = "MIT" edition = "2024" [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"] } hex = { version = "0.4.3" } @@ -22,12 +22,9 @@ 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-rc.0" } -[patch.crates-io] -simplicity-sys = { git = "https://github.com/BlockstreamResearch/rust-simplicity", tag = "simplicity-sys-0.6.1" } +electrsd = { version = "0.29.0", features = ["legacy"] } +simplicityhl = { version = "0.5.0" } [workspace.lints.rust] rust_2018_idioms = "warn" 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/cli/Cargo.toml b/crates/cli/Cargo.toml index b990aaf..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 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/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/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/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/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/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", From 17f3f65c022ce12f6ec1773d2d7585f9fa34c588 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Tue, 12 May 2026 20:41:26 +0300 Subject: [PATCH 15/15] upd changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e95cb3..85db4c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - 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. @@ -16,7 +20,6 @@ - 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. -- Renamed `with_pub_key` program method to `with_taproot_pubkey`. - Added fixtures for simplex integration tests. ### Simplexup