diff --git a/simplex/Cargo.lock b/Cargo.lock similarity index 65% rename from simplex/Cargo.lock rename to Cargo.lock index 951ef2f..2d638e5 100644 --- a/simplex/Cargo.lock +++ b/Cargo.lock @@ -96,15 +96,9 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.5.0" @@ -133,12 +127,6 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bech32" version = "0.11.1" @@ -172,8 +160,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ "bitcoin_hashes", - "rand 0.8.5", - "rand_core 0.6.4", + "rand", + "rand_core", "serde", "unicode-normalization", ] @@ -306,6 +294,16 @@ dependencies = [ "objc2", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -318,12 +316,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - [[package]] name = "cc" version = "1.2.56" @@ -391,7 +383,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -416,10 +408,20 @@ dependencies = [ ] [[package]] -name = "crossbeam-channel" -version = "0.5.15" +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] @@ -447,19 +449,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", - "nix 0.31.1", + "nix 0.31.2", "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" @@ -482,17 +475,6 @@ dependencies = [ "objc2", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "dotenvy" version = "0.15.7" @@ -598,48 +580,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -665,23 +605,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", @@ -702,6 +628,30 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.14", + "regex-syntax 0.8.10", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.11.0", + "ignore", + "walkdir", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -740,16 +690,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "hex-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7685beb53fc20efc2605f32f5d51e9ba18b8ef237961d1760169d2290d3bee" -dependencies = [ - "outref", - "vsimd", -] - [[package]] name = "hex_lit" version = "0.1.1" @@ -765,187 +705,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls 0.23.37", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.6", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - [[package]] name = "id-arena" version = "2.3.0" @@ -953,24 +712,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" +name = "ignore" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ - "icu_normalizer", - "icu_properties", + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.14", + "same-file", + "walkdir", + "winapi-util", ] [[package]] @@ -985,22 +739,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1024,9 +762,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.90" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1058,9 +796,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "linux-raw-sys" @@ -1070,15 +808,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" @@ -1086,12 +818,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "matchers" version = "0.2.0" @@ -1132,22 +858,11 @@ version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" dependencies = [ - "rustls 0.21.12", - "rustls-webpki 0.101.7", + "rustls", + "rustls-webpki", "serde", "serde_json", - "webpki-roots 0.25.4", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", + "webpki-roots", ] [[package]] @@ -1166,9 +881,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ "bitflags 2.11.0", "cfg-if", @@ -1185,12 +900,6 @@ 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.4" @@ -1227,24 +936,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1257,21 +954,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -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" @@ -1288,31 +970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", + "syn", ] [[package]] @@ -1335,74 +993,19 @@ dependencies = [ ] [[package]] -name = "quinn" -version = "0.11.9" +name = "quote" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls 0.23.37", - "socket2", - "thiserror", - "tokio", - "tracing", - "web-time", + "proc-macro2", ] [[package]] -name = "quinn-proto" -version = "0.11.13" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls 0.23.37", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" @@ -1411,18 +1014,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", + "rand_chacha", + "rand_core", ] [[package]] @@ -1432,17 +1025,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", + "rand_core", ] [[package]] @@ -1454,15 +1037,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - [[package]] name = "regex" version = "1.12.3" @@ -1509,44 +1083,6 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls 0.23.37", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 1.0.6", -] - [[package]] name = "ring" version = "0.17.14" @@ -1561,12 +1097,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustix" version = "0.38.44" @@ -1582,14 +1112,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -1601,34 +1131,10 @@ checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", - "rustls-webpki 0.101.7", + "rustls-webpki", "sct", ] -[[package]] -name = "rustls" -version = "0.23.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki 0.103.9", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] - [[package]] name = "rustls-webpki" version = "0.101.7" @@ -1639,17 +1145,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "rustls-webpki" -version = "0.103.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -1657,10 +1152,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] -name = "ryu" -version = "1.0.23" +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] [[package]] name = "santiago" @@ -1688,7 +1186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "bitcoin_hashes", - "rand 0.8.5", + "rand", "secp256k1-sys", "serde", ] @@ -1709,7 +1207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a44aed3002b5ae975f8624c5df3a949cfbf00479e18778b6058fcd213b76e3" dependencies = [ "bitcoin-private", - "rand 0.8.5", + "rand", "secp256k1", "secp256k1-zkp-sys", "serde", @@ -1758,7 +1256,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1783,18 +1281,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "sha2" version = "0.10.9" @@ -1830,7 +1316,6 @@ dependencies = [ "either", "serde", "simplex-macros", - "simplex-provider", "simplex-sdk", "simplex-test", "simplicityhl", @@ -1848,16 +1333,18 @@ dependencies = [ "dotenvy", "electrsd", "glob", + "globwalk", "serde", "simplex-macros-core", + "simplex-regtest", "simplex-sdk", + "simplex-template-gen-core", "simplex-test", "simplicityhl", "thiserror", "tokio", "toml 0.9.12+spec-1.1.0", "tracing", - "tracing-appender", "tracing-subscriber", ] @@ -1867,39 +1354,31 @@ version = "0.1.0" dependencies = [ "serde", "simplex-macros-core", - "syn 2.0.117", + "syn", ] [[package]] name = "simplex-macros-core" version = "0.1.0" dependencies = [ - "pathdiff", - "prettyplease", - "proc-macro-error", "proc-macro2", "quote", "serde", "simplex-test", "simplicityhl", - "syn 2.0.117", - "thiserror", + "syn", ] [[package]] -name = "simplex-provider" +name = "simplex-regtest" version = "0.1.0" dependencies = [ - "async-trait", - "bitcoin_hashes", "electrsd", - "hex-simd", - "minreq", - "reqwest", "serde", - "serde_json", + "simplex-sdk", "simplicityhl", "thiserror", + "toml 0.9.12+spec-1.1.0", ] [[package]] @@ -1908,24 +1387,39 @@ version = "0.1.0" dependencies = [ "async-trait", "bip39", + "bitcoin_hashes", "dyn-clone", + "electrsd", "elements-miniscript", "hex", "minreq", "serde", + "serde_json", "sha2", - "simplex-provider", "simplicityhl", "thiserror", ] +[[package]] +name = "simplex-template-gen-core" +version = "0.1.0" +dependencies = [ + "pathdiff", + "prettyplease", + "proc-macro2", + "quote", + "serde", + "simplex-macros-core", + "syn", + "thiserror", +] + [[package]] name = "simplex-test" version = "0.1.0" dependencies = [ "electrsd", "serde", - "simplex-provider", "simplex-sdk", "simplicityhl", "thiserror", @@ -1962,7 +1456,7 @@ dependencies = [ [[package]] name = "simplicityhl" version = "0.4.1" -source = "git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params#77755254414093ff0e1b51beedbc27367745388e" +source = "git+https://github.com/BlockstreamResearch/SimplicityHL.git?rev=568b462#568b4621d6145cd97dce68a3f3428c7eb85306b6" dependencies = [ "base64 0.21.7", "chumsky", @@ -1976,34 +1470,12 @@ dependencies = [ "simplicity-lang", ] -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "stacker" version = "0.1.23" @@ -2023,22 +1495,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.117" @@ -2050,26 +1506,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "target-triple" version = "1.0.0" @@ -2078,14 +1514,14 @@ checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -2115,7 +1551,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2127,47 +1563,6 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinyvec" version = "1.10.0" @@ -2185,38 +1580,23 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "bytes", - "libc", - "mio", "pin-project-lite", - "socket2", "tokio-macros", - "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls 0.23.37", - "tokio", + "syn", ] [[package]] @@ -2236,9 +1616,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.3+spec-1.1.0" +version = "1.0.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" +checksum = "c94c3321114413476740df133f0d8862c61d87c8d26f04c6841e033c8c80db47" dependencies = [ "indexmap", "serde_core", @@ -2282,51 +1662,6 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags 2.11.0", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - [[package]] name = "tracing" version = "0.1.44" @@ -2338,18 +1673,6 @@ 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" @@ -2358,7 +1681,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2400,12 +1723,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "trybuild" version = "1.0.116" @@ -2418,7 +1735,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml 1.0.3+spec-1.1.0", + "toml 1.0.4+spec-1.1.0", ] [[package]] @@ -2466,24 +1783,6 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -2509,18 +1808,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" [[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[package]] -name = "want" -version = "0.3.1" +name = "walkdir" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ - "try-lock", + "same-file", + "winapi-util", ] [[package]] @@ -2549,9 +1843,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -2560,25 +1854,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a89f4650b770e4521aa6573724e2aed4704372151bd0de9d16a3bbabb87441a" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2586,22 +1866,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -2640,41 +1920,12 @@ dependencies = [ "semver", ] -[[package]] -name = "web-sys" -version = "0.3.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "webpki-roots" version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "which" version = "4.4.2" @@ -2708,7 +1959,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2717,16 +1968,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -2744,31 +1986,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -2777,96 +2002,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.7.14" @@ -2903,7 +2080,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.117", + "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2919,7 +2096,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2961,35 +2138,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.8.40" @@ -3007,67 +2155,7 @@ checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "syn", ] [[package]] diff --git a/simplex/Cargo.toml b/Cargo.toml similarity index 81% rename from simplex/Cargo.toml rename to Cargo.toml index edc4306..bfb3f0d 100644 --- a/simplex/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "3" members = [ "crates/*", ] +exclude = ["examples/basic"] [workspace.package] license = "MIT OR Apache-2.0" @@ -12,10 +13,11 @@ edition = "2024" multiple_crate_versions = "allow" [workspace.dependencies] -simplex-provider = { path = "./crates/provider" } simplex-macros-core = { path = "./crates/macros-core", features = ["bincode", "serde"] } simplex-macros = { path = "./crates/macros" } +simplex-template-gen-core = { path = "./crates/template-gen" } simplex-test = { path = "./crates/test" } +simplex-regtest = { path = "./crates/regtest" } simplex-sdk = { path = "./crates/sdk" } simplex = { path = "./crates/simplex" } @@ -33,7 +35,7 @@ tracing = { version = "0.1.41" } minreq = { version = "2.14.1", features = ["https", "json-using-serde"] } electrsd = { version = "0.29.0", features = ["legacy"] } -simplicityhl = { git = "https://github.com/ikripaka/SimplicityHL/", branch = "feature/rich-params" } +simplicityhl = { git = "https://github.com/BlockstreamResearch/SimplicityHL.git", rev = "568b462" } [patch.crates-io] simplicity-sys = { git = "https://github.com/BlockstreamResearch/rust-simplicity", tag = "simplicity-sys-0.6.1" } diff --git a/README.md b/README.md index e970c91..8e93f93 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,18 @@ This collection of useful crates provides useful utilities for working with Simp - `simplex-core` - provides useful utilities. - `simplex-cli` - provides common cli interface and ability to setup your contract development environment. +## Quick start + +Install simplex with command: +```bash +cargo install --path ./crates/cli +``` + +To run tests for this crate - run +```bash +RUN_SLOW_TESTS=true cargo test +``` + ## License Dual-licensed under either of: diff --git a/assets/electrs b/assets/electrs new file mode 100755 index 0000000..4dd5a9d Binary files /dev/null and b/assets/electrs differ diff --git a/simplex/assets/elementsd b/assets/elementsd similarity index 64% rename from simplex/assets/elementsd rename to assets/elementsd index 30a7b82..93c13ca 100755 Binary files a/simplex/assets/elementsd and b/assets/elementsd differ diff --git a/simplex/crates/cli/Cargo.toml b/crates/cli/Cargo.toml similarity index 84% rename from simplex/crates/cli/Cargo.toml rename to crates/cli/Cargo.toml index 4e6624b..79ba7de 100644 --- a/simplex/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -15,9 +15,11 @@ path = "src/bin/main.rs" workspace = true [dependencies] +simplex-regtest = { workspace = true } simplex-test = { workspace = true } simplex-sdk = { workspace = true } simplex-macros-core = { workspace = true } +simplex-template-gen-core = { workspace = true } simplicityhl = { workspace = true } electrsd = { workspace = true } @@ -33,8 +35,4 @@ tracing = { version = "0.1.44" } tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } ctrlc = { version = "3.5.2", features = ["termination"] } glob = { version = "0.3.3"} - -[dev-dependencies] -tracing = { workspace = true } -tracing-appender = { version = "0.2.3" } -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +globwalk = { version = "0.9.1"} diff --git a/crates/cli/Simplex.default.toml b/crates/cli/Simplex.default.toml new file mode 100644 index 0000000..0b7fa1b --- /dev/null +++ b/crates/cli/Simplex.default.toml @@ -0,0 +1,9 @@ +# TEST CONFIG + +# [test] +# esplora = "esplora_api_url" + +# [test.rpc] +# url = "rpc_url" +# username = "username" +# password = "password" diff --git a/simplex/crates/cli/src/bin/main.rs b/crates/cli/src/bin/main.rs similarity index 70% rename from simplex/crates/cli/src/bin/main.rs rename to crates/cli/src/bin/main.rs index 78d30c5..c23114a 100644 --- a/simplex/crates/cli/src/bin/main.rs +++ b/crates/cli/src/bin/main.rs @@ -1,13 +1,9 @@ -#![warn(clippy::all, clippy::pedantic)] - use clap::Parser; #[tokio::main] async fn main() -> anyhow::Result<()> { let _ = dotenvy::dotenv(); - simplex_cli::logging::init(); - Box::pin(simplex_cli::cli::Cli::parse().run()).await?; Ok(()) diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs new file mode 100644 index 0000000..8852764 --- /dev/null +++ b/crates/cli/src/cli.rs @@ -0,0 +1,77 @@ +use std::path::PathBuf; + +use clap::Parser; + +use crate::commands::commands::Command; +use crate::commands::regtest::Regtest; +use crate::commands::test::Test; +use crate::config::{Config, INIT_CONFIG}; +use crate::error::CliError; + +#[derive(Debug, Parser)] +#[command(name = "Simplex")] +#[command(about = "Simplicity development framework")] +pub struct Cli { + pub config: Option, + + #[command(subcommand)] + pub command: Command, +} + +impl Cli { + pub async fn run(&self) -> Result<(), CliError> { + match &self.command { + Command::Init => { + let config_path = Config::get_default_path()?; + std::fs::write(&config_path, INIT_CONFIG)?; + + println!("Config written to: '{}'", config_path.display()); + + Ok(()) + } + Command::Config => { + let config_path = Config::get_default_path()?; + let loaded_config = Config::load(config_path)?; + + println!("{loaded_config:#?}"); + + Ok(()) + } + Command::Test { command } => { + let config_path = Config::get_default_path()?; + let loaded_config = Config::load(config_path)?; + + println!("{loaded_config:#?}"); + + let test_config = loaded_config.test.unwrap_or_default(); + + Ok(Test::run(test_config, command)?) + } + Command::Regtest => { + // TODO: pass config + Ok(Regtest::run()?) + } + 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(()) + } + } + } +} diff --git a/simplex/crates/cli/src/cli/commands.rs b/crates/cli/src/commands/commands.rs similarity index 56% rename from simplex/crates/cli/src/cli/commands.rs rename to crates/cli/src/commands/commands.rs index 10e0314..f0297cf 100644 --- a/simplex/crates/cli/src/cli/commands.rs +++ b/crates/cli/src/commands/commands.rs @@ -1,52 +1,41 @@ -use clap::{Args, Subcommand}; use std::path::PathBuf; +use clap::{Args, Subcommand}; + #[derive(Debug, Subcommand)] pub enum Command { - /// 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)] + #[arg(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")] + #[arg(long)] pub ignored: bool, } diff --git a/crates/cli/src/commands/error.rs b/crates/cli/src/commands/error.rs new file mode 100644 index 0000000..612bf82 --- /dev/null +++ b/crates/cli/src/commands/error.rs @@ -0,0 +1,11 @@ +#[derive(thiserror::Error, Debug)] +pub enum CommandError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error(transparent)] + Client(#[from] simplex_regtest::error::ClientError), + + #[error(transparent)] + Test(#[from] simplex_test::error::TestError), +} diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs new file mode 100644 index 0000000..26f5e87 --- /dev/null +++ b/crates/cli/src/commands/mod.rs @@ -0,0 +1,4 @@ +pub mod commands; +pub mod test; +pub mod regtest; +pub mod error; diff --git a/crates/cli/src/commands/regtest.rs b/crates/cli/src/commands/regtest.rs new file mode 100644 index 0000000..d6afe2a --- /dev/null +++ b/crates/cli/src/commands/regtest.rs @@ -0,0 +1,34 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use simplex_regtest::TestClient; + +use crate::commands::error::CommandError; + +pub struct Regtest {} + +impl Regtest { + pub fn run() -> Result<(), CommandError> { + let mut client = TestClient::new(); + + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) + .expect("Error setting Ctrl-C handler"); + + println!("======================================"); + println!("Waiting for Ctrl-C..."); + println!("rpc: {}", client.rpc_url()); + println!("esplora: {}", client.esplora_url()); + let auth = client.auth().get_user_pass().unwrap(); + println!("user: {:?}, password: {:?}", auth.0.unwrap(), auth.1.unwrap()); + println!("======================================"); + + while running.load(Ordering::SeqCst) {} + + Ok(client.kill()?) + } +} diff --git a/crates/cli/src/commands/test.rs b/crates/cli/src/commands/test.rs new file mode 100644 index 0000000..359c3da --- /dev/null +++ b/crates/cli/src/commands/test.rs @@ -0,0 +1,118 @@ +use std::path::PathBuf; +use std::process::Stdio; + +use simplex_test::TestConfig; + +use super::commands::{TestCommand, TestFlags}; +use super::error::CommandError; + +pub struct Test {} + +impl Test { + pub fn run(config: TestConfig, command: &TestCommand) -> Result<(), CommandError> { + let cache_path = Self::get_test_config_cache_name()?; + config.to_file(&cache_path)?; + + let mut cargo_test_command = Self::build_cargo_test_command(&cache_path, command); + + let output = cargo_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 build_cargo_test_command(cache_path: &PathBuf, command: &TestCommand) -> std::process::Command { + let mut command_as_arg = String::new(); + + match command { + TestCommand::Integration { additional_flags } => { + command_as_arg.push_str("cargo test --tests"); + + let flag_args = Self::build_test_flags(&additional_flags); + + if !flag_args.is_empty() { + command_as_arg.push_str(" --"); + command_as_arg.push_str(&flag_args); + } + } + TestCommand::Run { + tests, + additional_flags, + } => { + // TODO: check this behavior + if tests.is_empty() { + command_as_arg.push_str("cargo test --tests"); + } else { + let mut arg = "cargo test".to_string(); + + for test_name in tests { + arg.push_str(&format!(" --test {test_name}")); + } + + command_as_arg.push_str(&arg); + } + + let flag_args = Self::build_test_flags(&additional_flags); + + if !flag_args.is_empty() { + command_as_arg.push_str(" --"); + command_as_arg.push_str(&flag_args); + } + } + } + + let mut cargo_test_command = std::process::Command::new("sh"); + cargo_test_command.args(["-c".to_string(), command_as_arg]); + + cargo_test_command + .env(simplex_test::TEST_ENV_NAME, cache_path) + .stdin(Stdio::inherit()) + .stderr(Stdio::inherit()) + .stdout(Stdio::inherit()); + + cargo_test_command + } + + fn build_test_flags(flags: &TestFlags) -> String { + let mut opt_params = String::new(); + + if flags.nocapture { + opt_params.push_str(" --nocapture"); + } + + if flags.show_output { + opt_params.push_str(" --show-output"); + } + + if flags.ignored { + opt_params.push_str(" --ignored"); + } + + opt_params + } + + fn get_test_config_cache_name() -> Result { + const TARGET_DIR_NAME: &str = "target"; + const SIMPLEX_CACHE_DIR_NAME: &str = "simplex"; + const SIMPLEX_TEST_CONFIG_NAME: &str = "simplex_test_config.toml"; + + let cwd = std::env::current_dir()?; + + Ok(cwd + .join(TARGET_DIR_NAME) + .join(SIMPLEX_CACHE_DIR_NAME) + .join(SIMPLEX_TEST_CONFIG_NAME)) + } +} diff --git a/crates/cli/src/config/config.rs b/crates/cli/src/config/config.rs new file mode 100644 index 0000000..68d327c --- /dev/null +++ b/crates/cli/src/config/config.rs @@ -0,0 +1,48 @@ +use serde::Deserialize; +use std::path::{Path, PathBuf}; + +use simplex_test::TestConfig; + +use super::error::ConfigError; + +pub const INIT_CONFIG: &str = include_str!("../../Simplex.default.toml"); +pub const CONFIG_FILENAME: &str = "Simplex.toml"; + +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + #[serde(default)] + pub test: Option, + #[serde(default)] + pub build: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct BuildConf { + pub compile_simf: Option>, + pub out_dir: Option, +} + +impl Config { + pub fn get_default_path() -> Result { + let cwd = std::env::current_dir()?; + + Ok(cwd.join(CONFIG_FILENAME)) + } + + // TODO: load default values like `simf` path + pub fn load(path_buf: impl AsRef) -> Result { + let path = path_buf.as_ref().to_path_buf(); + + if !path.is_file() { + return Err(ConfigError::PathIsNotFile(path)); + } + + if !path.exists() { + return Err(ConfigError::PathNotExists(path)); + } + + let conf_str = std::fs::read_to_string(path)?; + + Ok(toml::from_str(conf_str.as_str()).map_err(ConfigError::UnableToDeserialize)?) + } +} diff --git a/crates/cli/src/config/error.rs b/crates/cli/src/config/error.rs new file mode 100644 index 0000000..86de7ff --- /dev/null +++ b/crates/cli/src/config/error.rs @@ -0,0 +1,22 @@ +use std::path::PathBuf; + +#[derive(thiserror::Error, Debug)] +pub enum ConfigError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("TOML parse error: {0}")] + TomlParse(#[from] toml::de::Error), + + #[error("Unable to deserialize config: {0}")] + UnableToDeserialize(toml::de::Error), + + #[error("Unable to get env variable: {0}")] + UnableToGetEnv(#[from] std::env::VarError), + + #[error("Path doesn't a file: '{0}'")] + PathIsNotFile(PathBuf), + + #[error("Path doesn't exist: '{0}'")] + PathNotExists(PathBuf), +} diff --git a/crates/cli/src/config/mod.rs b/crates/cli/src/config/mod.rs new file mode 100644 index 0000000..ce90ebd --- /dev/null +++ b/crates/cli/src/config/mod.rs @@ -0,0 +1,4 @@ +pub mod error; +pub mod config; + +pub use config::{Config, INIT_CONFIG}; diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs new file mode 100644 index 0000000..5f07829 --- /dev/null +++ b/crates/cli/src/error.rs @@ -0,0 +1,16 @@ +use crate::{commands::error::CommandError, config}; + +#[derive(thiserror::Error, Debug)] +pub enum CliError { + #[error(transparent)] + Config(#[from] config::error::ConfigError), + + #[error(transparent)] + Command(#[from] CommandError), + + #[error("IO error: '{0}'")] + Io(#[from] std::io::Error), + + #[error("Occurred code generation error, error: '{0}'")] + CodeGenerator(#[from] simplex_template_gen_core::CodeGeneratorError), +} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs new file mode 100644 index 0000000..9a253d5 --- /dev/null +++ b/crates/cli/src/lib.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod error; +pub mod cli; +pub mod commands; diff --git a/crates/macros-core/Cargo.toml b/crates/macros-core/Cargo.toml new file mode 100644 index 0000000..3bb3a65 --- /dev/null +++ b/crates/macros-core/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "simplex-macros-core" +description = "Macro support core for Simplex, the Rust SimplicityHl toolkit. Not intended to be used directly." +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[features] +bincode = [] +serde = ["bincode", "dep:serde"] +default = ["bincode", "serde"] + +[dependencies] +simplicityhl = { workspace = true } +simplex-test = { workspace = true } + +proc-macro2 = { version = "1.0.106", features = ["span-locations"] } +syn = { version = "2.0.114", default-features = false, features = ["proc-macro", "full", "parsing", "derive", "clone-impls", "extra-traits", "printing"] } +quote = { version = "1.0.44" } +serde = { version = "1.0.228", optional = true } diff --git a/simplex/crates/macros-core/src/attr/codegen.rs b/crates/macros-core/src/attr/codegen.rs similarity index 90% rename from simplex/crates/macros-core/src/attr/codegen.rs rename to crates/macros-core/src/attr/codegen.rs index c089898..041a61a 100644 --- a/simplex/crates/macros-core/src/attr/codegen.rs +++ b/crates/macros-core/src/attr/codegen.rs @@ -1,6 +1,5 @@ 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}; @@ -144,26 +143,24 @@ impl WitnessStruct { } } - impl simplex::serde::Serialize for #struct_name { + impl ::simplex::serde::Serialize for #struct_name { fn serialize(&self, serializer: S) -> Result where - S: simplex::serde::Serializer, + S: ::simplex::serde::Serializer, { self.build_arguments().serialize(serializer) } } - impl<'de> simplex::serde::Deserialize<'de> for #struct_name { + impl<'de> ::simplex::serde::Deserialize<'de> for #struct_name { fn deserialize(deserializer: D) -> Result where - D: simplex::serde::Deserializer<'de>, + D: ::simplex::serde::Deserializer<'de>, { let x = ::simplicityhl::Arguments::deserialize(deserializer)?; Self::from_arguments(&x).map_err(simplex::serde::de::Error::custom) } } - - // impl ::simplex_core::Encodable for #struct_name {} }, }) } @@ -183,13 +180,13 @@ impl WitnessStruct { Ok(GeneratedWitnessTokens { imports: quote! { - use std::collections::HashMap; - use simplicityhl::{WitnessValues, Value, ResolvedType}; - use simplicityhl::value::{UIntValue, ValueInner}; - use simplicityhl::num::U256; - use simplicityhl::str::WitnessName; - use simplicityhl::types::TypeConstructible; - use simplicityhl::value::ValueConstructible; + use ::std::collections::HashMap; + use ::simplicityhl::{WitnessValues, 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::program::WitnessTrait; }, struct_token_stream: quote! { @@ -219,26 +216,24 @@ impl WitnessStruct { } } - impl simplex::serde::Serialize for #struct_name { + impl ::simplex::serde::Serialize for #struct_name { fn serialize(&self, serializer: S) -> Result where - S: simplex::serde::Serializer, + S: ::simplex::serde::Serializer, { self.build_witness().serialize(serializer) } } - impl<'de> simplex::serde::Deserialize<'de> for #struct_name { + impl<'de> ::simplex::serde::Deserialize<'de> for #struct_name { fn deserialize(deserializer: D) -> Result where - D: simplex::serde::Deserializer<'de>, + D: ::simplex::serde::Deserializer<'de>, { let x = ::simplicityhl::WitnessValues::deserialize(deserializer)?; Self::from_witness(&x).map_err(simplex::serde::de::Error::custom) } } - - // impl ::simplex_core::Encodable for #struct_name {} }, }) } @@ -258,6 +253,7 @@ impl WitnessStruct { witness_values: WitnessStruct::generate_witness_fields(meta.iter())?, }) } + fn generate_witness_fields<'a>( iter: impl Iterator, ) -> syn::Result> { @@ -331,7 +327,7 @@ impl WitnessStruct { } } -pub(crate) fn convert_contract_name_to_struct_name(contract_name: &str) -> String { +pub fn convert_contract_name_to_struct_name(contract_name: &str) -> String { let words: Vec = contract_name .split('_') .filter(|w| !w.is_empty()) @@ -346,10 +342,10 @@ pub(crate) fn convert_contract_name_to_struct_name(contract_name: &str) -> Strin words.join("") } -pub(crate) fn convert_contract_name_to_contract_source_const(contract_name: &str) -> Ident { +pub fn convert_contract_name_to_contract_source_const(contract_name: &str) -> proc_macro2::Ident { format_ident!("{}_CONTRACT_SOURCE", contract_name.to_uppercase()) } -pub(crate) fn convert_contract_name_to_contract_module(contract_name: &str) -> Ident { +pub fn convert_contract_name_to_contract_module(contract_name: &str) -> proc_macro2::Ident { format_ident!("derived_{}", contract_name) } diff --git a/simplex/crates/macros-core/src/attr/mod.rs b/crates/macros-core/src/attr/mod.rs similarity index 57% rename from simplex/crates/macros-core/src/attr/mod.rs rename to crates/macros-core/src/attr/mod.rs index 0d367ba..d3b1c03 100644 --- a/simplex/crates/macros-core/src/attr/mod.rs +++ b/crates/macros-core/src/attr/mod.rs @@ -12,12 +12,6 @@ use proc_macro2::Span; use quote::quote; use simplicityhl::AbiMeta; use std::error::Error; -// TODO(Illia): add bincode generation feature (i.e. require bincode dependencies) -// TODO(Illia): add conditional compilation for simplicity-core to e included automatically - -// TODO(Illia): automatically derive bincode implementation -// TODO(Illia): extract either:serde feature and use it when simplicityhl has serde feature -// TODO(Illia): add features /// Expands helper functions for the given Simf content and metadata. /// @@ -52,46 +46,7 @@ fn construct_program_helpers(derived_meta: &SimfContractMeta) -> proc_macro2::To let contract_source_name = &derived_meta.contract_source_const_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; - pub const #contract_source_name: &str = #contract_content; - - /// Get the options template program for instantiation. - /// - /// # Panics - /// - if the embedded source fails to compile (should never happen). - // #[must_use] - // pub fn get_template_program() -> ::simplicityhl::TemplateProgram { - // ::simplicityhl::TemplateProgram::new(#contract_source_name).expect(#error_msg) - // } - - /// 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()) - // } - - /// 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() - // } } } diff --git a/simplex/crates/macros-core/src/attr/parse.rs b/crates/macros-core/src/attr/parse.rs similarity index 96% rename from simplex/crates/macros-core/src/attr/parse.rs rename to crates/macros-core/src/attr/parse.rs index b146466..28bb9dc 100644 --- a/simplex/crates/macros-core/src/attr/parse.rs +++ b/crates/macros-core/src/attr/parse.rs @@ -7,10 +7,6 @@ use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::{Expr, ExprLit, Lit}; -// TODO: come up with an idea of how to parse constant values and evaluate constant values that are passed inside -// pub const OPTION_SOURCE: &str = include_str!("source_simf/options.simf"); -// include_simf_source!(OPTION_SOURCE); - pub struct SynFilePath { _span_file: String, path_literal: String, diff --git a/simplex/crates/macros-core/src/attr/program.rs b/crates/macros-core/src/attr/program.rs similarity index 100% rename from simplex/crates/macros-core/src/attr/program.rs rename to crates/macros-core/src/attr/program.rs diff --git a/simplex/crates/macros-core/src/attr/types.rs b/crates/macros-core/src/attr/types.rs similarity index 100% rename from simplex/crates/macros-core/src/attr/types.rs rename to crates/macros-core/src/attr/types.rs diff --git a/simplex/crates/macros-core/src/lib.rs b/crates/macros-core/src/lib.rs similarity index 70% rename from simplex/crates/macros-core/src/lib.rs rename to crates/macros-core/src/lib.rs index 4194710..27285d0 100644 --- a/simplex/crates/macros-core/src/lib.rs +++ b/crates/macros-core/src/lib.rs @@ -1,8 +1,6 @@ #![warn(clippy::all, clippy::pedantic)] pub mod attr; -/// Module releted to simplex environment generation -pub mod env; pub mod test; /// Expands the `include_simf` macro. @@ -22,12 +20,6 @@ pub fn expand_include_simf(input: &attr::parse::SynFilePath) -> syn::Result syn::Result { - test::expand(args, input) -} - -pub fn expand_simplex_contract_enviroment( - outdir: impl AsRef, - simfs: &[impl AsRef], -) -> Result<(), env::CodeGeneratorError> { - env::CodeGenerator::generate_files(outdir, simfs) + // test::expand(args, input) + todo!() } diff --git a/simplex/crates/macros-core/src/test/mod.rs b/crates/macros-core/src/test/mod.rs similarity index 91% rename from simplex/crates/macros-core/src/test/mod.rs rename to crates/macros-core/src/test/mod.rs index d255f48..0309d45 100644 --- a/simplex/crates/macros-core/src/test/mod.rs +++ b/crates/macros-core/src/test/mod.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::quote; use syn::parse::Parser; pub(crate) fn expand_inner(input: &syn::ItemFn, args: AttributeArgs) -> syn::Result { @@ -10,10 +10,8 @@ pub(crate) fn expand_inner(input: &syn::ItemFn, args: AttributeArgs) -> syn::Res 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! { @@ -57,8 +55,6 @@ pub(crate) fn expand_inner(input: &syn::ItemFn, args: AttributeArgs) -> syn::Res } #ok_path_generation }; - // println!("fn name: {}, \n ident: {}", #fn_name_str, #ident); - // println!("input: {}, \n AttributeArgs: {}", "", #args_str); #name(test_context) } diff --git a/simplex/crates/macros/Cargo.toml b/crates/macros/Cargo.toml similarity index 100% rename from simplex/crates/macros/Cargo.toml rename to crates/macros/Cargo.toml diff --git a/simplex/crates/macros/src/lib.rs b/crates/macros/src/lib.rs similarity index 100% rename from simplex/crates/macros/src/lib.rs rename to crates/macros/src/lib.rs diff --git a/crates/regtest/Cargo.toml b/crates/regtest/Cargo.toml new file mode 100644 index 0000000..d5ff34a --- /dev/null +++ b/crates/regtest/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "simplex-regtest" +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +simplex-sdk = { workspace = true } + +thiserror = { workspace = true } +simplicityhl = { workspace = true } +electrsd = { workspace = true } +serde = { workspace = true } +toml = { workspace = true } diff --git a/crates/regtest/src/args.rs b/crates/regtest/src/args.rs new file mode 100644 index 0000000..8469f8c --- /dev/null +++ b/crates/regtest/src/args.rs @@ -0,0 +1,23 @@ +pub const DEFAULT_SAT_AMOUNT_FAUCET: u64 = 100000; + +pub fn get_elementsd_bin_args() -> Vec { + vec![ + "-fallbackfee=0.0001".to_string(), + "-dustrelayfee=0.00000001".to_string(), + "-acceptdiscountct=1".to_string(), + "-rest".to_string(), + "-evbparams=simplicity:-1:::".to_string(), + "-minrelaytxfee=0".to_string(), + "-blockmintxfee=0".to_string(), + "-chain=liquidregtest".to_string(), + "-txindex=1".to_string(), + "-validatepegin=0".to_string(), + "-initialfreecoins=2100000000000000".to_string(), + "-listen=1".to_string(), + "-txindex=1".to_string(), + ] +} + +pub fn get_electrs_bin_args() -> Vec { + vec!["-v".to_string()] +} diff --git a/crates/regtest/src/client.rs b/crates/regtest/src/client.rs new file mode 100644 index 0000000..3beae5a --- /dev/null +++ b/crates/regtest/src/client.rs @@ -0,0 +1,83 @@ +use std::path::{Path, PathBuf}; + +use electrsd::ElectrsD; +use electrsd::bitcoind; +use electrsd::bitcoind::bitcoincore_rpc::Auth; +use electrsd::bitcoind::{BitcoinD, Conf}; + +use super::error::ClientError; +use crate::args::{get_electrs_bin_args, get_elementsd_bin_args}; + +pub struct TestClient { + pub electrs: ElectrsD, + pub elements: BitcoinD, +} + +impl TestClient { + // TODO: pass custom config + pub fn new() -> Self { + let (electrs_path, elementsd_path) = Self::default_bin_paths(); + let elements = Self::create_bitcoind_node(elementsd_path); + let electrs = Self::create_electrs_node(electrs_path, &elements); + + Self { + electrs: electrs, + elements: elements, + } + } + + pub fn default_bin_paths() -> (PathBuf, PathBuf) { + // TODO: change binary into installed one in $PATH dir + const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); + const ELEMENTSD_BIN_PATH: &str = "../../assets/elementsd"; + const ELECTRS_BIN_PATH: &str = "../../assets/electrs"; + + ( + Path::new(MANIFEST_DIR).join(ELECTRS_BIN_PATH), + Path::new(MANIFEST_DIR).join(ELEMENTSD_BIN_PATH), + ) + } + + pub fn rpc_url(&self) -> String { + self.elements.rpc_url() + } + + pub fn esplora_url(&self) -> String { + self.electrs.esplora_url.clone().unwrap() + } + + pub fn auth(&self) -> Auth { + let cookie = self.elements.params.get_cookie_values().unwrap().unwrap(); + + Auth::UserPass(cookie.user, cookie.password) + } + + pub fn kill(&mut self) -> Result<(), ClientError> { + self.electrs.kill().map_err(|_| ClientError::ElectrsTermination())?; + self.elements.stop().map_err(|_| ClientError::ElementsTermination())?; + + Ok(()) + } + + fn create_bitcoind_node(bin_path: impl AsRef) -> BitcoinD { + let mut conf = Conf::default(); + let bin_args = get_elementsd_bin_args(); + + conf.args = bin_args.iter().map(|x| x.as_ref()).collect::>(); + conf.network = "liquidregtest"; + conf.p2p = bitcoind::P2P::Yes; + + BitcoinD::with_conf(bin_path.as_ref(), &conf).unwrap() + } + + fn create_electrs_node(bin_path: impl AsRef, elementsd: &BitcoinD) -> ElectrsD { + let mut conf = electrsd::Conf::default(); + let bin_args = get_electrs_bin_args(); + + conf.args = bin_args.iter().map(|x| x.as_ref()).collect::>(); + conf.http_enabled = true; + conf.network = "liquidregtest"; + + ElectrsD::with_conf(bin_path.as_ref(), &elementsd, &conf).unwrap() + } +} diff --git a/crates/regtest/src/error.rs b/crates/regtest/src/error.rs new file mode 100644 index 0000000..966185e --- /dev/null +++ b/crates/regtest/src/error.rs @@ -0,0 +1,8 @@ +#[derive(thiserror::Error, Debug)] +pub enum ClientError { + #[error("Failed to terminate elements")] + ElementsTermination(), + + #[error("Failed to terminate electrs")] + ElectrsTermination(), +} diff --git a/crates/regtest/src/lib.rs b/crates/regtest/src/lib.rs new file mode 100644 index 0000000..7a0cd31 --- /dev/null +++ b/crates/regtest/src/lib.rs @@ -0,0 +1,5 @@ +mod args; +pub mod client; +pub mod error; + +pub use client::TestClient; diff --git a/simplex/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml similarity index 77% rename from simplex/crates/sdk/Cargo.toml rename to crates/sdk/Cargo.toml index 9880bed..62f9067 100644 --- a/simplex/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -10,16 +10,17 @@ readme = "README.md" workspace = true [dependencies] -simplex-provider = { workspace = true } - async-trait = { workspace = true } thiserror = { workspace = true } sha2 = { workspace = true } minreq = { workspace = true } simplicityhl = { workspace = true } +electrsd = { workspace = true } hex = { version = "0.4" } -serde = { version = "1.0.228", features = ["derive"]} +serde = { version = "1.0.228", features = ["derive"] } +serde_json = { version = "1.0" } elements-miniscript = { version = "0.4", features = ["base64", "serde"] } bip39 = { version = "2.0.0", features = ["rand"] } dyn-clone = { version = "1.0.20" } +bitcoin_hashes = { version = "0.14.1" } diff --git a/crates/sdk/src/constants.rs b/crates/sdk/src/constants.rs new file mode 100644 index 0000000..b9c3b45 --- /dev/null +++ b/crates/sdk/src/constants.rs @@ -0,0 +1,16 @@ +pub const PUBLIC_SECRET_BLINDER_KEY: [u8; 32] = [1; 32]; +pub const DUMMY_SIGNATURE: [u8; 64] = [1; 64]; + +pub const MIN_FEE: u64 = 10; + +/// 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 const LIQUID_TESTNET_TEST_ASSET_ID_STR: &str = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; diff --git a/simplex/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs similarity index 100% rename from simplex/crates/sdk/src/lib.rs rename to crates/sdk/src/lib.rs index 96ab949..5dc4e44 100644 --- a/simplex/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1,7 +1,7 @@ pub mod constants; pub mod presets; pub mod program; -pub mod transaction; pub mod provider; pub mod signer; +pub mod transaction; pub mod utils; diff --git a/simplex/crates/sdk/src/presets/mod.rs b/crates/sdk/src/presets/mod.rs similarity index 100% rename from simplex/crates/sdk/src/presets/mod.rs rename to crates/sdk/src/presets/mod.rs diff --git a/simplex/crates/sdk/src/presets/p2pk.rs b/crates/sdk/src/presets/p2pk.rs similarity index 100% rename from simplex/crates/sdk/src/presets/p2pk.rs rename to crates/sdk/src/presets/p2pk.rs diff --git a/simplex/crates/sdk/src/presets/simf/p2pk.simf b/crates/sdk/src/presets/simf/p2pk.simf similarity index 98% rename from simplex/crates/sdk/src/presets/simf/p2pk.simf rename to crates/sdk/src/presets/simf/p2pk.simf index db4f27c..f6a75e6 100644 --- a/simplex/crates/sdk/src/presets/simf/p2pk.simf +++ b/crates/sdk/src/presets/simf/p2pk.simf @@ -1,3 +1,3 @@ fn main() { jet::bip_0340_verify((param::PUBLIC_KEY, jet::sig_all_hash()), witness::SIGNATURE) -} \ No newline at end of file +} diff --git a/simplex/crates/sdk/src/program/arguments.rs b/crates/sdk/src/program/arguments.rs similarity index 100% rename from simplex/crates/sdk/src/program/arguments.rs rename to crates/sdk/src/program/arguments.rs index a855e46..60799cd 100644 --- a/simplex/crates/sdk/src/program/arguments.rs +++ b/crates/sdk/src/program/arguments.rs @@ -1,5 +1,5 @@ -use simplicityhl::Arguments; use dyn_clone::DynClone; +use simplicityhl::Arguments; pub trait ArgumentsTrait: DynClone { fn build_arguments(&self) -> Arguments; diff --git a/simplex/crates/sdk/src/program/error.rs b/crates/sdk/src/program/error.rs similarity index 100% rename from simplex/crates/sdk/src/program/error.rs rename to crates/sdk/src/program/error.rs diff --git a/simplex/crates/sdk/src/program/mod.rs b/crates/sdk/src/program/mod.rs similarity index 100% rename from simplex/crates/sdk/src/program/mod.rs rename to crates/sdk/src/program/mod.rs diff --git a/simplex/crates/sdk/src/program/program.rs b/crates/sdk/src/program/program.rs similarity index 99% rename from simplex/crates/sdk/src/program/program.rs rename to crates/sdk/src/program/program.rs index 6ccc521..da9c97c 100644 --- a/simplex/crates/sdk/src/program/program.rs +++ b/crates/sdk/src/program/program.rs @@ -16,7 +16,7 @@ use simplicityhl::tracker::{DefaultTracker, TrackerLogLevel}; use super::arguments::ArgumentsTrait; use super::error::ProgramError; -use crate::constants::SimplicityNetwork; +use crate::provider::SimplicityNetwork; pub trait ProgramTrait: DynClone { fn get_env( diff --git a/simplex/crates/sdk/src/program/witness.rs b/crates/sdk/src/program/witness.rs similarity index 100% rename from simplex/crates/sdk/src/program/witness.rs rename to crates/sdk/src/program/witness.rs index b6d0735..374f8f6 100644 --- a/simplex/crates/sdk/src/program/witness.rs +++ b/crates/sdk/src/program/witness.rs @@ -1,5 +1,5 @@ -use simplicityhl::WitnessValues; use dyn_clone::DynClone; +use simplicityhl::WitnessValues; pub trait WitnessTrait: DynClone { fn build_witness(&self) -> WitnessValues; diff --git a/simplex/crates/sdk/src/provider/error.rs b/crates/sdk/src/provider/error.rs similarity index 83% rename from simplex/crates/sdk/src/provider/error.rs rename to crates/sdk/src/provider/error.rs index 81c9011..40a994b 100644 --- a/simplex/crates/sdk/src/provider/error.rs +++ b/crates/sdk/src/provider/error.rs @@ -1,5 +1,10 @@ +use crate::provider::rpc::error::RpcError; + #[derive(Debug, thiserror::Error)] pub enum ProviderError { + #[error(transparent)] + Rpc(#[from] RpcError), + #[error("HTTP request failed: {0}")] Request(String), diff --git a/simplex/crates/sdk/src/provider/esplora.rs b/crates/sdk/src/provider/esplora.rs similarity index 88% rename from simplex/crates/sdk/src/provider/esplora.rs rename to crates/sdk/src/provider/esplora.rs index e6f2b1e..e08becd 100644 --- a/simplex/crates/sdk/src/provider/esplora.rs +++ b/crates/sdk/src/provider/esplora.rs @@ -11,14 +11,12 @@ use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid use serde::Deserialize; -pub use simplex_provider::esplora::*; - use super::error::ProviderError; -use super::provider::ProviderTrait; +use super::provider::{DEFAULT_TIMEOUT_SECS, ProviderTrait}; -#[derive(Clone)] pub struct EsploraProvider { - esplora_url: String, + pub esplora_url: String, + pub timeout: Duration, } #[derive(Deserialize)] @@ -59,7 +57,10 @@ struct EsploraUtxo { impl EsploraProvider { pub fn new(url: String) -> Self { - Self { esplora_url: url } + Self { + esplora_url: url, + timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS), + } } fn esplora_utxo_to_outpoint(&self, utxo: &EsploraUtxo) -> Result { @@ -98,8 +99,10 @@ impl ProviderTrait for EsploraProvider { fn broadcast_transaction(&self, tx: &Transaction) -> Result { let tx_hex = encode::serialize_hex(tx); let url = format!("{}/tx", self.esplora_url); + let timeout_secs = self.timeout.as_secs(); let response = minreq::post(&url) + .with_timeout(timeout_secs) .with_body(tx_hex) .send() .map_err(|e| ProviderError::Request(e.to_string()))?; @@ -119,10 +122,12 @@ impl ProviderTrait for EsploraProvider { } fn wait(&self, txid: &Txid) -> Result<(), ProviderError> { - let status_url = format!("{}/tx/{}/status", self.esplora_url, txid); + let url = format!("{}/tx/{}/status", self.esplora_url, txid); + let timeout_secs = self.timeout.as_secs(); for _ in 1..10 { - let response = minreq::get(&status_url) + let response = minreq::get(&url) + .with_timeout(timeout_secs) .send() .map_err(|e| ProviderError::Request(e.to_string()))?; @@ -145,7 +150,10 @@ impl ProviderTrait for EsploraProvider { fn fetch_transaction(&self, txid: &Txid) -> Result { let url = format!("{}/tx/{}/raw", self.esplora_url, txid); + let timeout_secs = self.timeout.as_secs(); + let response = minreq::get(&url) + .with_timeout(timeout_secs) .send() .map_err(|e| ProviderError::Request(e.to_string()))?; @@ -164,7 +172,10 @@ impl ProviderTrait for EsploraProvider { fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError> { let url = format!("{}/address/{}/utxo", self.esplora_url, address); + let timeout_secs = self.timeout.as_secs(); + let response = minreq::get(&url) + .with_timeout(timeout_secs) .send() .map_err(|e| ProviderError::Request(e.to_string()))?; @@ -190,7 +201,10 @@ impl ProviderTrait for EsploraProvider { let scripthash = hex::encode(hash_bytes); let url = format!("{}/scripthash/{}/utxo", self.esplora_url, scripthash); + let timeout_secs = self.timeout.as_secs(); + let response = minreq::get(&url) + .with_timeout(timeout_secs) .send() .map_err(|e| ProviderError::Request(e.to_string()))?; @@ -212,7 +226,10 @@ impl ProviderTrait for EsploraProvider { fn fetch_fee_estimates(&self) -> Result, ProviderError> { let url = format!("{}/fee-estimates", self.esplora_url); + let timeout_secs = self.timeout.as_secs(); + let response = minreq::get(&url) + .with_timeout(timeout_secs) .send() .map_err(|e| ProviderError::Request(e.to_string()))?; diff --git a/simplex/crates/sdk/src/provider/mod.rs b/crates/sdk/src/provider/mod.rs similarity index 57% rename from simplex/crates/sdk/src/provider/mod.rs rename to crates/sdk/src/provider/mod.rs index f929382..916b68e 100644 --- a/simplex/crates/sdk/src/provider/mod.rs +++ b/crates/sdk/src/provider/mod.rs @@ -1,7 +1,12 @@ pub mod error; pub mod esplora; +pub mod network; pub mod provider; +pub(crate) mod rpc; +pub mod simplex; pub use error::ProviderError; pub use esplora::EsploraProvider; +pub use network::*; pub use provider::ProviderTrait; +pub use rpc::elements::ElementsRpc; diff --git a/simplex/crates/sdk/src/constants.rs b/crates/sdk/src/provider/network.rs similarity index 75% rename from simplex/crates/sdk/src/constants.rs rename to crates/sdk/src/provider/network.rs index 01aab2d..c7e3cd3 100644 --- a/simplex/crates/sdk/src/constants.rs +++ b/crates/sdk/src/provider/network.rs @@ -1,29 +1,10 @@ -use simplicityhl::simplicity::elements; -use simplicityhl::simplicity::hashes::{Hash, sha256}; use std::str::FromStr; -pub const PUBLIC_SECRET_BLINDER_KEY: [u8; 32] = [1; 32]; -pub const DUMMY_SIGNATURE: [u8; 64] = [1; 64]; - -pub const DEFAULT_TARGET_BLOCKS: u32 = 0; -pub const DEFAULT_FEE_RATE: f32 = 100.0; -pub const MIN_FEE: u64 = 10; -pub const PLACEHOLDER_FEE: u64 = 1; - -pub const WITNESS_SCALE_FACTOR: usize = 4; - -/// 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"; +use simplicityhl::simplicity::elements; +use simplicityhl::simplicity::hashes::{Hash, sha256}; -/// Example test asset id (hex, BE) on Liquid testnet. -pub static LIQUID_TESTNET_TEST_ASSET_ID_STR: &str = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; +use crate::constants::{LIQUID_DEFAULT_REGTEST_ASSET_STR, LIQUID_POLICY_ASSET_STR, LIQUID_TESTNET_POLICY_ASSET_STR}; pub static LIQUID_TESTNET_BITCOIN_ASSET: std::sync::LazyLock = std::sync::LazyLock::new(|| { elements::AssetId::from_inner(sha256::Midstate([ diff --git a/simplex/crates/sdk/src/provider/provider.rs b/crates/sdk/src/provider/provider.rs similarity index 95% rename from simplex/crates/sdk/src/provider/provider.rs rename to crates/sdk/src/provider/provider.rs index ca0a92c..dcc79d1 100644 --- a/simplex/crates/sdk/src/provider/provider.rs +++ b/crates/sdk/src/provider/provider.rs @@ -3,7 +3,9 @@ use std::collections::HashMap; use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid}; use super::error::ProviderError; -use crate::constants::DEFAULT_FEE_RATE; + +pub const DEFAULT_FEE_RATE: f32 = 100.0; +pub const DEFAULT_TIMEOUT_SECS: u64 = 10; pub trait ProviderTrait { fn broadcast_transaction(&self, tx: &Transaction) -> Result; diff --git a/crates/sdk/src/provider/rpc/elements.rs b/crates/sdk/src/provider/rpc/elements.rs new file mode 100644 index 0000000..3b4dc91 --- /dev/null +++ b/crates/sdk/src/provider/rpc/elements.rs @@ -0,0 +1,129 @@ +use std::str::FromStr; + +use bitcoind::bitcoincore_rpc::{Auth, Client, RpcApi}; +use electrsd::bitcoind; + +use serde_json::Value; + +use simplicityhl::elements::{Address, AssetId, BlockHash, Txid}; + +use super::error::RpcError; + +use crate::utils::sat2btc; + +pub struct ElementsRpc { + inner: Client, + auth: Auth, + pub url: String, +} + +impl ElementsRpc { + pub fn new(url: String, auth: Auth) -> Result { + let inner = Client::new(url.as_str(), auth.clone())?; + inner.ping()?; + + Ok(Self { + inner: inner, + auth: auth, + url: url, + }) + } + + pub fn height(&self) -> Result { + const METHOD: &str = "getblockcount"; + + self.inner + .call::(METHOD, &[])? + .as_u64() + .ok_or_else(|| RpcError::ElementsRpcUnexpectedReturn(METHOD.into())) + } + + pub fn block_hash(&self, height: u64) -> Result { + const METHOD: &str = "getblockhash"; + + let raw: Value = self.inner.call(METHOD, &[height.into()])?; + + Ok(BlockHash::from_str(raw.as_str().unwrap())?) + } + + pub fn sendtoaddress(&self, address: &Address, satoshi: u64, asset: Option) -> Result { + const METHOD: &str = "sendtoaddress"; + + let btc = sat2btc(satoshi); + let r = match asset { + Some(asset) => self.inner.call::( + METHOD, + &[ + address.to_string().into(), + btc.into(), + "".into(), + "".into(), + false.into(), + false.into(), + 1.into(), + "UNSET".into(), + false.into(), + asset.to_string().into(), + ], + )?, + None => self + .inner + .call::(METHOD, &[address.to_string().into(), btc.into()])?, + }; + + Ok(Txid::from_str(r.as_str().unwrap()).unwrap()) + } + + pub fn rescanblockchain(&self, start: Option, stop: Option) -> Result<(), RpcError> { + const METHOD: &str = "rescanblockchain"; + + let mut args = Vec::with_capacity(2); + + if start.is_some() { + args.push(start.into()); + } + + if stop.is_some() { + args.push(stop.into()); + } + + self.inner.call::(METHOD, &args)?; + + Ok(()) + } + + pub fn getnewaddress(&self, label: &str) -> Result { + const METHOD: &str = "getnewaddress"; + + let addr: Value = self.inner.call(METHOD, &[label.into(), "bech32".to_string().into()])?; + + Ok(Address::from_str(addr.as_str().unwrap()).unwrap()) + } + + pub fn generate_blocks(&self, block_num: u32) -> Result<(), RpcError> { + const METHOD: &str = "generatetoaddress"; + + let address = self.getnewaddress("")?.to_string(); + self.inner.call::(METHOD, &[block_num.into(), address.into()])?; + + Ok(()) + } + + pub fn sweep_initialfreecoins(&self) -> Result<(), RpcError> { + const METHOD: &str = "sendtoaddress"; + + let address = self.getnewaddress("")?; + self.inner.call::( + METHOD, + &[ + address.to_string().into(), + "21".into(), + "".into(), + "".into(), + true.into(), + ], + )?; + + Ok(()) + } +} diff --git a/crates/sdk/src/provider/rpc/error.rs b/crates/sdk/src/provider/rpc/error.rs new file mode 100644 index 0000000..0d76c06 --- /dev/null +++ b/crates/sdk/src/provider/rpc/error.rs @@ -0,0 +1,11 @@ +#[derive(thiserror::Error, Debug)] +pub enum RpcError { + #[error(transparent)] + ElementsRpcError(#[from] electrsd::bitcoind::bitcoincore_rpc::Error), + + #[error("Elements RPC returned an unexpected value for call {0}")] + ElementsRpcUnexpectedReturn(String), + + #[error("Failed to decode hex value to array, {0}")] + BitcoinHashesHex(#[from] bitcoin_hashes::hex::HexToArrayError), +} diff --git a/crates/sdk/src/provider/rpc/mod.rs b/crates/sdk/src/provider/rpc/mod.rs new file mode 100644 index 0000000..9201cc8 --- /dev/null +++ b/crates/sdk/src/provider/rpc/mod.rs @@ -0,0 +1,2 @@ +pub mod elements; +pub mod error; diff --git a/crates/sdk/src/provider/simplex.rs b/crates/sdk/src/provider/simplex.rs new file mode 100644 index 0000000..97af6a9 --- /dev/null +++ b/crates/sdk/src/provider/simplex.rs @@ -0,0 +1,54 @@ +use std::collections::HashMap; + +use bitcoind::bitcoincore_rpc::Auth; +use electrsd::bitcoind; + +use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid}; + +use crate::provider::{ProviderError, ProviderTrait}; + +use super::{ElementsRpc, EsploraProvider}; + +pub struct SimplexProvider { + pub esplora: EsploraProvider, + pub elements: ElementsRpc, +} + +impl SimplexProvider { + pub fn new(esplora_url: String, elements_url: String, auth: Auth) -> Result { + Ok(Self { + esplora: EsploraProvider::new(esplora_url), + elements: ElementsRpc::new(elements_url, auth)?, + }) + } +} + +impl ProviderTrait for SimplexProvider { + fn broadcast_transaction(&self, tx: &Transaction) -> Result { + let txid = self.esplora.broadcast_transaction(tx)?; + + self.elements.generate_blocks(1)?; + + Ok(txid) + } + + fn wait(&self, txid: &Txid) -> Result<(), ProviderError> { + Ok(self.esplora.wait(txid)?) + } + + fn fetch_transaction(&self, txid: &Txid) -> Result { + Ok(self.esplora.fetch_transaction(txid)?) + } + + fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError> { + Ok(self.esplora.fetch_address_utxos(address)?) + } + + fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError> { + Ok(self.esplora.fetch_scripthash_utxos(script)?) + } + + fn fetch_fee_estimates(&self) -> Result, ProviderError> { + Ok(self.esplora.fetch_fee_estimates()?) + } +} diff --git a/simplex/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs similarity index 100% rename from simplex/crates/sdk/src/signer/error.rs rename to crates/sdk/src/signer/error.rs diff --git a/simplex/crates/sdk/src/signer/mod.rs b/crates/sdk/src/signer/mod.rs similarity index 100% rename from simplex/crates/sdk/src/signer/mod.rs rename to crates/sdk/src/signer/mod.rs diff --git a/simplex/crates/sdk/src/signer/signer.rs b/crates/sdk/src/signer/signer.rs similarity index 97% rename from simplex/crates/sdk/src/signer/signer.rs rename to crates/sdk/src/signer/signer.rs index a485dd8..fe0d317 100644 --- a/simplex/crates/sdk/src/signer/signer.rs +++ b/crates/sdk/src/signer/signer.rs @@ -29,14 +29,17 @@ use elements_miniscript::{ }; use super::error::SignerError; -use crate::constants::{MIN_FEE, PLACEHOLDER_FEE, SimplicityNetwork}; +use crate::constants::MIN_FEE; use crate::program::ProgramTrait; use crate::provider::ProviderTrait; +use crate::provider::SimplicityNetwork; use crate::transaction::FinalTransaction; use crate::transaction::PartialInput; use crate::transaction::PartialOutput; use crate::transaction::RequiredSignature; +pub const PLACEHOLDER_FEE: u64 = 1; + pub trait SignerTrait { fn sign_program( &self, @@ -53,14 +56,14 @@ pub trait SignerTrait { ) -> Result<(PublicKey, ecdsa::Signature), SignerError>; } -pub struct Signer { +pub struct Signer<'a> { xprv: Xpriv, - provider: Box, + provider: Box<&'a dyn ProviderTrait>, network: SimplicityNetwork, secp: Secp256k1, } -impl SignerTrait for Signer { +impl<'a> SignerTrait for Signer<'a> { fn sign_program( &self, pst: &PartiallySignedTransaction, @@ -105,10 +108,10 @@ enum Estimate { Failure(u64), } -impl Signer { +impl<'a> Signer<'a> { pub fn new( mnemonic: &str, - provider: Box, + provider: &'a impl ProviderTrait, network: SimplicityNetwork, ) -> Result { let secp = Secp256k1::new(); @@ -121,7 +124,7 @@ impl Signer { Ok(Self { xprv, - provider: provider, + provider: Box::new(provider), network: network, secp: secp, }) @@ -386,7 +389,7 @@ mod tests { let provider = EsploraProvider::new("https://blockstream.info/liquidtestnet/api".to_string()); let signer = Signer::new( "exist carry drive collect lend cereal occur much tiger just involve mean", - Box::new(provider.clone()), + &provider, SimplicityNetwork::LiquidTestnet, ) .unwrap(); diff --git a/simplex/crates/sdk/src/transaction/error.rs b/crates/sdk/src/transaction/error.rs similarity index 100% rename from simplex/crates/sdk/src/transaction/error.rs rename to crates/sdk/src/transaction/error.rs diff --git a/simplex/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs similarity index 69% rename from simplex/crates/sdk/src/transaction/final_transaction.rs rename to crates/sdk/src/transaction/final_transaction.rs index 9215a8d..968b94e 100644 --- a/simplex/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -1,15 +1,20 @@ +use simplicityhl::elements::AssetId; use simplicityhl::elements::pset::PartiallySignedTransaction; -use crate::constants::{SimplicityNetwork, WITNESS_SCALE_FACTOR}; +use crate::provider::SimplicityNetwork; +use crate::utils::asset_entropy; use super::error::TransactionError; -use super::partial_input::{PartialInput, ProgramInput, RequiredSignature}; +use super::partial_input::{IssuanceInput, PartialInput, ProgramInput, RequiredSignature}; use super::partial_output::PartialOutput; +pub const WITNESS_SCALE_FACTOR: usize = 4; + #[derive(Clone)] pub struct FinalInput { pub partial_input: PartialInput, pub program_input: Option, + pub issuance_input: Option, pub required_sig: RequiredSignature, } @@ -29,7 +34,6 @@ impl FinalTransaction { } } - // TODO: require required_sig != Witness(String) pub fn add_input( &mut self, partial_input: PartialInput, @@ -47,12 +51,40 @@ impl FinalTransaction { self.inputs.push(FinalInput { partial_input: partial_input, program_input: None, + issuance_input: None, required_sig: required_sig, }); Ok(()) } + pub fn add_issuance_input( + &mut self, + partial_input: PartialInput, + issuance_input: IssuanceInput, + required_sig: RequiredSignature, + ) -> Result { + match required_sig { + RequiredSignature::Witness(_) => { + return Err(TransactionError::SignatureRequest( + "Requested signature is not NativeEcdsa or None".to_string(), + )); + } + _ => {} + } + + let asset_id = AssetId::from_entropy(asset_entropy(&partial_input.outpoint(), issuance_input.asset_entropy)); + + self.inputs.push(FinalInput { + partial_input: partial_input, + program_input: None, + issuance_input: Some(issuance_input), + required_sig: required_sig, + }); + + Ok(asset_id) + } + pub fn add_program_input( &mut self, partial_input: PartialInput, @@ -71,6 +103,7 @@ impl FinalTransaction { self.inputs.push(FinalInput { partial_input: partial_input, program_input: Some(program_input), + issuance_input: None, required_sig: required_sig, }); @@ -147,7 +180,19 @@ impl FinalTransaction { let mut pst = PartiallySignedTransaction::new_v2(); self.inputs.iter().for_each(|el| { - pst.add_input(el.partial_input.to_input()); + let mut input = el.partial_input.input(); + + // populate the input manually since `input.merge` is private + if el.issuance_input.is_some() { + let issue = el.issuance_input.clone().unwrap().input(); + + input.issuance_value_amount = issue.issuance_value_amount; + input.issuance_asset_entropy = issue.issuance_asset_entropy; + input.issuance_inflation_keys = issue.issuance_inflation_keys; + input.blinded_issuance = issue.blinded_issuance; + } + + pst.add_input(input); }); self.outputs.iter().for_each(|el| { diff --git a/simplex/crates/sdk/src/transaction/mod.rs b/crates/sdk/src/transaction/mod.rs similarity index 100% rename from simplex/crates/sdk/src/transaction/mod.rs rename to crates/sdk/src/transaction/mod.rs diff --git a/simplex/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs similarity index 68% rename from simplex/crates/sdk/src/transaction/partial_input.rs rename to crates/sdk/src/transaction/partial_input.rs index ef45e8d..f4e54a7 100644 --- a/simplex/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -28,6 +28,12 @@ pub struct ProgramInput { pub witness: Box, } +#[derive(Clone)] +pub struct IssuanceInput { + pub issuance_amount: u64, + pub asset_entropy: [u8; 32], +} + impl PartialInput { pub fn new(outpoint: OutPoint, txout: TxOut) -> Self { let amount = match txout.value { @@ -49,7 +55,14 @@ impl PartialInput { } } - pub fn to_input(&self) -> Input { + pub fn outpoint(&self) -> OutPoint { + OutPoint { + txid: self.witness_txid.clone(), + vout: self.witness_output_index, + } + } + + pub fn input(&self) -> Input { let mut input = Input::default(); input.previous_txid = self.witness_txid.clone(); @@ -71,3 +84,23 @@ impl ProgramInput { } } } + +impl IssuanceInput { + pub fn new(issuance_amount: u64, asset_entropy: [u8; 32]) -> Self { + Self { + issuance_amount: issuance_amount, + asset_entropy: asset_entropy, + } + } + + pub fn input(&self) -> Input { + let mut input = Input::default(); + + input.issuance_value_amount = Some(self.issuance_amount); + input.issuance_asset_entropy = Some(self.asset_entropy); + input.issuance_inflation_keys = None; + input.blinded_issuance = Some(0x00); + + input + } +} diff --git a/simplex/crates/sdk/src/transaction/partial_output.rs b/crates/sdk/src/transaction/partial_output.rs similarity index 100% rename from simplex/crates/sdk/src/transaction/partial_output.rs rename to crates/sdk/src/transaction/partial_output.rs diff --git a/crates/sdk/src/utils.rs b/crates/sdk/src/utils.rs new file mode 100644 index 0000000..0175f08 --- /dev/null +++ b/crates/sdk/src/utils.rs @@ -0,0 +1,23 @@ +use simplicityhl::simplicity::bitcoin; +use simplicityhl::simplicity::bitcoin::secp256k1; + +use simplicityhl::elements::{AssetId, ContractHash, OutPoint}; +use simplicityhl::simplicity::hashes::{Hash, sha256}; + +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, + ]) + .expect("key should be valid") +} + +pub fn asset_entropy(outpoint: &OutPoint, entropy: [u8; 32]) -> sha256::Midstate { + let contract_hash = ContractHash::from_byte_array(entropy); + AssetId::generate_asset_entropy(*outpoint, contract_hash) +} + +pub fn sat2btc(sat: u64) -> String { + let amount = bitcoin::Amount::from_sat(sat); + amount.to_string_in(bitcoin::amount::Denomination::Bitcoin) +} diff --git a/simplex/crates/simplex/Cargo.toml b/crates/simplex/Cargo.toml similarity index 96% rename from simplex/crates/simplex/Cargo.toml rename to crates/simplex/Cargo.toml index f7ac2c7..2ef6f72 100644 --- a/simplex/crates/simplex/Cargo.toml +++ b/crates/simplex/Cargo.toml @@ -23,7 +23,6 @@ sdk = ["dep:simplex-sdk"] [dependencies] simplex-macros = { workspace = true, features = [], optional = true } simplex-test = { workspace = true } -simplex-provider = { workspace = true } simplex-sdk = { workspace = true, optional = true } bincode = { workspace = true, optional = true } diff --git a/simplex/crates/simplex/src/lib.rs b/crates/simplex/src/lib.rs similarity index 100% rename from simplex/crates/simplex/src/lib.rs rename to crates/simplex/src/lib.rs diff --git a/crates/simplex/tests/compiletest.rs b/crates/simplex/tests/compiletest.rs new file mode 100644 index 0000000..0a2eabb --- /dev/null +++ b/crates/simplex/tests/compiletest.rs @@ -0,0 +1,11 @@ +const SLOW_TEST_ENV: &str = "RUN_SLOW_TESTS"; +#[test] +fn ui() { + if let Err(_) = std::env::var(SLOW_TEST_ENV) { + tracing::trace!("Set '{SLOW_TEST_ENV}' to true in order to run a test"); + return; + } + + let t = trybuild::TestCases::new(); + t.pass("tests/ui/*.rs"); +} diff --git a/crates/simplex/tests/simplex_test.rs b/crates/simplex/tests/simplex_test.rs new file mode 100644 index 0000000..72a51e3 --- /dev/null +++ b/crates/simplex/tests/simplex_test.rs @@ -0,0 +1,160 @@ +// use simplex_provider::elements_rpc::{AddressType, ElementsRpcClient}; +// use simplex_test::{DEFAULT_SAT_AMOUNT_FAUCET, ElementsDConf, TestContext}; +// use simplicityhl::elements::Address; +// use simplicityhl::elements::bitcoin::secp256k1; +// use simplicityhl::elements::secp256k1_zkp::Keypair; + +// use simplex::simplex_sdk::constants::SimplicityNetwork; +// use simplex::simplex_sdk::presets::{P2PK, P2PKArguments}; +// use simplex::simplex_sdk::utils::tr_unspendable_key; + +// #[ignore] +// #[simplex::simplex_macros::test(default_rpc)] +// fn test_execution(x: TestContext) { +// assert!(true) +// } + +// #[ignore] +// #[simplex::simplex_macros::test] +// fn test_execution3(x: TestContext) { +// assert!(true) +// } + +// #[ignore] +// #[test] +// fn test_execution2() { +// 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, TestClientProvider}; + +// fn test_invocation_tx_tracking( +// rpc: TestClientProvider, +// user1_addr: Address, +// user2_addr: Address, +// ) -> anyhow::Result<()> { +// // user input code +// { +// let network = SimplicityNetwork::default_regtest(); +// let keypair = Keypair::from_seckey_slice(&secp256k1::SECP256K1, &[1; 32])?; + +// let arguments = P2PKArguments { +// public_key: keypair.x_only_public_key().0.serialize(), +// }; + +// let p2pk_program = P2PK::new(tr_unspendable_key(), arguments); +// let p2pk = p2pk_program.get_program().get_tr_address(network).unwrap(); + +// dbg!(p2pk.to_string()); + +// // simplex runtime +// // - test provider +// // - fields from config +// // - +// // p2tr + +// dbg!(ElementsRpcClient::validateaddress(rpc.as_ref(), &p2pk.to_string())?); + +// // broadcast, fetch fee transaction + +// let result = ElementsRpcClient::sendtoaddress( +// rpc.as_ref(), +// &p2pk, +// DEFAULT_SAT_AMOUNT_FAUCET, +// Some(network.policy_asset()), +// )?; + +// ElementsRpcClient::generate_blocks(rpc.as_ref(), 5)?; + +// dbg!(ElementsRpcClient::listunspent( +// rpc.as_ref(), +// None, +// None, +// Some(vec![p2pk.to_string()]), +// None, +// None, +// )?,); + +// dbg!(ElementsRpcClient::scantxoutset( +// rpc.as_ref(), +// "start", +// Some(vec![format!("addr({})", p2pk)]), +// )?,); + +// Ok(()) +// } +// } + +// let network = SimplicityNetwork::default_regtest(); +// let rpc = TestClientProvider::init( +// ConfigOption::DefaultRegtest, +// ElementsDConf::obtain_default_elementsd_path(), +// ) +// .unwrap(); +// { +// ElementsRpcClient::generate_blocks(rpc.as_ref(), 1).unwrap(); +// ElementsRpcClient::rescanblockchain(rpc.as_ref(), None, None).unwrap(); +// ElementsRpcClient::sweep_initialfreecoins(rpc.as_ref()).unwrap(); +// ElementsRpcClient::generate_blocks(rpc.as_ref(), 100).unwrap(); +// } +// // TODO: remove berkeley +// // addresstype - bech32 + +// // + +// let user1_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); +// let user2_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); +// ElementsRpcClient::sendtoaddress( +// rpc.as_ref(), +// &user1_addr, +// DEFAULT_SAT_AMOUNT_FAUCET, +// Some(network.policy_asset()), +// ) +// .unwrap(); + +// ElementsRpcClient::sendtoaddress( +// rpc.as_ref(), +// &user2_addr, +// DEFAULT_SAT_AMOUNT_FAUCET, +// Some(network.policy_asset()), +// ) +// .unwrap(); + +// ElementsRpcClient::generate_blocks(rpc.as_ref(), 3).unwrap(); +// dbg!(ElementsRpcClient::listunspent( +// rpc.as_ref(), +// None, +// None, +// Some(vec![user1_addr.to_string(), user2_addr.to_string()]), +// None, +// None, +// )?,); +// test_invocation_tx_tracking(rpc, user1_addr, user2_addr) +// } diff --git a/simplex/crates/simplex/tests/ui/array_tr_storage.rs b/crates/simplex/tests/ui/array_tr_storage.rs similarity index 91% rename from simplex/crates/simplex/tests/ui/array_tr_storage.rs rename to crates/simplex/tests/ui/array_tr_storage.rs index 006ed37..6a9da79 100644 --- a/simplex/crates/simplex/tests/ui/array_tr_storage.rs +++ b/crates/simplex/tests/ui/array_tr_storage.rs @@ -1,7 +1,7 @@ use simplex::simplex_macros::*; use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; -include_simf!("../../../../crates/simplex/tests/ui/array_tr_storage.simf"); +include_simf!("../../../../crates/simplex/tests/ui_simfs/array_tr_storage.simf"); fn main() -> Result<(), String> { let original_witness = derived_array_tr_storage::ArrayTrStorageWitness { diff --git a/simplex/crates/simplex/tests/ui/bytes32_tr_storage.rs b/crates/simplex/tests/ui/bytes32_tr_storage.rs similarity index 90% rename from simplex/crates/simplex/tests/ui/bytes32_tr_storage.rs rename to crates/simplex/tests/ui/bytes32_tr_storage.rs index 241431d..4ffc886 100644 --- a/simplex/crates/simplex/tests/ui/bytes32_tr_storage.rs +++ b/crates/simplex/tests/ui/bytes32_tr_storage.rs @@ -1,7 +1,7 @@ use simplex::simplex_macros::*; use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; -include_simf!("../../../../crates/simplex/tests/ui/bytes32_tr_storage.simf"); +include_simf!("../../../../crates/simplex/tests/ui_simfs/bytes32_tr_storage.simf"); fn main() -> Result<(), String> { let original_witness = derived_bytes32_tr_storage::Bytes32TrStorageWitness { diff --git a/simplex/crates/simplex/tests/ui/dual_currency_deposit.rs b/crates/simplex/tests/ui/dual_currency_deposit.rs similarity index 95% rename from simplex/crates/simplex/tests/ui/dual_currency_deposit.rs rename to crates/simplex/tests/ui/dual_currency_deposit.rs index 9919676..8c48fca 100644 --- a/simplex/crates/simplex/tests/ui/dual_currency_deposit.rs +++ b/crates/simplex/tests/ui/dual_currency_deposit.rs @@ -1,7 +1,7 @@ use simplex::simplex_macros::*; use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; -include_simf!("../../../../crates/simplex/tests/ui/dual_currency_deposit.simf"); +include_simf!("../../../../crates/simplex/tests/ui_simfs/dual_currency_deposit.simf"); fn main() -> Result<(), String> { let original_witness = derived_dual_currency_deposit::DualCurrencyDepositWitness { diff --git a/simplex/crates/simplex/tests/ui/option_offer.rs b/crates/simplex/tests/ui/option_offer.rs similarity index 92% rename from simplex/crates/simplex/tests/ui/option_offer.rs rename to crates/simplex/tests/ui/option_offer.rs index 1cfc681..8393522 100644 --- a/simplex/crates/simplex/tests/ui/option_offer.rs +++ b/crates/simplex/tests/ui/option_offer.rs @@ -1,7 +1,7 @@ use simplex::simplex_macros::*; use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; -include_simf!("../../../../crates/simplex/tests/ui/option_offer.simf"); +include_simf!("../../../../crates/simplex/tests/ui_simfs/option_offer.simf"); fn main() -> Result<(), String> { let original_witness = derived_option_offer::OptionOfferWitness { path: simplex::either::Left((0, false)) }; diff --git a/simplex/crates/simplex/tests/ui/options.rs b/crates/simplex/tests/ui/options.rs similarity index 94% rename from simplex/crates/simplex/tests/ui/options.rs rename to crates/simplex/tests/ui/options.rs index 80d8aef..58b5a0e 100644 --- a/simplex/crates/simplex/tests/ui/options.rs +++ b/crates/simplex/tests/ui/options.rs @@ -1,7 +1,7 @@ use simplex::simplex_macros::*; use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; -include_simf!("../../../../crates/simplex/tests/ui/options.simf"); +include_simf!("../../../../crates/simplex/tests/ui_simfs/options.simf"); fn main() -> Result<(), String> { let original_witness = derived_options::OptionsWitness { diff --git a/simplex/crates/simplex/tests/ui/simple_storage.rs b/crates/simplex/tests/ui/simple_storage.rs similarity index 84% rename from simplex/crates/simplex/tests/ui/simple_storage.rs rename to crates/simplex/tests/ui/simple_storage.rs index ea1a329..d2a6ba5 100644 --- a/simplex/crates/simplex/tests/ui/simple_storage.rs +++ b/crates/simplex/tests/ui/simple_storage.rs @@ -1,7 +1,7 @@ use simplex::simplex_macros::*; -use simplex::simplex_sdk::witness::{WitnessTrait, ArgumentsTrait}; +use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; -include_simf!("../../../../crates/simplex/tests/ui/simple_storage.simf"); +include_simf!("../../../../crates/simplex/tests/ui_simfs/simple_storage.simf"); fn main() -> Result<(), String> { let original_witness = derived_simple_storage::SimpleStorageWitness { diff --git a/crates/simplex/tests/ui_simfs/array_tr_storage.simf b/crates/simplex/tests/ui_simfs/array_tr_storage.simf new file mode 100644 index 0000000..4918cf3 --- /dev/null +++ b/crates/simplex/tests/ui_simfs/array_tr_storage.simf @@ -0,0 +1,81 @@ +/* + * Extends `bytes32_tr_storage` using `array_fold` for larger buffers. + * Optimized for small, fixed-size states where linear hashing is more efficient + * than Merkle Trees. By avoiding proof overhead like sibling hashes, we reduce + * witness size and simplify contract logic for small N. + * This approach is particularly advantageous when updating all slots within every transaction. + */ + +fn hash_array_tr_storage(elem: u256, ctx: Ctx8) -> Ctx8 { + jet::sha_256_ctx_8_add_32(ctx, elem) +} + +fn hash_array_tr_storage_with_update(elem: u256, triplet: (Ctx8, u16, u16)) -> (Ctx8, u16, u16) { + let (ctx, i, changed_index): (Ctx8, u16, u16) = triplet; + + match jet::eq_16(i, changed_index) { + true => { + let (_, val): (bool, u16) = jet::increment_16(i); + + // There may be arbitrary logic here + let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(elem); + let new_state4: u64 = 20; + + let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); + ( + jet::sha_256_ctx_8_add_32(ctx, new_state), + val, + changed_index, + ) + }, + false => { + let (_, val): (bool, u16) = jet::increment_16(i); + ( + jet::sha_256_ctx_8_add_32(ctx, elem), + val, + changed_index, + ) + } + } +} + +fn script_hash_for_input_script(state: [u256; 3], changed_index: Option) -> u256 { + let tap_leaf: u256 = jet::tapleaf_hash(); + let ctx: Ctx8 = jet::tapdata_init(); + + let (ctx, _, _): (Ctx8, u16, u16) = match changed_index { + Some(ind: u16) => { + array_fold::(state, (ctx, 0, ind)) + }, + None => { + (array_fold::(state, ctx), 0, 0) + } + }; + + let computed: u256 = jet::sha_256_ctx_8_finalize(ctx); + let tap_node: u256 = jet::build_tapbranch(tap_leaf, computed); + + let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; + let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); + + let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); + let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 + let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); + jet::sha_256_ctx_8_finalize(hash_ctx3) +} + +fn main() { + let state: [u256; 3] = witness::STATE; + + // Assert that the input is correct, i.e. "load". + assert!(jet::eq_256( + script_hash_for_input_script(state, None), + unwrap(jet::input_script_hash(jet::current_index())) + )); + + // Assert that the output is correct, i.e. "store". + assert!(jet::eq_256( + script_hash_for_input_script(state, Some(witness::CHANGED_INDEX)), + unwrap(jet::output_script_hash(jet::current_index())) + )); +} \ No newline at end of file diff --git a/crates/simplex/tests/ui_simfs/bytes32_tr_storage.simf b/crates/simplex/tests/ui_simfs/bytes32_tr_storage.simf new file mode 100644 index 0000000..0d11b5f --- /dev/null +++ b/crates/simplex/tests/ui_simfs/bytes32_tr_storage.simf @@ -0,0 +1,66 @@ +/* + * Computes the "State Commitment" — the expected Script PubKey (address) + * for a specific state value. + * + * HOW IT WORKS: + * In Simplicity/Liquid, state is not stored in a dedicated database. Instead, + * it is verified via a "Commitment Scheme" inside the Taproot tree of the UTXO. + * + * This function reconstructs the Taproot structure to validate that the provided + * witness data (state_data) was indeed cryptographically embedded into the + * transaction output that is currently being spent. + * + * LOGIC FLOW: + * 1. Takes state_data (passed via witness at runtime). + * 2. Hashes it as a non-executable TapData leaf. + * 3. Combines it with the current program's CMR (tapleaf_hash). + * 4. Derives the tweaked_key (Internal Key + Merkle Root). + * 5. Returns the final SHA256 script hash (SegWit v1). + * + * USAGE: + * - In main, we verify: CalculatedHash(witness::STATE) == input_script_hash. + * - This assertion proves that the UTXO is "locked" not just by the code, + * but specifically by THIS instance of the state data. + */ + +fn script_hash_for_input_script(state_data: u256) -> u256 { + // This is the bulk of our "compute state commitment" logic from above. + let tap_leaf: u256 = jet::tapleaf_hash(); + let state_ctx1: Ctx8 = jet::tapdata_init(); + let state_ctx2: Ctx8 = jet::sha_256_ctx_8_add_32(state_ctx1, state_data); + let state_leaf: u256 = jet::sha_256_ctx_8_finalize(state_ctx2); + let tap_node: u256 = jet::build_tapbranch(tap_leaf, state_leaf); + + // Compute a taptweak using this. + let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; + let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); + + // Turn the taptweak into a script hash + let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); + let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 + let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); + jet::sha_256_ctx_8_finalize(hash_ctx3) +} + +fn main() { + let state_data: u256 = witness::STATE; + let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(state_data); + + // Assert that the input is correct, i.e. "load". + assert!(jet::eq_256( + script_hash_for_input_script(state_data), + unwrap(jet::input_script_hash(jet::current_index())) + )); + + // Do a state update (and fail on 64-bit overflow even though we've got 192 other + // bits we could be using..) + let (carry, new_state4): (bool, u64) = jet::increment_64(state4); + assert!(jet::eq_1(::into(carry), 0)); + + let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); + // Assert that the output is correct, i.e. "store". + assert!(jet::eq_256( + script_hash_for_input_script(new_state), + unwrap(jet::output_script_hash(jet::current_index())) + )); +} \ No newline at end of file diff --git a/crates/simplex/tests/ui_simfs/dual_currency_deposit.simf b/crates/simplex/tests/ui_simfs/dual_currency_deposit.simf new file mode 100644 index 0000000..e1a460a --- /dev/null +++ b/crates/simplex/tests/ui_simfs/dual_currency_deposit.simf @@ -0,0 +1,592 @@ +/* + * DCD: Dual Currency Deposit – price-attested settlement and funding windows + * + * Flows implemented: + * - Maker funding: deposit settlement asset and collateral, issue grantor tokens + * - Taker funding: deposit collateral in window and receive filler tokens + * - Settlement: at SETTLEMENT_HEIGHT, oracle Schnorr signature over (height, price) + * selects LBTC vs ALT branch based on price <= STRIKE_PRICE + * - Early/post-expiry termination: taker returns filler; maker burns grantor tokens + * - Merge: consolidate 2/3/4 token UTXOs + * + * All amounts and asset/script invariants are enforced on-chain; time guards use + * fallback locktime and height checks. + * + * Batching discussion: https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 + */ + +// Verify Schnorr signature against SHA256 of (u32 || u64) +fn checksig_priceblock(pk: Pubkey, current_block_height: u32, price_at_current_block_height: u64, sig: Signature) { + let hasher: Ctx8 = jet::sha_256_ctx_8_init(); + let hasher: Ctx8 = jet::sha_256_ctx_8_add_4(hasher, current_block_height); + let hasher: Ctx8 = jet::sha_256_ctx_8_add_8(hasher, price_at_current_block_height); + let msg: u256 = jet::sha_256_ctx_8_finalize(hasher); + jet::bip_0340_verify((pk, msg), sig); +} + +// Signed <= using XOR with 0x8000.. bias: a<=b (signed) iff (a^bias) <= (b^bias) (unsigned) +fn signed_le_u64(a_bits: u64, b_bits: u64) -> bool { + let bias: u64 = 0x8000000000000000; + jet::le_64(jet::xor_64(a_bits, bias), jet::xor_64(b_bits, bias)) +} + +fn signed_lt_u64(a: u64, b: u64) -> bool { + let bias: u64 = 0x8000000000000000; + jet::lt_64(jet::xor_64(a, bias), jet::xor_64(b, bias)) +} + +/// 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)); +} + +/// Assert: base_amount * basis_point_percentage == provided_amount * MAX_BASIS_POINTS +fn constraint_percentage(base_amount: u64, basis_point_percentage: u64, provided_amount: u64) { + let MAX_BASIS_POINTS: u64 = 10000; + + let arg1: u256 = <(u128, u128)>::into((0, jet::multiply_64(base_amount, basis_point_percentage))); + let arg2: u256 = <(u128, u128)>::into((0, jet::multiply_64(provided_amount, MAX_BASIS_POINTS))); + + assert!(jet::eq_256(arg1, arg2)); +} + +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 ensure_one_bit_or(bit1: bool, bit2: bool) { + assert!( + jet::eq_1( + ::into(jet::or_1(::into(bit1), ::into(bit2))), + 1 + ) + ); +} + +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) = 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 merge_2_tokens() { + // 2 tokens to merge + 1 input as fee + assert!(jet::eq_32(jet::num_inputs(), 3)); + // 3 outputs: 1 merged token + 1 change + 1 fee + assert!(jet::eq_32(jet::num_outputs(), 3)); + assert!(jet::le_32(jet::current_index(), 1)); + + ensure_input_and_output_script_hash_eq(0); + let script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(script_hash, get_input_script_hash(1))); +} + +fn merge_3_tokens() { + // 3 tokens to merge + 1 input as fee + assert!(jet::eq_32(jet::num_inputs(), 4)); + // 3 outputs: 1 merged token + 1 change + 1 fee + assert!(jet::eq_32(jet::num_outputs(), 3)); + assert!(jet::le_32(jet::current_index(), 2)); + + ensure_input_and_output_script_hash_eq(0); + let script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(script_hash, get_input_script_hash(1))); + assert!(jet::eq_256(script_hash, get_input_script_hash(2))); +} + +fn merge_4_tokens() { + // 4 tokens to merge + 1 input as fee + assert!(jet::eq_32(jet::num_inputs(), 5)); + // 3 outputs: 1 merged token + 1 change + 1 fee + assert!(jet::eq_32(jet::num_outputs(), 3)); + assert!(jet::le_32(jet::current_index(), 3)); + + ensure_input_and_output_script_hash_eq(0); + let script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(script_hash, get_input_script_hash(1))); + assert!(jet::eq_256(script_hash, get_input_script_hash(2))); + assert!(jet::eq_256(script_hash, get_input_script_hash(3))); +} + +/* +* Maker funding path +* Params: +* 1. FILLER_PER_SETTLEMENT_COLLATERAL +* 2. FILLER_PER_SETTLEMENT_ASSET +* 3. FILLER_PER_PRINCIPAL_COLLATERAL +* 4. GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET +* 5. GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL +* 6. GRANTOR_PER_SETTLEMENT_COLLATERAL +* 7. GRANTOR_PER_SETTLEMENT_ASSET +*/ +fn maker_funding_path(principal_collateral_amount: u64, principal_asset_amount: u64, interest_collateral_amount: u64, interest_asset_amount: u64) { + assert!(jet::eq_32(jet::num_inputs(), 5)); + assert!(jet::eq_32(jet::num_outputs(), 11)); + + let current_time: u32 = ::into(jet::lock_time()); + assert!(jet::lt_32(current_time, param::TAKER_FUNDING_START_TIME)); + + ensure_input_and_output_script_hash_eq(0); + ensure_input_and_output_script_hash_eq(1); + ensure_input_and_output_script_hash_eq(2); + + assert!(jet::le_32(jet::current_index(), 2)); + + let script_hash: u256 = get_output_script_hash(0); + ensure_output_script_hash_eq(1, script_hash); + ensure_output_script_hash_eq(2, script_hash); + ensure_output_script_hash_eq(3, script_hash); + ensure_output_script_hash_eq(4, script_hash); + ensure_output_script_hash_eq(5, script_hash); + + let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(3); + let (settlement_asset_bits, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(4); + let filler_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); + let grantor_collateral_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); + let grantor_settlement_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(2)))); + assert!(jet::eq_64(filler_token_amount, grantor_collateral_token_amount)); + assert!(jet::eq_64(filler_token_amount, grantor_settlement_token_amount)); + + divmod_eq(principal_asset_amount, param::STRIKE_PRICE, principal_collateral_amount); + + assert!(jet::eq_64(collateral_amount, interest_collateral_amount)); + constraint_percentage(principal_collateral_amount, param::INCENTIVE_BASIS_POINTS, collateral_amount); + + let MAX_BASIS_POINTS: u64 = 10000; + let (carry, asset_incentive_percentage): (bool, u64) = jet::add_64(param::INCENTIVE_BASIS_POINTS, MAX_BASIS_POINTS); + ensure_zero_bit(carry); + + constraint_percentage(principal_asset_amount, asset_incentive_percentage, settlement_amount); + + let (carry, calculated_total_asset_amount): (bool, u64) = jet::add_64(principal_asset_amount, interest_asset_amount); + ensure_zero_bit(carry); + assert!(jet::eq_64(calculated_total_asset_amount, settlement_amount)); + + let (carry, calculated_total_collateral_amount): (bool, u64) = jet::add_64(principal_collateral_amount, interest_collateral_amount); + ensure_zero_bit(carry); + + // Filler token constraints + divmod_eq(calculated_total_collateral_amount, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_token_amount); + divmod_eq(calculated_total_asset_amount, param::FILLER_PER_SETTLEMENT_ASSET, filler_token_amount); + divmod_eq(principal_collateral_amount, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount); + + // Grantor token constraints + divmod_eq(calculated_total_asset_amount, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_token_amount); + divmod_eq(interest_collateral_amount, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_token_amount); + + divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_collateral_token_amount); + // divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_settlement_token_amount); // duplicated because of lines 203-204 + + divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_collateral_token_amount); + // divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_settlement_token_amount); // duplicated because of lines 203-204 + + assert!(jet::eq_256(param::COLLATERAL_ASSET_ID, collateral_asset_bits)); + assert!(jet::eq_256(param::SETTLEMENT_ASSET_ID, settlement_asset_bits)); + + ensure_output_asset_with_amount_eq(5, param::FILLER_TOKEN_ASSET, filler_token_amount); + ensure_output_asset_with_amount_eq(6, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_token_amount); + ensure_output_asset_with_amount_eq(7, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_token_amount); + + ensure_input_asset_eq(3, param::SETTLEMENT_ASSET_ID); + ensure_input_asset_eq(4, param::COLLATERAL_ASSET_ID); + + ensure_output_asset_eq(8, param::COLLATERAL_ASSET_ID); + ensure_output_asset_eq(9, param::SETTLEMENT_ASSET_ID); + ensure_output_asset_eq(10, param::COLLATERAL_ASSET_ID); +} + +fn taker_funding_path(collateral_amount_to_deposit: u64, filler_token_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + assert!(jet::le_32(param::TAKER_FUNDING_START_TIME, current_time)); + assert!(jet::lt_32(current_time, param::TAKER_FUNDING_END_TIME)); + assert!(jet::lt_32(current_time, param::CONTRACT_EXPIRY_TIME)); + + let filler_token_input_index: u32 = 0; + let collateral_input_index: u32 = 1; + + let (collateral_to_covenant_output_index, filler_to_user_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(filler_token_input_index); + + // Check and ensure filler token change + ensure_correct_change_at_index(0, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(collateral_amount_to_deposit, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_get); + + // Ensure collateral asset and script hash are correct + ensure_output_asset_with_amount_eq(collateral_to_covenant_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_deposit); + ensure_output_script_hash_eq(collateral_to_covenant_output_index, expected_current_script_hash); + + ensure_output_asset_with_amount_eq(filler_to_user_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get); +} + +fn taker_early_termination_path(filler_token_amount_to_return: u64, collateral_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); + + let collateral_input_index: u32 = 0; + let filler_token_input_index: u32 = 1; + + let (return_filler_output_index, return_collateral_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::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_return); + + // Ensure filler token transferred to covenant + ensure_output_asset_with_amount_eq(return_filler_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_return); + ensure_output_script_hash_eq(return_filler_output_index, expected_current_script_hash); + + // Ensure collateral transferred to user + ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); +} + +fn maker_collateral_termination_path(grantor_collateral_amount_to_burn: u64, collateral_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); + + let collateral_input_index: u32 = 0; + let grantor_collateral_token_input_index: u32 = 1; + + let (burn_grantor_collateral_output_index, return_collateral_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::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_amount_to_burn); + + // Burn grantor collateral token + ensure_output_is_op_return(burn_grantor_collateral_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_amount_to_burn); + + // Ensure collateral transferred to user + ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); +} + +fn maker_settlement_termination_path(grantor_settlement_amount_to_burn: u64, settlement_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); + + let settlement_asset_input_index: u32 = 0; + let grantor_settlement_token_input_index: u32 = 1; + + let (burn_grantor_settlement_output_index, return_settlement_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset amount is correct + divmod_eq(settlement_amount_to_get, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_amount_to_burn); + + // Burn grantor settlement token + ensure_output_is_op_return(burn_grantor_settlement_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_amount_to_burn); + + // Ensure settlement asset transferred to user + ensure_output_asset_with_amount_eq(return_settlement_output_index, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get); +} + +fn ensure_correct_return_at(user_output_index: u32, asset_id: u256, amount_to_get: u64, fee_basis_points: u64) { + match jet::eq_64(fee_basis_points, 0) { + true => ensure_output_asset_with_amount_eq(user_output_index, asset_id, amount_to_get), + false => { + let fee_output_index: u32 = increment_by(user_output_index, 1); + + let (user_asset_bits, user_amount): (u256, u64) = get_output_explicit_asset_amount(user_output_index); + assert!(jet::eq_256(user_asset_bits, asset_id)); + + let (fee_asset_bits, fee_amount): (u256, u64) = get_output_explicit_asset_amount(fee_output_index); + assert!(jet::eq_256(fee_asset_bits, asset_id)); + + let (carry, calculated_total_amount): (bool, u64) = jet::add_64(user_amount, fee_amount); + ensure_zero_bit(carry); + + constraint_percentage(calculated_total_amount, fee_basis_points, fee_amount); + + ensure_output_script_hash_eq(fee_output_index, param::FEE_SCRIPT_HASH); + }, + }; +} + +fn maker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, grantor_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { + jet::check_lock_height(param::SETTLEMENT_HEIGHT); + checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); + + match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { + true => { + // Maker gets ALT + let settlement_asset_input_index: u32 = 0; + + let (burn_grantor_settlement_output_index, burn_grantor_collateral_output_index, settlement_output_index): (u32, u32, u32) = match is_change_needed { + true => (1, 2, 3), + false => (0, 1, 2), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset amount is correct + divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_amount_to_burn); + + // Burn grantor settlement and collateral tokens + ensure_output_is_op_return(burn_grantor_settlement_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); + ensure_output_is_op_return(burn_grantor_collateral_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); + + // Ensure settlement asset transferred to user + ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + false => { + // Maker gets the LBTC + let collateral_input_index: u32 = 0; + + let (burn_grantor_collateral_output_index, burn_grantor_settlement_output_index, collateral_output_index): (u32, u32, u32) = match is_change_needed { + true => (1, 2, 3), + false => (0, 1, 2), + }; + + 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, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_amount_to_burn); + + // Burn grantor collateral and settlement tokens + ensure_output_is_op_return(burn_grantor_collateral_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); + ensure_output_is_op_return(burn_grantor_settlement_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); + + // Ensure collateral transferred to user + ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + } +} + +fn taker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, filler_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { + jet::check_lock_height(param::SETTLEMENT_HEIGHT); + checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); + + match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { + true => { + // Taker receives LBTC principal+interest + let collateral_input_index: u32 = 0; + + let (burn_filler_output_index, collateral_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, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_amount_to_burn); + + // Burn filler token + ensure_output_is_op_return(burn_filler_output_index); + ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); + + // Ensure collateral transferred to user + ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + false => { + // Taker receives ALT + let settlement_asset_input_index: u32 = 0; + + let (burn_filler_output_index, settlement_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset amount is correct + divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_ASSET, filler_amount_to_burn); + + // Burn filler token + ensure_output_is_op_return(burn_filler_output_index); + ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); + + // Ensure filler token transferred to user + ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + } +} + +fn main() { + let token_branch: Either<(), ()> = witness::TOKEN_BRANCH; + let merge_branch: Either, ()> = witness::MERGE_BRANCH; + + match witness::PATH { + Left(funding_or_settlement: Either, (u64, Signature, u64, u64, bool)>) => match funding_or_settlement { + // Funding branches + Left(funding_params: Either<(u64, u64, u64, u64), (u64, u64, bool)>) => match funding_params { + // Maker funding: (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) + Left(params: (u64, u64, u64, u64)) => { + let (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount): (u64, u64, u64, u64) = params; + maker_funding_path(principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) + }, + // Taker funding: (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) + Right(params: (u64, u64, bool)) => { + let (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed): (u64, u64, bool) = params; + taker_funding_path(collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) + }, + }, + // Settlement branches (oracle price attested) + Right(params: (u64, Signature, u64, u64, bool)) => { + let (price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed): (u64, Signature, u64, u64, bool) = params; + + match token_branch { + // Maker settlement: burn grantor token + Left(u: ()) => maker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), + // Taker settlement: burn filler token + Right(u: ()) => taker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), + } + }, + }, + // Termination flows (early termination or post-expiry) or Merge flows + Right(termination_or_maker_or_merge: Either, ()>) => match termination_or_maker_or_merge { + Left(termination_or_maker: Either<(bool, u64, u64), (bool, u64, u64)>) => match termination_or_maker { + // Taker early termination: (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get) + Left(params: (bool, u64, u64)) => { + let (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get): (bool, u64, u64) = params; + taker_early_termination_path(filler_token_amount_to_return, collateral_amount_to_get, is_change_needed) + }, + // Maker termination (burn grantor token): choose collateral vs settlement token via token_branch + Right(params: (bool, u64, u64)) => { + let (is_change_needed, grantor_token_amount_to_burn, amount_to_get): (bool, u64, u64) = params; + + match token_branch { + // Burn grantor collateral token -> receive collateral + Left(u: ()) => maker_collateral_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), + // Burn grantor settlement token -> receive settlement asset + Right(u: ()) => maker_settlement_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), + } + }, + }, + Right(u: ()) => { + // Merge tokens based on MERGE_BRANCH discriminator + match merge_branch { + Left(left_or_right: Either<(), ()>) => match left_or_right { + Left(u: ()) => merge_2_tokens(), + Right(u: ()) => merge_3_tokens(), + }, + Right(u: ()) => merge_4_tokens(), + } + }, + }, + } + +} diff --git a/crates/simplex/tests/ui_simfs/option_offer.simf b/crates/simplex/tests/ui_simfs/option_offer.simf new file mode 100644 index 0000000..5cb2108 --- /dev/null +++ b/crates/simplex/tests/ui_simfs/option_offer.simf @@ -0,0 +1,213 @@ +/* + * Option Offer + * + * A covenant that allows a user to deposit collateral and premium assets, + * and have a counterparty swap settlement asset for both. + * The user can withdraw accumulated settlement asset at any time (with signature). + * After expiry, the user can reclaim any remaining collateral and premium (with signature). + * + * Paths: + * 1. Exercise: Counterparty swaps settlement asset for collateral + premium (no time restriction, optional change) + * 2. Withdraw: User withdraws settlement asset (no time restriction, signature required, full amount) + * 3. Expiry: User reclaims collateral + premium (after expiry, signature required, full amount) + * + * Constraints: + * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount + * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount + */ + +/// 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_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_zero_bit(bit: bool) { + assert!(jet::eq_1(::into(bit), 0)); +} + +fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { + let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); + assert!(jet::eq_256(asset, expected_bits)); + assert!(jet::eq_64(amount, expected_amount)); +} + +fn ensure_output_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); +} + +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 check_user_signature(sig: Signature) { + let msg: u256 = jet::sig_all_hash(); + jet::bip_0340_verify((param::USER_PUBKEY, msg), sig); +} + +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)); + + match is_change_needed { + true => { + ensure_input_and_output_script_hash_eq(index); + + let (carry, asset_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, asset_change); + }, + false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), + } +} + +/* + * Exercise Path + * + * Counterparty swaps settlement asset for collateral + premium. + * No time restriction - works before and after expiry. + * + * Constraints: + * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount + * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount + * + * Layout: + * + * Both: + * Input[0]: Collateral from covenant + * Input[1]: Premium from covenant + * + * With change (partial swap): + * Output[0]: Collateral change → covenant + * Output[1]: Premium change → covenant + * Output[2]: Settlement asset → covenant + * Output[3]: Collateral → counterparty + * Output[4]: Premium → counterparty + * + * Without change (full swap): + * Output[0]: Settlement asset → covenant + * Output[1]: Collateral → counterparty + * Output[2]: Premium → counterparty + */ +fn exercise_path(collateral_amount: u64, is_change_needed: bool) { + assert!(jet::le_32(jet::current_index(), 1)); + + let expected_covenant_script_hash: u256 = get_input_script_hash(0); + + assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); + + let premium_amount_u128: u128 = jet::multiply_64(collateral_amount, param::PREMIUM_PER_COLLATERAL); + let (left_part, premium_amount): (u64, u64) = dbg!(::into(premium_amount_u128)); + assert!(jet::eq_64(left_part, 0)); + + // Check collateral changes + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_covenant_script_hash, is_change_needed); + ensure_correct_change_at_index(1, param::PREMIUM_ASSET_ID, premium_amount, expected_covenant_script_hash, is_change_needed); + + let (settlement_output_index, collateral_output_index, premium_output_index): (u32, u32, u32) = match is_change_needed { + true => (2, 3, 4), + false => (0, 1, 2), + }; + + ensure_output_script_hash_eq(settlement_output_index, expected_covenant_script_hash); + + let (output_asset, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(settlement_output_index); + assert!(jet::eq_256(output_asset, param::SETTLEMENT_ASSET_ID)); + + divmod_eq(settlement_amount, param::COLLATERAL_PER_CONTRACT, collateral_amount); + + ensure_output_asset_with_amount_eq(collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(premium_output_index, param::PREMIUM_ASSET_ID, premium_amount); +} + +/* + * Withdraw Path + * + * User withdraws accumulated settlement asset. + * No time restriction. + * Requires signature from USER_PUBKEY. + * No change - full withdrawal only. + * + * Layout: + * Input[0]: Settlement asset from covenant + * Output[0]: Settlement asset → user (any address) + */ +fn withdraw_path(sig: Signature) { + assert!(jet::eq_32(jet::current_index(), 0)); + + let (input_asset, input_amount): (u256, u64) = get_input_explicit_asset_amount(0); + assert!(jet::eq_256(input_asset, param::SETTLEMENT_ASSET_ID)); + + check_user_signature(sig); + + ensure_output_asset_with_amount_eq(0, param::SETTLEMENT_ASSET_ID, input_amount); +} + +/* + * Expiry Path + * + * User reclaims remaining collateral and premium after expiry. + * Only allowed after EXPIRY_TIME. + * Requires signature from USER_PUBKEY. + * No change - full reclaim only. + * + * Layout: + * Input[0]: Collateral from covenant + * Input[1]: Premium from covenant + * Output[0]: Collateral → user (any address) + * Output[1]: Premium → user (any address) + */ +fn expiry_path(sig: Signature) { + jet::check_lock_time(param::EXPIRY_TIME); + + assert!(jet::le_32(jet::current_index(), 1)); + + let expected_covenant_script_hash: u256 = get_input_script_hash(0); + + assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); + + let (collateral_asset, collateral_amount): (u256, u64) = get_input_explicit_asset_amount(0); + assert!(jet::eq_256(collateral_asset, param::COLLATERAL_ASSET_ID)); + + let (premium_asset, premium_amount): (u256, u64) = get_input_explicit_asset_amount(1); + assert!(jet::eq_256(premium_asset, param::PREMIUM_ASSET_ID)); + + check_user_signature(sig); + + ensure_output_asset_with_amount_eq(0, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(1, param::PREMIUM_ASSET_ID, premium_amount); +} + +fn main() { + match witness::PATH { + Left(params: (u64, bool)) => { + let (collateral_amount, is_change_needed): (u64, bool) = params; + exercise_path(collateral_amount, is_change_needed) + }, + Right(withdraw_or_expiry: Either) => match withdraw_or_expiry { + Left(sig: Signature) => withdraw_path(sig), + Right(sig: Signature) => expiry_path(sig), + }, + } +} diff --git a/example/simf/options.simf b/crates/simplex/tests/ui_simfs/options.simf similarity index 100% rename from example/simf/options.simf rename to crates/simplex/tests/ui_simfs/options.simf diff --git a/crates/simplex/tests/ui_simfs/simple_storage.simf b/crates/simplex/tests/ui_simfs/simple_storage.simf new file mode 100644 index 0000000..7ae6c41 --- /dev/null +++ b/crates/simplex/tests/ui_simfs/simple_storage.simf @@ -0,0 +1,102 @@ +/* + * Simple Storage Program for Liquid + * + * Only the owner of the storage can modify the value. + * + * ==== IMPORTANT ==== + * + * Based on the following resources: + * https://github.com/ElementsProject/elements/blob/master/src/consensus/amount.h + * https://github.com/ElementsProject/rust-elements/blob/f6ffc7800df14b81c0f5ae1c94368a78b99612b9/src/blind.rs#L471 + * + * The maximum allowed amount is 2,100,000,000,000,000 + * (i.e., 21,000,000 × 10^8), which is approximately 51 bits. + */ + +fn checksig(pk: Pubkey, sig: Signature) { + let msg: u256 = jet::sig_all_hash(); + jet::bip_0340_verify((pk, msg), sig); +} + +fn ensure_current_index_eq(expected_index: u32){ + assert!(jet::eq_32(jet::current_index(), expected_index)); +} + +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 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_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_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 enforce_stage_checks(index: u32, new_value: u64) { + ensure_input_and_output_script_hash_eq(index); + + let (asset_bits, old_value): (u256, u64) = get_input_explicit_asset_amount(index); + assert!(jet::eq_256(asset_bits, param::SLOT_ID)); + + ensure_output_asset_with_amount_eq(index, param::SLOT_ID, new_value); + + match jet::lt_64(new_value, old_value) { + // burn + true => { + let burn_output_index: u32 = increment_by(index, 1); + + let (carry, amount_to_burn): (bool, u64) = jet::subtract_64(old_value, new_value); + ensure_zero_bit(carry); + + ensure_output_is_op_return(burn_output_index); + ensure_output_asset_with_amount_eq(burn_output_index, param::SLOT_ID, amount_to_burn); + }, + // mint + false => { + let reissuance_output_index: u32 = increment_by(index, 1); + ensure_input_and_output_script_hash_eq(reissuance_output_index); + }, + }; +} + +fn main() { + let index: u32 = 0; + enforce_stage_checks(index, witness::NEW_VALUE); + + checksig(param::USER, witness::USER_SIGNATURE) +} diff --git a/simplex/crates/macros-core/Cargo.toml b/crates/template-gen/Cargo.toml similarity index 83% rename from simplex/crates/macros-core/Cargo.toml rename to crates/template-gen/Cargo.toml index d50228a..51ab827 100644 --- a/simplex/crates/macros-core/Cargo.toml +++ b/crates/template-gen/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "simplex-macros-core" +name = "simplex-template-gen-core" description = "Macro support core for Simplex, the Rust SimplicityHl toolkit. Not intended to be used directly." version = "0.1.0" license.workspace = true @@ -14,13 +14,11 @@ serde = ["bincode", "dep:serde"] default = ["bincode", "serde"] [dependencies] -simplicityhl = { workspace = true } -simplex-test = { workspace = true } +simplex-macros-core = { workspace = true } thiserror = { workspace = true } -proc-macro-error = { version = "1.0" } -proc-macro2 = { version = "1.0.106", features = ["span-locations"] } syn = { version = "2.0.114", default-features = false, features = ["proc-macro", "full", "parsing", "derive", "clone-impls", "extra-traits", "printing"] } +proc-macro2 = { version = "1.0.106", features = ["span-locations"] } quote = { version = "1.0.44" } serde = { version = "1.0.228", optional = true } pathdiff = { version = "0.2.3" } diff --git a/crates/template-gen/src/env.rs b/crates/template-gen/src/env.rs new file mode 100644 index 0000000..0afb76a --- /dev/null +++ b/crates/template-gen/src/env.rs @@ -0,0 +1,256 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use simplex_macros_core::attr::SimfContent; +use simplex_macros_core::attr::codegen::{ + convert_contract_name_to_contract_module, convert_contract_name_to_contract_source_const, + convert_contract_name_to_struct_name, +}; +use std::collections::HashMap; +use std::io::Write; +use std::path::{Component, Path, PathBuf}; +use std::{fs, io}; + +#[derive(thiserror::Error, Debug)] +pub enum CodeGeneratorError { + #[error("IO error: {0}")] + Io(#[from] io::Error), + + #[error("Failed to extract content from path, err: '{0}'")] + FailedToExtractContent(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 }, + + #[error("Failed to find prefix for a file: {0}")] + NoBasePathForGeneration(#[from] std::path::StripPrefixError), +} + +pub struct CodeGenerator {} + +struct FileDescriptor { + simf_content: SimfContent, + simf_file: PathBuf, + cwd: PathBuf, +} + +type _ContractModName = String; +type _FolderName = String; +type _ContractName = String; + +#[derive(Debug, Default)] +struct _TreeNode { + files: Vec, + folders: HashMap<_FolderName, _TreeNode>, +} + +impl<'b> CodeGenerator { + pub fn generate_files( + cwd: impl AsRef, + out_dir: impl AsRef, + simfs: &[impl AsRef], + ) -> Result<(), CodeGeneratorError> { + let _ = Self::_generate_files(cwd, out_dir, simfs)?; + + Ok(()) + } + + pub fn generate_artifacts_mod( + out_dir_name: impl AsRef, + cwd: impl AsRef, + base_path: impl AsRef, + out_dir: impl AsRef, + simfs: &[impl AsRef], + ) -> Result<(), CodeGeneratorError> { + let out_dir = dbg!(out_dir.as_ref().join(out_dir_name.as_ref())); + + let tree = dbg!(Self::_build_directory_tree(simfs, &base_path)?); + Self::_generate_tree_file_structure(cwd.as_ref(), &out_dir, tree)?; + + Ok(()) + } +} + +impl<'b> CodeGenerator { + fn _generate_files( + cwd: impl AsRef, + out_dir: impl AsRef, + simfs: &[impl AsRef], + ) -> Result, CodeGeneratorError> { + let out_dir = out_dir.as_ref(); + let cwd = cwd.as_ref(); + + fs::create_dir_all(out_dir)?; + let mut module_files = Vec::with_capacity(simfs.len()); + + for simf_file_path in simfs { + let mod_name = Self::_generate_file(cwd, out_dir, simf_file_path)?; + module_files.push(mod_name); + } + Ok(module_files) + } + + fn _generate_tree_file_structure( + cwd: &Path, + out_dir: &Path, + path_tree: _TreeNode, + ) -> Result, CodeGeneratorError> { + let mut mod_filenames = Self::_generate_files(cwd, &out_dir, &path_tree.files)?; + for (folder_name, tree_node) in path_tree.folders.into_iter() { + Self::_generate_tree_file_structure(cwd, &out_dir.join(&folder_name), tree_node)?; + mod_filenames.push(folder_name); + } + Self::_generate_mod_rs(&out_dir, &mod_filenames)?; + Ok(mod_filenames) + } + + fn _generate_mod_rs( + out_dir: impl AsRef, + simfs_mod_name: &[_ContractModName], + ) -> Result<(), CodeGeneratorError> { + let out_dir = out_dir.as_ref(); + let output_file = out_dir.join("mod.rs"); + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&output_file)?; + let simfs_mod_name = simfs_mod_name.iter().map(|x| format_ident!("{x}")).collect::>(); + let code = quote! { + #(pub mod #simfs_mod_name);*; + }; + Self::expand_file(code, &mut file)?; + Ok(()) + } + + fn _build_directory_tree( + paths: &[impl AsRef], + base: impl AsRef, + ) -> Result<_TreeNode, CodeGeneratorError> { + let mut root = _TreeNode::default(); + + for path in paths { + let path = path.as_ref(); + + let relative_path = path + .strip_prefix(base.as_ref()) + .map_err(CodeGeneratorError::NoBasePathForGeneration)?; + + let components: Vec<_> = relative_path + .components() + .filter_map(|c| { + if let Component::Normal(name) = c { + Some(name) + } else { + None + } + }) + .collect(); + + let mut current_node = &mut root; + let components_len = components.len(); + + for (i, name) in components.into_iter().enumerate() { + let is_file = i == components_len - 1; + if is_file { + current_node.files.push(path.to_path_buf()); + } else { + let folder_name = name.to_string_lossy().into_owned(); + current_node = current_node.folders.entry(folder_name).or_default(); + } + } + } + + Ok(root) + } + + fn _generate_file( + cwd: impl AsRef, + out_dir: impl AsRef, + simf_file_path: impl AsRef, + ) -> Result<_ContractModName, CodeGeneratorError> { + let path_buf = PathBuf::from(simf_file_path.as_ref()); + let simf_content = + SimfContent::extract_content_from_path(&path_buf).map_err(CodeGeneratorError::FailedToExtractContent)?; + + dbg!(cwd.as_ref()); + fs::create_dir_all(&out_dir)?; + let output_file = out_dir.as_ref().join(format!("{}.rs", &simf_content.contract_name)); + let contract_name = simf_content.contract_name.clone(); + + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&output_file)?; + let code = Self::generate_simf_binding_code(FileDescriptor { + simf_content, + simf_file: path_buf, + cwd: cwd.as_ref().to_path_buf(), + })?; + Self::expand_file(code, &mut file)?; + Ok(contract_name) + } + + fn expand_file(code: TokenStream, buf: &mut dyn Write) -> Result<(), CodeGeneratorError> { + 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_simf_binding_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, &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::program::{ArgumentsTrait, Program}; + use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; + + pub struct #program_name { + program: Program, + } + + impl #program_name { + pub const SOURCE: &'static str = #include_simf_module::#include_simf_source_const; + + pub fn new(public_key: XOnlyPublicKey, arguments: impl ArgumentsTrait + 'static) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + + pub fn get_program(&self) -> &Program { + &self.program + } + + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } + } + + include_simf!(#pathdiff); + }; + + Ok(code) + } +} diff --git a/crates/template-gen/src/lib.rs b/crates/template-gen/src/lib.rs new file mode 100644 index 0000000..cc7e391 --- /dev/null +++ b/crates/template-gen/src/lib.rs @@ -0,0 +1,23 @@ +#![warn(clippy::all, clippy::pedantic)] + +mod env; +pub use env::CodeGeneratorError; + +pub fn expand_only_files( + cwd: impl AsRef, + outdir: impl AsRef, + simfs: &[impl AsRef], +) -> Result<(), CodeGeneratorError> { + env::CodeGenerator::generate_files(cwd, outdir, simfs) +} + +pub fn expand_files_with_nested_dirs( + cwd: impl AsRef, + base_dir: impl AsRef, + outdir: impl AsRef, + simfs: &[impl AsRef], +) -> Result<(), CodeGeneratorError> { + const ARTIFACTS_DIR_NAME: &str = "artifacts"; + + env::CodeGenerator::generate_artifacts_mod(ARTIFACTS_DIR_NAME, cwd, base_dir, outdir, simfs) +} diff --git a/simplex/crates/test/Cargo.toml b/crates/test/Cargo.toml similarity index 81% rename from simplex/crates/test/Cargo.toml rename to crates/test/Cargo.toml index 655691c..d93322b 100644 --- a/simplex/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -7,13 +7,11 @@ edition.workspace = true [lints] workspace = true - [dependencies] -simplex-provider = { workspace = true } simplex-sdk = { workspace = true } thiserror = { workspace = true } simplicityhl = { workspace = true } electrsd = { workspace = true } serde = { workspace = true } -toml = { workspace = true } \ No newline at end of file +toml = { workspace = true } diff --git a/crates/test/src/config.rs b/crates/test/src/config.rs new file mode 100644 index 0000000..2730675 --- /dev/null +++ b/crates/test/src/config.rs @@ -0,0 +1,50 @@ +use std::fs; +use std::fs::OpenOptions; +use std::io::Read; +use std::io::Write; +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use super::error::TestError; + +pub const TEST_ENV_NAME: &str = "SIMPLEX_TEST_ENV"; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct TestConfig { + #[serde(default)] + pub esplora: Option, + #[serde(default)] + pub rpc: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct RpcConfig { + url: String, + username: String, + password: String, +} + +impl TestConfig { + pub fn to_file(&self, path: &impl AsRef) -> Result<(), TestError> { + if let Some(parent_dir) = path.as_ref().parent() { + fs::create_dir_all(parent_dir)?; + } + + let mut file = OpenOptions::new().create(true).write(true).open(&path)?; + + file.write(toml::to_string_pretty(&self).unwrap().as_bytes())?; + file.flush()?; + + Ok(()) + } + + pub fn from_file(path: impl AsRef) -> Result { + let mut content = String::new(); + let mut file = OpenOptions::new().read(true).open(path)?; + + file.read_to_string(&mut content)?; + + Ok(toml::from_str(&content)?) + } +} diff --git a/crates/test/src/context.rs b/crates/test/src/context.rs new file mode 100644 index 0000000..fb30056 --- /dev/null +++ b/crates/test/src/context.rs @@ -0,0 +1,75 @@ +// use crate::TestError; +// pub use config::*; +// use electrsd::bitcoind::bitcoincore_rpc::Auth; +// pub use rpc_provider::*; +// 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/error.rs b/crates/test/src/error.rs new file mode 100644 index 0000000..2bb4ade --- /dev/null +++ b/crates/test/src/error.rs @@ -0,0 +1,12 @@ +use std::io; + +#[derive(thiserror::Error, Debug)] +pub enum TestError { + /// 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 new file mode 100644 index 0000000..a2bc69c --- /dev/null +++ b/crates/test/src/lib.rs @@ -0,0 +1,5 @@ +pub mod config; +pub mod context; +pub mod error; + +pub use config::{RpcConfig, TestConfig, TEST_ENV_NAME}; diff --git a/example/Cargo.toml b/example/Cargo.toml deleted file mode 100644 index e1f7989..0000000 --- a/example/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "draft_example" -edition = "2024" -rust-version = "1.90.0" -version = "0.1.0" - -[dependencies] -simplex = { path = "../simplex/crates/simplex" } - -simplicityhl = { git = "https://github.com/ikripaka/SimplicityHL/", branch = "feature/rich-params" } -trybuild = { version = "1.0.115" } -anyhow = { version = "1.0.101" } diff --git a/example/Simplex.toml b/example/Simplex.toml deleted file mode 100644 index 349e228..0000000 --- a/example/Simplex.toml +++ /dev/null @@ -1,8 +0,0 @@ -network = "liquidtestnet" - -[build] -compile_simf = ["simf/*.simf"] -out_dir = "./src/artifacts" - -[test] -elementsd_path = "../simplex/assets/elementsd" diff --git a/example/simplex b/example/simplex deleted file mode 100755 index d414575..0000000 Binary files a/example/simplex and /dev/null differ diff --git a/example/src/artifacts/mod.rs b/example/src/artifacts/mod.rs deleted file mode 100644 index cef7d47..0000000 --- a/example/src/artifacts/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod options; -pub mod p2pk; diff --git a/example/tests/draft_test.rs b/example/tests/draft_test.rs deleted file mode 100644 index 16cdc63..0000000 --- a/example/tests/draft_test.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[simplex::simplex_macros::test] -fn test_invocation_tx_tracking(context: simplex::simplex_test::TestContext) -> anyhow::Result<()> { - todo!() -} diff --git a/example/Cargo.lock b/examples/basic/Cargo.lock similarity index 95% rename from example/Cargo.lock rename to examples/basic/Cargo.lock index 532fb85..ae5b5a9 100644 --- a/example/Cargo.lock +++ b/examples/basic/Cargo.lock @@ -96,7 +96,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -421,7 +421,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -431,7 +431,6 @@ dependencies = [ "anyhow", "simplex", "simplicityhl", - "trybuild", ] [[package]] @@ -631,12 +630,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8449d342b1c67f49169e92e71deb7b9b27f30062301a16dbc27a4cc8d2351b7" -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "hashbrown" version = "0.15.5" @@ -1111,12 +1104,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -1160,31 +1147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", + "syn", ] [[package]] @@ -1630,7 +1593,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1704,23 +1667,19 @@ name = "simplex-macros" version = "0.1.0" dependencies = [ "simplex-macros-core", - "syn 2.0.117", + "syn", ] [[package]] name = "simplex-macros-core" version = "0.1.0" dependencies = [ - "pathdiff", - "prettyplease", - "proc-macro-error", "proc-macro2", "quote", "serde", "simplex-test", "simplicityhl", - "syn 2.0.117", - "thiserror", + "syn", ] [[package]] @@ -1766,7 +1725,7 @@ dependencies = [ "simplex-sdk", "simplicityhl", "thiserror", - "toml 0.9.12+spec-1.1.0", + "toml", ] [[package]] @@ -1800,7 +1759,7 @@ dependencies = [ [[package]] name = "simplicityhl" version = "0.4.1" -source = "git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params#77755254414093ff0e1b51beedbc27367745388e" +source = "git+https://github.com/BlockstreamResearch/SimplicityHL.git?rev=568b462#568b4621d6145cd97dce68a3f3428c7eb85306b6" dependencies = [ "base64 0.21.7", "chumsky", @@ -1867,16 +1826,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.117" @@ -1905,15 +1854,9 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] -[[package]] -name = "target-triple" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" - [[package]] name = "tempfile" version = "3.26.0" @@ -1927,15 +1870,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "2.0.18" @@ -1953,7 +1887,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2014,22 +1948,7 @@ dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml" -version = "1.0.3+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", "winnow", @@ -2044,15 +1963,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_datetime" -version = "1.0.0+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_parser" version = "1.0.9+spec-1.1.0" @@ -2132,7 +2042,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2150,21 +2060,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "trybuild" -version = "1.0.116" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c635f0191bd3a2941013e5062667100969f8c4e9cd787c14f977265d73616e" -dependencies = [ - "glob", - "serde", - "serde_derive", - "serde_json", - "target-triple", - "termcolor", - "toml 1.0.3+spec-1.1.0", -] - [[package]] name = "typenum" version = "1.19.0" @@ -2331,7 +2226,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wasm-bindgen-shared", ] @@ -2425,15 +2320,6 @@ dependencies = [ "rustix 0.38.44", ] -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "windows-link" version = "0.2.1" @@ -2641,7 +2527,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.117", + "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2657,7 +2543,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2724,7 +2610,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -2745,7 +2631,7 @@ checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2765,7 +2651,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -2805,7 +2691,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml new file mode 100644 index 0000000..a6c9551 --- /dev/null +++ b/examples/basic/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "draft_example" +edition = "2024" +rust-version = "1.90.0" +version = "0.1.0" + +[dependencies] +simplex = { path = "../../crates/simplex" } + +simplicityhl = { git = "https://github.com/BlockstreamResearch/SimplicityHL.git", rev = "568b462" } +anyhow = { version = "1.0.101" } diff --git a/examples/basic/Simplex.toml b/examples/basic/Simplex.toml new file mode 100644 index 0000000..30229db --- /dev/null +++ b/examples/basic/Simplex.toml @@ -0,0 +1,9 @@ +network = "elementsregtest" + +[build] +simf_files = ["*.simf"] +out_dir = "./src" +base_dir = "./simf" + +[test] +elementsd_path = "../../assets/elementsd" diff --git a/examples/basic/simf/another_dir/another_module/bytes32_tr_storage.simf b/examples/basic/simf/another_dir/another_module/bytes32_tr_storage.simf new file mode 100644 index 0000000..0d11b5f --- /dev/null +++ b/examples/basic/simf/another_dir/another_module/bytes32_tr_storage.simf @@ -0,0 +1,66 @@ +/* + * Computes the "State Commitment" — the expected Script PubKey (address) + * for a specific state value. + * + * HOW IT WORKS: + * In Simplicity/Liquid, state is not stored in a dedicated database. Instead, + * it is verified via a "Commitment Scheme" inside the Taproot tree of the UTXO. + * + * This function reconstructs the Taproot structure to validate that the provided + * witness data (state_data) was indeed cryptographically embedded into the + * transaction output that is currently being spent. + * + * LOGIC FLOW: + * 1. Takes state_data (passed via witness at runtime). + * 2. Hashes it as a non-executable TapData leaf. + * 3. Combines it with the current program's CMR (tapleaf_hash). + * 4. Derives the tweaked_key (Internal Key + Merkle Root). + * 5. Returns the final SHA256 script hash (SegWit v1). + * + * USAGE: + * - In main, we verify: CalculatedHash(witness::STATE) == input_script_hash. + * - This assertion proves that the UTXO is "locked" not just by the code, + * but specifically by THIS instance of the state data. + */ + +fn script_hash_for_input_script(state_data: u256) -> u256 { + // This is the bulk of our "compute state commitment" logic from above. + let tap_leaf: u256 = jet::tapleaf_hash(); + let state_ctx1: Ctx8 = jet::tapdata_init(); + let state_ctx2: Ctx8 = jet::sha_256_ctx_8_add_32(state_ctx1, state_data); + let state_leaf: u256 = jet::sha_256_ctx_8_finalize(state_ctx2); + let tap_node: u256 = jet::build_tapbranch(tap_leaf, state_leaf); + + // Compute a taptweak using this. + let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; + let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); + + // Turn the taptweak into a script hash + let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); + let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 + let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); + jet::sha_256_ctx_8_finalize(hash_ctx3) +} + +fn main() { + let state_data: u256 = witness::STATE; + let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(state_data); + + // Assert that the input is correct, i.e. "load". + assert!(jet::eq_256( + script_hash_for_input_script(state_data), + unwrap(jet::input_script_hash(jet::current_index())) + )); + + // Do a state update (and fail on 64-bit overflow even though we've got 192 other + // bits we could be using..) + let (carry, new_state4): (bool, u64) = jet::increment_64(state4); + assert!(jet::eq_1(::into(carry), 0)); + + let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); + // Assert that the output is correct, i.e. "store". + assert!(jet::eq_256( + script_hash_for_input_script(new_state), + unwrap(jet::output_script_hash(jet::current_index())) + )); +} \ No newline at end of file diff --git a/examples/basic/simf/another_dir/another_module/dual_currency_deposit.simf b/examples/basic/simf/another_dir/another_module/dual_currency_deposit.simf new file mode 100644 index 0000000..e1a460a --- /dev/null +++ b/examples/basic/simf/another_dir/another_module/dual_currency_deposit.simf @@ -0,0 +1,592 @@ +/* + * DCD: Dual Currency Deposit – price-attested settlement and funding windows + * + * Flows implemented: + * - Maker funding: deposit settlement asset and collateral, issue grantor tokens + * - Taker funding: deposit collateral in window and receive filler tokens + * - Settlement: at SETTLEMENT_HEIGHT, oracle Schnorr signature over (height, price) + * selects LBTC vs ALT branch based on price <= STRIKE_PRICE + * - Early/post-expiry termination: taker returns filler; maker burns grantor tokens + * - Merge: consolidate 2/3/4 token UTXOs + * + * All amounts and asset/script invariants are enforced on-chain; time guards use + * fallback locktime and height checks. + * + * Batching discussion: https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 + */ + +// Verify Schnorr signature against SHA256 of (u32 || u64) +fn checksig_priceblock(pk: Pubkey, current_block_height: u32, price_at_current_block_height: u64, sig: Signature) { + let hasher: Ctx8 = jet::sha_256_ctx_8_init(); + let hasher: Ctx8 = jet::sha_256_ctx_8_add_4(hasher, current_block_height); + let hasher: Ctx8 = jet::sha_256_ctx_8_add_8(hasher, price_at_current_block_height); + let msg: u256 = jet::sha_256_ctx_8_finalize(hasher); + jet::bip_0340_verify((pk, msg), sig); +} + +// Signed <= using XOR with 0x8000.. bias: a<=b (signed) iff (a^bias) <= (b^bias) (unsigned) +fn signed_le_u64(a_bits: u64, b_bits: u64) -> bool { + let bias: u64 = 0x8000000000000000; + jet::le_64(jet::xor_64(a_bits, bias), jet::xor_64(b_bits, bias)) +} + +fn signed_lt_u64(a: u64, b: u64) -> bool { + let bias: u64 = 0x8000000000000000; + jet::lt_64(jet::xor_64(a, bias), jet::xor_64(b, bias)) +} + +/// 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)); +} + +/// Assert: base_amount * basis_point_percentage == provided_amount * MAX_BASIS_POINTS +fn constraint_percentage(base_amount: u64, basis_point_percentage: u64, provided_amount: u64) { + let MAX_BASIS_POINTS: u64 = 10000; + + let arg1: u256 = <(u128, u128)>::into((0, jet::multiply_64(base_amount, basis_point_percentage))); + let arg2: u256 = <(u128, u128)>::into((0, jet::multiply_64(provided_amount, MAX_BASIS_POINTS))); + + assert!(jet::eq_256(arg1, arg2)); +} + +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 ensure_one_bit_or(bit1: bool, bit2: bool) { + assert!( + jet::eq_1( + ::into(jet::or_1(::into(bit1), ::into(bit2))), + 1 + ) + ); +} + +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) = 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 merge_2_tokens() { + // 2 tokens to merge + 1 input as fee + assert!(jet::eq_32(jet::num_inputs(), 3)); + // 3 outputs: 1 merged token + 1 change + 1 fee + assert!(jet::eq_32(jet::num_outputs(), 3)); + assert!(jet::le_32(jet::current_index(), 1)); + + ensure_input_and_output_script_hash_eq(0); + let script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(script_hash, get_input_script_hash(1))); +} + +fn merge_3_tokens() { + // 3 tokens to merge + 1 input as fee + assert!(jet::eq_32(jet::num_inputs(), 4)); + // 3 outputs: 1 merged token + 1 change + 1 fee + assert!(jet::eq_32(jet::num_outputs(), 3)); + assert!(jet::le_32(jet::current_index(), 2)); + + ensure_input_and_output_script_hash_eq(0); + let script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(script_hash, get_input_script_hash(1))); + assert!(jet::eq_256(script_hash, get_input_script_hash(2))); +} + +fn merge_4_tokens() { + // 4 tokens to merge + 1 input as fee + assert!(jet::eq_32(jet::num_inputs(), 5)); + // 3 outputs: 1 merged token + 1 change + 1 fee + assert!(jet::eq_32(jet::num_outputs(), 3)); + assert!(jet::le_32(jet::current_index(), 3)); + + ensure_input_and_output_script_hash_eq(0); + let script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(script_hash, get_input_script_hash(1))); + assert!(jet::eq_256(script_hash, get_input_script_hash(2))); + assert!(jet::eq_256(script_hash, get_input_script_hash(3))); +} + +/* +* Maker funding path +* Params: +* 1. FILLER_PER_SETTLEMENT_COLLATERAL +* 2. FILLER_PER_SETTLEMENT_ASSET +* 3. FILLER_PER_PRINCIPAL_COLLATERAL +* 4. GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET +* 5. GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL +* 6. GRANTOR_PER_SETTLEMENT_COLLATERAL +* 7. GRANTOR_PER_SETTLEMENT_ASSET +*/ +fn maker_funding_path(principal_collateral_amount: u64, principal_asset_amount: u64, interest_collateral_amount: u64, interest_asset_amount: u64) { + assert!(jet::eq_32(jet::num_inputs(), 5)); + assert!(jet::eq_32(jet::num_outputs(), 11)); + + let current_time: u32 = ::into(jet::lock_time()); + assert!(jet::lt_32(current_time, param::TAKER_FUNDING_START_TIME)); + + ensure_input_and_output_script_hash_eq(0); + ensure_input_and_output_script_hash_eq(1); + ensure_input_and_output_script_hash_eq(2); + + assert!(jet::le_32(jet::current_index(), 2)); + + let script_hash: u256 = get_output_script_hash(0); + ensure_output_script_hash_eq(1, script_hash); + ensure_output_script_hash_eq(2, script_hash); + ensure_output_script_hash_eq(3, script_hash); + ensure_output_script_hash_eq(4, script_hash); + ensure_output_script_hash_eq(5, script_hash); + + let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(3); + let (settlement_asset_bits, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(4); + let filler_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); + let grantor_collateral_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); + let grantor_settlement_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(2)))); + assert!(jet::eq_64(filler_token_amount, grantor_collateral_token_amount)); + assert!(jet::eq_64(filler_token_amount, grantor_settlement_token_amount)); + + divmod_eq(principal_asset_amount, param::STRIKE_PRICE, principal_collateral_amount); + + assert!(jet::eq_64(collateral_amount, interest_collateral_amount)); + constraint_percentage(principal_collateral_amount, param::INCENTIVE_BASIS_POINTS, collateral_amount); + + let MAX_BASIS_POINTS: u64 = 10000; + let (carry, asset_incentive_percentage): (bool, u64) = jet::add_64(param::INCENTIVE_BASIS_POINTS, MAX_BASIS_POINTS); + ensure_zero_bit(carry); + + constraint_percentage(principal_asset_amount, asset_incentive_percentage, settlement_amount); + + let (carry, calculated_total_asset_amount): (bool, u64) = jet::add_64(principal_asset_amount, interest_asset_amount); + ensure_zero_bit(carry); + assert!(jet::eq_64(calculated_total_asset_amount, settlement_amount)); + + let (carry, calculated_total_collateral_amount): (bool, u64) = jet::add_64(principal_collateral_amount, interest_collateral_amount); + ensure_zero_bit(carry); + + // Filler token constraints + divmod_eq(calculated_total_collateral_amount, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_token_amount); + divmod_eq(calculated_total_asset_amount, param::FILLER_PER_SETTLEMENT_ASSET, filler_token_amount); + divmod_eq(principal_collateral_amount, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount); + + // Grantor token constraints + divmod_eq(calculated_total_asset_amount, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_token_amount); + divmod_eq(interest_collateral_amount, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_token_amount); + + divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_collateral_token_amount); + // divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_settlement_token_amount); // duplicated because of lines 203-204 + + divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_collateral_token_amount); + // divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_settlement_token_amount); // duplicated because of lines 203-204 + + assert!(jet::eq_256(param::COLLATERAL_ASSET_ID, collateral_asset_bits)); + assert!(jet::eq_256(param::SETTLEMENT_ASSET_ID, settlement_asset_bits)); + + ensure_output_asset_with_amount_eq(5, param::FILLER_TOKEN_ASSET, filler_token_amount); + ensure_output_asset_with_amount_eq(6, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_token_amount); + ensure_output_asset_with_amount_eq(7, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_token_amount); + + ensure_input_asset_eq(3, param::SETTLEMENT_ASSET_ID); + ensure_input_asset_eq(4, param::COLLATERAL_ASSET_ID); + + ensure_output_asset_eq(8, param::COLLATERAL_ASSET_ID); + ensure_output_asset_eq(9, param::SETTLEMENT_ASSET_ID); + ensure_output_asset_eq(10, param::COLLATERAL_ASSET_ID); +} + +fn taker_funding_path(collateral_amount_to_deposit: u64, filler_token_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + assert!(jet::le_32(param::TAKER_FUNDING_START_TIME, current_time)); + assert!(jet::lt_32(current_time, param::TAKER_FUNDING_END_TIME)); + assert!(jet::lt_32(current_time, param::CONTRACT_EXPIRY_TIME)); + + let filler_token_input_index: u32 = 0; + let collateral_input_index: u32 = 1; + + let (collateral_to_covenant_output_index, filler_to_user_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(filler_token_input_index); + + // Check and ensure filler token change + ensure_correct_change_at_index(0, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(collateral_amount_to_deposit, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_get); + + // Ensure collateral asset and script hash are correct + ensure_output_asset_with_amount_eq(collateral_to_covenant_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_deposit); + ensure_output_script_hash_eq(collateral_to_covenant_output_index, expected_current_script_hash); + + ensure_output_asset_with_amount_eq(filler_to_user_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get); +} + +fn taker_early_termination_path(filler_token_amount_to_return: u64, collateral_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); + + let collateral_input_index: u32 = 0; + let filler_token_input_index: u32 = 1; + + let (return_filler_output_index, return_collateral_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::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_return); + + // Ensure filler token transferred to covenant + ensure_output_asset_with_amount_eq(return_filler_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_return); + ensure_output_script_hash_eq(return_filler_output_index, expected_current_script_hash); + + // Ensure collateral transferred to user + ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); +} + +fn maker_collateral_termination_path(grantor_collateral_amount_to_burn: u64, collateral_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); + + let collateral_input_index: u32 = 0; + let grantor_collateral_token_input_index: u32 = 1; + + let (burn_grantor_collateral_output_index, return_collateral_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::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_amount_to_burn); + + // Burn grantor collateral token + ensure_output_is_op_return(burn_grantor_collateral_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_amount_to_burn); + + // Ensure collateral transferred to user + ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); +} + +fn maker_settlement_termination_path(grantor_settlement_amount_to_burn: u64, settlement_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); + + let settlement_asset_input_index: u32 = 0; + let grantor_settlement_token_input_index: u32 = 1; + + let (burn_grantor_settlement_output_index, return_settlement_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset amount is correct + divmod_eq(settlement_amount_to_get, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_amount_to_burn); + + // Burn grantor settlement token + ensure_output_is_op_return(burn_grantor_settlement_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_amount_to_burn); + + // Ensure settlement asset transferred to user + ensure_output_asset_with_amount_eq(return_settlement_output_index, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get); +} + +fn ensure_correct_return_at(user_output_index: u32, asset_id: u256, amount_to_get: u64, fee_basis_points: u64) { + match jet::eq_64(fee_basis_points, 0) { + true => ensure_output_asset_with_amount_eq(user_output_index, asset_id, amount_to_get), + false => { + let fee_output_index: u32 = increment_by(user_output_index, 1); + + let (user_asset_bits, user_amount): (u256, u64) = get_output_explicit_asset_amount(user_output_index); + assert!(jet::eq_256(user_asset_bits, asset_id)); + + let (fee_asset_bits, fee_amount): (u256, u64) = get_output_explicit_asset_amount(fee_output_index); + assert!(jet::eq_256(fee_asset_bits, asset_id)); + + let (carry, calculated_total_amount): (bool, u64) = jet::add_64(user_amount, fee_amount); + ensure_zero_bit(carry); + + constraint_percentage(calculated_total_amount, fee_basis_points, fee_amount); + + ensure_output_script_hash_eq(fee_output_index, param::FEE_SCRIPT_HASH); + }, + }; +} + +fn maker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, grantor_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { + jet::check_lock_height(param::SETTLEMENT_HEIGHT); + checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); + + match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { + true => { + // Maker gets ALT + let settlement_asset_input_index: u32 = 0; + + let (burn_grantor_settlement_output_index, burn_grantor_collateral_output_index, settlement_output_index): (u32, u32, u32) = match is_change_needed { + true => (1, 2, 3), + false => (0, 1, 2), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset amount is correct + divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_amount_to_burn); + + // Burn grantor settlement and collateral tokens + ensure_output_is_op_return(burn_grantor_settlement_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); + ensure_output_is_op_return(burn_grantor_collateral_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); + + // Ensure settlement asset transferred to user + ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + false => { + // Maker gets the LBTC + let collateral_input_index: u32 = 0; + + let (burn_grantor_collateral_output_index, burn_grantor_settlement_output_index, collateral_output_index): (u32, u32, u32) = match is_change_needed { + true => (1, 2, 3), + false => (0, 1, 2), + }; + + 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, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_amount_to_burn); + + // Burn grantor collateral and settlement tokens + ensure_output_is_op_return(burn_grantor_collateral_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); + ensure_output_is_op_return(burn_grantor_settlement_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); + + // Ensure collateral transferred to user + ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + } +} + +fn taker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, filler_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { + jet::check_lock_height(param::SETTLEMENT_HEIGHT); + checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); + + match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { + true => { + // Taker receives LBTC principal+interest + let collateral_input_index: u32 = 0; + + let (burn_filler_output_index, collateral_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, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_amount_to_burn); + + // Burn filler token + ensure_output_is_op_return(burn_filler_output_index); + ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); + + // Ensure collateral transferred to user + ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + false => { + // Taker receives ALT + let settlement_asset_input_index: u32 = 0; + + let (burn_filler_output_index, settlement_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset amount is correct + divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_ASSET, filler_amount_to_burn); + + // Burn filler token + ensure_output_is_op_return(burn_filler_output_index); + ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); + + // Ensure filler token transferred to user + ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + } +} + +fn main() { + let token_branch: Either<(), ()> = witness::TOKEN_BRANCH; + let merge_branch: Either, ()> = witness::MERGE_BRANCH; + + match witness::PATH { + Left(funding_or_settlement: Either, (u64, Signature, u64, u64, bool)>) => match funding_or_settlement { + // Funding branches + Left(funding_params: Either<(u64, u64, u64, u64), (u64, u64, bool)>) => match funding_params { + // Maker funding: (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) + Left(params: (u64, u64, u64, u64)) => { + let (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount): (u64, u64, u64, u64) = params; + maker_funding_path(principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) + }, + // Taker funding: (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) + Right(params: (u64, u64, bool)) => { + let (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed): (u64, u64, bool) = params; + taker_funding_path(collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) + }, + }, + // Settlement branches (oracle price attested) + Right(params: (u64, Signature, u64, u64, bool)) => { + let (price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed): (u64, Signature, u64, u64, bool) = params; + + match token_branch { + // Maker settlement: burn grantor token + Left(u: ()) => maker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), + // Taker settlement: burn filler token + Right(u: ()) => taker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), + } + }, + }, + // Termination flows (early termination or post-expiry) or Merge flows + Right(termination_or_maker_or_merge: Either, ()>) => match termination_or_maker_or_merge { + Left(termination_or_maker: Either<(bool, u64, u64), (bool, u64, u64)>) => match termination_or_maker { + // Taker early termination: (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get) + Left(params: (bool, u64, u64)) => { + let (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get): (bool, u64, u64) = params; + taker_early_termination_path(filler_token_amount_to_return, collateral_amount_to_get, is_change_needed) + }, + // Maker termination (burn grantor token): choose collateral vs settlement token via token_branch + Right(params: (bool, u64, u64)) => { + let (is_change_needed, grantor_token_amount_to_burn, amount_to_get): (bool, u64, u64) = params; + + match token_branch { + // Burn grantor collateral token -> receive collateral + Left(u: ()) => maker_collateral_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), + // Burn grantor settlement token -> receive settlement asset + Right(u: ()) => maker_settlement_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), + } + }, + }, + Right(u: ()) => { + // Merge tokens based on MERGE_BRANCH discriminator + match merge_branch { + Left(left_or_right: Either<(), ()>) => match left_or_right { + Left(u: ()) => merge_2_tokens(), + Right(u: ()) => merge_3_tokens(), + }, + Right(u: ()) => merge_4_tokens(), + } + }, + }, + } + +} diff --git a/examples/basic/simf/another_dir/array_tr_storage.simf b/examples/basic/simf/another_dir/array_tr_storage.simf new file mode 100644 index 0000000..4918cf3 --- /dev/null +++ b/examples/basic/simf/another_dir/array_tr_storage.simf @@ -0,0 +1,81 @@ +/* + * Extends `bytes32_tr_storage` using `array_fold` for larger buffers. + * Optimized for small, fixed-size states where linear hashing is more efficient + * than Merkle Trees. By avoiding proof overhead like sibling hashes, we reduce + * witness size and simplify contract logic for small N. + * This approach is particularly advantageous when updating all slots within every transaction. + */ + +fn hash_array_tr_storage(elem: u256, ctx: Ctx8) -> Ctx8 { + jet::sha_256_ctx_8_add_32(ctx, elem) +} + +fn hash_array_tr_storage_with_update(elem: u256, triplet: (Ctx8, u16, u16)) -> (Ctx8, u16, u16) { + let (ctx, i, changed_index): (Ctx8, u16, u16) = triplet; + + match jet::eq_16(i, changed_index) { + true => { + let (_, val): (bool, u16) = jet::increment_16(i); + + // There may be arbitrary logic here + let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(elem); + let new_state4: u64 = 20; + + let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); + ( + jet::sha_256_ctx_8_add_32(ctx, new_state), + val, + changed_index, + ) + }, + false => { + let (_, val): (bool, u16) = jet::increment_16(i); + ( + jet::sha_256_ctx_8_add_32(ctx, elem), + val, + changed_index, + ) + } + } +} + +fn script_hash_for_input_script(state: [u256; 3], changed_index: Option) -> u256 { + let tap_leaf: u256 = jet::tapleaf_hash(); + let ctx: Ctx8 = jet::tapdata_init(); + + let (ctx, _, _): (Ctx8, u16, u16) = match changed_index { + Some(ind: u16) => { + array_fold::(state, (ctx, 0, ind)) + }, + None => { + (array_fold::(state, ctx), 0, 0) + } + }; + + let computed: u256 = jet::sha_256_ctx_8_finalize(ctx); + let tap_node: u256 = jet::build_tapbranch(tap_leaf, computed); + + let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; + let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); + + let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); + let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 + let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); + jet::sha_256_ctx_8_finalize(hash_ctx3) +} + +fn main() { + let state: [u256; 3] = witness::STATE; + + // Assert that the input is correct, i.e. "load". + assert!(jet::eq_256( + script_hash_for_input_script(state, None), + unwrap(jet::input_script_hash(jet::current_index())) + )); + + // Assert that the output is correct, i.e. "store". + assert!(jet::eq_256( + script_hash_for_input_script(state, Some(witness::CHANGED_INDEX)), + unwrap(jet::output_script_hash(jet::current_index())) + )); +} \ No newline at end of file diff --git a/examples/basic/simf/module/option_offer.simf b/examples/basic/simf/module/option_offer.simf new file mode 100644 index 0000000..5cb2108 --- /dev/null +++ b/examples/basic/simf/module/option_offer.simf @@ -0,0 +1,213 @@ +/* + * Option Offer + * + * A covenant that allows a user to deposit collateral and premium assets, + * and have a counterparty swap settlement asset for both. + * The user can withdraw accumulated settlement asset at any time (with signature). + * After expiry, the user can reclaim any remaining collateral and premium (with signature). + * + * Paths: + * 1. Exercise: Counterparty swaps settlement asset for collateral + premium (no time restriction, optional change) + * 2. Withdraw: User withdraws settlement asset (no time restriction, signature required, full amount) + * 3. Expiry: User reclaims collateral + premium (after expiry, signature required, full amount) + * + * Constraints: + * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount + * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount + */ + +/// 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_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_zero_bit(bit: bool) { + assert!(jet::eq_1(::into(bit), 0)); +} + +fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { + let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); + assert!(jet::eq_256(asset, expected_bits)); + assert!(jet::eq_64(amount, expected_amount)); +} + +fn ensure_output_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); +} + +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 check_user_signature(sig: Signature) { + let msg: u256 = jet::sig_all_hash(); + jet::bip_0340_verify((param::USER_PUBKEY, msg), sig); +} + +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)); + + match is_change_needed { + true => { + ensure_input_and_output_script_hash_eq(index); + + let (carry, asset_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, asset_change); + }, + false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), + } +} + +/* + * Exercise Path + * + * Counterparty swaps settlement asset for collateral + premium. + * No time restriction - works before and after expiry. + * + * Constraints: + * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount + * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount + * + * Layout: + * + * Both: + * Input[0]: Collateral from covenant + * Input[1]: Premium from covenant + * + * With change (partial swap): + * Output[0]: Collateral change → covenant + * Output[1]: Premium change → covenant + * Output[2]: Settlement asset → covenant + * Output[3]: Collateral → counterparty + * Output[4]: Premium → counterparty + * + * Without change (full swap): + * Output[0]: Settlement asset → covenant + * Output[1]: Collateral → counterparty + * Output[2]: Premium → counterparty + */ +fn exercise_path(collateral_amount: u64, is_change_needed: bool) { + assert!(jet::le_32(jet::current_index(), 1)); + + let expected_covenant_script_hash: u256 = get_input_script_hash(0); + + assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); + + let premium_amount_u128: u128 = jet::multiply_64(collateral_amount, param::PREMIUM_PER_COLLATERAL); + let (left_part, premium_amount): (u64, u64) = dbg!(::into(premium_amount_u128)); + assert!(jet::eq_64(left_part, 0)); + + // Check collateral changes + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_covenant_script_hash, is_change_needed); + ensure_correct_change_at_index(1, param::PREMIUM_ASSET_ID, premium_amount, expected_covenant_script_hash, is_change_needed); + + let (settlement_output_index, collateral_output_index, premium_output_index): (u32, u32, u32) = match is_change_needed { + true => (2, 3, 4), + false => (0, 1, 2), + }; + + ensure_output_script_hash_eq(settlement_output_index, expected_covenant_script_hash); + + let (output_asset, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(settlement_output_index); + assert!(jet::eq_256(output_asset, param::SETTLEMENT_ASSET_ID)); + + divmod_eq(settlement_amount, param::COLLATERAL_PER_CONTRACT, collateral_amount); + + ensure_output_asset_with_amount_eq(collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(premium_output_index, param::PREMIUM_ASSET_ID, premium_amount); +} + +/* + * Withdraw Path + * + * User withdraws accumulated settlement asset. + * No time restriction. + * Requires signature from USER_PUBKEY. + * No change - full withdrawal only. + * + * Layout: + * Input[0]: Settlement asset from covenant + * Output[0]: Settlement asset → user (any address) + */ +fn withdraw_path(sig: Signature) { + assert!(jet::eq_32(jet::current_index(), 0)); + + let (input_asset, input_amount): (u256, u64) = get_input_explicit_asset_amount(0); + assert!(jet::eq_256(input_asset, param::SETTLEMENT_ASSET_ID)); + + check_user_signature(sig); + + ensure_output_asset_with_amount_eq(0, param::SETTLEMENT_ASSET_ID, input_amount); +} + +/* + * Expiry Path + * + * User reclaims remaining collateral and premium after expiry. + * Only allowed after EXPIRY_TIME. + * Requires signature from USER_PUBKEY. + * No change - full reclaim only. + * + * Layout: + * Input[0]: Collateral from covenant + * Input[1]: Premium from covenant + * Output[0]: Collateral → user (any address) + * Output[1]: Premium → user (any address) + */ +fn expiry_path(sig: Signature) { + jet::check_lock_time(param::EXPIRY_TIME); + + assert!(jet::le_32(jet::current_index(), 1)); + + let expected_covenant_script_hash: u256 = get_input_script_hash(0); + + assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); + + let (collateral_asset, collateral_amount): (u256, u64) = get_input_explicit_asset_amount(0); + assert!(jet::eq_256(collateral_asset, param::COLLATERAL_ASSET_ID)); + + let (premium_asset, premium_amount): (u256, u64) = get_input_explicit_asset_amount(1); + assert!(jet::eq_256(premium_asset, param::PREMIUM_ASSET_ID)); + + check_user_signature(sig); + + ensure_output_asset_with_amount_eq(0, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(1, param::PREMIUM_ASSET_ID, premium_amount); +} + +fn main() { + match witness::PATH { + Left(params: (u64, bool)) => { + let (collateral_amount, is_change_needed): (u64, bool) = params; + exercise_path(collateral_amount, is_change_needed) + }, + Right(withdraw_or_expiry: Either) => match withdraw_or_expiry { + Left(sig: Signature) => withdraw_path(sig), + Right(sig: Signature) => expiry_path(sig), + }, + } +} diff --git a/examples/basic/simf/options.simf b/examples/basic/simf/options.simf new file mode 100644 index 0000000..e7da014 --- /dev/null +++ b/examples/basic/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/example/simf/p2pk.simf b/examples/basic/simf/p2pk.simf similarity index 100% rename from example/simf/p2pk.simf rename to examples/basic/simf/p2pk.simf diff --git a/examples/basic/src/artifacts/another_dir/another_module/bytes32_tr_storage.rs b/examples/basic/src/artifacts/another_dir/another_module/bytes32_tr_storage.rs new file mode 100644 index 0000000..07978a1 --- /dev/null +++ b/examples/basic/src/artifacts/another_dir/another_module/bytes32_tr_storage.rs @@ -0,0 +1,24 @@ +use simplex::simplex_macros::include_simf; +use simplex::simplex_sdk::program::{ArgumentsTrait, Program}; +use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +pub struct Bytes32TrStorageProgram { + program: Program, +} +impl Bytes32TrStorageProgram { + pub const SOURCE: &'static str = derived_bytes32_tr_storage::BYTES32_TR_STORAGE_CONTRACT_SOURCE; + pub fn new( + public_key: XOnlyPublicKey, + arguments: impl ArgumentsTrait + 'static, + ) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + pub fn get_program(&self) -> &Program { + &self.program + } + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } +} +include_simf!("simf/another_dir/another_module/bytes32_tr_storage.simf"); diff --git a/examples/basic/src/artifacts/another_dir/another_module/dual_currency_deposit.rs b/examples/basic/src/artifacts/another_dir/another_module/dual_currency_deposit.rs new file mode 100644 index 0000000..1cdc1ce --- /dev/null +++ b/examples/basic/src/artifacts/another_dir/another_module/dual_currency_deposit.rs @@ -0,0 +1,24 @@ +use simplex::simplex_macros::include_simf; +use simplex::simplex_sdk::program::{ArgumentsTrait, Program}; +use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +pub struct DualCurrencyDepositProgram { + program: Program, +} +impl DualCurrencyDepositProgram { + pub const SOURCE: &'static str = derived_dual_currency_deposit::DUAL_CURRENCY_DEPOSIT_CONTRACT_SOURCE; + pub fn new( + public_key: XOnlyPublicKey, + arguments: impl ArgumentsTrait + 'static, + ) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + pub fn get_program(&self) -> &Program { + &self.program + } + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } +} +include_simf!("simf/another_dir/another_module/dual_currency_deposit.simf"); diff --git a/examples/basic/src/artifacts/another_dir/another_module/mod.rs b/examples/basic/src/artifacts/another_dir/another_module/mod.rs new file mode 100644 index 0000000..121de82 --- /dev/null +++ b/examples/basic/src/artifacts/another_dir/another_module/mod.rs @@ -0,0 +1,2 @@ +pub mod dual_currency_deposit; +pub mod bytes32_tr_storage; diff --git a/examples/basic/src/artifacts/another_dir/array_tr_storage.rs b/examples/basic/src/artifacts/another_dir/array_tr_storage.rs new file mode 100644 index 0000000..6d5f8f0 --- /dev/null +++ b/examples/basic/src/artifacts/another_dir/array_tr_storage.rs @@ -0,0 +1,24 @@ +use simplex::simplex_macros::include_simf; +use simplex::simplex_sdk::program::{ArgumentsTrait, Program}; +use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +pub struct ArrayTrStorageProgram { + program: Program, +} +impl ArrayTrStorageProgram { + pub const SOURCE: &'static str = derived_array_tr_storage::ARRAY_TR_STORAGE_CONTRACT_SOURCE; + pub fn new( + public_key: XOnlyPublicKey, + arguments: impl ArgumentsTrait + 'static, + ) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + pub fn get_program(&self) -> &Program { + &self.program + } + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } +} +include_simf!("simf/another_dir/array_tr_storage.simf"); diff --git a/examples/basic/src/artifacts/another_dir/mod.rs b/examples/basic/src/artifacts/another_dir/mod.rs new file mode 100644 index 0000000..f7bff3b --- /dev/null +++ b/examples/basic/src/artifacts/another_dir/mod.rs @@ -0,0 +1,2 @@ +pub mod array_tr_storage; +pub mod another_module; diff --git a/examples/basic/src/artifacts/mod.rs b/examples/basic/src/artifacts/mod.rs new file mode 100644 index 0000000..b78b75d --- /dev/null +++ b/examples/basic/src/artifacts/mod.rs @@ -0,0 +1,4 @@ +pub mod p2pk; +pub mod options; +pub mod module; +pub mod another_dir; diff --git a/examples/basic/src/artifacts/module/mod.rs b/examples/basic/src/artifacts/module/mod.rs new file mode 100644 index 0000000..920b31d --- /dev/null +++ b/examples/basic/src/artifacts/module/mod.rs @@ -0,0 +1 @@ +pub mod option_offer; diff --git a/examples/basic/src/artifacts/module/option_offer.rs b/examples/basic/src/artifacts/module/option_offer.rs new file mode 100644 index 0000000..17e639e --- /dev/null +++ b/examples/basic/src/artifacts/module/option_offer.rs @@ -0,0 +1,24 @@ +use simplex::simplex_macros::include_simf; +use simplex::simplex_sdk::program::{ArgumentsTrait, Program}; +use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +pub struct OptionOfferProgram { + program: Program, +} +impl OptionOfferProgram { + pub const SOURCE: &'static str = derived_option_offer::OPTION_OFFER_CONTRACT_SOURCE; + pub fn new( + public_key: XOnlyPublicKey, + arguments: impl ArgumentsTrait + 'static, + ) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + pub fn get_program(&self) -> &Program { + &self.program + } + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } +} +include_simf!("simf/module/option_offer.simf"); diff --git a/example/src/artifacts/options.rs b/examples/basic/src/artifacts/options.rs similarity index 100% rename from example/src/artifacts/options.rs rename to examples/basic/src/artifacts/options.rs diff --git a/example/src/artifacts/p2pk.rs b/examples/basic/src/artifacts/p2pk.rs similarity index 100% rename from example/src/artifacts/p2pk.rs rename to examples/basic/src/artifacts/p2pk.rs diff --git a/example/src/main.rs b/examples/basic/src/main.rs similarity index 78% rename from example/src/main.rs rename to examples/basic/src/main.rs index ea4d1bb..094eaa2 100644 --- a/example/src/main.rs +++ b/examples/basic/src/main.rs @@ -2,8 +2,8 @@ mod artifacts; use simplicityhl::elements::{Script, Txid}; -use simplex::simplex_sdk::constants::{DUMMY_SIGNATURE, SimplicityNetwork}; -use simplex::simplex_sdk::provider::{EsploraProvider, ProviderTrait}; +use simplex::simplex_sdk::constants::DUMMY_SIGNATURE; +use simplex::simplex_sdk::provider::{EsploraProvider, ProviderTrait, SimplicityNetwork}; use simplex::simplex_sdk::signer::Signer; use simplex::simplex_sdk::transaction::{ FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, @@ -30,7 +30,7 @@ fn get_p2pk(signer: &Signer) -> (P2pkProgram, Script) { } fn spend_p2wpkh(signer: &Signer, provider: &EsploraProvider) -> Txid { - let (_, p2pk_script) = get_p2pk(&signer); + let (_, p2pk_script) = get_p2pk(signer); let mut ft = FinalTransaction::new(SimplicityNetwork::LiquidTestnet); @@ -49,13 +49,11 @@ fn spend_p2wpkh(signer: &Signer, provider: &EsploraProvider) -> Txid { } fn spend_p2pk(signer: &Signer, provider: &EsploraProvider) -> Txid { - let (p2pk, p2pk_script) = get_p2pk(&signer); + let (p2pk, p2pk_script) = get_p2pk(signer); let mut p2pk_utxos = provider.fetch_scripthash_utxos(&p2pk_script).unwrap(); - p2pk_utxos.retain(|el| { - el.1.asset.explicit().unwrap() == SimplicityNetwork::LiquidTestnet.policy_asset() - }); + p2pk_utxos.retain(|el| el.1.asset.explicit().unwrap() == SimplicityNetwork::LiquidTestnet.policy_asset()); let mut ft = FinalTransaction::new(SimplicityNetwork::LiquidTestnet); @@ -64,11 +62,8 @@ fn spend_p2pk(signer: &Signer, provider: &EsploraProvider) -> Txid { }; ft.add_program_input( - PartialInput::new(p2pk_utxos[0].0.clone(), p2pk_utxos[0].1.clone()), - ProgramInput::new( - Box::new(p2pk.get_program().clone()), - Box::new(witness.clone()), - ), + PartialInput::new(p2pk_utxos[0].0, p2pk_utxos[0].1.clone()), + ProgramInput::new(Box::new(p2pk.get_program().clone()), Box::new(witness.clone())), RequiredSignature::Witness("SIGNATURE".to_string()), ) .unwrap(); @@ -81,26 +76,27 @@ fn spend_p2pk(signer: &Signer, provider: &EsploraProvider) -> Txid { res } -fn main() { +fn main() -> anyhow::Result<()> { let provider = EsploraProvider::new(ESPLORA_URL.to_string()); let signer = Signer::new( "exist carry drive collect lend cereal occur much tiger just involve mean", - Box::new(provider.clone()), + provider.clone(), SimplicityNetwork::LiquidTestnet, - ) - .unwrap(); + )?; let tx = spend_p2wpkh(&signer, &provider); - provider.wait(&tx).unwrap(); + provider.wait(&tx)?; println!("Confirmed"); let tx = spend_p2pk(&signer, &provider); - provider.wait(&tx).unwrap(); + provider.wait(&tx)?; println!("Confirmed"); println!("OK"); + + Ok(()) } diff --git a/examples/basic/tests/draft_test.rs b/examples/basic/tests/draft_test.rs new file mode 100644 index 0000000..e4fa66c --- /dev/null +++ b/examples/basic/tests/draft_test.rs @@ -0,0 +1,4 @@ +#[simplex::simplex_macros::test] +fn test_invocation_tx_tracking(_context: simplex::simplex_test::TestContext) -> anyhow::Result<()> { + Ok(()) +} diff --git a/simplex/rustfmt.toml b/rustfmt.toml similarity index 100% rename from simplex/rustfmt.toml rename to rustfmt.toml diff --git a/simplex/crates/cli/Simplex.example.toml b/simplex/crates/cli/Simplex.example.toml deleted file mode 100644 index fd8148f..0000000 --- a/simplex/crates/cli/Simplex.example.toml +++ /dev/null @@ -1 +0,0 @@ -network = "liquidtestnet" \ No newline at end of file diff --git a/simplex/crates/cli/src/cache_storage.rs b/simplex/crates/cli/src/cache_storage.rs deleted file mode 100644 index 5fff7e9..0000000 --- a/simplex/crates/cli/src/cache_storage.rs +++ /dev/null @@ -1,37 +0,0 @@ -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/simplex/crates/cli/src/cli/mod.rs b/simplex/crates/cli/src/cli/mod.rs deleted file mode 100644 index c09cd8c..0000000 --- a/simplex/crates/cli/src/cli/mod.rs +++ /dev/null @@ -1,198 +0,0 @@ -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_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}; - -#[derive(Debug, Parser)] -#[command(name = "simplicity-dex")] -#[command(about = "CLI for Simplicity Options trading on Liquid")] -pub struct Cli { - #[arg(short, long, env = "SIMPLEX_CONFIG")] - pub config: Option, - - #[command(subcommand)] - pub command: commands::Command, -} - -struct TestParams { - cache_path: PathBuf, - test_path: TestPaths, - test_flags: TestFlags, -} - -enum TestPaths { - AllIntegration, - Names(Vec), -} - -impl Cli { - /// Runs the CLI command. - /// - /// # Errors - /// Returns an error if the command execution fails. - pub async fn run(&self) -> Result<(), Error> { - 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 => { - 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(); - - ctrlc::set_handler(move || { - r.store(false, Ordering::SeqCst); - }) - .expect("Error setting Ctrl-C handler"); - - let mut node = - TestClientProvider::create_default_node_with_stdin(loaded_config.test_config.elemendsd_path); - - println!("======================================"); - println!("Waiting for Ctrl-C..."); - println!("url: {}", node.rpc_url()); - let cookie_values = node.params.get_cookie_values()?.unwrap(); - println!("user: {:?}, password: {:?}", cookie_values.user, cookie_values.password); - println!("======================================"); - - while running.load(Ordering::SeqCst) {} - let _ = node.stop(); - 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(()) - } - } - } - - 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/simplex/crates/cli/src/config.rs b/simplex/crates/cli/src/config.rs deleted file mode 100644 index a03a5e8..0000000 --- a/simplex/crates/cli/src/config.rs +++ /dev/null @@ -1,241 +0,0 @@ -use serde::{Deserialize, Serialize}; -use simplex_sdk::constants::SimplicityNetwork; -use simplex_test::{ElementsDConf, RpcCreds}; -use std::io; -use std::path::{Path, PathBuf}; -use std::str::FromStr; - -pub const DEFAULT_CONFIG: &str = include_str!("../Simplex.example.toml"); -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}'")] - PathNotExist(PathBuf), - - /// Config is missing. - #[error("Config is missing in path: '{0}'")] - MissingConfig(PathBuf), -} - -#[derive(Debug, Clone)] -pub struct Config { - pub provider_config: ProviderConf, - pub test_config: ElementsDConf, - pub build_config: Option, -} - -#[derive(Debug, Clone)] -pub struct ProviderConf { - simplicity_network: SimplicityNetwork, -} - -#[derive(Debug, Default, Clone)] -pub struct ConfigOverride { - pub rpc_creds: Option, - pub network: Option, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct BuildConf { - pub compile_simf: Vec, - pub out_dir: PathBuf, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct _BuildConf { - compile_simf: Vec, - out_dir: PathBuf, -} - -impl Default for ProviderConf { - fn default() -> Self { - ProviderConf { - simplicity_network: SimplicityNetwork::LiquidTestnet, - } - } -} - -impl Config { - pub fn get_path() -> Result { - let cwd = std::env::current_dir()?; - Ok(cwd.join(CONFIG_FILENAME)) - } - - pub fn discover(cfg_override: Option<&ConfigOverride>) -> Result { - match Config::_discover() { - Ok(mut cfg) => { - 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), - } - } - - 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::PathNotExist(path)); - } - Ok(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: ProviderConf { - simplicity_network: cfg.network.unwrap_or_default().into(), - }, - 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)?, - }), - }, - }) - } -} - -#[derive(Debug, Serialize, Deserialize)] -struct _Config { - network: Option<_NetworkName>, - 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)] -#[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(), - } - } -} - -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/simplex/crates/cli/src/error.rs b/simplex/crates/cli/src/error.rs deleted file mode 100644 index aa41540..0000000 --- a/simplex/crates/cli/src/error.rs +++ /dev/null @@ -1,37 +0,0 @@ -use simplicityhl::simplicity::hex::HexToArrayError; - -/// Errors that can occur when using the Simplex CLI. -#[derive(thiserror::Error, Debug)] -pub enum Error { - /// Errors related to configuration loading or validation. - #[error("Configuration error: '{0}'")] - Config(String), - - /// Standard I/O errors. - #[error("IO error: '{0}'")] - Io(#[from] std::io::Error), - - /// Errors related to Partially Signed Elements Transactions (PSET). - #[error("PSET error: '{0}'")] - Pset(#[from] simplicityhl::elements::pset::Error), - - /// Errors when converting hex strings to byte arrays. - #[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}'")] - Test(#[from] Box), - - /// Errors when building config. - #[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/simplex/crates/cli/src/lib.rs b/simplex/crates/cli/src/lib.rs deleted file mode 100644 index eb7d538..0000000 --- a/simplex/crates/cli/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] - -pub mod cache_storage; -pub mod cli; -pub mod config; -pub mod error; -pub mod logging; diff --git a/simplex/crates/cli/src/logging.rs b/simplex/crates/cli/src/logging.rs deleted file mode 100644 index 6247051..0000000 --- a/simplex/crates/cli/src/logging.rs +++ /dev/null @@ -1,10 +0,0 @@ -use tracing_subscriber::{EnvFilter, fmt, prelude::*}; - -pub fn init() { - let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); - - tracing_subscriber::registry() - .with(fmt::layer().with_target(true)) - .with(filter) - .init(); -} diff --git a/simplex/crates/cli/tests/hello/mod.rs b/simplex/crates/cli/tests/hello/mod.rs deleted file mode 100644 index 3a426b3..0000000 --- a/simplex/crates/cli/tests/hello/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod test333; diff --git a/simplex/crates/cli/tests/hello/test333.rs b/simplex/crates/cli/tests/hello/test333.rs deleted file mode 100644 index cda6b9b..0000000 --- a/simplex/crates/cli/tests/hello/test333.rs +++ /dev/null @@ -1,32 +0,0 @@ -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/simplex/crates/cli/tests/test2.rs b/simplex/crates/cli/tests/test2.rs deleted file mode 100644 index 9542e02..0000000 --- a/simplex/crates/cli/tests/test2.rs +++ /dev/null @@ -1,25 +0,0 @@ -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/simplex/crates/macros-core/src/env/mod.rs b/simplex/crates/macros-core/src/env/mod.rs deleted file mode 100644 index 8adef87..0000000 --- a/simplex/crates/macros-core/src/env/mod.rs +++ /dev/null @@ -1,135 +0,0 @@ -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::program::{ArgumentsTrait, Program}; - use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; - - pub struct #program_name { - program: Program, - } - - impl #program_name { - pub const SOURCE: &'static str = #include_simf_module::#include_simf_source_const; - - pub fn new(public_key: XOnlyPublicKey, arguments: impl ArgumentsTrait + 'static) -> Self { - Self { - program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), - } - } - - pub fn get_program(&self) -> &Program { - &self.program - } - - pub fn get_program_mut(&mut self) -> &mut Program { - &mut self.program - } - } - - include_simf!(#pathdiff); - }; - - Ok(code) - } -} diff --git a/simplex/crates/provider/Cargo.toml b/simplex/crates/provider/Cargo.toml deleted file mode 100644 index 20fa314..0000000 --- a/simplex/crates/provider/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "simplex-provider" -version = "0.1.0" -license.workspace = true -edition.workspace = true -rust-version = "1.90.0" -description = "High-level explorer helper for standardising blocks exploration in Simplicity." -repository = "https://github.com/BlockstreamResearch/simplex" -documentation = "https://docs.rs/simplex" -keywords = ["esplora", "waterfall", "bitcoin", "elements", "explorer"] -categories = ["cryptography::cryptocurrencies", "web-programming::http-client", "wasm"] - -[lints] -workspace = true - -[dependencies] -simplicityhl = { workspace = true } -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" diff --git a/simplex/crates/provider/api-esplora.md b/simplex/crates/provider/api-esplora.md deleted file mode 100644 index 4d909f8..0000000 --- a/simplex/crates/provider/api-esplora.md +++ /dev/null @@ -1,442 +0,0 @@ -# Esplora HTTP API - -JSON over RESTful HTTP. Amounts are always represented in satoshis. - -The blockstream.info public APIs are available at: -- Bitcoin: https://blockstream.info/api/ -- Bitcoin Testnet: https://blockstream.info/testnet/api/ -- Bitcoin Signet: https://blockstream.info/signet/api/ -- Liquid: https://blockstream.info/liquid/api/ -- Liquid Testnet: https://blockstream.info/liquidtestnet/api/ - -For example: -```bash -$ curl https://blockstream.info/api/blocks/tip/hash -``` - -You can also [self-host the Esplora API server](https://github.com/Blockstream/esplora#how-to-run-the-explorer-for-bitcoin-mainnet), which provides better privacy and security. - -## Transactions - -### `GET /tx/:txid` - -Returns information about the transaction. - -Available fields: `txid`, `version`, `locktime`, `size`, `weight`, `fee`, `vin`, `vout` and `status` -(see [transaction format](#transaction-format) for details). - -### `GET /tx/:txid/status` - -Returns the transaction confirmation status. - -Available fields: `confirmed` (boolean), `block_height` (optional) and `block_hash` (optional). - -### `GET /tx/:txid/hex` -### `GET /tx/:txid/raw` - -Returns the raw transaction in hex or as binary data. - -### `GET /tx/:txid/merkleblock-proof` - -Returns a merkle inclusion proof for the transaction using -[bitcoind's merkleblock](https://bitcoin.org/en/glossary/merkle-block) format. - -*Note:* This endpoint is not currently available for Liquid/Elements-based chains. - -### `GET /tx/:txid/merkle-proof` - -Returns a merkle inclusion proof for the transaction using -[Electrum's `blockchain.transaction.get_merkle`](https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get-merkle) -format. - -### `GET /tx/:txid/outspend/:vout` - -Returns the spending status of a transaction output. - -Available fields: `spent` (boolean), `txid` (optional), `vin` (optional) and `status` (optional, the status of the spending tx). - -### `GET /tx/:txid/outspends` - -Returns the spending status of all transaction outputs. - -### `POST /tx` - -Broadcast a raw transaction to the network. - -The transaction should be provided as hex in the request body. -The `txid` will be returned on success. - -### `POST /txs/package` - -Broadcast a package of raw transactions to the network. - -A transaction package is a group of related transactions that may depend on each other (e.g., a child transaction spending outputs from an unconfirmed parent transaction). This is useful for CPFP (Child Pays For Parent) and other scenarios where transactions need to be evaluated together. - -The request body should contain a JSON array of transaction hex strings. - -Example request body: -```json -["02000000...", "02000000..."] -``` - -Returns a JSON object containing the package acceptance result. On success, returns information about each transaction in the package. - -*Note:* This endpoint requires Bitcoin Core 28.0 or later. - -## Addresses - -### `GET /address/:address` -### `GET /scripthash/:hash` - -Get information about an address/scripthash. - -Available fields: `address`/`scripthash`, `chain_stats` and `mempool_stats`. - -`{chain,mempool}_stats` each contain an object with `tx_count`, `funded_txo_count`, `funded_txo_sum`, `spent_txo_count` and `spent_txo_sum`. - -Elements-based chains don't have the `{funded,spent}_txo_sum` fields. - -### `GET /address/:address/txs` -### `GET /scripthash/:hash/txs` - -Get transaction history for the specified address/scripthash, sorted with newest first. - -Returns up to 50 mempool transactions plus the first 25 confirmed transactions. -You can request more confirmed transactions using `:last_seen_txid`(see below). - -### `GET /address/:address/txs/chain[/:last_seen_txid]` -### `GET /scripthash/:hash/txs/chain[/:last_seen_txid]` - -Get confirmed transaction history for the specified address/scripthash, sorted with newest first. - -Returns 25 transactions per page. More can be requested by specifying the last txid seen by the previous query. - -### `GET /address/:address/txs/mempool` -### `GET /scripthash/:hash/txs/mempool` - -Get unconfirmed transaction history for the specified address/scripthash. - -Returns up to 50 transactions (no paging). - -### `GET /address/:address/utxo` -### `GET /scripthash/:hash/utxo` - -Get the list of unspent transaction outputs associated with the address/scripthash. - -Available fields: `txid`, `vout`, `value` and `status` (with the status of the funding tx). - -Elements-based chains have a `valuecommitment` field that may appear in place of `value`, plus the following additional fields: `asset`/`assetcommitment`, `nonce`/`noncecommitment`, `surjection_proof` and `range_proof`. - -### `GET /address-prefix/:prefix` - -Search for addresses beginning with `:prefix`. - -Returns a JSON array with up to 10 results. - -## Blocks - -### `GET /block/:hash` - -Returns information about a block. - -Available fields: `id`, `height`, `version`, `timestamp`, `mediantime`, `bits`, `nonce`, `merkle_root`, `tx_count`, `size`, `weight`, and `previousblockhash`. -Elements-based chains have an additional `proof` field. -See [block format](#block-format) for more details. - -The response from this endpoint can be cached indefinitely. - -### `GET /block/:hash/header` - -Returns the hex-encoded block header. - -The response from this endpoint can be cached indefinitely. - -### `GET /block/:hash/status` - -Returns the block status. - -Available fields: `in_best_chain` (boolean, false for orphaned blocks), `next_best` (the hash of the next block, only available for blocks in the best chain). - -### `GET /block/:hash/txs[/:start_index]` - -Returns a list of transactions in the block (up to 25 transactions beginning at `start_index`). - -*Note:* The `start_index` value must be a multiple of 25. - -The response from this endpoint can be cached indefinitely. - -### `GET /block/:hash/txids` - -Returns a list of all txids in the block. - -The response from this endpoint can be cached indefinitely. - -### `GET /block/:hash/txid/:index` - -Returns the transaction at index `:index` within the specified block. - -The response from this endpoint can be cached indefinitely. - -### `GET /block/:hash/raw` - -Returns the raw block representation in binary. - -The response from this endpoint can be cached indefinitely. - -### `GET /block-height/:height` - -Returns the hash of the block currently at `height`. - -### `GET /blocks[/:start_height]` - -Returns the 10 newest blocks starting at the tip or at `start_height` if specified. - -### `GET /blocks/tip/height` - -Returns the height of the last block. - -### `GET /blocks/tip/hash` - -Returns the hash of the last block. - -## Mempool - -### `GET /mempool` - -Get mempool backlog statistics. Returns an object with: - -- `count`: the number of transactions in the mempool - -- `vsize`: the total size of mempool transactions in virtual bytes - -- `total_fee`: the total fee paid by mempool transactions in satoshis - -- `fee_histogram`: mempool fee-rate distribution histogram - - An array of `(feerate, vsize)` tuples, where each entry's `vsize` is the total vsize of transactions - paying more than `feerate` but less than the previous entry's `feerate` (except for the first entry, which has no upper bound). - This matches the format used by the Electrum RPC protocol for `mempool.get_fee_histogram`. - -Example output: - -``` -{ - "count": 8134, - "vsize": 3444604, - "total_fee":29204625, - "fee_histogram": [[53.01, 102131], [38.56, 110990], [34.12, 138976], [24.34, 112619], [3.16, 246346], [2.92, 239701], [1.1, 775272]] -} -``` - -> In this example, there are transactions weighting a total of 102,131 vbytes that are paying more than 53 sat/vB, -110,990 vbytes of transactions paying between 38 and 53 sat/vB, 138,976 vbytes paying between 34 and 38, etc. - - -### `GET /mempool/txids` - -Get the full list of txids in the mempool as an array. - -The order of the txids is arbitrary and does not match bitcoind's. - -### `GET /mempool/recent` - -Get a list of the last 10 transactions to enter the mempool. - -Each transaction object contains simplified overview data, with the following fields: `txid`, `fee`, `vsize` and `value` - -## Fee estimates - -### `GET /fee-estimates` - -Get an object where the key is the confirmation target (in number of blocks) -and the value is the estimated feerate (in sat/vB). - -The available confirmation targets are 1-25, 144, 504 and 1008 blocks. - -For example: `{ "1": 87.882, "2": 87.882, "3": 87.882, "4": 87.882, "5": 81.129, "6": 68.285, ..., "144": 1.027, "504": 1.027, "1008": 1.027 }` - -## Assets (Elements/Liquid only) - -### `GET /asset/:asset_id` - -Get information about an asset. - -For the network's native asset (i.e. LBTC in Liquid), returns an object with: - -- `asset_id` -- `chain_stats` and `mempool_stats`, each with: - - `tx_count` - - `peg_in_count` - - `peg_in_amount` - - `peg_out_amount` - - `peg_out_count` - - `burn_count` - - `burned_amount` - -For user-issued assets, returns an object with: - -- `asset_id` -- `issuance_txin`: the issuance transaction input - - `txid` - - `vin` -- `issuance_prevout`: the previous output spent for the issuance - - `txid` - - `vout` -- `status`: the confirmation status of the initial asset issuance transaction -- `contract_hash`: the contract hash committed as the issuance entropy -- `reissuance_token`: the asset id of the reissuance token -- `chain_stats` and `mempool_stats`, each with: - - `tx_count`: the number of transactions associated with this asset (does not include confidential transactions) - - `issuance_count`: the number of (re)issuance transactions - - `issued_amount`: the total known amount issued (should be considered a minimum bound when `has_blinded_issuances` is true) - - `burned_amount`: the total amount provably burned - - `has_blinded_issuances`: whether at least one of the (re)issuances were blind - - `reissuance_tokens`: the number of reissuance tokens - - `burned_reissuance_tokens`: the number of reissuance tokens burned - -If the asset is available on the registry, the following fields are returned as well: - -- `contract`: the full json contract json committed in the issuance -- `entity`: the entity linked to this asset. the only available type is currently `domain`, which is encoded as `{ "domain": "foobar.com>" }` (required) -- `ticker`: a 3-5 characters ticker associated with the asset (optional) -- `precision`: the number of decimal places for units of this asset (defaults to 0) -- `name`: a description for the asset (up to 255 characters) - -Example native asset: - -``` -{ - "asset_id": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d", - "chain_stats": {"tx_count": 54, "peg_in_count": 2, "peg_in_amount": 1600000000, "peg_out_count": 51, "peg_out_amount": 250490000, "burn_count":0, "burned_amount": 0 }, - "mempool_stats": {"tx_count": 3, "peg_in_count": 0, "peg_in_amount": 0, "peg_out_count": 3, "peg_out_amount": 70020000, "burn_count": 0, "burned_amount": 0 } -} -``` - -Example user-issued asset: - -``` -{ - "asset_id": "d8a317ce2c14241192cbb3ebdb9696250ca1251a58ba6251c29fcfe126c9ca1f", - "issuance_txin":{ "txid": "39affca34bd51ed080f89f1e7a5c7a49d6d9e4779c84424ae50df67dd60dcaf7", "vin": 0}, - "issuance_prevout": { "txid": "0cdd74c540af637d5a3874ce8500891fd8e94ec8e3d5d436d86e87b6759a7674", "vout": 0 }, - "reissuance_token": "eb8b210d42566699796dbf78649120fd5c9d9b04cabc8f480856e04bd5e9fc22", - "contract_hash": "025d983cc774da665f412ccc6ccf51cb017671c2cb0d3c32d10d50ffdf0a57de", - "status": { "confirmed": true, "block_height": 105, "block_hash": "7bf84f2aea30b02981a220943f543a6d6e7ac646d59ef76cff27dca8d27b2b67", "block_time": 1586248729 }, - "chain_stats": { "tx_count": 1, "issuance_count": 1, "issued_amount": 0, "burned_amount": 0, "has_blinded_issuances": true, "reissuance_tokens": 0, "burned_reissuance_tokens": 0 }, - "mempool_stats": { "tx_count": 0, "issuance_count": 0, "issued_amount": 0, "burned_amount": 0, "has_blinded_issuances": false, "reissuance_tokens": null, "burned_reissuance_tokens": 0 } -} -``` - -### `GET /asset/:asset_id/txs` -### `GET /asset/:asset_id/txs/mempool` -### `GET /asset/:asset_id/txs/chain[/:last_seen]` - -Get transactions associated with the specified asset. - -For the network's native asset, returns a list of peg in, peg out and burn transactions. - -For user-issued assets, returns a list of issuance, reissuance and burn transactions. - -Does not include regular transactions transferring this asset. - -### `GET /asset/:asset_id/supply` -### `GET /asset/:asset_id/supply/decimal` - -Get the current total supply of the specified asset. - -For the native asset (LBTC), this is calculated as `{chain,mempool}_stats.peg_in_amount - {chain,mempool}_stats.peg_out_amount - {chain,mempool}_stats.burned_amount`. - -For issued assets, this is calculated as `{chain,mempool}_stats.issued_amount - {chain,mempool}_stats.burned_amount`. - -Not available for assets with blinded issuances. - -If `/decimal` is specified, returns the supply as a decimal according to the asset's divisibility. -Otherwise, returned in base units. - -### `GET /assets/registry` - -Get the list of issued assets in the asset registry. - -Query string parameters: - -- `start_index`: the start index to use for paging. defaults to 0. -- `limit`: maximum number of assets to return. defaults to 25, maximum 100. -- `sort_field`: field to sort assets by. one of `name`, `ticker` or `domain`. defaults to `ticker`. -- `sort_dir`: sorting direction. one of `asc` or `desc`. defaults to `asc`. - -Assets are returned in the same format as in `GET /asset/:asset_id`. - - -The total number of results will be returned as the `x-total-results` header. - -## Transaction format - -- `txid` -- `version` -- `locktime` -- `size` -- `weight` -- `fee` -- `vin[]` - - `txid` - - `vout` - - `is_coinbase` - - `scriptsig` - - `scriptsig_asm` - - `inner_redeemscript_asm` - - `inner_witnessscript_asm` - - `sequence` - - `witness[]` - - `prevout` (previous output in the same format as in `vout` below) - - *(Elements only)* - - `is_pegin` - - `issuance` (available for asset issuance transactions, `null` otherwise) - - `asset_id` - - `is_reissuance` - - `asset_id` - - `asset_blinding_nonce` - - `asset_entropy` - - `contract_hash` - - `assetamount` or `assetamountcommitment` - - `tokenamount` or `tokenamountcommitment` -- `vout[]` - - `scriptpubkey` - - `scriptpubkey_asm` - - `scriptpubkey_type` - - `scriptpubkey_address` - - `value` - - *(Elements only)* - - `valuecommitment` - - `asset` or `assetcommitment` - - `pegout` (available for peg-out outputs, `null` otherwise) - - `genesis_hash` - - `scriptpubkey` - - `scriptpubkey_asm` - - `scriptpubkey_address` -- `status` - - `confirmed` (boolean) - - `block_height` (available for confirmed transactions, `null` otherwise) - - `block_hash` (available for confirmed transactions, `null` otherwise) - - `block_time` (available for confirmed transactions, `null` otherwise) - -## Block format - -- `id` -- `height` -- `version` -- `timestamp` -- `bits` -- `nonce` -- `difficulty` -- `merkle_root` -- `tx_count` -- `size` -- `weight` -- `previousblockhash` -- `mediantime` (median time-past) -- *(Elements only)* -- `proof` - - `challenge` - - `challenge_asm` - - `solution` - - `solution_asm` \ No newline at end of file diff --git a/simplex/crates/provider/api-waterfall.md b/simplex/crates/provider/api-waterfall.md deleted file mode 100644 index a9fdf5d..0000000 --- a/simplex/crates/provider/api-waterfall.md +++ /dev/null @@ -1,364 +0,0 @@ -# Waterfalls API Documentation - -This document describes all available API endpoints for the Waterfalls server, which provides blockchain data indexing and querying capabilities for Bitcoin and Elements/Liquid networks. - -## Waterfalls Endpoints - -These endpoints provide transaction history and UTXO data for descriptors or addresses. Available in both JSON and CBOR formats. - -### Waterfalls Data (JSON) -``` -GET /v2/waterfalls? -``` - -### Waterfalls Data (CBOR) -``` -GET /v2/waterfalls.cbor? -``` - -**Note:** v1 exists for compatibility and v3 endpoints have been removed and return 404. - -**Query Parameters:** - -- `descriptor` (string): Bitcoin/Elements descriptor (plain text or encrypted with server key) - - Cannot be used together with `addresses` - - Supports encryption using age encryption with server's public key - - Network validation: mainnet descriptors (xpub) cannot be used on testnet/regtest - -- `addresses` (string): Comma-separated list of Bitcoin/Elements addresses - - Cannot be used together with `descriptor` - - Maximum addresses limited by server configuration - - Addresses cannot be blinded (confidential) - -- `page` (integer, optional): Page number for pagination (default: 0) - -- `to_index` (integer, optional): Maximum derivation index for descriptors (default: 0) - -- `utxo_only` (boolean, optional): Return only unspent outputs (default: false) - -**Response Format (JSON):** -```json -{ - "txs_seen": { - "descriptor_or_addresses": [ - { - "txid": "transaction_id", - "height": 12345, - "block_hash": "block_hash", - "block_timestamp": 1234567890, - "v": 1 - } - ] - }, - "page": 0, - "tip": "current_tip_hash" -} -``` - -**Differences between v1 and v2:** -- v2 includes `tip` field in response - -### Waterfalls Data with Full Tip Metadata (v4) - -``` -GET /v4/waterfalls? -GET /v4/waterfalls.cbor? -``` - -The v4 endpoints accept the same query parameters as v2 but return extended tip metadata including block height. This is particularly useful for Bitcoin, where the block height cannot be derived from the header alone. - -**Response Format (JSON):** -```json -{ - "txs_seen": { - "descriptor_or_addresses": [ - { - "txid": "transaction_id", - "height": 12345, - "block_hash": "block_hash", - "block_timestamp": 1234567890, - "v": 1 - } - ] - }, - "page": 0, - "tip_meta": { - "b": "current_tip_block_hash", - "t": 1234567890, - "h": 876543 - } -} -``` - -**Differences between v2 and v4:** -- v4 returns `tip_meta` object instead of `tip` hash string -- `tip_meta` contains: - - `b` (string): Block hash of the current tip - - `t` (integer): Block timestamp (Unix epoch seconds) - - `h` (integer): Block height - -### Last Used Index -``` -GET /v1/last_used_index?descriptor= -``` - -Returns the highest derivation index that has been used (has transaction history) for both external and internal chains. This is useful for quickly determining the next unused address without downloading full transaction history. - -**Query Parameters:** - -- `descriptor` (string, required): Bitcoin/Elements descriptor (plain text or encrypted with server key) - - Supports encryption using age encryption with server's public key - - Network validation: mainnet descriptors (xpub) cannot be used on testnet/regtest - -**Response Format (JSON):** -```json -{ - "external": 42, - "internal": 15, - "tip": "current_tip_hash" -} -``` - -**Response Fields:** - -- `external` (integer or null): Last used index on the external (receive) chain, or null if no addresses have been used -- `internal` (integer or null): Last used index on the internal (change) chain, or null if no addresses have been used -- `tip` (string, optional): Current blockchain tip hash for reference - -**Use Case:** - -This endpoint is optimized for applications that only need to know the next unused address index (e.g., Point of Sale systems) without the overhead of downloading full transaction history or computing balances. - -**Example:** - -To get the next unused external address, use index `external + 1` (or index `0` if `external` is null). - -## Base Endpoints - -### Server Information - -#### Get Server Public Key -``` -GET /v1/server_recipient -``` -Returns the server's public key for encryption purposes. - -**Response:** Plain text string containing the public key - -#### Get Server Address -``` -GET /v1/server_address -``` -Returns the server's Bitcoin/Elements address for message signing verification. - -**Response:** Plain text string containing the address - -#### Time Since Last Block -``` -GET /v1/time_since_last_block -``` -Returns the time elapsed since the last block and a freshness indicator. - -**Response:** Plain text describing time elapsed and status (e.g., "120 seconds since last block, less than 10 minutes") - -#### Build Information -``` -GET /v1/build_info -``` -Returns build and version information including git commit ID. - -**Response (JSON):** -```json -{ - "version": "0.9.4", - "git_commit": "a1b2c3d4e5f6789..." -} -``` - - - -## Blockchain Data Endpoints - -### Get Tip Hash -``` -GET /blocks/tip/hash -``` -Returns the hash of the current blockchain tip. - -**Response:** Plain text string containing the block hash - -### Get Block Hash by Height -``` -GET /block-height/{height} -``` -Returns the block hash for a specific block height. - -**Parameters:** -- `height` (integer): Block height - -**Response:** Plain text string containing the block hash, or 404 if not found - -### Get Block Header -``` -GET /block/{hash}/header -``` -Returns the block header for a specific block hash. - -**Parameters:** -- `hash` (string): Block hash - -**Response:** Hex-encoded block header, or 404 if not found - -### Get Raw Transaction -``` -GET /tx/{txid}/raw -``` -Returns the raw transaction data. - -**Parameters:** -- `txid` (string): Transaction ID - -**Response:** Binary transaction data (application/octet-stream) - -### Get Address Transactions -``` -GET /address/{address}/txs -``` -Returns transaction history for a specific address in Esplora-compatible format. - -**Parameters:** -- `address` (string): Bitcoin/Elements address - -**Response (JSON):** -```json -[ - { - "txid": "transaction_id", - "status": { - "block_height": 12345, - "block_hash": "block_hash_or_null" - } - } -] -``` - -## Transaction Operations - -### Broadcast Transaction -``` -POST /tx -``` -Broadcasts a raw transaction to the network. - -**Request Body:** Raw transaction hex string - -**Response:** -- Success (200): Transaction ID -- Error (400): Error message - -## Monitoring - -### Prometheus Metrics -``` -GET /metrics -``` -Returns Prometheus-formatted metrics for monitoring. - -**Response:** Text format metrics (text/plain) - -## Error Responses - -The API returns appropriate HTTP status codes: - -- `200 OK`: Successful request -- `400 Bad Request`: Invalid parameters or transaction broadcast failure -- `404 Not Found`: Resource not found (block, transaction, endpoint) -- `422 Unprocessable Entity`: Decryption failure (wrong identity used for encrypted descriptor) -- `500 Internal Server Error`: Server error - -Common error conditions: -- `AtLeastOneFieldMandatory`: Neither descriptor nor addresses provided -- `CannotSpecifyBothDescriptorAndAddresses`: Both descriptor and addresses provided -- `WrongNetwork`: Network mismatch (e.g., mainnet descriptor on testnet) -- `TooManyAddresses`: Exceeds maximum address limit -- `AddressCannotBeBlinded`: Blinded/confidential address provided -- `InvalidTxid`: Malformed transaction ID -- `InvalidBlockHash`: Malformed block hash -- `CannotFindTx`: Transaction not found -- `CannotFindBlockHeader`: Block header not found - -## Client Usage Examples - -The codebase includes a `WaterfallClient` class with the following methods: - -### Waterfalls Queries -```rust -// Query with descriptor (v2, JSON) -let (response, headers) = client.waterfalls(descriptor).await?; - -// Query with addresses -let (response, headers) = client.waterfalls_addresses(&addresses).await?; - -// Version-specific queries -let (response, headers) = client.waterfalls_v1(descriptor).await?; -let (response, headers) = client.waterfalls_v2(descriptor).await?; - -// UTXO-only query -let (response, headers) = client.waterfalls_v2_utxo_only(descriptor).await?; - -// Generic version with all parameters -let (response, headers) = client.waterfalls_version( - descriptor, - version, - page, - to_index, - utxo_only -).await?; -``` - -### Blockchain Data -```rust -// Get current tip -let tip_hash = client.tip_hash().await?; - -// Get block header -let header = client.header(block_hash).await?; - -// Get transaction -let transaction = client.tx(txid).await?; - -// Get address transactions -let txs_json = client.address_txs(&address).await?; -``` - -### Server Information -```rust -// Get server public key -let recipient = client.server_recipient().await?; - -// Get server address -let address = client.server_address().await?; -``` - -### Transaction Broadcasting -```rust -// Broadcast transaction -let txid = client.broadcast(&transaction).await?; -``` - -## Security Features - -- **Message Signing**: Responses include cryptographic signatures in headers: - - `X-Content-Signature`: Message signature - - `X-Content-Digest`: Content digest - - `X-Server-Address`: Server address for verification - -- **Encryption Support**: Descriptors can be encrypted using age encryption with the server's public key - -- **CORS Support**: Configurable CORS headers for web client access - -## Rate Limiting and Caching - -- Responses include appropriate cache control headers -- Address and transaction endpoints have long cache times for confirmed data -- Mempool/tip data has shorter cache times or no caching diff --git a/simplex/crates/provider/src/elements_rpc/mod.rs b/simplex/crates/provider/src/elements_rpc/mod.rs deleted file mode 100644 index 667d82d..0000000 --- a/simplex/crates/provider/src/elements_rpc/mod.rs +++ /dev/null @@ -1,419 +0,0 @@ -mod types; - -pub use types::*; - -use crate::error::ExplorerError; -use bitcoind::bitcoincore_rpc::{Auth, Client, RpcApi, bitcoin}; -use electrsd::bitcoind; -use serde_json::Value; -use simplicityhl::elements::{Address, AssetId, BlockHash, Txid}; -use std::str::FromStr; - -pub struct ElementsRpcClient { - inner: Client, - #[allow(unused)] - auth: Auth, - #[allow(unused)] - url: String, -} - -impl ElementsRpcClient { - pub fn new(url: &str, auth: Auth) -> Result { - let inner = Client::new(url, auth.clone())?; - inner.ping()?; - Ok(Self { - inner, - auth, - url: url.to_string(), - }) - } - - pub fn new_from_credentials(url: &str, user: &str, pass: &str) -> Result { - let auth = Auth::UserPass(user.to_string(), pass.to_string()); - Self::new(url, auth) - } - - pub fn client(&self) -> &Client { - &self.inner - } -} - -impl ElementsRpcClient { - pub fn height(client: &Client) -> Result { - const METHOD: &str = "getblockcount"; - - client - .call::(METHOD, &[])? - .as_u64() - .ok_or_else(|| ExplorerError::ElementsRpcUnexpectedReturn(METHOD.into())) - } - - pub fn blockchain_info(client: &Client) -> Result { - const METHOD: &str = "getblockchaininfo"; - - Ok(client.call::(METHOD, &[])?) - } - - pub fn sendtoaddress( - client: &Client, - address: &Address, - satoshi: u64, - asset: Option, - ) -> Result { - const METHOD: &str = "sendtoaddress"; - - let btc = sat2btc(satoshi); - let r = match asset { - Some(asset) => client.call::( - METHOD, - &[ - address.to_string().into(), - btc.into(), - "".into(), - "".into(), - false.into(), - false.into(), - 1.into(), - "UNSET".into(), - false.into(), - asset.to_string().into(), - ], - )?, - None => client.call::(METHOD, &[address.to_string().into(), btc.into()])?, - }; - Ok(Txid::from_str(r.as_str().unwrap()).unwrap()) - } - - pub fn rescanblockchain(client: &Client, start: Option, stop: Option) -> Result<(), ExplorerError> { - const METHOD: &str = "rescanblockchain"; - - let mut args = Vec::with_capacity(2); - if start.is_some() { - args.push(start.into()); - } - if stop.is_some() { - args.push(stop.into()); - } - client.call::(METHOD, &args)?; - Ok(()) - } - - pub fn getnewaddress(client: &Client, label: &str, kind: AddressType) -> Result { - const METHOD: &str = "getnewaddress"; - - let addr: Value = client.call(METHOD, &[label.into(), kind.to_string().into()])?; - Ok(Address::from_str(addr.as_str().unwrap()).unwrap()) - } - - pub fn generate_blocks(client: &Client, block_num: u32) -> Result<(), ExplorerError> { - const METHOD: &str = "generatetoaddress"; - - let address = Self::getnewaddress(client, "", AddressType::default())?.to_string(); - client.call::(METHOD, &[block_num.into(), address.into()])?; - Ok(()) - } - - pub fn sweep_initialfreecoins(client: &Client) -> Result<(), ExplorerError> { - const METHOD: &str = "sendtoaddress"; - - let address = Self::getnewaddress(client, "", AddressType::default())?; - client.call::( - METHOD, - &[ - address.to_string().into(), - "21".into(), - "".into(), - "".into(), - true.into(), - ], - )?; - Ok(()) - } - - pub fn issueasset(client: &Client, satoshi: u64) -> Result { - const METHOD: &str = "issueasset"; - - let btc = sat2btc(satoshi); - let r = client.call::(METHOD, &[btc.into(), 0.into()])?; - let asset = r.get("asset").unwrap().as_str().unwrap().to_string(); - Ok(AssetId::from_str(&asset)?) - } - - /// Get the genesis block hash from the running elementsd node. - /// - /// Could differ from the hardcoded one because parameters like `-initialfreecoins` - /// change the genesis hash. - pub fn genesis_block_hash(client: &Client) -> Result { - Self::block_hash(client, 0) - } - - pub fn block_hash(client: &Client, height: u64) -> Result { - const METHOD: &str = "getblockhash"; - - let raw: Value = client.call(METHOD, &[height.into()])?; - Ok(BlockHash::from_str(raw.as_str().unwrap())?) - } - - pub fn getpeginaddress(client: &Client) -> Result<(bitcoin::Address, String), ExplorerError> { - #[derive(serde::Deserialize)] - struct GetpeginaddressResult { - getpeginaddress: String, - claim_script: String, - } - - const METHOD: &str = "getpeginaddress"; - let value: GetpeginaddressResult = client.call(METHOD, &[]).unwrap(); - - let mainchain_address = bitcoin::Address::from_str(&value.getpeginaddress) - .unwrap() - .assume_checked(); - - Ok((mainchain_address, value.claim_script)) - } - - pub fn raw_createpsbt(client: &Client, inputs: Value, outputs: Value) -> Result { - const METHOD: &str = "createpsbt"; - - let psbt: serde_json::Value = client.call(METHOD, &[inputs, outputs, 0.into(), false.into()])?; - Ok(psbt.as_str().unwrap().to_string()) - } - - pub fn expected_next(client: &Client, base64: &str) -> Result { - const METHOD: &str = "analyzepsbt"; - - let value: serde_json::Value = client.call(METHOD, &[base64.into()])?; - Ok(value.get("next").unwrap().as_str().unwrap().to_string()) - } - - pub fn walletprocesspsbt(client: &Client, psbt: &str) -> Result { - const METHOD: &str = "walletprocesspsbt"; - - let value: serde_json::Value = client.call(METHOD, &[psbt.into()])?; - Ok(value.get("psbt").unwrap().as_str().unwrap().to_string()) - } - - pub fn finalizepsbt(client: &Client, psbt: &str) -> Result { - const METHOD: &str = "finalizepsbt"; - - let value: serde_json::Value = client.call(METHOD, &[psbt.into()])?; - assert!(value.get("complete").unwrap().as_bool().unwrap()); - Ok(value.get("hex").unwrap().as_str().unwrap().to_string()) - } - - 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()])?; - Ok(value.as_str().unwrap().to_string()) - } - - pub fn testmempoolaccept(client: &Client, tx: &str) -> Result { - const METHOD: &str = "testmempoolaccept"; - - let value: serde_json::Value = client.call(METHOD, &[[tx].into()])?; - Ok(value.as_array().unwrap()[0].get("allowed").unwrap().as_bool().unwrap()) - } - - pub fn create_wallet(client: &Client, wallet_name: Option) -> Result { - const METHOD: &str = "createwallet"; - - #[derive(serde::Deserialize)] - pub struct CreatewalletResult { - name: String, - #[allow(dead_code)] - warning: String, - } - - let value: CreatewalletResult = client.call( - METHOD, - &[ - wallet_name.unwrap_or("my_wallet_name".to_string()).into(), - false.into(), - false.into(), - "".into(), - false.into(), - false.into(), - true.into(), - false.into(), - ], - )?; - Ok(WalletMeta { name: value.name }) - } - - pub fn getbalance(client: &Client, conf: Option) -> Result { - const METHOD: &str = "getbalance"; - - Ok(client.call::(METHOD, &["*".into(), conf.unwrap_or_default().into()])?) - } - - pub fn listunspent( - client: &Client, - min_conf: Option, - max_conf: Option, - addresses: Option>, - include_unsafe: Option, - query_options: Option, - ) -> Result, ExplorerError> { - const METHOD: &str = "listunspent"; - - let mut args = Vec::new(); - args.push(min_conf.unwrap_or(1).into()); - args.push(max_conf.unwrap_or(9_999_999).into()); - - if let Some(addrs) = addresses { - args.push(addrs.into()); - } else { - args.push(serde_json::to_value(Vec::::new()).unwrap()); - } - - if include_unsafe.is_some() || query_options.is_some() { - args.push(include_unsafe.unwrap_or(true).into()); - } - - if let Some(opts) = query_options { - args.push(serde_json::to_value(opts).unwrap()); - } - - Ok(client.call::>(METHOD, &args)?) - } - - pub fn importaddress( - client: &Client, - address: &str, - label: Option<&str>, - rescan: Option, - p2sh: Option, - ) -> Result<(), ExplorerError> { - const METHOD: &str = "importaddress"; - - let mut args = vec![address.into()]; - - if let Some(lbl) = label { - args.push(lbl.into()); - } else { - args.push("".into()); - } - - if rescan.is_some() || p2sh.is_some() { - args.push(rescan.unwrap_or(true).into()); - } - - if let Some(p2sh_val) = p2sh { - args.push(p2sh_val.into()); - } - - client.call::(METHOD, &args)?; - Ok(()) - } - - pub fn validateaddress(client: &Client, address: &str) -> Result { - const METHOD: &str = "validateaddress"; - - let value: serde_json::Value = client.call(METHOD, &[address.into()])?; - value - .get("isvalid") - .and_then(serde_json::Value::as_bool) - .ok_or_else(|| ExplorerError::ElementsRpcUnexpectedReturn(METHOD.into())) - } - - pub fn scantxoutset( - client: &Client, - action: &str, - scanobjects: Option>, - ) -> Result { - const METHOD: &str = "scantxoutset"; - - let mut args = vec![action.into()]; - - match action { - "start" => { - if let Some(objects) = scanobjects { - 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(), - )); - } - } - "abort" | "status" => { - if scanobjects.is_some() { - return Err(ExplorerError::InvalidInput(format!( - "scantxoutset '{action}' action does not accept scanobjects", - ))); - } - } - _ => { - return Err(ExplorerError::InvalidInput(format!( - "unknown scantxoutset action: {action}" - ))); - } - } - - let response = client.call::(METHOD, &args)?; - dbg!("response: {}", response.to_string()); - 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 { - let amount = bitcoin::Amount::from_sat(sat); - amount.to_string_in(bitcoin::amount::Denomination::Bitcoin) -} diff --git a/simplex/crates/provider/src/elements_rpc/types.rs b/simplex/crates/provider/src/elements_rpc/types.rs deleted file mode 100644 index bcb358a..0000000 --- a/simplex/crates/provider/src/elements_rpc/types.rs +++ /dev/null @@ -1,328 +0,0 @@ -use serde::ser::Error; -use serde_json::Value; -use std::collections::HashMap; - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct GetBlockchainInfo { - pub chain: String, - pub blocks: u64, - pub headers: u64, - pub bestblockhash: String, - // pub difficulty: f64, - pub time: u64, - pub mediantime: u64, - pub verificationprogress: f64, - pub initialblockdownload: bool, - // pub chainwork: String, - pub size_on_disk: u64, - pub pruned: bool, - - // Elements specific fields - pub current_params_root: String, - // pub signblock_asm: String, - // pub signblock_hex: String, - pub current_signblock_asm: String, - pub current_signblock_hex: String, - pub max_block_witness: u64, - pub epoch_length: u64, - pub total_valid_epochs: u64, - pub epoch_age: u64, - - // Using Value here as the documentation describes it generically as "extension fields" - pub extension_space: Vec, - - // Optional pruning fields (only present if pruning is enabled) - #[serde(skip_serializing_if = "Option::is_none")] - pub pruneheight: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub automatic_pruning: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub prune_target_size: Option, - - // Softforks are deprecated but might still be present if configured - #[serde(skip_serializing_if = "Option::is_none")] - pub softforks: Option>, - - pub warnings: String, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct Softfork { - #[serde(rename = "type")] - pub fork_type: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub height: Option, - pub active: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub bip9: Option, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct SoftforkBip9 { - #[serde(skip_serializing_if = "Option::is_none")] - pub bit: Option, - pub start_time: u64, - pub timeout: u64, - pub min_activation_height: u64, - pub status: String, - pub since: u64, - pub status_next: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub statistics: Option, - pub signalling: String, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct SoftforkStatistics { - pub period: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub threshold: Option, - pub elapsed: u64, - pub count: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub possible: Option, -} - -pub struct WalletMeta { - pub name: String, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct GetBalance { - #[serde(rename = "mine")] - pub mine: BalanceDetails, - #[serde(rename = "watchonly")] - pub watchonly: Option, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct BalanceDetails { - pub trusted: f64, - pub untrusted_pending: f64, - pub immature: f64, -} - -#[derive(Default, Debug, Clone, Copy)] -pub enum AddressType { - Legacy, - #[default] - P2shSegwit, - Bech32, - Bech32m, -} - -impl std::fmt::Display for AddressType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let str = match self { - AddressType::Legacy => "legacy".to_string(), - AddressType::P2shSegwit => "p2sh-segwit".to_string(), - AddressType::Bech32 => "bech32".to_string(), - AddressType::Bech32m => "bech32m".to_string(), - }; - write!(f, "{str}") - } -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct ListUnspent { - pub txid: String, - pub vout: u32, - pub address: String, - pub label: Option, - #[serde(rename = "scriptPubKey")] - pub script_pubkey: String, - pub amount: f64, - pub amountcommitment: Option, - pub asset: Option, - pub assetcommitment: Option, - pub confirmations: u64, - pub bcconfirmations: Option, - pub spendable: bool, - pub solvable: bool, - pub safe: bool, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct QueryOptions { - #[serde(rename = "minimumAmount")] - pub minimum_amount: Option, - #[serde(rename = "maximumAmount")] - pub maximum_amount: Option, - #[serde(rename = "maximumCount")] - pub maximum_count: Option, - #[serde(rename = "minimumSumAmount")] - pub minimum_sum_amount: Option, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -#[serde(untagged)] -pub enum ScantxoutsetResult { - Start { - bestblock: String, - height: u64, - success: bool, - total_unblinded_bitcoin_amount: f64, - txouts: u32, - unspents: Vec, - }, - Abort { - success: bool, - }, - Status { - progress: f64, - searched_items: Option, - }, -} - -impl ScantxoutsetResult { - /// Parse the RPC response based on the action that was sent - pub fn from_value(value: serde_json::Value, action: &str) -> Result { - match action { - "start" => serde_json::from_value(value).map(|start_data: StartData| ScantxoutsetResult::Start { - bestblock: start_data.bestblock, - height: start_data.height, - success: start_data.success, - total_unblinded_bitcoin_amount: start_data.total_unblinded_bitcoin_amount, - txouts: start_data.txouts, - unspents: start_data.unspents, - }), - "abort" => serde_json::from_value(value).map(|abort_data: AbortData| ScantxoutsetResult::Abort { - success: abort_data.success, - }), - "status" => serde_json::from_value(value).map(|status_data: StatusData| ScantxoutsetResult::Status { - progress: status_data.progress, - searched_items: status_data.searched_items, - }), - _ => Err(serde_json::Error::custom(format!("unknown action: {action}"))), - } - } -} - -#[derive(Debug, serde::Deserialize)] -struct StartData { - bestblock: String, - height: u64, - success: bool, - total_unblinded_bitcoin_amount: f64, - txouts: u32, - unspents: Vec, -} - -#[derive(Debug, serde::Deserialize)] -struct AbortData { - success: bool, -} - -#[derive(Debug, serde::Deserialize)] -struct StatusData { - progress: f64, - searched_items: Option, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct ScantxoutsetUtxo { - pub amount: f64, - pub asset: String, - pub desc: String, - pub height: u64, - #[serde(rename = "scriptPubKey")] - pub scriptpubkey: String, - 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/simplex/crates/provider/src/error.rs b/simplex/crates/provider/src/error.rs deleted file mode 100644 index 60ddc9d..0000000 --- a/simplex/crates/provider/src/error.rs +++ /dev/null @@ -1,124 +0,0 @@ -use electrsd::bitcoind::bitcoincore_rpc::jsonrpc::minreq; -use reqwest::{StatusCode, Url}; - -#[derive(thiserror::Error, Debug)] -pub enum ExplorerError { - #[error("Failed to type to Url, {0}")] - UrlConversion(String), - - #[error("Failed to send request, [url: '{url:?}', code: {status:?}, text: '{text}']")] - Request { - 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, - 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, - status: Option, - 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), - - #[error("Failed to decode hex value to array, {0}")] - ElementsHex(simplicityhl::elements::hex::Error), - - #[error("Failed to convert address value to Address, {0}")] - AddressConversion(String), - - #[error("Failed to decode commitment, type: {commitment_type:?}, error: {error}")] - CommitmentDecode { - commitment_type: CommitmentType, - error: simplicityhl::elements::encode::Error, - }, - - #[error("Failed to decode hex string using hex_simd, error: {0}")] - HexSimdDecode(hex_simd::Error), - - #[error("Failed to deserialize Transaction from hex, error: {0}")] - TransactionDecode(String), - - #[error(transparent)] - ElementsRpcError(#[from] electrsd::bitcoind::bitcoincore_rpc::Error), - - #[error("Elements RPC returned an unexpected value for call {0}")] - ElementsRpcUnexpectedReturn(String), - - #[error("Invalid input, err: {0}")] - InvalidInput(String), -} - -#[derive(Debug, Clone)] -pub enum CommitmentType { - Asset, - Nonce, - Value, -} - -impl ExplorerError { - #[inline] - pub(crate) fn response_failed_reqwest(e: &reqwest::Error) -> Self { - ExplorerError::Request { - url: e.url().cloned().map(|x| x.to_string()), - status: e.status(), - text: e.to_string(), - } - } - - #[inline] - pub(crate) fn erroneous_response_reqwest(e: &reqwest::Response) -> Self { - ExplorerError::ErroneousRequest { - url: Some(e.url().clone().to_string()), - status: Some(e.status()), - text: String::new(), - } - } - - #[inline] - 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/simplex/crates/provider/src/esplora/mod.rs b/simplex/crates/provider/src/esplora/mod.rs deleted file mode 100644 index e3e81d9..0000000 --- a/simplex/crates/provider/src/esplora/mod.rs +++ /dev/null @@ -1,2207 +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 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/simplex/crates/provider/src/esplora/types.rs b/simplex/crates/provider/src/esplora/types.rs deleted file mode 100644 index 5f0a733..0000000 --- a/simplex/crates/provider/src/esplora/types.rs +++ /dev/null @@ -1,167 +0,0 @@ -use serde::Deserialize; -use simplicityhl::elements::{AssetId, BlockHash, OutPoint, Script, TxMerkleNode, Txid}; -use std::collections::HashMap; - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct EsploraTransaction { - pub txid: Txid, - pub version: u32, - pub locktime: u32, - pub vin: Vec, - pub vout: Vec, - pub size: u64, - pub weight: u64, - pub fee: u64, - pub status: TxStatus, - pub discount_vsize: u64, - pub discount_weight: u64, -} - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct Vin { - pub out_point: OutPoint, - pub is_coinbase: bool, - pub scriptsig: String, - pub scriptsig_asm: String, - pub inner_redeemscript_asm: Option, - pub inner_witnessscript_asm: Option, - pub sequence: u32, - pub witness: Vec, - pub prevout: Option, -} - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct Vout { - pub scriptpubkey: Script, - pub scriptpubkey_asm: String, - pub scriptpubkey_type: String, - pub scriptpubkey_address: Option, - pub value: Option, -} - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct TxStatus { - pub confirmed: bool, - pub block_height: Option, - pub block_hash: Option, - pub block_time: Option, -} - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct Outspend { - pub spent: bool, - pub txid: Option, - pub vin: Option, - pub status: Option, -} - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct AddressInfo { - pub address: simplicityhl::elements::Address, - pub chain_stats: ChainStats, - pub mempool_stats: MempoolStats, -} - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct ScripthashInfo { - pub scripthash: Script, - pub chain_stats: Stats, - pub mempool_stats: Stats, -} - -pub type MempoolStats = ChainStats; - -#[derive(Debug, Clone, Deserialize, Hash, Eq, PartialEq)] -pub struct ChainStats { - #[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 { - #[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)] -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct Utxo { - pub outpoint: OutPoint, - pub value: u64, - pub status: TxStatus, -} - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct AddressUtxo { - pub outpoint: OutPoint, - pub status: TxStatus, - pub utxo_info: UtxoInfo, -} - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub enum UtxoInfo { - Confidential { - value_comm: simplicityhl::elements::confidential::Value, - asset_comm: simplicityhl::elements::confidential::Asset, - nonce_comm: simplicityhl::elements::confidential::Nonce, - }, - Explicit { - value: u64, - asset: AssetId, - }, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Block { - pub id: String, - pub height: u64, - pub version: u32, - pub timestamp: u64, - pub tx_count: u64, - pub size: u64, - pub weight: u64, - pub merkle_root: TxMerkleNode, - pub mediantime: u64, - pub previousblockhash: BlockHash, - pub ext: Option, -} - -#[derive(Debug, Clone, Deserialize, Hash, Eq, PartialEq)] -pub struct BlockStatus { - pub in_best_chain: bool, - pub height: u64, - pub next_best: Option, -} - -#[derive(Debug, Clone, Deserialize, PartialEq)] -pub struct MempoolInfo { - pub count: u64, - pub vsize: u64, - pub total_fee: u64, - pub fee_histogram: Vec<(f64, u64)>, -} - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct MempoolRecent { - pub txid: Txid, - pub fee: u64, - pub vsize: u64, - pub discount_vsize: u64, -} - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct MerkleProof { - pub block_height: u64, - pub merkle: Vec, - pub pos: u64, -} - -pub type FeeEstimates = HashMap; diff --git a/simplex/crates/provider/src/lib.rs b/simplex/crates/provider/src/lib.rs deleted file mode 100644 index 7c1ac7c..0000000 --- a/simplex/crates/provider/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] - -pub mod elements_rpc; -mod error; -pub mod esplora; - -pub use error::*; diff --git a/simplex/crates/sdk/src/utils.rs b/simplex/crates/sdk/src/utils.rs deleted file mode 100644 index d1577c8..0000000 --- a/simplex/crates/sdk/src/utils.rs +++ /dev/null @@ -1,9 +0,0 @@ -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, - ]) - .expect("key should be valid") -} diff --git a/simplex/crates/simplex/tests/simplex_test.rs b/simplex/crates/simplex/tests/simplex_test.rs deleted file mode 100644 index 8b9b2cd..0000000 --- a/simplex/crates/simplex/tests/simplex_test.rs +++ /dev/null @@ -1,158 +0,0 @@ -use simplex_provider::elements_rpc::{AddressType, ElementsRpcClient}; -use simplex_test::{DEFAULT_SAT_AMOUNT_FAUCET, ElementsDConf, TestContext}; -use simplicityhl::elements::Address; -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::secp256k1_zkp::Keypair; - -use simplex::simplex_sdk::constants::SimplicityNetwork; -use simplex::simplex_sdk::presets::{P2PK, P2PKArguments}; -use simplex::simplex_sdk::utils::tr_unspendable_key; - -#[ignore] -#[simplex::simplex_macros::test(default_rpc)] -fn test_execution(x: TestContext) { - assert!(true) -} - -#[ignore] -#[simplex::simplex_macros::test] -fn test_execution3(x: TestContext) { - assert!(true) -} - -#[ignore] -#[test] -fn test_execution2() { - 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, TestClientProvider}; - - fn test_invocation_tx_tracking( - rpc: TestClientProvider, - user1_addr: Address, - user2_addr: Address, - ) -> anyhow::Result<()> { - // user input code - { - let network = SimplicityNetwork::default_regtest(); - let keypair = Keypair::from_seckey_slice(&secp256k1::SECP256K1, &[1; 32])?; - - let arguments = P2PKArguments { - public_key: keypair.x_only_public_key().0.serialize(), - }; - - let p2pk_program = P2PK::new(tr_unspendable_key(), arguments); - let p2pk = p2pk_program.get_program().get_tr_address(network).unwrap(); - - dbg!(p2pk.to_string()); - - // simplex runtime - // - test provider - // - fields from config - // - - // p2tr - - // TODO: uncomment and fix - dbg!(ElementsRpcClient::validateaddress(rpc.as_ref(), &p2pk.to_string())?); - // ElementsRpcClient::importaddress(rpc.as_ref(), &p2pk.to_string(), None, None, None)?; - - // broadcast, fetch fee transaction - - let result = ElementsRpcClient::sendtoaddress( - rpc.as_ref(), - &p2pk, - DEFAULT_SAT_AMOUNT_FAUCET, - Some(network.policy_asset()), - )?; - - ElementsRpcClient::generate_blocks(rpc.as_ref(), 5)?; - - dbg!(ElementsRpcClient::listunspent( - rpc.as_ref(), - None, - None, - Some(vec![p2pk.to_string()]), - None, - None, - )?,); - - dbg!(ElementsRpcClient::scantxoutset( - rpc.as_ref(), - "start", - Some(vec![format!("addr({})", p2pk)]), - )?,); - - Ok(()) - } - } - - let network = SimplicityNetwork::default_regtest(); - let rpc = TestClientProvider::init( - ConfigOption::DefaultRegtest, - ElementsDConf::obtain_default_elementsd_path(), - ) - .unwrap(); - { - ElementsRpcClient::generate_blocks(rpc.as_ref(), 1).unwrap(); - ElementsRpcClient::rescanblockchain(rpc.as_ref(), None, None).unwrap(); - ElementsRpcClient::sweep_initialfreecoins(rpc.as_ref()).unwrap(); - ElementsRpcClient::generate_blocks(rpc.as_ref(), 100).unwrap(); - } - - let user1_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); - let user2_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); - ElementsRpcClient::sendtoaddress( - rpc.as_ref(), - &user1_addr, - DEFAULT_SAT_AMOUNT_FAUCET, - Some(network.policy_asset()), - ) - .unwrap(); - - ElementsRpcClient::sendtoaddress( - rpc.as_ref(), - &user2_addr, - DEFAULT_SAT_AMOUNT_FAUCET, - Some(network.policy_asset()), - ) - .unwrap(); - - ElementsRpcClient::generate_blocks(rpc.as_ref(), 3).unwrap(); - dbg!(ElementsRpcClient::listunspent( - rpc.as_ref(), - None, - None, - Some(vec![user1_addr.to_string(), user2_addr.to_string()]), - None, - None, - )?,); - test_invocation_tx_tracking(rpc, user1_addr, user2_addr) -} diff --git a/simplex/crates/test/src/common.rs b/simplex/crates/test/src/common.rs deleted file mode 100644 index 0cd6630..0000000 --- a/simplex/crates/test/src/common.rs +++ /dev/null @@ -1,28 +0,0 @@ -pub const DEFAULT_SAT_AMOUNT_FAUCET: u64 = 100000; - -pub trait ElementsdParams { - fn get_bin_args(&self) -> Vec; -} - -pub struct DefaultElementsdParams; - -impl ElementsdParams for DefaultElementsdParams { - fn get_bin_args(&self) -> Vec { - vec![ - "-fallbackfee=0.0001".to_string(), - "-dustrelayfee=0.00000001".to_string(), - "-acceptdiscountct=1".to_string(), - "-rest".to_string(), - "-evbparams=simplicity:-1:::".to_string(), // Enable Simplicity from block 0 - "-minrelaytxfee=0".to_string(), // test tx with no fees/asset fees - "-blockmintxfee=0".to_string(), // test tx with no fees/asset fees - "-chain=liquidregtest".to_string(), - "-txindex=1".to_string(), - "-validatepegin=0".to_string(), - "-initialfreecoins=2100000000000000".to_string(), - "-listen=1".to_string(), - "-txindex=1".to_string(), - // "-disablewallet=0".to_string(), - ] - } -} diff --git a/simplex/crates/test/src/error.rs b/simplex/crates/test/src/error.rs deleted file mode 100644 index 9cd42e8..0000000 --- a/simplex/crates/test/src/error.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::io; - -use electrsd::electrum_client::bitcoin::hex::HexToArrayError; - -use simplex_provider::ExplorerError; - -#[derive(thiserror::Error, Debug)] -pub enum TestError { - #[error("Explorer error occurred: {0}")] - Explorer(#[from] ExplorerError), - - #[error("Unhealthy rpc connection, error: {0}")] - UnhealthyRpc(ExplorerError), - - #[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/simplex/crates/test/src/lib.rs b/simplex/crates/test/src/lib.rs deleted file mode 100644 index 4ad8493..0000000 --- a/simplex/crates/test/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod common; -mod error; -mod testing; - -pub use common::*; -pub use error::*; -pub use testing::*; diff --git a/simplex/crates/test/src/testing/config.rs b/simplex/crates/test/src/testing/config.rs deleted file mode 100644 index 3ea2615..0000000 --- a/simplex/crates/test/src/testing/config.rs +++ /dev/null @@ -1,41 +0,0 @@ -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/simplex/crates/test/src/testing/mod.rs b/simplex/crates/test/src/testing/mod.rs deleted file mode 100644 index 8965b46..0000000 --- a/simplex/crates/test/src/testing/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -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/simplex/crates/test/src/testing/rpc_provider.rs b/simplex/crates/test/src/testing/rpc_provider.rs deleted file mode 100644 index afd387e..0000000 --- a/simplex/crates/test/src/testing/rpc_provider.rs +++ /dev/null @@ -1,294 +0,0 @@ -use std::collections::HashMap; -use std::path::Path; -use std::str::FromStr; - -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}; - -use simplicityhl::elements::Transaction; -use simplicityhl::elements::hex::ToHex; -use simplicityhl::elements::{Address, AssetId, BlockHash, OutPoint, Script, TxOut, Txid}; - -pub use simplex_provider::elements_rpc::*; - -use simplex_sdk::constants::SimplicityNetwork; -use simplex_sdk::provider::{ProviderError, ProviderTrait}; - -use crate::{ElementsdParams, TestError, common}; - -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; - - 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 ProviderTrait 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| ProviderError::Request(e.to_string())) - } - - fn wait(&self, txid: &Txid) -> Result<(), ProviderError> { - todo!() - } - - fn fetch_fee_estimates(&self) -> Result, ProviderError> { - // Todo: search for appropriate endpoint - let mut map = HashMap::new(); - map.insert("".to_string(), 0.1); - Ok(map) - } - - fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError> { - todo!() - } - - fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError> { - todo!() - } - - fn fetch_transaction(&self, txid: &Txid) -> Result { - self.gettransaction(&txid) - .map_err(|e| ProviderError::Request(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)?) - } - - pub fn scantxoutset( - &self, - action: &str, - scanobjects: Option>, - ) -> Result { - let client = self.provider.client(); - Ok(ElementsRpcClient::scantxoutset(client, action, scanobjects)?) - } -}