diff --git a/.gitignore b/.gitignore index f2880e6..82cb3bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Build output -/target/ +target/ # IDE/editors (optional but common minimal) **/.DS_Store @@ -7,7 +7,7 @@ .vscode/ .cache .env -Simplex.toml +./Simplex.toml config.toml # Debugging data diff --git a/Cargo.lock b/Cargo.lock index 4fe6324..b70ff78 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" @@ -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" @@ -461,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" @@ -1022,15 +1056,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" @@ -1136,6 +1161,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" @@ -1172,6 +1203,14 @@ 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" @@ -1179,27 +1218,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +name = "p2pk" +version = "0.1.0" dependencies = [ - "lock_api", - "parking_lot_core", + "simplex", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "pathdiff" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "percent-encoding" @@ -1207,6 +1237,49 @@ 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 = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" version = "0.2.16" @@ -1228,6 +1301,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" @@ -1419,15 +1498,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" @@ -1636,12 +1706,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" @@ -1791,16 +1855,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" @@ -1809,24 +1863,16 @@ dependencies = [ "bincode", "either", "serde", - "simplex-core", "simplex-macros", - "simplex-runtime", + "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", ] -[[package]] -name = "simplex-build" -version = "0.1.0" -dependencies = [ - "simplex-sdk", - "simplicityhl", - "thiserror", -] - [[package]] name = "simplex-cli" version = "0.1.0" @@ -1836,87 +1882,70 @@ dependencies = [ "ctrlc", "dotenvy", "electrsd", + "glob", "serde", - "simplex-config", + "simplex-macros-core", + "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", ] [[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" +name = "simplex-macros" version = "0.1.0" dependencies = [ - "bincode", - "hex", - "minreq", "serde", - "sha2", - "simplicityhl", - "thiserror", + "simplex-macros-core", + "syn 2.0.116", ] [[package]] -name = "simplex-macro-core" +name = "simplex-macros-core" version = "0.1.0" dependencies = [ + "pathdiff", + "prettyplease", "proc-macro-error", "proc-macro2", "quote", - "simplicityhl", - "syn 2.0.116", - "thiserror", -] - -[[package]] -name = "simplex-macros" -version = "0.1.0" -dependencies = [ "serde", - "simplex-macro-core", + "simplex-test", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", "syn 2.0.116", + "thiserror", ] [[package]] -name = "simplex-runtime" +name = "simplex-provider" version = "0.1.0" dependencies = [ "async-trait", "bitcoin_hashes", "electrsd", "hex-simd", + "minreq", "reqwest", "serde", "serde_json", - "simplex-core", - "simplicityhl", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", "thiserror", - "tokio", ] [[package]] name = "simplex-sdk" version = "0.1.0" dependencies = [ + "async-trait", "minreq", "sha2", - "simplex-runtime", - "simplicityhl", + "simplex-provider", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", "thiserror", ] @@ -1925,12 +1954,12 @@ name = "simplex-test" version = "0.1.0" dependencies = [ "electrsd", - "simplex-config", - "simplex-core", - "simplex-runtime", + "serde", + "simplex-provider", "simplex-sdk", - "simplicityhl", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", "thiserror", + "toml 0.9.12+spec-1.1.0", ] [[package]] @@ -1968,6 +1997,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" @@ -1985,6 +2033,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" @@ -2136,6 +2196,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" @@ -2170,9 +2261,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -2318,6 +2407,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" @@ -2395,6 +2496,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 2b7afd3..397eb18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [workspace] resolver = "3" members = [ - "crates/*" + "crates/*", + "examples/options", + "examples/p2pk", + "examples/test_usage", ] [workspace.package] @@ -12,19 +15,20 @@ 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-core = { path = "./crates/macros-core", features = ["bincode", "serde"] } 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" } +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" } +toml = { version = "0.9.8" } hex = { version = "0.4.3" } tracing = { version = "0.1.41" } 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/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/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..b331c99 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -16,18 +16,24 @@ workspace = true [dependencies] simplex-test = { workspace = true } -simplex-config = { workspace = true } - -serde = { version = "1.0.228" } -toml = { version = "0.9.8" } +simplex-sdk = { workspace = true } +simplex-macros-core = { workspace = true } +simplicityhl = { workspace = true } +electrsd = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } +toml = { workspace = true } 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 } +glob = { version = "0.3.3"} + +[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/cache_storage.rs b/crates/cli/src/cache_storage.rs new file mode 100644 index 0000000..5fff7e9 --- /dev/null +++ b/crates/cli/src/cache_storage.rs @@ -0,0 +1,37 @@ +use crate::error::Error; +use simplex_test::ElementsDConf; +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: &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); + + 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 75b0c7a..10e0314 100644 --- a/crates/cli/src/cli/commands.rs +++ b/crates/cli/src/cli/commands.rs @@ -1,9 +1,52 @@ -use clap::Subcommand; +use clap::{Args, Subcommand}; +use std::path::PathBuf; #[derive(Debug, Subcommand)] pub enum Command { - /// Show current configuration + /// Initialize a project with the default configuration + Init, + /// Show the current configuration Config, /// Launch `elementsd` in regtest mode with a default config Regtest, + /// Launch test with + Test { + #[command(subcommand)] + command: TestCommand, + }, + Build { + #[arg(env = "OUT_DIR")] + out_dir: Option, + }, +} + +/// Test management commands +#[derive(Debug, Subcommand)] +pub enum TestCommand { + /// Run integration tests using simplex conventions + 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, + }, +} + +/// 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, + /// 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 35d990b..c09cd8c 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -1,45 +1,72 @@ pub mod commands; +use crate::cache_storage::CacheStorage; +use crate::cli::commands::{Command, TestCommand, TestFlags}; +use crate::config::{Config, DEFAULT_CONFIG}; use crate::error::Error; use clap::Parser; -use simplex_config::Config; -use simplex_test::TestProvider; +use simplex_macros_core::env::CodeGenerator; +use simplex_test::TestClientProvider; use std::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, } -impl Cli { - #[must_use] - pub fn load_config(&self) -> Config { - Config::load_or_default(&self.config) - } +struct TestParams { + cache_path: PathBuf, + test_path: TestPaths, + test_flags: TestFlags, +} + +enum TestPaths { + AllIntegration, + Names(Vec), +} +impl Cli { /// 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.clone()).map_err(|e| Error::ConfigDiscoveryFailure(e))?; + println!("{loaded_config:#?}"); + Ok(()) + } + 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)?; + 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(); @@ -48,7 +75,8 @@ 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(loaded_config.test_config.elemendsd_path); println!("======================================"); println!("Waiting for Ctrl-C..."); @@ -62,11 +90,109 @@ impl Cli { println!("Exiting..."); Ok(()) } + // 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))?; + + 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(()) + } } } -} -#[must_use] -pub fn default_config_path() -> PathBuf { - PathBuf::from(DEFAULT_CONFIG_PATH) + 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 { additional_flags } => Self::form_test_command(TestParams { + cache_path, + test_path: TestPaths::AllIntegration, + test_flags: *additional_flags, + }), + TestCommand::Run { + tests, + additional_flags, + } => { + 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: *additional_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"); + let mut command_as_arg = String::new(); + match params.test_path { + TestPaths::AllIntegration => { + command_as_arg.push_str("cargo test --tests"); + } + TestPaths::Names(names) => { + let mut arg = "cargo test".to_string(); + for test_name in names { + arg.push_str(&format!(" --test {test_name}")); + } + command_as_arg.push_str(&arg); + } + } + { + 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]); + dbg!(test_command.get_args()); + test_command + .env(simplex_test::TEST_ENV_NAME, params.cache_path) + .stdin(Stdio::inherit()) + .stderr(Stdio::inherit()) + .stdout(Stdio::inherit()); + test_command + } } diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index ea5b6c6..41a31f1 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -1,10 +1,11 @@ use serde::{Deserialize, Serialize}; -use simplex_core::SimplicityNetwork; -use std::fmt::Display; +use simplex_sdk::constants::SimplicityNetwork; +use simplex_test::{ElementsDConf, RpcCreds}; +use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; -const MANIFEST_DIR: &str = "CARGO_MANIFEST_DIR"; +pub const DEFAULT_CONFIG: &str = include_str!("../../../Simplex.example.toml"); const CONFIG_FILENAME: &str = "Simplex.toml"; #[derive(thiserror::Error, Debug)] @@ -31,85 +32,95 @@ 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)] +#[derive(Debug, Clone)] pub struct Config { - pub provider_config: ProviderConfig, - pub test_config: TestConfig, + pub provider_config: ProviderConf, + pub test_config: ElementsDConf, + pub build_config: Option, } #[derive(Debug, Clone)] -pub struct ProviderConfig { +pub struct ProviderConf { simplicity_network: SimplicityNetwork, } -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct TestConfig { - pub rpc_creds: RpcCreds, +#[derive(Debug, Default, Clone)] +pub struct ConfigOverride { + pub rpc_creds: Option, + pub network: Option, } -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub enum RpcCreds { - Auth { - rpc_username: String, - rpc_password: String, - }, - #[default] - None, +#[derive(Debug, Clone, Deserialize)] +pub struct BuildConf { + pub compile_simf: Vec, + pub out_dir: PathBuf, } -#[derive(Debug, Default, Clone)] -pub struct ConfigOverride { - pub rpc_creds: Option, - pub network: Option, +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct _BuildConf { + compile_simf: Vec, + out_dir: PathBuf, } -impl Default for ProviderConfig { +impl Default for ProviderConf { fn default() -> Self { - ProviderConfig { + ProviderConf { 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 get_path() -> Result { + let cwd = std::env::current_dir()?; + Ok(cwd.join(CONFIG_FILENAME)) } - 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() + 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; + } + } + Ok(cfg) } - }) + Err(e) => Err(e), + } } - fn _discover() -> Result, ConfigError> { - let cwd = std::env::current_dir()?; - let path = cwd.join(CONFIG_FILENAME); + pub fn load(path_buf: impl AsRef) -> Result { + Self::from_path(&path_buf) + } + + pub fn load_or_discover(path_buf: Option>) -> Result { + match path_buf { + Some(path) => Self::load(path), + None => Self::_discover(), + } + } + + fn _discover() -> Result { + let path = Self::get_path()?; dbg!(&path); if !path.is_file() { 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 { @@ -123,10 +134,28 @@ 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.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, + }), + 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)?, + }), + }, }) } } @@ -134,7 +163,14 @@ impl FromStr for Config { #[derive(Debug, Serialize, Deserialize)] struct _Config { network: Option<_NetworkName>, - test: Option, + test: Option, + build: Option<_BuildConf>, +} + +#[derive(Debug, Serialize, Deserialize)] +struct TestingConfig { + elementsd_path: Option, + rpc_creds: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] @@ -155,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/cli/src/error.rs b/crates/cli/src/error.rs index 02f0125..aa41540 100644 --- a/crates/cli/src/error.rs +++ b/crates/cli/src/error.rs @@ -4,26 +4,34 @@ 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}")] - ConfigError(#[from] simplex_config::ConfigError), + #[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), + + /// 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 4e66fdb..eb7d538 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,3 +1,7 @@ +#![warn(clippy::all, clippy::pedantic)] + +pub mod cache_storage; pub mod cli; +pub mod config; pub mod error; pub mod logging; 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..cda6b9b --- /dev/null +++ b/crates/cli/tests/hello/test333.rs @@ -0,0 +1,32 @@ +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() -> 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}'" + ); + 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(); + test_context + } + }; + tracing::trace!("Running 'test_in_custom_folder_custom_333' with simplex configuration"); + test_in_custom_folder_custom_333(test_context) +} + +#[test] +fn test_in_custom_folder2_custom_333() { + assert_eq!(2 + 2, 4); +} diff --git a/crates/cli/tests/test2.rs b/crates/cli/tests/test2.rs new file mode 100644 index 0000000..9542e02 --- /dev/null +++ b/crates/cli/tests/test2.rs @@ -0,0 +1,25 @@ +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); +} 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/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 index f8e1b29..5c70ea1 100644 --- a/crates/macros-core/Cargo.toml +++ b/crates/macros-core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "simplex-macro-core" +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 @@ -10,7 +10,7 @@ workspace = true [features] bincode = [] -serde = ["bincode"] +serde = ["bincode", "dep:serde"] default = ["bincode", "serde"] [dependencies] @@ -19,5 +19,9 @@ 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-core/src/attr/codegen.rs b/crates/macros-core/src/attr/codegen.rs index c1625db..a944b50 100644 --- a/crates/macros-core/src/attr/codegen.rs +++ b/crates/macros-core/src/attr/codegen.rs @@ -1,5 +1,6 @@ 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, @@ -105,28 +106,21 @@ 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 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 }, 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 +134,16 @@ impl WitnessStruct { } + impl ::simplex::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 +152,7 @@ impl WitnessStruct { self.build_arguments().serialize(serializer) } } + impl<'de> simplex::serde::Deserialize<'de> for #struct_name { fn deserialize(deserializer: D) -> Result where @@ -158,7 +163,7 @@ impl WitnessStruct { } } - impl ::simplex_core::Encodable for #struct_name {} + // impl ::simplex_core::Encodable for #struct_name {} }, }) } @@ -185,25 +190,18 @@ impl WitnessStruct { use simplicityhl::str::WitnessName; use simplicityhl::types::TypeConstructible; use simplicityhl::value::ValueConstructible; + use ::simplex::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 +209,16 @@ impl WitnessStruct { } } + impl ::simplex::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 @@ -230,28 +238,13 @@ impl WitnessStruct { } } - impl ::simplex_core::Encodable for #struct_name {} + // impl ::simplex_core::Encodable for #struct_name {} }, }) } - 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())?, @@ -259,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())?, @@ -337,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-core/src/attr/mod.rs b/crates/macros-core/src/attr/mod.rs index 55faded..0d367ba 100644 --- a/crates/macros-core/src/attr/mod.rs +++ b/crates/macros-core/src/attr/mod.rs @@ -1,12 +1,15 @@ pub mod codegen; pub mod parse; +pub(crate) mod program; mod types; pub use parse::SimfContent; -use crate::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) @@ -25,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)?; @@ -46,18 +49,13 @@ 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; - 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; @@ -65,52 +63,35 @@ fn construct_program_helpers(derived_meta: &SimfContractMeta) -> proc_macro2::To /// /// # 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) - } - - /// 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(), - )) - } + // #[must_use] + // pub fn get_template_program() -> ::simplicityhl::TemplateProgram { + // ::simplicityhl::TemplateProgram::new(#contract_source_name).expect(#error_msg) + // } /// 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-core/src/attr/parse.rs index 680ce28..b146466 100644 --- a/crates/macros-core/src/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-core/src/program.rs b/crates/macros-core/src/attr/program.rs similarity index 100% rename from crates/macros-core/src/program.rs rename to crates/macros-core/src/attr/program.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..bccccaa --- /dev/null +++ b/crates/macros-core/src/env/mod.rs @@ -0,0 +1,136 @@ +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, io}; + +#[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)?; + + 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 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().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, + })?; + 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-core/src/lib.rs b/crates/macros-core/src/lib.rs index 8132d6a..4194710 100644 --- a/crates/macros-core/src/lib.rs +++ b/crates/macros-core/src/lib.rs @@ -1,8 +1,9 @@ #![warn(clippy::all, clippy::pedantic)] pub mod attr; - -pub(crate) mod program; +/// Module releted to simplex environment generation +pub mod env; +pub mod test; /// Expands the `include_simf` macro. /// @@ -10,7 +11,7 @@ pub(crate) mod program; /// 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) @@ -20,28 +21,13 @@ 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)) +pub fn expand_test(args: proc_macro2::TokenStream, input: syn::ItemFn) -> syn::Result { + test::expand(args, 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 - } - } +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-core/src/test/mod.rs b/crates/macros-core/src/test/mod.rs new file mode 100644 index 0000000..d255f48 --- /dev/null +++ b/crates/macros-core/src/test/mod.rs @@ -0,0 +1,110 @@ +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::parse::Parser; + +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; + let body = &input.block; + let attrs = &input.attrs; + + let fn_name_str = name.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:#?}"); + 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 { + use ::simplex::tracing; + use ::std::path::PathBuf; + use ::simplex::simplex_test::TestContextBuilder; + + fn #name(#inputs) #ret { + #body + } + + let test_context = match std::env::var(#simplex_test_env) { + Err(e) => { + tracing::trace!( + "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_generation + }; + // println!("fn name: {}, \n ident: {}", #fn_name_str, #ident); + // println!("input: {}, \n AttributeArgs: {}", "", #args_str); + + #name(test_context) + } + }; + Ok(expansion) +} + +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)?; + + expand_inner(&input, 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", + )); + } + + 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/macros/Cargo.toml b/crates/macros/Cargo.toml index f15fbc3..de24136 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -16,17 +16,15 @@ 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"] } +simplex-macros-core = { workspace = true } serde = { version = "1.0.228", optional = true } 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 8179b38..861e98e 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1,14 +1,13 @@ -use proc_macro::TokenStream; +#![warn(clippy::all, clippy::pedantic)] -// 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 +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 simplex_macros_core::attr::parse::SynFilePath); + + match simplex_macros_core::expand_include_simf(&input) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), } @@ -19,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 simplex_macro_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/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..175f2d0 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,14 @@ 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"] } +minreq = { version = "2.14", features = ["https", "json-using-serde"] } 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 81% rename from crates/runtime/src/elements_rpc/mod.rs rename to crates/provider/src/elements_rpc/mod.rs index 00ef807..667d82d 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 { @@ -102,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(()) @@ -213,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()])?; @@ -233,6 +229,7 @@ impl ElementsRpcClient { #[derive(serde::Deserialize)] pub struct CreatewalletResult { name: String, + #[allow(dead_code)] warning: String, } @@ -270,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()); @@ -322,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( @@ -340,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(), @@ -350,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}" ))); } } @@ -368,6 +367,50 @@ 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()])?; + let value = value + .as_str() + .ok_or_else(|| ExplorerError::InvalidInput("Failed to deserialize a String".to_string()))?; + Ok(value.to_string()) + } } fn sat2btc(sat: u64) -> String { diff --git a/crates/runtime/src/elements_rpc/types.rs b/crates/provider/src/elements_rpc/types.rs similarity index 72% rename from crates/runtime/src/elements_rpc/types.rs rename to crates/provider/src/elements_rpc/types.rs index fcb6902..bcb358a 100644 --- a/crates/runtime/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}"))), } } } @@ -230,3 +230,99 @@ 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, + #[serde(rename = "scriptPubKey")] + pub script_pubkey: 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/runtime/src/error.rs b/crates/provider/src/error.rs similarity index 62% rename from crates/runtime/src/error.rs rename to crates/provider/src/error.rs index 2b58259..60ddc9d 100644 --- a/crates/runtime/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,49 @@ 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] + #[allow(clippy::cast_sign_loss)] + 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 new file mode 100644 index 0000000..e3e81d9 --- /dev/null +++ b/crates/provider/src/esplora/mod.rs @@ -0,0 +1,2207 @@ +mod types; + +// TODO(Illia): remove #[allow(dead_code)] + +use crate::error::ExplorerError; +use crate::esplora::deserializable::TypeConversion; +use simplicityhl::elements::pset::serialize::Deserialize; +use simplicityhl::elements::{BlockHash, Txid}; +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 EsploraClientAsync { + url_builder: UrlBuilder, + client: reqwest::Client, +} + +pub struct EsploraClientSync { + url_builder: UrlBuilder, +} + +#[derive(Debug, Clone)] +pub struct EsploraClientBuilder { + url: Option, +} + +#[allow(dead_code)] +pub struct EsploraConfig { + url: String, +} + +// TODO: Illia add caching as optional parameter +// TODO: Add api backend trait implementation +impl EsploraClientBuilder { + fn default_url() -> String { + 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()), + } + } + + pub fn custom(url: impl Into) -> Self { + // todo: remove trailling slash + EsploraClientBuilder { url: Some(url.into()) } + } + + #[must_use] + pub fn build_async(self) -> EsploraClientAsync { + EsploraClientAsync { + url_builder: UrlBuilder { + base_url: self.url.unwrap_or(Self::default_url()), + }, + client: reqwest::Client::new(), + } + } + + #[must_use] + pub fn build_sync(self) -> EsploraClientSync { + EsploraClientSync { + url_builder: UrlBuilder { + base_url: self.url.unwrap_or(Self::default_url()), + }, + } + } +} + +impl Default for EsploraClientBuilder { + fn default() -> Self { + EsploraClientBuilder::liquid_testnet() + } +} + +impl Default for EsploraClientAsync { + fn default() -> Self { + EsploraClientBuilder::default().build_async() + } +} + +impl Default for EsploraClientSync { + fn default() -> Self { + EsploraClientBuilder::default().build_sync() + } +} + +mod deserializable { + use crate::error::{CommitmentType, ExplorerError}; + use crate::esplora::types; + use crate::esplora::types::Stats; + use bitcoin_hashes::sha256d::Hash; + use simplicityhl::elements::confidential::{Asset, Nonce, Value}; + use simplicityhl::elements::{Address, AssetId, BlockHash, OutPoint, Script, TxMerkleNode, Txid}; + use std::str::FromStr; + + pub(crate) trait TypeConversion { + fn convert(self) -> Result; + } + + #[derive(serde::Deserialize)] + pub struct EsploraTransaction { + pub txid: String, + pub version: u32, + pub locktime: u32, + pub size: u64, + pub weight: u64, + pub fee: u64, + pub vin: Vec, + pub vout: Vec, + pub status: TxStatus, + pub discount_vsize: u64, + pub discount_weight: u64, + } + + #[allow(dead_code)] + #[derive(serde::Deserialize)] + pub struct Vin { + pub txid: String, + pub vout: u32, + pub is_coinbase: bool, + pub scriptsig: String, + pub scriptsig_asm: String, + pub inner_redeemscript_asm: Option, + pub inner_witnessscript_asm: Option, + pub sequence: u32, + #[serde(default)] + pub witness: Vec, + pub prevout: Option, + } + + #[derive(serde::Deserialize)] + pub struct Vout { + pub scriptpubkey: String, + pub scriptpubkey_asm: String, + pub scriptpubkey_type: String, + pub scriptpubkey_address: Option, + pub value: Option, + } + + #[derive(serde::Deserialize)] + pub struct TxStatus { + pub confirmed: bool, + pub block_height: Option, + pub block_hash: Option, + pub block_time: Option, + } + + #[derive(serde::Deserialize)] + pub struct AddressUtxo { + pub txid: String, + pub vout: u32, + pub status: TxStatus, + #[serde(flatten)] + pub utxo_info: UtxoInfo, + } + + #[derive(serde::Deserialize)] + #[serde(untagged)] + pub enum UtxoInfo { + Confidential { + valuecommitment: String, + assetcommitment: String, + noncecommitment: String, + }, + Explicit { + value: u64, + asset: String, + }, + } + + #[derive(serde::Deserialize)] + pub struct AddressInfo { + pub address: String, + pub chain_stats: types::ChainStats, + pub mempool_stats: types::MempoolStats, + } + + #[derive(serde::Deserialize)] + pub struct MerkleProof { + pub block_height: u64, + pub merkle: Vec, + pub pos: u64, + } + + #[derive(serde::Deserialize)] + pub struct Outspend { + pub spent: bool, + pub txid: Option, + pub vin: Option, + pub status: Option, + } + + #[allow(dead_code)] + #[derive(serde::Deserialize)] + pub struct MempoolRecent { + pub txid: String, + pub fee: u64, + pub vsize: u64, + pub discount_vsize: u64, + } + + #[derive(serde::Deserialize)] + pub struct ScripthashInfo { + pub scripthash: String, + pub chain_stats: Stats, + pub mempool_stats: Stats, + } + + #[derive(serde::Deserialize)] + pub struct Block { + pub id: String, + pub height: u64, + pub version: u32, + pub timestamp: u64, + pub mediantime: u64, + pub merkle_root: String, + pub tx_count: u64, + pub size: u64, + pub weight: u64, + pub previousblockhash: String, + pub ext: Option, + } + + #[allow(dead_code)] + #[derive(serde::Deserialize)] + #[serde(untagged)] + pub enum BlockExtDataRaw { + Proof { + challenge: String, + solution: String, + }, + Dynafed { + current: DynafedParamsRaw, + proposed: DynafedParamsRaw, + signblock_witness: Vec>, + }, + } + + #[allow(dead_code)] + #[derive(serde::Deserialize)] + #[serde(untagged)] + pub enum DynafedParamsRaw { + Null {}, + Compact { + signblockscript: String, + signblock_witness_limit: u32, + elided_root: String, + }, + } + + impl TypeConversion for TxStatus { + fn convert(self) -> Result { + let block_hash = match self.block_hash { + None => None, + Some(val) => match BlockHash::from_str(&val) { + Ok(x) => Some(x), + Err(e) => return Err(ExplorerError::BitcoinHashesHex(e)), + }, + }; + Ok(types::TxStatus { + confirmed: self.confirmed, + block_height: self.block_height, + block_hash, + block_time: self.block_time, + }) + } + } + + impl TypeConversion for AddressUtxo { + fn convert(self) -> Result { + let block_hash = self.status.block_hash.map(|hash| BlockHash::from_str(&hash)); + let block_hash = match block_hash { + None => None, + Some(Err(err)) => return Err(ExplorerError::BitcoinHashesHex(err)), + Some(Ok(x)) => Some(x), + }; + let utxo_info = match self.utxo_info { + UtxoInfo::Confidential { + assetcommitment, + noncecommitment, + valuecommitment, + } => types::UtxoInfo::Confidential { + asset_comm: Asset::from_commitment( + &hex_simd::decode_to_vec(assetcommitment).map_err(ExplorerError::HexSimdDecode)?, + ) + .map_err(|e| ExplorerError::CommitmentDecode { + commitment_type: CommitmentType::Asset, + error: e, + })?, + value_comm: Value::from_commitment( + &hex_simd::decode_to_vec(valuecommitment).map_err(ExplorerError::HexSimdDecode)?, + ) + .map_err(|e| ExplorerError::CommitmentDecode { + commitment_type: CommitmentType::Asset, + error: e, + })?, + nonce_comm: Nonce::from_commitment( + &hex_simd::decode_to_vec(noncecommitment).map_err(ExplorerError::HexSimdDecode)?, + ) + .map_err(|e| ExplorerError::CommitmentDecode { + commitment_type: CommitmentType::Asset, + error: e, + })?, + }, + UtxoInfo::Explicit { asset, value } => types::UtxoInfo::Explicit { + value, + asset: AssetId::from_str(&asset).map_err(ExplorerError::BitcoinHashesHex)?, + }, + }; + + Ok(types::AddressUtxo { + outpoint: OutPoint::new(Txid::from_str(&self.txid)?, self.vout), + status: types::TxStatus { + confirmed: self.status.confirmed, + block_height: self.status.block_height, + block_hash, + block_time: self.status.block_time, + }, + utxo_info, + }) + } + } + + impl TypeConversion for MerkleProof { + fn convert(self) -> Result { + let hashes = self + .merkle + .into_iter() + .map(|x| Hash::from_str(&x)) + .collect::, bitcoin_hashes::hex::HexToArrayError>>()?; + let merkle_proofs = hashes.into_iter().map(TxMerkleNode::from_raw_hash).collect(); + Ok(types::MerkleProof { + block_height: self.block_height, + merkle: merkle_proofs, + pos: self.pos, + }) + } + } + + impl TypeConversion for AddressInfo { + fn convert(self) -> Result { + Ok(types::AddressInfo { + address: Address::from_str(&self.address) + .map_err(|e| ExplorerError::AddressConversion(e.to_string()))?, + chain_stats: self.chain_stats, + mempool_stats: self.mempool_stats, + }) + } + } + + impl TypeConversion for EsploraTransaction { + fn convert(self) -> Result { + let status = self.status.convert()?; + 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)?, + version: self.version, + locktime: self.locktime, + size: self.size, + weight: self.weight, + fee: self.fee, + vin, + vout, + status, + discount_vsize: self.discount_vsize, + discount_weight: self.discount_weight, + }) + } + } + + impl TypeConversion for Vout { + fn convert(self) -> Result { + Ok(types::Vout { + scriptpubkey: Script::from_str(&self.scriptpubkey).map_err(ExplorerError::ElementsHex)?, + scriptpubkey_asm: self.scriptpubkey_asm, + scriptpubkey_type: self.scriptpubkey_type, + scriptpubkey_address: self.scriptpubkey_address, + value: self.value, + }) + } + } + impl TypeConversion for Vin { + fn convert(self) -> Result { + let prevout = match self.prevout { + None => None, + Some(val) => Some(val.convert()?), + }; + + Ok(types::Vin { + out_point: OutPoint::default(), + is_coinbase: self.is_coinbase, + scriptsig: self.scriptsig, + scriptsig_asm: self.scriptsig_asm, + inner_redeemscript_asm: self.inner_redeemscript_asm, + inner_witnessscript_asm: self.inner_witnessscript_asm, + sequence: self.sequence, + witness: self.witness, + prevout, + }) + } + } + + impl TypeConversion for Outspend { + fn convert(self) -> Result { + let status = match self.status { + None => None, + Some(val) => Some(val.convert()?), + }; + let txid = match self.txid { + None => None, + Some(val) => Some(Txid::from_str(&val)?), + }; + + Ok(types::Outspend { + spent: self.spent, + txid, + vin: self.vin, + status, + }) + } + } + + impl TypeConversion for MempoolRecent { + fn convert(self) -> Result { + Ok(types::MempoolRecent { + txid: Txid::from_str(&self.txid)?, + fee: 0, + vsize: 0, + discount_vsize: 0, + }) + } + } + + impl TypeConversion for ScripthashInfo { + fn convert(self) -> Result { + Ok(types::ScripthashInfo { + scripthash: Script::from_str(&self.scripthash).map_err(ExplorerError::ElementsHex)?, + chain_stats: self.chain_stats, + mempool_stats: self.mempool_stats, + }) + } + } + + impl TypeConversion for Block { + fn convert(self) -> Result { + let ext = match self.ext { + None => None, + Some(val) => Some(val.convert()?), + }; + Ok(types::Block { + id: self.id, + height: self.height, + version: self.version, + timestamp: self.timestamp, + tx_count: self.tx_count, + size: self.size, + weight: self.weight, + merkle_root: TxMerkleNode::from_str(&self.merkle_root)?, + mediantime: self.mediantime, + previousblockhash: BlockHash::from_str(&self.previousblockhash)?, + ext, + }) + } + } + + impl TypeConversion for BlockExtDataRaw { + fn convert(self) -> Result { + todo!() + } + } +} + +impl EsploraClientAsync { + #[inline] + fn filter_resp(resp: &reqwest::Response) -> Result<(), ExplorerError> { + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + let resp = resp.convert()?; + + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + let resp = resp.convert()?; + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + 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 + .client + .get(url) + .send() + .await + .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_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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + let resp = resp.convert()?; + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + let resp = resp.convert()?; + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + 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()?; + let resp = self + .client + .post(url) + .body(tx_hex) + .send() + .await + .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)?) + } + + /// 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], + ) -> Result { + let url = self.url_builder.get_broadcast_tx_package_url()?; + let tx_hexes = txs + .iter() + .map(simplicityhl::elements::encode::serialize_hex) + .collect::>(); + + let resp = self + .client + .post(url) + .json(&tx_hexes) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + let resp = resp.convert()?; + + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + 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, + last_seen_txid: Option<&str>, + ) -> Result, ExplorerError> { + 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_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + 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, + ) -> Result, ExplorerError> { + 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_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + let resp = resp.convert()?; + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + 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, + last_seen_txid: Option<&str>, + ) -> Result { + 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_reqwest(&e))?; + Self::filter_resp(&resp)?; + + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + let resp = resp.convert()?; + 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 + .client + .get(url) + .send() + .await + .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) + } + + /// 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + resp.json::() + .await + .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, + start_index: Option, + ) -> Result, ExplorerError> { + 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_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + + let resp = resp + .into_iter() + .map(|val| Txid::from_str(&val)) + .collect::>()?; + 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 + .client + .get(url) + .send() + .await + .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)?) + } + + /// 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 + .client + .get(url) + .send() + .await + .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_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 + .client + .get(url) + .send() + .await + .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) + } + + /// 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + 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 + .client + .get(url) + .send() + .await + .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) + } + + /// 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + let resp = resp + .into_iter() + .map(|val| Txid::from_str(&val)) + .collect::>()?; + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::deserialize_reqwest(&e))?; + 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 + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed_reqwest(&e))?; + Self::filter_resp(&resp)?; + + resp.json::() + .await + .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(()) + } + + /// 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + 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(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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .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(ExplorerError::deserialize_minreq)? + .send() + .map_err(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .as_str() + .map_err(ExplorerError::response_failed_minreq)? + .to_string(); + Ok(Txid::from_str(&resp)?) + } + + /// 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], + ) -> 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(ExplorerError::deserialize_minreq)? + .send() + .map_err(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + let resp = resp + .json::>() + .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, + 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .map_err(ExplorerError::response_failed_minreq)?; + let resp = resp.convert()?; + Ok(resp) + } + + /// 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + Ok(resp + .as_str() + .map_err(ExplorerError::response_failed_minreq)? + .to_string()) + } + + /// 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + Ok(resp + .as_str() + .map_err(ExplorerError::response_failed_minreq)? + .to_string()) + } + + /// 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + Ok(resp + .as_str() + .map_err(ExplorerError::response_failed_minreq)? + .to_string()) + } + + /// 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + Ok(resp + .as_str() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .map_err(ExplorerError::response_failed_minreq)?; + let resp = resp.convert()?; + Ok(resp) + } + + /// 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .as_str() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + resp.json::() + .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, + 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .map_err(ExplorerError::response_failed_minreq)?; + + let resp = resp + .into_iter() + .map(|val| Txid::from_str(&val)) + .collect::>()?; + 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp.as_str().map_err(ExplorerError::response_failed_minreq)?; + + 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(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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp.as_str().map_err(ExplorerError::response_failed_minreq)?; + + 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .map_err(ExplorerError::response_failed_minreq)?; + let resp = resp + .into_iter() + .map(|val| Txid::from_str(&val)) + .collect::>()?; + 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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .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(ExplorerError::response_failed_minreq)?; + Self::filter_resp(&resp)?; + + resp.json::() + .map_err(ExplorerError::response_failed_minreq) + } +} + +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 { + 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/runtime/src/esplora/types.rs b/crates/provider/src/esplora/types.rs similarity index 91% rename from crates/runtime/src/esplora/types.rs rename to crates/provider/src/esplora/types.rs index 95dfda9..5f0a733 100644 --- a/crates/runtime/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/runtime/src/lib.rs b/crates/provider/src/lib.rs similarity index 62% rename from crates/runtime/src/lib.rs rename to crates/provider/src/lib.rs index a58835d..7c1ac7c 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -1,7 +1,7 @@ +#![warn(clippy::all, clippy::pedantic)] + pub mod elements_rpc; mod error; pub mod esplora; -mod waterfall; pub use error::*; -// pub use waterfall::*; diff --git a/crates/runtime/src/esplora/mod.rs b/crates/runtime/src/esplora/mod.rs deleted file mode 100644 index 74bc175..0000000 --- a/crates/runtime/src/esplora/mod.rs +++ /dev/null @@ -1,1070 +0,0 @@ -mod types; - -// TODO(Illia): remove #[allow(dead_code)] - -use crate::error::ExplorerError; -use crate::esplora::deserializable::TypeConversion; -use simplicityhl::elements::pset::serialize::Deserialize; -use simplicityhl::elements::{BlockHash, Txid}; -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, - client: reqwest::Client, -} - -#[derive(Debug, Clone)] -pub struct EsploraClientBuilder { - url: Option, -} - -#[allow(dead_code)] -pub struct EsploraConfig { - url: String, -} - -// TODO: Illia add caching as optional parameter -// TODO: Add api backend trait implementation -impl EsploraClientBuilder { - fn default_url() -> String { - ESPLORA_LIQUID_TESTNET.to_string() - } - - pub fn liquid_testnet() -> Self { - Self { - url: Some(ESPLORA_LIQUID_TESTNET.to_string()), - } - } - - pub fn liquid_mainnet() -> Self { - Self { - url: Some(ESPLORA_LIQUID.to_string()), - } - } - - pub fn custom(url: impl Into) -> Self { - // todo: remove trailling slash - EsploraClientBuilder { url: Some(url.into()) } - } - - pub fn build(self) -> EsploraClient { - EsploraClient { - base_url: self.url.unwrap_or(Self::default_url()), - client: reqwest::Client::new(), - } - } -} - -impl Default for EsploraClientBuilder { - fn default() -> Self { - EsploraClientBuilder::liquid_testnet() - } -} - -impl Default for EsploraClient { - fn default() -> Self { - EsploraClientBuilder::default().build() - } -} - -mod deserializable { - use crate::error::{CommitmentType, ExplorerError}; - use crate::esplora::types; - use crate::esplora::types::Stats; - use bitcoin_hashes::sha256d::Hash; - use simplicityhl::elements::confidential::{Asset, Nonce, Value}; - use simplicityhl::elements::{Address, AssetId, BlockHash, OutPoint, Script, TxMerkleNode, Txid}; - use std::str::FromStr; - - pub(crate) trait TypeConversion { - fn convert(self) -> Result; - } - - #[derive(serde::Deserialize)] - pub struct EsploraTransaction { - pub txid: String, - pub version: u32, - pub locktime: u32, - pub size: u64, - pub weight: u64, - pub fee: u64, - pub vin: Vec, - pub vout: Vec, - pub status: TxStatus, - pub discount_vsize: u64, - pub discount_weight: u64, - } - - #[allow(dead_code)] - #[derive(serde::Deserialize)] - pub struct Vin { - pub txid: String, - pub vout: u32, - pub is_coinbase: bool, - pub scriptsig: String, - pub scriptsig_asm: String, - pub inner_redeemscript_asm: Option, - pub inner_witnessscript_asm: Option, - pub sequence: u32, - #[serde(default)] - pub witness: Vec, - pub prevout: Option, - } - - #[derive(serde::Deserialize)] - pub struct Vout { - pub scriptpubkey: String, - pub scriptpubkey_asm: String, - pub scriptpubkey_type: String, - pub scriptpubkey_address: Option, - pub value: Option, - } - - #[derive(serde::Deserialize)] - pub struct TxStatus { - pub confirmed: bool, - pub block_height: Option, - pub block_hash: Option, - pub block_time: Option, - } - - #[derive(serde::Deserialize)] - pub struct AddressUtxo { - pub txid: String, - pub vout: u32, - pub status: TxStatus, - #[serde(flatten)] - pub utxo_info: UtxoInfo, - } - - #[derive(serde::Deserialize)] - #[serde(untagged)] - pub enum UtxoInfo { - Confidential { - valuecommitment: String, - assetcommitment: String, - noncecommitment: String, - }, - Explicit { - value: u64, - asset: String, - }, - } - - #[derive(serde::Deserialize)] - pub struct AddressInfo { - pub address: String, - pub chain_stats: types::ChainStats, - pub mempool_stats: types::MempoolStats, - } - - #[derive(serde::Deserialize)] - pub struct MerkleProof { - pub block_height: u64, - pub merkle: Vec, - pub pos: u64, - } - - #[derive(serde::Deserialize)] - pub struct Outspend { - pub spent: bool, - pub txid: Option, - pub vin: Option, - pub status: Option, - } - - #[allow(dead_code)] - #[derive(serde::Deserialize)] - pub struct MempoolRecent { - pub txid: String, - pub fee: u64, - pub vsize: u64, - pub discount_vsize: u64, - } - - #[derive(serde::Deserialize)] - pub struct ScripthashInfo { - pub scripthash: String, - pub chain_stats: Stats, - pub mempool_stats: Stats, - } - - #[derive(serde::Deserialize)] - pub struct Block { - pub id: String, - pub height: u64, - pub version: u32, - pub timestamp: u64, - pub mediantime: u64, - pub merkle_root: String, - pub tx_count: u64, - pub size: u64, - pub weight: u64, - pub previousblockhash: String, - pub ext: Option, - } - - #[allow(dead_code)] - #[derive(serde::Deserialize)] - #[serde(untagged)] - pub enum BlockExtDataRaw { - Proof { - challenge: String, - solution: String, - }, - Dynafed { - current: DynafedParamsRaw, - proposed: DynafedParamsRaw, - signblock_witness: Vec>, - }, - } - - #[allow(dead_code)] - #[derive(serde::Deserialize)] - #[serde(untagged)] - pub enum DynafedParamsRaw { - Null {}, - Compact { - signblockscript: String, - signblock_witness_limit: u32, - elided_root: String, - }, - } - - impl TypeConversion for TxStatus { - fn convert(self) -> Result { - let block_hash = match self.block_hash { - None => None, - Some(val) => match BlockHash::from_str(&val) { - Ok(x) => Some(x), - Err(e) => return Err(ExplorerError::BitcoinHashesHex(e)), - }, - }; - Ok(types::TxStatus { - confirmed: self.confirmed, - block_height: self.block_height, - block_hash, - block_time: self.block_time, - }) - } - } - - impl TypeConversion for AddressUtxo { - fn convert(self) -> Result { - let block_hash = self.status.block_hash.map(|hash| BlockHash::from_str(&hash)); - let block_hash = match block_hash { - None => None, - Some(Err(err)) => return Err(ExplorerError::BitcoinHashesHex(err)), - Some(Ok(x)) => Some(x), - }; - let utxo_info = match self.utxo_info { - UtxoInfo::Confidential { - assetcommitment, - noncecommitment, - valuecommitment, - } => types::UtxoInfo::Confidential { - asset_comm: Asset::from_commitment( - &hex_simd::decode_to_vec(assetcommitment).map_err(ExplorerError::HexSimdDecode)?, - ) - .map_err(|e| ExplorerError::CommitmentDecode { - commitment_type: CommitmentType::Asset, - error: e, - })?, - value_comm: Value::from_commitment( - &hex_simd::decode_to_vec(valuecommitment).map_err(ExplorerError::HexSimdDecode)?, - ) - .map_err(|e| ExplorerError::CommitmentDecode { - commitment_type: CommitmentType::Asset, - error: e, - })?, - nonce_comm: Nonce::from_commitment( - &hex_simd::decode_to_vec(noncecommitment).map_err(ExplorerError::HexSimdDecode)?, - ) - .map_err(|e| ExplorerError::CommitmentDecode { - commitment_type: CommitmentType::Asset, - error: e, - })?, - }, - UtxoInfo::Explicit { asset, value } => types::UtxoInfo::Explicit { - value, - asset: AssetId::from_str(&asset).map_err(ExplorerError::BitcoinHashesHex)?, - }, - }; - - Ok(types::AddressUtxo { - outpoint: OutPoint::new(Txid::from_str(&self.txid)?, self.vout), - status: types::TxStatus { - confirmed: self.status.confirmed, - block_height: self.status.block_height, - block_hash, - block_time: self.status.block_time, - }, - utxo_info, - }) - } - } - - impl TypeConversion for MerkleProof { - fn convert(self) -> Result { - let hashes = self - .merkle - .into_iter() - .map(|x| Hash::from_str(&x)) - .collect::, bitcoin_hashes::hex::HexToArrayError>>()?; - let merkle_proofs = hashes.into_iter().map(TxMerkleNode::from_raw_hash).collect(); - Ok(types::MerkleProof { - block_height: self.block_height, - merkle: merkle_proofs, - pos: self.pos, - }) - } - } - - impl TypeConversion for AddressInfo { - fn convert(self) -> Result { - Ok(types::AddressInfo { - address: Address::from_str(&self.address) - .map_err(|e| ExplorerError::AddressConversion(e.to_string()))?, - chain_stats: self.chain_stats, - mempool_stats: self.mempool_stats, - }) - } - } - - 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::>()?; - - Ok(types::EsploraTransaction { - txid: Txid::from_str(&self.txid)?, - version: self.version, - locktime: self.locktime, - size: self.size, - weight: self.weight, - fee: self.fee, - vin, - vout, - status, - discount_vsize: self.discount_vsize, - discount_weight: self.discount_weight, - }) - } - } - - impl TypeConversion for Vout { - fn convert(self) -> Result { - Ok(types::Vout { - scriptpubkey: Script::from_str(&self.scriptpubkey).map_err(ExplorerError::ElementsHex)?, - scriptpubkey_asm: self.scriptpubkey_asm, - scriptpubkey_type: self.scriptpubkey_type, - scriptpubkey_address: self.scriptpubkey_address, - value: self.value, - }) - } - } - impl TypeConversion for Vin { - fn convert(self) -> Result { - let prevout = match self.prevout { - None => None, - Some(val) => Some(val.convert()?), - }; - - Ok(types::Vin { - out_point: Default::default(), - is_coinbase: self.is_coinbase, - scriptsig: self.scriptsig, - scriptsig_asm: self.scriptsig_asm, - inner_redeemscript_asm: self.inner_redeemscript_asm, - inner_witnessscript_asm: self.inner_witnessscript_asm, - sequence: self.sequence, - witness: self.witness, - prevout, - }) - } - } - - impl TypeConversion for Outspend { - fn convert(self) -> Result { - let status = match self.status { - None => None, - Some(val) => Some(val.convert()?), - }; - let txid = match self.txid { - None => None, - Some(val) => Some(Txid::from_str(&val)?), - }; - - Ok(types::Outspend { - spent: self.spent, - txid, - vin: self.vin, - status, - }) - } - } - - impl TypeConversion for MempoolRecent { - fn convert(self) -> Result { - Ok(types::MempoolRecent { - txid: Txid::from_str(&self.txid)?, - fee: 0, - vsize: 0, - discount_vsize: 0, - }) - } - } - - impl TypeConversion for ScripthashInfo { - fn convert(self) -> Result { - Ok(types::ScripthashInfo { - scripthash: Script::from_str(&self.scripthash).map_err(ExplorerError::ElementsHex)?, - chain_stats: self.chain_stats, - mempool_stats: self.mempool_stats, - }) - } - } - - impl TypeConversion for Block { - fn convert(self) -> Result { - let ext = match self.ext { - None => None, - Some(val) => Some(val.convert()?), - }; - Ok(types::Block { - id: self.id, - height: self.height, - version: self.version, - timestamp: self.timestamp, - tx_count: self.tx_count, - size: self.size, - weight: self.weight, - merkle_root: TxMerkleNode::from_str(&self.merkle_root)?, - mediantime: self.mediantime, - previousblockhash: BlockHash::from_str(&self.previousblockhash)?, - ext, - }) - } - } - - impl TypeConversion for BlockExtDataRaw { - fn convert(self) -> Result { - todo!() - } - } -} - -impl EsploraClient { - #[inline] - fn join_url(&self, str: impl AsRef) -> Result { - Ok(format!("{}/{}", self.base_url, str.as_ref())) - } - - #[inline] - fn filter_resp(resp: &reqwest::Response) -> Result<(), ExplorerError> { - if !(200..300).contains(&resp.status().as_u16()) { - return Err(ExplorerError::erroneous_response(resp)); - } - Ok(()) - } - - pub async fn get_tx(&self, txid: &str) -> Result { - let url = self.join_url(format!("/tx/{txid}"))?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::() - .await - .map_err(|e| ExplorerError::deserialize(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::() - .await - .map_err(|e| ExplorerError::deserialize(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - resp.text().await.map_err(|e| ExplorerError::deserialize(&e)) - } - - pub async fn get_tx_raw(&self, txid: &str) -> Result, ExplorerError> { - let url = self.join_url(format!("tx/{txid}/raw"))?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - resp.bytes() - .await - .map(|b| b.to_vec()) - .map_err(|e| ExplorerError::deserialize(&e)) - } - - 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())) - } - - pub async fn get_tx_merkle_proof(&self, txid: &str) -> Result { - let url = self.join_url(format!("tx/{txid}/merkle-proof"))?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::() - .await - .map_err(|e| ExplorerError::response_failed(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::() - .await - .map_err(|e| ExplorerError::response_failed(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::>() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - resp.into_iter().map(|x| x.convert()).collect::, _>>() - } - - 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 resp = self - .client - .post(url) - .body(tx_hex) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) - } - - // 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 tx_hexes = txs - .iter() - .map(simplicityhl::elements::encode::serialize_hex) - .collect::>(); - - let resp = self - .client - .post(url) - .json(&tx_hexes) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - resp.json().await.map_err(|e| ExplorerError::response_failed(&e)) - } - - pub async fn get_address(&self, address: &str) -> Result { - let url = self.join_url(format!("address/{address}"))?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::() - .await - .map_err(|e| ExplorerError::response_failed(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - let resp = resp - .json::>() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; - Ok(resp) - } - - pub async fn get_address_txs_chain( - &self, - 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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::>() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; - Ok(resp) - } - - pub async fn get_address_txs_mempool( - &self, - address: &str, - ) -> Result, ExplorerError> { - let url = self.join_url(format!("address/{address}/txs/mempool"))?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::>() - .await - .map_err(|e| ExplorerError::response_failed(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::>() - .await - .map_err(|e| ExplorerError::response_failed(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::() - .await - .map_err(|e| ExplorerError::response_failed(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - resp.text().await.map_err(|e| ExplorerError::response_failed(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - resp.text().await.map_err(|e| ExplorerError::response_failed(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) - } - - // TODO: check output - pub async fn get_scripthash_utxo(&self, hash: &str) -> Result { - let url = self.join_url(format!("scripthash/{hash}/utxo"))?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) - } - - pub async fn get_block(&self, hash: &str) -> Result { - let url = self.join_url(format!("block/{hash}"))?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::() - .await - .map_err(|e| ExplorerError::response_failed(&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 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))?; - Ok(resp) - } - - pub async fn get_block_status(&self, hash: &str) -> Result { - let url = self.join_url(format!("block/{hash}/status"))?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - resp.json::() - .await - .map_err(|e| ExplorerError::response_failed(&e)) - } - - pub async fn get_block_txs( - &self, - 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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - let resp = resp - .json::>() - .await - .map_err(|e| ExplorerError::response_failed(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - let resp = resp - .json::>() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - - let resp = resp - .into_iter() - .map(|val| Txid::from_str(&val)) - .collect::>()?; - Ok(resp) - } - - pub async fn get_block_txid(&self, hash: &str, index: u32) -> Result { - let url = self.join_url(format!("block/{hash}/txid/{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))?; - - 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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - resp.bytes() - .await - .map(|b| b.to_vec()) - .map_err(|e| ExplorerError::response_failed(&e)) - } - - pub async fn get_block_height(&self, height: u64) -> Result { - let url = self.join_url(format!("block-height/{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))?; - 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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::>() - .await - .map_err(|e| ExplorerError::response_failed(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Ok(resp) - } - - pub async fn get_blocks_tip_hash(&self) -> Result { - let url = self.join_url("blocks/tip/hash")?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp.text().await.map_err(|e| ExplorerError::response_failed(&e))?; - let resp = BlockHash::from_str(&resp)?; - Ok(resp) - } - - pub async fn get_mempool(&self) -> Result { - let url = self.join_url("mempool")?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - resp.json().await.map_err(|e| ExplorerError::response_failed(&e)) - } - - pub async fn get_mempool_txids(&self) -> Result, ExplorerError> { - let url = self.join_url("mempool/txids")?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::>() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - let resp = resp - .into_iter() - .map(|val| Txid::from_str(&val)) - .collect::>()?; - Ok(resp) - } - - pub async fn get_mempool_recent(&self) -> Result, ExplorerError> { - let url = self.join_url("mempool/recent")?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - let resp = resp - .json::>() - .await - .map_err(|e| ExplorerError::response_failed(&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 resp = self - .client - .get(url) - .send() - .await - .map_err(|e| ExplorerError::response_failed(&e))?; - Self::filter_resp(&resp)?; - - resp.json::() - .await - .map_err(|e| ExplorerError::response_failed(&e)) - } -} 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..5e16a89 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -10,8 +10,9 @@ readme = "README.md" workspace = true [dependencies] -simplex-runtime = { workspace = true } +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..64f606b 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: Box }, } diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index a0a5dc9..7b167c9 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1,10 +1,12 @@ -pub mod witness_transaction; -pub mod program; +#![warn(clippy::all, clippy::pedantic)] + 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..388c180 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_runtime::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: Box::new(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: Box::new(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: Box::new(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: 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: Box::new(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: Box::new(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/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.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/sdk/src/witness_transaction.rs b/crates/sdk/src/witness_transaction.rs index ef6b05d..3f4b58d 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(); @@ -83,7 +83,6 @@ where change_recipient_script.clone(), PLACEHOLDER_FEE, self.network.policy_asset(), - None, )); @@ -188,6 +187,7 @@ where Ok(final_tx) } + #[allow(clippy::too_many_arguments)] fn finalize_tx_with_signer( &self, final_tx: Transaction, @@ -201,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( @@ -212,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 { @@ -221,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/Cargo.toml b/crates/simplex/Cargo.toml index 1abd039..ca92a5c 100644 --- a/crates/simplex/Cargo.toml +++ b/crates/simplex/Cargo.toml @@ -15,24 +15,24 @@ categories = ["cryptography::cryptocurrencies"] workspace = true [features] -default = ["macros", "encoding", "core"] +default = ["macros", "encoding", "sdk"] +macros = ["dep:simplex-macros", "tracing", "encoding"] encoding = ["dep:bincode"] -macros = ["dep:simplex-macros"] -core = ["dep:simplex-core"] +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"]} +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 56fd2f4..3e58b17 100644 --- a/crates/simplex/src/lib.rs +++ b/crates/simplex/src/lib.rs @@ -1,15 +1,19 @@ #![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; + +#[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 7bb673f..b4a3b4c 100644 --- a/crates/simplex/tests/simplex_test.rs +++ b/crates/simplex/tests/simplex_test.rs @@ -1,25 +1,70 @@ -use simplex_runtime::elements_rpc::{AddressType, ElementsRpcClient}; -use simplex_test::DEFAULT_SAT_AMOUNT_FAUCET; +use simplex_provider::elements_rpc::{AddressType, ElementsRpcClient}; +use simplex_sdk::constants::SimplicityNetwork; +use simplex_test::{DEFAULT_SAT_AMOUNT_FAUCET, ElementsDConf, TestContext}; use simplicityhl::elements::Address; use simplicityhl::elements::bitcoin::secp256k1; use simplicityhl::elements::secp256k1_zkp::Keypair; +#[ignore] +#[simplex::simplex_macros::test(default_rpc)] +fn test_execution(x: TestContext) { + assert!(true) +} + +#[ignore] #[simplex::simplex_macros::test] -// #[test] -fn test_execution() { - assert!(true); +fn test_execution3(x: TestContext) { + assert!(true) +} + +#[ignore] +#[test] +fn test_execution2() { + use ::simplex::tracing; + use simplex_test::TestContextBuilder; + use std::path::PathBuf; + + 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<()> { - use simplex_test::{ConfigOption, TestProvider}; + use simplex_test::{ConfigOption, TestClientProvider}; - fn test_invocation_tx_tracking(rpc: TestProvider, user1_addr: Address, user2_addr: Address) -> anyhow::Result<()> { + fn test_invocation_tx_tracking( + rpc: TestClientProvider, + user1_addr: Address, + user2_addr: Address, + ) -> anyhow::Result<()> { // user input code { - let network = rpc.network(); + 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)?; + let p2pk = simplicityhl_core::get_p2pk_address( + &keypair.x_only_public_key().0, + simplicityhl_core::SimplicityNetwork::default_regtest(), + )?; dbg!(p2pk.to_string()); @@ -39,7 +84,7 @@ fn test_invocation_tx_tracking() -> anyhow::Result<()> { rpc.as_ref(), &p2pk, DEFAULT_SAT_AMOUNT_FAUCET, - Some(rpc.network().policy_asset()), + Some(network.policy_asset()), )?; ElementsRpcClient::generate_blocks(rpc.as_ref(), 5)?; @@ -62,7 +107,13 @@ fn test_invocation_tx_tracking() -> anyhow::Result<()> { Ok(()) } } - let rpc = TestProvider::init(ConfigOption::DefaultRegtest).unwrap(); + + let network = SimplicityNetwork::default_regtest(); + 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(); @@ -76,7 +127,7 @@ fn test_invocation_tx_tracking() -> anyhow::Result<()> { rpc.as_ref(), &user1_addr, DEFAULT_SAT_AMOUNT_FAUCET, - Some(rpc.network().policy_asset()), + Some(network.policy_asset()), ) .unwrap(); @@ -84,7 +135,7 @@ fn test_invocation_tx_tracking() -> anyhow::Result<()> { rpc.as_ref(), &user2_addr, DEFAULT_SAT_AMOUNT_FAUCET, - Some(rpc.network().policy_asset()), + Some(network.policy_asset()), ) .unwrap(); diff --git a/crates/simplex/tests/ui/array_tr_storage.rs b/crates/simplex/tests/ui/array_tr_storage.rs index 93dd077..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"); @@ -18,8 +20,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..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"); @@ -17,8 +19,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..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"); @@ -43,8 +45,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..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"); @@ -23,8 +25,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..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> { @@ -27,8 +30,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..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"); @@ -18,8 +20,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 diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index 8399bfe..655691c 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -9,11 +9,11 @@ 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 } 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 57b0cda..59f3076 100644 --- a/crates/test/src/error.rs +++ b/crates/test/src/error.rs @@ -1,4 +1,6 @@ -use simplex_runtime::ExplorerError; +use electrsd::electrum_client::bitcoin::hex::HexToArrayError; +use simplex_provider::ExplorerError; +use std::io; #[derive(thiserror::Error, Debug)] pub enum TestError { @@ -10,4 +12,20 @@ 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), + + /// 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 2547057..4ad8493 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -4,128 +4,4 @@ mod testing; pub use common::*; 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 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 TestProvider { - ConfiguredNode { node: BitcoinD, network: SimplicityNetwork }, - CustomRpc(ElementsRpcClient), -} - -pub enum ConfigOption<'a> { - DefaultRegtest, - CustomConfRegtest { conf: Conf<'a> }, - CustomRpcUrlRegtest { url: String, auth: Auth }, -} - -impl TestProvider { - 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 } => { - let network = SimplicityNetwork::default_regtest(); - Self::CustomRpc(ElementsRpcClient::new(network, &rpc_url, auth)?) - } - }; - - if let Err(e) = ElementsRpcClient::blockchain_info(rpc.as_ref()) { - return Err(TestError::UnhealthyRpc(e)); - } - 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"; - 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 { - TestProvider::ConfiguredNode { node, .. } => &node.client, - TestProvider::CustomRpc(x) => x.client(), - } - } - - pub fn network(&self) -> SimplicityNetwork { - match self { - TestProvider::ConfiguredNode { network, .. } => *network, - TestProvider::CustomRpc(x) => x.network(), - } - } -} - -impl TestProvider { - pub fn fund(satoshi: u64, address: Option

, asset: Option) { - todo!() - } - - pub fn get_height() {} - - pub fn get_blockchain_info() { - todo!() - } -} - -impl AsRef for TestProvider { - fn as_ref(&self) -> &Client { - self.client() - } -} +pub use testing::*; diff --git a/crates/test/src/testing/config.rs b/crates/test/src/testing/config.rs index 40dd9cc..3ea2615 100644 --- a/crates/test/src/testing/config.rs +++ b/crates/test/src/testing/config.rs @@ -1 +1,41 @@ -pub struct ConfigBuilder {} +use crate::TestError; +use electrsd::bitcoind::bitcoincore_rpc::jsonrpc::serde::{Deserialize, Serialize}; +use std::fs::OpenOptions; +use std::io::Read; +use std::path::{Path, PathBuf}; + +pub const TEST_ENV_NAME: &str = "SIMPLEX_TEST_ENV"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ElementsDConf { + pub elemendsd_path: PathBuf, + pub rpc_creds: RpcCreds, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub enum RpcCreds { + Auth { + url: String, + username: String, + password: String, + }, + #[default] + None, +} + +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 295f12b..8965b46 100644 --- a/crates/test/src/testing/mod.rs +++ b/crates/test/src/testing/mod.rs @@ -1,5 +1,79 @@ mod config; mod rpc_provider; +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: ElementsDConf, + rpc: TestRpcProvider, +} + +pub enum TestContextBuilder { + Default, + FromConfigPath(PathBuf), +} + +impl TestContextBuilder { + pub fn build(self) -> Result { + let context = match self { + Self::Default => { + let elementsd_path = ElementsDConf::obtain_default_elementsd_path(); + let rpc = TestRpcProvider::init(ConfigOption::DefaultRegtest, &elementsd_path)?; + TestContext { + config: ElementsDConf { + elemendsd_path: elementsd_path, + rpc_creds: RpcCreds::None, + }, + rpc, + } + } + Self::FromConfigPath(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()), + }, + &config.elemendsd_path, + )?; + TestContext { config, rpc } + } + RpcCreds::None => { + let rpc = TestRpcProvider::init(ConfigOption::DefaultRegtest, &config.elemendsd_path)?; + TestContext { config, rpc } + } + } + } + }; + Ok(context) + } +} + +impl TestContext { + pub fn get_config(&self) -> &ElementsDConf { + &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 b589060..c1cb7a7 100644 --- a/crates/test/src/testing/rpc_provider.rs +++ b/crates/test/src/testing/rpc_provider.rs @@ -1,20 +1,277 @@ +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::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::path::Path; +use std::str::FromStr; -pub struct TestRpcProvider {} +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, elementsd_path: impl AsRef) -> Result { + let rpc = match init_option { + ConfigOption::DefaultRegtest => { + 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, elementsd_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) + } + + fn create_default_node(bin_path: impl AsRef) -> 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; -impl Provider for TestRpcProvider { - fn broadcast_transaction(&self, tx: &Transaction) -> Result { - todo!() + BitcoinD::with_conf(bin_path.as_ref(), &conf).unwrap() + } + + 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(); + + 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(bin_path.as_ref(), &conf).unwrap() + } + + 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 { + match self { + TestClientProvider::ConfiguredNode { node, .. } => &node.client, + TestClientProvider::CustomRpc(x) => x.client(), + } + } +} + +impl AsRef for TestClientProvider { + fn as_ref(&self) -> &Client { + self.client() + } +} + +pub struct TestRpcProvider { + provider: TestClientProvider, +} + +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, bin_path: impl AsRef) -> Result { + Ok(Self { + provider: TestClientProvider::init(init_option, bin_path)?, + }) + } + + 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/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" 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); -} diff --git a/crates/config/Cargo.toml b/examples/options/Cargo.toml similarity index 50% rename from crates/config/Cargo.toml rename to examples/options/Cargo.toml index 55cf923..72ab800 100644 --- a/crates/config/Cargo.toml +++ b/examples/options/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "simplex-config" +name = "options" version = "0.1.0" license.workspace = true edition.workspace = true @@ -8,11 +8,7 @@ edition.workspace = true [lints] workspace = true - [dependencies] -simplex-core = { workspace = true } +simplex = { workspace = true } simplicityhl = { workspace = true } -serde = { version = "1.0.228" } -thiserror = { workspace = true } -toml = { version = "0.9.8" } diff --git a/examples/options/Simplex.toml b/examples/options/Simplex.toml new file mode 100644 index 0000000..cccff48 --- /dev/null +++ b/examples/options/Simplex.toml @@ -0,0 +1,8 @@ +network = "liquidtestnet" + +[build] +compile_simf = ["simf/*.simf"] +out_dir = "./src/program" + +[tests] +elementsd_path = "../../assets/elementsd" \ 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..2dc508d --- /dev/null +++ b/examples/options/src/lib.rs @@ -0,0 +1,5 @@ +#![warn(clippy::all, clippy::pedantic)] + +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..619b668 --- /dev/null +++ b/examples/options/src/program/options.rs @@ -0,0 +1,22 @@ +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 OptionsProgram<'a> { + program: Program<'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 new file mode 100644 index 0000000..599c870 --- /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" } diff --git a/examples/p2pk/Simplex.toml b/examples/p2pk/Simplex.toml new file mode 100644 index 0000000..1ebb2e9 --- /dev/null +++ b/examples/p2pk/Simplex.toml @@ -0,0 +1,8 @@ +network = "liquidtestnet" + +[build] +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 new file mode 100644 index 0000000..e5a7b2d --- /dev/null +++ b/examples/p2pk/out_dir/p2pk.rs @@ -0,0 +1,25 @@ +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 P2pkProgram<'a> { + program: Program<'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 { + 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/crates/core/src/source_simf/p2pk.simf b/examples/p2pk/simf/p2pk.simf similarity index 100% rename from crates/core/src/source_simf/p2pk.simf rename to examples/p2pk/simf/p2pk.simf 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::*; 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..5a4c980 --- /dev/null +++ b/examples/test_usage/tests/draft_test.rs @@ -0,0 +1,73 @@ +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::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(()) + } +}