From e151ab844cf01c2255e0aacaf1f2c41efce68762 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Thu, 19 Feb 2026 18:41:28 +0200 Subject: [PATCH 01/14] structure refactoring --- Cargo.lock | 67 +--- Cargo.toml | 5 +- crates/artifacts/Cargo.toml | 16 - crates/artifacts/src/asset_auth.rs | 89 ----- crates/artifacts/src/lib.rs | 1 - crates/cli/Cargo.toml | 9 +- crates/cli/src/cli/mod.rs | 2 +- crates/cli/src/config.rs | 3 +- crates/cli/src/error.rs | 2 +- crates/cli/src/lib.rs | 1 + crates/config/Cargo.toml | 18 - crates/config/src/lib.rs | 159 --------- crates/core/Cargo.toml | 28 -- crates/core/README.md | 20 -- crates/core/src/assets/test-tx-incl-block.hex | 1 - crates/core/src/blinder.rs | 17 - crates/core/src/constants.rs | 128 ------- crates/core/src/error.rs | 52 --- crates/core/src/fee_rate_fetcher.rs | 123 ------- crates/core/src/lib.rs | 311 ----------------- crates/core/src/runner.rs | 40 --- crates/core/src/scripts.rs | 83 ----- crates/core/src/source_simf/p2pk.simf | 3 - crates/core/src/tx_inclusion.rs | 173 ---------- crates/macros-core/Cargo.toml | 23 -- crates/macros/Cargo.toml | 15 +- crates/macros/src/lib.rs | 8 +- .../src/macros_core}/attr/codegen.rs | 8 +- .../src/macros_core}/attr/mod.rs | 68 ++-- .../src/macros_core}/attr/parse.rs | 0 .../src/macros_core}/attr/types.rs | 0 .../lib.rs => macros/src/macros_core/mod.rs} | 0 .../src => macros/src/macros_core}/program.rs | 2 +- crates/{runtime => provider}/Cargo.toml | 15 +- crates/{runtime => provider}/README.md | 0 crates/{runtime => provider}/api-esplora.md | 0 crates/{runtime => provider}/api-waterfall.md | 0 .../src/elements_rpc/mod.rs | 19 +- .../src/elements_rpc/types.rs | 0 crates/{runtime => provider}/src/error.rs | 0 .../{runtime => provider}/src/esplora/mod.rs | 0 .../src/esplora/types.rs | 0 crates/{runtime => provider}/src/lib.rs | 2 - crates/runtime/src/waterfall/mod.rs | 319 ------------------ crates/runtime/src/waterfall/types.rs | 62 ---- crates/sdk/Cargo.toml | 2 +- crates/sdk/src/lib.rs | 12 +- crates/sdk/src/provider/esplora.rs | 2 +- crates/sdk/src/signed_transaction.rs | 135 -------- crates/sdk/src/utils.rs | 5 +- crates/sdk/src/witness_transaction.rs | 1 - crates/simplex/Cargo.toml | 11 +- crates/simplex/src/lib.rs | 8 +- crates/simplex/tests/simplex_test.rs | 173 +++++----- crates/test/Cargo.toml | 4 +- crates/test/src/error.rs | 2 +- crates/test/src/lib.rs | 20 +- crates/user/Cargo.toml | 4 +- 58 files changed, 188 insertions(+), 2083 deletions(-) delete mode 100644 crates/artifacts/Cargo.toml delete mode 100644 crates/artifacts/src/asset_auth.rs delete mode 100644 crates/artifacts/src/lib.rs delete mode 100644 crates/config/Cargo.toml delete mode 100644 crates/config/src/lib.rs delete mode 100644 crates/core/Cargo.toml delete mode 100644 crates/core/README.md delete mode 100644 crates/core/src/assets/test-tx-incl-block.hex delete mode 100644 crates/core/src/blinder.rs delete mode 100644 crates/core/src/constants.rs delete mode 100644 crates/core/src/error.rs delete mode 100644 crates/core/src/fee_rate_fetcher.rs delete mode 100644 crates/core/src/lib.rs delete mode 100644 crates/core/src/runner.rs delete mode 100644 crates/core/src/scripts.rs delete mode 100644 crates/core/src/source_simf/p2pk.simf delete mode 100644 crates/core/src/tx_inclusion.rs delete mode 100644 crates/macros-core/Cargo.toml rename crates/{macros-core/src => macros/src/macros_core}/attr/codegen.rs (98%) rename crates/{macros-core/src => macros/src/macros_core}/attr/mod.rs (68%) rename crates/{macros-core/src => macros/src/macros_core}/attr/parse.rs (100%) rename crates/{macros-core/src => macros/src/macros_core}/attr/types.rs (100%) rename crates/{macros-core/src/lib.rs => macros/src/macros_core/mod.rs} (100%) rename crates/{macros-core/src => macros/src/macros_core}/program.rs (89%) rename crates/{runtime => provider}/Cargo.toml (85%) rename crates/{runtime => provider}/README.md (100%) rename crates/{runtime => provider}/api-esplora.md (100%) rename crates/{runtime => provider}/api-waterfall.md (100%) rename crates/{runtime => provider}/src/elements_rpc/mod.rs (96%) rename crates/{runtime => provider}/src/elements_rpc/types.rs (100%) rename crates/{runtime => provider}/src/error.rs (100%) rename crates/{runtime => provider}/src/esplora/mod.rs (100%) rename crates/{runtime => provider}/src/esplora/types.rs (100%) rename crates/{runtime => provider}/src/lib.rs (63%) delete mode 100644 crates/runtime/src/waterfall/mod.rs delete mode 100644 crates/runtime/src/waterfall/types.rs delete mode 100644 crates/sdk/src/signed_transaction.rs diff --git a/Cargo.lock b/Cargo.lock index 4fe6324..aaa2a83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -667,12 +667,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "hex-conservative" version = "0.2.2" @@ -1809,24 +1803,15 @@ dependencies = [ "bincode", "either", "serde", - "simplex-core", "simplex-macros", - "simplex-runtime", + "simplex-provider", + "simplex-sdk", "simplex-test", "simplicityhl", "tokio", "trybuild", ] -[[package]] -name = "simplex-build" -version = "0.1.0" -dependencies = [ - "simplex-sdk", - "simplicityhl", - "thiserror", -] - [[package]] name = "simplex-cli" version = "0.1.0" @@ -1837,7 +1822,7 @@ dependencies = [ "dotenvy", "electrsd", "serde", - "simplex-config", + "simplex-sdk", "simplex-test", "simplicityhl", "thiserror", @@ -1848,52 +1833,20 @@ dependencies = [ ] [[package]] -name = "simplex-config" -version = "0.1.0" -dependencies = [ - "serde", - "simplex-core", - "simplicityhl", - "thiserror", - "toml 0.9.12+spec-1.1.0", -] - -[[package]] -name = "simplex-core" -version = "0.1.0" -dependencies = [ - "bincode", - "hex", - "minreq", - "serde", - "sha2", - "simplicityhl", - "thiserror", -] - -[[package]] -name = "simplex-macro-core" +name = "simplex-macros" version = "0.1.0" dependencies = [ "proc-macro-error", "proc-macro2", "quote", + "serde", "simplicityhl", "syn 2.0.116", "thiserror", ] [[package]] -name = "simplex-macros" -version = "0.1.0" -dependencies = [ - "serde", - "simplex-macro-core", - "syn 2.0.116", -] - -[[package]] -name = "simplex-runtime" +name = "simplex-provider" version = "0.1.0" dependencies = [ "async-trait", @@ -1903,10 +1856,8 @@ dependencies = [ "reqwest", "serde", "serde_json", - "simplex-core", "simplicityhl", "thiserror", - "tokio", ] [[package]] @@ -1915,7 +1866,7 @@ version = "0.1.0" dependencies = [ "minreq", "sha2", - "simplex-runtime", + "simplex-provider", "simplicityhl", "thiserror", ] @@ -1925,9 +1876,7 @@ name = "simplex-test" version = "0.1.0" dependencies = [ "electrsd", - "simplex-config", - "simplex-core", - "simplex-runtime", + "simplex-provider", "simplex-sdk", "simplicityhl", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 2b7afd3..c9c633d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,13 +12,10 @@ edition = "2024" multiple_crate_versions = "allow" [workspace.dependencies] -simplex-core = { path = "./crates/core" } -simplex-runtime = { path = "./crates/runtime" } +simplex-provider = { path = "./crates/provider" } simplex-macros = { path = "./crates/macros" } -simplex-macros-core = { path = "./crates/macros-core" } simplex-test = { path = "./crates/test" } simplex-sdk = { path = "./crates/sdk" } -simplex-config = { path = "./crates/config" } simplex = { path = "./crates/simplex" } bincode = { version = "2.0.1", features = ["serde"] } diff --git a/crates/artifacts/Cargo.toml b/crates/artifacts/Cargo.toml deleted file mode 100644 index c50113f..0000000 --- a/crates/artifacts/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "simplex-build" -version = "0.1.0" -edition = "2024" -description = "Simplex Build" -license = "MIT OR Apache-2.0" -readme = "README.md" - -[lints] -workspace = true - -[dependencies] -thiserror = { workspace = true } - -simplex-sdk = { workspace = true } -simplicityhl = { workspace = true } diff --git a/crates/artifacts/src/asset_auth.rs b/crates/artifacts/src/asset_auth.rs deleted file mode 100644 index fea1b84..0000000 --- a/crates/artifacts/src/asset_auth.rs +++ /dev/null @@ -1,89 +0,0 @@ -use simplex_sdk::arguments::ArgumentsTrait; -use simplex_sdk::program::Program; - -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; - -// use simplex_macros::simplex_build; - -// #[derive(SimplexBuild)] -// #[simplex("../../contracts/src/asset_auth/source_simf/asset_auth.simf")] -pub struct AssetAuth<'a> { - program: Program<'a>, -} - -impl<'a> AssetAuth<'a> { - // the path is autogenerated - pub const SOURCE: &'static str = ""; - // include_str!("../../contracts/src/asset_auth/source_simf/asset_auth.simf"); - - pub fn new(public_key: &'a XOnlyPublicKey, arguments: &'a impl ArgumentsTrait) -> Self { - Self { - program: Program::new(Self::SOURCE, public_key, arguments), - } - } - - pub fn get_program(&self) -> &Program<'a> { - &self.program - } -} - -// Expanded by macro - -pub mod asset_auth_build { - use simplex_sdk::arguments::ArgumentsTrait; - use simplex_sdk::witness::WitnessTrait; - use simplicityhl::value::UIntValue; - use simplicityhl::value::ValueConstructible; - use simplicityhl::{Value, WitnessValues}; - use std::collections::HashMap; - - pub struct AssetAuthWitness { - pub path: (bool, u64, u64), - } - - pub struct AssetAuthArguments { - pub first: u64, - pub second: bool, - } - - impl WitnessTrait for AssetAuthWitness { - fn build_witness(&self) -> WitnessValues { - WitnessValues::from(HashMap::from([( - simplicityhl::str::WitnessName::from_str_unchecked("PATH"), - Value::tuple([ - Value::from(self.path.0), - Value::from(UIntValue::U64(self.path.1)), - Value::from(UIntValue::U64(self.path.1)), - ]), - )])) - } - - // fn from_witness(_witness: &::simplicityhl::WitnessValues) -> Self { - // Self { - // path: (false, 0, 0), - // } - // } - } - - impl ArgumentsTrait for AssetAuthArguments { - fn build_arguments(&self) -> simplicityhl::Arguments { - simplicityhl::Arguments::from(HashMap::from([ - ( - simplicityhl::str::WitnessName::from_str_unchecked("FIRST"), - Value::from(UIntValue::U64(self.first)), - ), - ( - simplicityhl::str::WitnessName::from_str_unchecked("SECOND"), - Value::from(self.second), - ), - ])) - } - - // fn from_arguments(_args: &simplicityhl::Arguments) -> Self { - // Self { - // first: 0, - // second: false, - // } - // } - } -} diff --git a/crates/artifacts/src/lib.rs b/crates/artifacts/src/lib.rs deleted file mode 100644 index c37f1ff..0000000 --- a/crates/artifacts/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod asset_auth; diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 3a10be4..fa7e940 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -16,18 +16,17 @@ workspace = true [dependencies] simplex-test = { workspace = true } -simplex-config = { workspace = true } +simplex-sdk = { workspace = true } +simplicityhl = { workspace = true } +electrsd = { workspace = true } +thiserror = { workspace = true } serde = { version = "1.0.228" } toml = { version = "0.9.8" } - anyhow = "1" dotenvy = "0.15" clap = { version = "4", features = ["derive", "env"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -simplicityhl = { workspace = true } tracing = { version = "0.1.44" } -thiserror = { workspace = true } tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } ctrlc = { version = "3.5.2", features = ["termination"] } -electrsd = { workspace = true } diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 35d990b..0ee172a 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -1,8 +1,8 @@ pub mod commands; +use crate::config::Config; use crate::error::Error; use clap::Parser; -use simplex_config::Config; use simplex_test::TestProvider; use std::path::PathBuf; use std::sync::Arc; diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index ea5b6c6..73a8ca7 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -1,6 +1,5 @@ use serde::{Deserialize, Serialize}; -use simplex_core::SimplicityNetwork; -use std::fmt::Display; +use simplex_sdk::constants::SimplicityNetwork; use std::path::{Path, PathBuf}; use std::str::FromStr; diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs index 02f0125..779a55a 100644 --- a/crates/cli/src/error.rs +++ b/crates/cli/src/error.rs @@ -25,5 +25,5 @@ pub enum Error { /// Errors when building config. #[error("Occurred error with config building, error: {0}")] - ConfigError(#[from] simplex_config::ConfigError), + ConfigError(#[from] crate::config::ConfigError), } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 4e66fdb..c7a1415 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,3 +1,4 @@ pub mod cli; +pub mod config; pub mod error; pub mod logging; diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml deleted file mode 100644 index 55cf923..0000000 --- a/crates/config/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "simplex-config" -version = "0.1.0" -license.workspace = true -edition.workspace = true - - -[lints] -workspace = true - - -[dependencies] -simplex-core = { workspace = true } - -simplicityhl = { workspace = true } -serde = { version = "1.0.228" } -thiserror = { workspace = true } -toml = { version = "0.9.8" } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs deleted file mode 100644 index e42f44a..0000000 --- a/crates/config/src/lib.rs +++ /dev/null @@ -1,159 +0,0 @@ -use serde::{Deserialize, Serialize}; -use simplex_core::SimplicityNetwork; -use std::fmt::Display; -use std::path::{Path, PathBuf}; -use std::str::FromStr; - -const MANIFEST_DIR: &str = "CARGO_MANIFEST_DIR"; -const CONFIG_FILENAME: &str = "Simplex.toml"; - -#[derive(thiserror::Error, Debug)] -pub enum ConfigError { - /// Standard I/O errors. - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - /// Errors when parsing TOML configuration files. - #[error("TOML parse error: {0}")] - TomlParse(#[from] toml::de::Error), - - /// Errors when parsing TOML configuration files. - #[error("Unable to deserialize config: {0}")] - UnableToDeserialize(toml::de::Error), - - /// Errors when parsing env variable. - #[error("Unable to get env variable: {0}")] - UnableToGetEnv(#[from] std::env::VarError), - - /// Errors when getting a path to config. - #[error("Path doesn't a file: '{0}'")] - PathIsNotFile(PathBuf), - - /// Errors when getting a path to config. - #[error("Path doesn't exist: '{0}'")] - PathIsNotEsixt(PathBuf), -} - -#[derive(Debug, Default, Clone)] -pub struct Config { - pub provider_config: ProviderConfig, - pub test_config: TestConfig, -} - -#[derive(Debug, Clone)] -pub struct ProviderConfig { - simplicity_network: SimplicityNetwork, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct TestConfig { - pub rpc_creds: RpcCreds, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub enum RpcCreds { - Auth { - rpc_username: String, - rpc_password: String, - }, - #[default] - None, -} - -#[derive(Debug, Default, Clone)] -pub struct ConfigOverride { - pub rpc_creds: Option, - pub network: Option, -} - -impl Default for ProviderConfig { - fn default() -> Self { - ProviderConfig { - simplicity_network: SimplicityNetwork::LiquidTestnet, - } - } -} - -impl Config { - pub fn discover(cfg_override: &ConfigOverride) -> Result, ConfigError> { - Config::_discover().map(|opt| { - opt.map(|mut cfg| { - if let Some(test_conf) = cfg_override.rpc_creds.clone() { - cfg.test_config = test_conf; - } - if let Some(network) = cfg_override.network { - cfg.provider_config.simplicity_network = network; - } - cfg - }) - }) - } - - pub fn load_or_default(path_buf: impl AsRef) -> Self { - Self::from_path(path_buf).unwrap_or_else(|_| { - if let Ok(Some(conf)) = Self::_discover() { - conf - } else { - Self::default() - } - }) - } - - fn _discover() -> Result, ConfigError> { - let path = std::env::var(MANIFEST_DIR)?; - let path = PathBuf::from_str(&path).unwrap(); - let path = path.join(CONFIG_FILENAME); - dbg!(&path); - if !path.is_file() { - return Err(ConfigError::PathIsNotFile(path)); - } - if !path.exists() { - return Err(ConfigError::PathIsNotEsixt(path)); - } - dbg!(3); - Ok(Some(Config::from_path(&path)?)) - } - - fn from_path(p: impl AsRef) -> Result { - std::fs::read_to_string(p.as_ref())?.parse() - } -} - -impl FromStr for Config { - type Err = ConfigError; - - fn from_str(s: &str) -> Result { - let cfg: _Config = toml::from_str(s).map_err(ConfigError::UnableToDeserialize)?; - Ok(Config { - provider_config: ProviderConfig { - simplicity_network: cfg.network.unwrap_or_default().into(), - }, - test_config: cfg.test.unwrap_or_default(), - }) - } -} - -#[derive(Debug, Serialize, Deserialize)] -struct _Config { - network: Option<_NetworkName>, - test: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "lowercase")] -enum _NetworkName { - #[default] - Liquid, - LiquidTestnet, - ElementsRegtest, -} - -impl Into for _NetworkName { - fn into(self) -> SimplicityNetwork { - match self { - _NetworkName::Liquid => SimplicityNetwork::Liquid, - _NetworkName::LiquidTestnet => SimplicityNetwork::LiquidTestnet, - _NetworkName::ElementsRegtest => SimplicityNetwork::default_regtest(), - } - } -} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml deleted file mode 100644 index 513aead..0000000 --- a/crates/core/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "simplex-core" -version = "0.1.0" -edition = "2024" -rust-version = "1.90" -description = "High-level helpers for compiling and executing Simplicity programs on Liquid" -license = "MIT OR Apache-2.0" -repository = "https://github.com/BlockstreamResearch/simplicity-contracts" -homepage = "https://github.com/BlockstreamResearch/simplicity-contracts/tree/dev/crates/simplicityhl-core" -readme = "README.md" -documentation = "https://docs.rs/simplicityhl-core" -keywords = ["simplicity", "liquid", "bitcoin", "elements", "taproot"] -categories = ["cryptography::cryptocurrencies"] - -[lints] -workspace = true - -[features] -encoding = ["dep:bincode"] - -[dependencies] -thiserror = { workspace = true } -bincode = { workspace = true, optional = true } -sha2 = { workspace = true } -hex = { workspace = true } -simplicityhl = { workspace = true } -minreq = { workspace = true } -serde = { version = "1.0.228" } \ No newline at end of file diff --git a/crates/core/README.md b/crates/core/README.md deleted file mode 100644 index caf7a9d..0000000 --- a/crates/core/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Simpelex HL Core - -This crate provides useful utilities for working with Simplicity on Elements. - -- `blinder.rs` — derives deterministic blinder keypair from a "public secret" -- `constants.rs` — Liquid network constants (policy asset IDs, genesis hashes) -- `explorer.rs` — explorer API utilities (behind `explorer` feature) -- `runner.rs` — program execution helpers with logging -- `scripts.rs` — P2TR address creation, Taproot control block, and asset entropy utilities -- `lib.rs` — P2PK program helpers and transaction finalization - -Consider this more like a test helper tool rather than a production-ready version. - -## License - -Dual-licensed under either of: -- Apache License, Version 2.0 (Apache-2.0) -- MIT license (MIT) - -at your option. diff --git a/crates/core/src/assets/test-tx-incl-block.hex b/crates/core/src/assets/test-tx-incl-block.hex deleted file mode 100644 index e957c2b..0000000 --- a/crates/core/src/assets/test-tx-incl-block.hex +++ /dev/null @@ -1 +0,0 @@ -000000207e3dba98460e4136659f0fccf3e59338dfe53ed5f094fb0bb94d771c48341854d875900105c87e5dd46c740cb1129c06f8f4007e868f61b25e37cffa946c718d8742805b01000000015100030200000001010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0201230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000009b64001976a914608c0ea8194a8ceb57f0196f44a6b48a54fc065988ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000000000266a24aa21a9ed8f8a98e5623643b24167266c2648ead4a50d18b0491c6f34e11398aaee0ca6e8000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000020000000001eb04b68e9a26d116046c76e8ff47332fb71dda90ff4bef5370f25226d3bc09fc0000000000feffffff0201230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b20100000002540bd71c001976a91448633e2c0ee9495dd3f9c43732c47f4702a362c888ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000000ce4000000000000020000000101f23ceddac67cfbbc997199daa651384d0746fb2a5482b8c8629ba8df4b788f75000000006b483045022100e0feb3e2f292000d67e24b821d87c9532230dac1de428d6a0068c9f416583abf02200e76f072788dd411b2327267cd91c6b1659809598cd4fae35be475efe1e4bbad01210201e15c23c021652d07c1557b607ea0379fca0462aca840d6c33c4d4927524547feffffff030b60424a423335923c15ae387d95d4f80d944722020bfa55b9f0a0e67579e3c13c081c4f215239c77456d121eb73bd9914a9a6398fe369b4eb8f88a5f78e257fcaa303301ee46349950886ae115c9556607fcda9381c2f72368f4b5286488c62aa0b081976a9148bb6c4d5814d43fefb9e330575e326632136389c88ac0bd436b0539f5497af792d7cb281f09b73d8a5abc198b3ce6239d79e68893e5e5d0923899fd35071ba8a209d85b556d5747b6c35539c3b2f8631a27c0d477a1f45a603d1d350b8cbf900f7666da66541bf6252fc4c162141ad49c670884c93c57db6ba1976a9148c7ab6e0fca387d03643d4846f708bf39d47c1e988ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000008e800000000000000000000043010001dc65ae13f76fde4a7172e0fb380b1a5cc8dc88eaa0659e638a25eac8ae30d79bf93eb7e487eeee323e4ac8e3a2fe6523bdeba6acce32b9b085f2286174c04655fd6c0a6020000000000000000178ad016b3e5d8165423e56d8b37e3eaee96009b2f970043ccf65d61b5c3c1e1ef343e0c479bdba442717dc861c9591566010240b9d4607efb9252a5fcef05edf640e0bb6b606729246ad07baa49d0d3b52042c65a03ca737744e45b2d2d6d177c36569ae9d6eb4437305b169bbc59f85cabff3bc49a2d6d08c177cce3121a509d3c47961bd22e35c932b79d4ec5ccaf913fac04034bfebdadbc4ff3127af96344b02ee6b967bb08326cbe6a4e1c924485e64a8c0fdf70b98c99f38acaa15aa0adb2b5b7335ed5502443891bcd657310347cbd928f40f38f1dec087a2b947c9cf7d304798f77bbc4a2c843796b2d49acce91de4e88a0a9c261277df28ffc3320d7f7d64790f592ddded48a1068ef88271395fa5606389ef90856ddd6bd6710a8d27e0147983b5dde2a7efae44e83ad02a3c3da04be43d5f2c05c205f1e17b48554c2177670f46dbb6600bd2e6c75dd5ea2e1072c5f22483dcf05d8124e3f9063a5ddb179a29c23a2d15d6e89f2192f03dae5938f66fcdcff000c5a96ffd2920f23881880af72153c96a56dd80c218bb48b44a18e54a8050ff32c869c1264ee574cdb4002f86e0779c724d11dc4a768dbec1bd22054886f1fdf2e7347e4c247b829159d1375f881c6ce0a5c4da8534000e7fec3a980afb1edc99b725c29de80f260dcf144c873bf589ae1812ef6cb05f2234f9c66c23e874a0d5d0dc52f2209e015bbcf74ee449a397f6b0318c915b7e58dea5904abbe35285e90ccf548ad1f3f52f60c3b19b3cd67644d633e68aef42d8ef1782f22a8edd0620f55f29070720ca7a078ac83e87b9ebd2783ecad17dd854ef1bbd319f1a6d3a1e4931f9097422f5a3c4af037b99e06c7610ee61102c6eea763af108e9a16b93b2dc0891658d5c6a197df6aae9b306b2c895d21c79cb6cb6dd85b4018b0a9fe7468336e3907eb4adcaf930cacc97e8e951d2d6b25744a4143679bad1f31b210c9a2ed54b80d8f5d7dc1f1c985681534c1926920cd683d95dca7e8ea285f9906d2e89cd8bfa76a98e38ee4b5152522d55f79610fe8d5278fe6ed5866b5da4dcf330ea84307c34f30e1a66eb1934dafebb0074fc27c2ff73d8c0bae8416cc87bf611f81119aba9e2a911beaf3ac9507e621fc1ed1cf15dfb31408cf55e2bfdd2880db2d3489a336d6f8348347648d882f9f376331e469e809115c6cc82468f363c910673e9ded172ded90a369e1cdd135676f623e11a1531ed221177812b1ef0c65e5ca92c0df8de7fe664710f3228a226e019c99607fe1395ecd5643e1c7ad8a132bf5131737cb970a7f0dabc00029755bf71b3f47bd69ba39b3ab104c74f04239f4919dca1dfce7c9c41cba9d449073e106ebabe3c313b598ee8b11702ec46e9ee53fb9422f0326371898b8fa4c21a951684c687398e0bebd6f6fd91b829e8666b9a19a4273cfda0f34b8ecb902f7adc6539fb9a0cba6f87a63a957acfb2dfa18973f4a3063668767b2be780311513c63f1814f082176f6a953f2ffaa49ec9b39fecc2eab603be7a969bb4c1dbebf8d39fa90f802d5ea52378b5025a19b64a8c2c2dd6a6133bd8d29730bd5724b5bf50c158b238d1137082937ad91a176aaf91577868db7581b457c917e612b242ce0065ad47e11dcdc1fc6158687142249bcf312497a547b6f43e795af7d4ae8cd022e44e417987e35e83de21e39dcdf86b97bd421e6e61881a432fa2284f20be80e32459443736b875d9036468ceb881589394441e2d10aa10b6c93332951e8ba56f89fac70baf415b4511873c0f3e418ca4fe8954a28f1f7b5f590d34470119f694e2712f184882d90396c8e6aa850eaa3c2ae51990543638c46c59512167a2c5ad593532dc2142ffb6560476e4159213b9ef017ec75310d2e4624a405bb26f7192a485a94890674928c9caa4a5819ca4ddcba8fa71afc1a6baf63f039452c8fe994f8b63d58c876dfddd61a476345eaed4f66bdc0fcfc38d485c6a5b0e27d0fbc50427ff591ba38d63445c01642cfbd7d4c032f2546a6fe80bc3b598362502c552049523fe360c3bcf1cc572feb04386f97d55871dd8cea0393cdd964e724082adc98126e6f2fe1d576be4bf911e9aca70e35538175f8382bbcd614bbecc97c9607ef25da2ff08a6e5b6f76cbe9ccb0e0fdc3528e3e2c3675a5c897d295bb76524ec8a73a70b97909368f44d92f9aceaef0b03f3dafa1faa89fc663a92da3c19b4952463fac0e825e78cf046e266cfb9975af72e9d50d2c2cafee88fe2cecae2b1465fc07b280d83b66062dc9e7a372f81aec8e0bb9e97877814a5a6813c67746e35cd068d45d8664528bd00d5a306a5319e1bea7f38345da92d3a10d91476a26aed6b8441f0f72fbbad5d5e0f8ae5cabc9f4f08e6be7902b5c53632db5264afee7422c87b3237a32d5213ad0eb807b61977d9d90666cbb0c70500526b0eb762c99351796db41166b0aa2f221b5607e0d629fac4e938488245c11557381a4f8addcc49913b11d42481cf8668e37bacbad4a20509e4fe4ccbcee7aea2909a2abe59052f7f28b9340cd92f69729d615b8d3b530941c0b30506498cd4e561a9c82d915266bb7115967bc76c5593c06d094bdf4294b868afc5fa52742d3bdbd5932df599f0e1187c49f0dba8679c771a514cc9da75e03506957800bf470d4a07c4bb8918d6085499bb8ceeaba23c0b465863327e9ab8b6b8cf8b3ca530ca7b02cfadf85437b750f305e8fbc8855c95bee8595a7e9e1f0993a03adbadc68665a18936cc99b6530b4518c0754990d7bfdfdac76f88cfcbcb7b3d9a71ee10cbd3a1bdbc2e50b642c1fef56511962f845bbec6eab727b1d4add335db8d80c4c07e8356ad05adad68b012489fa5bb5d9019a667778ddf7f5edd80f1d3c4abd64397a89e554c8007809336ddc2b2e7d5219c39fdf39aad33b9350f6b18fe3b98c690b9068f36d4b7669530fd216373842fbf70fe9bbe80854b31eed4bd515d6caeb065d6c609846c9bfae1b3fce3db70b5bfb448ec69512e7f25019c789301b77a75f2a0f81c65ec29f41bf96d597a00c310e8ba4b48ac82b5a735c1e83f22394eb2fc9b35d42a35533c938f26290a5860175637982f1733c99be39c44ac4a09187406306bde2fd3d28e4e7bda73719912c338804dea03987757dac4d73def665e11da126f9414f71624a3b753797eb0472bd334094515c4f9fe57fdd8d185f22b4bf82e4b5f6b800870cce19a0c8174dc11ee9f1cb9ffe0ac6f6fff1ebf7c915c7ae20172bb70390e3759912e0e0a4e83a0a2d2318f4386314a89f6438ccb331f89377ff7947fe4b24f788aef85c1656ca87ee41c959f1b09bde09f20c2a51ac481646b28e9b0fc2ff49cfe8cf28577bf5bf6f261f54f97fcd2875da4210c6dfe685450280b68e378d9a486243cc682ed4ec747c37de1fde848e4a8f70498d22e40c462c469c884cd67330e77b694e759232313f31a1624e0e1960f23ddae47b68ff553d0de0910c8abe2e8e5fb063aa744ff77465fc731c7af79a84dcaa9b3f741a46dd3c932877d49242c6d883e14392b8c4530986605812b636a73590ef437f27e40d1af37ed1cbd68fb4e9ca5b0b41e5daee0142c1bf59c9d71f6c19b25e6148dfbb9fb142107aabe3701e36611a7e0b13ea32d3c5f8a51f63c5f34415baa15f6ca77300eb323241ffe73c5acd97fcb682c21dc8911392979e9cb81be5218acf452b5b93f6681d323b7989fdd10efe6fe9e2ac88d0d76a4cf3ee45e3b5c430100014142c1fc7e8a658eff437594a25cf34d269556d8511918f27fdc7e9d6dd73f0e4790b91f225e9d131e6abb3dbfb66549a9aa57948fbd2f183fcd951b1d2305bffd6c0a602000000000000000016f5cdf9fb6c1b5e98a36befdc2c55bd4fd8793d554b2506f51c909362495e1216ee83cd270ddb0a00785600ba23bd3363f0798e3a7a117990415adec88e61be65170bd587ab4d2ee38edb22a91e5c29afa397dd5a73465c51c6263f5fbde47fa801ce84464acc32589acaafadfe44d6558774b7085612a88f3424b6dca3c6f07217d1cbd5c41bda46a6a492a0119c1de4d25b58c94250bee3fba6b8223777535673a2f4da6af27598030f88144f408120f07ca9c98d5d9edcdf6cdc9073f118fce55e6c9d0be80b5e87992ddaa9c22053b3a00d42bdedc9768de25c0b37a5c4fb4e86710b33cebed5588d88adde607f6bca14f0279ce35126d403ffa50f288c87f528c19749ed43bd846c513fcd92c173fe76d8f2e69770439d3d075cb19b1094a42ee07ae1de197e8c136e2bc688a75a74db24adb0fbb73872dc80074f61c9cce9bd33861bdd921ee3edacab1d6e7cec325c172b6b6e82ada11687e4fc931225074dd1f20a0f9342dbce1fc3fdbf5bb6cb74ab6475e574e9f5f247a2f7e4fcfcc354d4da8c8066e574642c7fccbbb9ef0aa592ecab5366fe87eb8e14cd64aee34578aa48f68f8f4c5372df2c3fc429f5a3e39ef6c034c87f9c52b2ea35e28c7bf3be737c3817efd6569466dc859e8ff8965c5249b6f045934d3d08b0ffd388aec58df8194ac2c4fec2152942d2626595e65664b1fa33b5dae8ee796a840a56d885cbf7ae6483fad05e507ada3f075ebce0d791b626c6dfe93f8492c4dd3b34aafc33d7644c5c8e38bfd8c19194f65be88fcb4538778632e489a626896372fdd2498b16e64daa7d3c5cfac688d6f9cdf3717261b0a1f25be1bdd6be6558ddb826fa04b5f668810a291aea51a6f05ff7c34dcf81c74849a8015bad5e4e416989b10ef01de304775db725fa0b665f4330dc9c540dc29aab144837362a97d6bb0165cb3272338c2d32386cd95ee3e66d876b591a25a6907237523cf908f736d2fdc8e54ea8d9c7562697161d1f72fc4d7b775052415cd0e5ae5bdf6edfab5776b6ff75ce5e1f8f2beea6ec74252b63966cca58abd638279dc5c998a1068079f3e5dcc8a69165c304c3d8c362ccfadab05ad12208a5655ab389eb727e8ed5f86b300331a13be26e2fbabf89fbfd2b98481dd5edb52ed456a0e03a84b6f89761f91ff251412f5cfa286e35fb9f48ef0e044c4742b6e860a08767ecb80548c2f3df3b371cdb40e86dbe118f64e84faf45ecb78d73364e9e31e3412ca2a3fad0a35983370ea9e6264a222edd1fd4aca30e3c169d7ca2d07609262e786ecd019c1417a06b7dfa32a54e0897afdc6492f26611555cbff47dba3b76381f239d597a8f687669333e0b47b53d5bcc4fea1919490bad3c6f0b6a58a50aca7ddeb9745ead454e0a38d9486fb52aefe0dbb92bf7fd6c215078aba3482b11274ec8cddff92c359bbc6d20bd823ad0bbf859cfaadf8e775b3d37b3078319f46c6d2a112cf60a673fee467538c70f1687d97fbe9d9f8a0856061592a4e00b6d10e979e674dd2cd0ba8b853f733877cd508062d5f723d58d215ad69c2be6be742496aef54eb87338622eb36a9bbc5a7a602d280a45e095b1e078dab54479e783a513c722066acaae44ccc15f9560da91ed053ec05c36d82f6809766876c45c4fbeb2321d50f48f7995437d0c5fc365974a571fb0352d28cb1cdbd21d69fab576a2e68d6b881776027bcdb7f01be22b1c847d91f26e680ef6ab2c128a89b59432383d9bd661b0b01432cf8a25319426d38ac2e2114825f59b4250569c798b1094920bb31130728313ff56a6eef2e6c4b275215dce3786d0f9024952b5f572566c53597e7ef4ab1f75743e605a564054d667f48906b5481d924769ef65751e349891d725a2c1bf8b102fea4c25c874d2fc2ce1bfec4b39bea76fbf7a28855725d52b595a4fc96892c3f1f961d46310ebd5221df729c02060035c559baf0fd7efa73a2213ca29642857aeb8ebf7efdf9d2f5c84746b6fc35ab355a8dca56e7dde4831e47ca1be6b62af30cfcf807c384e56ab84ff03bbe786251e6c4b932c9217bf671046217bd0511fdc06aa69050c1480281e4843eb73d80095a2fb8e68a2c0c98c9aea637b99d87ad847a3a76d59ea308c751f9cb4a4fce2989822bd6ba2f901f09df647536dc30730ea3160dd35b8c6dcc9aa815b79ed492a8a299a298ccdf784b9b0211ca877ec1723817c98529acaa4d3727162b5740b0fc9b498dfb2212a3cbf0c63dc4f7663fafad7905643a792862b651e8497b0f0da632b897ecf9ee63f2b20b54fa5eb2f2e424dcce5a075f50b856af266655be3a815fc83ed8027508b2536976982196b160e2219ffdb5c7a56dd3e6b700860c711f4439dbf72973f4f26fe3260ec43a3446fe14444b9787d877e107be610147eec4a3574745e95a1f424aff062f84c559d13b1e6b59e8dc2221515c229f07db8eb39c515a321d8bd07b1bd6c9a79dac6d951c04415553c7a2ce1eb77495c7f89c4d5b4cffd289435b69bc53585095083cc5a1b191781342266e204e1566aca8175e2ae84a8bd711d188b666dfb65a6442776d3e23c1b5192af09ec712537f2157d0ccbc1bb3b3a1969d9705671f16bdc266e615ad2e50a8cbd666f3ee7465cc430c6cd69d30c91e717b12f7094b6f0ef89134d6c1620d28d8f238c181146448b348e4ca2e93c737210350f18fb878fb91b70ecc5689e5b6101ecfc545f6a1c903115b0c6419c91a50fb2dbe2edd362f2815f0c75070974507c34130ac9b29747ff7efbe6e37ee4c62be3ecfedfa817fdf3309163aaff677775b77f0d288c9858cfe59cb0fa18afa591e7d574eaef43c82e79d71542c4177de4e5bd724b18cfd33c68530665728a9d5ef192772094acbf3d885d5146c1634e74754e3fbcb94fa349eac8280cfd7d1f46a0813b57a83bd078b1f7cb5a60a59b59380fe04e1c600c33b33d1add69a9ff1be546f0ec5c0083979fce940b23711f382ac0d011c1103f02cb6082c18e39cf7a9c3bf4c081f905ae7b87951a7880b57e934465ccd634e5a17fd8d8866abfdfebd33b2c3d2c5be58144900c04e9c18de0c80270660e62a3c185277555f89da4c41bd33cec1359f4ed21abdb586e1d97f720a92d16014d7f1822f1836f74c97cb7f7b38e073477c6ab064fde835916c1e624de81f2ad90f6260073c5e1848582860f033630bde225821b39c2572b30c36adf8fdb8317c33df05f6413447f4985d12e9012629df09dc8f43373a6d0db4b0048453a6f1ec662472c77a30d5cf4ac7084f736d0d598c251f2aefc986052fbf12a657885d7140ad36b07c63ab86388a2be12d943747f3f29ef9f2e11e1444cc873df0ed7826eef675389a0d5a0388a8504fe89c4791ea4a572bfd406d5f01418b4f888c9a7a566e32811936bf6950bbf786b86c41c28f2045d31953fcd15f179e7bc00c72870890537921f7deff82270b0e44b88720aa738f60a85567deb7c90b0c2444467621e53e1c079436d31d3d0b34dd237fc281eb9d87175237a9a433142db4bb7f8c4cb6a34e2dc73f074045d216695ce88ef68e18564c935c9cbd902e939655c258de2ab78def8746bffd972083afce3b6881b7147262e1a44e0224689fafa1a3cb823c8da6eb7df091bec0638bf728b7b10aa95f2bce512ec8d3252938d2eb77b44ace7a2f976588032cac5af670f9e5ca25cb0721bc1baec26f9c3a9f41b02fb62997d6cb0a01314845e9d0e78139ea49f2ead8736e0000 \ No newline at end of file diff --git a/crates/core/src/blinder.rs b/crates/core/src/blinder.rs deleted file mode 100644 index 8754bdc..0000000 --- a/crates/core/src/blinder.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::PUBLIC_SECRET_BLINDER_KEY; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::secp256k1_zkp::SecretKey; - -/// Derives a deterministic blinder keypair from the hardcoded public secret. -/// -/// # Panics -/// -/// Panics if the secret key bytes are invalid (should never happen with valid constant). -#[must_use] -pub fn derive_public_blinder_key() -> secp256k1::Keypair { - secp256k1::Keypair::from_secret_key( - secp256k1::SECP256K1, - &SecretKey::from_slice(&PUBLIC_SECRET_BLINDER_KEY).unwrap(), - ) -} diff --git a/crates/core/src/constants.rs b/crates/core/src/constants.rs deleted file mode 100644 index 70f8f20..0000000 --- a/crates/core/src/constants.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Common Liquid network constants and helpers. -//! -//! Exposes policy asset identifiers and the Liquid testnet genesis hash. -//! -//! These are used throughout the CLI and examples to ensure consistent -//! parameters when constructing Elements transactions. - -use simplicityhl::simplicity::elements; -use simplicityhl::simplicity::hashes::{Hash, sha256}; -use std::str::FromStr; - -pub const PUBLIC_SECRET_BLINDER_KEY: [u8; 32] = [1; 32]; - -/// `PLACEHOLDER_ISSUANCE_VALUE` constant is used for issuance and reissuance tokens during the blinding process. -/// -/// During blinding, the PSET collects surjection proof inputs from all outputs with matching asset IDs. -/// For issuance tokens, only the `asset_id` from `TxOutSectet` is copied into surjection proofs while the `value` is set to `0`, -/// as the exact value inserted for the issuance token is irrelevant to the proof computation. -/// This is because issuance and reissuance tokens surjection proofs only care about the `asset_id` or `token_id`, not the token value. -/// -/// See: `` -pub const PLACEHOLDER_ISSUANCE_VALUE: u64 = 0; - -/// Policy asset id (hex, BE) for Liquid mainnet. -pub const LIQUID_POLICY_ASSET_STR: &str = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"; - -/// Policy asset id (hex, BE) for Liquid testnet. -pub const LIQUID_TESTNET_POLICY_ASSET_STR: &str = "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; - -/// Policy asset id (hex, BE) for Elements regtest. -pub const LIQUID_DEFAULT_REGTEST_ASSET_STR: &str = "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"; - -/// Example test asset id (hex, BE) on Liquid testnet. -pub static LIQUID_TESTNET_TEST_ASSET_ID_STR: &str = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; - -/// LBTC asset id for 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, - 0x90, 0xca, 0xc1, 0xbc, 0x3a, 0x6f, 0x6d, 0x71, 0xaa, 0x44, 0x43, 0x65, 0x4c, 0x14, - ])) -}); - -/// 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, - 0xa1, 0xa3, 0x59, 0xa0, 0x4c, 0x94, 0xb2, 0x0d, 0x22, 0x36, 0x58, 0x27, 0x66, 0x14, - ]) -}); - -/// 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, - 0x82, 0x99, 0x9a, 0x1e, 0xed, 0x81, 0xd5, 0x6a, 0xee, 0x52, 0x8e, 0xda, 0x71, 0xa7, - ]) -}); - -/// Genesis block hash for Liquid regtest. -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, - 0x6a, 0xd9, 0x15, 0xc8, 0xd9, 0xb5, 0x83, 0xca, 0xc2, 0x70, 0x6b, 0x2a, 0x90, 0x00, - ]) -}); - -/// The network of the elements blockchain. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum SimplicityNetwork { - // Liquid mainnet policy asset - Liquid, - // Liquid testnet policy asset - LiquidTestnet, - /// Liquid regtest with a custom policy asset. - ElementsRegtest { - /// The policy asset to use for this regtest network. - /// You can use the default one using [`SimplicityNetwork::default_regtest()`]. - policy_asset: elements::AssetId, - }, -} - -impl SimplicityNetwork { - /// Return the default policy asset for regtest network. - /// - /// # Panics - /// - /// Doesn't panic as constants are defined correctly. - #[must_use] - pub fn default_regtest() -> Self { - let policy_asset = elements::AssetId::from_str(LIQUID_DEFAULT_REGTEST_ASSET_STR).unwrap(); - Self::ElementsRegtest { policy_asset } - } - - /// Return the policy asset for specific network. - /// - /// # Panics - /// - /// Doesn't panic as constants are defined correctly. - #[must_use] - pub fn policy_asset(&self) -> elements::AssetId { - match self { - Self::Liquid => elements::AssetId::from_str(LIQUID_POLICY_ASSET_STR).unwrap(), - Self::LiquidTestnet => elements::AssetId::from_str(LIQUID_TESTNET_POLICY_ASSET_STR).unwrap(), - Self::ElementsRegtest { policy_asset } => *policy_asset, - } - } - - /// Return the genesis block hash for this network. - #[must_use] - pub fn genesis_block_hash(&self) -> elements::BlockHash { - match self { - Self::Liquid => *LIQUID_MAINNET_GENESIS, - Self::LiquidTestnet => *LIQUID_TESTNET_GENESIS, - Self::ElementsRegtest { .. } => *LIQUID_REGTEST_GENESIS, - } - } - - /// Return the address parameters for this network to generate addresses compatible for this network. - #[must_use] - pub const fn address_params(&self) -> &'static elements::AddressParams { - match self { - Self::Liquid => &elements::AddressParams::LIQUID, - Self::LiquidTestnet => &elements::AddressParams::LIQUID_TESTNET, - Self::ElementsRegtest { .. } => &elements::AddressParams::ELEMENTS, - } - } -} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs deleted file mode 100644 index 22b1afb..0000000 --- a/crates/core/src/error.rs +++ /dev/null @@ -1,52 +0,0 @@ -/// Errors that occur during binary or hex encoding/decoding operations. -/// -/// These errors are returned by the [`Encodable`](crate::Encodable) trait methods -/// when serializing or deserializing data. -#[cfg(feature = "encoding")] -#[derive(Debug, thiserror::Error)] -pub enum EncodingError { - #[error("Failed to encode to binary: {0}")] - BinaryEncode(#[from] bincode::error::EncodeError), - - #[error("Failed to decode from binary: {0}")] - BinaryDecode(#[from] bincode::error::DecodeError), - - /// Returned when a hex string cannot be parsed. - #[error("Failed to decode hex string: {0}")] - HexDecode(#[from] hex::FromHexError), -} - -/// Errors that occur during Simplicity program compilation, execution, or environment setup. -/// -/// These errors cover the full lifecycle of working with Simplicity programs: -/// loading source, satisfying witnesses, running on the Bit Machine, and -/// validating transaction environments. -#[derive(Debug, thiserror::Error)] -pub enum ProgramError { - #[error("Failed to compile Simplicity program: {0}")] - Compilation(String), - - /// Returned when witness values cannot satisfy the program's requirements. - #[error("Failed to satisfy witness: {0}")] - WitnessSatisfaction(String), - - /// Returned when the program cannot be pruned against the transaction environment. - #[error("Failed to prune program: {0}")] - Pruning(#[from] simplicityhl::simplicity::bit_machine::ExecutionError), - - #[error("Failed to construct a Bit Machine with enough space: {0}")] - BitMachineCreation(#[from] simplicityhl::simplicity::bit_machine::LimitError), - - #[error("Failed to execute program on the Bit Machine: {0}")] - Execution(simplicityhl::simplicity::bit_machine::ExecutionError), - - #[error("UTXO index {input_index} out of bounds (have {utxo_count} UTXOs)")] - UtxoIndexOutOfBounds { input_index: usize, utxo_count: usize }, - - /// Returned when the UTXO's script does not match the expected program address. - #[error("Script pubkey mismatch: expected hash {expected_hash}, got {actual_hash}")] - ScriptPubkeyMismatch { expected_hash: String, actual_hash: String }, - - #[error("Input index exceeds u32 maximum: {0}")] - InputIndexOverflow(#[from] std::num::TryFromIntError), -} diff --git a/crates/core/src/fee_rate_fetcher.rs b/crates/core/src/fee_rate_fetcher.rs deleted file mode 100644 index 1036740..0000000 --- a/crates/core/src/fee_rate_fetcher.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::collections::HashMap; - -/// Fee estimates response from Esplora. -/// Key: confirmation target (in blocks as string), Value: fee rate (sat/vB). -pub type FeeEstimates = HashMap; - -/// Default Target blocks value for using `DEFAULT_FEE_RATE` later -pub const DEFAULT_TARGET_BLOCKS: u32 = 0; - -/// Default fallback fee rate in sats/kvb (0.10 sat/vB). -/// Higher than LWK default to meet Liquid minimum relay fee requirements. -pub const DEFAULT_FEE_RATE: f32 = 100.0; - -/// Error type for Esplora sync operations. -#[derive(thiserror::Error, Debug)] -pub enum FeeFetcherError { - #[error("HTTP request failed: {0}")] - Request(String), - - #[error("Failed to deserialize response: {0}")] - Deserialize(String), - - #[error("Invalid txid format: {0}")] - InvalidTxid(String), -} - -pub trait SyncFeeFetcher { - /// Fetch fee estimates for various confirmation targets. - /// - /// # Errors - /// - /// Returns error if the HTTP request fails or response body cannot be parsed. - fn fetch_fee_estimates() -> Result; - - /// Get fee rate for a specific confirmation target. - /// - /// Fetches fee estimates from Esplora and returns the rate for the given target. - /// If the exact target is not available, falls back to higher targets. - /// - /// # Arguments - /// - /// * `target_blocks` - Desired confirmation target in blocks (1-25, 144, 504, 1008) - /// - /// # Returns - /// - /// Fee rate in sats/kvb (satoshis per 1000 virtual bytes). - /// Multiply Esplora's sat/vB value by 1000. - /// - /// # Errors - /// - /// Returns an error if the `fetch_fee_estimates()` fails or no suitable fee rate is found. - #[allow(clippy::cast_possible_truncation)] - fn get_fee_rate(target_blocks: u32) -> Result { - if target_blocks == 0 { - return Ok(DEFAULT_FEE_RATE); - } - - let estimates = Self::fetch_fee_estimates()?; - - let target_str = target_blocks.to_string(); - if let Some(&rate) = estimates.get(&target_str) { - return Ok((rate * 1000.0) as f32); // Convert sat/vB to sats/kvb - } - - // Fall back to higher targets (lower fee rates) - // Available targets: 1-25, 144, 504, 1008 - let fallback_targets = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 144, 504, 1008, - ]; - - for &target in fallback_targets.iter().filter(|&&t| t >= target_blocks) { - let key = target.to_string(); - if let Some(&rate) = estimates.get(&key) { - return Ok((rate * 1000.0) as f32); - } - } - - // If no higher target found, try any available rate (use lowest target = highest rate) - for &target in &fallback_targets { - let key = target.to_string(); - if let Some(&rate) = estimates.get(&key) { - return Ok((rate * 1000.0) as f32); - } - } - - Err(FeeFetcherError::Request("No fee estimates available".to_string())) - } -} - -pub struct EsploraFeeFetcher; - -impl SyncFeeFetcher for EsploraFeeFetcher { - /// Fetch fee estimates for various confirmation targets. - /// - /// Uses the `GET /fee-estimates` endpoint. - /// Note: Liquid testnet typically returns empty results, so callers should - /// use a fallback rate (see `config.fee.fallback_rate`). - /// - /// Returns a map where key is confirmation target (blocks) and value is fee rate (sat/vB). - /// - /// Example response: `{ "1": 87.882, "2": 87.882, ..., "144": 1.027, "1008": 1.027 }` - fn fetch_fee_estimates() -> Result { - const ESPLORA_URL: &str = "https://blockstream.info/liquidtestnet/api"; - - let url = format!("{ESPLORA_URL}/fee-estimates"); - let response = minreq::get(&url) - .send() - .map_err(|e| FeeFetcherError::Request(e.to_string()))?; - - if response.status_code != 200 { - return Err(FeeFetcherError::Request(format!( - "HTTP {}: {}", - response.status_code, response.reason_phrase - ))); - } - - let estimates: FeeEstimates = response - .json() - .map_err(|e| FeeFetcherError::Deserialize(e.to_string()))?; - - Ok(estimates) - } -} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs deleted file mode 100644 index 2a59260..0000000 --- a/crates/core/src/lib.rs +++ /dev/null @@ -1,311 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] - -//! High-level helpers for building and executing Simplicity programs on Liquid. - -mod blinder; -mod constants; -mod error; -mod fee_rate_fetcher; -mod runner; -mod scripts; -mod tx_inclusion; - -#[cfg(feature = "encoding")] -pub mod encoding { - use crate::EncodingError; - pub use bincode::{Decode, Encode}; - - /// Trait for binary encoding/decoding with hex string support. - pub trait Encodable { - /// Encode to binary bytes. - /// - /// # Errors - /// Returns error if encoding fails. - fn encode(&self) -> Result, EncodingError> - where - Self: serde::Serialize, - { - Ok(bincode::serde::encode_to_vec(self, bincode::config::standard())?) - } - - /// Decode from binary bytes. - /// - /// # Errors - /// Returns error if decoding fails. - fn decode(buf: &[u8]) -> Result - where - Self: Sized + serde::de::DeserializeOwned, - { - Ok(bincode::serde::decode_from_slice(buf, bincode::config::standard())?.0) - } - - /// Encode to hex string. - /// - /// # Errors - /// Returns error if encoding fails. - fn to_hex(&self) -> Result - where - Self: serde::Serialize, - { - Ok(hex::encode(Encodable::encode(self)?)) - } - - /// Decode from hex string. - /// - /// # Errors - /// Returns error if hex decoding or binary decoding fails. - fn from_hex(hex: &str) -> Result - where - Self: serde::de::DeserializeOwned, - { - Encodable::decode(&hex::decode(hex)?) - } - } -} - -pub use blinder::*; -pub use constants::*; -pub use error::ProgramError; - -#[cfg(feature = "encoding")] -pub use error::EncodingError; - -pub use runner::*; -pub use scripts::*; -pub use tx_inclusion::*; - -pub use fee_rate_fetcher::*; - -#[cfg(feature = "encoding")] -pub use encoding::Encodable; - -use simplicityhl::elements::secp256k1_zkp::schnorr::Signature; - -use std::collections::HashMap; -use std::sync::Arc; - -use simplicityhl::num::U256; -use simplicityhl::simplicity::RedeemNode; -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; -use simplicityhl::simplicity::elements::{Address, Transaction, TxInWitness, TxOut}; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; -use simplicityhl::str::WitnessName; -use simplicityhl::tracker::TrackerLogLevel; -use simplicityhl::value::ValueConstructible; -use simplicityhl::{CompiledProgram, Value, WitnessValues, elements}; - -/// Embedded Simplicity source for a basic P2PK program used to sign a single input. -pub const P2PK_SOURCE: &str = include_str!("source_simf/p2pk.simf"); - -/// Construct a P2TR address for the embedded P2PK program and the provided public key. -/// -/// # Errors -/// Returns error if the P2PK program fails to compile. -pub fn get_p2pk_address( - x_only_public_key: &XOnlyPublicKey, - network: SimplicityNetwork, -) -> Result { - Ok(create_p2tr_address( - get_p2pk_program(x_only_public_key)?.commit().cmr(), - x_only_public_key, - network.address_params(), - )) -} - -/// Compile the embedded P2PK program with the given X-only public key as argument. -/// -/// # Errors -/// Returns error if program compilation fails. -pub fn get_p2pk_program(account_public_key: &XOnlyPublicKey) -> Result { - let arguments = simplicityhl::Arguments::from(HashMap::from([( - WitnessName::from_str_unchecked("PUBLIC_KEY"), - Value::u256(U256::from_byte_array(account_public_key.serialize())), - )])); - - load_program(P2PK_SOURCE, arguments) -} - -/// Execute the compiled P2PK program against the provided env, producing a pruned redeem node. -/// -/// The `schnorr_signature` should be created by signing the `sighash_all` from the environment: -/// ```ignore -/// let sighash_all = secp256k1::Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); -/// let schnorr_signature = keypair.sign_schnorr(sighash_all); -/// ``` -/// -/// # Errors -/// Returns error if program execution fails. -pub fn execute_p2pk_program( - compiled_program: &CompiledProgram, - schnorr_signature: &Signature, - env: &ElementsEnv>, - runner_log_level: TrackerLogLevel, -) -> Result>, ProgramError> { - let witness_values = WitnessValues::from(HashMap::from([( - WitnessName::from_str_unchecked("SIGNATURE"), - Value::byte_array(schnorr_signature.serialize()), - )])); - - Ok(run_program(compiled_program, witness_values, env, runner_log_level)?.0) -} - -/// Create a Schnorr signature for the P2PK program by signing the `sighash_all` of the transaction. -/// -/// This is a convenience function that builds the environment and signs the transaction hash. -/// -/// # Errors -/// Returns error if program compilation or environment verification fails. -pub fn create_p2pk_signature( - tx: &Transaction, - utxos: &[TxOut], - keypair: &elements::schnorr::Keypair, - input_index: usize, - network: SimplicityNetwork, -) -> Result { - use simplicityhl::simplicity::hashes::Hash as _; - - let x_only_public_key = keypair.x_only_public_key().0; - let p2pk_program = get_p2pk_program(&x_only_public_key)?; - - let env = get_and_verify_env(tx, &p2pk_program, &x_only_public_key, utxos, network, input_index)?; - - let sighash_all = elements::secp256k1_zkp::Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); - Ok(keypair.sign_schnorr(sighash_all)) -} - -/// Finalize the given transaction by attaching a Simplicity witness for the specified P2PK input. -/// -/// The `schnorr_signature` should be created by signing the `sighash_all` from the environment. -/// Use [`create_p2pk_signature`] to create the signature if you have access to the secret key: -/// ```ignore -/// let signature = create_p2pk_signature(&tx, &utxos, &keypair, input_index, params, genesis_hash)?; -/// let tx = finalize_p2pk_transaction(tx, &utxos, &public_key, &signature, input_index, params, genesis_hash, TrackerLogLevel::None)?; -/// ``` -/// -/// Preconditions: -/// - `utxos[input_index]` must match the P2PK address derived from `x_only_public_key` and program CMR. -/// -/// # Errors -/// Returns error if program compilation, execution, or environment verification fails. -#[allow(clippy::too_many_arguments)] -pub fn finalize_p2pk_transaction( - mut tx: Transaction, - utxos: &[TxOut], - x_only_public_key: &XOnlyPublicKey, - schnorr_signature: &Signature, - input_index: usize, - network: SimplicityNetwork, - log_level: TrackerLogLevel, -) -> Result { - let p2pk_program = get_p2pk_program(x_only_public_key)?; - - let env = get_and_verify_env(&tx, &p2pk_program, x_only_public_key, utxos, network, input_index)?; - - let pruned = execute_p2pk_program(&p2pk_program, schnorr_signature, &env, log_level)?; - - let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); - let cmr = pruned.cmr(); - - tx.input[input_index].witness = TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, - script_witness: vec![ - simplicity_witness_bytes, - simplicity_program_bytes, - cmr.as_ref().to_vec(), - control_block(cmr, *x_only_public_key).serialize(), - ], - pegin_witness: vec![], - }; - - Ok(tx) -} - -/// Finalize transaction with a Simplicity witness for the specified input. -/// -/// # Errors -/// Returns error if environment verification or program execution fails. -#[allow(clippy::too_many_arguments)] -pub fn finalize_transaction( - mut tx: Transaction, - program: &CompiledProgram, - program_public_key: &XOnlyPublicKey, - utxos: &[TxOut], - input_index: usize, - witness_values: WitnessValues, - network: SimplicityNetwork, - log_level: TrackerLogLevel, -) -> Result { - let env = get_and_verify_env(&tx, program, program_public_key, utxos, network, input_index)?; - - let pruned = run_program(program, witness_values, &env, log_level)?.0; - - let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); - let cmr = pruned.cmr(); - - tx.input[input_index].witness = TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, - script_witness: vec![ - simplicity_witness_bytes, - simplicity_program_bytes, - cmr.as_ref().to_vec(), - control_block(cmr, *program_public_key).serialize(), - ], - pegin_witness: vec![], - }; - - Ok(tx) -} - -/// Build and verify an Elements environment for program execution. -/// -/// # Errors -/// Returns error if UTXO index is invalid or script pubkey doesn't match. -pub fn get_and_verify_env( - tx: &Transaction, - program: &CompiledProgram, - program_public_key: &XOnlyPublicKey, - utxos: &[TxOut], - network: SimplicityNetwork, - input_index: usize, -) -> Result>, ProgramError> { - let params = network.address_params(); - let genesis_hash = network.genesis_block_hash(); - let cmr = program.commit().cmr(); - - if utxos.len() <= input_index { - return Err(ProgramError::UtxoIndexOutOfBounds { - input_index, - utxo_count: utxos.len(), - }); - } - - let target_utxo = &utxos[input_index]; - let script_pubkey = create_p2tr_address(cmr, program_public_key, params).script_pubkey(); - - if target_utxo.script_pubkey != script_pubkey { - return Err(ProgramError::ScriptPubkeyMismatch { - expected_hash: script_pubkey.script_hash().to_string(), - actual_hash: target_utxo.script_pubkey.script_hash().to_string(), - }); - } - - Ok(ElementsEnv::new( - Arc::new(tx.clone()), - utxos - .iter() - .map(|utxo| ElementsUtxo { - script_pubkey: utxo.script_pubkey.clone(), - asset: utxo.asset, - value: utxo.value, - }) - .collect(), - u32::try_from(input_index)?, - cmr, - control_block(cmr, *program_public_key), - None, - genesis_hash, - )) -} diff --git a/crates/core/src/runner.rs b/crates/core/src/runner.rs deleted file mode 100644 index 5695e45..0000000 --- a/crates/core/src/runner.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Program execution helpers and logging levels for Simplicity programs. -//! -//! Provides `run_program` which satisfies and executes a compiled program -//! against an `ElementsEnv`, with optional debug and jet-trace logging. - -use std::sync::Arc; - -use simplicityhl::simplicity::elements::Transaction; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::ElementsEnv; -use simplicityhl::simplicity::{BitMachine, RedeemNode, Value}; -use simplicityhl::tracker::{DefaultTracker, TrackerLogLevel}; -use simplicityhl::{CompiledProgram, WitnessValues}; - -use crate::error::ProgramError; - -/// Satisfy and execute a compiled program in the provided environment. -/// Returns the pruned program and the resulting value. -/// -/// # Errors -/// Returns error if witness satisfaction or program execution fails. -pub fn run_program( - program: &CompiledProgram, - witness_values: WitnessValues, - env: &ElementsEnv>, - log_level: TrackerLogLevel, -) -> Result<(Arc>, Value), ProgramError> { - let satisfied = program - .satisfy(witness_values) - .map_err(ProgramError::WitnessSatisfaction)?; - - let mut tracker = DefaultTracker::new(satisfied.debug_symbols()).with_log_level(log_level); - - let pruned = satisfied.redeem().prune_with_tracker(env, &mut tracker)?; - let mut mac = BitMachine::for_program(&pruned)?; - - let result = mac.exec(&pruned, env).map_err(ProgramError::Execution)?; - - Ok((pruned, result)) -} diff --git a/crates/core/src/scripts.rs b/crates/core/src/scripts.rs deleted file mode 100644 index 0b82ac6..0000000 --- a/crates/core/src/scripts.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Script and Taproot utilities plus minor helpers around Elements types. - -use sha2::{Digest, Sha256}; - -use simplicityhl::elements::{Address, AddressParams, AssetId, ContractHash, OutPoint, Script, script, taproot}; - -use simplicityhl::simplicity::bitcoin::{XOnlyPublicKey, secp256k1}; -use simplicityhl::simplicity::hashes::{Hash, sha256}; -use simplicityhl::{Arguments, CompiledProgram}; - -use crate::error::ProgramError; - -/// Load program source and compile it to a Simplicity program. -/// -/// # Errors -/// Returns error if the program fails to compile. -pub fn load_program(source: &str, arguments: Arguments) -> Result { - let compiled = CompiledProgram::new(source, arguments, true).map_err(ProgramError::Compilation)?; - Ok(compiled) -} - -/// Generate a non-confidential P2TR address for the given program CMR and key. -#[must_use] -pub fn create_p2tr_address( - cmr: simplicityhl::simplicity::Cmr, - x_only_public_key: &XOnlyPublicKey, - params: &'static AddressParams, -) -> Address { - let spend_info = taproot_spending_info(cmr, *x_only_public_key); - - Address::p2tr( - secp256k1::SECP256K1, - spend_info.internal_key(), - spend_info.merkle_root(), - None, - params, - ) -} - -fn script_version(cmr: simplicityhl::simplicity::Cmr) -> (Script, taproot::LeafVersion) { - let script = script::Script::from(cmr.as_ref().to_vec()); - (script, simplicityhl::simplicity::leaf_version()) -} - -fn taproot_spending_info( - cmr: simplicityhl::simplicity::Cmr, - internal_key: XOnlyPublicKey, -) -> taproot::TaprootSpendInfo { - let builder = taproot::TaprootBuilder::new(); - let (script, version) = script_version(cmr); - let builder = builder - .add_leaf_with_ver(0, script, version) - .expect("tap tree should be valid"); - builder - .finalize(secp256k1::SECP256K1, internal_key) - .expect("tap tree should be valid") -} - -/// Compute the Taproot control block for script-path spending. -/// -/// # Panics -/// Panics if the taproot tree is invalid (should never happen with valid CMR). -#[must_use] -pub fn control_block(cmr: simplicityhl::simplicity::Cmr, internal_key: XOnlyPublicKey) -> taproot::ControlBlock { - let info = taproot_spending_info(cmr, internal_key); - let script_ver = script_version(cmr); - info.control_block(&script_ver).expect("control block should exist") -} - -/// SHA256 hash of an address's scriptPubKey bytes. -#[must_use] -pub fn hash_script(script: &Script) -> [u8; 32] { - let mut hasher = Sha256::new(); - sha2::digest::Update::update(&mut hasher, script.as_bytes()); - hasher.finalize().into() -} - -/// Compute issuance entropy for a new asset given an outpoint and contract hash entropy. -#[must_use] -pub fn get_new_asset_entropy(outpoint: &OutPoint, entropy: [u8; 32]) -> sha256::Midstate { - let contract_hash = ContractHash::from_byte_array(entropy); - AssetId::generate_asset_entropy(*outpoint, contract_hash) -} diff --git a/crates/core/src/source_simf/p2pk.simf b/crates/core/src/source_simf/p2pk.simf deleted file mode 100644 index db4f27c..0000000 --- a/crates/core/src/source_simf/p2pk.simf +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - jet::bip_0340_verify((param::PUBLIC_KEY, jet::sig_all_hash()), witness::SIGNATURE) -} \ No newline at end of file diff --git a/crates/core/src/tx_inclusion.rs b/crates/core/src/tx_inclusion.rs deleted file mode 100644 index 80cc7fa..0000000 --- a/crates/core/src/tx_inclusion.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! Transaction inclusion verification using Merkle proofs for Liquid/Elements blocks. -//! -//! This module provides SPV (Simplified Payment Verification) functionality to prove -//! a transaction exists in a block without downloading all transactions. - -use simplicityhl::elements::hashes::{Hash, HashEngine}; -use simplicityhl::elements::{Block, TxMerkleNode, Txid}; - -/// Merkle proof: (`transaction_index`, `sibling_hashes`) -pub type MerkleProof = (usize, Vec); - -/// Constructs a Merkle inclusion proof (Merkle branch). -/// -/// For a transaction TXID in a block, using Bitcoin consensus Merkle tree construction rules -/// (pairwise double-SHA256 hashing with odd-hash duplication). -/// -/// Liquid inherits the same Merkle tree semantics via the Elements codebase: -/// -/// -/// Returns `None` if the transaction is not present in the block. -#[must_use] -pub fn merkle_branch(tx: &Txid, block: &Block) -> Option { - if block.txdata.is_empty() { - return None; - } - - let tx_index = block.txdata.iter().position(|t| &t.txid() == tx)?; - - Some((tx_index, build_merkle_branch(tx_index, block))) -} - -/// Verifies a Merkle inclusion proof (Merkle branch). -/// -/// For a transaction TXID against the given Merkle root using Bitcoin consensus Merkle tree rules -/// (pairwise double-SHA256 hashing with left/right ordering). -/// -/// Liquid inherits the same Merkle tree semantics via the Elements codebase: -/// -/// -/// Returns `true` if the proof commits the transaction to the given root. -#[must_use] -pub fn verify_tx(tx: &Txid, root: &TxMerkleNode, proof: &MerkleProof) -> bool { - root.eq(&compute_merkle_root_from_branch(tx, proof.0, &proof.1)) -} - -fn build_merkle_branch(tx_index: usize, block: &Block) -> Vec { - if block.txdata.is_empty() || block.txdata.len() == 1 { - return vec![]; - } - - let mut branch = vec![]; - let mut layer = block - .txdata - .iter() - .map(|tx| TxMerkleNode::from_raw_hash(*tx.txid().as_raw_hash())) - .collect::>(); - let mut index = tx_index; - - // Bottom-up traversal: pair nodes, hash parents, collect siblings along path to root - while layer.len() > 1 { - let mut next_layer = vec![]; - - for i in (0..layer.len()).step_by(2) { - let left = layer[i]; - let right = if i + 1 < layer.len() { layer[i + 1] } else { layer[i] }; - - let mut eng = TxMerkleNode::engine(); - eng.input(left.as_raw_hash().as_byte_array()); - eng.input(right.as_raw_hash().as_byte_array()); - - next_layer.push(TxMerkleNode::from_engine(eng)); - - if index / 2 == i / 2 { - let sibling = if index.is_multiple_of(2) { right } else { left }; - branch.push(sibling); - } - } - - index /= 2; - layer = next_layer; - } - - branch -} - -fn compute_merkle_root_from_branch(tx: &Txid, tx_index: usize, branch: &[TxMerkleNode]) -> TxMerkleNode { - let mut res = TxMerkleNode::from_raw_hash(*tx.as_raw_hash()); - let mut pos = tx_index; - - for leaf in branch { - let mut eng = TxMerkleNode::engine(); - - if pos & 1 == 0 { - eng.input(res.as_raw_hash().as_byte_array()); - eng.input(leaf.as_raw_hash().as_byte_array()); - } else { - eng.input(leaf.as_raw_hash().as_byte_array()); - eng.input(res.as_raw_hash().as_byte_array()); - } - res = TxMerkleNode::from_engine(eng); - - pos >>= 1; - } - - res -} - -#[cfg(test)] -mod test { - - use super::*; - - /// Taken from rust-elements - /// - macro_rules! hex_deserialize( - ($e:expr) => ({ - use simplicityhl::elements::encode::deserialize; - - fn hex_char(c: char) -> u8 { - match c { - '0' => 0, - '1' => 1, - '2' => 2, - '3' => 3, - '4' => 4, - '5' => 5, - '6' => 6, - '7' => 7, - '8' => 8, - '9' => 9, - 'a' | 'A' => 10, - 'b' | 'B' => 11, - 'c' | 'C' => 12, - 'd' | 'D' => 13, - 'e' | 'E' => 14, - 'f' | 'F' => 15, - x => panic!("Invalid character {} in hex string", x), - } - } - - let mut ret = Vec::with_capacity($e.len() / 2); - let mut byte = 0; - for (ch, store) in $e.chars().zip([false, true].iter().cycle()) { - byte = (byte << 4) + hex_char(ch); - if *store { - ret.push(byte); - byte = 0; - } - } - deserialize(&ret).expect("deserialize object") - }); - ); - - // Unfortunately, `hex_deserialize` macro aforehead returns error trying deserialize - // blocks from elements-cli regtest, so this block, taken from `elements::Block::block`, is - // the only test case I have found so far. - const BLOCK_STR: &str = include_str!("./assets/test-tx-incl-block.hex"); - - #[test] - fn test_merkle_branch_construction() { - let block: Block = hex_deserialize!(BLOCK_STR); - - assert_eq!(block.txdata.len(), 3); - - let tx = block.txdata[1].txid(); - let proof = merkle_branch(&tx, &block).expect("Failed to find tx in block"); - - assert!( - verify_tx(&tx, &block.header.merkle_root, &proof), - "Invalid merkle proof" - ); - } -} diff --git a/crates/macros-core/Cargo.toml b/crates/macros-core/Cargo.toml deleted file mode 100644 index f8e1b29..0000000 --- a/crates/macros-core/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "simplex-macro-core" -description = "Macro support core for Simplex, the Rust SimplicityHl toolkit. Not intended to be used directly." -version = "0.1.0" -license.workspace = true -edition.workspace = true - -[lints] -workspace = true - -[features] -bincode = [] -serde = ["bincode"] -default = ["bincode", "serde"] - -[dependencies] -proc-macro-error = { version = "1.0" } -proc-macro2 = { version = "1.0.106", features = ["span-locations"] } -syn = { version = "2.0.114", default-features = false, features = ["proc-macro", "full", "parsing", "derive", "clone-impls", "extra-traits", "printing"] } -thiserror = { workspace = true } -quote = { version = "1.0.44" } - -simplicityhl = { workspace = true } diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index f15fbc3..354324d 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -16,17 +16,20 @@ categories = ["cryptography::cryptocurrencies"] proc-macro = true [features] -macros = [] -derive = [] default = ["macros", "derive",] serde = ["macros", "dep:serde"] +macros = [] +derive = [] [lints] workspace = true - [dependencies] -simplex-macro-core = { path = "../macros-core", features = ["bincode", "serde"] } -serde = { version = "1.0.228", optional = true } -syn = { version = "2.0.114", default-features = false, features = ["parsing", "proc-macro"] } +thiserror = { workspace = true } +simplicityhl = { workspace = true } +serde = { version = "1.0.228", optional = true } +proc-macro-error = { version = "1.0" } +proc-macro2 = { version = "1.0.106", features = ["span-locations"] } +syn = { version = "2.0.114", default-features = false, features = ["proc-macro", "full", "parsing", "derive", "clone-impls", "extra-traits", "printing"] } +quote = { version = "1.0.44" } diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 8179b38..e117118 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1,5 +1,7 @@ use proc_macro::TokenStream; +mod macros_core; + // TODO(Illia): add path to exported crates to be able users to use their own https://stackoverflow.com/questions/79595543/rust-how-to-re-export-3rd-party-crate // #[serde(crate = "exporter::reexports::serde")] // simplicityhl, either @@ -7,8 +9,8 @@ use proc_macro::TokenStream; #[cfg(feature = "macros")] #[proc_macro] pub fn include_simf(tokenstream: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(tokenstream as simplex_macro_core::attr::parse::SynFilePath); - match simplex_macro_core::expand_include_simf(&input) { + let input = syn::parse_macro_input!(tokenstream as macros_core::attr::parse::SynFilePath); + match macros_core::expand_include_simf(&input) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), } @@ -19,7 +21,7 @@ pub fn include_simf(tokenstream: TokenStream) -> TokenStream { pub fn test(args: TokenStream, input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as syn::ItemFn); - match simplex_macro_core::expand_test(&args.into(), &input) { + match macros_core::expand_test(&args.into(), &input) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), } diff --git a/crates/macros-core/src/attr/codegen.rs b/crates/macros/src/macros_core/attr/codegen.rs similarity index 98% rename from crates/macros-core/src/attr/codegen.rs rename to crates/macros/src/macros_core/attr/codegen.rs index c1625db..50e1bdb 100644 --- a/crates/macros-core/src/attr/codegen.rs +++ b/crates/macros/src/macros_core/attr/codegen.rs @@ -1,5 +1,5 @@ -use crate::attr::SimfContent; -use crate::attr::types::RustType; +use crate::macros_core::attr::SimfContent; +use crate::macros_core::attr::types::RustType; use quote::{format_ident, quote}; use simplicityhl::str::WitnessName; use simplicityhl::{AbiMeta, Parameters, ResolvedType, WitnessTypes}; @@ -158,7 +158,7 @@ impl WitnessStruct { } } - impl ::simplex_core::Encodable for #struct_name {} + // impl ::simplex_core::Encodable for #struct_name {} }, }) } @@ -230,7 +230,7 @@ impl WitnessStruct { } } - impl ::simplex_core::Encodable for #struct_name {} + // impl ::simplex_core::Encodable for #struct_name {} }, }) } diff --git a/crates/macros-core/src/attr/mod.rs b/crates/macros/src/macros_core/attr/mod.rs similarity index 68% rename from crates/macros-core/src/attr/mod.rs rename to crates/macros/src/macros_core/attr/mod.rs index 55faded..f58966f 100644 --- a/crates/macros-core/src/attr/mod.rs +++ b/crates/macros/src/macros_core/attr/mod.rs @@ -4,7 +4,7 @@ mod types; pub use parse::SimfContent; -use crate::attr::codegen::{GeneratedArgumentTokens, GeneratedWitnessTokens, SimfContractMeta}; +use crate::macros_core::attr::codegen::{GeneratedArgumentTokens, GeneratedWitnessTokens, SimfContractMeta}; use proc_macro2::Span; use quote::{format_ident, quote}; use simplicityhl::AbiMeta; @@ -54,63 +54,63 @@ fn construct_program_helpers(derived_meta: &SimfContractMeta) -> proc_macro2::To let contract_arguments_struct_name = &derived_meta.args_struct.struct_name; quote! { - use simplicityhl::elements::Address; - use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; - use simplex::simplex_core::{create_p2tr_address, load_program, ProgramError, SimplicityNetwork}; - use simplicityhl::CompiledProgram; + // use simplicityhl::elements::Address; + // use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; + // use simplex::simplex_core::{create_p2tr_address, load_program, ProgramError, SimplicityNetwork}; + // use simplicityhl::CompiledProgram; - pub const #contract_source_name: &str = #contract_content; + // pub const #contract_source_name: &str = #contract_content; /// Get the options template program for instantiation. /// /// # Panics /// - if the embedded source fails to compile (should never happen). - #[must_use] - pub fn get_template_program() -> ::simplicityhl::TemplateProgram { - ::simplicityhl::TemplateProgram::new(#contract_source_name).expect(#error_msg) - } + // #[must_use] + // pub fn get_template_program() -> ::simplicityhl::TemplateProgram { + // ::simplicityhl::TemplateProgram::new(#contract_source_name).expect(#error_msg) + // } /// Derive P2TR address for an option offer contract. /// /// # Errors /// /// Returns error if program compilation fails. - pub fn get_option_offer_address( - x_only_public_key: &XOnlyPublicKey, - arguments: &#contract_arguments_struct_name, - network: SimplicityNetwork, - ) -> Result { - Ok(create_p2tr_address( - get_loaded_program(arguments)?.commit().cmr(), - x_only_public_key, - network.address_params(), - )) - } + // pub fn get_option_offer_address( + // x_only_public_key: &XOnlyPublicKey, + // arguments: &#contract_arguments_struct_name, + // network: SimplicityNetwork, + // ) -> Result { + // Ok(create_p2tr_address( + // get_loaded_program(arguments)?.commit().cmr(), + // x_only_public_key, + // network.address_params(), + // )) + // } /// Compile option offer program with the given arguments. /// /// # Errors /// /// Returns error if compilation fails. - pub fn get_loaded_program( - arguments: &#contract_arguments_struct_name, - ) -> Result { - load_program(#contract_source_name, arguments.build_arguments()) - } + // pub fn get_loaded_program( + // arguments: &#contract_arguments_struct_name, + // ) -> Result { + // load_program(#contract_source_name, arguments.build_arguments()) + // } /// Get compiled option offer program, panicking on failure. /// /// # Panics /// /// Panics if program instantiation fails. - #[must_use] - pub fn get_compiled_program(arguments: &#contract_arguments_struct_name) -> CompiledProgram { - let program = get_template_program(); - - program - .instantiate(arguments.build_arguments(), true) - .unwrap() - } + // #[must_use] + // pub fn get_compiled_program(arguments: &#contract_arguments_struct_name) -> CompiledProgram { + // let program = get_template_program(); + + // program + // .instantiate(arguments.build_arguments(), true) + // .unwrap() + // } } } diff --git a/crates/macros-core/src/attr/parse.rs b/crates/macros/src/macros_core/attr/parse.rs similarity index 100% rename from crates/macros-core/src/attr/parse.rs rename to crates/macros/src/macros_core/attr/parse.rs diff --git a/crates/macros-core/src/attr/types.rs b/crates/macros/src/macros_core/attr/types.rs similarity index 100% rename from crates/macros-core/src/attr/types.rs rename to crates/macros/src/macros_core/attr/types.rs diff --git a/crates/macros-core/src/lib.rs b/crates/macros/src/macros_core/mod.rs similarity index 100% rename from crates/macros-core/src/lib.rs rename to crates/macros/src/macros_core/mod.rs diff --git a/crates/macros-core/src/program.rs b/crates/macros/src/macros_core/program.rs similarity index 89% rename from crates/macros-core/src/program.rs rename to crates/macros/src/macros_core/program.rs index 67cd275..2cf86d1 100644 --- a/crates/macros-core/src/program.rs +++ b/crates/macros/src/macros_core/program.rs @@ -1,4 +1,4 @@ -use crate::attr::parse::SimfContent; +use crate::macros_core::attr::parse::SimfContent; use proc_macro2::Span; use simplicityhl::{AbiMeta, TemplateProgram}; use std::error::Error; diff --git a/crates/runtime/Cargo.toml b/crates/provider/Cargo.toml similarity index 85% rename from crates/runtime/Cargo.toml rename to crates/provider/Cargo.toml index 08fe472..9ee7360 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "simplex-runtime" +name = "simplex-provider" version = "0.1.0" license.workspace = true edition.workspace = true @@ -13,20 +13,13 @@ categories = ["cryptography::cryptocurrencies", "web-programming::http-client", [lints] workspace = true -[features] - - [dependencies] -async-trait = { version = "0.1.89" } simplicityhl = { workspace = true } -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } thiserror = { workspace = true } +electrsd = { workspace = true } +async-trait = { version = "0.1.89" } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" hex-simd = "0.8.0" bitcoin_hashes = "0.14.1" -simplex-core = { workspace = true } -electrsd = { workspace = true } - -[dev-dependencies] -tokio = { version = "1.49.0", features = ["full"] } diff --git a/crates/runtime/README.md b/crates/provider/README.md similarity index 100% rename from crates/runtime/README.md rename to crates/provider/README.md diff --git a/crates/runtime/api-esplora.md b/crates/provider/api-esplora.md similarity index 100% rename from crates/runtime/api-esplora.md rename to crates/provider/api-esplora.md diff --git a/crates/runtime/api-waterfall.md b/crates/provider/api-waterfall.md similarity index 100% rename from crates/runtime/api-waterfall.md rename to crates/provider/api-waterfall.md diff --git a/crates/runtime/src/elements_rpc/mod.rs b/crates/provider/src/elements_rpc/mod.rs similarity index 96% rename from crates/runtime/src/elements_rpc/mod.rs rename to crates/provider/src/elements_rpc/mod.rs index 00ef807..cd0c688 100644 --- a/crates/runtime/src/elements_rpc/mod.rs +++ b/crates/provider/src/elements_rpc/mod.rs @@ -6,49 +6,36 @@ use crate::error::ExplorerError; use bitcoind::bitcoincore_rpc::{Auth, Client, RpcApi, bitcoin}; use electrsd::bitcoind; use serde_json::Value; -use simplex_core::SimplicityNetwork; use simplicityhl::elements::{Address, AssetId, BlockHash, Txid}; use std::str::FromStr; pub struct ElementsRpcClient { inner: Client, #[allow(unused)] - network: SimplicityNetwork, - #[allow(unused)] auth: Auth, #[allow(unused)] url: String, } impl ElementsRpcClient { - pub fn new(network: SimplicityNetwork, url: &str, auth: Auth) -> Result { + pub fn new(url: &str, auth: Auth) -> Result { let inner = Client::new(url, auth.clone())?; inner.ping()?; Ok(Self { inner, - network, auth, url: url.to_string(), }) } - pub fn new_from_credentials( - network: SimplicityNetwork, - url: &str, - user: &str, - pass: &str, - ) -> Result { + pub fn new_from_credentials(url: &str, user: &str, pass: &str) -> Result { let auth = Auth::UserPass(user.to_string(), pass.to_string()); - Self::new(network, url, auth) + Self::new(url, auth) } pub fn client(&self) -> &Client { &self.inner } - - pub fn network(&self) -> SimplicityNetwork { - self.network - } } impl ElementsRpcClient { diff --git a/crates/runtime/src/elements_rpc/types.rs b/crates/provider/src/elements_rpc/types.rs similarity index 100% rename from crates/runtime/src/elements_rpc/types.rs rename to crates/provider/src/elements_rpc/types.rs diff --git a/crates/runtime/src/error.rs b/crates/provider/src/error.rs similarity index 100% rename from crates/runtime/src/error.rs rename to crates/provider/src/error.rs diff --git a/crates/runtime/src/esplora/mod.rs b/crates/provider/src/esplora/mod.rs similarity index 100% rename from crates/runtime/src/esplora/mod.rs rename to crates/provider/src/esplora/mod.rs diff --git a/crates/runtime/src/esplora/types.rs b/crates/provider/src/esplora/types.rs similarity index 100% rename from crates/runtime/src/esplora/types.rs rename to crates/provider/src/esplora/types.rs diff --git a/crates/runtime/src/lib.rs b/crates/provider/src/lib.rs similarity index 63% rename from crates/runtime/src/lib.rs rename to crates/provider/src/lib.rs index a58835d..fdb7040 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -1,7 +1,5 @@ pub mod elements_rpc; mod error; pub mod esplora; -mod waterfall; pub use error::*; -// pub use waterfall::*; diff --git a/crates/runtime/src/waterfall/mod.rs b/crates/runtime/src/waterfall/mod.rs deleted file mode 100644 index 3a02aba..0000000 --- a/crates/runtime/src/waterfall/mod.rs +++ /dev/null @@ -1,319 +0,0 @@ -mod types; - -// pub struct WaterfallClient { -// base_url: String, -// client: reqwest::Client, -// } - -// impl WaterfallClient { -// pub fn new(base_url: impl Into) -> Self { -// Self { -// base_url: base_url.into(), -// client: reqwest::Client::new(), -// } -// } -// -// fn url(&self, path: &str) -> String { -// format!( -// "{}/{}", -// self.base_url.trim_end_matches('/'), -// path.trim_start_matches('/') -// ) -// } -// -// // Waterfalls v2 endpoints (JSON) -// pub async fn waterfalls_v2( -// &self, -// descriptor: &str, -// page: Option, -// to_index: Option, -// utxo_only: bool, -// ) -> Result<(WaterfallResponse, reqwest::header::HeaderMap), reqwest::Error> { -// let mut url = self.url(&format!("v2/waterfalls?descriptor={}", urlencoding::encode(descriptor))); -// -// if let Some(p) = page { -// url.push_str(&format!("&page={}", p)); -// } -// if let Some(idx) = to_index { -// url.push_str(&format!("&to_index={}", idx)); -// } -// if utxo_only { -// url.push_str("&utxo_only=true"); -// } -// -// let response = self.client.get(&url).send().await?; -// let headers = response.headers().clone(); -// let data = response.json().await?; -// Ok((data, headers)) -// } -// -// pub async fn waterfalls_v2_addresses( -// &self, -// addresses: &[String], -// page: Option, -// utxo_only: bool, -// ) -> Result<(WaterfallResponse, reqwest::header::HeaderMap), reqwest::Error> { -// let mut url = self.url(&format!("v2/waterfalls?addresses={}", addresses.join(","))); -// -// if let Some(p) = page { -// url.push_str(&format!("&page={}", p)); -// } -// if utxo_only { -// url.push_str("&utxo_only=true"); -// } -// -// let response = self.client.get(&url).send().await?; -// let headers = response.headers().clone(); -// let data = response.json().await?; -// Ok((data, headers)) -// } -// -// pub async fn waterfalls_v2_utxo_only( -// &self, -// descriptor: &str, -// to_index: Option, -// ) -> Result<(WaterfallResponse, reqwest::header::HeaderMap), reqwest::Error> { -// self.waterfalls_v2(descriptor, None, to_index, true).await -// } -// -// // Waterfalls v4 endpoints (JSON with extended tip metadata) -// pub async fn waterfalls_v4( -// &self, -// descriptor: &str, -// page: Option, -// to_index: Option, -// utxo_only: bool, -// ) -> Result<(WaterfallResponseV4, reqwest::header::HeaderMap), reqwest::Error> { -// let mut url = self.url(&format!("v4/waterfalls?descriptor={}", urlencoding::encode(descriptor))); -// -// if let Some(p) = page { -// url.push_str(&format!("&page={}", p)); -// } -// if let Some(idx) = to_index { -// url.push_str(&format!("&to_index={}", idx)); -// } -// if utxo_only { -// url.push_str("&utxo_only=true"); -// } -// -// let response = self.client.get(&url).send().await?; -// let headers = response.headers().clone(); -// let data = response.json().await?; -// Ok((data, headers)) -// } -// -// pub async fn waterfalls_v4_addresses( -// &self, -// addresses: &[String], -// page: Option, -// utxo_only: bool, -// ) -> Result<(WaterfallResponseV4, reqwest::header::HeaderMap), reqwest::Error> { -// let mut url = self.url(&format!("v4/waterfalls?addresses={}", addresses.join(","))); -// -// if let Some(p) = page { -// url.push_str(&format!("&page={}", p)); -// } -// if utxo_only { -// url.push_str("&utxo_only=true"); -// } -// -// let response = self.client.get(&url).send().await?; -// let headers = response.headers().clone(); -// let data = response.json().await?; -// Ok((data, headers)) -// } -// -// pub async fn waterfalls_v4_utxo_only( -// &self, -// descriptor: &str, -// to_index: Option, -// ) -> Result<(WaterfallResponseV4, reqwest::header::HeaderMap), reqwest::Error> { -// self.waterfalls_v4(descriptor, None, to_index, true).await -// } -// -// // Waterfalls v1 endpoint (for compatibility) -// pub async fn waterfalls_v1( -// &self, -// descriptor: &str, -// page: Option, -// to_index: Option, -// utxo_only: bool, -// ) -> Result<(WaterfallResponse, reqwest::header::HeaderMap), reqwest::Error> { -// let mut url = self.url(&format!("v1/waterfalls?descriptor={}", urlencoding::encode(descriptor))); -// -// if let Some(p) = page { -// url.push_str(&format!("&page={}", p)); -// } -// if let Some(idx) = to_index { -// url.push_str(&format!("&to_index={}", idx)); -// } -// if utxo_only { -// url.push_str("&utxo_only=true"); -// } -// -// let response = self.client.get(&url).send().await?; -// let headers = response.headers().clone(); -// let data = response.json().await?; -// Ok((data, headers)) -// } -// -// // CBOR endpoints -// pub async fn waterfalls_v2_cbor( -// &self, -// descriptor: &str, -// page: Option, -// to_index: Option, -// utxo_only: bool, -// ) -> Result<(Vec, reqwest::header::HeaderMap), reqwest::Error> { -// let mut url = self.url(&format!( -// "v2/waterfalls.cbor?descriptor={}", -// urlencoding::encode(descriptor) -// )); -// -// if let Some(p) = page { -// url.push_str(&format!("&page={}", p)); -// } -// if let Some(idx) = to_index { -// url.push_str(&format!("&to_index={}", idx)); -// } -// if utxo_only { -// url.push_str("&utxo_only=true"); -// } -// -// let response = self.client.get(&url).send().await?; -// let headers = response.headers().clone(); -// let data = response.bytes().await?.to_vec(); -// Ok((data, headers)) -// } -// -// pub async fn waterfalls_v4_cbor( -// &self, -// descriptor: &str, -// page: Option, -// to_index: Option, -// utxo_only: bool, -// ) -> Result<(Vec, reqwest::header::HeaderMap), reqwest::Error> { -// let mut url = self.url(&format!( -// "v4/waterfalls.cbor?descriptor={}", -// urlencoding::encode(descriptor) -// )); -// -// if let Some(p) = page { -// url.push_str(&format!("&page={}", p)); -// } -// if let Some(idx) = to_index { -// url.push_str(&format!("&to_index={}", idx)); -// } -// if utxo_only { -// url.push_str("&utxo_only=true"); -// } -// -// let response = self.client.get(&url).send().await?; -// let headers = response.headers().clone(); -// let data = response.bytes().await?.to_vec(); -// Ok((data, headers)) -// } -// -// // Last used index endpoint -// pub async fn last_used_index(&self, descriptor: &str) -> Result { -// self.client -// .get(&self.url(&format!( -// "v1/last_used_index?descriptor={}", -// urlencoding::encode(descriptor) -// ))) -// .send() -// .await? -// .json() -// .await -// } -// -// // Server information endpoints -// pub async fn server_recipient(&self) -> Result { -// self.client -// .get(&self.url("v1/server_recipient")) -// .send() -// .await? -// .text() -// .await -// } -// -// pub async fn server_address(&self) -> Result { -// self.client -// .get(&self.url("v1/server_address")) -// .send() -// .await? -// .text() -// .await -// } -// -// pub async fn time_since_last_block(&self) -> Result { -// self.client -// .get(&self.url("v1/time_since_last_block")) -// .send() -// .await? -// .text() -// .await -// } -// -// pub async fn build_info(&self) -> Result { -// self.client.get(&self.url("v1/build_info")).send().await?.json().await -// } -// -// // Blockchain data endpoints -// pub async fn tip_hash(&self) -> Result { -// self.client.get(&self.url("blocks/tip/hash")).send().await?.text().await -// } -// -// pub async fn block_hash_by_height(&self, height: u64) -> Result { -// self.client -// .get(&self.url(&format!("block-height/{}", height))) -// .send() -// .await? -// .text() -// .await -// } -// -// pub async fn block_header(&self, hash: &str) -> Result { -// self.client -// .get(&self.url(&format!("block/{}/header", hash))) -// .send() -// .await? -// .text() -// .await -// } -// -// pub async fn tx_raw(&self, txid: &str) -> Result, reqwest::Error> { -// self.client -// .get(&self.url(&format!("tx/{}/raw", txid))) -// .send() -// .await? -// .bytes() -// .await -// .map(|b| b.to_vec()) -// } -// -// pub async fn address_txs(&self, address: &str) -> Result, reqwest::Error> { -// self.client -// .get(&self.url(&format!("address/{}/txs", address))) -// .send() -// .await? -// .json() -// .await -// } -// -// // Transaction broadcasting -// pub async fn broadcast(&self, tx_hex: &str) -> Result { -// self.client -// .post(&self.url("tx")) -// .body(tx_hex.to_string()) -// .send() -// .await? -// .text() -// .await -// } -// -// // Prometheus metrics -// pub async fn metrics(&self) -> Result { -// self.client.get(&self.url("metrics")).send().await?.text().await -// } -// } diff --git a/crates/runtime/src/waterfall/types.rs b/crates/runtime/src/waterfall/types.rs deleted file mode 100644 index 3c1650a..0000000 --- a/crates/runtime/src/waterfall/types.rs +++ /dev/null @@ -1,62 +0,0 @@ -// use serde::{Deserialize, Serialize}; -// use std::collections::HashMap; -// -// #[derive(Debug, Clone, Serialize, Deserialize)] -// pub struct WaterfallResponse { -// pub txs_seen: HashMap>, -// pub page: u32, -// #[serde(skip_serializing_if = "Option::is_none")] -// pub tip: Option, -// } -// -// #[derive(Debug, Clone, Serialize, Deserialize)] -// pub struct WaterfallResponseV4 { -// pub txs_seen: HashMap>, -// pub page: u32, -// pub tip_meta: TipMeta, -// } -// -// #[derive(Debug, Clone, Serialize, Deserialize)] -// pub struct TxSeen { -// pub txid: String, -// pub height: u64, -// pub block_hash: String, -// pub block_timestamp: u64, -// pub v: u32, -// } -// -// #[derive(Debug, Clone, Serialize, Deserialize)] -// pub struct TipMeta { -// pub b: String, // block hash -// pub t: u64, // timestamp -// pub h: u64, // height -// } -// -// #[derive(Debug, Clone, Serialize, Deserialize)] -// pub struct LastUsedIndex { -// #[serde(skip_serializing_if = "Option::is_none")] -// pub external: Option, -// #[serde(skip_serializing_if = "Option::is_none")] -// pub internal: Option, -// #[serde(skip_serializing_if = "Option::is_none")] -// pub tip: Option, -// } -// -// #[derive(Debug, Clone, Serialize, Deserialize)] -// pub struct BuildInfo { -// pub version: String, -// pub git_commit: String, -// } -// -// #[derive(Debug, Clone, Serialize, Deserialize)] -// pub struct AddressTxs { -// pub txid: String, -// pub status: AddressTxStatus, -// } -// -// #[derive(Debug, Clone, Serialize, Deserialize)] -// pub struct AddressTxStatus { -// pub block_height: u64, -// #[serde(skip_serializing_if = "Option::is_none")] -// pub block_hash: Option, -// } diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 333a736..0db46a0 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -10,7 +10,7 @@ readme = "README.md" workspace = true [dependencies] -simplex-runtime = { workspace = true } +simplex-provider = { workspace = true } thiserror = { workspace = true } sha2 = { workspace = true } diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index a0a5dc9..7ef9571 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1,10 +1,10 @@ -pub mod witness_transaction; -pub mod program; pub mod arguments; -pub mod witness; -pub mod signer; -pub mod provider; -pub mod utils; pub mod constants; pub mod error; pub mod presets; +pub mod program; +pub mod provider; +pub mod signer; +pub mod utils; +pub mod witness; +pub mod witness_transaction; diff --git a/crates/sdk/src/provider/esplora.rs b/crates/sdk/src/provider/esplora.rs index a4927ab..fff3c91 100644 --- a/crates/sdk/src/provider/esplora.rs +++ b/crates/sdk/src/provider/esplora.rs @@ -3,7 +3,7 @@ use crate::provider::Provider; use simplicityhl::elements::{Transaction, Txid}; use std::collections::HashMap; -pub use simplex_runtime::esplora::*; +pub use simplex_provider::esplora::*; impl Provider for EsploraClient { fn broadcast_transaction(&self, tx: &Transaction) -> Result { diff --git a/crates/sdk/src/signed_transaction.rs b/crates/sdk/src/signed_transaction.rs deleted file mode 100644 index 3e71102..0000000 --- a/crates/sdk/src/signed_transaction.rs +++ /dev/null @@ -1,135 +0,0 @@ -use simplicityhl::WitnessValues; -use simplicityhl::elements::secp256k1_zkp::schnorr::Signature; -use simplicityhl::elements::{Transaction, TxOut}; - -use crate::constants::{SimplicityNetwork, WITNESS_SCALE_FACTOR}; -use crate::error::SimplexError; -use crate::program::ProgramTrait; -use crate::provider::Provider; -use crate::signer::SignerTrait; -use crate::witness::WitnessTrait; - -struct SignedInput<'a, T> { - program: &'a dyn ProgramTrait, - witness: &'a dyn WitnessTrait, - signer: Option<&'a dyn SignerTrait>, - signer_lambda: Option, -} - -pub struct SignedTransaction<'a, T> { - tx: Transaction, - utxos: &'a [TxOut], - network: SimplicityNetwork, - inputs: Vec>, -} - -impl<'a, T> SignedTransaction<'a, T> -where - T: Fn(WitnessValues, Signature) -> Result + Clone, -{ - pub fn new(tx: Transaction, utxos: &'a [TxOut], network: SimplicityNetwork) -> Self { - Self { - tx, - utxos, - network, - inputs: Vec::new(), - } - } - - pub fn add_input(&mut self, program: &'a dyn ProgramTrait, witness: &'a dyn WitnessTrait) { - let signed_input = SignedInput { - program, - witness, - signer: None, - signer_lambda: None, - }; - - self.inputs.push(signed_input); - } - - pub fn add_signed_input( - &mut self, - program: &'a dyn ProgramTrait, - witness: &'a dyn WitnessTrait, - signer: &'a dyn SignerTrait, - signer_lambda: T, - ) { - let signed_input = SignedInput { - program, - witness, - signer: Some(signer), - signer_lambda: Some(signer_lambda), - }; - - self.inputs.push(signed_input); - } - - pub fn finalize_with_fee( - &self, - target_blocks: u32, - provider: impl Provider, - ) -> Result<(Transaction, u64), SimplexError> { - let fee_rate = provider.get_fee_rate(target_blocks)?; - let final_tx = self.finalize()?; - - let fee = self.calculate_fee(final_tx.weight(), fee_rate); - - Ok((final_tx, fee)) - } - - pub fn finalize(&self) -> Result { - let mut final_tx = self.tx.clone(); - - for index in 0..self.inputs.len() { - let (program, witness, signer, signer_lambda) = { - let input = &self.inputs[index]; - (input.program, input.witness, input.signer, input.signer_lambda.clone()) - }; - - if signer.is_some() { - final_tx = self.finalize_with_signer( - final_tx, - program, - witness.build_witness(), - index, - signer.unwrap(), - signer_lambda.unwrap(), - )?; - } else { - final_tx = self.finalize_as_is(final_tx, program, witness.build_witness(), index)?; - } - } - - Ok(final_tx) - } - - fn finalize_with_signer( - &self, - final_tx: Transaction, - program: &dyn ProgramTrait, - witness: WitnessValues, - index: usize, - signer: &dyn SignerTrait, - signer_lambda: T, - ) -> Result { - let signature = signer.sign(program, &final_tx, self.utxos, index, self.network)?; - let new_witness = signer_lambda(witness, signature)?; - - Ok(self.finalize_as_is(final_tx, program, new_witness, index)?) - } - - fn finalize_as_is( - &self, - final_tx: Transaction, - program: &dyn ProgramTrait, - witness: WitnessValues, - index: usize, - ) -> Result { - Ok(program.finalize(witness, final_tx, self.utxos, index, self.network)?) - } - - 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 - } -} diff --git a/crates/sdk/src/utils.rs b/crates/sdk/src/utils.rs index a8398c1..d1577c8 100644 --- a/crates/sdk/src/utils.rs +++ b/crates/sdk/src/utils.rs @@ -2,9 +2,8 @@ use simplicityhl::simplicity::bitcoin::secp256k1; 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, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, - 0x3a, 0xc0, + 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e, 0x07, 0x8a, + 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0, ]) .expect("key should be valid") } diff --git a/crates/sdk/src/witness_transaction.rs b/crates/sdk/src/witness_transaction.rs index ef6b05d..c69300e 100644 --- a/crates/sdk/src/witness_transaction.rs +++ b/crates/sdk/src/witness_transaction.rs @@ -83,7 +83,6 @@ where change_recipient_script.clone(), PLACEHOLDER_FEE, self.network.policy_asset(), - None, )); diff --git a/crates/simplex/Cargo.toml b/crates/simplex/Cargo.toml index 1abd039..8062896 100644 --- a/crates/simplex/Cargo.toml +++ b/crates/simplex/Cargo.toml @@ -15,22 +15,21 @@ categories = ["cryptography::cryptocurrencies"] workspace = true [features] -default = ["macros", "encoding", "core"] -encoding = ["dep:bincode"] +default = ["macros", "encoding", "sdk"] macros = ["dep:simplex-macros"] -core = ["dep:simplex-core"] +encoding = ["dep:bincode"] +sdk = ["dep:simplex-sdk"] [dependencies] simplex-macros = { workspace = true, features = [], optional = true } -simplex-core = { workspace = true, features = ["encoding"], optional = true } simplex-test = { workspace = true } -simplex-runtime = { workspace = true } +simplex-provider = { workspace = true } +simplex-sdk = { workspace = true, optional = true } bincode = { workspace = true, optional = true } simplicityhl = { workspace = true, features = ["serde"] } serde = { version = "1.0.228" } either = { version = "1.15.0", features = ["serde"] } - tokio = { version = "1.49.0", features = ["full"]} [dev-dependencies] diff --git a/crates/simplex/src/lib.rs b/crates/simplex/src/lib.rs index 56fd2f4..9bd9fb4 100644 --- a/crates/simplex/src/lib.rs +++ b/crates/simplex/src/lib.rs @@ -1,15 +1,11 @@ -#![warn(clippy::all, clippy::pedantic)] - -//! High-level helpers for building and executing Simplicity programs on Liquid. - pub extern crate either; pub extern crate serde; #[cfg(feature = "macros")] pub extern crate simplex_macros; -#[cfg(feature = "core")] -pub extern crate simplex_core; +#[cfg(feature = "sdk")] +pub extern crate simplex_sdk; #[cfg(feature = "macros")] pub extern crate simplex_test; diff --git a/crates/simplex/tests/simplex_test.rs b/crates/simplex/tests/simplex_test.rs index 7bb673f..1e42356 100644 --- a/crates/simplex/tests/simplex_test.rs +++ b/crates/simplex/tests/simplex_test.rs @@ -1,101 +1,102 @@ -use simplex_runtime::elements_rpc::{AddressType, ElementsRpcClient}; -use simplex_test::DEFAULT_SAT_AMOUNT_FAUCET; -use simplicityhl::elements::Address; -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::secp256k1_zkp::Keypair; +// use simplex_provider::elements_rpc::{AddressType, ElementsRpcClient}; +// use simplex_sdk::constants::SimplicityNetwork; +// use simplex_test::DEFAULT_SAT_AMOUNT_FAUCET; +// use simplicityhl::elements::Address; +// use simplicityhl::elements::bitcoin::secp256k1; +// use simplicityhl::elements::secp256k1_zkp::Keypair; -#[simplex::simplex_macros::test] -// #[test] -fn test_execution() { - assert!(true); -} +// #[simplex::simplex_macros::test] +// // #[test] +// fn test_execution() { +// assert!(true); +// } -#[test] -fn test_invocation_tx_tracking() -> anyhow::Result<()> { - use simplex_test::{ConfigOption, TestProvider}; +// #[test] +// fn test_invocation_tx_tracking() -> anyhow::Result<()> { +// use simplex_test::{ConfigOption, TestProvider}; - fn test_invocation_tx_tracking(rpc: TestProvider, user1_addr: Address, user2_addr: Address) -> anyhow::Result<()> { - // user input code - { - let network = rpc.network(); - let keypair = Keypair::from_seckey_slice(&secp256k1::SECP256K1, &[1; 32])?; - let p2pk = simplex_core::get_p2pk_address(&keypair.x_only_public_key().0, network)?; +// fn test_invocation_tx_tracking(rpc: TestProvider, user1_addr: Address, user2_addr: Address) -> anyhow::Result<()> { +// // user input code +// { +// let network = SimplicityNetwork::default_regtest(); +// let keypair = Keypair::from_seckey_slice(&secp256k1::SECP256K1, &[1; 32])?; +// let p2pk = simplex_core::get_p2pk_address(&keypair.x_only_public_key().0, network)?; - dbg!(p2pk.to_string()); +// dbg!(p2pk.to_string()); - // simplex runtime - // - test provider - // - fields from config - // - - // p2tr +// // simplex runtime +// // - test provider +// // - fields from config +// // - +// // p2tr - // TODO: uncomment and fix - dbg!(ElementsRpcClient::validateaddress(rpc.as_ref(), &p2pk.to_string())?); - // ElementsRpcClient::importaddress(rpc.as_ref(), &p2pk.to_string(), None, None, None)?; +// // TODO: uncomment and fix +// dbg!(ElementsRpcClient::validateaddress(rpc.as_ref(), &p2pk.to_string())?); +// // ElementsRpcClient::importaddress(rpc.as_ref(), &p2pk.to_string(), None, None, None)?; - // broadcast, fetch fee transaction +// // broadcast, fetch fee transaction - let result = ElementsRpcClient::sendtoaddress( - rpc.as_ref(), - &p2pk, - DEFAULT_SAT_AMOUNT_FAUCET, - Some(rpc.network().policy_asset()), - )?; +// let result = ElementsRpcClient::sendtoaddress( +// rpc.as_ref(), +// &p2pk, +// DEFAULT_SAT_AMOUNT_FAUCET, +// Some(rpc.network().policy_asset()), +// )?; - ElementsRpcClient::generate_blocks(rpc.as_ref(), 5)?; +// ElementsRpcClient::generate_blocks(rpc.as_ref(), 5)?; - dbg!(ElementsRpcClient::listunspent( - rpc.as_ref(), - None, - None, - Some(vec![p2pk.to_string()]), - None, - None, - )?,); +// dbg!(ElementsRpcClient::listunspent( +// rpc.as_ref(), +// None, +// None, +// Some(vec![p2pk.to_string()]), +// None, +// None, +// )?,); - dbg!(ElementsRpcClient::scantxoutset( - rpc.as_ref(), - "start", - Some(vec![format!("addr({})", p2pk)]), - )?,); +// dbg!(ElementsRpcClient::scantxoutset( +// rpc.as_ref(), +// "start", +// Some(vec![format!("addr({})", p2pk)]), +// )?,); - Ok(()) - } - } - let rpc = TestProvider::init(ConfigOption::DefaultRegtest).unwrap(); - { - ElementsRpcClient::generate_blocks(rpc.as_ref(), 1).unwrap(); - ElementsRpcClient::rescanblockchain(rpc.as_ref(), None, None).unwrap(); - ElementsRpcClient::sweep_initialfreecoins(rpc.as_ref()).unwrap(); - ElementsRpcClient::generate_blocks(rpc.as_ref(), 100).unwrap(); - } +// Ok(()) +// } +// } +// let rpc = TestProvider::init(ConfigOption::DefaultRegtest).unwrap(); +// { +// ElementsRpcClient::generate_blocks(rpc.as_ref(), 1).unwrap(); +// ElementsRpcClient::rescanblockchain(rpc.as_ref(), None, None).unwrap(); +// ElementsRpcClient::sweep_initialfreecoins(rpc.as_ref()).unwrap(); +// ElementsRpcClient::generate_blocks(rpc.as_ref(), 100).unwrap(); +// } - let user1_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); - let user2_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); - ElementsRpcClient::sendtoaddress( - rpc.as_ref(), - &user1_addr, - DEFAULT_SAT_AMOUNT_FAUCET, - Some(rpc.network().policy_asset()), - ) - .unwrap(); +// let user1_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); +// let user2_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); +// ElementsRpcClient::sendtoaddress( +// rpc.as_ref(), +// &user1_addr, +// DEFAULT_SAT_AMOUNT_FAUCET, +// Some(rpc.network().policy_asset()), +// ) +// .unwrap(); - ElementsRpcClient::sendtoaddress( - rpc.as_ref(), - &user2_addr, - DEFAULT_SAT_AMOUNT_FAUCET, - Some(rpc.network().policy_asset()), - ) - .unwrap(); +// ElementsRpcClient::sendtoaddress( +// rpc.as_ref(), +// &user2_addr, +// DEFAULT_SAT_AMOUNT_FAUCET, +// Some(rpc.network().policy_asset()), +// ) +// .unwrap(); - ElementsRpcClient::generate_blocks(rpc.as_ref(), 3).unwrap(); - dbg!(ElementsRpcClient::listunspent( - rpc.as_ref(), - None, - None, - Some(vec![user1_addr.to_string(), user2_addr.to_string()]), - None, - None, - )?,); - test_invocation_tx_tracking(rpc, user1_addr, user2_addr) -} +// ElementsRpcClient::generate_blocks(rpc.as_ref(), 3).unwrap(); +// dbg!(ElementsRpcClient::listunspent( +// rpc.as_ref(), +// None, +// None, +// Some(vec![user1_addr.to_string(), user2_addr.to_string()]), +// None, +// None, +// )?,); +// test_invocation_tx_tracking(rpc, user1_addr, user2_addr) +// } diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index 8399bfe..a8c5bab 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -9,10 +9,8 @@ workspace = true [dependencies] -simplex-runtime = { workspace = true } -simplex-core = { workspace = true } +simplex-provider = { workspace = true } simplex-sdk = { workspace = true } -simplex-config = { workspace = true } thiserror = { workspace = true } simplicityhl = { workspace = true } diff --git a/crates/test/src/error.rs b/crates/test/src/error.rs index 57b0cda..17c83c9 100644 --- a/crates/test/src/error.rs +++ b/crates/test/src/error.rs @@ -1,4 +1,4 @@ -use simplex_runtime::ExplorerError; +use simplex_provider::ExplorerError; #[derive(thiserror::Error, Debug)] pub enum TestError { diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 2547057..8840c58 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -8,9 +8,8 @@ pub use error::*; use bitcoind::bitcoincore_rpc::{Auth, Client}; use bitcoind::{BitcoinD, Conf}; use electrsd::bitcoind; -use simplex_config::Config; -use simplex_core::SimplicityNetwork; -use simplex_runtime::elements_rpc::ElementsRpcClient; +use simplex_provider::elements_rpc::ElementsRpcClient; +use simplex_sdk::constants::SimplicityNetwork; use simplicityhl::elements::secp256k1_zkp::PublicKey; use simplicityhl::elements::{Address, AssetId}; use std::path::{Path, PathBuf}; @@ -45,8 +44,7 @@ impl TestProvider { Self::ConfiguredNode { node, network } } ConfigOption::CustomRpcUrlRegtest { auth, url: rpc_url } => { - let network = SimplicityNetwork::default_regtest(); - Self::CustomRpc(ElementsRpcClient::new(network, &rpc_url, auth)?) + Self::CustomRpc(ElementsRpcClient::new(&rpc_url, auth)?) } }; @@ -56,11 +54,6 @@ impl TestProvider { Ok(rpc) } - // TODO: is it ok? - pub fn obtain_test_config() -> Config { - todo!() - } - pub fn get_bin_path() -> PathBuf { // TODO: change binary into installed one in $HOME dir const ELEMENTSD_BIN_PATH: &str = "../../assets/elementsd"; @@ -103,13 +96,6 @@ impl TestProvider { TestProvider::CustomRpc(x) => x.client(), } } - - pub fn network(&self) -> SimplicityNetwork { - match self { - TestProvider::ConfiguredNode { network, .. } => *network, - TestProvider::CustomRpc(x) => x.network(), - } - } } impl TestProvider { diff --git a/crates/user/Cargo.toml b/crates/user/Cargo.toml index 6538daa..5b42d86 100644 --- a/crates/user/Cargo.toml +++ b/crates/user/Cargo.toml @@ -10,6 +10,6 @@ readme = "README.md" workspace = true [dependencies] -thiserror = "2" +simplex-sdk = { workspace = true } -simplex-sdk = { path = "../sdk" } +thiserror = "2" From cacc148eb856d0c825bfe9905b95d6ff921632c7 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Fri, 20 Feb 2026 13:51:25 +0200 Subject: [PATCH 02/14] Fix test errors, restrict config alignment --- .gitignore | 2 +- crates/cli/src/cli/mod.rs | 4 +- crates/cli/src/config.rs | 52 +++++++++++++------ crates/simplex/tests/ui/array_tr_storage.rs | 3 -- crates/simplex/tests/ui/bytes32_tr_storage.rs | 3 -- .../simplex/tests/ui/dual_currency_deposit.rs | 3 -- crates/simplex/tests/ui/option_offer.rs | 3 -- crates/simplex/tests/ui/options.rs | 3 -- crates/simplex/tests/ui/simple_storage.rs | 3 -- 9 files changed, 38 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index f2880e6..5144208 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ .vscode/ .cache .env -Simplex.toml +Simplex4.toml config.toml # Debugging data diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 0ee172a..020042a 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -23,8 +23,8 @@ pub struct Cli { impl Cli { #[must_use] - pub fn load_config(&self) -> Config { - Config::load_or_default(&self.config) + pub fn load_config(&self) -> Result { + Ok(Config::load_or_discover(&self.config)?) } /// Runs the CLI command. diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index 73a8ca7..698dca2 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -30,7 +30,11 @@ pub enum ConfigError { /// Errors when getting a path to config. #[error("Path doesn't exist: '{0}'")] - PathIsNotEsixt(PathBuf), + PathNotExist(PathBuf), + + /// Config is missing. + #[error("Config is missing in path: '{0}'")] + MissingConfig(PathBuf), } #[derive(Debug, Default, Clone)] @@ -74,23 +78,37 @@ impl Default for ProviderConfig { } impl Config { - pub fn discover(cfg_override: &ConfigOverride) -> Result, ConfigError> { - Config::_discover().map(|opt| { - opt.map(|mut cfg| { - if let Some(test_conf) = cfg_override.rpc_creds.clone() { - cfg.test_config = test_conf; - } - if let Some(network) = cfg_override.network { - cfg.provider_config.simplicity_network = network; + pub fn discover(cfg_override: Option<&ConfigOverride>) -> Result { + match Config::_discover() { + Ok(mut cfg) => { + if let Some(cfg_override) = cfg_override { + if let Some(test_conf) = cfg_override.rpc_creds.clone() { + cfg.test_config = test_conf; + } + if let Some(network) = cfg_override.network { + cfg.provider_config.simplicity_network = network; + } } - cfg - }) - }) + Ok(cfg) + } + Err(e) => Err(e), + } + } + + pub fn load(path_buf: impl AsRef) -> Result { + Self::from_path(&path_buf) + } + + pub fn load_or_discover(path_buf: impl AsRef) -> Result { + match Self::load(path_buf) { + Ok(cfg) => Ok(cfg), + Err(_) => Self::_discover(), + } } pub fn load_or_default(path_buf: impl AsRef) -> Self { - Self::from_path(path_buf).unwrap_or_else(|_| { - if let Ok(Some(conf)) = Self::_discover() { + Self::load(path_buf).unwrap_or_else(|_| { + if let Ok(conf) = Self::_discover() { conf } else { Self::default() @@ -98,7 +116,7 @@ impl Config { }) } - fn _discover() -> Result, ConfigError> { + fn _discover() -> Result { let cwd = std::env::current_dir()?; let path = cwd.join(CONFIG_FILENAME); dbg!(&path); @@ -106,9 +124,9 @@ impl Config { return Err(ConfigError::PathIsNotFile(path)); } if !path.exists() { - return Err(ConfigError::PathIsNotEsixt(path)); + return Err(ConfigError::PathNotExist(path)); } - Ok(Some(Config::from_path(&path)?)) + Ok(Config::from_path(&path)?) } fn from_path(p: impl AsRef) -> Result { diff --git a/crates/simplex/tests/ui/array_tr_storage.rs b/crates/simplex/tests/ui/array_tr_storage.rs index 93dd077..bbf5413 100644 --- a/crates/simplex/tests/ui/array_tr_storage.rs +++ b/crates/simplex/tests/ui/array_tr_storage.rs @@ -18,8 +18,5 @@ fn main() -> Result<(), String> { let recovered_witness = derived_array_tr_storage::ArrayTrStorageArguments::from_arguments(&witness_values)?; assert_eq!(original_arguments, recovered_witness); - let _template = derived_array_tr_storage::get_template_program(); - let _compiled = derived_array_tr_storage::get_compiled_program(&original_arguments); - Ok(()) } \ No newline at end of file diff --git a/crates/simplex/tests/ui/bytes32_tr_storage.rs b/crates/simplex/tests/ui/bytes32_tr_storage.rs index 463266c..01fcb22 100644 --- a/crates/simplex/tests/ui/bytes32_tr_storage.rs +++ b/crates/simplex/tests/ui/bytes32_tr_storage.rs @@ -17,8 +17,5 @@ fn main() -> Result<(), String> { let recovered_witness = derived_bytes32_tr_storage::Bytes32TrStorageArguments::from_arguments(&witness_values)?; assert_eq!(original_arguments, recovered_witness); - let _template = derived_bytes32_tr_storage::get_template_program(); - let _compiled = derived_bytes32_tr_storage::get_compiled_program(&original_arguments); - Ok(()) } \ No newline at end of file diff --git a/crates/simplex/tests/ui/dual_currency_deposit.rs b/crates/simplex/tests/ui/dual_currency_deposit.rs index 92d4997..eb055a4 100644 --- a/crates/simplex/tests/ui/dual_currency_deposit.rs +++ b/crates/simplex/tests/ui/dual_currency_deposit.rs @@ -43,8 +43,5 @@ fn main() -> Result<(), String> { derived_dual_currency_deposit::DualCurrencyDepositArguments::from_arguments(&witness_values)?; assert_eq!(original_arguments, recovered_witness); - let _template = derived_dual_currency_deposit::get_template_program(); - let _compiled = derived_dual_currency_deposit::get_compiled_program(&original_arguments); - Ok(()) } \ No newline at end of file diff --git a/crates/simplex/tests/ui/option_offer.rs b/crates/simplex/tests/ui/option_offer.rs index 9277639..0db710c 100644 --- a/crates/simplex/tests/ui/option_offer.rs +++ b/crates/simplex/tests/ui/option_offer.rs @@ -23,8 +23,5 @@ fn main() -> Result<(), String> { let recovered_witness = derived_option_offer::OptionOfferArguments::from_arguments(&witness_values)?; assert_eq!(original_arguments, recovered_witness); - let _template = derived_option_offer::get_template_program(); - let _compiled = derived_option_offer::get_compiled_program(&original_arguments); - Ok(()) } \ No newline at end of file diff --git a/crates/simplex/tests/ui/options.rs b/crates/simplex/tests/ui/options.rs index 3f7c182..32553e3 100644 --- a/crates/simplex/tests/ui/options.rs +++ b/crates/simplex/tests/ui/options.rs @@ -27,8 +27,5 @@ fn main() -> Result<(), String> { let recovered_witness = derived_options::OptionsArguments::from_arguments(&witness_values)?; assert_eq!(original_arguments, recovered_witness); - let _template = derived_options::get_template_program(); - let _compiled = derived_options::get_compiled_program(&original_arguments); - Ok(()) } \ No newline at end of file diff --git a/crates/simplex/tests/ui/simple_storage.rs b/crates/simplex/tests/ui/simple_storage.rs index 27d6f57..0aa3718 100644 --- a/crates/simplex/tests/ui/simple_storage.rs +++ b/crates/simplex/tests/ui/simple_storage.rs @@ -18,8 +18,5 @@ fn main() -> Result<(), String> { let recovered_witness = derived_simple_storage::SimpleStorageArguments::from_arguments(&witness_values)?; assert_eq!(original_arguments, recovered_witness); - let _template = derived_simple_storage::get_template_program(); - let _compiled = derived_simple_storage::get_compiled_program(&original_arguments); - Ok(()) } \ No newline at end of file From c9d0606b7897654deedd50f5c6f6b2ee39a7cbf2 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Fri, 20 Feb 2026 14:22:43 +0200 Subject: [PATCH 03/14] Add simplex init command, --- .gitignore | 2 +- Simplex.example.toml | 2 +- crates/cli/src/cli/commands.rs | 2 ++ crates/cli/src/cli/mod.rs | 19 ++++++++++--------- crates/cli/src/config.rs | 9 +++++++-- crates/cli/src/error.rs | 16 ++++++++++------ crates/test/src/testing/config.rs | 2 +- 7 files changed, 32 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 5144208..f2880e6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ .vscode/ .cache .env -Simplex4.toml +Simplex.toml config.toml # Debugging data diff --git a/Simplex.example.toml b/Simplex.example.toml index 54129ce..fd8148f 100644 --- a/Simplex.example.toml +++ b/Simplex.example.toml @@ -1 +1 @@ -network = "liquid" \ No newline at end of file +network = "liquidtestnet" \ No newline at end of file diff --git a/crates/cli/src/cli/commands.rs b/crates/cli/src/cli/commands.rs index 75b0c7a..321cbc6 100644 --- a/crates/cli/src/cli/commands.rs +++ b/crates/cli/src/cli/commands.rs @@ -2,6 +2,8 @@ use clap::Subcommand; #[derive(Debug, Subcommand)] pub enum Command { + /// Initialize project wiht default configuration + Init, /// Show current configuration Config, /// Launch `elementsd` in regtest mode with a default config diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 020042a..e3fabd2 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -1,6 +1,6 @@ pub mod commands; -use crate::config::Config; +use crate::config::{Config, DEFAULT_CONFIG}; use crate::error::Error; use clap::Parser; use simplex_test::TestProvider; @@ -22,21 +22,22 @@ pub struct Cli { } impl Cli { - #[must_use] - pub fn load_config(&self) -> Result { - Ok(Config::load_or_discover(&self.config)?) - } - /// Runs the CLI command. /// /// # Errors /// Returns an error if the command execution fails. pub async fn run(&self) -> Result<(), Error> { - let config = self.load_config(); - match &self.command { + commands::Command::Init => { + let config_path = Config::get_path()?; + std::fs::write(&config_path, DEFAULT_CONFIG)?; + println!("Config written to: '{}'", config_path.display()); + Ok(()) + } commands::Command::Config => { - println!("{config:#?}"); + let loaded_config = + Config::load_or_discover(&self.config).map_err(|e| Error::ConfigDiscoveryFailure(e))?; + println!("{loaded_config:#?}"); Ok(()) } commands::Command::Regtest => { diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index 698dca2..d4a86bd 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -3,6 +3,7 @@ use simplex_sdk::constants::SimplicityNetwork; use std::path::{Path, PathBuf}; use std::str::FromStr; +pub const DEFAULT_CONFIG: &str = include_str!("../../../Simplex.example.toml"); const MANIFEST_DIR: &str = "CARGO_MANIFEST_DIR"; const CONFIG_FILENAME: &str = "Simplex.toml"; @@ -78,6 +79,11 @@ impl Default for ProviderConfig { } impl Config { + pub fn get_path() -> Result { + let cwd = std::env::current_dir()?; + Ok(cwd.join(CONFIG_FILENAME)) + } + pub fn discover(cfg_override: Option<&ConfigOverride>) -> Result { match Config::_discover() { Ok(mut cfg) => { @@ -117,8 +123,7 @@ impl Config { } fn _discover() -> Result { - let cwd = std::env::current_dir()?; - let path = cwd.join(CONFIG_FILENAME); + let path = Self::get_path()?; dbg!(&path); if !path.is_file() { return Err(ConfigError::PathIsNotFile(path)); diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs index 779a55a..981c011 100644 --- a/crates/cli/src/error.rs +++ b/crates/cli/src/error.rs @@ -4,26 +4,30 @@ use simplicityhl::simplicity::hex::HexToArrayError; #[derive(thiserror::Error, Debug)] pub enum Error { /// Errors related to configuration loading or validation. - #[error("Configuration error: {0}")] + #[error("Configuration error: '{0}'")] Config(String), /// Standard I/O errors. - #[error("IO error: {0}")] + #[error("IO error: '{0}'")] Io(#[from] std::io::Error), /// Errors related to Partially Signed Elements Transactions (PSET). - #[error("PSET error: {0}")] + #[error("PSET error: '{0}'")] Pset(#[from] simplicityhl::elements::pset::Error), /// Errors when converting hex strings to byte arrays. - #[error("Hex to array error: {0}")] + #[error("Hex to array error: '{0}'")] HexToArray(#[from] HexToArrayError), /// Errors when using test suite to run elementsd node in regtest. - #[error("Occurred error with test suite, error: {0}")] + #[error("Occurred error with test suite, error: '{0}'")] Test(#[from] Box), /// Errors when building config. - #[error("Occurred error with config building, error: {0}")] + #[error("Occurred error with config building, error: '{0}'")] ConfigError(#[from] crate::config::ConfigError), + + /// Errors when building config. + #[error("Failed to discover config, check existence or create new one with `simplex init`, error: '{0}'")] + ConfigDiscoveryFailure(crate::config::ConfigError), } diff --git a/crates/test/src/testing/config.rs b/crates/test/src/testing/config.rs index 40dd9cc..153e59c 100644 --- a/crates/test/src/testing/config.rs +++ b/crates/test/src/testing/config.rs @@ -1 +1 @@ -pub struct ConfigBuilder {} +pub struct TestingConfig {} From 30de4fab0d6b9ad32125981579b3d7c8406055d3 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Mon, 23 Feb 2026 17:54:44 +0200 Subject: [PATCH 04/14] Add simplex test command --- Cargo.lock | 229 ++++-- Cargo.toml | 2 + crates/cli/Cargo.toml | 7 +- crates/cli/src/cli/commands.rs | 34 +- crates/cli/src/cli/mod.rs | 156 +++- crates/cli/src/config.rs | 39 +- crates/macros/src/macros_core/mod.rs | 26 +- crates/macros/src/macros_core/test/mod.rs | 30 + crates/provider/Cargo.toml | 1 + crates/provider/src/elements_rpc/mod.rs | 52 +- crates/provider/src/elements_rpc/types.rs | 95 +++ crates/provider/src/error.rs | 43 +- crates/provider/src/esplora/mod.rs | 945 ++++++++++++++++++---- crates/sdk/Cargo.toml | 1 + crates/sdk/src/error.rs | 11 + crates/sdk/src/provider/esplora.rs | 54 +- crates/sdk/src/provider/mod.rs | 70 +- crates/sdk/src/witness_transaction.rs | 4 +- crates/simplex/Cargo.toml | 5 +- crates/simplex/src/lib.rs | 3 + crates/simplex/tests/simplex_test.rs | 210 ++--- crates/test/Cargo.toml | 1 + crates/test/src/error.rs | 9 + crates/test/src/lib.rs | 13 +- crates/test/src/testing/config.rs | 19 +- crates/test/src/testing/rpc_provider.rs | 194 ++++- crates/user/src/lib.rs | 28 + crates/user/src/main.rs | 27 - 28 files changed, 1865 insertions(+), 443 deletions(-) create mode 100644 crates/macros/src/macros_core/test/mod.rs create mode 100644 crates/user/src/lib.rs delete mode 100644 crates/user/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index aaa2a83..f90d2b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -401,6 +401,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.7" @@ -422,6 +437,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -667,6 +691,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hex-conservative" version = "0.2.2" @@ -1016,15 +1046,6 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.29" @@ -1130,6 +1151,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + [[package]] name = "objc2" version = "0.6.3" @@ -1173,33 +1200,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] -name = "parking_lot" -version = "0.12.5" +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ - "lock_api", - "parking_lot_core", + "memchr", + "ucd-trie", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "pest_derive" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", + "pest", + "pest_generator", ] [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "pest_generator" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] [[package]] name = "pin-project-lite" @@ -1222,6 +1269,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1413,15 +1466,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.11.0", -] - [[package]] name = "regex" version = "1.12.3" @@ -1630,12 +1674,6 @@ dependencies = [ "regex", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "sct" version = "0.7.1" @@ -1785,16 +1823,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - [[package]] name = "simplex" version = "0.1.0" @@ -1807,8 +1835,9 @@ dependencies = [ "simplex-provider", "simplex-sdk", "simplex-test", - "simplicityhl", - "tokio", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", + "simplicityhl-core", + "tracing", "trybuild", ] @@ -1824,11 +1853,12 @@ dependencies = [ "serde", "simplex-sdk", "simplex-test", - "simplicityhl", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", "thiserror", "tokio", "toml 0.9.12+spec-1.1.0", "tracing", + "tracing-appender", "tracing-subscriber", ] @@ -1840,7 +1870,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "simplicityhl", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", "syn 2.0.116", "thiserror", ] @@ -1853,10 +1883,11 @@ dependencies = [ "bitcoin_hashes", "electrsd", "hex-simd", + "minreq", "reqwest", "serde", "serde_json", - "simplicityhl", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", "thiserror", ] @@ -1864,10 +1895,11 @@ dependencies = [ name = "simplex-sdk" version = "0.1.0" dependencies = [ + "async-trait", "minreq", "sha2", "simplex-provider", - "simplicityhl", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", "thiserror", ] @@ -1876,9 +1908,10 @@ name = "simplex-test" version = "0.1.0" dependencies = [ "electrsd", + "serde", "simplex-provider", "simplex-sdk", - "simplicityhl", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", "thiserror", ] @@ -1917,6 +1950,25 @@ dependencies = [ "cc", ] +[[package]] +name = "simplicityhl" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa7477fc9bfef4cc53ae969db00539f0e67af38156822ac79662513d04f6fee" +dependencies = [ + "base64 0.21.7", + "clap", + "either", + "getrandom 0.2.17", + "itertools", + "miniscript", + "pest", + "pest_derive", + "serde", + "serde_json", + "simplicity-lang", +] + [[package]] name = "simplicityhl" version = "0.4.1" @@ -1934,6 +1986,18 @@ dependencies = [ "simplicity-lang", ] +[[package]] +name = "simplicityhl-core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e341dd0a1c4967109d4b71bc6821d45d0c2310ea0b70efeefe154cd1a0f8932" +dependencies = [ + "hex", + "sha2", + "simplicityhl 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", +] + [[package]] name = "slab" version = "0.4.12" @@ -2085,6 +2149,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -2119,9 +2214,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -2267,6 +2360,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -2344,6 +2449,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index c9c633d..97e4b0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,11 @@ simplex-test = { path = "./crates/test" } simplex-sdk = { path = "./crates/sdk" } simplex = { path = "./crates/simplex" } +async-trait = { version = "0.1.89" } bincode = { version = "2.0.1", features = ["serde"] } ring = { version = "0.17.14" } sha2 = { version = "0.10.9", features = ["compress"] } +serde = { version = "1.0.228" } thiserror = { version = "2.0.18" } hex = { version = "0.4.3" } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index fa7e940..cfcf436 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -21,7 +21,7 @@ simplex-sdk = { workspace = true } simplicityhl = { workspace = true } electrsd = { workspace = true } thiserror = { workspace = true } -serde = { version = "1.0.228" } +serde = { workspace = true } toml = { version = "0.9.8" } anyhow = "1" dotenvy = "0.15" @@ -30,3 +30,8 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] } tracing = { version = "0.1.44" } tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } ctrlc = { version = "3.5.2", features = ["termination"] } + +[dev-dependencies] +tracing = { workspace = true } +tracing-appender = { version = "0.2.3" } +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } \ No newline at end of file diff --git a/crates/cli/src/cli/commands.rs b/crates/cli/src/cli/commands.rs index 321cbc6..d61eccb 100644 --- a/crates/cli/src/cli/commands.rs +++ b/crates/cli/src/cli/commands.rs @@ -1,11 +1,41 @@ -use clap::Subcommand; +use clap::{Args, Subcommand}; #[derive(Debug, Subcommand)] pub enum Command { /// Initialize project wiht default configuration Init, - /// Show current configuration + /// Show the current configuration Config, /// Launch `elementsd` in regtest mode with a default config Regtest, + /// Launch test with + Test { + #[command(subcommand)] + command: TestCommand, + #[command(flatten)] + additional_flags: TestFlags, + }, +} + +/// Test management commands +#[derive(Debug, Subcommand)] +pub enum TestCommand { + /// Run integration tests using simplex conventions + Tests, + /// Run only specific files by path for testing + Test { + #[arg(short = 't', long)] + tests: Vec, + }, +} + +/// Additional flags for tests management +#[derive(Debug, Args, Copy, Clone)] +pub struct TestFlags { + /// Flag for not capturing output in tests + #[arg(long)] + pub nocapture: bool, + /// Show output + #[arg(long = "show-output")] + pub show_output: bool, } diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index e3fabd2..38ef52b 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -1,26 +1,39 @@ pub mod commands; +use crate::cli::commands::{TestCommand, TestFlags}; use crate::config::{Config, DEFAULT_CONFIG}; use crate::error::Error; use clap::Parser; -use simplex_test::TestProvider; -use std::path::PathBuf; +use simplex_test::{TestClientProvider, TestConfig}; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Stdio; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -const DEFAULT_CONFIG_PATH: &str = "config.toml"; - #[derive(Debug, Parser)] #[command(name = "simplicity-dex")] #[command(about = "CLI for Simplicity Options trading on Liquid")] pub struct Cli { - #[arg(short, long, default_value_os_t = default_config_path(), env = "SIMPLEX_CONFIG")] - pub config: PathBuf, + #[arg(short, long, env = "SIMPLEX_CONFIG")] + pub config: Option, #[command(subcommand)] pub command: commands::Command, } +struct TestParams { + cache_path: PathBuf, + test_path: TestPaths, + test_flags: TestFlags, +} + +enum TestPaths { + AllIntegration, + Names(Vec), +} + impl Cli { /// Runs the CLI command. /// @@ -36,8 +49,20 @@ impl Cli { } commands::Command::Config => { let loaded_config = - Config::load_or_discover(&self.config).map_err(|e| Error::ConfigDiscoveryFailure(e))?; + Config::load_or_discover(self.config.clone()).map_err(|e| Error::ConfigDiscoveryFailure(e))?; + println!("{loaded_config:#?}"); + Ok(()) + } + commands::Command::Test { + command, + additional_flags, + } => { + let loaded_config = + Config::load_or_discover(self.config.clone()).map_err(|e| Error::ConfigDiscoveryFailure(e))?; println!("{loaded_config:#?}"); + + self.run_test_command(loaded_config, command, additional_flags)?; + Ok(()) } commands::Command::Regtest => { @@ -49,7 +74,7 @@ impl Cli { }) .expect("Error setting Ctrl-C handler"); - let mut node = TestProvider::create_default_node_with_stdin(); + let mut node = TestClientProvider::create_default_node_with_stdin(); println!("======================================"); println!("Waiting for Ctrl-C..."); @@ -65,9 +90,116 @@ impl Cli { } } } -} -#[must_use] -pub fn default_config_path() -> PathBuf { - PathBuf::from(DEFAULT_CONFIG_PATH) + pub(crate) fn run_test_command( + &self, + config: Config, + command: &TestCommand, + test_flags: &TestFlags, + ) -> Result<(), Error> { + let cache_path = Self::save_cached_test_config(&config.test_config)?; + let mut test_command = match command { + TestCommand::Tests => Self::form_test_command(TestParams { + cache_path, + test_path: TestPaths::AllIntegration, + test_flags: *test_flags, + }), + TestCommand::Test { tests } => { + let test_path = if tests.is_empty() { + TestPaths::AllIntegration + } else { + TestPaths::Names(tests.clone()) + }; + Self::form_test_command(TestParams { + cache_path, + test_path, + test_flags: *test_flags, + }) + } + }; + let output = test_command.output()?; + match output.status.code() { + Some(code) => { + println!("Exit Status: {}", code); + + if code == 0 { + println!("{}", String::from_utf8(output.stdout).unwrap()); + } + } + None => { + println!("Process terminated."); + } + } + Ok(()) + } + + fn form_test_command(params: TestParams) -> std::process::Command { + let mut test_command = std::process::Command::new("sh"); + test_command.arg("-c"); + match params.test_path { + TestPaths::AllIntegration => { + test_command.args(["cargo test --tests"]); + } + TestPaths::Names(names) => { + let mut args: Vec = Vec::with_capacity(1 + names.len()); + args.push("cargo test".to_string()); + for test_name in names { + args.push(format!("--test {test_name}")); + } + test_command.args(args.into_iter()); + } + } + { + match params.test_flags.show_output { + true => match params.test_flags.nocapture { + true => { + test_command.args(["--", "--nocapture", "--show-output"]); + } + false => { + test_command.args(["--", "--show-output"]); + } + }, + false => match params.test_flags.nocapture { + true => { + test_command.args(["--", "--nocapture"]); + } + false => {} + }, + } + } + test_command + .env(simplex_test::TEST_ENV_NAME, params.cache_path) + .stdin(Stdio::inherit()) + .stderr(Stdio::inherit()) + .stdout(Stdio::inherit()); + test_command + } + + fn save_cached_test_config(test_config: &TestConfig) -> Result { + let cache_dir = Self::get_cache_dir()?; + std::fs::create_dir_all(&cache_dir)?; + let test_config_cache_name = Self::create_cache_name(&cache_dir); + + let mut file = OpenOptions::new() + .create(true) + .write(true) + .open(&test_config_cache_name)?; + file.write(toml::to_string_pretty(&test_config).unwrap().as_bytes())?; + file.flush()?; + Ok(test_config_cache_name) + } + + fn get_cache_dir() -> Result { + const TARGET_DIR_NAME: &str = "target"; + const SIMPLEX_CACHE_DIR_NAME: &str = "simplex"; + + let cwd = std::env::current_dir()?; + Ok(cwd.join(TARGET_DIR_NAME).join(SIMPLEX_CACHE_DIR_NAME)) + } + + fn create_cache_name(path: impl AsRef) -> PathBuf { + const TEST_CACHE_NAME: &str = "test_config.toml"; + + path.as_ref().join(TEST_CACHE_NAME) + } } diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index d4a86bd..cd40853 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -1,10 +1,10 @@ use serde::{Deserialize, Serialize}; use simplex_sdk::constants::SimplicityNetwork; +use simplex_test::TestConfig; use std::path::{Path, PathBuf}; use std::str::FromStr; pub const DEFAULT_CONFIG: &str = include_str!("../../../Simplex.example.toml"); -const MANIFEST_DIR: &str = "CARGO_MANIFEST_DIR"; const CONFIG_FILENAME: &str = "Simplex.toml"; #[derive(thiserror::Error, Debug)] @@ -49,21 +49,6 @@ pub struct ProviderConfig { simplicity_network: SimplicityNetwork, } -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct TestConfig { - pub rpc_creds: RpcCreds, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub enum RpcCreds { - Auth { - rpc_username: String, - rpc_password: String, - }, - #[default] - None, -} - #[derive(Debug, Default, Clone)] pub struct ConfigOverride { pub rpc_creds: Option, @@ -105,21 +90,19 @@ impl Config { Self::from_path(&path_buf) } - pub fn load_or_discover(path_buf: impl AsRef) -> Result { - match Self::load(path_buf) { - Ok(cfg) => Ok(cfg), - Err(_) => Self::_discover(), + pub fn load_or_discover(path_buf: Option>) -> Result { + match path_buf { + Some(path) => Self::load(path), + None => Self::_discover(), } } - pub fn load_or_default(path_buf: impl AsRef) -> Self { - Self::load(path_buf).unwrap_or_else(|_| { - if let Ok(conf) = Self::_discover() { - conf - } else { - Self::default() - } - }) + pub fn load_or_default(path_buf: Option>) -> Self { + let x = match path_buf { + Some(path) => Self::load(path), + None => Self::_discover(), + }; + x.unwrap_or_else(|_| Self::default()) } fn _discover() -> Result { diff --git a/crates/macros/src/macros_core/mod.rs b/crates/macros/src/macros_core/mod.rs index 8132d6a..8cbe550 100644 --- a/crates/macros/src/macros_core/mod.rs +++ b/crates/macros/src/macros_core/mod.rs @@ -1,6 +1,7 @@ #![warn(clippy::all, clippy::pedantic)] -pub mod attr; +pub(crate) mod attr; +pub(crate) mod test; pub(crate) mod program; @@ -22,26 +23,5 @@ pub fn expand_include_simf(input: &attr::parse::SynFilePath) -> syn::Result syn::Result { // TODO: maybe check crate attributes to allow user to do smth like in sqlx? - Ok(expand_simple(input)) -} - -fn expand_simple(input: &syn::ItemFn) -> proc_macro2::TokenStream { - let ret = &input.sig.output; - let name = &input.sig.ident; - let body = &input.block; - let attrs = &input.attrs; - - let fn_name_str = name.to_string(); - let ident = format!("{input:#?}"); - quote::quote! { - #[::core::prelude::v1::test] - #(#attrs)* - fn #name() #ret { - #body - // ::sqlx::test_block_on(async { #body }) - // before - println!("Running test: {}, \n -- {}", #fn_name_str, #ident); - //revert - } - } + Ok(test::expand_simple(input)) } diff --git a/crates/macros/src/macros_core/test/mod.rs b/crates/macros/src/macros_core/test/mod.rs new file mode 100644 index 0000000..ef09f63 --- /dev/null +++ b/crates/macros/src/macros_core/test/mod.rs @@ -0,0 +1,30 @@ +pub(crate) fn expand_simple(input: &syn::ItemFn) -> proc_macro2::TokenStream { + let ret = &input.sig.output; + let name = &input.sig.ident; + let body = &input.block; + let attrs = &input.attrs; + + let fn_name_str = name.to_string(); + let ident = format!("{input:#?}"); + quote::quote! { + #[::core::prelude::v1::test] + #(#attrs)* + fn #name() #ret { + use ::simplex::tracing; + if std::env::var(simplex_test::TEST_ENV_NAME).is_err() { + tracing::trace!("Test '{}' connected with simplex is disabled, run `simplex test` in order to test it", #fn_name_str); + println!("disabled"); + return; + } else { + tracing::trace!("Running '{}' with simplex configuration", #fn_name_str); + println!("running"); + } + + #body + // ::sqlx::test_block_on(async { #body }) + // before + println!("Running test: {}, \n -- {}", #fn_name_str, #ident); + //revert + } + } +} diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml index 9ee7360..175f2d0 100644 --- a/crates/provider/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -19,6 +19,7 @@ thiserror = { workspace = true } electrsd = { workspace = true } async-trait = { version = "0.1.89" } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +minreq = { version = "2.14", features = ["https", "json-using-serde"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" hex-simd = "0.8.0" diff --git a/crates/provider/src/elements_rpc/mod.rs b/crates/provider/src/elements_rpc/mod.rs index cd0c688..682d241 100644 --- a/crates/provider/src/elements_rpc/mod.rs +++ b/crates/provider/src/elements_rpc/mod.rs @@ -200,7 +200,16 @@ impl ElementsRpcClient { Ok(value.get("hex").unwrap().as_str().unwrap().to_string()) } - pub fn sendrawtransaction(client: &Client, tx: &str) -> Result { + pub fn sendrawtransaction(client: &Client, tx: &str) -> Result { + const METHOD: &str = "sendrawtransaction"; + + let value: serde_json::Value = client.call(METHOD, &[tx.into()])?; + Ok(SendRawTransaction { + txid: value.as_str().unwrap().to_string(), + }) + } + + pub fn sendrawtransaction_txid(client: &Client, tx: &str) -> Result { const METHOD: &str = "sendrawtransaction"; let value: serde_json::Value = client.call(METHOD, &[tx.into()])?; @@ -355,6 +364,47 @@ impl ElementsRpcClient { ScantxoutsetResult::from_value(response, action) .map_err(|e| ExplorerError::ElementsRpcUnexpectedReturn(e.to_string())) } + + pub fn gettransaction( + client: &Client, + txid: &str, + include_watchonly: Option, + ) -> Result { + const METHOD: &str = "gettransaction"; + + let mut args = vec![txid.into()]; + + if let Some(watchonly) = include_watchonly { + args.push(watchonly.into()); + } + + Ok(client.call::(METHOD, &args)?) + } + + pub fn getrawtransaction( + client: &Client, + txid: &str, + verbose: Option, + ) -> Result { + const METHOD: &str = "getrawtransaction"; + + let mut args = vec![txid.into()]; + + if let Some(v) = verbose { + args.push(v.into()); + } else { + args.push(true.into()); + } + + Ok(client.call::(METHOD, &args)?) + } + + pub fn getrawtransaction_hex(client: &Client, txid: &str) -> Result { + const METHOD: &str = "getrawtransaction"; + + let value: serde_json::Value = client.call(METHOD, &[txid.into(), false.into()])?; + Ok(value.as_str().unwrap().to_string()) + } } fn sat2btc(sat: u64) -> String { diff --git a/crates/provider/src/elements_rpc/types.rs b/crates/provider/src/elements_rpc/types.rs index fcb6902..b5a73c2 100644 --- a/crates/provider/src/elements_rpc/types.rs +++ b/crates/provider/src/elements_rpc/types.rs @@ -230,3 +230,98 @@ pub struct ScantxoutsetUtxo { pub txid: String, pub vout: u32, } + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GetTransaction { + pub amount: f64, + pub fee: Option, + pub confirmations: i32, + pub blockhash: Option, + pub blockindex: Option, + pub blocktime: Option, + pub txid: String, + pub time: u64, + pub timereceived: u64, + #[serde(default)] + pub bip125_replaceable: String, + pub details: Vec, + pub hex: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TransactionDetail { + pub involveswatchonly: Option, + pub address: Option, + pub category: String, + pub amount: f64, + pub label: Option, + pub vout: u32, + #[serde(default)] + pub fee: Option, + pub abandoned: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SendRawTransaction { + pub txid: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GetRawTransaction { + pub in_active_chain: Option, + pub hex: String, + pub txid: String, + pub hash: String, + pub size: u32, + pub vsize: u32, + pub weight: u32, + pub version: u32, + pub locktime: u32, + pub vin: Vec, + pub vout: Vec, + pub blockhash: Option, + pub confirmations: Option, + pub time: Option, + pub blocktime: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RawTransactionInput { + pub txid: String, + pub vout: u32, + #[serde(rename = "scriptSig")] + pub script_sig: ScriptSig, + #[serde(default)] + pub txinwitness: Vec, + pub sequence: u32, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ScriptSig { + pub asm: String, + pub hex: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RawTransactionOutput { + pub value: f64, + pub n: u32, + pub scriptPubKey: ScriptPubKey, + #[serde(default)] + pub asset: Option, + #[serde(default)] + pub assetcommitment: Option, + #[serde(default)] + pub valuecommitment: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ScriptPubKey { + pub asm: String, + pub hex: String, + #[serde(rename = "reqSigs")] + pub req_sigs: Option, + #[serde(rename = "type")] + pub script_type: String, + pub addresses: Option>, +} diff --git a/crates/provider/src/error.rs b/crates/provider/src/error.rs index 2b58259..c657e67 100644 --- a/crates/provider/src/error.rs +++ b/crates/provider/src/error.rs @@ -1,3 +1,4 @@ +use electrsd::bitcoind::bitcoincore_rpc::jsonrpc::minreq; use reqwest::{StatusCode, Url}; #[derive(thiserror::Error, Debug)] @@ -7,18 +8,24 @@ pub enum ExplorerError { #[error("Failed to send request, [url: '{url:?}', code: {status:?}, text: '{text}']")] Request { - url: Option, + url: Option, status: Option, text: String, }, + #[error("Failed to minreq send request, [err: '{err}']")] + RequestMinreq { err: minreq::Error }, + #[error("Erroneous response, [url: '{url:?}', code: {status:?}, text: '{text}']")] ErroneousRequest { - url: Option, + url: Option, status: Option, text: String, }, + #[error("Erroneous minreq response, [err: '{err}']")] + ErroneousRequestMinreq { err: minreq::Error }, + #[error("Failed to deserialize response, [url: '{url:?}', code: {status:?}, text: '{text}']")] Deserialize { url: Option, @@ -26,6 +33,9 @@ pub enum ExplorerError { text: String, }, + #[error("Failed to deserialize minreq response, [err: '{err}']")] + DeserializeMinreq { err: minreq::Error }, + #[error("Failed to decode hex value to array, {0}")] BitcoinHashesHex(#[from] bitcoin_hashes::hex::HexToArrayError), @@ -66,29 +76,48 @@ pub enum CommitmentType { impl ExplorerError { #[inline] - pub(crate) fn response_failed(e: &reqwest::Error) -> Self { + pub(crate) fn response_failed_reqwest(e: &reqwest::Error) -> Self { ExplorerError::Request { - url: e.url().cloned(), + url: e.url().cloned().map(|x| x.to_string()), status: e.status(), text: e.to_string(), } } #[inline] - pub(crate) fn erroneous_response(e: &reqwest::Response) -> Self { + pub(crate) fn erroneous_response_reqwest(e: &reqwest::Response) -> Self { ExplorerError::ErroneousRequest { - url: Some(e.url().clone()), + url: Some(e.url().clone().to_string()), status: Some(e.status()), text: String::new(), } } #[inline] - pub(crate) fn deserialize(e: &reqwest::Error) -> Self { + pub(crate) fn response_failed_minreq(e: minreq::Error) -> Self { + ExplorerError::RequestMinreq { err: e } + } + + #[inline] + pub(crate) fn erroneous_response_minreq(e: &minreq::Response) -> Self { + ExplorerError::ErroneousRequest { + url: Some(e.url.clone()), + status: Some(StatusCode::from_u16(e.status_code as u16).unwrap()), + text: e.reason_phrase.clone(), + } + } + + #[inline] + pub(crate) fn deserialize_reqwest(e: &reqwest::Error) -> Self { ExplorerError::Deserialize { url: e.url().cloned(), status: e.status(), text: e.to_string(), } } + + #[inline] + pub(crate) fn deserialize_minreq(e: minreq::Error) -> Self { + ExplorerError::DeserializeMinreq { err: e } + } } diff --git a/crates/provider/src/esplora/mod.rs b/crates/provider/src/esplora/mod.rs index 74bc175..5c9297b 100644 --- a/crates/provider/src/esplora/mod.rs +++ b/crates/provider/src/esplora/mod.rs @@ -11,11 +11,15 @@ use std::str::FromStr; const ESPLORA_LIQUID_TESTNET: &str = "https://blockstream.info/liquidtestnet/api"; const ESPLORA_LIQUID: &str = "https://blockstream.info/liquid/api"; -pub struct EsploraClient { - base_url: String, +pub struct EsploraClientAsync { + url_builder: UrlBuilder, client: reqwest::Client, } +pub struct EsploraClientSync { + url_builder: UrlBuilder, +} + #[derive(Debug, Clone)] pub struct EsploraClientBuilder { url: Option, @@ -50,12 +54,22 @@ impl EsploraClientBuilder { EsploraClientBuilder { url: Some(url.into()) } } - pub fn build(self) -> EsploraClient { - EsploraClient { - base_url: self.url.unwrap_or(Self::default_url()), + pub fn build_async(self) -> EsploraClientAsync { + EsploraClientAsync { + url_builder: UrlBuilder { + base_url: self.url.unwrap_or(Self::default_url()), + }, client: reqwest::Client::new(), } } + + pub fn build_sync(self) -> EsploraClientSync { + EsploraClientSync { + url_builder: UrlBuilder { + base_url: self.url.unwrap_or(Self::default_url()), + }, + } + } } impl Default for EsploraClientBuilder { @@ -64,9 +78,15 @@ impl Default for EsploraClientBuilder { } } -impl Default for EsploraClient { +impl Default for EsploraClientAsync { fn default() -> Self { - EsploraClientBuilder::default().build() + EsploraClientBuilder::default().build_async() + } +} + +impl Default for EsploraClientSync { + fn default() -> Self { + EsploraClientBuilder::default().build_sync() } } @@ -458,84 +478,79 @@ mod deserializable { } } -impl EsploraClient { - #[inline] - fn join_url(&self, str: impl AsRef) -> Result { - Ok(format!("{}/{}", self.base_url, str.as_ref())) - } - +impl EsploraClientAsync { #[inline] fn filter_resp(resp: &reqwest::Response) -> Result<(), ExplorerError> { - if !(200..300).contains(&resp.status().as_u16()) { - return Err(ExplorerError::erroneous_response(resp)); + if is_resp_ok(resp.status().as_u16() as i32) { + return Err(ExplorerError::erroneous_response_reqwest(resp)); } Ok(()) } pub async fn get_tx(&self, txid: &str) -> Result { - let url = self.join_url(format!("/tx/{txid}"))?; + let url = self.url_builder.get_tx_url(txid)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::() .await - .map_err(|e| ExplorerError::deserialize(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.convert()?; Ok(resp) } pub async fn get_tx_status(&self, txid: &str) -> Result { - let url = self.join_url(format!("tx/{txid}/status"))?; + let url = self.url_builder.get_tx_status_url(txid)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::() .await - .map_err(|e| ExplorerError::deserialize(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.convert()?; Ok(resp) } pub async fn get_tx_hex(&self, txid: &str) -> Result { - let url = self.join_url(format!("tx/{txid}/hex"))?; + let url = self.url_builder.get_tx_hex_url(txid)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; - resp.text().await.map_err(|e| ExplorerError::deserialize(&e)) + resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } pub async fn get_tx_raw(&self, txid: &str) -> Result, ExplorerError> { - let url = self.join_url(format!("tx/{txid}/raw"))?; + let url = self.url_builder.get_tx_raw_url(txid)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; resp.bytes() .await .map(|b| b.to_vec()) - .map_err(|e| ExplorerError::deserialize(&e)) + .map_err(|e| ExplorerError::deserialize_reqwest(&e)) } pub async fn get_tx_elements(&self, txid: &str) -> Result { @@ -545,79 +560,79 @@ impl EsploraClient { } pub async fn get_tx_merkle_proof(&self, txid: &str) -> Result { - let url = self.join_url(format!("tx/{txid}/merkle-proof"))?; + let url = self.url_builder.get_tx_merkle_proof_url(txid)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.convert()?; Ok(resp) } pub async fn get_tx_outspend(&self, txid: &str, vout: u32) -> Result { - let url = self.join_url(format!("tx/{txid}/outspend/{vout}"))?; + let url = self.url_builder.get_tx_outspend_url(txid, vout)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.convert()?; Ok(resp) } pub async fn get_tx_outspends(&self, txid: &str) -> Result, ExplorerError> { - let url = self.join_url(format!("tx/{txid}/outspends"))?; + let url = self.url_builder.get_tx_outspends_url(txid)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::>() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; resp.into_iter().map(|x| x.convert()).collect::, _>>() } - pub async fn broadcast_tx(&self, tx: &simplicityhl::elements::Transaction) -> Result { + pub async fn broadcast_tx(&self, tx: &simplicityhl::elements::Transaction) -> Result { let tx_hex = simplicityhl::elements::encode::serialize_hex(tx); - let url = self.join_url("tx")?; + let url = self.url_builder.get_broadcast_tx_url()?; let resp = self .client .post(url) .body(tx_hex) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; - resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) + let resp = resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + Ok(Txid::from_str(&resp)?) } - // TODO: add batch execution with 10 elements pub async fn broadcast_tx_package( &self, txs: &[simplicityhl::elements::Transaction], ) -> Result { - let url = self.join_url("txs/package")?; + let url = self.url_builder.get_broadcast_tx_package_url()?; let tx_hexes = txs .iter() .map(simplicityhl::elements::encode::serialize_hex) @@ -629,44 +644,44 @@ impl EsploraClient { .json(&tx_hexes) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; - resp.json().await.map_err(|e| ExplorerError::response_failed(&e)) + resp.json().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } pub async fn get_address(&self, address: &str) -> Result { - let url = self.join_url(format!("address/{address}"))?; + let url = self.url_builder.get_address_url(address)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.convert()?; Ok(resp) } pub async fn get_address_txs(&self, address: &str) -> Result, ExplorerError> { - let url = self.join_url(format!("address/{address}/txs"))?; + let url = self.url_builder.get_address_txs_url(address)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::>() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; Ok(resp) } @@ -676,23 +691,19 @@ impl EsploraClient { address: &str, last_seen_txid: Option<&str>, ) -> Result, ExplorerError> { - let url = if let Some(txid) = last_seen_txid { - self.join_url(format!("address/{address}/txs/chain/{txid}"))? - } else { - self.join_url(format!("address/{address}/txs/chain"))? - }; + let url = self.url_builder.get_address_txs_chain_url(address, last_seen_txid)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::>() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; Ok(resp) } @@ -701,166 +712,159 @@ impl EsploraClient { &self, address: &str, ) -> Result, ExplorerError> { - let url = self.join_url(format!("address/{address}/txs/mempool"))?; + let url = self.url_builder.get_address_txs_mempool_url(address)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::>() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; Ok(resp) } pub async fn get_address_utxo(&self, address: &str) -> Result, ExplorerError> { - let url = self.join_url(format!("address/{address}/utxo"))?; + let url = self.url_builder.get_address_utxo_url(address)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::>() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; resp.into_iter().map(|x| x.convert()).collect::, _>>() } pub async fn get_scripthash(&self, hash: &str) -> Result { - let url = self.join_url(format!("scripthash/{hash}"))?; + let url = self.url_builder.get_scripthash_url(hash)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.convert()?; Ok(resp) } - // TODO: check output pub async fn get_scripthash_txs(&self, hash: &str) -> Result { - let url = self.join_url(format!("scripthash/{hash}/txs"))?; + let url = self.url_builder.get_scripthash_txs_url(hash)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; - resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) + resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } - // TODO: check output pub async fn get_scripthash_txs_chain( &self, hash: &str, last_seen_txid: Option<&str>, ) -> Result { - let url = if let Some(txid) = last_seen_txid { - self.join_url(format!("scripthash/{hash}/txs/chain/{txid}"))? - } else { - self.join_url(format!("scripthash/{hash}/txs/chain"))? - }; + let url = self.url_builder.get_scripthash_txs_chain_url(hash, last_seen_txid)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; - resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) + resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } - // TODO: check output pub async fn get_scripthash_txs_mempool(&self, hash: &str) -> Result { - let url = self.join_url(format!("scripthash/{hash}/txs/mempool"))?; + let url = self.url_builder.get_scripthash_txs_mempool_url(hash)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; - resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) + resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } - // TODO: check output pub async fn get_scripthash_utxo(&self, hash: &str) -> Result { - let url = self.join_url(format!("scripthash/{hash}/utxo"))?; + let url = self.url_builder.get_scripthash_utxo_url(hash)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; - resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) + resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } pub async fn get_block(&self, hash: &str) -> Result { - let url = self.join_url(format!("block/{hash}"))?; + let url = self.url_builder.get_block_url(hash)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.convert()?; Ok(resp) } - // TODO: decode hex into elements::BlockHeader (no method to do this) pub async fn get_block_header(&self, hash: &str) -> Result { - let url = self.join_url(format!("block/{hash}/header"))?; + let url = self.url_builder.get_block_header_url(hash)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; - let resp = resp.text().await.map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e))?; Ok(resp) } pub async fn get_block_status(&self, hash: &str) -> Result { - let url = self.join_url(format!("block/{hash}/status"))?; + let url = self.url_builder.get_block_status_url(hash)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; resp.json::() .await - .map_err(|e| ExplorerError::response_failed(&e)) + .map_err(|e| ExplorerError::deserialize_reqwest(&e)) } pub async fn get_block_txs( @@ -868,37 +872,37 @@ impl EsploraClient { hash: &str, start_index: Option, ) -> Result, ExplorerError> { - let url = if let Some(index) = start_index { - self.join_url(format!("block/{hash}/txs/{index}"))? - } else { - self.join_url(format!("block/{hash}/txs"))? - }; + let url = self.url_builder.get_block_txs_url(hash, start_index)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + let resp = resp .json::>() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.into_iter().map(|val| val.convert()).collect::>()?; Ok(resp) } pub async fn get_block_txids(&self, hash: &str) -> Result, ExplorerError> { - let url = self.join_url(format!("block/{hash}/txids"))?; + let url = self.url_builder.get_block_txids_url(hash)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + let resp = resp .json::>() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp .into_iter() @@ -908,126 +912,128 @@ impl EsploraClient { } pub async fn get_block_txid(&self, hash: &str, index: u32) -> Result { - let url = self.join_url(format!("block/{hash}/txid/{index}"))?; + let url = self.url_builder.get_block_txid_url(hash, index)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; - let resp = resp.text().await.map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e))?; Ok(Txid::from_str(&resp)?) } pub async fn get_block_raw(&self, hash: &str) -> Result, ExplorerError> { - let url = self.join_url(format!("block/{hash}/raw"))?; + let url = self.url_builder.get_block_raw_url(hash)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + resp.bytes() .await .map(|b| b.to_vec()) - .map_err(|e| ExplorerError::response_failed(&e)) + .map_err(|e| ExplorerError::deserialize_reqwest(&e)) } pub async fn get_block_height(&self, height: u64) -> Result { - let url = self.join_url(format!("block-height/{height}"))?; + let url = self.url_builder.get_block_height_url(height)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; - let resp = resp.text().await.map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = BlockHash::from_str(&resp)?; Ok(resp) } pub async fn get_blocks(&self, start_height: Option) -> Result, ExplorerError> { - let url = if let Some(height) = start_height { - self.join_url(format!("blocks/{}", height))? - } else { - self.join_url("blocks")? - }; + let url = self.url_builder.get_blocks_url(start_height)?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::>() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.into_iter().map(|val| val.convert()).collect::>()?; Ok(resp) } pub async fn get_blocks_tip_height(&self) -> Result { - let url = self.join_url("blocks/tip/height")?; + let url = self.url_builder.get_blocks_tip_height_url()?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; Ok(resp) } pub async fn get_blocks_tip_hash(&self) -> Result { - let url = self.join_url("blocks/tip/hash")?; + let url = self.url_builder.get_blocks_tip_hash_url()?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; - let resp = resp.text().await.map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = BlockHash::from_str(&resp)?; Ok(resp) } pub async fn get_mempool(&self) -> Result { - let url = self.join_url("mempool")?; + let url = self.url_builder.get_mempool_url()?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; - resp.json().await.map_err(|e| ExplorerError::response_failed(&e)) + resp.json().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } pub async fn get_mempool_txids(&self) -> Result, ExplorerError> { - let url = self.join_url("mempool/txids")?; + let url = self.url_builder.get_mempool_txids_url()?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::>() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp .into_iter() .map(|val| Txid::from_str(&val)) @@ -1036,35 +1042,694 @@ impl EsploraClient { } pub async fn get_mempool_recent(&self) -> Result, ExplorerError> { - let url = self.join_url("mempool/recent")?; + let url = self.url_builder.get_mempool_recent_url()?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; let resp = resp .json::>() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; Ok(resp) } pub async fn get_fee_estimates(&self) -> Result { - let url = self.join_url("fee-estimates")?; + let url = self.url_builder.get_fee_estimates_url()?; let resp = self .client .get(url) .send() .await - .map_err(|e| ExplorerError::response_failed(&e))?; + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; Self::filter_resp(&resp)?; resp.json::() .await - .map_err(|e| ExplorerError::response_failed(&e)) + .map_err(|e| ExplorerError::deserialize_reqwest(&e)) + } +} + +impl EsploraClientSync { + #[inline] + fn filter_resp(resp: &minreq::Response) -> Result<(), ExplorerError> { + if is_resp_ok(resp.status_code) { + return Err(ExplorerError::erroneous_response_minreq(resp)); + } + Ok(()) + } + + pub fn get_tx(&self, txid: &str) -> Result { + let url: String = self.url_builder.get_tx_url(txid)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .map_err(|e| ExplorerError::deserialize_minreq(e))?; + let resp = resp.convert()?; + + Ok(resp) + } + + pub fn get_tx_status(&self, txid: &str) -> Result { + let url: String = self.url_builder.get_tx_status_url(txid)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .map_err(|e| ExplorerError::deserialize_minreq(e))?; + let resp = resp.convert()?; + Ok(resp) + } + + pub fn get_tx_hex(&self, txid: &str) -> Result { + let url: String = self.url_builder.get_tx_hex_url(txid)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + Ok(resp + .as_str() + .map_err(|e| ExplorerError::deserialize_minreq(e))? + .to_string()) + } + + pub fn get_tx_raw(&self, txid: &str) -> Result, ExplorerError> { + let url: String = self.url_builder.get_tx_raw_url(txid)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + Ok(resp.as_bytes().to_vec()) + } + + pub fn get_tx_elements(&self, txid: &str) -> Result { + let bytes = self.get_tx_raw(txid)?; + simplicityhl::elements::Transaction::deserialize(&bytes) + .map_err(|e| ExplorerError::TransactionDecode(e.to_string())) + } + + pub fn get_tx_merkle_proof(&self, txid: &str) -> Result { + let url: String = self.url_builder.get_tx_merkle_proof_url(txid)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.convert()?; + Ok(resp) + } + + pub fn get_tx_outspend(&self, txid: &str, vout: u32) -> Result { + let url: String = self.url_builder.get_tx_outspend_url(txid, vout)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.convert()?; + Ok(resp) + } + + pub fn get_tx_outspends(&self, txid: &str) -> Result, ExplorerError> { + let url: String = self.url_builder.get_tx_outspends_url(txid)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + resp.into_iter().map(|x| x.convert()).collect::, _>>() + } + + pub fn broadcast_tx(&self, tx: &simplicityhl::elements::Transaction) -> Result { + let tx_hex = simplicityhl::elements::encode::serialize_hex(tx); + let url: String = self.url_builder.get_broadcast_tx_url()?; + let resp = minreq::post(url) + .with_json(&tx_hex) + .map_err(|e| ExplorerError::deserialize_minreq(e))? + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .as_str() + .map_err(|e| ExplorerError::response_failed_minreq(e))? + .to_string(); + Ok(Txid::from_str(&resp)?) + } + + // TODO: add batch execution with 10 elements + pub fn broadcast_tx_package( + &self, + txs: &[simplicityhl::elements::Transaction], + ) -> Result { + let url: String = self.url_builder.get_broadcast_tx_package_url()?; + let tx_hexes = txs + .iter() + .map(simplicityhl::elements::encode::serialize_hex) + .collect::>(); + + let resp = minreq::post(url) + .with_json(&tx_hexes) + .map_err(|e| ExplorerError::deserialize_minreq(e))? + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + resp.json().map_err(|e| ExplorerError::response_failed_minreq(e)) + } + + pub fn get_address(&self, address: &str) -> Result { + let url: String = self.url_builder.get_address_url(address)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.convert()?; + + Ok(resp) + } + + pub fn get_address_txs(&self, address: &str) -> Result, ExplorerError> { + let url: String = self.url_builder.get_address_txs_url(address)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + let resp = resp + .json::>() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + Ok(resp) + } + + pub fn get_address_txs_chain( + &self, + address: &str, + last_seen_txid: Option<&str>, + ) -> Result, ExplorerError> { + let url: String = self.url_builder.get_address_txs_chain_url(address, last_seen_txid)?; + + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + Ok(resp) + } + + pub fn get_address_txs_mempool(&self, address: &str) -> Result, ExplorerError> { + let url: String = self.url_builder.get_address_txs_mempool_url(address)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + Ok(resp) + } + + pub fn get_address_utxo(&self, address: &str) -> Result, ExplorerError> { + let url: String = self.url_builder.get_address_utxo_url(address)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + resp.into_iter().map(|x| x.convert()).collect::, _>>() + } + + pub fn get_scripthash(&self, hash: &str) -> Result { + let url: String = self.url_builder.get_scripthash_url(hash)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.convert()?; + Ok(resp) + } + + // TODO: check output + pub fn get_scripthash_txs(&self, hash: &str) -> Result { + let url: String = self.url_builder.get_scripthash_txs_url(hash)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + Ok(resp + .as_str() + .map_err(|e| ExplorerError::response_failed_minreq(e))? + .to_string()) + } + + // TODO: check output + pub fn get_scripthash_txs_chain(&self, hash: &str, last_seen_txid: Option<&str>) -> Result { + let url: String = self.url_builder.get_scripthash_txs_chain_url(hash, last_seen_txid)?; + + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + Ok(resp + .as_str() + .map_err(|e| ExplorerError::response_failed_minreq(e))? + .to_string()) + } + + // TODO: check output + pub fn get_scripthash_txs_mempool(&self, hash: &str) -> Result { + let url: String = self.url_builder.get_scripthash_txs_mempool_url(hash)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + Ok(resp + .as_str() + .map_err(|e| ExplorerError::response_failed_minreq(e))? + .to_string()) } + + // TODO: check output + pub fn get_scripthash_utxo(&self, hash: &str) -> Result { + let url: String = self.url_builder.get_scripthash_utxo_url(hash)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + Ok(resp + .as_str() + .map_err(|e| ExplorerError::response_failed_minreq(e))? + .to_string()) + } + + pub fn get_block(&self, hash: &str) -> Result { + let url: String = self.url_builder.get_block_url(hash)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.convert()?; + Ok(resp) + } + + // TODO: decode hex into elements::BlockHeader (no method to do this) + pub fn get_block_header(&self, hash: &str) -> Result { + let url: String = self.url_builder.get_block_header_url(hash)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .as_str() + .map_err(|e| ExplorerError::response_failed_minreq(e))? + .to_string(); + Ok(resp) + } + + pub fn get_block_status(&self, hash: &str) -> Result { + let url: String = self.url_builder.get_block_status_url(hash)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + resp.json::() + .map_err(|e| ExplorerError::response_failed_minreq(e)) + } + + pub fn get_block_txs( + &self, + hash: &str, + start_index: Option, + ) -> Result, ExplorerError> { + let url: String = self.url_builder.get_block_txs_url(hash, start_index)?; + + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.into_iter().map(|val| val.convert()).collect::>()?; + Ok(resp) + } + + pub fn get_block_txids(&self, hash: &str) -> Result, ExplorerError> { + let url: String = self.url_builder.get_block_txids_url(hash)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + + let resp = resp + .into_iter() + .map(|val| Txid::from_str(&val)) + .collect::>()?; + Ok(resp) + } + + pub fn get_block_txid(&self, hash: &str, index: u32) -> Result { + let url: String = self.url_builder.get_block_txid_url(hash, index)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp.as_str().map_err(|e| ExplorerError::response_failed_minreq(e))?; + + Ok(Txid::from_str(&resp)?) + } + + pub fn get_block_raw(&self, hash: &str) -> Result, ExplorerError> { + let url: String = self.url_builder.get_block_raw_url(hash)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + Ok(resp.as_bytes().to_vec()) + } + + pub fn get_block_height(&self, height: u64) -> Result { + let url: String = self.url_builder.get_block_height_url(height)?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp.as_str().map_err(|e| ExplorerError::response_failed_minreq(e))?; + + let resp = BlockHash::from_str(&resp)?; + Ok(resp) + } + + pub fn get_blocks(&self, start_height: Option) -> Result, ExplorerError> { + let url = self.url_builder.get_blocks_url(start_height)?; + + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.into_iter().map(|val| val.convert()).collect::>()?; + Ok(resp) + } + + pub fn get_blocks_tip_height(&self) -> Result { + let url: String = self.url_builder.get_blocks_tip_height_url()?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Ok(resp) + } + + pub fn get_blocks_tip_hash(&self) -> Result { + let url: String = self.url_builder.get_blocks_tip_hash_url()?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp.as_str().map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = BlockHash::from_str(&resp)?; + Ok(resp) + } + + pub fn get_mempool(&self) -> Result { + let url: String = self.url_builder.get_mempool_url()?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + resp.json().map_err(|e| ExplorerError::response_failed_minreq(e)) + } + + pub fn get_mempool_txids(&self) -> Result, ExplorerError> { + let url: String = self.url_builder.get_mempool_txids_url()?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp + .into_iter() + .map(|val| Txid::from_str(&val)) + .collect::>()?; + Ok(resp) + } + + pub fn get_mempool_recent(&self) -> Result, ExplorerError> { + let url: String = self.url_builder.get_mempool_recent_url()?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + Ok(resp) + } + + pub fn get_fee_estimates(&self) -> Result { + let url: String = self.url_builder.get_fee_estimates_url()?; + let resp = minreq::get(url) + .send() + .map_err(|e| ExplorerError::response_failed_minreq(e))?; + Self::filter_resp(&resp)?; + + resp.json::() + .map_err(|e| ExplorerError::response_failed_minreq(e)) + } +} + +struct UrlBuilder { + base_url: String, +} + +impl UrlBuilder { + fn get_tx_url(&self, txid: &str) -> Result { + self.join_url(format!("/tx/{txid}")) + } + fn get_tx_status_url(&self, txid: &str) -> Result { + self.join_url(format!("tx/{txid}/status")) + } + fn get_tx_hex_url(&self, txid: &str) -> Result { + self.join_url(format!("tx/{txid}/hex")) + } + fn get_tx_raw_url(&self, txid: &str) -> Result { + self.join_url(format!("tx/{txid}/raw")) + } + fn get_tx_merkle_proof_url(&self, txid: &str) -> Result { + self.join_url(format!("tx/{txid}/merkle-proof")) + } + fn get_tx_outspend_url(&self, txid: &str, vout: u32) -> Result { + self.join_url(format!("tx/{txid}/outspend/{vout}")) + } + fn get_tx_outspends_url(&self, txid: &str) -> Result { + self.join_url(format!("tx/{txid}/outspends")) + } + fn get_broadcast_tx_url(&self) -> Result { + self.join_url("tx") + } + fn get_broadcast_tx_package_url(&self) -> Result { + self.join_url("txs/package") + } + fn get_address_url(&self, address: &str) -> Result { + self.join_url(format!("address/{address}")) + } + fn get_address_txs_url(&self, address: &str) -> Result { + self.join_url(format!("address/{address}/txs")) + } + fn get_address_txs_chain_url(&self, address: &str, last_seen_txid: Option<&str>) -> Result { + if let Some(txid) = last_seen_txid { + self.join_url(format!("address/{address}/txs/chain/{txid}")) + } else { + self.join_url(format!("address/{address}/txs/chain")) + } + } + fn get_address_txs_mempool_url(&self, address: &str) -> Result { + self.join_url(format!("address/{address}/txs/mempool")) + } + fn get_address_utxo_url(&self, address: &str) -> Result { + self.join_url(format!("address/{address}/utxo")) + } + fn get_scripthash_url(&self, hash: &str) -> Result { + self.join_url(format!("scripthash/{hash}")) + } + fn get_scripthash_txs_url(&self, hash: &str) -> Result { + self.join_url(format!("scripthash/{hash}/txs")) + } + fn get_scripthash_txs_chain_url(&self, hash: &str, last_seen_txid: Option<&str>) -> Result { + if let Some(txid) = last_seen_txid { + self.join_url(format!("scripthash/{hash}/txs/chain/{txid}")) + } else { + self.join_url(format!("scripthash/{hash}/txs/chain")) + } + } + fn get_scripthash_txs_mempool_url(&self, hash: &str) -> Result { + self.join_url(format!("scripthash/{hash}/txs/mempool")) + } + fn get_scripthash_utxo_url(&self, hash: &str) -> Result { + self.join_url(format!("scripthash/{hash}/utxo")) + } + fn get_block_url(&self, hash: &str) -> Result { + self.join_url(format!("block/{hash}")) + } + fn get_block_header_url(&self, hash: &str) -> Result { + self.join_url(format!("block/{hash}/header")) + } + fn get_block_status_url(&self, hash: &str) -> Result { + self.join_url(format!("block/{hash}/status")) + } + fn get_block_txs_url(&self, hash: &str, start_index: Option) -> Result { + if let Some(index) = start_index { + self.join_url(format!("block/{hash}/txs/{index}")) + } else { + self.join_url(format!("block/{hash}/txs")) + } + } + fn get_block_txids_url(&self, hash: &str) -> Result { + self.join_url(format!("block/{hash}/txids")) + } + fn get_block_txid_url(&self, hash: &str, index: u32) -> Result { + self.join_url(format!("block/{hash}/txid/{index}")) + } + fn get_block_raw_url(&self, hash: &str) -> Result { + self.join_url(format!("block/{hash}/raw")) + } + fn get_block_height_url(&self, height: u64) -> Result { + self.join_url(format!("block-height/{height}")) + } + fn get_blocks_url(&self, start_height: Option) -> Result { + if let Some(height) = start_height { + self.join_url(format!("blocks/{height}")) + } else { + self.join_url("blocks") + } + } + fn get_blocks_tip_height_url(&self) -> Result { + self.join_url("blocks/tip/height") + } + fn get_blocks_tip_hash_url(&self) -> Result { + self.join_url("blocks/tip/hash") + } + fn get_mempool_url(&self) -> Result { + self.join_url("mempool") + } + fn get_mempool_txids_url(&self) -> Result { + self.join_url("mempool/txids") + } + fn get_mempool_recent_url(&self) -> Result { + self.join_url("mempool/recent") + } + fn get_fee_estimates_url(&self) -> Result { + self.join_url("fee-estimates") + } +} + +trait BaseUrlGetter { + fn get_base_url(&self) -> &str; +} + +trait UrlAppender { + #[inline] + fn join_url(&self, str: impl AsRef) -> Result; +} + +impl UrlAppender for T { + #[inline] + fn join_url(&self, str: impl AsRef) -> Result { + Ok(format!("{}/{}", self.get_base_url(), str.as_ref())) + } +} + +impl BaseUrlGetter for UrlBuilder { + fn get_base_url(&self) -> &str { + self.base_url.as_str() + } +} + +impl BaseUrlGetter for EsploraClientAsync { + fn get_base_url(&self) -> &str { + self.url_builder.get_base_url() + } +} + +impl BaseUrlGetter for EsploraClientSync { + fn get_base_url(&self) -> &str { + self.url_builder.get_base_url() + } +} + +fn is_resp_ok(code: i32) -> bool { + !(200..300).contains(&code) } diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 0db46a0..5e16a89 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -12,6 +12,7 @@ workspace = true [dependencies] simplex-provider = { workspace = true } +async-trait = { workspace = true } thiserror = { workspace = true } sha2 = { workspace = true } minreq = { workspace = true } diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs index 5ef80ad..8d89469 100644 --- a/crates/sdk/src/error.rs +++ b/crates/sdk/src/error.rs @@ -1,4 +1,6 @@ +use simplex_provider::ExplorerError; use simplicityhl::elements::secp256k1_zkp; +use simplicityhl::simplicity::hex::HexToArrayError; #[derive(Debug, thiserror::Error)] pub enum SimplexError { @@ -52,4 +54,13 @@ pub enum SimplexError { #[error("Invalid txid format: {0}")] InvalidTxid(String), + + #[error("Failed to execute rpc query, err: '{0}'")] + RpcExecution(String), + + #[error("Hex to array error: '{0}'")] + HexToArray(#[from] HexToArrayError), + + #[error("Failed to execute provider method: '{method}', err: '{err}'")] + ProviderError { method: String, err: ExplorerError }, } diff --git a/crates/sdk/src/provider/esplora.rs b/crates/sdk/src/provider/esplora.rs index fff3c91..3ce3cdf 100644 --- a/crates/sdk/src/provider/esplora.rs +++ b/crates/sdk/src/provider/esplora.rs @@ -1,20 +1,56 @@ use crate::error::SimplexError; -use crate::provider::Provider; +use crate::provider::{ProviderAsync, ProviderSync}; +pub use simplex_provider::esplora::*; +use simplicityhl::elements::hex::ToHex; use simplicityhl::elements::{Transaction, Txid}; use std::collections::HashMap; -pub use simplex_provider::esplora::*; - -impl Provider for EsploraClient { - fn broadcast_transaction(&self, tx: &Transaction) -> Result { - todo!() +impl ProviderSync for EsploraClientSync { + fn broadcast_transaction(&self, tx: &Transaction) -> Result { + self.broadcast_tx(tx).map_err(|e| SimplexError::ProviderError { + method: "broadcast_tx".to_string(), + err: e, + }) } fn fetch_fee_estimates(&self) -> Result, SimplexError> { - todo!() + self.get_fee_estimates().map_err(|e| SimplexError::ProviderError { + method: "get_fee_estimates".to_string(), + err: e, + }) + } + + fn fetch_transaction(&self, txid: &Txid) -> Result { + self.get_tx_elements(&txid.to_hex()) + .map_err(|e| SimplexError::ProviderError { + method: "get_tx_elements".to_string(), + err: e, + }) + } +} + +#[async_trait::async_trait] +impl ProviderAsync for EsploraClientAsync { + async fn broadcast_transaction(&self, tx: &Transaction) -> Result { + self.broadcast_tx(tx).await.map_err(|e| SimplexError::ProviderError { + method: "broadcast_tx".to_string(), + err: e, + }) + } + + async fn fetch_fee_estimates(&self) -> Result, SimplexError> { + self.get_fee_estimates().await.map_err(|e| SimplexError::ProviderError { + method: "get_fee_estimates".to_string(), + err: e, + }) } - fn fetch_transaction(&self, txid: Txid) -> Result { - todo!() + async fn fetch_transaction(&self, txid: &Txid) -> Result { + self.get_tx_elements(&txid.to_hex()) + .await + .map_err(|e| SimplexError::ProviderError { + method: "get_tx_elements".to_string(), + err: e, + }) } } diff --git a/crates/sdk/src/provider/mod.rs b/crates/sdk/src/provider/mod.rs index 3d6a3a0..5556328 100644 --- a/crates/sdk/src/provider/mod.rs +++ b/crates/sdk/src/provider/mod.rs @@ -1,20 +1,22 @@ mod esplora; -use std::collections::HashMap; - use simplicityhl::elements::encode; use simplicityhl::elements::hex::ToHex; use simplicityhl::elements::{Transaction, Txid}; +use std::collections::HashMap; +use std::str::FromStr; use crate::constants::DEFAULT_FEE_RATE; use crate::error::SimplexError; -pub trait Provider { - fn broadcast_transaction(&self, tx: &Transaction) -> Result; +pub use simplex_provider::esplora::*; + +pub trait ProviderSync { + fn broadcast_transaction(&self, tx: &Transaction) -> Result; fn fetch_fee_estimates(&self) -> Result, SimplexError>; - fn fetch_transaction(&self, txid: Txid) -> Result; + fn fetch_transaction(&self, txid: &Txid) -> Result; fn get_fee_rate(&self, target_blocks: u32) -> Result { if target_blocks == 0 { @@ -53,6 +55,51 @@ pub trait Provider { } } +#[async_trait::async_trait] +pub trait ProviderAsync { + async fn broadcast_transaction(&self, tx: &Transaction) -> Result; + + async fn fetch_fee_estimates(&self) -> Result, SimplexError>; + + async fn fetch_transaction(&self, txid: &Txid) -> Result; + + async fn get_fee_rate(&self, target_blocks: u32) -> Result { + if target_blocks == 0 { + return Ok(DEFAULT_FEE_RATE); + } + + let estimates = self.fetch_fee_estimates().await?; + + let target_str = target_blocks.to_string(); + + if let Some(&rate) = estimates.get(&target_str) { + return Ok((rate * 1000.0) as f32); // Convert sat/vB to sats/kvb + } + + let fallback_targets = [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 144, 504, 1008, + ]; + + for &target in fallback_targets.iter().filter(|&&t| t >= target_blocks) { + let key = target.to_string(); + + if let Some(&rate) = estimates.get(&key) { + return Ok((rate * 1000.0) as f32); + } + } + + for &target in &fallback_targets { + let key = target.to_string(); + + if let Some(&rate) = estimates.get(&key) { + return Ok((rate * 1000.0) as f32); + } + } + + Err(SimplexError::Request("No fee estimates available".to_string())) + } +} + pub struct EsploraProvider { esplora_url: String, } @@ -63,8 +110,8 @@ impl EsploraProvider { } } -impl Provider for EsploraProvider { - fn broadcast_transaction(&self, tx: &Transaction) -> Result { +impl ProviderSync for EsploraProvider { + fn broadcast_transaction(&self, tx: &Transaction) -> Result { let tx_hex = encode::serialize_hex(tx); let url = format!("{}/tx", self.esplora_url); @@ -84,12 +131,11 @@ impl Provider for EsploraProvider { message: body, }); } - - Ok(body) + Ok(Txid::from_str(&body)?) } fn fetch_fee_estimates(&self) -> Result, SimplexError> { - let url = self.esplora_url.clone() + "/fee-estimates"; + let url = format!("{}/fee-estimates", self.esplora_url.clone()); let response = minreq::get(&url) .send() .map_err(|e| SimplexError::Request(e.to_string()))?; @@ -106,8 +152,8 @@ impl Provider for EsploraProvider { Ok(estimates) } - fn fetch_transaction(&self, txid: Txid) -> Result { - let url = self.esplora_url.clone() + "/tx/" + txid.to_hex().as_str() + "/raw"; + fn fetch_transaction(&self, txid: &Txid) -> Result { + let url = format!("{}/tx/{}/raw", self.esplora_url.clone(), txid.to_hex().as_str()); let response = minreq::get(&url) .send() .map_err(|e| SimplexError::Request(e.to_string()))?; diff --git a/crates/sdk/src/witness_transaction.rs b/crates/sdk/src/witness_transaction.rs index c69300e..9b5addb 100644 --- a/crates/sdk/src/witness_transaction.rs +++ b/crates/sdk/src/witness_transaction.rs @@ -6,7 +6,7 @@ use simplicityhl::elements::{Script, Transaction, TxOut}; use crate::constants::{MIN_FEE, PLACEHOLDER_FEE, SimplicityNetwork, WITNESS_SCALE_FACTOR}; use crate::error::SimplexError; use crate::program::ProgramTrait; -use crate::provider::Provider; +use crate::provider::ProviderSync; use crate::signer::SignerTrait; use crate::witness::WitnessTrait; @@ -67,7 +67,7 @@ where &self, target_blocks: u32, change_recipient_script: Script, - provider: impl Provider, + provider: impl ProviderSync, ) -> Result<(Transaction, u64), SimplexError> { let policy_amount_delta = self.calculate_fee_delta(); diff --git a/crates/simplex/Cargo.toml b/crates/simplex/Cargo.toml index 8062896..df70a71 100644 --- a/crates/simplex/Cargo.toml +++ b/crates/simplex/Cargo.toml @@ -16,7 +16,7 @@ workspace = true [features] default = ["macros", "encoding", "sdk"] -macros = ["dep:simplex-macros"] +macros = ["dep:simplex-macros", "tracing"] encoding = ["dep:bincode"] sdk = ["dep:simplex-sdk"] @@ -30,8 +30,9 @@ bincode = { workspace = true, optional = true } simplicityhl = { workspace = true, features = ["serde"] } serde = { version = "1.0.228" } either = { version = "1.15.0", features = ["serde"] } -tokio = { version = "1.49.0", features = ["full"]} +tracing = { workspace = true, optional = true } [dev-dependencies] trybuild = { version = "1.0.115" } anyhow = { version = "1.0.101" } +simplicityhl-core = "0.4.2" diff --git a/crates/simplex/src/lib.rs b/crates/simplex/src/lib.rs index 9bd9fb4..9875284 100644 --- a/crates/simplex/src/lib.rs +++ b/crates/simplex/src/lib.rs @@ -9,3 +9,6 @@ pub extern crate simplex_sdk; #[cfg(feature = "macros")] pub extern crate simplex_test; + +#[cfg(feature = "macros")] +pub extern crate tracing; diff --git a/crates/simplex/tests/simplex_test.rs b/crates/simplex/tests/simplex_test.rs index 1e42356..680bb60 100644 --- a/crates/simplex/tests/simplex_test.rs +++ b/crates/simplex/tests/simplex_test.rs @@ -1,102 +1,108 @@ -// use simplex_provider::elements_rpc::{AddressType, ElementsRpcClient}; -// use simplex_sdk::constants::SimplicityNetwork; -// use simplex_test::DEFAULT_SAT_AMOUNT_FAUCET; -// use simplicityhl::elements::Address; -// use simplicityhl::elements::bitcoin::secp256k1; -// use simplicityhl::elements::secp256k1_zkp::Keypair; - -// #[simplex::simplex_macros::test] -// // #[test] -// fn test_execution() { -// assert!(true); -// } - -// #[test] -// fn test_invocation_tx_tracking() -> anyhow::Result<()> { -// use simplex_test::{ConfigOption, TestProvider}; - -// fn test_invocation_tx_tracking(rpc: TestProvider, user1_addr: Address, user2_addr: Address) -> anyhow::Result<()> { -// // user input code -// { -// let network = SimplicityNetwork::default_regtest(); -// let keypair = Keypair::from_seckey_slice(&secp256k1::SECP256K1, &[1; 32])?; -// let p2pk = simplex_core::get_p2pk_address(&keypair.x_only_public_key().0, network)?; - -// dbg!(p2pk.to_string()); - -// // simplex runtime -// // - test provider -// // - fields from config -// // - -// // p2tr - -// // TODO: uncomment and fix -// dbg!(ElementsRpcClient::validateaddress(rpc.as_ref(), &p2pk.to_string())?); -// // ElementsRpcClient::importaddress(rpc.as_ref(), &p2pk.to_string(), None, None, None)?; - -// // broadcast, fetch fee transaction - -// let result = ElementsRpcClient::sendtoaddress( -// rpc.as_ref(), -// &p2pk, -// DEFAULT_SAT_AMOUNT_FAUCET, -// Some(rpc.network().policy_asset()), -// )?; - -// ElementsRpcClient::generate_blocks(rpc.as_ref(), 5)?; - -// dbg!(ElementsRpcClient::listunspent( -// rpc.as_ref(), -// None, -// None, -// Some(vec![p2pk.to_string()]), -// None, -// None, -// )?,); - -// dbg!(ElementsRpcClient::scantxoutset( -// rpc.as_ref(), -// "start", -// Some(vec![format!("addr({})", p2pk)]), -// )?,); - -// Ok(()) -// } -// } -// let rpc = TestProvider::init(ConfigOption::DefaultRegtest).unwrap(); -// { -// ElementsRpcClient::generate_blocks(rpc.as_ref(), 1).unwrap(); -// ElementsRpcClient::rescanblockchain(rpc.as_ref(), None, None).unwrap(); -// ElementsRpcClient::sweep_initialfreecoins(rpc.as_ref()).unwrap(); -// ElementsRpcClient::generate_blocks(rpc.as_ref(), 100).unwrap(); -// } - -// let user1_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); -// let user2_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); -// ElementsRpcClient::sendtoaddress( -// rpc.as_ref(), -// &user1_addr, -// DEFAULT_SAT_AMOUNT_FAUCET, -// Some(rpc.network().policy_asset()), -// ) -// .unwrap(); - -// ElementsRpcClient::sendtoaddress( -// rpc.as_ref(), -// &user2_addr, -// DEFAULT_SAT_AMOUNT_FAUCET, -// Some(rpc.network().policy_asset()), -// ) -// .unwrap(); - -// ElementsRpcClient::generate_blocks(rpc.as_ref(), 3).unwrap(); -// dbg!(ElementsRpcClient::listunspent( -// rpc.as_ref(), -// None, -// None, -// Some(vec![user1_addr.to_string(), user2_addr.to_string()]), -// None, -// None, -// )?,); -// test_invocation_tx_tracking(rpc, user1_addr, user2_addr) -// } +use simplex_provider::elements_rpc::{AddressType, ElementsRpcClient}; +use simplex_sdk::constants::SimplicityNetwork; +use simplex_test::DEFAULT_SAT_AMOUNT_FAUCET; +use simplicityhl::elements::Address; +use simplicityhl::elements::bitcoin::secp256k1; +use simplicityhl::elements::secp256k1_zkp::Keypair; + +#[simplex::simplex_macros::test] +fn test_execution() {} + +#[test] +fn test_invocation_tx_tracking() -> anyhow::Result<()> { + use simplex_test::{ConfigOption, TestClientProvider}; + + fn test_invocation_tx_tracking( + rpc: TestClientProvider, + user1_addr: Address, + user2_addr: Address, + ) -> anyhow::Result<()> { + // user input code + { + let network = SimplicityNetwork::default_regtest(); + let keypair = Keypair::from_seckey_slice(&secp256k1::SECP256K1, &[1; 32])?; + let p2pk = simplicityhl_core::get_p2pk_address( + &keypair.x_only_public_key().0, + simplicityhl_core::SimplicityNetwork::default_regtest(), + )?; + + dbg!(p2pk.to_string()); + + // simplex runtime + // - test provider + // - fields from config + // - + // p2tr + + // TODO: uncomment and fix + dbg!(ElementsRpcClient::validateaddress(rpc.as_ref(), &p2pk.to_string())?); + // ElementsRpcClient::importaddress(rpc.as_ref(), &p2pk.to_string(), None, None, None)?; + + // broadcast, fetch fee transaction + + let result = ElementsRpcClient::sendtoaddress( + rpc.as_ref(), + &p2pk, + DEFAULT_SAT_AMOUNT_FAUCET, + Some(network.policy_asset()), + )?; + + ElementsRpcClient::generate_blocks(rpc.as_ref(), 5)?; + + dbg!(ElementsRpcClient::listunspent( + rpc.as_ref(), + None, + None, + Some(vec![p2pk.to_string()]), + None, + None, + )?,); + + dbg!(ElementsRpcClient::scantxoutset( + rpc.as_ref(), + "start", + Some(vec![format!("addr({})", p2pk)]), + )?,); + + Ok(()) + } + } + + let network = SimplicityNetwork::default_regtest(); + let rpc = TestClientProvider::init(ConfigOption::DefaultRegtest).unwrap(); + { + ElementsRpcClient::generate_blocks(rpc.as_ref(), 1).unwrap(); + ElementsRpcClient::rescanblockchain(rpc.as_ref(), None, None).unwrap(); + ElementsRpcClient::sweep_initialfreecoins(rpc.as_ref()).unwrap(); + ElementsRpcClient::generate_blocks(rpc.as_ref(), 100).unwrap(); + } + + let user1_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); + let user2_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); + ElementsRpcClient::sendtoaddress( + rpc.as_ref(), + &user1_addr, + DEFAULT_SAT_AMOUNT_FAUCET, + Some(network.policy_asset()), + ) + .unwrap(); + + ElementsRpcClient::sendtoaddress( + rpc.as_ref(), + &user2_addr, + DEFAULT_SAT_AMOUNT_FAUCET, + Some(network.policy_asset()), + ) + .unwrap(); + + ElementsRpcClient::generate_blocks(rpc.as_ref(), 3).unwrap(); + dbg!(ElementsRpcClient::listunspent( + rpc.as_ref(), + None, + None, + Some(vec![user1_addr.to_string(), user2_addr.to_string()]), + None, + None, + )?,); + test_invocation_tx_tracking(rpc, user1_addr, user2_addr) +} diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index a8c5bab..2e3c39b 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -15,3 +15,4 @@ simplex-sdk = { workspace = true } thiserror = { workspace = true } simplicityhl = { workspace = true } electrsd = { workspace = true } +serde = { workspace = true } diff --git a/crates/test/src/error.rs b/crates/test/src/error.rs index 17c83c9..8ab293c 100644 --- a/crates/test/src/error.rs +++ b/crates/test/src/error.rs @@ -1,3 +1,4 @@ +use electrsd::electrum_client::bitcoin::hex::HexToArrayError; use simplex_provider::ExplorerError; #[derive(thiserror::Error, Debug)] @@ -10,4 +11,12 @@ pub enum TestError { #[error("Node failed to start, error: {0}")] NodeFailedToStart(String), + + /// Errors when converting hex strings to byte arrays. + #[error("Hex to array error: '{0}'")] + HexToArray(#[from] HexToArrayError), + + /// Errors when failed to decode transaction. + #[error("Failed to decode transaction: '{0}'")] + TransactionDecode(String), } diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 8840c58..7346446 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -4,6 +4,7 @@ mod testing; pub use common::*; pub use error::*; +pub use testing::*; use bitcoind::bitcoincore_rpc::{Auth, Client}; use bitcoind::{BitcoinD, Conf}; @@ -19,7 +20,7 @@ pub struct User { pubkey: PublicKey, } -pub enum TestProvider { +pub enum TestClientProvider { ConfiguredNode { node: BitcoinD, network: SimplicityNetwork }, CustomRpc(ElementsRpcClient), } @@ -30,7 +31,7 @@ pub enum ConfigOption<'a> { CustomRpcUrlRegtest { url: String, auth: Auth }, } -impl TestProvider { +impl TestClientProvider { pub fn init(init_option: ConfigOption) -> Result { let rpc = match init_option { ConfigOption::DefaultRegtest => { @@ -92,13 +93,13 @@ impl TestProvider { pub fn client(&self) -> &Client { match self { - TestProvider::ConfiguredNode { node, .. } => &node.client, - TestProvider::CustomRpc(x) => x.client(), + TestClientProvider::ConfiguredNode { node, .. } => &node.client, + TestClientProvider::CustomRpc(x) => x.client(), } } } -impl TestProvider { +impl TestClientProvider { pub fn fund(satoshi: u64, address: Option
, asset: Option) { todo!() } @@ -110,7 +111,7 @@ impl TestProvider { } } -impl AsRef for TestProvider { +impl AsRef for TestClientProvider { fn as_ref(&self) -> &Client { self.client() } diff --git a/crates/test/src/testing/config.rs b/crates/test/src/testing/config.rs index 153e59c..aa20349 100644 --- a/crates/test/src/testing/config.rs +++ b/crates/test/src/testing/config.rs @@ -1 +1,18 @@ -pub struct TestingConfig {} +use electrsd::bitcoind::bitcoincore_rpc::jsonrpc::serde::{Deserialize, Serialize}; + +pub const TEST_ENV_NAME: &str = "SIMPLEX_TEST_ENV"; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct TestConfig { + pub rpc_creds: RpcCreds, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub enum RpcCreds { + Auth { + rpc_username: String, + rpc_password: String, + }, + #[default] + None, +} diff --git a/crates/test/src/testing/rpc_provider.rs b/crates/test/src/testing/rpc_provider.rs index b589060..26a84a2 100644 --- a/crates/test/src/testing/rpc_provider.rs +++ b/crates/test/src/testing/rpc_provider.rs @@ -1,20 +1,196 @@ +use crate::{ConfigOption, TestClientProvider, TestError}; +use bitcoind::bitcoincore_rpc::bitcoin; +use electrsd::bitcoind; +use electrsd::bitcoind::bitcoincore_rpc::jsonrpc::serde_json::Value; +pub use simplex_provider::elements_rpc::*; use simplex_sdk::error::SimplexError; -use simplex_sdk::provider::Provider; -use simplicityhl::elements::{Transaction, Txid}; +use simplex_sdk::provider::ProviderSync; +use simplicityhl::elements::Transaction; +use simplicityhl::elements::hex::ToHex; +use simplicityhl::elements::{Address, AssetId, BlockHash, Txid}; use std::collections::HashMap; +use std::str::FromStr; -pub struct TestRpcProvider {} +pub struct TestRpcProvider { + provider: TestClientProvider, +} -impl Provider for TestRpcProvider { - fn broadcast_transaction(&self, tx: &Transaction) -> Result { - todo!() +impl ProviderSync for TestRpcProvider { + fn broadcast_transaction(&self, tx: &Transaction) -> Result { + use simplicityhl::simplicity::elements::encode; + let tx_hex = encode::serialize_hex(tx); + self.sendrawtransaction(&tx_hex) + .map_err(|e| SimplexError::RpcExecution(e.to_string())) } fn fetch_fee_estimates(&self) -> Result, SimplexError> { - todo!() + // Todo: search for appropriate endpoint + let mut map = HashMap::new(); + map.insert("".to_string(), 0.1); + Ok(map) + } + + fn fetch_transaction(&self, txid: &Txid) -> Result { + self.gettransaction(&txid) + .map_err(|e| SimplexError::RpcExecution(e.to_string())) + } +} + +impl TestRpcProvider { + pub fn init(init_option: ConfigOption) -> Result { + Ok(Self { + provider: TestClientProvider::init(init_option)?, + }) + } + + pub fn gettransaction(&self, txid: &Txid) -> Result { + use simplicityhl::elements::encode; + + let client = self.provider.client(); + let res = ElementsRpcClient::getrawtransaction_hex(client, &txid.to_hex())?; + let tx: Transaction = + encode::deserialize(res.as_bytes()).map_err(|e| TestError::TransactionDecode(e.to_string()))?; + Ok(tx) + } + + pub fn height(&self) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::height(client)?) + } + + pub fn blockchain_info(&self) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::blockchain_info(client)?) + } + + pub fn sendtoaddress(&self, address: &Address, satoshi: u64, asset: Option) -> Result { + Ok(ElementsRpcClient::sendtoaddress( + self.provider.client(), + address, + satoshi, + asset, + )?) + } + pub fn rescanblockchain(&self, start: Option, stop: Option) -> Result<(), TestError> { + let client = self.provider.client(); + Ok(ElementsRpcClient::rescanblockchain(client, start, stop)?) + } + + pub fn getnewaddress(&self, label: &str, kind: AddressType) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::getnewaddress(client, label, kind)?) + } + + pub fn generate_blocks(&self, block_num: u32) -> Result<(), TestError> { + let client = self.provider.client(); + Ok(ElementsRpcClient::generate_blocks(client, block_num)?) + } + + pub fn sweep_initialfreecoins(&self) -> Result<(), TestError> { + let client = self.provider.client(); + Ok(ElementsRpcClient::sweep_initialfreecoins(client)?) + } + + pub fn issueasset(&self, satoshi: u64) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::issueasset(client, satoshi)?) + } + + pub fn genesis_block_hash(&self) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::genesis_block_hash(client)?) + } + + pub fn block_hash(&self, height: u64) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::block_hash(client, height)?) + } + + pub fn getpeginaddress(&self) -> Result<(bitcoin::Address, String), TestError> { + let client = self.provider.client(); + Ok(ElementsRpcClient::getpeginaddress(client)?) + } + + pub fn raw_createpsbt(&self, inputs: Value, outputs: Value) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::raw_createpsbt(client, inputs, outputs)?) + } + + pub fn expected_next(&self, base64: &str) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::expected_next(client, base64)?) + } + + pub fn walletprocesspsbt(&self, psbt: &str) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::walletprocesspsbt(client, psbt)?) + } + + pub fn finalizepsbt(&self, psbt: &str) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::finalizepsbt(client, psbt)?) + } + + pub fn sendrawtransaction(&self, tx: &str) -> Result { + let client = self.provider.client(); + let res = ElementsRpcClient::sendrawtransaction(client, tx)?; + Ok(Txid::from_str(&res.txid)?) + } + + pub fn testmempoolaccept(&self, tx: &str) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::testmempoolaccept(client, tx)?) + } + + pub fn create_wallet(&self, wallet_name: Option) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::create_wallet(client, wallet_name)?) + } + + pub fn getbalance(&self, conf: Option) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::getbalance(client, conf)?) + } + + pub fn listunspent( + &self, + min_conf: Option, + max_conf: Option, + addresses: Option>, + include_unsafe: Option, + query_options: Option, + ) -> Result, TestError> { + let client = self.provider.client(); + Ok(ElementsRpcClient::listunspent( + client, + min_conf, + max_conf, + addresses, + include_unsafe, + query_options, + )?) + } + pub fn importaddress( + &self, + address: &str, + label: Option<&str>, + rescan: Option, + p2sh: Option, + ) -> Result<(), TestError> { + let client = self.provider.client(); + Ok(ElementsRpcClient::importaddress(client, address, label, rescan, p2sh)?) + } + pub fn validateaddress(&self, address: &str) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::validateaddress(client, address)?) } - fn fetch_transaction(&self, txid: Txid) -> Result { - todo!() + pub fn scantxoutset( + &self, + action: &str, + scanobjects: Option>, + ) -> Result { + let client = self.provider.client(); + Ok(ElementsRpcClient::scantxoutset(client, action, scanobjects)?) } } diff --git a/crates/user/src/lib.rs b/crates/user/src/lib.rs new file mode 100644 index 0000000..7f63866 --- /dev/null +++ b/crates/user/src/lib.rs @@ -0,0 +1,28 @@ +#[cfg(test)] +mod tests { + use simplex_sdk::constants::DUMMY_SIGNATURE; + use simplex_sdk::presets::p2pk::p2pk_build::P2PKWitness; + use simplex_sdk::signer::Signer; + + #[test] + #[ignore] + fn main() { + let signer = Signer::from_seed( + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + .as_bytes() + .try_into() + .unwrap(), + ) + .unwrap(); + + let witness = P2PKWitness { + signature: DUMMY_SIGNATURE, + }; + + // let arguments = P2PKArguments { + // public_key: signer.public_key(), + // }; + + // let p2pk = P2PK::new(&tr_unspendable_key(), &arguments); + } +} diff --git a/crates/user/src/main.rs b/crates/user/src/main.rs deleted file mode 100644 index 65d8931..0000000 --- a/crates/user/src/main.rs +++ /dev/null @@ -1,27 +0,0 @@ -use simplex_sdk::presets::p2pk::P2PK; -use simplex_sdk::presets::p2pk::p2pk_build::{P2PKArguments, P2PKWitness}; - -use simplex_sdk::signer::{Signer, SignerTrait}; - -use simplex_sdk::constants::DUMMY_SIGNATURE; -use simplex_sdk::utils::tr_unspendable_key; - -fn main() { - let signer = Signer::from_seed( - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - .as_bytes() - .try_into() - .unwrap(), - ) - .unwrap(); - - let witness = P2PKWitness { - signature: DUMMY_SIGNATURE, - }; - - // let arguments = P2PKArguments { - // public_key: signer.public_key(), - // }; - - // let p2pk = P2PK::new(&tr_unspendable_key(), &arguments); -} From d1b31fcd2f94543fc155f70b9508564e39bfdac7 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Mon, 23 Feb 2026 17:55:15 +0200 Subject: [PATCH 05/14] Add draft test cases --- crates/cli/tests/hello/mod.rs | 1 + crates/cli/tests/hello/test333.rs | 63 +++++++++++++++++++++++++++++++ crates/cli/tests/test2.rs | 24 ++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 crates/cli/tests/hello/mod.rs create mode 100644 crates/cli/tests/hello/test333.rs create mode 100644 crates/cli/tests/test2.rs diff --git a/crates/cli/tests/hello/mod.rs b/crates/cli/tests/hello/mod.rs new file mode 100644 index 0000000..3a426b3 --- /dev/null +++ b/crates/cli/tests/hello/mod.rs @@ -0,0 +1 @@ +mod test333; diff --git a/crates/cli/tests/hello/test333.rs b/crates/cli/tests/hello/test333.rs new file mode 100644 index 0000000..e63bb47 --- /dev/null +++ b/crates/cli/tests/hello/test333.rs @@ -0,0 +1,63 @@ +use std::io; + +use tracing::{level_filters::LevelFilter, trace}; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt}; + +#[test] +fn test_in_custom_folder_custom_333() { + let _guard = init_logger(); // Store the guard to keep it alive + + if std::env::var(simplex_test::TEST_ENV_NAME).is_err() { + trace!( + "Test 'test_in_custom_folder_custom_333' connected with simplex is disabled, run `simplex test` in order to test it" + ); + println!("Ter to test it"); + return; + } else { + tracing::trace!("Running 'test_in_custom_folder_custom_333' with simplex configuration"); + println!("Ter td222o test it"); + } + + assert_eq!(2 + 2, 4); +} + +#[test] +fn test_in_custom_folder2_custom_333() { + assert_eq!(2 + 2, 4); +} + +#[derive(Debug)] +pub struct LoggerGuard { + _std_out_guard: WorkerGuard, + _std_err_guard: WorkerGuard, +} + +pub fn init_logger() -> LoggerGuard { + let (std_out_writer, std_out_guard) = tracing_appender::non_blocking(io::stdout()); + let (std_err_writer, std_err_guard) = tracing_appender::non_blocking(io::stderr()); + let std_out_layer = fmt::layer() + .with_writer(std_out_writer) + .with_ansi(false) + .with_target(false) + .with_level(true) + .with_filter(EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("debug"))); + + let std_err_layer = fmt::layer() + .with_writer(std_err_writer) + .with_ansi(false) + .with_target(false) + .with_level(true) + .with_filter(LevelFilter::WARN); + + tracing_subscriber::registry() + .with(std_out_layer) + .with(std_err_layer) + .init(); + + trace!("Logging successfully initialized!"); + LoggerGuard { + _std_out_guard: std_out_guard, + _std_err_guard: std_err_guard, + } +} diff --git a/crates/cli/tests/test2.rs b/crates/cli/tests/test2.rs new file mode 100644 index 0000000..bb3b516 --- /dev/null +++ b/crates/cli/tests/test2.rs @@ -0,0 +1,24 @@ +use std::env; + +mod hello; +#[test] +fn test_in_custom_folder_integration() { + if let Ok(value) = env::var("SIMPLEX_TEST_RUN") { + println!("hello"); + } else { + return; + } + + assert_eq!(2 + 2, 4); +} + +#[test] +fn test_in_custom_folder2_integration() { + if let Ok(value) = env::var("SIMPLEX_TEST_RUN") { + println!("hello"); + } else { + return; + } + + assert_eq!(2 + 2, 4); +} From 8df6f158996042c71e89ac348ecd8ae3f8cd421a Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Tue, 24 Feb 2026 10:23:33 +0200 Subject: [PATCH 06/14] Fix argument gathering in a test collection --- crates/cli/src/cache_storage.rs | 37 +++++++++++++++++++++ crates/cli/src/cli/commands.rs | 4 +-- crates/cli/src/cli/mod.rs | 59 +++++++++------------------------ crates/cli/src/lib.rs | 1 + 4 files changed, 56 insertions(+), 45 deletions(-) create mode 100644 crates/cli/src/cache_storage.rs diff --git a/crates/cli/src/cache_storage.rs b/crates/cli/src/cache_storage.rs new file mode 100644 index 0000000..6942f25 --- /dev/null +++ b/crates/cli/src/cache_storage.rs @@ -0,0 +1,37 @@ +use crate::error::Error; +use simplex_test::TestConfig; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::{Path, PathBuf}; + +pub struct CacheStorage {} + +impl CacheStorage { + pub fn save_cached_test_config(test_config: &TestConfig) -> Result { + let cache_dir = Self::get_cache_dir()?; + std::fs::create_dir_all(&cache_dir)?; + let test_config_cache_name = Self::create_test_cache_name(&cache_dir); + + let mut file = OpenOptions::new() + .create(true) + .write(true) + .open(&test_config_cache_name)?; + file.write(toml::to_string_pretty(&test_config).unwrap().as_bytes())?; + file.flush()?; + Ok(test_config_cache_name) + } + + pub fn get_cache_dir() -> Result { + const TARGET_DIR_NAME: &str = "target"; + const SIMPLEX_CACHE_DIR_NAME: &str = "simplex"; + + let cwd = std::env::current_dir()?; + Ok(cwd.join(TARGET_DIR_NAME).join(SIMPLEX_CACHE_DIR_NAME)) + } + + pub fn create_test_cache_name(path: impl AsRef) -> PathBuf { + const TEST_CACHE_NAME: &str = "test_config.toml"; + + path.as_ref().join(TEST_CACHE_NAME) + } +} diff --git a/crates/cli/src/cli/commands.rs b/crates/cli/src/cli/commands.rs index d61eccb..e84c102 100644 --- a/crates/cli/src/cli/commands.rs +++ b/crates/cli/src/cli/commands.rs @@ -21,9 +21,9 @@ pub enum Command { #[derive(Debug, Subcommand)] pub enum TestCommand { /// Run integration tests using simplex conventions - Tests, + Integration, /// Run only specific files by path for testing - Test { + Run { #[arg(short = 't', long)] tests: Vec, }, diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 38ef52b..14641bf 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -1,13 +1,12 @@ pub mod commands; +use crate::cache_storage::CacheStorage; use crate::cli::commands::{TestCommand, TestFlags}; use crate::config::{Config, DEFAULT_CONFIG}; use crate::error::Error; use clap::Parser; -use simplex_test::{TestClientProvider, TestConfig}; -use std::fs::OpenOptions; -use std::io::Write; -use std::path::{Path, PathBuf}; +use simplex_test::TestClientProvider; +use std::path::PathBuf; use std::process::Stdio; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -97,14 +96,14 @@ impl Cli { command: &TestCommand, test_flags: &TestFlags, ) -> Result<(), Error> { - let cache_path = Self::save_cached_test_config(&config.test_config)?; + let cache_path = CacheStorage::save_cached_test_config(&config.test_config)?; let mut test_command = match command { - TestCommand::Tests => Self::form_test_command(TestParams { + TestCommand::Integration => Self::form_test_command(TestParams { cache_path, test_path: TestPaths::AllIntegration, test_flags: *test_flags, }), - TestCommand::Test { tests } => { + TestCommand::Run { tests: tests } => { let test_path = if tests.is_empty() { TestPaths::AllIntegration } else { @@ -136,37 +135,39 @@ impl Cli { fn form_test_command(params: TestParams) -> std::process::Command { let mut test_command = std::process::Command::new("sh"); test_command.arg("-c"); + let mut command_as_arg = String::new(); match params.test_path { TestPaths::AllIntegration => { - test_command.args(["cargo test --tests"]); + command_as_arg.push_str("cargo test --tests"); } TestPaths::Names(names) => { - let mut args: Vec = Vec::with_capacity(1 + names.len()); - args.push("cargo test".to_string()); + let mut arg = "cargo test".to_string(); for test_name in names { - args.push(format!("--test {test_name}")); + arg.push_str(&format!(" --test {test_name}")); } - test_command.args(args.into_iter()); + command_as_arg.push_str(&arg); } } { match params.test_flags.show_output { true => match params.test_flags.nocapture { true => { - test_command.args(["--", "--nocapture", "--show-output"]); + command_as_arg.push_str(&"-- --nocapture --show-output"); } false => { - test_command.args(["--", "--show-output"]); + command_as_arg.push_str(&"-- --show-output"); } }, false => match params.test_flags.nocapture { true => { - test_command.args(["--", "--nocapture"]); + command_as_arg.push_str(&"-- --nocapture"); } false => {} }, } } + test_command.args([command_as_arg]); + dbg!(test_command.get_args()); test_command .env(simplex_test::TEST_ENV_NAME, params.cache_path) .stdin(Stdio::inherit()) @@ -174,32 +175,4 @@ impl Cli { .stdout(Stdio::inherit()); test_command } - - fn save_cached_test_config(test_config: &TestConfig) -> Result { - let cache_dir = Self::get_cache_dir()?; - std::fs::create_dir_all(&cache_dir)?; - let test_config_cache_name = Self::create_cache_name(&cache_dir); - - let mut file = OpenOptions::new() - .create(true) - .write(true) - .open(&test_config_cache_name)?; - file.write(toml::to_string_pretty(&test_config).unwrap().as_bytes())?; - file.flush()?; - Ok(test_config_cache_name) - } - - fn get_cache_dir() -> Result { - const TARGET_DIR_NAME: &str = "target"; - const SIMPLEX_CACHE_DIR_NAME: &str = "simplex"; - - let cwd = std::env::current_dir()?; - Ok(cwd.join(TARGET_DIR_NAME).join(SIMPLEX_CACHE_DIR_NAME)) - } - - fn create_cache_name(path: impl AsRef) -> PathBuf { - const TEST_CACHE_NAME: &str = "test_config.toml"; - - path.as_ref().join(TEST_CACHE_NAME) - } } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index c7a1415..d0487d5 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,3 +1,4 @@ +pub mod cache_storage; pub mod cli; pub mod config; pub mod error; From 76c9b26bd59d63181dcdb79c35fdf33c3a8b60e8 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Tue, 24 Feb 2026 11:52:01 +0200 Subject: [PATCH 07/14] Add option to build config for test from itself --- Cargo.lock | 1 + Cargo.toml | 1 + crates/cli/Cargo.toml | 2 +- crates/cli/src/cli/commands.rs | 9 +- crates/cli/src/cli/mod.rs | 31 +++---- crates/cli/tests/hello/test333.rs | 73 +++++----------- crates/cli/tests/test2.rs | 1 + crates/test/Cargo.toml | 1 + crates/test/src/error.rs | 9 ++ crates/test/src/lib.rs | 111 ------------------------ crates/test/src/testing/config.rs | 18 +++- crates/test/src/testing/mod.rs | 58 +++++++++++++ crates/test/src/testing/rpc_provider.rs | 103 +++++++++++++++++++++- 13 files changed, 229 insertions(+), 189 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f90d2b1..8271bdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1913,6 +1913,7 @@ dependencies = [ "simplex-sdk", "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", "thiserror", + "toml 0.9.12+spec-1.1.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 97e4b0b..3ae657b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ ring = { version = "0.17.14" } sha2 = { version = "0.10.9", features = ["compress"] } serde = { version = "1.0.228" } thiserror = { version = "2.0.18" } +toml = { version = "0.9.8" } hex = { version = "0.4.3" } tracing = { version = "0.1.41" } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index cfcf436..475853a 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -22,7 +22,7 @@ simplicityhl = { workspace = true } electrsd = { workspace = true } thiserror = { workspace = true } serde = { workspace = true } -toml = { version = "0.9.8" } +toml = { workspace = true } anyhow = "1" dotenvy = "0.15" clap = { version = "4", features = ["derive", "env"] } diff --git a/crates/cli/src/cli/commands.rs b/crates/cli/src/cli/commands.rs index e84c102..05e081f 100644 --- a/crates/cli/src/cli/commands.rs +++ b/crates/cli/src/cli/commands.rs @@ -12,8 +12,6 @@ pub enum Command { Test { #[command(subcommand)] command: TestCommand, - #[command(flatten)] - additional_flags: TestFlags, }, } @@ -21,11 +19,16 @@ pub enum Command { #[derive(Debug, Subcommand)] pub enum TestCommand { /// Run integration tests using simplex conventions - Integration, + Integration { + #[command(flatten)] + additional_flags: TestFlags, + }, /// Run only specific files by path for testing Run { #[arg(short = 't', long)] tests: Vec, + #[command(flatten)] + additional_flags: TestFlags, }, } diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 14641bf..71dec52 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -52,15 +52,12 @@ impl Cli { println!("{loaded_config:#?}"); Ok(()) } - commands::Command::Test { - command, - additional_flags, - } => { + commands::Command::Test { command } => { let loaded_config = Config::load_or_discover(self.config.clone()).map_err(|e| Error::ConfigDiscoveryFailure(e))?; println!("{loaded_config:#?}"); - self.run_test_command(loaded_config, command, additional_flags)?; + self.run_test_command(loaded_config, command)?; Ok(()) } @@ -90,20 +87,18 @@ impl Cli { } } - pub(crate) fn run_test_command( - &self, - config: Config, - command: &TestCommand, - test_flags: &TestFlags, - ) -> Result<(), Error> { + pub(crate) fn run_test_command(&self, config: Config, command: &TestCommand) -> Result<(), Error> { let cache_path = CacheStorage::save_cached_test_config(&config.test_config)?; let mut test_command = match command { - TestCommand::Integration => Self::form_test_command(TestParams { + TestCommand::Integration { additional_flags } => Self::form_test_command(TestParams { cache_path, test_path: TestPaths::AllIntegration, - test_flags: *test_flags, + test_flags: *additional_flags, }), - TestCommand::Run { tests: tests } => { + TestCommand::Run { + tests, + additional_flags, + } => { let test_path = if tests.is_empty() { TestPaths::AllIntegration } else { @@ -112,7 +107,7 @@ impl Cli { Self::form_test_command(TestParams { cache_path, test_path, - test_flags: *test_flags, + test_flags: *additional_flags, }) } }; @@ -152,15 +147,15 @@ impl Cli { match params.test_flags.show_output { true => match params.test_flags.nocapture { true => { - command_as_arg.push_str(&"-- --nocapture --show-output"); + command_as_arg.push_str(&" -- --nocapture --show-output"); } false => { - command_as_arg.push_str(&"-- --show-output"); + command_as_arg.push_str(&" -- --show-output"); } }, false => match params.test_flags.nocapture { true => { - command_as_arg.push_str(&"-- --nocapture"); + command_as_arg.push_str(&" -- --nocapture"); } false => {} }, diff --git a/crates/cli/tests/hello/test333.rs b/crates/cli/tests/hello/test333.rs index e63bb47..9c4eae6 100644 --- a/crates/cli/tests/hello/test333.rs +++ b/crates/cli/tests/hello/test333.rs @@ -1,63 +1,30 @@ -use std::io; - -use tracing::{level_filters::LevelFilter, trace}; -use tracing_appender::non_blocking::WorkerGuard; +use simplex_test::TestContextBuilder; +use std::path::PathBuf; use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt}; #[test] fn test_in_custom_folder_custom_333() { - let _guard = init_logger(); // Store the guard to keep it alive - - if std::env::var(simplex_test::TEST_ENV_NAME).is_err() { - trace!( - "Test 'test_in_custom_folder_custom_333' connected with simplex is disabled, run `simplex test` in order to test it" - ); - println!("Ter to test it"); - return; - } else { - tracing::trace!("Running 'test_in_custom_folder_custom_333' with simplex configuration"); - println!("Ter td222o test it"); - } - - assert_eq!(2 + 2, 4); + let user_context = |_| -> _ { + assert_eq!(2 + 2, 4); + }; + let test_context = match std::env::var("SIMPLEX_TEST_ENV") { + Err(e) => { + tracing::trace!( + "Test 'test_in_custom_folder_custom_333' connected with simplex is disabled, run `simplex test` in order to test it, err: '{e}'" + ); + return; + } + Ok(path) => { + let path = PathBuf::from(path); + let test_context = TestContextBuilder::FromConfigPath(path).build().unwrap(); + test_context + } + }; + tracing::trace!("Running 'test_in_custom_folder_custom_333' with simplex configuration"); + user_context(test_context) } #[test] fn test_in_custom_folder2_custom_333() { assert_eq!(2 + 2, 4); } - -#[derive(Debug)] -pub struct LoggerGuard { - _std_out_guard: WorkerGuard, - _std_err_guard: WorkerGuard, -} - -pub fn init_logger() -> LoggerGuard { - let (std_out_writer, std_out_guard) = tracing_appender::non_blocking(io::stdout()); - let (std_err_writer, std_err_guard) = tracing_appender::non_blocking(io::stderr()); - let std_out_layer = fmt::layer() - .with_writer(std_out_writer) - .with_ansi(false) - .with_target(false) - .with_level(true) - .with_filter(EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("debug"))); - - let std_err_layer = fmt::layer() - .with_writer(std_err_writer) - .with_ansi(false) - .with_target(false) - .with_level(true) - .with_filter(LevelFilter::WARN); - - tracing_subscriber::registry() - .with(std_out_layer) - .with(std_err_layer) - .init(); - - trace!("Logging successfully initialized!"); - LoggerGuard { - _std_out_guard: std_out_guard, - _std_err_guard: std_err_guard, - } -} diff --git a/crates/cli/tests/test2.rs b/crates/cli/tests/test2.rs index bb3b516..9542e02 100644 --- a/crates/cli/tests/test2.rs +++ b/crates/cli/tests/test2.rs @@ -1,6 +1,7 @@ use std::env; mod hello; + #[test] fn test_in_custom_folder_integration() { if let Ok(value) = env::var("SIMPLEX_TEST_RUN") { diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index 2e3c39b..655691c 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -16,3 +16,4 @@ thiserror = { workspace = true } simplicityhl = { workspace = true } electrsd = { workspace = true } serde = { workspace = true } +toml = { workspace = true } \ No newline at end of file diff --git a/crates/test/src/error.rs b/crates/test/src/error.rs index 8ab293c..59f3076 100644 --- a/crates/test/src/error.rs +++ b/crates/test/src/error.rs @@ -1,5 +1,6 @@ use electrsd::electrum_client::bitcoin::hex::HexToArrayError; use simplex_provider::ExplorerError; +use std::io; #[derive(thiserror::Error, Debug)] pub enum TestError { @@ -19,4 +20,12 @@ pub enum TestError { /// Errors when failed to decode transaction. #[error("Failed to decode transaction: '{0}'")] TransactionDecode(String), + + /// Errors when io error occurred. + #[error("Occurred io error: '{0}'")] + Io(#[from] io::Error), + + /// Errors when io error occurred. + #[error("Occurred config deserialization error: '{0}'")] + ConfigDeserialize(#[from] toml::de::Error), } diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 7346446..4ad8493 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -5,114 +5,3 @@ mod testing; pub use common::*; pub use error::*; pub use testing::*; - -use bitcoind::bitcoincore_rpc::{Auth, Client}; -use bitcoind::{BitcoinD, Conf}; -use electrsd::bitcoind; -use simplex_provider::elements_rpc::ElementsRpcClient; -use simplex_sdk::constants::SimplicityNetwork; -use simplicityhl::elements::secp256k1_zkp::PublicKey; -use simplicityhl::elements::{Address, AssetId}; -use std::path::{Path, PathBuf}; - -#[derive(Hash, Clone, Debug, Eq, PartialEq)] -pub struct User { - pubkey: PublicKey, -} - -pub enum TestClientProvider { - ConfiguredNode { node: BitcoinD, network: SimplicityNetwork }, - CustomRpc(ElementsRpcClient), -} - -pub enum ConfigOption<'a> { - DefaultRegtest, - CustomConfRegtest { conf: Conf<'a> }, - CustomRpcUrlRegtest { url: String, auth: Auth }, -} - -impl TestClientProvider { - pub fn init(init_option: ConfigOption) -> Result { - let rpc = match init_option { - ConfigOption::DefaultRegtest => { - let node = Self::create_default_node(); - let network = SimplicityNetwork::default_regtest(); - Self::ConfiguredNode { node, network } - } - ConfigOption::CustomConfRegtest { conf } => { - let node = Self::create_node(conf, Self::get_bin_path())?; - let network = SimplicityNetwork::default_regtest(); - Self::ConfiguredNode { node, network } - } - ConfigOption::CustomRpcUrlRegtest { auth, url: rpc_url } => { - Self::CustomRpc(ElementsRpcClient::new(&rpc_url, auth)?) - } - }; - - if let Err(e) = ElementsRpcClient::blockchain_info(rpc.as_ref()) { - return Err(TestError::UnhealthyRpc(e)); - } - Ok(rpc) - } - - pub fn get_bin_path() -> PathBuf { - // TODO: change binary into installed one in $HOME dir - const ELEMENTSD_BIN_PATH: &str = "../../assets/elementsd"; - const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); - - Path::new(MANIFEST_DIR).join(ELEMENTSD_BIN_PATH) - } - - fn create_default_node() -> BitcoinD { - let mut conf = Conf::default(); - let bin_args = common::DefaultElementsdParams {}.get_bin_args(); - - conf.args = bin_args.iter().map(|x| x.as_ref()).collect::>(); - conf.network = "liquidregtest"; - conf.p2p = bitcoind::P2P::Yes; - - BitcoinD::with_conf(Self::get_bin_path(), &conf).unwrap() - } - - pub fn create_default_node_with_stdin() -> BitcoinD { - let mut conf = Conf::default(); - let bin_args = common::DefaultElementsdParams {}.get_bin_args(); - - conf.args = bin_args.iter().map(|x| x.as_ref()).collect::>(); - conf.view_stdout = true; - conf.attempts = 2; - conf.network = "liquidregtest"; - conf.p2p = bitcoind::P2P::Yes; - - BitcoinD::with_conf(Self::get_bin_path(), &conf).unwrap() - } - - fn create_node(conf: Conf, bin_path: PathBuf) -> Result { - BitcoinD::with_conf(bin_path, &conf).map_err(|e| TestError::NodeFailedToStart(e.to_string())) - } - - pub fn client(&self) -> &Client { - match self { - TestClientProvider::ConfiguredNode { node, .. } => &node.client, - TestClientProvider::CustomRpc(x) => x.client(), - } - } -} - -impl TestClientProvider { - pub fn fund(satoshi: u64, address: Option
, asset: Option) { - todo!() - } - - pub fn get_height() {} - - pub fn get_blockchain_info() { - todo!() - } -} - -impl AsRef for TestClientProvider { - fn as_ref(&self) -> &Client { - self.client() - } -} diff --git a/crates/test/src/testing/config.rs b/crates/test/src/testing/config.rs index aa20349..bc71cf9 100644 --- a/crates/test/src/testing/config.rs +++ b/crates/test/src/testing/config.rs @@ -1,4 +1,8 @@ +use crate::TestError; use electrsd::bitcoind::bitcoincore_rpc::jsonrpc::serde::{Deserialize, Serialize}; +use std::fs::OpenOptions; +use std::io::Read; +use std::path::Path; pub const TEST_ENV_NAME: &str = "SIMPLEX_TEST_ENV"; @@ -10,9 +14,19 @@ pub struct TestConfig { #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub enum RpcCreds { Auth { - rpc_username: String, - rpc_password: String, + url: String, + username: String, + password: String, }, #[default] None, } + +impl TestConfig { + pub fn from_file(path: impl AsRef) -> Result { + let mut content = String::new(); + let mut file = OpenOptions::new().read(true).open(path)?; + file.read_to_string(&mut content)?; + Ok(toml::from_str(&content)?) + } +} diff --git a/crates/test/src/testing/mod.rs b/crates/test/src/testing/mod.rs index 295f12b..0d8c54c 100644 --- a/crates/test/src/testing/mod.rs +++ b/crates/test/src/testing/mod.rs @@ -1,5 +1,63 @@ mod config; mod rpc_provider; +use crate::TestError; pub use config::*; +use electrsd::bitcoind::bitcoincore_rpc::Auth; pub use rpc_provider::*; +use std::path::PathBuf; + +pub struct TestContext { + config: TestConfig, + rpc: TestRpcProvider, +} + +pub enum TestContextBuilder { + FromConfigPath(PathBuf), +} + +impl TestContextBuilder { + pub fn build(self) -> Result { + let context = match self { + Self::FromConfigPath(path) => { + let config: TestConfig = TestConfig::from_file(&path)?; + match &config.rpc_creds { + RpcCreds::Auth { + url, + username, + password, + } => { + let rpc = TestRpcProvider::init(ConfigOption::CustomRpcUrlRegtest { + url: url.clone(), + auth: Auth::UserPass(username.clone(), password.clone()), + })?; + TestContext { config, rpc } + } + RpcCreds::None => { + let rpc = TestRpcProvider::init(ConfigOption::DefaultRegtest)?; + TestContext { config, rpc } + } + } + } + }; + Ok(context) + } +} + +impl TestContext { + pub fn get_config(&self) -> &TestConfig { + &self.config + } + + pub fn get_rpc_provider(&self) -> &TestRpcProvider { + &self.rpc + } + + pub fn default_rpc_setup(&self) -> Result<(), TestError> { + self.rpc.generate_blocks(1)?; + self.rpc.rescanblockchain(None, None)?; + self.rpc.sweep_initialfreecoins()?; + self.rpc.generate_blocks(100)?; + Ok(()) + } +} diff --git a/crates/test/src/testing/rpc_provider.rs b/crates/test/src/testing/rpc_provider.rs index 26a84a2..0a1d461 100644 --- a/crates/test/src/testing/rpc_provider.rs +++ b/crates/test/src/testing/rpc_provider.rs @@ -1,16 +1,117 @@ -use crate::{ConfigOption, TestClientProvider, TestError}; +use crate::{ElementsdParams, TestError, common}; use bitcoind::bitcoincore_rpc::bitcoin; use electrsd::bitcoind; use electrsd::bitcoind::bitcoincore_rpc::jsonrpc::serde_json::Value; +use electrsd::bitcoind::bitcoincore_rpc::{Auth, Client}; +use electrsd::bitcoind::{BitcoinD, Conf}; pub use simplex_provider::elements_rpc::*; +use simplex_sdk::constants::SimplicityNetwork; use simplex_sdk::error::SimplexError; use simplex_sdk::provider::ProviderSync; use simplicityhl::elements::Transaction; use simplicityhl::elements::hex::ToHex; use simplicityhl::elements::{Address, AssetId, BlockHash, Txid}; use std::collections::HashMap; +use std::path::{Path, PathBuf}; use std::str::FromStr; +pub enum TestClientProvider { + ConfiguredNode { node: BitcoinD, network: SimplicityNetwork }, + CustomRpc(ElementsRpcClient), +} + +pub enum ConfigOption<'a> { + DefaultRegtest, + CustomConfRegtest { conf: Conf<'a> }, + CustomRpcUrlRegtest { url: String, auth: Auth }, +} + +impl TestClientProvider { + pub fn init(init_option: ConfigOption) -> Result { + let rpc = match init_option { + ConfigOption::DefaultRegtest => { + let node = Self::create_default_node(); + let network = SimplicityNetwork::default_regtest(); + Self::ConfiguredNode { node, network } + } + ConfigOption::CustomConfRegtest { conf } => { + let node = Self::create_node(conf, Self::get_bin_path())?; + let network = SimplicityNetwork::default_regtest(); + Self::ConfiguredNode { node, network } + } + ConfigOption::CustomRpcUrlRegtest { auth, url: rpc_url } => { + Self::CustomRpc(ElementsRpcClient::new(&rpc_url, auth)?) + } + }; + + if let Err(e) = ElementsRpcClient::blockchain_info(rpc.as_ref()) { + return Err(TestError::UnhealthyRpc(e)); + } + Ok(rpc) + } + + pub fn get_bin_path() -> PathBuf { + // TODO: change binary into installed one in $HOME dir + const ELEMENTSD_BIN_PATH: &str = "../../assets/elementsd"; + const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); + + Path::new(MANIFEST_DIR).join(ELEMENTSD_BIN_PATH) + } + + fn create_default_node() -> BitcoinD { + let mut conf = Conf::default(); + let bin_args = common::DefaultElementsdParams {}.get_bin_args(); + + conf.args = bin_args.iter().map(|x| x.as_ref()).collect::>(); + conf.network = "liquidregtest"; + conf.p2p = bitcoind::P2P::Yes; + + BitcoinD::with_conf(Self::get_bin_path(), &conf).unwrap() + } + + pub fn create_default_node_with_stdin() -> BitcoinD { + let mut conf = Conf::default(); + let bin_args = common::DefaultElementsdParams {}.get_bin_args(); + + conf.args = bin_args.iter().map(|x| x.as_ref()).collect::>(); + conf.view_stdout = true; + conf.attempts = 2; + conf.network = "liquidregtest"; + conf.p2p = bitcoind::P2P::Yes; + + BitcoinD::with_conf(Self::get_bin_path(), &conf).unwrap() + } + + fn create_node(conf: Conf, bin_path: PathBuf) -> Result { + BitcoinD::with_conf(bin_path, &conf).map_err(|e| TestError::NodeFailedToStart(e.to_string())) + } + + pub fn client(&self) -> &Client { + match self { + TestClientProvider::ConfiguredNode { node, .. } => &node.client, + TestClientProvider::CustomRpc(x) => x.client(), + } + } +} + +impl TestClientProvider { + pub fn fund(satoshi: u64, address: Option
, asset: Option) { + todo!() + } + + pub fn get_height() {} + + pub fn get_blockchain_info() { + todo!() + } +} + +impl AsRef for TestClientProvider { + fn as_ref(&self) -> &Client { + self.client() + } +} + pub struct TestRpcProvider { provider: TestClientProvider, } From 7dfe16dab3ce36ffa8ad8c5134366e35aab9c80b Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Tue, 24 Feb 2026 13:52:38 +0200 Subject: [PATCH 08/14] setup draft macros example --- Cargo.lock | 1 + crates/cli/src/cli/commands.rs | 3 + crates/cli/src/cli/mod.rs | 30 +- crates/cli/tests/hello/test333.rs | 12 +- crates/macros/Cargo.toml | 1 + crates/macros/src/lib.rs | 7 +- crates/macros/src/macros_core/attr/mod.rs | 1 + .../src/macros_core/{ => attr}/program.rs | 0 crates/macros/src/macros_core/mod.rs | 9 +- crates/macros/src/macros_core/test/mod.rs | 303 +++++++++++++++++- crates/simplex/tests/simplex_test.rs | 42 ++- 11 files changed, 361 insertions(+), 48 deletions(-) rename crates/macros/src/macros_core/{ => attr}/program.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 8271bdd..855f271 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1870,6 +1870,7 @@ dependencies = [ "proc-macro2", "quote", "serde", + "simplex-test", "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", "syn 2.0.116", "thiserror", diff --git a/crates/cli/src/cli/commands.rs b/crates/cli/src/cli/commands.rs index 05e081f..ba95585 100644 --- a/crates/cli/src/cli/commands.rs +++ b/crates/cli/src/cli/commands.rs @@ -41,4 +41,7 @@ pub struct TestFlags { /// Show output #[arg(long = "show-output")] pub show_output: bool, + /// Run ignored tests + #[arg(long = "ignored")] + pub ignored: bool, } diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 71dec52..07bae20 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -8,8 +8,8 @@ use clap::Parser; use simplex_test::TestClientProvider; use std::path::PathBuf; use std::process::Stdio; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; #[derive(Debug, Parser)] #[command(name = "simplicity-dex")] @@ -144,21 +144,19 @@ impl Cli { } } { - match params.test_flags.show_output { - true => match params.test_flags.nocapture { - true => { - command_as_arg.push_str(&" -- --nocapture --show-output"); - } - false => { - command_as_arg.push_str(&" -- --show-output"); - } - }, - false => match params.test_flags.nocapture { - true => { - command_as_arg.push_str(&" -- --nocapture"); - } - false => {} - }, + let mut opt_params = String::new(); + if params.test_flags.show_output { + opt_params.push_str(" --show-output"); + } + if params.test_flags.nocapture { + opt_params.push_str(" --nocapture"); + } + if params.test_flags.ignored { + opt_params.push_str(" --ignored"); + } + if params.test_flags.show_output || params.test_flags.nocapture || params.test_flags.ignored { + command_as_arg.push_str(" --"); + command_as_arg.push_str(&opt_params); } } test_command.args([command_as_arg]); diff --git a/crates/cli/tests/hello/test333.rs b/crates/cli/tests/hello/test333.rs index 9c4eae6..370f524 100644 --- a/crates/cli/tests/hello/test333.rs +++ b/crates/cli/tests/hello/test333.rs @@ -1,18 +1,20 @@ -use simplex_test::TestContextBuilder; +use simplex_test::{TestContext, TestContextBuilder}; use std::path::PathBuf; use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt}; +#[ignore] #[test] -fn test_in_custom_folder_custom_333() { - let user_context = |_| -> _ { +fn test_in_custom_folder_custom_333() -> anyhow::Result<()>{ + fn test_in_custom_folder_custom_333(test_context: TestContext) -> anyhow::Result<()> { assert_eq!(2 + 2, 4); + Ok(()) }; let test_context = match std::env::var("SIMPLEX_TEST_ENV") { Err(e) => { tracing::trace!( "Test 'test_in_custom_folder_custom_333' connected with simplex is disabled, run `simplex test` in order to test it, err: '{e}'" ); - return; + panic!("Failed to run this test, required to use `simplex test`.") } Ok(path) => { let path = PathBuf::from(path); @@ -21,7 +23,7 @@ fn test_in_custom_folder_custom_333() { } }; tracing::trace!("Running 'test_in_custom_folder_custom_333' with simplex configuration"); - user_context(test_context) + test_in_custom_folder_custom_333(test_context) } #[test] diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index 354324d..d039746 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -25,6 +25,7 @@ derive = [] workspace = true [dependencies] +simplex-test = { workspace = true } thiserror = { workspace = true } simplicityhl = { workspace = true } diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index e117118..d9fa658 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -2,14 +2,11 @@ use proc_macro::TokenStream; mod macros_core; -// TODO(Illia): add path to exported crates to be able users to use their own https://stackoverflow.com/questions/79595543/rust-how-to-re-export-3rd-party-crate -// #[serde(crate = "exporter::reexports::serde")] -// simplicityhl, either - #[cfg(feature = "macros")] #[proc_macro] pub fn include_simf(tokenstream: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(tokenstream as macros_core::attr::parse::SynFilePath); + match macros_core::expand_include_simf(&input) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), @@ -21,7 +18,7 @@ pub fn include_simf(tokenstream: TokenStream) -> TokenStream { pub fn test(args: TokenStream, input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as syn::ItemFn); - match macros_core::expand_test(&args.into(), &input) { + match macros_core::expand_test(args.into(), input) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), } diff --git a/crates/macros/src/macros_core/attr/mod.rs b/crates/macros/src/macros_core/attr/mod.rs index f58966f..a98a90f 100644 --- a/crates/macros/src/macros_core/attr/mod.rs +++ b/crates/macros/src/macros_core/attr/mod.rs @@ -1,6 +1,7 @@ pub mod codegen; pub mod parse; mod types; +pub(crate) mod program; pub use parse::SimfContent; diff --git a/crates/macros/src/macros_core/program.rs b/crates/macros/src/macros_core/attr/program.rs similarity index 100% rename from crates/macros/src/macros_core/program.rs rename to crates/macros/src/macros_core/attr/program.rs diff --git a/crates/macros/src/macros_core/mod.rs b/crates/macros/src/macros_core/mod.rs index 8cbe550..399dff4 100644 --- a/crates/macros/src/macros_core/mod.rs +++ b/crates/macros/src/macros_core/mod.rs @@ -3,15 +3,13 @@ pub(crate) mod attr; pub(crate) mod test; -pub(crate) mod program; - /// Expands the `include_simf` macro. /// /// # Errors /// Returns a `syn::Result` with an error if parsing, compilation, or expansion fails. pub fn expand_include_simf(input: &attr::parse::SynFilePath) -> syn::Result { let simf_content = attr::SimfContent::eval_path_expr(input)?; - let abi_meta = program::compile_simf(&simf_content)?; + let abi_meta = attr::program::compile_simf(&simf_content)?; let generated = attr::expand_helpers(simf_content, abi_meta)?; Ok(generated) @@ -21,7 +19,6 @@ pub fn expand_include_simf(input: &attr::parse::SynFilePath) -> syn::Result syn::Result { - // TODO: maybe check crate attributes to allow user to do smth like in sqlx? - Ok(test::expand_simple(input)) +pub fn expand_test(args: proc_macro2::TokenStream, input: syn::ItemFn) -> syn::Result { + test::expand(args, input) } diff --git a/crates/macros/src/macros_core/test/mod.rs b/crates/macros/src/macros_core/test/mod.rs index ef09f63..1ef9ace 100644 --- a/crates/macros/src/macros_core/test/mod.rs +++ b/crates/macros/src/macros_core/test/mod.rs @@ -1,30 +1,309 @@ -pub(crate) fn expand_simple(input: &syn::ItemFn) -> proc_macro2::TokenStream { +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse::Parser; + +pub(crate) fn expand_simple(input: &syn::ItemFn, args: AttributeArgs) -> proc_macro2::TokenStream { let ret = &input.sig.output; let name = &input.sig.ident; + let inputs = &input.sig.inputs; let body = &input.block; let attrs = &input.attrs; let fn_name_str = name.to_string(); + let args = args.to_token_stream().to_string(); + let simplex_test_env = simplex_test::TEST_ENV_NAME; let ident = format!("{input:#?}"); quote::quote! { #[::core::prelude::v1::test] #(#attrs)* fn #name() #ret { use ::simplex::tracing; - if std::env::var(simplex_test::TEST_ENV_NAME).is_err() { - tracing::trace!("Test '{}' connected with simplex is disabled, run `simplex test` in order to test it", #fn_name_str); - println!("disabled"); - return; - } else { - tracing::trace!("Running '{}' with simplex configuration", #fn_name_str); - println!("running"); + use std::path::PathBuf; + use simplex_test::TestContextBuilder; + + fn #name(#inputs) #ret { + #body } - #body - // ::sqlx::test_block_on(async { #body }) + let test_context = match std::env::var(#simplex_test_env) { + Err(e) => { + tracing::trace!( + "Test 'test_in_custom_folder_custom_333' connected with simplex is disabled, run `simplex test` in order to test it, err: '{e}'" + ); + panic!("Failed to run this test, required to use `simplex test`"); + } + Ok(path) => { + let path = PathBuf::from(path); + let test_context = TestContextBuilder::FromConfigPath(path).build().unwrap(); + tracing::trace!("Running 'test_in_custom_folder_custom_333' with simplex configuration"); + test_context + } + }; // before - println!("Running test: {}, \n -- {}", #fn_name_str, #ident); - //revert + println!("fn name: {}, \n ident: {}", #fn_name_str, #ident); + println!("input: {}, \n AttributeArgs: {}", "", #args); + + #name(test_context) } } } + +struct Args { + config_option: ConfigOpt, +} + +enum ConfigOpt { + Config, + None, +} + +type AttributeArgs = syn::punctuated::Punctuated; + +pub fn expand(args: TokenStream, input: syn::ItemFn) -> syn::Result { + let parser = AttributeArgs::parse_terminated; + let args = parser.parse2(args)?; + + Ok(expand_simple(&input, args)) + + // if input.sig.inputs.is_empty() { + // if !args.is_empty() { + // if cfg!(not(feature = "migrate")) { + // return Err(syn::Error::new_spanned( + // args.first().unwrap(), + // "control attributes are not allowed unless \ + // the `migrate` feature is enabled and \ + // automatic test DB management is used; see docs", + // ) + // .into()); + // } + // + // return Err(syn::Error::new_spanned( + // args.first().unwrap(), + // "control attributes are not allowed unless \ + // automatic test DB management is used; see docs", + // ) + // .into()); + // } + // + // return Ok(expand_simple(&input, args)); + // } + // + // expand_advanced(args, input) +} + +// fn expand_advanced(args: AttributeArgs, input: syn::ItemFn) -> syn::Result { +// let config = sqlx_core::config::Config::try_from_crate_or_default()?; +// +// let ret = &input.sig.output; +// let name = &input.sig.ident; +// let inputs = &input.sig.inputs; +// let body = &input.block; +// let attrs = &input.attrs; +// +// let args = parse_args(args)?; +// +// let fn_arg_types = inputs.iter().map(|_| quote! { _ }); +// +// let mut fixtures = Vec::new(); +// +// for (fixture_type, fixtures_local) in args.fixtures { +// let mut res = match fixture_type { +// FixturesType::None => vec![], +// FixturesType::RelativePath => fixtures_local +// .into_iter() +// .map(|fixture| { +// let mut fixture_str = fixture.value(); +// add_sql_extension_if_missing(&mut fixture_str); +// +// let path = format!("fixtures/{}", fixture_str); +// +// quote! { +// ::sqlx::testing::TestFixture { +// path: #path, +// contents: include_str!(#path), +// } +// } +// }) +// .collect(), +// FixturesType::CustomRelativePath(path) => fixtures_local +// .into_iter() +// .map(|fixture| { +// let mut fixture_str = fixture.value(); +// add_sql_extension_if_missing(&mut fixture_str); +// +// let path = format!("{}/{}", path.value(), fixture_str); +// +// quote! { +// ::sqlx::testing::TestFixture { +// path: #path, +// contents: include_str!(#path), +// } +// } +// }) +// .collect(), +// FixturesType::ExplicitPath => fixtures_local +// .into_iter() +// .map(|fixture| { +// let path = fixture.value(); +// +// quote! { +// ::sqlx::testing::TestFixture { +// path: #path, +// contents: include_str!(#path), +// } +// } +// }) +// .collect(), +// }; +// fixtures.append(&mut res) +// } +// +// let migrations = match args.migrations { +// ConfigOpt::ExplicitPath(path) => { +// let migrator = crate::migrate::expand(Some(path))?; +// quote! { args.migrator(&#migrator); } +// } +// ConfigOpt::InferredPath if !inputs.is_empty() => { +// let path = crate::migrate::default_path(&config); +// +// let resolved_path = crate::common::resolve_path(path, proc_macro2::Span::call_site())?; +// +// if resolved_path.is_dir() { +// let migrator = crate::migrate::expand_with_path(&config, &resolved_path)?; +// quote! { args.migrator(&#migrator); } +// } else { +// quote! {} +// } +// } +// ConfigOpt::ExplicitMigrator(path) => { +// quote! { args.migrator(&#path); } +// } +// _ => quote! {}, +// }; +// +// Ok(quote! { +// #(#attrs)* +// #[::core::prelude::v1::test] +// fn #name() #ret { +// async fn #name(#inputs) #ret { +// #body +// } +// +// let mut args = ::sqlx::testing::TestArgs::new(concat!(module_path!(), "::", stringify!(#name))); +// +// #migrations +// +// args.fixtures(&[#(#fixtures),*]); +// +// // We need to give a coercion site or else we get "unimplemented trait" errors. +// let f: fn(#(#fn_arg_types),*) -> _ = #name; +// +// ::sqlx::testing::TestFn::run_test(f, args) +// } +// }) +// } + +// fn parse_args(attr_args: AttributeArgs) -> syn::Result { +// use syn::{ +// parenthesized, parse::Parse, punctuated::Punctuated, token::Comma, Expr, Lit, LitStr, Meta, MetaNameValue, +// Token, +// }; +// +// let mut fixtures = Vec::new(); +// let mut migrations = ConfigOpt::InferredPath; +// +// for arg in attr_args { +// let path = arg.path().clone(); +// +// match arg { +// syn::Meta::List(list) if list.path.is_ident("fixtures") => { +// let mut fixtures_local = vec![]; +// let mut fixtures_type = FixturesType::None; +// +// let parse_nested = list.parse_nested_meta(|meta| { +// if meta.path.is_ident("path") { +// // fixtures(path = "", scripts("","")) checking `path` argument +// meta.input.parse::()?; +// let val: LitStr = meta.input.parse()?; +// parse_fixtures_path_args(&mut fixtures_type, val)?; +// } else if meta.path.is_ident("scripts") { +// // fixtures(path = "", scripts("","")) checking `scripts` argument +// let content; +// parenthesized!(content in meta.input); +// let list = content.parse_terminated(::parse, Comma)?; +// parse_fixtures_scripts_args(&mut fixtures_type, list, &mut fixtures_local)?; +// } else { +// return Err(syn::Error::new_spanned(meta.path, "unexpected fixture meta")); +// } +// +// Ok(()) +// }); +// +// if parse_nested.is_err() { +// // fixtures("","") or fixtures("","") +// let args = list.parse_args_with(>::parse_terminated)?; +// for arg in args { +// parse_fixtures_args(&mut fixtures_type, arg, &mut fixtures_local)?; +// } +// } +// +// fixtures.push((fixtures_type, fixtures_local)); +// } +// syn::Meta::NameValue(value) if value.path.is_ident("migrations") => { +// if !matches!(migrations, ConfigOpt::InferredPath) { +// return Err(syn::Error::new_spanned( +// value, +// "cannot have more than one `migrations` or `migrator` arg", +// )); +// } +// +// fn recurse_lit_lookup(expr: Expr) -> Option { +// match expr { +// Expr::Lit(syn::ExprLit { lit, .. }) => Some(lit), +// Expr::Group(syn::ExprGroup { expr, .. }) => recurse_lit_lookup(*expr), +// _ => None, +// } +// } +// +// let Some(lit) = recurse_lit_lookup(value.value) else { +// return Err(syn::Error::new_spanned(path, "expected string or `false`")); +// }; +// +// migrations = match lit { +// // migrations = false +// Lit::Bool(b) if !b.value => ConfigOpt::Disabled, +// // migrations = true +// Lit::Bool(b) => { +// return Err(syn::Error::new_spanned(b, "`migrations = true` is redundant")); +// } +// // migrations = "path" +// Lit::Str(s) => ConfigOpt::ExplicitPath(s), +// lit => return Err(syn::Error::new_spanned(lit, "expected string or `false`")), +// }; +// } +// // migrator = "" +// Meta::NameValue(MetaNameValue { value, .. }) if path.is_ident("migrator") => { +// if !matches!(migrations, ConfigOpt::InferredPath) { +// return Err(syn::Error::new_spanned( +// path, +// "cannot have more than one `migrations` or `migrator` arg", +// )); +// } +// +// let Expr::Lit(syn::ExprLit { lit: Lit::Str(lit), .. }) = value else { +// return Err(syn::Error::new_spanned(path, "expected string")); +// }; +// +// migrations = ConfigOpt::ExplicitMigrator(lit.parse()?); +// } +// arg => { +// return Err(syn::Error::new_spanned( +// arg, +// r#"expected `fixtures("", ...)` or `migrations = "" | false` or `migrator = ""`"#, +// )); +// } +// } +// } +// +// Ok(Args { config_option: ConfigOpt::None }) +// } diff --git a/crates/simplex/tests/simplex_test.rs b/crates/simplex/tests/simplex_test.rs index 680bb60..5aa33fe 100644 --- a/crates/simplex/tests/simplex_test.rs +++ b/crates/simplex/tests/simplex_test.rs @@ -1,12 +1,46 @@ use simplex_provider::elements_rpc::{AddressType, ElementsRpcClient}; use simplex_sdk::constants::SimplicityNetwork; -use simplex_test::DEFAULT_SAT_AMOUNT_FAUCET; -use simplicityhl::elements::Address; +use simplex_test::{TestContext, DEFAULT_SAT_AMOUNT_FAUCET}; use simplicityhl::elements::bitcoin::secp256k1; use simplicityhl::elements::secp256k1_zkp::Keypair; +use simplicityhl::elements::Address; + +#[ignore] +#[simplex::simplex_macros::test(hello = "hi")] +fn test_execution(x: TestContext) { + assert!(true) +} + +#[ignore] +#[test] +fn test_execution2() { + use ::simplex::tracing; + use simplex_test::TestContextBuilder; + use std::path::PathBuf; -#[simplex::simplex_macros::test] -fn test_execution() {} + fn test_execution2(x: TestContext) { + assert!(true); + } + + let test_context = match std::env::var("SIMPLEX_TEST_ENV") { + Err(e) => { + tracing::trace!( + "Test 'test_in_custom_folder_custom_333' connected with simplex is disabled, run `simplex test` in order to test it, err: '{e}'" + ); + panic!("Failed to run this test, required to use `simplex test`"); + } + Ok(path) => { + let path = PathBuf::from(path); + let test_context = TestContextBuilder::FromConfigPath(path).build().unwrap(); + tracing::trace!("Running 'test_in_custom_folder_custom_333' with simplex configuration"); + test_context + } + }; + println!("fn name: {}, \n ident: {}", "test_execution2", "#ident"); + println!("input: {}, \n AttributeArgs: {}", "#input", "#args"); + + test_execution2(test_context) +} #[test] fn test_invocation_tx_tracking() -> anyhow::Result<()> { From 56aa1be06da67a7991cc99720f758a642ea32741 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Tue, 24 Feb 2026 16:31:58 +0200 Subject: [PATCH 09/14] Add macros generation for WitnessTrait & ArgumentsTrait --- crates/cli/src/cli/mod.rs | 2 +- crates/cli/tests/hello/test333.rs | 2 +- crates/macros/src/macros_core/attr/codegen.rs | 41 +++++++++++-------- crates/macros/src/macros_core/attr/mod.rs | 2 +- crates/macros/src/macros_core/test/mod.rs | 8 ++-- crates/sdk/src/witness.rs | 4 +- crates/simplex/tests/simplex_test.rs | 4 +- crates/simplex/tests/ui/array_tr_storage.rs | 2 + crates/simplex/tests/ui/bytes32_tr_storage.rs | 2 + .../simplex/tests/ui/dual_currency_deposit.rs | 2 + crates/simplex/tests/ui/option_offer.rs | 2 + crates/simplex/tests/ui/options.rs | 3 ++ crates/simplex/tests/ui/simple_storage.rs | 2 + 13 files changed, 47 insertions(+), 29 deletions(-) diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 07bae20..b3820f5 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -8,8 +8,8 @@ use clap::Parser; use simplex_test::TestClientProvider; use std::path::PathBuf; use std::process::Stdio; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; #[derive(Debug, Parser)] #[command(name = "simplicity-dex")] diff --git a/crates/cli/tests/hello/test333.rs b/crates/cli/tests/hello/test333.rs index 370f524..cda6b9b 100644 --- a/crates/cli/tests/hello/test333.rs +++ b/crates/cli/tests/hello/test333.rs @@ -4,7 +4,7 @@ use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::Subs #[ignore] #[test] -fn test_in_custom_folder_custom_333() -> anyhow::Result<()>{ +fn test_in_custom_folder_custom_333() -> anyhow::Result<()> { fn test_in_custom_folder_custom_333(test_context: TestContext) -> anyhow::Result<()> { assert_eq!(2 + 2, 4); Ok(()) diff --git a/crates/macros/src/macros_core/attr/codegen.rs b/crates/macros/src/macros_core/attr/codegen.rs index 50e1bdb..742d18f 100644 --- a/crates/macros/src/macros_core/attr/codegen.rs +++ b/crates/macros/src/macros_core/attr/codegen.rs @@ -112,6 +112,7 @@ impl WitnessStruct { use simplicityhl::str::WitnessName; use simplicityhl::types::TypeConstructible; use simplicityhl::value::ValueConstructible; + use simplex_sdk::arguments::ArgumentsTrait; use bincode::*; }, struct_token_stream: quote! { @@ -119,14 +120,6 @@ impl WitnessStruct { }, struct_impl: quote! { impl #struct_name { - /// Build Simplicity arguments for contract instantiation. - #[must_use] - pub fn build_arguments(&self) -> ::simplicityhl::Arguments { - ::simplicityhl::Arguments::from(HashMap::from([ - #(#tuples),* - ])) - } - /// Build struct from Simplicity Arguments. /// /// # Errors @@ -140,6 +133,16 @@ impl WitnessStruct { } + impl ::simplex_sdk::arguments::ArgumentsTrait for #struct_name { + /// Build Simplicity arguments for contract instantiation. + #[must_use] + fn build_arguments(&self) -> ::simplicityhl::Arguments { + ::simplicityhl::Arguments::from(HashMap::from([ + #(#tuples),* + ])) + } + } + impl simplex::serde::Serialize for #struct_name { fn serialize(&self, serializer: S) -> Result where @@ -148,6 +151,7 @@ impl WitnessStruct { self.build_arguments().serialize(serializer) } } + impl<'de> simplex::serde::Deserialize<'de> for #struct_name { fn deserialize(deserializer: D) -> Result where @@ -185,25 +189,18 @@ impl WitnessStruct { use simplicityhl::str::WitnessName; use simplicityhl::types::TypeConstructible; use simplicityhl::value::ValueConstructible; + use simplex_sdk::witness::WitnessTrait; }, struct_token_stream: quote! { #generated_struct }, struct_impl: quote! { impl #struct_name { - /// Build Simplicity witness values for contract execution. - #[must_use] - pub fn build_witness(&self) -> ::simplicityhl::WitnessValues { - ::simplicityhl::WitnessValues::from(HashMap::from([ - #(#tuples),* - ])) - } - /// Build struct from Simplicity WitnessValues. /// /// # Errors /// - /// Returns error if any required witness is missing, has wrong type, or has invalid value. + /// Returns error if any required witness is missing, has the wrong type, or has an invalid value. pub fn from_witness(witness: &WitnessValues) -> Result { #arguments_conversion_from_args_map @@ -211,6 +208,16 @@ impl WitnessStruct { } } + impl ::simplex_sdk::witness::WitnessTrait for #struct_name { + /// Build Simplicity witness values for contract execution. + #[must_use] + fn build_witness(&self) -> ::simplicityhl::WitnessValues { + ::simplicityhl::WitnessValues::from(HashMap::from([ + #(#tuples),* + ])) + } + } + impl simplex::serde::Serialize for #struct_name { fn serialize(&self, serializer: S) -> Result where diff --git a/crates/macros/src/macros_core/attr/mod.rs b/crates/macros/src/macros_core/attr/mod.rs index a98a90f..9b9ce47 100644 --- a/crates/macros/src/macros_core/attr/mod.rs +++ b/crates/macros/src/macros_core/attr/mod.rs @@ -1,7 +1,7 @@ pub mod codegen; pub mod parse; -mod types; pub(crate) mod program; +mod types; pub use parse::SimfContent; diff --git a/crates/macros/src/macros_core/test/mod.rs b/crates/macros/src/macros_core/test/mod.rs index 1ef9ace..4034913 100644 --- a/crates/macros/src/macros_core/test/mod.rs +++ b/crates/macros/src/macros_core/test/mod.rs @@ -18,8 +18,8 @@ pub(crate) fn expand_simple(input: &syn::ItemFn, args: AttributeArgs) -> proc_ma #(#attrs)* fn #name() #ret { use ::simplex::tracing; - use std::path::PathBuf; - use simplex_test::TestContextBuilder; + use ::std::path::PathBuf; + use ::simplex_test::TestContextBuilder; fn #name(#inputs) #ret { #body @@ -28,14 +28,14 @@ pub(crate) fn expand_simple(input: &syn::ItemFn, args: AttributeArgs) -> proc_ma let test_context = match std::env::var(#simplex_test_env) { Err(e) => { tracing::trace!( - "Test 'test_in_custom_folder_custom_333' connected with simplex is disabled, run `simplex test` in order to test it, err: '{e}'" + "Test '{}' connected with simplex is disabled, run `simplex test` in order to test it, err: '{e}'", #fn_name_str ); panic!("Failed to run this test, required to use `simplex test`"); } Ok(path) => { let path = PathBuf::from(path); let test_context = TestContextBuilder::FromConfigPath(path).build().unwrap(); - tracing::trace!("Running 'test_in_custom_folder_custom_333' with simplex configuration"); + tracing::trace!("Running '{}' with simplex configuration", #fn_name_str); test_context } }; diff --git a/crates/sdk/src/witness.rs b/crates/sdk/src/witness.rs index 526c48e..7e58e40 100644 --- a/crates/sdk/src/witness.rs +++ b/crates/sdk/src/witness.rs @@ -1,5 +1,3 @@ -use simplicityhl::WitnessValues; - pub trait WitnessTrait { - fn build_witness(&self) -> WitnessValues; + fn build_witness(&self) -> simplicityhl::WitnessValues; } diff --git a/crates/simplex/tests/simplex_test.rs b/crates/simplex/tests/simplex_test.rs index 5aa33fe..68b058b 100644 --- a/crates/simplex/tests/simplex_test.rs +++ b/crates/simplex/tests/simplex_test.rs @@ -1,9 +1,9 @@ use simplex_provider::elements_rpc::{AddressType, ElementsRpcClient}; use simplex_sdk::constants::SimplicityNetwork; -use simplex_test::{TestContext, DEFAULT_SAT_AMOUNT_FAUCET}; +use simplex_test::{DEFAULT_SAT_AMOUNT_FAUCET, TestContext}; +use simplicityhl::elements::Address; use simplicityhl::elements::bitcoin::secp256k1; use simplicityhl::elements::secp256k1_zkp::Keypair; -use simplicityhl::elements::Address; #[ignore] #[simplex::simplex_macros::test(hello = "hi")] diff --git a/crates/simplex/tests/ui/array_tr_storage.rs b/crates/simplex/tests/ui/array_tr_storage.rs index bbf5413..f04d583 100644 --- a/crates/simplex/tests/ui/array_tr_storage.rs +++ b/crates/simplex/tests/ui/array_tr_storage.rs @@ -1,4 +1,6 @@ use simplex_macros::*; +use simplex_sdk::witness::WitnessTrait; +use simplex_sdk::arguments::ArgumentsTrait; include_simf!("../../../../crates/simplex/tests/ui/array_tr_storage.simf"); diff --git a/crates/simplex/tests/ui/bytes32_tr_storage.rs b/crates/simplex/tests/ui/bytes32_tr_storage.rs index 01fcb22..5937bcc 100644 --- a/crates/simplex/tests/ui/bytes32_tr_storage.rs +++ b/crates/simplex/tests/ui/bytes32_tr_storage.rs @@ -1,4 +1,6 @@ use simplex_macros::*; +use simplex_sdk::witness::WitnessTrait; +use simplex_sdk::arguments::ArgumentsTrait; include_simf!("../../../../crates/simplex/tests/ui/bytes32_tr_storage.simf"); diff --git a/crates/simplex/tests/ui/dual_currency_deposit.rs b/crates/simplex/tests/ui/dual_currency_deposit.rs index eb055a4..466fd77 100644 --- a/crates/simplex/tests/ui/dual_currency_deposit.rs +++ b/crates/simplex/tests/ui/dual_currency_deposit.rs @@ -1,4 +1,6 @@ use simplex_macros::*; +use simplex_sdk::witness::WitnessTrait; +use simplex_sdk::arguments::ArgumentsTrait; include_simf!("../../../../crates/simplex/tests/ui/dual_currency_deposit.simf"); diff --git a/crates/simplex/tests/ui/option_offer.rs b/crates/simplex/tests/ui/option_offer.rs index 0db710c..96d7eaa 100644 --- a/crates/simplex/tests/ui/option_offer.rs +++ b/crates/simplex/tests/ui/option_offer.rs @@ -1,4 +1,6 @@ use simplex_macros::*; +use simplex_sdk::witness::WitnessTrait; +use simplex_sdk::arguments::ArgumentsTrait; include_simf!("../../../../crates/simplex/tests/ui/option_offer.simf"); diff --git a/crates/simplex/tests/ui/options.rs b/crates/simplex/tests/ui/options.rs index 32553e3..fd92cf9 100644 --- a/crates/simplex/tests/ui/options.rs +++ b/crates/simplex/tests/ui/options.rs @@ -1,4 +1,7 @@ use simplex_macros::*; +use simplex_sdk::witness::WitnessTrait; +use simplex_sdk::arguments::ArgumentsTrait; + include_simf!("../../../../crates/simplex/tests/ui/options.simf"); fn main() -> Result<(), String> { diff --git a/crates/simplex/tests/ui/simple_storage.rs b/crates/simplex/tests/ui/simple_storage.rs index 0aa3718..b5be89b 100644 --- a/crates/simplex/tests/ui/simple_storage.rs +++ b/crates/simplex/tests/ui/simple_storage.rs @@ -1,4 +1,6 @@ use simplex_macros::*; +use simplex_sdk::witness::WitnessTrait; +use simplex_sdk::arguments::ArgumentsTrait; include_simf!("../../../../crates/simplex/tests/ui/simple_storage.simf"); From adc20d468097552a209e2b1fb8e9ee04bafd8693 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Tue, 24 Feb 2026 17:31:35 +0200 Subject: [PATCH 10/14] add default_rpc field for macros generation to force default config usage --- crates/macros/src/macros_core/test/mod.rs | 311 ++++------------------ crates/simplex/tests/simplex_test.rs | 8 +- crates/test/src/testing/mod.rs | 8 + 3 files changed, 71 insertions(+), 256 deletions(-) diff --git a/crates/macros/src/macros_core/test/mod.rs b/crates/macros/src/macros_core/test/mod.rs index 4034913..8728ad8 100644 --- a/crates/macros/src/macros_core/test/mod.rs +++ b/crates/macros/src/macros_core/test/mod.rs @@ -1,8 +1,8 @@ use proc_macro2::TokenStream; -use quote::ToTokens; +use quote::{ToTokens, quote}; use syn::parse::Parser; -pub(crate) fn expand_simple(input: &syn::ItemFn, args: AttributeArgs) -> proc_macro2::TokenStream { +pub(crate) fn expand_inner(input: &syn::ItemFn, args: AttributeArgs) -> syn::Result { let ret = &input.sig.output; let name = &input.sig.ident; let inputs = &input.sig.inputs; @@ -10,10 +10,33 @@ pub(crate) fn expand_simple(input: &syn::ItemFn, args: AttributeArgs) -> proc_ma let attrs = &input.attrs; let fn_name_str = name.to_string(); - let args = args.to_token_stream().to_string(); + let args_str = args.clone().to_token_stream().to_string(); + let parsed_attribute_args = parse_args(args)?; let simplex_test_env = simplex_test::TEST_ENV_NAME; let ident = format!("{input:#?}"); - quote::quote! { + let ok_path_generation = match parsed_attribute_args.config_option { + ConfigOpt::Config => { + quote! { + Ok(_) => { + let test_context = TestContextBuilder::Default.build().unwrap(); + tracing::trace!("Running '{}' with simplex configuration", #fn_name_str); + test_context + } + } + } + ConfigOpt::None => { + quote! { + Ok(path) => { + let path = PathBuf::from(path); + let test_context = TestContextBuilder::FromConfigPath(path).build().unwrap(); + tracing::trace!("Running '{}' with simplex configuration", #fn_name_str); + test_context + } + } + } + }; + + let expansion = quote::quote! { #[::core::prelude::v1::test] #(#attrs)* fn #name() #ret { @@ -32,20 +55,15 @@ pub(crate) fn expand_simple(input: &syn::ItemFn, args: AttributeArgs) -> proc_ma ); panic!("Failed to run this test, required to use `simplex test`"); } - Ok(path) => { - let path = PathBuf::from(path); - let test_context = TestContextBuilder::FromConfigPath(path).build().unwrap(); - tracing::trace!("Running '{}' with simplex configuration", #fn_name_str); - test_context - } + #ok_path_generation }; - // before - println!("fn name: {}, \n ident: {}", #fn_name_str, #ident); - println!("input: {}, \n AttributeArgs: {}", "", #args); + // println!("fn name: {}, \n ident: {}", #fn_name_str, #ident); + // println!("input: {}, \n AttributeArgs: {}", "", #args_str); #name(test_context) } - } + }; + Ok(expansion) } struct Args { @@ -63,247 +81,30 @@ pub fn expand(args: TokenStream, input: syn::ItemFn) -> syn::Result let parser = AttributeArgs::parse_terminated; let args = parser.parse2(args)?; - Ok(expand_simple(&input, args)) - - // if input.sig.inputs.is_empty() { - // if !args.is_empty() { - // if cfg!(not(feature = "migrate")) { - // return Err(syn::Error::new_spanned( - // args.first().unwrap(), - // "control attributes are not allowed unless \ - // the `migrate` feature is enabled and \ - // automatic test DB management is used; see docs", - // ) - // .into()); - // } - // - // return Err(syn::Error::new_spanned( - // args.first().unwrap(), - // "control attributes are not allowed unless \ - // automatic test DB management is used; see docs", - // ) - // .into()); - // } - // - // return Ok(expand_simple(&input, args)); - // } - // - // expand_advanced(args, input) + expand_inner(&input, args) } -// fn expand_advanced(args: AttributeArgs, input: syn::ItemFn) -> syn::Result { -// let config = sqlx_core::config::Config::try_from_crate_or_default()?; -// -// let ret = &input.sig.output; -// let name = &input.sig.ident; -// let inputs = &input.sig.inputs; -// let body = &input.block; -// let attrs = &input.attrs; -// -// let args = parse_args(args)?; -// -// let fn_arg_types = inputs.iter().map(|_| quote! { _ }); -// -// let mut fixtures = Vec::new(); -// -// for (fixture_type, fixtures_local) in args.fixtures { -// let mut res = match fixture_type { -// FixturesType::None => vec![], -// FixturesType::RelativePath => fixtures_local -// .into_iter() -// .map(|fixture| { -// let mut fixture_str = fixture.value(); -// add_sql_extension_if_missing(&mut fixture_str); -// -// let path = format!("fixtures/{}", fixture_str); -// -// quote! { -// ::sqlx::testing::TestFixture { -// path: #path, -// contents: include_str!(#path), -// } -// } -// }) -// .collect(), -// FixturesType::CustomRelativePath(path) => fixtures_local -// .into_iter() -// .map(|fixture| { -// let mut fixture_str = fixture.value(); -// add_sql_extension_if_missing(&mut fixture_str); -// -// let path = format!("{}/{}", path.value(), fixture_str); -// -// quote! { -// ::sqlx::testing::TestFixture { -// path: #path, -// contents: include_str!(#path), -// } -// } -// }) -// .collect(), -// FixturesType::ExplicitPath => fixtures_local -// .into_iter() -// .map(|fixture| { -// let path = fixture.value(); -// -// quote! { -// ::sqlx::testing::TestFixture { -// path: #path, -// contents: include_str!(#path), -// } -// } -// }) -// .collect(), -// }; -// fixtures.append(&mut res) -// } -// -// let migrations = match args.migrations { -// ConfigOpt::ExplicitPath(path) => { -// let migrator = crate::migrate::expand(Some(path))?; -// quote! { args.migrator(&#migrator); } -// } -// ConfigOpt::InferredPath if !inputs.is_empty() => { -// let path = crate::migrate::default_path(&config); -// -// let resolved_path = crate::common::resolve_path(path, proc_macro2::Span::call_site())?; -// -// if resolved_path.is_dir() { -// let migrator = crate::migrate::expand_with_path(&config, &resolved_path)?; -// quote! { args.migrator(&#migrator); } -// } else { -// quote! {} -// } -// } -// ConfigOpt::ExplicitMigrator(path) => { -// quote! { args.migrator(&#path); } -// } -// _ => quote! {}, -// }; -// -// Ok(quote! { -// #(#attrs)* -// #[::core::prelude::v1::test] -// fn #name() #ret { -// async fn #name(#inputs) #ret { -// #body -// } -// -// let mut args = ::sqlx::testing::TestArgs::new(concat!(module_path!(), "::", stringify!(#name))); -// -// #migrations -// -// args.fixtures(&[#(#fixtures),*]); -// -// // We need to give a coercion site or else we get "unimplemented trait" errors. -// let f: fn(#(#fn_arg_types),*) -> _ = #name; -// -// ::sqlx::testing::TestFn::run_test(f, args) -// } -// }) -// } +fn parse_args(attr_args: AttributeArgs) -> syn::Result { + if attr_args.is_empty() { + return Ok(Args { + config_option: ConfigOpt::Config, + }); + } + + if attr_args.len() > 1 { + return Err(syn::Error::new_spanned( + &attr_args, + "only a single `default_rpc` flag is allowed", + )); + } -// fn parse_args(attr_args: AttributeArgs) -> syn::Result { -// use syn::{ -// parenthesized, parse::Parse, punctuated::Punctuated, token::Comma, Expr, Lit, LitStr, Meta, MetaNameValue, -// Token, -// }; -// -// let mut fixtures = Vec::new(); -// let mut migrations = ConfigOpt::InferredPath; -// -// for arg in attr_args { -// let path = arg.path().clone(); -// -// match arg { -// syn::Meta::List(list) if list.path.is_ident("fixtures") => { -// let mut fixtures_local = vec![]; -// let mut fixtures_type = FixturesType::None; -// -// let parse_nested = list.parse_nested_meta(|meta| { -// if meta.path.is_ident("path") { -// // fixtures(path = "", scripts("","")) checking `path` argument -// meta.input.parse::()?; -// let val: LitStr = meta.input.parse()?; -// parse_fixtures_path_args(&mut fixtures_type, val)?; -// } else if meta.path.is_ident("scripts") { -// // fixtures(path = "", scripts("","")) checking `scripts` argument -// let content; -// parenthesized!(content in meta.input); -// let list = content.parse_terminated(::parse, Comma)?; -// parse_fixtures_scripts_args(&mut fixtures_type, list, &mut fixtures_local)?; -// } else { -// return Err(syn::Error::new_spanned(meta.path, "unexpected fixture meta")); -// } -// -// Ok(()) -// }); -// -// if parse_nested.is_err() { -// // fixtures("","") or fixtures("","") -// let args = list.parse_args_with(>::parse_terminated)?; -// for arg in args { -// parse_fixtures_args(&mut fixtures_type, arg, &mut fixtures_local)?; -// } -// } -// -// fixtures.push((fixtures_type, fixtures_local)); -// } -// syn::Meta::NameValue(value) if value.path.is_ident("migrations") => { -// if !matches!(migrations, ConfigOpt::InferredPath) { -// return Err(syn::Error::new_spanned( -// value, -// "cannot have more than one `migrations` or `migrator` arg", -// )); -// } -// -// fn recurse_lit_lookup(expr: Expr) -> Option { -// match expr { -// Expr::Lit(syn::ExprLit { lit, .. }) => Some(lit), -// Expr::Group(syn::ExprGroup { expr, .. }) => recurse_lit_lookup(*expr), -// _ => None, -// } -// } -// -// let Some(lit) = recurse_lit_lookup(value.value) else { -// return Err(syn::Error::new_spanned(path, "expected string or `false`")); -// }; -// -// migrations = match lit { -// // migrations = false -// Lit::Bool(b) if !b.value => ConfigOpt::Disabled, -// // migrations = true -// Lit::Bool(b) => { -// return Err(syn::Error::new_spanned(b, "`migrations = true` is redundant")); -// } -// // migrations = "path" -// Lit::Str(s) => ConfigOpt::ExplicitPath(s), -// lit => return Err(syn::Error::new_spanned(lit, "expected string or `false`")), -// }; -// } -// // migrator = "" -// Meta::NameValue(MetaNameValue { value, .. }) if path.is_ident("migrator") => { -// if !matches!(migrations, ConfigOpt::InferredPath) { -// return Err(syn::Error::new_spanned( -// path, -// "cannot have more than one `migrations` or `migrator` arg", -// )); -// } -// -// let Expr::Lit(syn::ExprLit { lit: Lit::Str(lit), .. }) = value else { -// return Err(syn::Error::new_spanned(path, "expected string")); -// }; -// -// migrations = ConfigOpt::ExplicitMigrator(lit.parse()?); -// } -// arg => { -// return Err(syn::Error::new_spanned( -// arg, -// r#"expected `fixtures("", ...)` or `migrations = "" | false` or `migrator = ""`"#, -// )); -// } -// } -// } -// -// Ok(Args { config_option: ConfigOpt::None }) -// } + match attr_args.iter().next().unwrap() { + syn::Meta::Path(path) if path.is_ident("default_rpc") => Ok(Args { + config_option: ConfigOpt::None, + }), + arg => Err(syn::Error::new_spanned( + arg, + "expected only the `default_rpc` flag with no assignment or value", + )), + } +} diff --git a/crates/simplex/tests/simplex_test.rs b/crates/simplex/tests/simplex_test.rs index 68b058b..0020a7f 100644 --- a/crates/simplex/tests/simplex_test.rs +++ b/crates/simplex/tests/simplex_test.rs @@ -6,11 +6,17 @@ use simplicityhl::elements::bitcoin::secp256k1; use simplicityhl::elements::secp256k1_zkp::Keypair; #[ignore] -#[simplex::simplex_macros::test(hello = "hi")] +#[simplex::simplex_macros::test(default_rpc)] fn test_execution(x: TestContext) { assert!(true) } +#[ignore] +#[simplex::simplex_macros::test] +fn test_execution3(x: TestContext) { + assert!(true) +} + #[ignore] #[test] fn test_execution2() { diff --git a/crates/test/src/testing/mod.rs b/crates/test/src/testing/mod.rs index 0d8c54c..88df0b4 100644 --- a/crates/test/src/testing/mod.rs +++ b/crates/test/src/testing/mod.rs @@ -13,12 +13,20 @@ pub struct TestContext { } pub enum TestContextBuilder { + Default, FromConfigPath(PathBuf), } impl TestContextBuilder { pub fn build(self) -> Result { let context = match self { + Self::Default => { + let rpc = TestRpcProvider::init(ConfigOption::DefaultRegtest)?; + TestContext { + config: TestConfig::default(), + rpc, + } + } Self::FromConfigPath(path) => { let config: TestConfig = TestConfig::from_file(&path)?; match &config.rpc_creds { From f05da0db05b3478a73894f808d36eb3715e16270 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Wed, 25 Feb 2026 12:11:05 +0200 Subject: [PATCH 11/14] Add examples for building and alignment of simplex contracts --- .gitignore | 2 +- Cargo.lock | 16 + Cargo.toml | 4 +- Simplex.toml | 4 + crates/cli/src/cache_storage.rs | 4 +- crates/cli/src/cli/mod.rs | 7 +- crates/cli/src/code_generator.rs | 60 +++ crates/cli/src/config.rs | 37 +- crates/cli/src/lib.rs | 1 + crates/macros/src/macros_core/attr/codegen.rs | 24 +- crates/macros/src/macros_core/attr/mod.rs | 24 +- crates/simplex/Cargo.toml | 2 +- crates/simplex/src/lib.rs | 3 + crates/simplex/tests/simplex_test.rs | 8 +- crates/test/src/testing/config.rs | 17 +- crates/test/src/testing/mod.rs | 28 +- crates/test/src/testing/rpc_provider.rs | 44 +- examples/options/Cargo.toml | 14 + examples/options/Simplex.toml | 5 + examples/options/simf/options.simf | 395 ++++++++++++++++++ examples/options/src/lib.rs | 3 + examples/options/src/program/mod.rs | 1 + examples/options/src/program/options.rs | 28 ++ examples/p2pk/Cargo.toml | 15 + examples/p2pk/Simplex.toml | 5 + examples/p2pk/out_dir/p2pk.rs | 28 ++ examples/p2pk/simf/p2pk.simf | 3 + examples/p2pk/src/lib.rs | 6 + 28 files changed, 685 insertions(+), 103 deletions(-) create mode 100644 Simplex.toml create mode 100644 crates/cli/src/code_generator.rs create mode 100644 examples/options/Cargo.toml create mode 100644 examples/options/Simplex.toml create mode 100644 examples/options/simf/options.simf create mode 100644 examples/options/src/lib.rs create mode 100644 examples/options/src/program/mod.rs create mode 100644 examples/options/src/program/options.rs create mode 100644 examples/p2pk/Cargo.toml create mode 100644 examples/p2pk/Simplex.toml create mode 100644 examples/p2pk/out_dir/p2pk.rs create mode 100644 examples/p2pk/simf/p2pk.simf create mode 100644 examples/p2pk/src/lib.rs diff --git a/.gitignore b/.gitignore index f2880e6..0b2a0d2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ .vscode/ .cache .env -Simplex.toml +./Simplex.toml config.toml # Debugging data diff --git a/Cargo.lock b/Cargo.lock index 855f271..8c58ae5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,12 +1193,28 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "options" +version = "0.1.0" +dependencies = [ + "simplex", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", +] + [[package]] name = "outref" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "p2pk" +version = "0.1.0" +dependencies = [ + "simplex", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", +] + [[package]] name = "percent-encoding" version = "2.3.2" diff --git a/Cargo.toml b/Cargo.toml index 3ae657b..67f5cc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,9 @@ [workspace] resolver = "3" members = [ - "crates/*" + "crates/*", + "examples/options", + "examples/p2pk" ] [workspace.package] diff --git a/Simplex.toml b/Simplex.toml new file mode 100644 index 0000000..562808f --- /dev/null +++ b/Simplex.toml @@ -0,0 +1,4 @@ +network = "liquidtestnet" + +[test] +exe_path = "../assets/bin" \ No newline at end of file diff --git a/crates/cli/src/cache_storage.rs b/crates/cli/src/cache_storage.rs index 6942f25..5fff7e9 100644 --- a/crates/cli/src/cache_storage.rs +++ b/crates/cli/src/cache_storage.rs @@ -1,5 +1,5 @@ use crate::error::Error; -use simplex_test::TestConfig; +use simplex_test::ElementsDConf; use std::fs::OpenOptions; use std::io::Write; use std::path::{Path, PathBuf}; @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; pub struct CacheStorage {} impl CacheStorage { - pub fn save_cached_test_config(test_config: &TestConfig) -> Result { + pub fn save_cached_test_config(test_config: &ElementsDConf) -> Result { let cache_dir = Self::get_cache_dir()?; std::fs::create_dir_all(&cache_dir)?; let test_config_cache_name = Self::create_test_cache_name(&cache_dir); diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index b3820f5..4013c03 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -62,6 +62,10 @@ impl Cli { Ok(()) } commands::Command::Regtest => { + let loaded_config = + Config::load_or_discover(self.config.clone()).map_err(|e| Error::ConfigDiscoveryFailure(e))?; + println!("{loaded_config:#?}"); + let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); @@ -70,7 +74,8 @@ impl Cli { }) .expect("Error setting Ctrl-C handler"); - let mut node = TestClientProvider::create_default_node_with_stdin(); + let mut node = + TestClientProvider::create_default_node_with_stdin(loaded_config.test_config.elemendsd_path); println!("======================================"); println!("Waiting for Ctrl-C..."); diff --git a/crates/cli/src/code_generator.rs b/crates/cli/src/code_generator.rs new file mode 100644 index 0000000..910caf9 --- /dev/null +++ b/crates/cli/src/code_generator.rs @@ -0,0 +1,60 @@ +// pub struct CodeGenerator<'a, 'b> { +// context: &'a mut Context<'b>, +// package: String, +// type_path: Vec, +// source_info: Option, +// syntax: Syntax, +// depth: u8, +// path: Vec, +// buf: &'a mut String, +// } +// +// #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +// #[derive(Clone, PartialEq, ::prost::Message)] +// pub struct FileDescriptorProto { +// /// file name, relative to root of source tree +// #[prost(string, optional, tag = "1")] +// pub name: ::core::option::Option<::prost::alloc::string::String>, +// /// e.g. "foo", "foo.bar", etc. +// #[prost(string, optional, tag = "2")] +// pub package: ::core::option::Option<::prost::alloc::string::String>, +// /// Names of files imported by this file. +// #[prost(string, repeated, tag = "3")] +// pub dependency: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +// /// Indexes of the public imported files in the dependency list above. +// #[prost(int32, repeated, packed = "false", tag = "10")] +// pub public_dependency: ::prost::alloc::vec::Vec, +// /// Indexes of the weak imported files in the dependency list. +// /// For Google-internal migration only. Do not use. +// #[prost(int32, repeated, packed = "false", tag = "11")] +// pub weak_dependency: ::prost::alloc::vec::Vec, +// /// All top-level definitions in this file. +// #[prost(message, repeated, tag = "4")] +// pub message_type: ::prost::alloc::vec::Vec, +// #[prost(message, repeated, tag = "5")] +// pub enum_type: ::prost::alloc::vec::Vec, +// #[prost(message, repeated, tag = "6")] +// pub service: ::prost::alloc::vec::Vec, +// #[prost(message, repeated, tag = "7")] +// pub extension: ::prost::alloc::vec::Vec, +// #[prost(message, optional, tag = "8")] +// pub options: ::core::option::Option, +// /// This field contains optional information about the original source code. +// /// You may safely remove this entire field without harming runtime +// /// functionality of the descriptors -- the information is needed only by +// /// development tools. +// #[prost(message, optional, tag = "9")] +// pub source_code_info: ::core::option::Option, +// /// The syntax of the proto file. +// /// The supported values are "proto2" and "proto3". +// #[prost(string, optional, tag = "12")] +// pub syntax: ::core::option::Option<::prost::alloc::string::String>, +// } +// +// impl<'b> CodeGenerator<'_, 'b> { +// fn config(&self) -> &Config { +// self.context.config() +// } +// +// pub(crate) fn generate(context: &mut Context<'b>, file: FileDescriptorProto, buf: &mut String) {} +// } diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index cd40853..3d7c753 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use simplex_sdk::constants::SimplicityNetwork; -use simplex_test::TestConfig; +use simplex_test::{ElementsDConf, RpcCreds}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -38,10 +38,10 @@ pub enum ConfigError { MissingConfig(PathBuf), } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct Config { pub provider_config: ProviderConfig, - pub test_config: TestConfig, + pub test_config: ElementsDConf, } #[derive(Debug, Clone)] @@ -51,7 +51,7 @@ pub struct ProviderConfig { #[derive(Debug, Default, Clone)] pub struct ConfigOverride { - pub rpc_creds: Option, + pub rpc_creds: Option, pub network: Option, } @@ -97,14 +97,6 @@ impl Config { } } - pub fn load_or_default(path_buf: Option>) -> Self { - let x = match path_buf { - Some(path) => Self::load(path), - None => Self::_discover(), - }; - x.unwrap_or_else(|_| Self::default()) - } - fn _discover() -> Result { let path = Self::get_path()?; dbg!(&path); @@ -131,7 +123,18 @@ impl FromStr for Config { provider_config: ProviderConfig { simplicity_network: cfg.network.unwrap_or_default().into(), }, - test_config: cfg.test.unwrap_or_default(), + test_config: cfg + .test + .map(|x| ElementsDConf { + elemendsd_path: x + .elementsd_path + .unwrap_or(ElementsDConf::obtain_default_elementsd_path()), + rpc_creds: x.rpc_creds.unwrap_or_default(), + }) + .unwrap_or(ElementsDConf { + elemendsd_path: ElementsDConf::obtain_default_elementsd_path(), + rpc_creds: RpcCreds::None, + }), }) } } @@ -139,7 +142,13 @@ impl FromStr for Config { #[derive(Debug, Serialize, Deserialize)] struct _Config { network: Option<_NetworkName>, - test: Option, + test: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct TestingConfig { + elementsd_path: Option, + rpc_creds: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index d0487d5..673c3d8 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,5 +1,6 @@ pub mod cache_storage; pub mod cli; +pub mod code_generator; pub mod config; pub mod error; pub mod logging; diff --git a/crates/macros/src/macros_core/attr/codegen.rs b/crates/macros/src/macros_core/attr/codegen.rs index 742d18f..24cb084 100644 --- a/crates/macros/src/macros_core/attr/codegen.rs +++ b/crates/macros/src/macros_core/attr/codegen.rs @@ -105,15 +105,15 @@ impl WitnessStruct { Ok(GeneratedArgumentTokens { imports: quote! { - use std::collections::HashMap; - use simplicityhl::{Arguments, Value, ResolvedType}; - use simplicityhl::value::{UIntValue, ValueInner}; - use simplicityhl::num::U256; - use simplicityhl::str::WitnessName; - use simplicityhl::types::TypeConstructible; - use simplicityhl::value::ValueConstructible; - use simplex_sdk::arguments::ArgumentsTrait; - use bincode::*; + use ::std::collections::HashMap; + use ::simplicityhl::{Arguments, Value, ResolvedType}; + use ::simplicityhl::value::{UIntValue, ValueInner}; + use ::simplicityhl::num::U256; + use ::simplicityhl::str::WitnessName; + use ::simplicityhl::types::TypeConstructible; + use ::simplicityhl::value::ValueConstructible; + use ::simplex::simplex_sdk::arguments::ArgumentsTrait; + use ::simplex::bincode::*; }, struct_token_stream: quote! { #generated_struct @@ -133,7 +133,7 @@ impl WitnessStruct { } - impl ::simplex_sdk::arguments::ArgumentsTrait for #struct_name { + impl ::simplex::simplex_sdk::arguments::ArgumentsTrait for #struct_name { /// Build Simplicity arguments for contract instantiation. #[must_use] fn build_arguments(&self) -> ::simplicityhl::Arguments { @@ -189,7 +189,7 @@ impl WitnessStruct { use simplicityhl::str::WitnessName; use simplicityhl::types::TypeConstructible; use simplicityhl::value::ValueConstructible; - use simplex_sdk::witness::WitnessTrait; + use ::simplex::simplex_sdk::witness::WitnessTrait; }, struct_token_stream: quote! { #generated_struct @@ -208,7 +208,7 @@ impl WitnessStruct { } } - impl ::simplex_sdk::witness::WitnessTrait for #struct_name { + impl ::simplex::simplex_sdk::witness::WitnessTrait for #struct_name { /// Build Simplicity witness values for contract execution. #[must_use] fn build_witness(&self) -> ::simplicityhl::WitnessValues { diff --git a/crates/macros/src/macros_core/attr/mod.rs b/crates/macros/src/macros_core/attr/mod.rs index 9b9ce47..e77e71c 100644 --- a/crates/macros/src/macros_core/attr/mod.rs +++ b/crates/macros/src/macros_core/attr/mod.rs @@ -47,12 +47,7 @@ fn gen_helpers_inner(simf_content: SimfContent, meta: AbiMeta) -> Result proc_macro2::TokenStream { let contract_content = &derived_meta.simf_content.content; - let error_msg = format!( - "INTERNAL: expected '{}' Program to compile successfully.", - derived_meta.simf_content.contract_name - ); let contract_source_name = &derived_meta.contract_source_const_name; - let contract_arguments_struct_name = &derived_meta.args_struct.struct_name; quote! { // use simplicityhl::elements::Address; @@ -60,7 +55,7 @@ fn construct_program_helpers(derived_meta: &SimfContractMeta) -> proc_macro2::To // use simplex::simplex_core::{create_p2tr_address, load_program, ProgramError, SimplicityNetwork}; // use simplicityhl::CompiledProgram; - // pub const #contract_source_name: &str = #contract_content; + pub const #contract_source_name: &str = #contract_content; /// Get the options template program for instantiation. /// @@ -71,23 +66,6 @@ fn construct_program_helpers(derived_meta: &SimfContractMeta) -> proc_macro2::To // ::simplicityhl::TemplateProgram::new(#contract_source_name).expect(#error_msg) // } - /// Derive P2TR address for an option offer contract. - /// - /// # Errors - /// - /// Returns error if program compilation fails. - // pub fn get_option_offer_address( - // x_only_public_key: &XOnlyPublicKey, - // arguments: &#contract_arguments_struct_name, - // network: SimplicityNetwork, - // ) -> Result { - // Ok(create_p2tr_address( - // get_loaded_program(arguments)?.commit().cmr(), - // x_only_public_key, - // network.address_params(), - // )) - // } - /// Compile option offer program with the given arguments. /// /// # Errors diff --git a/crates/simplex/Cargo.toml b/crates/simplex/Cargo.toml index df70a71..ca92a5c 100644 --- a/crates/simplex/Cargo.toml +++ b/crates/simplex/Cargo.toml @@ -16,7 +16,7 @@ workspace = true [features] default = ["macros", "encoding", "sdk"] -macros = ["dep:simplex-macros", "tracing"] +macros = ["dep:simplex-macros", "tracing", "encoding"] encoding = ["dep:bincode"] sdk = ["dep:simplex-sdk"] diff --git a/crates/simplex/src/lib.rs b/crates/simplex/src/lib.rs index 9875284..6311730 100644 --- a/crates/simplex/src/lib.rs +++ b/crates/simplex/src/lib.rs @@ -12,3 +12,6 @@ pub extern crate simplex_test; #[cfg(feature = "macros")] pub extern crate tracing; + +#[cfg(feature = "encoding")] +pub extern crate bincode; diff --git a/crates/simplex/tests/simplex_test.rs b/crates/simplex/tests/simplex_test.rs index 0020a7f..b4a3b4c 100644 --- a/crates/simplex/tests/simplex_test.rs +++ b/crates/simplex/tests/simplex_test.rs @@ -1,6 +1,6 @@ use simplex_provider::elements_rpc::{AddressType, ElementsRpcClient}; use simplex_sdk::constants::SimplicityNetwork; -use simplex_test::{DEFAULT_SAT_AMOUNT_FAUCET, TestContext}; +use simplex_test::{DEFAULT_SAT_AMOUNT_FAUCET, ElementsDConf, TestContext}; use simplicityhl::elements::Address; use simplicityhl::elements::bitcoin::secp256k1; use simplicityhl::elements::secp256k1_zkp::Keypair; @@ -109,7 +109,11 @@ fn test_invocation_tx_tracking() -> anyhow::Result<()> { } let network = SimplicityNetwork::default_regtest(); - let rpc = TestClientProvider::init(ConfigOption::DefaultRegtest).unwrap(); + let rpc = TestClientProvider::init( + ConfigOption::DefaultRegtest, + ElementsDConf::obtain_default_elementsd_path(), + ) + .unwrap(); { ElementsRpcClient::generate_blocks(rpc.as_ref(), 1).unwrap(); ElementsRpcClient::rescanblockchain(rpc.as_ref(), None, None).unwrap(); diff --git a/crates/test/src/testing/config.rs b/crates/test/src/testing/config.rs index bc71cf9..3ea2615 100644 --- a/crates/test/src/testing/config.rs +++ b/crates/test/src/testing/config.rs @@ -2,12 +2,13 @@ use crate::TestError; use electrsd::bitcoind::bitcoincore_rpc::jsonrpc::serde::{Deserialize, Serialize}; use std::fs::OpenOptions; use std::io::Read; -use std::path::Path; +use std::path::{Path, PathBuf}; pub const TEST_ENV_NAME: &str = "SIMPLEX_TEST_ENV"; -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct TestConfig { +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ElementsDConf { + pub elemendsd_path: PathBuf, pub rpc_creds: RpcCreds, } @@ -22,11 +23,19 @@ pub enum RpcCreds { None, } -impl TestConfig { +impl ElementsDConf { pub fn from_file(path: impl AsRef) -> Result { let mut content = String::new(); let mut file = OpenOptions::new().read(true).open(path)?; file.read_to_string(&mut content)?; Ok(toml::from_str(&content)?) } + + pub fn obtain_default_elementsd_path() -> PathBuf { + // TODO: change binary into installed one in $PATH dir + const ELEMENTSD_BIN_PATH: &str = "../../assets/elementsd"; + const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); + + Path::new(MANIFEST_DIR).join(ELEMENTSD_BIN_PATH) + } } diff --git a/crates/test/src/testing/mod.rs b/crates/test/src/testing/mod.rs index 88df0b4..8965b46 100644 --- a/crates/test/src/testing/mod.rs +++ b/crates/test/src/testing/mod.rs @@ -5,10 +5,11 @@ use crate::TestError; pub use config::*; use electrsd::bitcoind::bitcoincore_rpc::Auth; pub use rpc_provider::*; +use std::io; use std::path::PathBuf; pub struct TestContext { - config: TestConfig, + config: ElementsDConf, rpc: TestRpcProvider, } @@ -21,28 +22,35 @@ impl TestContextBuilder { pub fn build(self) -> Result { let context = match self { Self::Default => { - let rpc = TestRpcProvider::init(ConfigOption::DefaultRegtest)?; + let elementsd_path = ElementsDConf::obtain_default_elementsd_path(); + let rpc = TestRpcProvider::init(ConfigOption::DefaultRegtest, &elementsd_path)?; TestContext { - config: TestConfig::default(), + config: ElementsDConf { + elemendsd_path: elementsd_path, + rpc_creds: RpcCreds::None, + }, rpc, } } Self::FromConfigPath(path) => { - let config: TestConfig = TestConfig::from_file(&path)?; + let config: ElementsDConf = ElementsDConf::from_file(&path)?; match &config.rpc_creds { RpcCreds::Auth { url, username, password, } => { - let rpc = TestRpcProvider::init(ConfigOption::CustomRpcUrlRegtest { - url: url.clone(), - auth: Auth::UserPass(username.clone(), password.clone()), - })?; + let rpc = TestRpcProvider::init( + ConfigOption::CustomRpcUrlRegtest { + url: url.clone(), + auth: Auth::UserPass(username.clone(), password.clone()), + }, + &config.elemendsd_path, + )?; TestContext { config, rpc } } RpcCreds::None => { - let rpc = TestRpcProvider::init(ConfigOption::DefaultRegtest)?; + let rpc = TestRpcProvider::init(ConfigOption::DefaultRegtest, &config.elemendsd_path)?; TestContext { config, rpc } } } @@ -53,7 +61,7 @@ impl TestContextBuilder { } impl TestContext { - pub fn get_config(&self) -> &TestConfig { + pub fn get_config(&self) -> &ElementsDConf { &self.config } diff --git a/crates/test/src/testing/rpc_provider.rs b/crates/test/src/testing/rpc_provider.rs index 0a1d461..c1cb7a7 100644 --- a/crates/test/src/testing/rpc_provider.rs +++ b/crates/test/src/testing/rpc_provider.rs @@ -12,7 +12,7 @@ use simplicityhl::elements::Transaction; use simplicityhl::elements::hex::ToHex; use simplicityhl::elements::{Address, AssetId, BlockHash, Txid}; use std::collections::HashMap; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::str::FromStr; pub enum TestClientProvider { @@ -27,15 +27,15 @@ pub enum ConfigOption<'a> { } impl TestClientProvider { - pub fn init(init_option: ConfigOption) -> Result { + pub fn init(init_option: ConfigOption, elementsd_path: impl AsRef) -> Result { let rpc = match init_option { ConfigOption::DefaultRegtest => { - let node = Self::create_default_node(); + let node = Self::create_default_node(elementsd_path); let network = SimplicityNetwork::default_regtest(); Self::ConfiguredNode { node, network } } ConfigOption::CustomConfRegtest { conf } => { - let node = Self::create_node(conf, Self::get_bin_path())?; + let node = Self::create_node(conf, elementsd_path)?; let network = SimplicityNetwork::default_regtest(); Self::ConfiguredNode { node, network } } @@ -50,15 +50,7 @@ impl TestClientProvider { Ok(rpc) } - pub fn get_bin_path() -> PathBuf { - // TODO: change binary into installed one in $HOME dir - const ELEMENTSD_BIN_PATH: &str = "../../assets/elementsd"; - const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); - - Path::new(MANIFEST_DIR).join(ELEMENTSD_BIN_PATH) - } - - fn create_default_node() -> BitcoinD { + fn create_default_node(bin_path: impl AsRef) -> BitcoinD { let mut conf = Conf::default(); let bin_args = common::DefaultElementsdParams {}.get_bin_args(); @@ -66,10 +58,10 @@ impl TestClientProvider { conf.network = "liquidregtest"; conf.p2p = bitcoind::P2P::Yes; - BitcoinD::with_conf(Self::get_bin_path(), &conf).unwrap() + BitcoinD::with_conf(bin_path.as_ref(), &conf).unwrap() } - pub fn create_default_node_with_stdin() -> BitcoinD { + pub fn create_default_node_with_stdin(bin_path: impl AsRef) -> BitcoinD { let mut conf = Conf::default(); let bin_args = common::DefaultElementsdParams {}.get_bin_args(); @@ -79,11 +71,11 @@ impl TestClientProvider { conf.network = "liquidregtest"; conf.p2p = bitcoind::P2P::Yes; - BitcoinD::with_conf(Self::get_bin_path(), &conf).unwrap() + BitcoinD::with_conf(bin_path.as_ref(), &conf).unwrap() } - fn create_node(conf: Conf, bin_path: PathBuf) -> Result { - BitcoinD::with_conf(bin_path, &conf).map_err(|e| TestError::NodeFailedToStart(e.to_string())) + fn create_node(conf: Conf, bin_path: impl AsRef) -> Result { + BitcoinD::with_conf(bin_path.as_ref(), &conf).map_err(|e| TestError::NodeFailedToStart(e.to_string())) } pub fn client(&self) -> &Client { @@ -94,18 +86,6 @@ impl TestClientProvider { } } -impl TestClientProvider { - pub fn fund(satoshi: u64, address: Option
, asset: Option) { - todo!() - } - - pub fn get_height() {} - - pub fn get_blockchain_info() { - todo!() - } -} - impl AsRef for TestClientProvider { fn as_ref(&self) -> &Client { self.client() @@ -138,9 +118,9 @@ impl ProviderSync for TestRpcProvider { } impl TestRpcProvider { - pub fn init(init_option: ConfigOption) -> Result { + pub fn init(init_option: ConfigOption, bin_path: impl AsRef) -> Result { Ok(Self { - provider: TestClientProvider::init(init_option)?, + provider: TestClientProvider::init(init_option, bin_path)?, }) } diff --git a/examples/options/Cargo.toml b/examples/options/Cargo.toml new file mode 100644 index 0000000..b731907 --- /dev/null +++ b/examples/options/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "options" +version = "0.1.0" +license.workspace = true +edition.workspace = true + + +[lints] +workspace = true + +[dependencies] +simplex = { path = "./../../crates/simplex" } + +simplicityhl = { git = "https://github.com/ikripaka/SimplicityHL/", branch = "feature/rich-params" } diff --git a/examples/options/Simplex.toml b/examples/options/Simplex.toml new file mode 100644 index 0000000..6be1df5 --- /dev/null +++ b/examples/options/Simplex.toml @@ -0,0 +1,5 @@ +network = "liquidtestnet" + +[build] +compile-simf = ["simf/options.simf"] +out_dir = "./src/program" \ No newline at end of file diff --git a/examples/options/simf/options.simf b/examples/options/simf/options.simf new file mode 100644 index 0000000..e7da014 --- /dev/null +++ b/examples/options/simf/options.simf @@ -0,0 +1,395 @@ +/* + * Options + * + * Important: Currently only the LBTC collateral is supported. + * + * Based on the https://blockstream.com/assets/downloads/pdf/options-whitepaper.pdf + * + * This contract implements cash-settled European-style options using covenant-locked collateral. + * + * Room for optimization: + * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/2 (Use input asset to determine option covenent type) + * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/3 (Simplify match token_branch in funding_path.) + * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 (why batching is hard to implement) + * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/5 (Reduce Contract Parameters) + * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/21 (explains why funding is limited) + */ + +/// Assert: a == b * expected_q, via divmod +fn divmod_eq(a: u64, b: u64, expected_q: u64) { + let (q, r): (u64, u64) = jet::div_mod_64(a, b); + assert!(jet::eq_64(q, expected_q)); + assert!(jet::eq_64(r, 0)); +} + +fn get_output_script_hash(index: u32) -> u256 { + unwrap(jet::output_script_hash(index)) +} + +fn get_input_script_hash(index: u32) -> u256 { + unwrap(jet::input_script_hash(index)) +} + +fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } +fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } + +fn increment_by(index: u32, amount: u32) -> u32 { + let (carry, result): (bool, u32) = jet::add_32(index, amount); + ensure_zero_bit(carry); + result +} + +fn ensure_input_and_output_script_hash_eq(index: u32) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); +} + +fn ensure_output_is_op_return(index: u32) { + match jet::output_null_datum(index, 0) { + Some(entry: Option>>) => (), + None => panic!(), + } +} + +fn ensure_input_asset_eq(index: u32, expected_bits: u256) { + let asset: Asset1 = unwrap(jet::input_asset(index)); + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + assert!(jet::eq_256(asset_bits, expected_bits)); +} + +fn ensure_output_asset_eq(index: u32, expected_bits: u256) { + let asset: Asset1 = unwrap(jet::output_asset(index)); + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + assert!(jet::eq_256(asset_bits, expected_bits)); +} + +fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { + let (asset, amount): (u256, u64) = dbg!(get_output_explicit_asset_amount(index)); + assert!(jet::eq_256(asset, expected_bits)); + assert!(jet::eq_64(amount, expected_amount)); +} + +fn ensure_input_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected)); +} + +fn ensure_output_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); +} + +fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { + let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); + assert!(jet::eq_32(jet::current_index(), index)); + + match is_change_needed { + true => { + ensure_input_and_output_script_hash_eq(index); + + let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); + ensure_zero_bit(carry); + ensure_output_asset_with_amount_eq(index, asset_id, collateral_change); + }, + false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), + } +} + +fn check_y(expected_y: Fe, actual_y: Fe) { + match jet::eq_256(expected_y, actual_y) { + true => {}, + false => { + assert!(jet::eq_256(expected_y, jet::fe_negate(actual_y))); + } + }; +} + +fn ensure_input_and_output_reissuance_token_eq(index: u32) { + let (input_asset, input_amount): (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (output_asset, output_amount): (Asset1, Amount1) = unwrap(jet::output_amount(index)); + + match (input_asset) { + Left(in_conf: Point) => { + let (input_asset_parity, input_asset_x): (u1, u256) = in_conf; + let (output_asset_parity, output_asset_x): (u1, u256) = unwrap_left::(output_asset); + + assert!(jet::eq_1(input_asset_parity, output_asset_parity)); + assert!(jet::eq_256(input_asset_x, output_asset_x)); + }, + Right(in_expl: u256) => { + let out_expl: u256 = unwrap_right::(output_asset); + assert!(jet::eq_256(in_expl, out_expl)); + } + }; + + match (input_amount) { + Left(in_conf: Point) => { + let (input_amount_parity, input_amount_x): (u1, u256) = in_conf; + let (output_amount_parity, output_amount_x): (u1, u256) = unwrap_left::(output_amount); + + assert!(jet::eq_1(input_amount_parity, output_amount_parity)); + assert!(jet::eq_256(input_amount_x, output_amount_x)); + }, + Right(in_expl: u64) => { + let out_expl: u64 = unwrap_right::(output_amount); + assert!(jet::eq_64(in_expl, out_expl)); + } + }; +} + +// Verify that a reissuance token commitment matches the expected token ID using provided blinding factors. +// Reissuance tokens are confidential because, in Elements, +// the asset must be provided in blinded form in order to reissue tokens. +// https://github.com/BlockstreamResearch/simplicity-contracts/issues/21#issuecomment-3691599583 +fn verify_token_commitment(actual_asset: Asset1, actual_amount: Amount1, expected_token_id: u256, abf: u256, vbf: u256) { + match actual_asset { + Left(conf_token: Point) => { + let amount_scalar: u256 = 1; + let (actual_ax, actual_ay): Ge = unwrap(jet::decompress(conf_token)); + + let gej_point: Gej = (jet::hash_to_curve(expected_token_id), 1); + let asset_blind_point: Gej = jet::generate(abf); + + let asset_generator: Gej = jet::gej_add(gej_point, asset_blind_point); + let (ax, ay): Ge = unwrap(jet::gej_normalize(asset_generator)); + + assert!(jet::eq_256(actual_ax, ax)); + check_y(actual_ay, ay); + + // Check amount + let conf_val: Point = unwrap_left::(actual_amount); + let (actual_vx, actual_vy): Ge = unwrap(jet::decompress(conf_val)); + + let amount_part: Gej = jet::scale(amount_scalar, asset_generator); + let vbf_part: Gej = jet::generate(vbf); + + let value_generator: Gej = jet::gej_add(amount_part, vbf_part); + let (vx, vy): Ge = unwrap(jet::gej_normalize(value_generator)); + + assert!(jet::eq_256(actual_vx, vx)); + check_y(actual_vy, vy); + }, + Right(reissuance_token: u256) => { + let expected_amount: u64 = 1; + let actual_amount: u64 = unwrap_right::(actual_amount); + + assert!(jet::eq_64(expected_amount, actual_amount)); + assert!(jet::eq_256(reissuance_token, expected_token_id)); + } + }; +} + +fn verify_output_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) { + let (asset, amount): (Asset1, Amount1) = unwrap(jet::output_amount(index)); + verify_token_commitment(asset, amount, expected_token_id, abf, vbf); +} + +fn verify_input_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) { + let (asset, amount): (Asset1, Amount1) = unwrap(jet::input_amount(index)); + verify_token_commitment(asset, amount, expected_token_id, abf, vbf); +} + +/* + * Funding Path + */ +fn funding_path( + expected_asset_amount: u64, + input_option_abf: u256, + input_option_vbf: u256, + input_grantor_abf: u256, + input_grantor_vbf: u256, + output_option_abf: u256, + output_option_vbf: u256, + output_grantor_abf: u256, + output_grantor_vbf: u256 +) { + ensure_input_and_output_script_hash_eq(0); + ensure_input_and_output_script_hash_eq(1); + + verify_input_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, input_option_abf, input_option_vbf); + verify_input_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, input_grantor_abf, input_grantor_vbf); + + verify_output_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, output_option_abf, output_option_vbf); + verify_output_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, output_grantor_abf, output_grantor_vbf); + + assert!(dbg!(jet::eq_256(get_output_script_hash(0), get_output_script_hash(1)))); + + assert!(jet::le_32(jet::current_index(), 1)); + + ensure_output_script_hash_eq(2, get_output_script_hash(0)); + + let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(2); + let option_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); + let grantor_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); + assert!(jet::eq_64(option_token_amount, grantor_token_amount)); + + divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, option_token_amount); + divmod_eq(expected_asset_amount, param::SETTLEMENT_PER_CONTRACT, option_token_amount); + + ensure_output_asset_with_amount_eq(2, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(3, param::OPTION_TOKEN_ASSET, option_token_amount); + ensure_output_asset_with_amount_eq(4, param::GRANTOR_TOKEN_ASSET, grantor_token_amount); +} + +/* + * Cancellation Path + */ +fn cancellation_path(amount_to_burn: u64, collateral_amount_to_withdraw: u64, is_change_needed: bool) { + let collateral_input_index: u32 = 0; + let option_input_index: u32 = 1; + let grantor_input_index: u32 = 2; + + let (burn_option_output_index, burn_grantor_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_withdraw, expected_current_script_hash, is_change_needed); + + // Burn option and grantor tokens + ensure_output_is_op_return(burn_option_output_index); + ensure_output_is_op_return(burn_grantor_output_index); + + ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, amount_to_burn); + ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, amount_to_burn); + + // Ensure returned collateral amount is correct + divmod_eq(collateral_amount_to_withdraw, param::COLLATERAL_PER_CONTRACT, amount_to_burn); +} + +/* + * Exercise Path + */ +fn exercise_path(option_amount_to_burn: u64, collateral_amount_to_get: u64, asset_amount_to_pay: u64, is_change_needed: bool) { + jet::check_lock_time(param::START_TIME); + + let collateral_input_index: u32 = 0; + + let (burn_option_output_index, asset_to_covenant_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(collateral_amount_to_get, param::COLLATERAL_PER_CONTRACT, option_amount_to_burn); + divmod_eq(asset_amount_to_pay, param::SETTLEMENT_PER_CONTRACT, option_amount_to_burn); + + // Burn option token + ensure_output_is_op_return(burn_option_output_index); + ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, option_amount_to_burn); + + // Ensure settlement asset and script hash are correct + ensure_output_asset_with_amount_eq(asset_to_covenant_output_index, param::SETTLEMENT_ASSET_ID, asset_amount_to_pay); + ensure_output_script_hash_eq(asset_to_covenant_output_index, expected_current_script_hash); +} + +/* + * Settlement Path + */ +fn settlement_path(grantor_token_amount_to_burn: u64, asset_amount: u64, is_change_needed: bool) { + jet::check_lock_time(param::START_TIME); + + let target_asset_input_index: u32 = 0; + + let burn_grantor_output_index: u32 = match is_change_needed { + true => 1, + false => 0, + }; + + let expected_current_script_hash: u256 = get_input_script_hash(target_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, asset_amount, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset and grantor token amounts are correct + divmod_eq(asset_amount, param::SETTLEMENT_PER_CONTRACT, grantor_token_amount_to_burn); + + // Burn grantor token + ensure_output_is_op_return(burn_grantor_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn); +} + +/* + * Expiry Path + */ +fn expiry_path(grantor_token_amount_to_burn: u64, collateral_amount: u64, is_change_needed: bool) { + jet::check_lock_time(param::EXPIRY_TIME); + + let collateral_input_index: u32 = 0; + + let burn_grantor_output_index: u32 = match is_change_needed { + true => 1, + false => 0, + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_current_script_hash, is_change_needed); + + // Ensure collateral amount is correct + divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, grantor_token_amount_to_burn); + + // Burn grantor token + ensure_output_is_op_return(burn_grantor_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn); +} + +fn main() { + match witness::PATH { + Left(left_or_right: Either<(u64, u256, u256, u256, u256, u256, u256, u256, u256), Either<(bool, u64, u64, u64), (bool, u64, u64)>>) => match left_or_right { + Left(params: (u64, u256, u256, u256, u256, u256, u256, u256, u256)) => { + let (expected_asset_amount, input_option_abf, input_option_vbf, input_grantor_abf, input_grantor_vbf, output_option_abf, output_option_vbf, output_grantor_abf, output_grantor_vbf): (u64, u256, u256, u256, u256, u256, u256, u256, u256) = params; + funding_path( + expected_asset_amount, + input_option_abf, input_option_vbf, + input_grantor_abf, input_grantor_vbf, + output_option_abf, output_option_vbf, + output_grantor_abf, output_grantor_vbf + ); + }, + Right(exercise_or_settlement: Either<(bool, u64, u64, u64), (bool, u64, u64)>) => match exercise_or_settlement { + Left(params: (bool, u64, u64, u64)) => { + let (is_change_needed, amount_to_burn, collateral_amount, asset_amount): (bool, u64, u64, u64) = dbg!(params); + exercise_path(amount_to_burn, collateral_amount, asset_amount, is_change_needed) + }, + Right(params: (bool, u64, u64)) => { + let (is_change_needed, amount_to_burn, asset_amount): (bool, u64, u64) = dbg!(params); + settlement_path(amount_to_burn, asset_amount, is_change_needed) + }, + }, + }, + Right(left_or_right: Either<(bool, u64, u64), (bool, u64, u64)>) => match left_or_right { + Left(params: (bool, u64, u64)) => { + let (is_change_needed, grantor_token_amount_to_burn, collateral_amount): (bool, u64, u64) = params; + expiry_path(grantor_token_amount_to_burn, collateral_amount, is_change_needed) + }, + Right(params: (bool, u64, u64)) => { + let (is_change_needed, amount_to_burn, collateral_amount): (bool, u64, u64) = params; + cancellation_path(amount_to_burn, collateral_amount, is_change_needed) + }, + }, + } +} diff --git a/examples/options/src/lib.rs b/examples/options/src/lib.rs new file mode 100644 index 0000000..f54c04a --- /dev/null +++ b/examples/options/src/lib.rs @@ -0,0 +1,3 @@ +mod program; + +pub use program::*; diff --git a/examples/options/src/program/mod.rs b/examples/options/src/program/mod.rs new file mode 100644 index 0000000..66dd779 --- /dev/null +++ b/examples/options/src/program/mod.rs @@ -0,0 +1 @@ +pub mod options; diff --git a/examples/options/src/program/options.rs b/examples/options/src/program/options.rs new file mode 100644 index 0000000..05e8e0d --- /dev/null +++ b/examples/options/src/program/options.rs @@ -0,0 +1,28 @@ +use simplex::simplex_macros::include_simf; +use simplex::simplex_sdk::arguments::ArgumentsTrait; +use simplex::simplex_sdk::program::Program; +use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; + +pub struct Options<'a> { + program: Program<'a>, +} + +impl<'a> Options<'a> { + pub const SOURCE: &'static str = derived_options::OPTIONS_CONTRACT_SOURCE; + + pub fn new(public_key: &'a XOnlyPublicKey, arguments: &'a impl ArgumentsTrait) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, arguments), + } + } + + pub fn get_program(&self) -> &Program<'a> { + &self.program + } + + pub fn get_program_mut(&mut self) -> &mut Program<'a> { + &mut self.program + } +} + +include_simf!("./simf/options.simf"); diff --git a/examples/p2pk/Cargo.toml b/examples/p2pk/Cargo.toml new file mode 100644 index 0000000..1ef4404 --- /dev/null +++ b/examples/p2pk/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "p2pk" +version = "0.1.0" +license.workspace = true +edition.workspace = true + + +[lints] +workspace = true + + +[dependencies] +simplex = { path = "./../../crates/simplex" } + +simplicityhl = { git = "https://github.com/ikripaka/SimplicityHL/", branch = "feature/rich-params" } \ No newline at end of file diff --git a/examples/p2pk/Simplex.toml b/examples/p2pk/Simplex.toml new file mode 100644 index 0000000..6f095fb --- /dev/null +++ b/examples/p2pk/Simplex.toml @@ -0,0 +1,5 @@ +network = "liquidtestnet" + +[build] +compile-simf = ["simf/p2pk.simf"] +out_dir = "./out_dir" \ No newline at end of file diff --git a/examples/p2pk/out_dir/p2pk.rs b/examples/p2pk/out_dir/p2pk.rs new file mode 100644 index 0000000..8210b7a --- /dev/null +++ b/examples/p2pk/out_dir/p2pk.rs @@ -0,0 +1,28 @@ +use simplex::simplex_macros::include_simf; +use simplex::simplex_sdk::arguments::ArgumentsTrait; +use simplex::simplex_sdk::program::Program; +use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; + +pub struct P2PK<'a> { + program: Program<'a>, +} + +impl<'a> P2PK<'a> { + pub const SOURCE: &'static str = derived_p2pk::P2PK_CONTRACT_SOURCE; + + pub fn new(public_key: &'a XOnlyPublicKey, arguments: &'a impl ArgumentsTrait) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, arguments), + } + } + + pub fn get_program(&self) -> &Program<'a> { + &self.program + } + + pub fn get_program_mut(&mut self) -> &mut Program<'a> { + &mut self.program + } +} + +include_simf!("./simf/p2pk.simf"); diff --git a/examples/p2pk/simf/p2pk.simf b/examples/p2pk/simf/p2pk.simf new file mode 100644 index 0000000..db4f27c --- /dev/null +++ b/examples/p2pk/simf/p2pk.simf @@ -0,0 +1,3 @@ +fn main() { + jet::bip_0340_verify((param::PUBLIC_KEY, jet::sig_all_hash()), witness::SIGNATURE) +} \ No newline at end of file diff --git a/examples/p2pk/src/lib.rs b/examples/p2pk/src/lib.rs new file mode 100644 index 0000000..3a3024c --- /dev/null +++ b/examples/p2pk/src/lib.rs @@ -0,0 +1,6 @@ +pub use simplex; + +mod p2pk_program { + include!(concat!("../out_dir", "/p2pk.rs")); +} +pub use p2pk_program::*; From d5d7e9c6e28b2545793531524dc2af1900ab4fb2 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Wed, 25 Feb 2026 14:33:28 +0200 Subject: [PATCH 12/14] Add and edit examples --- .gitignore | 2 +- Cargo.lock | 15 ++++- Cargo.toml | 3 +- crates/cli/Cargo.toml | 1 + crates/cli/src/cli/commands.rs | 3 +- crates/cli/src/cli/mod.rs | 8 ++- crates/cli/src/config.rs | 80 +++++++++++++++++++++-- crates/macros/src/macros_core/test/mod.rs | 2 +- examples/options/Cargo.toml | 4 +- examples/options/Simplex.toml | 7 +- examples/options/src/program/options.rs | 4 +- examples/p2pk/Cargo.toml | 4 +- examples/p2pk/Simplex.toml | 7 +- examples/p2pk/out_dir/p2pk.rs | 2 +- examples/test_usage/Cargo.toml | 17 +++++ examples/test_usage/Simplex.toml | 4 ++ examples/test_usage/src/lib.rs | 1 + examples/test_usage/tests/draft_test.rs | 74 +++++++++++++++++++++ 18 files changed, 215 insertions(+), 23 deletions(-) create mode 100644 examples/test_usage/Cargo.toml create mode 100644 examples/test_usage/Simplex.toml create mode 100644 examples/test_usage/src/lib.rs create mode 100644 examples/test_usage/tests/draft_test.rs diff --git a/.gitignore b/.gitignore index 0b2a0d2..82cb3bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Build output -/target/ +target/ # IDE/editors (optional but common minimal) **/.DS_Store diff --git a/Cargo.lock b/Cargo.lock index 8c58ae5..a084bb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,9 +69,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "ar_archive_writer" @@ -485,6 +485,16 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "draft_example" +version = "0.1.0" +dependencies = [ + "anyhow", + "simplex", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", + "simplicityhl-core", +] + [[package]] name = "either" version = "1.15.0" @@ -1866,6 +1876,7 @@ dependencies = [ "ctrlc", "dotenvy", "electrsd", + "glob", "serde", "simplex-sdk", "simplex-test", diff --git a/Cargo.toml b/Cargo.toml index 67f5cc7..3d67899 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ resolver = "3" members = [ "crates/*", "examples/options", - "examples/p2pk" + "examples/p2pk", + "examples/test_usage", ] [workspace.package] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 475853a..aa590d8 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -30,6 +30,7 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] } tracing = { version = "0.1.44" } tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } ctrlc = { version = "3.5.2", features = ["termination"] } +glob = { version = "0.3.3"} [dev-dependencies] tracing = { workspace = true } diff --git a/crates/cli/src/cli/commands.rs b/crates/cli/src/cli/commands.rs index ba95585..f3d31d3 100644 --- a/crates/cli/src/cli/commands.rs +++ b/crates/cli/src/cli/commands.rs @@ -2,7 +2,7 @@ use clap::{Args, Subcommand}; #[derive(Debug, Subcommand)] pub enum Command { - /// Initialize project wiht default configuration + /// Initialize a project with the default configuration Init, /// Show the current configuration Config, @@ -13,6 +13,7 @@ pub enum Command { #[command(subcommand)] command: TestCommand, }, + Build, } /// Test management commands diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 4013c03..5edb095 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -1,7 +1,7 @@ pub mod commands; use crate::cache_storage::CacheStorage; -use crate::cli::commands::{TestCommand, TestFlags}; +use crate::cli::commands::{Command, TestCommand, TestFlags}; use crate::config::{Config, DEFAULT_CONFIG}; use crate::error::Error; use clap::Parser; @@ -89,6 +89,12 @@ impl Cli { println!("Exiting..."); Ok(()) } + Command::Build => { + let loaded_config = + Config::load_or_discover(self.config.clone()).map_err(|e| Error::ConfigDiscoveryFailure(e))?; + println!("{loaded_config:#?}"); + Ok(()) + } } } diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index 3d7c753..14abc31 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use simplex_sdk::constants::SimplicityNetwork; use simplex_test::{ElementsDConf, RpcCreds}; +use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -40,12 +41,13 @@ pub enum ConfigError { #[derive(Debug, Clone)] pub struct Config { - pub provider_config: ProviderConfig, + pub provider_config: ProviderConf, pub test_config: ElementsDConf, + pub build_config: Option, } #[derive(Debug, Clone)] -pub struct ProviderConfig { +pub struct ProviderConf { simplicity_network: SimplicityNetwork, } @@ -55,9 +57,21 @@ pub struct ConfigOverride { pub network: Option, } -impl Default for ProviderConfig { +#[derive(Debug, Clone)] +pub struct BuildConf { + compile_simf: Vec, + out_dir: PathBuf, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct _BuildConf { + compile_simf: Vec, + out_dir: PathBuf, +} + +impl Default for ProviderConf { fn default() -> Self { - ProviderConfig { + ProviderConf { simplicity_network: SimplicityNetwork::LiquidTestnet, } } @@ -120,7 +134,7 @@ impl FromStr for Config { fn from_str(s: &str) -> Result { let cfg: _Config = toml::from_str(s).map_err(ConfigError::UnableToDeserialize)?; Ok(Config { - provider_config: ProviderConfig { + provider_config: ProviderConf { simplicity_network: cfg.network.unwrap_or_default().into(), }, test_config: cfg @@ -135,6 +149,13 @@ impl FromStr for Config { elemendsd_path: ElementsDConf::obtain_default_elementsd_path(), rpc_creds: RpcCreds::None, }), + build_config: match cfg.build { + None => None, + Some(x) => Some(BuildConf { + compile_simf: resolve_glob_paths(&x.compile_simf)?, + out_dir: resolve_dir_path(x.out_dir)?, + }), + }, }) } } @@ -143,6 +164,7 @@ impl FromStr for Config { struct _Config { network: Option<_NetworkName>, test: Option, + build: Option<_BuildConf>, } #[derive(Debug, Serialize, Deserialize)] @@ -169,3 +191,51 @@ impl Into for _NetworkName { } } } + +fn resolve_glob_paths(pattern: &[impl AsRef]) -> io::Result> { + let mut paths = Vec::new(); + for path in pattern.iter().map(|x| resolve_glob_path(x.as_ref())) { + let path = path?; + paths.extend_from_slice(&path); + } + Ok(paths) +} + +fn resolve_glob_path(pattern: impl AsRef) -> io::Result> { + let mut paths = Vec::new(); + for path in glob::glob(pattern.as_ref()) + .map_err(|e| io::Error::other(e.to_string()))? + .filter_map(Result::ok) + { + println!("path: '{}', pattern: '{}'", path.display(), pattern.as_ref()); + paths.push(path); + } + Ok(paths) +} + +fn resolve_dir_path(path: impl AsRef) -> io::Result { + let mut path_outer = PathBuf::from(path.as_ref()); + + if !path_outer.is_absolute() { + let manifest_dir = std::env::current_dir()?; + + let mut path_local = PathBuf::from(manifest_dir); + path_local.push(path_outer); + + path_outer = path_local; + } + + if path_outer.extension().is_some() { + return Err(io::Error::other(format!( + "Folder can't have an extension, path: '{}'", + path_outer.display() + ))); + } + if path_outer.is_file() { + return Err(io::Error::other(format!( + "Folder can't be a path, path: '{}'", + path_outer.display() + ))); + } + Ok(path_outer) +} diff --git a/crates/macros/src/macros_core/test/mod.rs b/crates/macros/src/macros_core/test/mod.rs index 8728ad8..d255f48 100644 --- a/crates/macros/src/macros_core/test/mod.rs +++ b/crates/macros/src/macros_core/test/mod.rs @@ -42,7 +42,7 @@ pub(crate) fn expand_inner(input: &syn::ItemFn, args: AttributeArgs) -> syn::Res fn #name() #ret { use ::simplex::tracing; use ::std::path::PathBuf; - use ::simplex_test::TestContextBuilder; + use ::simplex::simplex_test::TestContextBuilder; fn #name(#inputs) #ret { #body diff --git a/examples/options/Cargo.toml b/examples/options/Cargo.toml index b731907..72ab800 100644 --- a/examples/options/Cargo.toml +++ b/examples/options/Cargo.toml @@ -9,6 +9,6 @@ edition.workspace = true workspace = true [dependencies] -simplex = { path = "./../../crates/simplex" } +simplex = { workspace = true } -simplicityhl = { git = "https://github.com/ikripaka/SimplicityHL/", branch = "feature/rich-params" } +simplicityhl = { workspace = true } diff --git a/examples/options/Simplex.toml b/examples/options/Simplex.toml index 6be1df5..cccff48 100644 --- a/examples/options/Simplex.toml +++ b/examples/options/Simplex.toml @@ -1,5 +1,8 @@ network = "liquidtestnet" [build] -compile-simf = ["simf/options.simf"] -out_dir = "./src/program" \ No newline at end of file +compile_simf = ["simf/*.simf"] +out_dir = "./src/program" + +[tests] +elementsd_path = "../../assets/elementsd" \ No newline at end of file diff --git a/examples/options/src/program/options.rs b/examples/options/src/program/options.rs index 05e8e0d..8316eb2 100644 --- a/examples/options/src/program/options.rs +++ b/examples/options/src/program/options.rs @@ -1,7 +1,7 @@ use simplex::simplex_macros::include_simf; use simplex::simplex_sdk::arguments::ArgumentsTrait; use simplex::simplex_sdk::program::Program; -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; +use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; pub struct Options<'a> { program: Program<'a>, @@ -25,4 +25,4 @@ impl<'a> Options<'a> { } } -include_simf!("./simf/options.simf"); +include_simf!("simf/options.simf"); diff --git a/examples/p2pk/Cargo.toml b/examples/p2pk/Cargo.toml index 1ef4404..013ce34 100644 --- a/examples/p2pk/Cargo.toml +++ b/examples/p2pk/Cargo.toml @@ -10,6 +10,6 @@ workspace = true [dependencies] -simplex = { path = "./../../crates/simplex" } +simplex = { workspace = true} -simplicityhl = { git = "https://github.com/ikripaka/SimplicityHL/", branch = "feature/rich-params" } \ No newline at end of file +simplicityhl = { workspace = true } \ No newline at end of file diff --git a/examples/p2pk/Simplex.toml b/examples/p2pk/Simplex.toml index 6f095fb..1ebb2e9 100644 --- a/examples/p2pk/Simplex.toml +++ b/examples/p2pk/Simplex.toml @@ -1,5 +1,8 @@ network = "liquidtestnet" [build] -compile-simf = ["simf/p2pk.simf"] -out_dir = "./out_dir" \ No newline at end of file +compile_simf = ["simf/*.simf"] +out_dir = "./out_dir" + +[tests] +elementsd_path = "../../assets/elementsd" \ No newline at end of file diff --git a/examples/p2pk/out_dir/p2pk.rs b/examples/p2pk/out_dir/p2pk.rs index 8210b7a..1fb0c9e 100644 --- a/examples/p2pk/out_dir/p2pk.rs +++ b/examples/p2pk/out_dir/p2pk.rs @@ -1,7 +1,7 @@ use simplex::simplex_macros::include_simf; use simplex::simplex_sdk::arguments::ArgumentsTrait; use simplex::simplex_sdk::program::Program; -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; +use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; pub struct P2PK<'a> { program: Program<'a>, diff --git a/examples/test_usage/Cargo.toml b/examples/test_usage/Cargo.toml new file mode 100644 index 0000000..68c46e4 --- /dev/null +++ b/examples/test_usage/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "draft_example" +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +simplex = { workspace = true } + +simplicityhl = { workspace = true } + +[dev-dependencies] +anyhow = { version = "1.0.102" } +simplicityhl-core = { version = "0.4.2" } \ No newline at end of file diff --git a/examples/test_usage/Simplex.toml b/examples/test_usage/Simplex.toml new file mode 100644 index 0000000..675755b --- /dev/null +++ b/examples/test_usage/Simplex.toml @@ -0,0 +1,4 @@ +network = "liquidtestnet" + +[tests] +elementsd_path = "../../assets/elementsd" \ No newline at end of file diff --git a/examples/test_usage/src/lib.rs b/examples/test_usage/src/lib.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/test_usage/src/lib.rs @@ -0,0 +1 @@ + diff --git a/examples/test_usage/tests/draft_test.rs b/examples/test_usage/tests/draft_test.rs new file mode 100644 index 0000000..e1967a2 --- /dev/null +++ b/examples/test_usage/tests/draft_test.rs @@ -0,0 +1,74 @@ +use simplex::simplex_sdk::constants::SimplicityNetwork; +use simplex::simplex_test::{AddressType, ElementsRpcClient}; +use simplex::simplex_test::{ConfigOption, TestClientProvider}; +use simplex::simplex_test::{DEFAULT_SAT_AMOUNT_FAUCET, ElementsDConf}; +use simplicityhl::elements::Address; +use simplicityhl::elements::bitcoin::secp256k1; +use simplicityhl::elements::secp256k1_zkp::Keypair; + +#[ignore] +#[simplex::simplex_macros::test] +fn test_invocation_tx_tracking(test_context: simplex::simplex_test::TestContext) -> anyhow::Result<()> { + let network = SimplicityNetwork::default_regtest(); + test_context.default_rpc_setup()?; + + let rpc_provider = test_context.get_rpc_provider(); + + let user1_addr = rpc_provider.getnewaddress("", AddressType::default()).unwrap(); + let user2_addr = rpc_provider.getnewaddress("", AddressType::default()).unwrap(); + test_context.get_rpc_provider().sendtoaddress( + &user1_addr, + DEFAULT_SAT_AMOUNT_FAUCET, + Some(network.policy_asset()), + )?; + + test_context.get_rpc_provider().sendtoaddress( + &user2_addr, + DEFAULT_SAT_AMOUNT_FAUCET, + Some(network.policy_asset()), + )?; + + test_context.get_rpc_provider().generate_blocks(3)?; + dbg!(test_context.get_rpc_provider().listunspent( + None, + None, + Some(vec![user1_addr.to_string(), user2_addr.to_string()]), + None, + None, + )?,); + + { + let network = SimplicityNetwork::default_regtest(); + let keypair = Keypair::from_seckey_slice(&secp256k1::SECP256K1, &[1; 32])?; + let p2pk = simplicityhl_core::get_p2pk_address( + &keypair.x_only_public_key().0, + simplicityhl_core::SimplicityNetwork::default_regtest(), + )?; + + dbg!(p2pk.to_string()); + + dbg!(test_context.get_rpc_provider().validateaddress(&p2pk.to_string())?); + + let result = test_context.get_rpc_provider().sendtoaddress( + &p2pk, + DEFAULT_SAT_AMOUNT_FAUCET, + Some(network.policy_asset()), + )?; + + test_context.get_rpc_provider().generate_blocks(5)?; + + dbg!( + test_context + .get_rpc_provider() + .listunspent(None, None, Some(vec![p2pk.to_string()]), None, None,)?, + ); + + dbg!( + test_context + .get_rpc_provider() + .scantxoutset("start", Some(vec![format!("addr({})", p2pk)]),)?, + ); + + Ok(()) + } +} From 4dee4987027c4c2a466597f24270e20555cb4773 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Wed, 25 Feb 2026 18:31:30 +0200 Subject: [PATCH 13/14] Add correct file generation for examples --- Cargo.lock | 18 +++ Cargo.toml | 1 + crates/cli/Cargo.toml | 1 + crates/cli/src/cli/commands.rs | 6 +- crates/cli/src/cli/mod.rs | 20 ++- crates/cli/src/code_generator.rs | 60 -------- crates/cli/src/config.rs | 6 +- crates/cli/src/error.rs | 4 + crates/cli/src/lib.rs | 1 - crates/macros-core/Cargo.toml | 27 ++++ .../src}/attr/codegen.rs | 49 ++++--- .../src}/attr/mod.rs | 8 +- .../src}/attr/parse.rs | 2 +- .../src}/attr/program.rs | 2 +- .../src}/attr/types.rs | 0 crates/macros-core/src/env/mod.rs | 130 ++++++++++++++++++ .../mod.rs => macros-core/src/lib.rs} | 13 +- .../src}/test/mod.rs | 0 crates/macros/Cargo.toml | 10 +- crates/macros/src/lib.rs | 8 +- examples/options/src/program/options.rs | 10 +- examples/p2pk/Cargo.toml | 4 +- examples/p2pk/out_dir/p2pk.rs | 17 +-- examples/test_usage/tests/draft_test.rs | 1 - 24 files changed, 270 insertions(+), 128 deletions(-) delete mode 100644 crates/cli/src/code_generator.rs create mode 100644 crates/macros-core/Cargo.toml rename crates/{macros/src/macros_core => macros-core/src}/attr/codegen.rs (91%) rename crates/{macros/src/macros_core => macros-core/src}/attr/mod.rs (94%) rename crates/{macros/src/macros_core => macros-core/src}/attr/parse.rs (98%) rename crates/{macros/src/macros_core => macros-core/src}/attr/program.rs (89%) rename crates/{macros/src/macros_core => macros-core/src}/attr/types.rs (100%) create mode 100644 crates/macros-core/src/env/mod.rs rename crates/{macros/src/macros_core/mod.rs => macros-core/src/lib.rs} (70%) rename crates/{macros/src/macros_core => macros-core/src}/test/mod.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index a084bb8..b70ff78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1225,6 +1225,12 @@ dependencies = [ "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1878,6 +1884,7 @@ dependencies = [ "electrsd", "glob", "serde", + "simplex-macros-core", "simplex-sdk", "simplex-test", "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", @@ -1893,6 +1900,17 @@ dependencies = [ name = "simplex-macros" version = "0.1.0" dependencies = [ + "serde", + "simplex-macros-core", + "syn 2.0.116", +] + +[[package]] +name = "simplex-macros-core" +version = "0.1.0" +dependencies = [ + "pathdiff", + "prettyplease", "proc-macro-error", "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 3d67899..397eb18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ multiple_crate_versions = "allow" [workspace.dependencies] simplex-provider = { path = "./crates/provider" } +simplex-macros-core = { path = "./crates/macros-core", features = ["bincode", "serde"] } simplex-macros = { path = "./crates/macros" } simplex-test = { path = "./crates/test" } simplex-sdk = { path = "./crates/sdk" } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index aa590d8..b331c99 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [dependencies] simplex-test = { workspace = true } simplex-sdk = { workspace = true } +simplex-macros-core = { workspace = true } simplicityhl = { workspace = true } electrsd = { workspace = true } diff --git a/crates/cli/src/cli/commands.rs b/crates/cli/src/cli/commands.rs index f3d31d3..10e0314 100644 --- a/crates/cli/src/cli/commands.rs +++ b/crates/cli/src/cli/commands.rs @@ -1,4 +1,5 @@ use clap::{Args, Subcommand}; +use std::path::PathBuf; #[derive(Debug, Subcommand)] pub enum Command { @@ -13,7 +14,10 @@ pub enum Command { #[command(subcommand)] command: TestCommand, }, - Build, + Build { + #[arg(env = "OUT_DIR")] + out_dir: Option, + }, } /// Test management commands diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 5edb095..c09cd8c 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -5,6 +5,7 @@ use crate::cli::commands::{Command, TestCommand, TestFlags}; use crate::config::{Config, DEFAULT_CONFIG}; use crate::error::Error; use clap::Parser; +use simplex_macros_core::env::CodeGenerator; use simplex_test::TestClientProvider; use std::path::PathBuf; use std::process::Stdio; @@ -89,10 +90,25 @@ impl Cli { println!("Exiting..."); Ok(()) } - Command::Build => { + // TODO: add overriding of value or delete + Command::Build { out_dir: _out_dir } => { let loaded_config = Config::load_or_discover(self.config.clone()).map_err(|e| Error::ConfigDiscoveryFailure(e))?; - println!("{loaded_config:#?}"); + + if loaded_config.build_config.is_none() { + return Err(Error::Config( + "No build config to build contracts environment, please add appropriate config".to_string(), + )); + } + + let build_config = loaded_config.build_config.unwrap(); + if build_config.compile_simf.is_empty() { + return Err(Error::Config("No files listed to build contracts environment, please check glob patterns or 'compile_simf' field in config.".to_string())); + } + + CodeGenerator::generate_files(&build_config.out_dir, &build_config.compile_simf)?; + + println!("{build_config:#?}"); Ok(()) } } diff --git a/crates/cli/src/code_generator.rs b/crates/cli/src/code_generator.rs deleted file mode 100644 index 910caf9..0000000 --- a/crates/cli/src/code_generator.rs +++ /dev/null @@ -1,60 +0,0 @@ -// pub struct CodeGenerator<'a, 'b> { -// context: &'a mut Context<'b>, -// package: String, -// type_path: Vec, -// source_info: Option, -// syntax: Syntax, -// depth: u8, -// path: Vec, -// buf: &'a mut String, -// } -// -// #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -// #[derive(Clone, PartialEq, ::prost::Message)] -// pub struct FileDescriptorProto { -// /// file name, relative to root of source tree -// #[prost(string, optional, tag = "1")] -// pub name: ::core::option::Option<::prost::alloc::string::String>, -// /// e.g. "foo", "foo.bar", etc. -// #[prost(string, optional, tag = "2")] -// pub package: ::core::option::Option<::prost::alloc::string::String>, -// /// Names of files imported by this file. -// #[prost(string, repeated, tag = "3")] -// pub dependency: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, -// /// Indexes of the public imported files in the dependency list above. -// #[prost(int32, repeated, packed = "false", tag = "10")] -// pub public_dependency: ::prost::alloc::vec::Vec, -// /// Indexes of the weak imported files in the dependency list. -// /// For Google-internal migration only. Do not use. -// #[prost(int32, repeated, packed = "false", tag = "11")] -// pub weak_dependency: ::prost::alloc::vec::Vec, -// /// All top-level definitions in this file. -// #[prost(message, repeated, tag = "4")] -// pub message_type: ::prost::alloc::vec::Vec, -// #[prost(message, repeated, tag = "5")] -// pub enum_type: ::prost::alloc::vec::Vec, -// #[prost(message, repeated, tag = "6")] -// pub service: ::prost::alloc::vec::Vec, -// #[prost(message, repeated, tag = "7")] -// pub extension: ::prost::alloc::vec::Vec, -// #[prost(message, optional, tag = "8")] -// pub options: ::core::option::Option, -// /// This field contains optional information about the original source code. -// /// You may safely remove this entire field without harming runtime -// /// functionality of the descriptors -- the information is needed only by -// /// development tools. -// #[prost(message, optional, tag = "9")] -// pub source_code_info: ::core::option::Option, -// /// The syntax of the proto file. -// /// The supported values are "proto2" and "proto3". -// #[prost(string, optional, tag = "12")] -// pub syntax: ::core::option::Option<::prost::alloc::string::String>, -// } -// -// impl<'b> CodeGenerator<'_, 'b> { -// fn config(&self) -> &Config { -// self.context.config() -// } -// -// pub(crate) fn generate(context: &mut Context<'b>, file: FileDescriptorProto, buf: &mut String) {} -// } diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index 14abc31..41a31f1 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -57,10 +57,10 @@ pub struct ConfigOverride { pub network: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize)] pub struct BuildConf { - compile_simf: Vec, - out_dir: PathBuf, + pub compile_simf: Vec, + pub out_dir: PathBuf, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs index 981c011..aa41540 100644 --- a/crates/cli/src/error.rs +++ b/crates/cli/src/error.rs @@ -30,4 +30,8 @@ pub enum Error { /// Errors when building config. #[error("Failed to discover config, check existence or create new one with `simplex init`, error: '{0}'")] ConfigDiscoveryFailure(crate::config::ConfigError), + + /// Errors when generating code for simplicity environment. + #[error("Occurred code generation error, error: '{0}'")] + CodeGenerator(#[from] simplex_macros_core::env::CodeGeneratorError), } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 673c3d8..d0487d5 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,6 +1,5 @@ pub mod cache_storage; pub mod cli; -pub mod code_generator; pub mod config; pub mod error; pub mod logging; diff --git a/crates/macros-core/Cargo.toml b/crates/macros-core/Cargo.toml new file mode 100644 index 0000000..5c70ea1 --- /dev/null +++ b/crates/macros-core/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "simplex-macros-core" +description = "Macro support core for Simplex, the Rust SimplicityHl toolkit. Not intended to be used directly." +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[features] +bincode = [] +serde = ["bincode", "dep:serde"] +default = ["bincode", "serde"] + +[dependencies] +proc-macro-error = { version = "1.0" } +proc-macro2 = { version = "1.0.106", features = ["span-locations"] } +syn = { version = "2.0.114", default-features = false, features = ["proc-macro", "full", "parsing", "derive", "clone-impls", "extra-traits", "printing"] } +thiserror = { workspace = true } +quote = { version = "1.0.44" } +simplicityhl = { workspace = true } +simplex-test = { workspace = true } +serde = { version = "1.0.228", optional = true } +pathdiff = { version = "0.2.3" } +prettyplease = { version = "0.2.37" } + diff --git a/crates/macros/src/macros_core/attr/codegen.rs b/crates/macros-core/src/attr/codegen.rs similarity index 91% rename from crates/macros/src/macros_core/attr/codegen.rs rename to crates/macros-core/src/attr/codegen.rs index 24cb084..a944b50 100644 --- a/crates/macros/src/macros_core/attr/codegen.rs +++ b/crates/macros-core/src/attr/codegen.rs @@ -1,5 +1,6 @@ -use crate::macros_core::attr::SimfContent; -use crate::macros_core::attr::types::RustType; +use crate::attr::SimfContent; +use crate::attr::types::RustType; +use proc_macro2::Ident; use quote::{format_ident, quote}; use simplicityhl::str::WitnessName; use simplicityhl::{AbiMeta, Parameters, ResolvedType, WitnessTypes}; @@ -44,7 +45,7 @@ impl SimfContractMeta { let args_struct = WitnessStruct::generate_args_struct(&simf_content.contract_name, &abi_meta.param_types)?; let witness_struct = WitnessStruct::generate_witness_struct(&simf_content.contract_name, &abi_meta.witness_types)?; - let contract_source_const_name = format_ident!("{}_CONTRACT_SOURCE", simf_content.contract_name.to_uppercase()); + let contract_source_const_name = convert_contract_name_to_contract_source_const(&simf_content.contract_name); Ok(SimfContractMeta { contract_source_const_name, args_struct, @@ -242,23 +243,8 @@ impl WitnessStruct { }) } - fn convert_contract_name_to_struct_name(contract_name: &str) -> String { - let words: Vec = contract_name - .split('_') - .filter(|w| !w.is_empty()) - .map(|word| { - let mut chars = word.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_uppercase().collect::() + chars.as_str(), - } - }) - .collect(); - words.join("") - } - fn generate_args_struct(contract_name: &str, meta: &Parameters) -> syn::Result { - let base_name = Self::convert_contract_name_to_struct_name(contract_name); + let base_name = convert_contract_name_to_struct_name(contract_name); Ok(WitnessStruct { struct_name: format_ident!("{}Arguments", base_name), witness_values: WitnessStruct::generate_witness_fields(meta.iter())?, @@ -266,7 +252,7 @@ impl WitnessStruct { } fn generate_witness_struct(contract_name: &str, meta: &WitnessTypes) -> syn::Result { - let base_name = Self::convert_contract_name_to_struct_name(contract_name); + let base_name = convert_contract_name_to_struct_name(contract_name); Ok(WitnessStruct { struct_name: format_ident!("{}Witness", base_name), witness_values: WitnessStruct::generate_witness_fields(meta.iter())?, @@ -344,3 +330,26 @@ impl WitnessStruct { (extractions, struct_init) } } + +pub(crate) fn convert_contract_name_to_struct_name(contract_name: &str) -> String { + let words: Vec = contract_name + .split('_') + .filter(|w| !w.is_empty()) + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + } + }) + .collect(); + words.join("") +} + +pub(crate) fn convert_contract_name_to_contract_source_const(contract_name: &str) -> Ident { + format_ident!("{}_CONTRACT_SOURCE", contract_name.to_uppercase()) +} + +pub(crate) fn convert_contract_name_to_contract_module(contract_name: &str) -> Ident { + format_ident!("derived_{}", contract_name) +} diff --git a/crates/macros/src/macros_core/attr/mod.rs b/crates/macros-core/src/attr/mod.rs similarity index 94% rename from crates/macros/src/macros_core/attr/mod.rs rename to crates/macros-core/src/attr/mod.rs index e77e71c..0d367ba 100644 --- a/crates/macros/src/macros_core/attr/mod.rs +++ b/crates/macros-core/src/attr/mod.rs @@ -5,9 +5,11 @@ mod types; pub use parse::SimfContent; -use crate::macros_core::attr::codegen::{GeneratedArgumentTokens, GeneratedWitnessTokens, SimfContractMeta}; +use crate::attr::codegen::{ + GeneratedArgumentTokens, GeneratedWitnessTokens, SimfContractMeta, convert_contract_name_to_contract_module, +}; use proc_macro2::Span; -use quote::{format_ident, quote}; +use quote::quote; use simplicityhl::AbiMeta; use std::error::Error; // TODO(Illia): add bincode generation feature (i.e. require bincode dependencies) @@ -26,7 +28,7 @@ pub fn expand_helpers(simf_content: SimfContent, meta: AbiMeta) -> syn::Result

Result> { - let mod_ident = format_ident!("derived_{}", simf_content.contract_name); + let mod_ident = convert_contract_name_to_contract_module(&simf_content.contract_name); let derived_meta = SimfContractMeta::try_from(simf_content, meta)?; diff --git a/crates/macros/src/macros_core/attr/parse.rs b/crates/macros-core/src/attr/parse.rs similarity index 98% rename from crates/macros/src/macros_core/attr/parse.rs rename to crates/macros-core/src/attr/parse.rs index 680ce28..b146466 100644 --- a/crates/macros/src/macros_core/attr/parse.rs +++ b/crates/macros-core/src/attr/parse.rs @@ -154,7 +154,7 @@ impl SimfContent { syn::parse_str::(s).is_err() } - fn extract_content_from_path(path: &PathBuf) -> std::io::Result { + pub fn extract_content_from_path(path: &PathBuf) -> std::io::Result { let contract_name = { let name = path .file_prefix() diff --git a/crates/macros/src/macros_core/attr/program.rs b/crates/macros-core/src/attr/program.rs similarity index 89% rename from crates/macros/src/macros_core/attr/program.rs rename to crates/macros-core/src/attr/program.rs index 2cf86d1..67cd275 100644 --- a/crates/macros/src/macros_core/attr/program.rs +++ b/crates/macros-core/src/attr/program.rs @@ -1,4 +1,4 @@ -use crate::macros_core::attr::parse::SimfContent; +use crate::attr::parse::SimfContent; use proc_macro2::Span; use simplicityhl::{AbiMeta, TemplateProgram}; use std::error::Error; diff --git a/crates/macros/src/macros_core/attr/types.rs b/crates/macros-core/src/attr/types.rs similarity index 100% rename from crates/macros/src/macros_core/attr/types.rs rename to crates/macros-core/src/attr/types.rs diff --git a/crates/macros-core/src/env/mod.rs b/crates/macros-core/src/env/mod.rs new file mode 100644 index 0000000..30d0b03 --- /dev/null +++ b/crates/macros-core/src/env/mod.rs @@ -0,0 +1,130 @@ +use crate::attr::SimfContent; +use crate::attr::codegen::{ + convert_contract_name_to_contract_module, convert_contract_name_to_contract_source_const, + convert_contract_name_to_struct_name, +}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use std::io::Write; +use std::path::PathBuf; +use std::{env, fs}; +use syn::parse::{Parse, ParseStream}; + +#[derive(thiserror::Error, Debug)] +pub enum CodeGeneratorError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Failed to extract content from path, err: '{0}'")] + FailedToExtractContent(std::io::Error), + + #[error("Failed to generate file: {0}")] + GenerationFailed(String), + + #[error( + "Failed to resolve correct relative path for include_simf! macro, cwd: '{cwd:?}', simf_file: '{simf_file:?}'" + )] + FailedToFindCorrectRelativePath { cwd: PathBuf, simf_file: PathBuf }, +} + +pub struct CodeGenerator {} + +struct FileDescriptor { + simf_content: SimfContent, + simf_file: PathBuf, + out_dir: PathBuf, + cwd: PathBuf, +} + +impl<'b> CodeGenerator { + pub fn generate_files( + out_dir: impl AsRef, + simfs: &[impl AsRef], + ) -> Result<(), CodeGeneratorError> { + let out_dir = out_dir.as_ref(); + + fs::create_dir_all(out_dir)?; + + // Process each file path from compile_simf + for simf_file_path in simfs { + let path_buf = PathBuf::from(simf_file_path.as_ref()); + let simf_content = SimfContent::extract_content_from_path(&path_buf) + .map_err(CodeGeneratorError::FailedToExtractContent)?; + + let output_file = out_dir.join(format!("{}.rs", simf_content.contract_name)); + + let mut file = fs::OpenOptions::new().write(true).truncate(true).open(&output_file)?; + Self::expand_file( + FileDescriptor { + simf_content, + simf_file: PathBuf::from(simf_file_path.as_ref()), + out_dir: PathBuf::from(out_dir), + cwd: env::current_dir()?, + }, + &mut file, + )?; + } + + Ok(()) + } + + fn expand_file(file_descriptor: FileDescriptor, buf: &mut dyn Write) -> Result<(), CodeGeneratorError> { + let code = Self::generate_code(file_descriptor)?; + let file: syn::File = syn::parse2(code).map_err(|e| CodeGeneratorError::GenerationFailed(e.to_string()))?; + let prettystr = prettyplease::unparse(&file); + buf.write_all(prettystr.as_bytes())?; + buf.flush()?; + Ok(()) + } + + fn generate_code(file_descriptor: FileDescriptor) -> Result { + let contract_name = &file_descriptor.simf_content.contract_name; + let content = &file_descriptor.simf_content.content; + let program_name = { + let base_name = convert_contract_name_to_struct_name(contract_name); + format_ident!("{base_name}Program") + }; + let include_simf_source_const = convert_contract_name_to_contract_source_const(contract_name); + let include_simf_module = convert_contract_name_to_contract_module(contract_name); + + let pathdiff = pathdiff::diff_paths(&file_descriptor.simf_file.canonicalize().unwrap(), &file_descriptor.cwd) + .ok_or(CodeGeneratorError::FailedToFindCorrectRelativePath { + cwd: file_descriptor.cwd, + simf_file: file_descriptor.simf_file, + })?; + let pathdiff = format!("{}", pathdiff.display()); + + let code = quote! { + use simplex::simplex_macros::include_simf; + use simplex::simplex_sdk::arguments::ArgumentsTrait; + use simplex::simplex_sdk::program::Program; + use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; + + pub struct #program_name<'a> { + program: Program<'a>, + } + + impl<'a> #program_name<'a> { + pub const SOURCE: &'static str = #include_simf_module::#include_simf_source_const; + + pub fn new(public_key: &'a XOnlyPublicKey, arguments: &'a impl ArgumentsTrait) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, arguments), + } + } + + pub fn get_program(&self) -> &Program<'a> { + &self.program + } + + pub fn get_program_mut(&mut self) -> &mut Program<'a> { + &mut self.program + } + } + + include_simf!(#pathdiff); + }; + + Ok(code) + } +} diff --git a/crates/macros/src/macros_core/mod.rs b/crates/macros-core/src/lib.rs similarity index 70% rename from crates/macros/src/macros_core/mod.rs rename to crates/macros-core/src/lib.rs index 399dff4..4194710 100644 --- a/crates/macros/src/macros_core/mod.rs +++ b/crates/macros-core/src/lib.rs @@ -1,7 +1,9 @@ #![warn(clippy::all, clippy::pedantic)] -pub(crate) mod attr; -pub(crate) mod test; +pub mod attr; +/// Module releted to simplex environment generation +pub mod env; +pub mod test; /// Expands the `include_simf` macro. /// @@ -22,3 +24,10 @@ pub fn expand_include_simf(input: &attr::parse::SynFilePath) -> syn::Result syn::Result { test::expand(args, input) } + +pub fn expand_simplex_contract_enviroment( + outdir: impl AsRef, + simfs: &[impl AsRef], +) -> Result<(), env::CodeGeneratorError> { + env::CodeGenerator::generate_files(outdir, simfs) +} diff --git a/crates/macros/src/macros_core/test/mod.rs b/crates/macros-core/src/test/mod.rs similarity index 100% rename from crates/macros/src/macros_core/test/mod.rs rename to crates/macros-core/src/test/mod.rs diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index d039746..de24136 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -25,12 +25,6 @@ derive = [] workspace = true [dependencies] -simplex-test = { workspace = true } -thiserror = { workspace = true } -simplicityhl = { workspace = true } - +simplex-macros-core = { workspace = true } serde = { version = "1.0.228", optional = true } -proc-macro-error = { version = "1.0" } -proc-macro2 = { version = "1.0.106", features = ["span-locations"] } -syn = { version = "2.0.114", default-features = false, features = ["proc-macro", "full", "parsing", "derive", "clone-impls", "extra-traits", "printing"] } -quote = { version = "1.0.44" } +syn = { version = "2.0.114", default-features = false, features = ["parsing", "proc-macro"] } diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index d9fa658..656ed8c 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1,13 +1,11 @@ use proc_macro::TokenStream; -mod macros_core; - #[cfg(feature = "macros")] #[proc_macro] pub fn include_simf(tokenstream: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(tokenstream as macros_core::attr::parse::SynFilePath); + let input = syn::parse_macro_input!(tokenstream as simplex_macros_core::attr::parse::SynFilePath); - match macros_core::expand_include_simf(&input) { + match simplex_macros_core::expand_include_simf(&input) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), } @@ -18,7 +16,7 @@ pub fn include_simf(tokenstream: TokenStream) -> TokenStream { pub fn test(args: TokenStream, input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as syn::ItemFn); - match macros_core::expand_test(args.into(), input) { + match simplex_macros_core::expand_test(args.into(), input) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), } diff --git a/examples/options/src/program/options.rs b/examples/options/src/program/options.rs index 8316eb2..619b668 100644 --- a/examples/options/src/program/options.rs +++ b/examples/options/src/program/options.rs @@ -2,27 +2,21 @@ use simplex::simplex_macros::include_simf; use simplex::simplex_sdk::arguments::ArgumentsTrait; use simplex::simplex_sdk::program::Program; use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; - -pub struct Options<'a> { +pub struct OptionsProgram<'a> { program: Program<'a>, } - -impl<'a> Options<'a> { +impl<'a> OptionsProgram<'a> { pub const SOURCE: &'static str = derived_options::OPTIONS_CONTRACT_SOURCE; - pub fn new(public_key: &'a XOnlyPublicKey, arguments: &'a impl ArgumentsTrait) -> Self { Self { program: Program::new(Self::SOURCE, public_key, arguments), } } - pub fn get_program(&self) -> &Program<'a> { &self.program } - pub fn get_program_mut(&mut self) -> &mut Program<'a> { &mut self.program } } - include_simf!("simf/options.simf"); diff --git a/examples/p2pk/Cargo.toml b/examples/p2pk/Cargo.toml index 013ce34..599c870 100644 --- a/examples/p2pk/Cargo.toml +++ b/examples/p2pk/Cargo.toml @@ -10,6 +10,6 @@ workspace = true [dependencies] -simplex = { workspace = true} +simplex = { path = "../../crates/simplex" } -simplicityhl = { workspace = true } \ No newline at end of file +simplicityhl = { git = "https://github.com/ikripaka/SimplicityHL/", branch = "feature/rich-params" } diff --git a/examples/p2pk/out_dir/p2pk.rs b/examples/p2pk/out_dir/p2pk.rs index 1fb0c9e..e5a7b2d 100644 --- a/examples/p2pk/out_dir/p2pk.rs +++ b/examples/p2pk/out_dir/p2pk.rs @@ -2,27 +2,24 @@ use simplex::simplex_macros::include_simf; use simplex::simplex_sdk::arguments::ArgumentsTrait; use simplex::simplex_sdk::program::Program; use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; - -pub struct P2PK<'a> { +pub struct P2pkProgram<'a> { program: Program<'a>, } - -impl<'a> P2PK<'a> { +impl<'a> P2pkProgram<'a> { pub const SOURCE: &'static str = derived_p2pk::P2PK_CONTRACT_SOURCE; - - pub fn new(public_key: &'a XOnlyPublicKey, arguments: &'a impl ArgumentsTrait) -> Self { + pub fn new( + public_key: &'a XOnlyPublicKey, + arguments: &'a impl ArgumentsTrait, + ) -> Self { Self { program: Program::new(Self::SOURCE, public_key, arguments), } } - pub fn get_program(&self) -> &Program<'a> { &self.program } - pub fn get_program_mut(&mut self) -> &mut Program<'a> { &mut self.program } } - -include_simf!("./simf/p2pk.simf"); +include_simf!("simf/p2pk.simf"); diff --git a/examples/test_usage/tests/draft_test.rs b/examples/test_usage/tests/draft_test.rs index e1967a2..5a4c980 100644 --- a/examples/test_usage/tests/draft_test.rs +++ b/examples/test_usage/tests/draft_test.rs @@ -2,7 +2,6 @@ use simplex::simplex_sdk::constants::SimplicityNetwork; use simplex::simplex_test::{AddressType, ElementsRpcClient}; use simplex::simplex_test::{ConfigOption, TestClientProvider}; use simplex::simplex_test::{DEFAULT_SAT_AMOUNT_FAUCET, ElementsDConf}; -use simplicityhl::elements::Address; use simplicityhl::elements::bitcoin::secp256k1; use simplicityhl::elements::secp256k1_zkp::Keypair; From bdf30ecac7f936108d409bc607c227788a82773e Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Thu, 26 Feb 2026 10:53:01 +0200 Subject: [PATCH 14/14] Polishing code --- crates/cli/src/lib.rs | 2 + crates/macros-core/src/env/mod.rs | 18 +- crates/macros/src/lib.rs | 2 + crates/provider/src/elements_rpc/mod.rs | 30 +- crates/provider/src/elements_rpc/types.rs | 7 +- crates/provider/src/error.rs | 1 + crates/provider/src/esplora/mod.rs | 820 +++++++++++++++++----- crates/provider/src/esplora/types.rs | 18 +- crates/provider/src/lib.rs | 2 + crates/sdk/src/error.rs | 2 +- crates/sdk/src/lib.rs | 2 + crates/sdk/src/provider/esplora.rs | 12 +- crates/sdk/src/witness_transaction.rs | 9 +- crates/simplex/src/lib.rs | 2 + examples/options/src/lib.rs | 2 + 15 files changed, 717 insertions(+), 212 deletions(-) diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index d0487d5..eb7d538 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,3 +1,5 @@ +#![warn(clippy::all, clippy::pedantic)] + pub mod cache_storage; pub mod cli; pub mod config; diff --git a/crates/macros-core/src/env/mod.rs b/crates/macros-core/src/env/mod.rs index 30d0b03..bccccaa 100644 --- a/crates/macros-core/src/env/mod.rs +++ b/crates/macros-core/src/env/mod.rs @@ -7,8 +7,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use std::io::Write; use std::path::PathBuf; -use std::{env, fs}; -use syn::parse::{Parse, ParseStream}; +use std::{env, fs, io}; #[derive(thiserror::Error, Debug)] pub enum CodeGeneratorError { @@ -45,7 +44,6 @@ impl<'b> CodeGenerator { fs::create_dir_all(out_dir)?; - // Process each file path from compile_simf for simf_file_path in simfs { let path_buf = PathBuf::from(simf_file_path.as_ref()); let simf_content = SimfContent::extract_content_from_path(&path_buf) @@ -79,7 +77,6 @@ impl<'b> CodeGenerator { fn generate_code(file_descriptor: FileDescriptor) -> Result { let contract_name = &file_descriptor.simf_content.contract_name; - let content = &file_descriptor.simf_content.content; let program_name = { let base_name = convert_contract_name_to_struct_name(contract_name); format_ident!("{base_name}Program") @@ -87,8 +84,17 @@ impl<'b> CodeGenerator { let include_simf_source_const = convert_contract_name_to_contract_source_const(contract_name); let include_simf_module = convert_contract_name_to_contract_module(contract_name); - let pathdiff = pathdiff::diff_paths(&file_descriptor.simf_file.canonicalize().unwrap(), &file_descriptor.cwd) - .ok_or(CodeGeneratorError::FailedToFindCorrectRelativePath { + let pathdiff = pathdiff::diff_paths( + &file_descriptor.simf_file.canonicalize().map_err(|e| { + io::Error::other(format!( + "Failed to canonicalize simf file descriptor, '{}', err: '{}'", + file_descriptor.simf_file.display(), + e + )) + })?, + &file_descriptor.cwd, + ) + .ok_or(CodeGeneratorError::FailedToFindCorrectRelativePath { cwd: file_descriptor.cwd, simf_file: file_descriptor.simf_file, })?; diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 656ed8c..861e98e 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; #[cfg(feature = "macros")] diff --git a/crates/provider/src/elements_rpc/mod.rs b/crates/provider/src/elements_rpc/mod.rs index 682d241..667d82d 100644 --- a/crates/provider/src/elements_rpc/mod.rs +++ b/crates/provider/src/elements_rpc/mod.rs @@ -89,10 +89,10 @@ impl ElementsRpcClient { let mut args = Vec::with_capacity(2); if start.is_some() { - args.push(start.into()) + args.push(start.into()); } if stop.is_some() { - args.push(stop.into()) + args.push(stop.into()); } client.call::(METHOD, &args)?; Ok(()) @@ -229,6 +229,7 @@ impl ElementsRpcClient { #[derive(serde::Deserialize)] pub struct CreatewalletResult { name: String, + #[allow(dead_code)] warning: String, } @@ -266,7 +267,7 @@ impl ElementsRpcClient { let mut args = Vec::new(); args.push(min_conf.unwrap_or(1).into()); - args.push(max_conf.unwrap_or(9999999).into()); + args.push(max_conf.unwrap_or(9_999_999).into()); if let Some(addrs) = addresses { args.push(addrs.into()); @@ -318,10 +319,10 @@ impl ElementsRpcClient { const METHOD: &str = "validateaddress"; let value: serde_json::Value = client.call(METHOD, &[address.into()])?; - Ok(value + value .get("isvalid") - .and_then(|v| v.as_bool()) - .ok_or_else(|| ExplorerError::ElementsRpcUnexpectedReturn(METHOD.into()))?) + .and_then(serde_json::Value::as_bool) + .ok_or_else(|| ExplorerError::ElementsRpcUnexpectedReturn(METHOD.into())) } pub fn scantxoutset( @@ -336,7 +337,11 @@ impl ElementsRpcClient { match action { "start" => { if let Some(objects) = scanobjects { - args.push(serde_json::to_value(objects).unwrap()); + args.push(serde_json::to_value(objects).map_err(|e| { + ExplorerError::InvalidInput(format!( + "Failed to transform objects into serde_json::Value, err: '{e}'" + )) + })?); } else { return Err(ExplorerError::InvalidInput( "scantxoutset 'start' action requires scanobjects".to_string(), @@ -346,15 +351,13 @@ impl ElementsRpcClient { "abort" | "status" => { if scanobjects.is_some() { return Err(ExplorerError::InvalidInput(format!( - "scantxoutset '{}' action does not accept scanobjects", - action + "scantxoutset '{action}' action does not accept scanobjects", ))); } } _ => { return Err(ExplorerError::InvalidInput(format!( - "unknown scantxoutset action: {}", - action + "unknown scantxoutset action: {action}" ))); } } @@ -403,7 +406,10 @@ impl ElementsRpcClient { const METHOD: &str = "getrawtransaction"; let value: serde_json::Value = client.call(METHOD, &[txid.into(), false.into()])?; - Ok(value.as_str().unwrap().to_string()) + let value = value + .as_str() + .ok_or_else(|| ExplorerError::InvalidInput("Failed to deserialize a String".to_string()))?; + Ok(value.to_string()) } } diff --git a/crates/provider/src/elements_rpc/types.rs b/crates/provider/src/elements_rpc/types.rs index b5a73c2..bcb358a 100644 --- a/crates/provider/src/elements_rpc/types.rs +++ b/crates/provider/src/elements_rpc/types.rs @@ -119,7 +119,7 @@ impl std::fmt::Display for AddressType { AddressType::Bech32 => "bech32".to_string(), AddressType::Bech32m => "bech32m".to_string(), }; - write!(f, "{}", str) + write!(f, "{str}") } } @@ -193,7 +193,7 @@ impl ScantxoutsetResult { progress: status_data.progress, searched_items: status_data.searched_items, }), - _ => Err(serde_json::Error::custom(format!("unknown action: {}", action))), + _ => Err(serde_json::Error::custom(format!("unknown action: {action}"))), } } } @@ -306,7 +306,8 @@ pub struct ScriptSig { pub struct RawTransactionOutput { pub value: f64, pub n: u32, - pub scriptPubKey: ScriptPubKey, + #[serde(rename = "scriptPubKey")] + pub script_pubkey: ScriptPubKey, #[serde(default)] pub asset: Option, #[serde(default)] diff --git a/crates/provider/src/error.rs b/crates/provider/src/error.rs index c657e67..60ddc9d 100644 --- a/crates/provider/src/error.rs +++ b/crates/provider/src/error.rs @@ -99,6 +99,7 @@ impl ExplorerError { } #[inline] + #[allow(clippy::cast_sign_loss)] pub(crate) fn erroneous_response_minreq(e: &minreq::Response) -> Self { ExplorerError::ErroneousRequest { url: Some(e.url.clone()), diff --git a/crates/provider/src/esplora/mod.rs b/crates/provider/src/esplora/mod.rs index 5c9297b..e3e81d9 100644 --- a/crates/provider/src/esplora/mod.rs +++ b/crates/provider/src/esplora/mod.rs @@ -37,12 +37,14 @@ impl EsploraClientBuilder { ESPLORA_LIQUID_TESTNET.to_string() } + #[must_use] pub fn liquid_testnet() -> Self { Self { url: Some(ESPLORA_LIQUID_TESTNET.to_string()), } } + #[must_use] pub fn liquid_mainnet() -> Self { Self { url: Some(ESPLORA_LIQUID.to_string()), @@ -54,6 +56,7 @@ impl EsploraClientBuilder { EsploraClientBuilder { url: Some(url.into()) } } + #[must_use] pub fn build_async(self) -> EsploraClientAsync { EsploraClientAsync { url_builder: UrlBuilder { @@ -63,6 +66,7 @@ impl EsploraClientBuilder { } } + #[must_use] pub fn build_sync(self) -> EsploraClientSync { EsploraClientSync { url_builder: UrlBuilder { @@ -357,8 +361,16 @@ mod deserializable { impl TypeConversion for EsploraTransaction { fn convert(self) -> Result { let status = self.status.convert()?; - let vin = self.vin.into_iter().map(|x| x.convert()).collect::>()?; - let vout = self.vout.into_iter().map(|x| x.convert()).collect::>()?; + let vin = self + .vin + .into_iter() + .map(TypeConversion::convert) + .collect::>()?; + let vout = self + .vout + .into_iter() + .map(TypeConversion::convert) + .collect::>()?; Ok(types::EsploraTransaction { txid: Txid::from_str(&self.txid)?, @@ -395,7 +407,7 @@ mod deserializable { }; Ok(types::Vin { - out_point: Default::default(), + out_point: OutPoint::default(), is_coinbase: self.is_coinbase, scriptsig: self.scriptsig, scriptsig_asm: self.scriptsig_asm, @@ -481,12 +493,19 @@ mod deserializable { impl EsploraClientAsync { #[inline] fn filter_resp(resp: &reqwest::Response) -> Result<(), ExplorerError> { - if is_resp_ok(resp.status().as_u16() as i32) { + if is_resp_ok(i32::from(resp.status().as_u16())) { return Err(ExplorerError::erroneous_response_reqwest(resp)); } Ok(()) } + /// Retrieves transaction details by transaction ID. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub async fn get_tx(&self, txid: &str) -> Result { let url = self.url_builder.get_tx_url(txid)?; let resp = self @@ -506,6 +525,13 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves transaction status by transaction ID. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if block hash parsing fails pub async fn get_tx_status(&self, txid: &str) -> Result { let url = self.url_builder.get_tx_status_url(txid)?; let resp = self @@ -524,6 +550,12 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves transaction hex by transaction ID. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if response text extraction fails pub async fn get_tx_hex(&self, txid: &str) -> Result { let url = self.url_builder.get_tx_hex_url(txid)?; let resp = self @@ -537,6 +569,12 @@ impl EsploraClientAsync { resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } + /// Retrieves raw transaction bytes by transaction ID. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if response bytes extraction fails pub async fn get_tx_raw(&self, txid: &str) -> Result, ExplorerError> { let url = self.url_builder.get_tx_raw_url(txid)?; let resp = self @@ -553,12 +591,24 @@ impl EsploraClientAsync { .map_err(|e| ExplorerError::deserialize_reqwest(&e)) } + /// Retrieves and deserializes a transaction as an Elements transaction. + /// + /// # Errors + /// - Returns all errors from `get_tx_raw` + /// - Returns `ExplorerError::TransactionDecode` if transaction deserialization fails pub async fn get_tx_elements(&self, txid: &str) -> Result { let bytes = self.get_tx_raw(txid).await?; simplicityhl::elements::Transaction::deserialize(&bytes) .map_err(|e| ExplorerError::TransactionDecode(e.to_string())) } + /// Retrieves merkle proof for a transaction. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if merkle hash parsing fails pub async fn get_tx_merkle_proof(&self, txid: &str) -> Result { let url = self.url_builder.get_tx_merkle_proof_url(txid)?; let resp = self @@ -577,6 +627,13 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves outspend information for a specific output. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub async fn get_tx_outspend(&self, txid: &str, vout: u32) -> Result { let url = self.url_builder.get_tx_outspend_url(txid, vout)?; let resp = self @@ -595,6 +652,13 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves outspend information for all outputs of a transaction. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub async fn get_tx_outspends(&self, txid: &str) -> Result, ExplorerError> { let url = self.url_builder.get_tx_outspends_url(txid)?; let resp = self @@ -609,9 +673,18 @@ impl EsploraClientAsync { .json::>() .await .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; - resp.into_iter().map(|x| x.convert()).collect::, _>>() - } - + resp.into_iter() + .map(deserializable::TypeConversion::convert) + .collect::, _>>() + } + + /// Broadcasts a transaction to the network. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if response text extraction fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub async fn broadcast_tx(&self, tx: &simplicityhl::elements::Transaction) -> Result { let tx_hex = simplicityhl::elements::encode::serialize_hex(tx); let url = self.url_builder.get_broadcast_tx_url()?; @@ -628,6 +701,12 @@ impl EsploraClientAsync { Ok(Txid::from_str(&resp)?) } + /// Broadcasts a package of transactions. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails pub async fn broadcast_tx_package( &self, txs: &[simplicityhl::elements::Transaction], @@ -650,6 +729,13 @@ impl EsploraClientAsync { resp.json().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } + /// Retrieves address information. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::AddressConversion` if address parsing fails pub async fn get_address(&self, address: &str) -> Result { let url = self.url_builder.get_address_url(address)?; let resp = self @@ -669,6 +755,13 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves all transactions for an address. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID/block hash parsing fails pub async fn get_address_txs(&self, address: &str) -> Result, ExplorerError> { let url = self.url_builder.get_address_txs_url(address)?; let resp = self @@ -686,6 +779,13 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves confirmed transactions for an address with pagination. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID/block hash parsing fails pub async fn get_address_txs_chain( &self, address: &str, @@ -704,10 +804,20 @@ impl EsploraClientAsync { .json::>() .await .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; - let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + let resp = resp + .into_iter() + .map(deserializable::TypeConversion::convert) + .collect::>()?; Ok(resp) } + /// Retrieves mempool transactions for an address. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub async fn get_address_txs_mempool( &self, address: &str, @@ -725,10 +835,22 @@ impl EsploraClientAsync { .json::>() .await .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; - let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + let resp = resp + .into_iter() + .map(deserializable::TypeConversion::convert) + .collect::>()?; Ok(resp) } + /// Retrieves UTXOs for an address. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::HexSimdDecode` if hex decoding fails + /// - Returns `ExplorerError::CommitmentDecode` if commitment parsing fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID/block hash parsing fails pub async fn get_address_utxo(&self, address: &str) -> Result, ExplorerError> { let url = self.url_builder.get_address_utxo_url(address)?; let resp = self @@ -743,9 +865,18 @@ impl EsploraClientAsync { .json::>() .await .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; - resp.into_iter().map(|x| x.convert()).collect::, _>>() - } - + resp.into_iter() + .map(deserializable::TypeConversion::convert) + .collect::, _>>() + } + + /// Retrieves scripthash information. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::ElementsHex` if script parsing fails pub async fn get_scripthash(&self, hash: &str) -> Result { let url = self.url_builder.get_scripthash_url(hash)?; let resp = self @@ -764,6 +895,12 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves transactions for a scripthash. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if response text extraction fails pub async fn get_scripthash_txs(&self, hash: &str) -> Result { let url = self.url_builder.get_scripthash_txs_url(hash)?; let resp = self @@ -777,6 +914,12 @@ impl EsploraClientAsync { resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } + /// Retrieves confirmed transactions for a scripthash with pagination. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if response text extraction fails pub async fn get_scripthash_txs_chain( &self, hash: &str, @@ -794,6 +937,12 @@ impl EsploraClientAsync { resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } + /// Retrieves mempool transactions for a scripthash. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if response text extraction fails pub async fn get_scripthash_txs_mempool(&self, hash: &str) -> Result { let url = self.url_builder.get_scripthash_txs_mempool_url(hash)?; let resp = self @@ -807,6 +956,12 @@ impl EsploraClientAsync { resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } + /// Retrieves UTXOs for a scripthash. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if response text extraction fails pub async fn get_scripthash_utxo(&self, hash: &str) -> Result { let url = self.url_builder.get_scripthash_utxo_url(hash)?; let resp = self @@ -820,6 +975,13 @@ impl EsploraClientAsync { resp.text().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } + /// Retrieves block information by block hash. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if hash/merkle node parsing fails pub async fn get_block(&self, hash: &str) -> Result { let url = self.url_builder.get_block_url(hash)?; let resp = self @@ -838,6 +1000,12 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves block header as hex string. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if response text extraction fails pub async fn get_block_header(&self, hash: &str) -> Result { let url = self.url_builder.get_block_header_url(hash)?; let resp = self @@ -852,6 +1020,12 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves block status information. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails pub async fn get_block_status(&self, hash: &str) -> Result { let url = self.url_builder.get_block_status_url(hash)?; let resp = self @@ -867,6 +1041,13 @@ impl EsploraClientAsync { .map_err(|e| ExplorerError::deserialize_reqwest(&e)) } + /// Retrieves transactions in a block with optional pagination. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID/block hash parsing fails pub async fn get_block_txs( &self, hash: &str, @@ -885,10 +1066,20 @@ impl EsploraClientAsync { .json::>() .await .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; - let resp = resp.into_iter().map(|val| val.convert()).collect::>()?; + let resp = resp + .into_iter() + .map(deserializable::TypeConversion::convert) + .collect::>()?; Ok(resp) } + /// Retrieves transaction IDs in a block. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub async fn get_block_txids(&self, hash: &str) -> Result, ExplorerError> { let url = self.url_builder.get_block_txids_url(hash)?; let resp = self @@ -911,6 +1102,13 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves a specific transaction ID from a block. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if response text extraction fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub async fn get_block_txid(&self, hash: &str, index: u32) -> Result { let url = self.url_builder.get_block_txid_url(hash, index)?; let resp = self @@ -926,6 +1124,12 @@ impl EsploraClientAsync { Ok(Txid::from_str(&resp)?) } + /// Retrieves raw block bytes. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if response bytes extraction fails pub async fn get_block_raw(&self, hash: &str) -> Result, ExplorerError> { let url = self.url_builder.get_block_raw_url(hash)?; let resp = self @@ -942,6 +1146,13 @@ impl EsploraClientAsync { .map_err(|e| ExplorerError::deserialize_reqwest(&e)) } + /// Retrieves block hash by block height. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if response text extraction fails + /// - Returns `ExplorerError::BitcoinHashesHex` if block hash parsing fails pub async fn get_block_height(&self, height: u64) -> Result { let url = self.url_builder.get_block_height_url(height)?; let resp = self @@ -957,6 +1168,13 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves blocks starting from a given height. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if hash/merkle node parsing fails pub async fn get_blocks(&self, start_height: Option) -> Result, ExplorerError> { let url = self.url_builder.get_blocks_url(start_height)?; let resp = self @@ -971,10 +1189,19 @@ impl EsploraClientAsync { .json::>() .await .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; - let resp = resp.into_iter().map(|val| val.convert()).collect::>()?; + let resp = resp + .into_iter() + .map(deserializable::TypeConversion::convert) + .collect::>()?; Ok(resp) } + /// Retrieves the height of the blockchain tip. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails pub async fn get_blocks_tip_height(&self) -> Result { let url = self.url_builder.get_blocks_tip_height_url()?; let resp = self @@ -992,6 +1219,13 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves the hash of the blockchain tip. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if response text extraction fails + /// - Returns `ExplorerError::BitcoinHashesHex` if block hash parsing fails pub async fn get_blocks_tip_hash(&self) -> Result { let url = self.url_builder.get_blocks_tip_hash_url()?; let resp = self @@ -1007,6 +1241,12 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves mempool information. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails pub async fn get_mempool(&self) -> Result { let url = self.url_builder.get_mempool_url()?; let resp = self @@ -1020,6 +1260,13 @@ impl EsploraClientAsync { resp.json().await.map_err(|e| ExplorerError::deserialize_reqwest(&e)) } + /// Retrieves all transaction IDs in the mempool. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub async fn get_mempool_txids(&self) -> Result, ExplorerError> { let url = self.url_builder.get_mempool_txids_url()?; let resp = self @@ -1041,6 +1288,13 @@ impl EsploraClientAsync { Ok(resp) } + /// Retrieves recent mempool transactions. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub async fn get_mempool_recent(&self) -> Result, ExplorerError> { let url = self.url_builder.get_mempool_recent_url()?; let resp = self @@ -1055,10 +1309,19 @@ impl EsploraClientAsync { .json::>() .await .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; - let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + let resp = resp + .into_iter() + .map(deserializable::TypeConversion::convert) + .collect::>()?; Ok(resp) } + /// Retrieves fee estimates. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_reqwest` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_reqwest` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_reqwest` if JSON deserialization fails pub async fn get_fee_estimates(&self) -> Result { let url = self.url_builder.get_fee_estimates_url()?; let resp = self @@ -1084,123 +1347,171 @@ impl EsploraClientSync { Ok(()) } + /// Retrieves transaction details by transaction ID. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub fn get_tx(&self, txid: &str) -> Result { let url: String = self.url_builder.get_tx_url(txid)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::() - .map_err(|e| ExplorerError::deserialize_minreq(e))?; + .map_err(ExplorerError::deserialize_minreq)?; let resp = resp.convert()?; Ok(resp) } + /// Retrieves transaction status by transaction ID. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if block hash parsing fails pub fn get_tx_status(&self, txid: &str) -> Result { let url: String = self.url_builder.get_tx_status_url(txid)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::() - .map_err(|e| ExplorerError::deserialize_minreq(e))?; + .map_err(ExplorerError::deserialize_minreq)?; let resp = resp.convert()?; Ok(resp) } + /// Retrieves transaction hex by transaction ID. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if response text extraction fails pub fn get_tx_hex(&self, txid: &str) -> Result { let url: String = self.url_builder.get_tx_hex_url(txid)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; - Ok(resp - .as_str() - .map_err(|e| ExplorerError::deserialize_minreq(e))? - .to_string()) + Ok(resp.as_str().map_err(ExplorerError::deserialize_minreq)?.to_string()) } + /// Retrieves raw transaction bytes by transaction ID. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code pub fn get_tx_raw(&self, txid: &str) -> Result, ExplorerError> { let url: String = self.url_builder.get_tx_raw_url(txid)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; Ok(resp.as_bytes().to_vec()) } + /// Retrieves and deserializes a transaction as an Elements transaction. + /// + /// # Errors + /// - Returns all errors from `get_tx_raw` + /// - Returns `ExplorerError::TransactionDecode` if transaction deserialization fails pub fn get_tx_elements(&self, txid: &str) -> Result { let bytes = self.get_tx_raw(txid)?; simplicityhl::elements::Transaction::deserialize(&bytes) .map_err(|e| ExplorerError::TransactionDecode(e.to_string())) } + /// Retrieves merkle proof for a transaction. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if merkle hash parsing fails pub fn get_tx_merkle_proof(&self, txid: &str) -> Result { let url: String = self.url_builder.get_tx_merkle_proof_url(txid)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + .map_err(ExplorerError::response_failed_minreq)?; let resp = resp.convert()?; Ok(resp) } + /// Retrieves outspend information for a specific output. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub fn get_tx_outspend(&self, txid: &str, vout: u32) -> Result { let url: String = self.url_builder.get_tx_outspend_url(txid, vout)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + .map_err(ExplorerError::response_failed_minreq)?; let resp = resp.convert()?; Ok(resp) } + /// Retrieves outspend information for all outputs of a transaction. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub fn get_tx_outspends(&self, txid: &str) -> Result, ExplorerError> { let url: String = self.url_builder.get_tx_outspends_url(txid)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::>() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; - resp.into_iter().map(|x| x.convert()).collect::, _>>() - } - + .map_err(ExplorerError::response_failed_minreq)?; + resp.into_iter() + .map(deserializable::TypeConversion::convert) + .collect::, _>>() + } + + /// Broadcasts a transaction to the network. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON serialization or response text extraction fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub fn broadcast_tx(&self, tx: &simplicityhl::elements::Transaction) -> Result { let tx_hex = simplicityhl::elements::encode::serialize_hex(tx); let url: String = self.url_builder.get_broadcast_tx_url()?; let resp = minreq::post(url) .with_json(&tx_hex) - .map_err(|e| ExplorerError::deserialize_minreq(e))? + .map_err(ExplorerError::deserialize_minreq)? .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + .map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .as_str() - .map_err(|e| ExplorerError::response_failed_minreq(e))? + .map_err(ExplorerError::response_failed_minreq)? .to_string(); Ok(Txid::from_str(&resp)?) } - // TODO: add batch execution with 10 elements + /// Broadcasts a package of transactions. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code or JSON parsing fails + /// - Returns `ExplorerError::deserialize_minreq` if JSON serialization fails pub fn broadcast_tx_package( &self, txs: &[simplicityhl::elements::Transaction], @@ -1213,42 +1524,62 @@ impl EsploraClientSync { let resp = minreq::post(url) .with_json(&tx_hexes) - .map_err(|e| ExplorerError::deserialize_minreq(e))? + .map_err(ExplorerError::deserialize_minreq)? .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + .map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; - resp.json().map_err(|e| ExplorerError::response_failed_minreq(e)) + resp.json().map_err(ExplorerError::response_failed_minreq) } + /// Retrieves address information. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::AddressConversion` if address parsing fails pub fn get_address(&self, address: &str) -> Result { let url: String = self.url_builder.get_address_url(address)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + .map_err(ExplorerError::response_failed_minreq)?; let resp = resp.convert()?; Ok(resp) } + /// Retrieves all transactions for an address. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID/block hash parsing fails pub fn get_address_txs(&self, address: &str) -> Result, ExplorerError> { let url: String = self.url_builder.get_address_txs_url(address)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::>() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; - let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + .map_err(ExplorerError::response_failed_minreq)?; + let resp = resp + .into_iter() + .map(deserializable::TypeConversion::convert) + .collect::>()?; Ok(resp) } + /// Retrieves confirmed transactions for an address with pagination. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID/block hash parsing fails pub fn get_address_txs_chain( &self, address: &str, @@ -1256,156 +1587,210 @@ impl EsploraClientSync { ) -> Result, ExplorerError> { let url: String = self.url_builder.get_address_txs_chain_url(address, last_seen_txid)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::>() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; - let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + .map_err(ExplorerError::response_failed_minreq)?; + let resp = resp + .into_iter() + .map(deserializable::TypeConversion::convert) + .collect::>()?; Ok(resp) } + /// Retrieves mempool transactions for an address. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub fn get_address_txs_mempool(&self, address: &str) -> Result, ExplorerError> { let url: String = self.url_builder.get_address_txs_mempool_url(address)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::>() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; - let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + .map_err(ExplorerError::response_failed_minreq)?; + let resp = resp + .into_iter() + .map(deserializable::TypeConversion::convert) + .collect::>()?; Ok(resp) } + /// Retrieves UTXOs for an address. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::HexSimdDecode` if hex decoding fails + /// - Returns `ExplorerError::CommitmentDecode` if commitment parsing fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID/block hash parsing fails pub fn get_address_utxo(&self, address: &str) -> Result, ExplorerError> { let url: String = self.url_builder.get_address_utxo_url(address)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::>() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; - resp.into_iter().map(|x| x.convert()).collect::, _>>() - } - + .map_err(ExplorerError::response_failed_minreq)?; + resp.into_iter() + .map(deserializable::TypeConversion::convert) + .collect::, _>>() + } + + /// Retrieves scripthash information. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::ElementsHex` if script parsing fails pub fn get_scripthash(&self, hash: &str) -> Result { let url: String = self.url_builder.get_scripthash_url(hash)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + .map_err(ExplorerError::response_failed_minreq)?; let resp = resp.convert()?; Ok(resp) } - // TODO: check output + /// Retrieves transactions for a scripthash. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if response text extraction fails pub fn get_scripthash_txs(&self, hash: &str) -> Result { let url: String = self.url_builder.get_scripthash_txs_url(hash)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; Ok(resp .as_str() - .map_err(|e| ExplorerError::response_failed_minreq(e))? + .map_err(ExplorerError::response_failed_minreq)? .to_string()) } - // TODO: check output + /// Retrieves confirmed transactions for a scripthash with pagination. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if response text extraction fails pub fn get_scripthash_txs_chain(&self, hash: &str, last_seen_txid: Option<&str>) -> Result { let url: String = self.url_builder.get_scripthash_txs_chain_url(hash, last_seen_txid)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; Ok(resp .as_str() - .map_err(|e| ExplorerError::response_failed_minreq(e))? + .map_err(ExplorerError::response_failed_minreq)? .to_string()) } - // TODO: check output + /// Retrieves mempool transactions for a scripthash. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if response text extraction fails pub fn get_scripthash_txs_mempool(&self, hash: &str) -> Result { let url: String = self.url_builder.get_scripthash_txs_mempool_url(hash)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; Ok(resp .as_str() - .map_err(|e| ExplorerError::response_failed_minreq(e))? + .map_err(ExplorerError::response_failed_minreq)? .to_string()) } - // TODO: check output + /// Retrieves UTXOs for a scripthash. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if response text extraction fails pub fn get_scripthash_utxo(&self, hash: &str) -> Result { let url: String = self.url_builder.get_scripthash_utxo_url(hash)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; Ok(resp .as_str() - .map_err(|e| ExplorerError::response_failed_minreq(e))? + .map_err(ExplorerError::response_failed_minreq)? .to_string()) } + /// Retrieves block information by block hash. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if hash/merkle node parsing fails pub fn get_block(&self, hash: &str) -> Result { let url: String = self.url_builder.get_block_url(hash)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + .map_err(ExplorerError::response_failed_minreq)?; let resp = resp.convert()?; Ok(resp) } - // TODO: decode hex into elements::BlockHeader (no method to do this) + /// Retrieves block header as hex string. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if response text extraction fails pub fn get_block_header(&self, hash: &str) -> Result { let url: String = self.url_builder.get_block_header_url(hash)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .as_str() - .map_err(|e| ExplorerError::response_failed_minreq(e))? + .map_err(ExplorerError::response_failed_minreq)? .to_string(); Ok(resp) } + /// Retrieves block status information. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails pub fn get_block_status(&self, hash: &str) -> Result { let url: String = self.url_builder.get_block_status_url(hash)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; resp.json::() - .map_err(|e| ExplorerError::response_failed_minreq(e)) + .map_err(ExplorerError::response_failed_minreq) } + /// Retrieves transactions in a block with optional pagination. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID/block hash parsing fails pub fn get_block_txs( &self, hash: &str, @@ -1413,28 +1798,34 @@ impl EsploraClientSync { ) -> Result, ExplorerError> { let url: String = self.url_builder.get_block_txs_url(hash, start_index)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::>() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; - let resp = resp.into_iter().map(|val| val.convert()).collect::>()?; + .map_err(ExplorerError::response_failed_minreq)?; + let resp = resp + .into_iter() + .map(deserializable::TypeConversion::convert) + .collect::>()?; Ok(resp) } + /// Retrieves transaction IDs in a block. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub fn get_block_txids(&self, hash: &str) -> Result, ExplorerError> { let url: String = self.url_builder.get_block_txids_url(hash)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::>() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + .map_err(ExplorerError::response_failed_minreq)?; let resp = resp .into_iter() @@ -1443,101 +1834,138 @@ impl EsploraClientSync { Ok(resp) } + /// Retrieves a specific transaction ID from a block. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if response text extraction fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub fn get_block_txid(&self, hash: &str, index: u32) -> Result { let url: String = self.url_builder.get_block_txid_url(hash, index)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; - let resp = resp.as_str().map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.as_str().map_err(ExplorerError::response_failed_minreq)?; - Ok(Txid::from_str(&resp)?) + Ok(Txid::from_str(resp)?) } + /// Retrieves raw block bytes. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code pub fn get_block_raw(&self, hash: &str) -> Result, ExplorerError> { let url: String = self.url_builder.get_block_raw_url(hash)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; Ok(resp.as_bytes().to_vec()) } + /// Retrieves block hash by block height. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if response text extraction fails + /// - Returns `ExplorerError::BitcoinHashesHex` if block hash parsing fails pub fn get_block_height(&self, height: u64) -> Result { let url: String = self.url_builder.get_block_height_url(height)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; - let resp = resp.as_str().map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.as_str().map_err(ExplorerError::response_failed_minreq)?; - let resp = BlockHash::from_str(&resp)?; + let resp = BlockHash::from_str(resp)?; Ok(resp) } + /// Retrieves blocks starting from a given height. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if hash/merkle node parsing fails pub fn get_blocks(&self, start_height: Option) -> Result, ExplorerError> { let url = self.url_builder.get_blocks_url(start_height)?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::>() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; - let resp = resp.into_iter().map(|val| val.convert()).collect::>()?; + .map_err(ExplorerError::response_failed_minreq)?; + let resp = resp + .into_iter() + .map(deserializable::TypeConversion::convert) + .collect::>()?; Ok(resp) } + /// Retrieves the height of the blockchain tip. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails pub fn get_blocks_tip_height(&self) -> Result { let url: String = self.url_builder.get_blocks_tip_height_url()?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; - let resp = resp - .json::() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = resp.json::().map_err(ExplorerError::response_failed_minreq)?; Ok(resp) } + /// Retrieves the hash of the blockchain tip. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if response text extraction fails + /// - Returns `ExplorerError::BitcoinHashesHex` if block hash parsing fails pub fn get_blocks_tip_hash(&self) -> Result { let url: String = self.url_builder.get_blocks_tip_hash_url()?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; - let resp = resp.as_str().map_err(|e| ExplorerError::response_failed_minreq(e))?; - let resp = BlockHash::from_str(&resp)?; + let resp = resp.as_str().map_err(ExplorerError::response_failed_minreq)?; + let resp = BlockHash::from_str(resp)?; Ok(resp) } + /// Retrieves mempool information. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails pub fn get_mempool(&self) -> Result { let url: String = self.url_builder.get_mempool_url()?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; - resp.json().map_err(|e| ExplorerError::response_failed_minreq(e)) + resp.json().map_err(ExplorerError::response_failed_minreq) } + /// Retrieves all transaction IDs in the mempool. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub fn get_mempool_txids(&self) -> Result, ExplorerError> { let url: String = self.url_builder.get_mempool_txids_url()?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::>() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + .map_err(ExplorerError::response_failed_minreq)?; let resp = resp .into_iter() .map(|val| Txid::from_str(&val)) @@ -1545,29 +1973,41 @@ impl EsploraClientSync { Ok(resp) } + /// Retrieves recent mempool transactions. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails + /// - Returns `ExplorerError::BitcoinHashesHex` if TXID parsing fails pub fn get_mempool_recent(&self) -> Result, ExplorerError> { let url: String = self.url_builder.get_mempool_recent_url()?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; let resp = resp .json::>() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; - let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + .map_err(ExplorerError::response_failed_minreq)?; + let resp = resp + .into_iter() + .map(deserializable::TypeConversion::convert) + .collect::>()?; Ok(resp) } + /// Retrieves fee estimates. + /// + /// # Errors + /// - Returns `ExplorerError::response_failed_minreq` if the HTTP request fails + /// - Returns `ExplorerError::erroneous_response_minreq` if the API returns an error status code + /// - Returns `ExplorerError::deserialize_minreq` if JSON deserialization fails pub fn get_fee_estimates(&self) -> Result { let url: String = self.url_builder.get_fee_estimates_url()?; - let resp = minreq::get(url) - .send() - .map_err(|e| ExplorerError::response_failed_minreq(e))?; + let resp = minreq::get(url).send().map_err(ExplorerError::response_failed_minreq)?; Self::filter_resp(&resp)?; resp.json::() - .map_err(|e| ExplorerError::response_failed_minreq(e)) + .map_err(ExplorerError::response_failed_minreq) } } @@ -1579,36 +2019,47 @@ impl UrlBuilder { fn get_tx_url(&self, txid: &str) -> Result { self.join_url(format!("/tx/{txid}")) } + fn get_tx_status_url(&self, txid: &str) -> Result { self.join_url(format!("tx/{txid}/status")) } + fn get_tx_hex_url(&self, txid: &str) -> Result { self.join_url(format!("tx/{txid}/hex")) } + fn get_tx_raw_url(&self, txid: &str) -> Result { self.join_url(format!("tx/{txid}/raw")) } + fn get_tx_merkle_proof_url(&self, txid: &str) -> Result { self.join_url(format!("tx/{txid}/merkle-proof")) } + fn get_tx_outspend_url(&self, txid: &str, vout: u32) -> Result { self.join_url(format!("tx/{txid}/outspend/{vout}")) } + fn get_tx_outspends_url(&self, txid: &str) -> Result { self.join_url(format!("tx/{txid}/outspends")) } + fn get_broadcast_tx_url(&self) -> Result { self.join_url("tx") } + fn get_broadcast_tx_package_url(&self) -> Result { self.join_url("txs/package") } + fn get_address_url(&self, address: &str) -> Result { self.join_url(format!("address/{address}")) } + fn get_address_txs_url(&self, address: &str) -> Result { self.join_url(format!("address/{address}/txs")) } + fn get_address_txs_chain_url(&self, address: &str, last_seen_txid: Option<&str>) -> Result { if let Some(txid) = last_seen_txid { self.join_url(format!("address/{address}/txs/chain/{txid}")) @@ -1616,18 +2067,23 @@ impl UrlBuilder { self.join_url(format!("address/{address}/txs/chain")) } } + fn get_address_txs_mempool_url(&self, address: &str) -> Result { self.join_url(format!("address/{address}/txs/mempool")) } + fn get_address_utxo_url(&self, address: &str) -> Result { self.join_url(format!("address/{address}/utxo")) } + fn get_scripthash_url(&self, hash: &str) -> Result { self.join_url(format!("scripthash/{hash}")) } + fn get_scripthash_txs_url(&self, hash: &str) -> Result { self.join_url(format!("scripthash/{hash}/txs")) } + fn get_scripthash_txs_chain_url(&self, hash: &str, last_seen_txid: Option<&str>) -> Result { if let Some(txid) = last_seen_txid { self.join_url(format!("scripthash/{hash}/txs/chain/{txid}")) @@ -1635,21 +2091,27 @@ impl UrlBuilder { self.join_url(format!("scripthash/{hash}/txs/chain")) } } + fn get_scripthash_txs_mempool_url(&self, hash: &str) -> Result { self.join_url(format!("scripthash/{hash}/txs/mempool")) } + fn get_scripthash_utxo_url(&self, hash: &str) -> Result { self.join_url(format!("scripthash/{hash}/utxo")) } + fn get_block_url(&self, hash: &str) -> Result { self.join_url(format!("block/{hash}")) } + fn get_block_header_url(&self, hash: &str) -> Result { self.join_url(format!("block/{hash}/header")) } + fn get_block_status_url(&self, hash: &str) -> Result { self.join_url(format!("block/{hash}/status")) } + fn get_block_txs_url(&self, hash: &str, start_index: Option) -> Result { if let Some(index) = start_index { self.join_url(format!("block/{hash}/txs/{index}")) @@ -1657,18 +2119,23 @@ impl UrlBuilder { self.join_url(format!("block/{hash}/txs")) } } + fn get_block_txids_url(&self, hash: &str) -> Result { self.join_url(format!("block/{hash}/txids")) } + fn get_block_txid_url(&self, hash: &str, index: u32) -> Result { self.join_url(format!("block/{hash}/txid/{index}")) } + fn get_block_raw_url(&self, hash: &str) -> Result { self.join_url(format!("block/{hash}/raw")) } + fn get_block_height_url(&self, height: u64) -> Result { self.join_url(format!("block-height/{height}")) } + fn get_blocks_url(&self, start_height: Option) -> Result { if let Some(height) = start_height { self.join_url(format!("blocks/{height}")) @@ -1676,21 +2143,27 @@ impl UrlBuilder { self.join_url("blocks") } } + fn get_blocks_tip_height_url(&self) -> Result { self.join_url("blocks/tip/height") } + fn get_blocks_tip_hash_url(&self) -> Result { self.join_url("blocks/tip/hash") } + fn get_mempool_url(&self) -> Result { self.join_url("mempool") } + fn get_mempool_txids_url(&self) -> Result { self.join_url("mempool/txids") } + fn get_mempool_recent_url(&self) -> Result { self.join_url("mempool/recent") } + fn get_fee_estimates_url(&self) -> Result { self.join_url("fee-estimates") } @@ -1701,7 +2174,6 @@ trait BaseUrlGetter { } trait UrlAppender { - #[inline] fn join_url(&self, str: impl AsRef) -> Result; } diff --git a/crates/provider/src/esplora/types.rs b/crates/provider/src/esplora/types.rs index 95dfda9..5f0a733 100644 --- a/crates/provider/src/esplora/types.rs +++ b/crates/provider/src/esplora/types.rs @@ -73,16 +73,22 @@ pub type MempoolStats = ChainStats; #[derive(Debug, Clone, Deserialize, Hash, Eq, PartialEq)] pub struct ChainStats { - pub funded_txo_count: u64, - pub spent_txo_count: u64, - pub tx_count: u64, + #[serde(rename = "funded_txo_count")] + pub funded_txo: u64, + #[serde(rename = "spent_txo_count")] + pub spent_txo: u64, + #[serde(rename = "tx_count")] + pub tx: u64, } #[derive(Debug, Clone, Deserialize, Hash, Eq, PartialEq)] pub struct Stats { - pub tx_count: u64, - pub funded_txo_count: u64, - pub spent_txo_count: u64, + #[serde(rename = "tx_count")] + pub tx: u64, + #[serde(rename = "funded_txo_count")] + pub funded_txo: u64, + #[serde(rename = "spent_txo_count")] + pub spent_txo: u64, } #[allow(dead_code)] diff --git a/crates/provider/src/lib.rs b/crates/provider/src/lib.rs index fdb7040..7c1ac7c 100644 --- a/crates/provider/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -1,3 +1,5 @@ +#![warn(clippy::all, clippy::pedantic)] + pub mod elements_rpc; mod error; pub mod esplora; diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs index 8d89469..64f606b 100644 --- a/crates/sdk/src/error.rs +++ b/crates/sdk/src/error.rs @@ -62,5 +62,5 @@ pub enum SimplexError { HexToArray(#[from] HexToArrayError), #[error("Failed to execute provider method: '{method}', err: '{err}'")] - ProviderError { method: String, err: ExplorerError }, + ProviderError { method: String, err: Box }, } diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 7ef9571..7b167c9 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1,3 +1,5 @@ +#![warn(clippy::all, clippy::pedantic)] + pub mod arguments; pub mod constants; pub mod error; diff --git a/crates/sdk/src/provider/esplora.rs b/crates/sdk/src/provider/esplora.rs index 3ce3cdf..388c180 100644 --- a/crates/sdk/src/provider/esplora.rs +++ b/crates/sdk/src/provider/esplora.rs @@ -9,14 +9,14 @@ impl ProviderSync for EsploraClientSync { fn broadcast_transaction(&self, tx: &Transaction) -> Result { self.broadcast_tx(tx).map_err(|e| SimplexError::ProviderError { method: "broadcast_tx".to_string(), - err: e, + err: Box::new(e), }) } fn fetch_fee_estimates(&self) -> Result, SimplexError> { self.get_fee_estimates().map_err(|e| SimplexError::ProviderError { method: "get_fee_estimates".to_string(), - err: e, + err: Box::new(e), }) } @@ -24,7 +24,7 @@ impl ProviderSync for EsploraClientSync { self.get_tx_elements(&txid.to_hex()) .map_err(|e| SimplexError::ProviderError { method: "get_tx_elements".to_string(), - err: e, + err: Box::new(e), }) } } @@ -34,14 +34,14 @@ impl ProviderAsync for EsploraClientAsync { async fn broadcast_transaction(&self, tx: &Transaction) -> Result { self.broadcast_tx(tx).await.map_err(|e| SimplexError::ProviderError { method: "broadcast_tx".to_string(), - err: e, + err: Box::new(e), }) } async fn fetch_fee_estimates(&self) -> Result, SimplexError> { self.get_fee_estimates().await.map_err(|e| SimplexError::ProviderError { method: "get_fee_estimates".to_string(), - err: e, + err: Box::new(e), }) } @@ -50,7 +50,7 @@ impl ProviderAsync for EsploraClientAsync { .await .map_err(|e| SimplexError::ProviderError { method: "get_tx_elements".to_string(), - err: e, + err: Box::new(e), }) } } diff --git a/crates/sdk/src/witness_transaction.rs b/crates/sdk/src/witness_transaction.rs index 9b5addb..3f4b58d 100644 --- a/crates/sdk/src/witness_transaction.rs +++ b/crates/sdk/src/witness_transaction.rs @@ -187,6 +187,7 @@ where Ok(final_tx) } + #[allow(clippy::too_many_arguments)] fn finalize_tx_with_signer( &self, final_tx: Transaction, @@ -200,7 +201,7 @@ where let signature = signer.sign(program, &final_tx, utxos, index, self.network)?; let new_witness = signer_lambda(&witness, &signature)?; - Ok(self.finalize_tx_as_is(final_tx, utxos, program, new_witness, index)?) + self.finalize_tx_as_is(final_tx, utxos, program, new_witness, index) } fn finalize_tx_as_is( @@ -211,7 +212,7 @@ where witness: WitnessValues, index: usize, ) -> Result { - Ok(program.finalize(witness, final_tx, utxos, index, self.network)?) + program.finalize(witness, final_tx, utxos, index, self.network) } fn calculate_fee_delta(&self) -> u64 { @@ -220,14 +221,14 @@ where .inputs() .iter() .filter(|input| input.asset.unwrap() == self.network.policy_asset()) - .fold(0 as u64, |acc, input| acc + input.amount.unwrap()); + .fold(0_u64, |acc, input| acc + input.amount.unwrap()); let consumed_amount = self .pst .outputs() .iter() .filter(|output| output.asset.unwrap() == self.network.policy_asset()) - .fold(0 as u64, |acc, output| acc + output.amount.unwrap()); + .fold(0_u64, |acc, output| acc + output.amount.unwrap()); available_amount - consumed_amount } diff --git a/crates/simplex/src/lib.rs b/crates/simplex/src/lib.rs index 6311730..3e58b17 100644 --- a/crates/simplex/src/lib.rs +++ b/crates/simplex/src/lib.rs @@ -1,3 +1,5 @@ +#![warn(clippy::all, clippy::pedantic)] + pub extern crate either; pub extern crate serde; diff --git a/examples/options/src/lib.rs b/examples/options/src/lib.rs index f54c04a..2dc508d 100644 --- a/examples/options/src/lib.rs +++ b/examples/options/src/lib.rs @@ -1,3 +1,5 @@ +#![warn(clippy::all, clippy::pedantic)] + mod program; pub use program::*;