diff --git a/Cargo.lock b/Cargo.lock index 1e795b2..e5ce080 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,9 +87,15 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "base58ck" version = "0.1.0" @@ -106,6 +112,12 @@ 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" @@ -186,12 +198,6 @@ dependencies = [ "hex-conservative", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.10.0" @@ -222,6 +228,12 @@ 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.55" @@ -238,6 +250,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.5.57" @@ -269,7 +287,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -278,22 +296,6 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" -[[package]] -name = "cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "config", - "dotenvy", - "hex", - "minreq", - "simplex-simplicity", - "sled", - "thiserror", - "tokio", -] - [[package]] name = "colorchoice" version = "1.0.4" @@ -335,7 +337,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.17", "once_cell", "tiny-keccak", ] @@ -358,30 +360,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - [[package]] name = "crunchy" version = "0.2.4" @@ -408,6 +386,17 @@ dependencies = [ "crypto-common", ] +[[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.114", +] + [[package]] name = "dlv-list" version = "0.5.2" @@ -428,6 +417,9 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "elements" @@ -449,6 +441,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "erased-serde" version = "0.4.9" @@ -460,6 +458,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -473,22 +481,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "fs2" -version = "0.4.3" +name = "form_urlencoded" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ - "libc", - "winapi", + "percent-encoding", ] [[package]] -name = "fxhash" -version = "0.2.1" +name = "futures-channel" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ - "byteorder", + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", ] [[package]] @@ -514,12 +546,32 @@ dependencies = [ "wasm-bindgen", ] +[[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 = "ghost-cell" 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.14.5" @@ -535,6 +587,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" @@ -565,6 +623,16 @@ 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" @@ -572,12 +640,231 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" [[package]] -name = "instant" -version = "0.1.13" +name = "http" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ - "cfg-if", + "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.36", + "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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[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]] @@ -622,12 +909,27 @@ dependencies = [ "serde", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "libc" version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "lock_api" version = "0.4.14" @@ -643,6 +945,21 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.6" @@ -665,11 +982,31 @@ version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" dependencies = [ - "rustls", - "rustls-webpki", + "rustls 0.21.12", + "rustls-webpki 0.101.7", "serde", "serde_json", - "webpki-roots", + "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", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", ] [[package]] @@ -694,29 +1031,33 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parking_lot" -version = "0.11.2" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ - "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.6" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", - "instant", "libc", "redox_syscall", "smallvec", - "winapi", + "windows-link", ] [[package]] @@ -725,6 +1066,12 @@ 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 = "pest" version = "2.8.5" @@ -755,7 +1102,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -774,6 +1121,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +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 = "ppv-lite86" version = "0.2.21" @@ -784,14 +1146,93 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.106" +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", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.36", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +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.36", + "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" @@ -801,6 +1242,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -808,8 +1255,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "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", ] [[package]] @@ -819,7 +1276,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "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", ] [[package]] @@ -828,16 +1295,25 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "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 = "redox_syscall" -version = "0.2.16" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 1.3.2", + "bitflags", ] [[package]] @@ -869,6 +1345,44 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[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.36", + "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" @@ -877,7 +1391,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -889,7 +1403,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ - "bitflags 2.10.0", + "bitflags", "once_cell", "serde", "serde_derive", @@ -907,6 +1421,12 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustls" version = "0.21.12" @@ -915,10 +1435,34 @@ checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +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" @@ -929,12 +1473,29 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "santiago" version = "1.3.1" @@ -967,7 +1528,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "bitcoin_hashes", - "rand", + "rand 0.8.5", "secp256k1-sys", ] @@ -987,7 +1548,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a44aed3002b5ae975f8624c5df3a949cfbf00479e18778b6058fcd213b76e3" dependencies = [ "bitcoin-private", - "rand", + "rand 0.8.5", "secp256k1", "secp256k1-zkp-sys", ] @@ -1041,7 +1602,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1066,6 +1627,18 @@ 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" @@ -1077,6 +1650,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1084,17 +1666,96 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "simplex-simplicity" +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simplex" +version = "0.1.0" +dependencies = [ + "bincode", + "either", + "serde", + "simplex-core", + "simplex-macros", + "simplicityhl", + "trybuild", +] + +[[package]] +name = "simplex-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "config", + "dotenvy", + "serde", + "simplicityhl", + "thiserror", + "tokio", + "toml", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "simplex-core" version = "0.1.0" dependencies = [ "bincode", "hex", "minreq", + "serde", "sha2", "simplicityhl", "thiserror", ] +[[package]] +name = "simplex-explorer" +version = "0.1.0" +dependencies = [ + "async-trait", + "bitcoin_hashes", + "hex-simd", + "lazy_static", + "reqwest", + "serde", + "serde_json", + "simplicityhl", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "simplex-macro-core" +version = "0.1.0" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "simplicityhl", + "syn 2.0.114", + "thiserror", +] + +[[package]] +name = "simplex-macros" +version = "0.1.0" +dependencies = [ + "serde", + "simplex-macro-core", + "syn 2.0.114", +] + [[package]] name = "simplicity-lang" version = "0.7.0" @@ -1105,7 +1766,7 @@ dependencies = [ "bitcoin_hashes", "byteorder", "elements", - "getrandom", + "getrandom 0.2.17", "ghost-cell", "hex-conservative", "miniscript", @@ -1125,13 +1786,12 @@ dependencies = [ [[package]] name = "simplicityhl" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa7477fc9bfef4cc53ae969db00539f0e67af38156822ac79662513d04f6fee" +source = "git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params#69928ce05aa4450ccfdf93b226135f6e1496789a" dependencies = [ - "base64", + "base64 0.21.7", "clap", "either", - "getrandom", + "getrandom 0.2.17", "itertools", "miniscript", "pest", @@ -1142,20 +1802,10 @@ dependencies = [ ] [[package]] -name = "sled" -version = "0.34.7" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -1163,12 +1813,50 @@ 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 = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" 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.114" @@ -1180,6 +1868,41 @@ 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.114", +] + +[[package]] +name = "target-triple" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + +[[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" @@ -1197,7 +1920,16 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", ] [[package]] @@ -1209,14 +1941,46 @@ dependencies = [ "crunchy", ] +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", + "socket2", "tokio-macros", + "windows-sys 0.61.2", ] [[package]] @@ -1227,7 +1991,17 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.36", + "tokio", ] [[package]] @@ -1236,10 +2010,12 @@ version = "0.9.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ + "indexmap", "serde_core", "serde_spanned", "toml_datetime", "toml_parser", + "toml_writer", "winnow", ] @@ -1261,6 +2037,139 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +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", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "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.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f614c21bd3a61bad9501d75cbb7686f00386c806d7f456778432c25cf86948a" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "typeid" version = "1.0.3" @@ -1303,12 +2212,36 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -1321,12 +2254,36 @@ version = "0.0.18" 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -1340,6 +2297,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.108" @@ -1359,7 +2330,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -1373,32 +2344,48 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.25.4" +name = "web-sys" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] -name = "winapi" -version = "0.3.9" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +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 = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] [[package]] name = "windows-link" @@ -1412,7 +2399,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "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", ] [[package]] @@ -1430,14 +2426,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "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", + "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", ] [[package]] @@ -1446,48 +2459,96 @@ 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" @@ -1497,6 +2558,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "yaml-rust2" version = "0.10.4" @@ -1508,6 +2581,29 @@ dependencies = [ "hashlink", ] +[[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.114", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.38" @@ -1525,7 +2621,67 @@ checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", +] + +[[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.114", + "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.114", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 103cb79..59f89c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,16 +12,18 @@ edition = "2024" multiple_crate_versions = "allow" [workspace.dependencies] -ring = "0.17.14" +simplex-core = { path = "./crates/core" } + +bincode = { version = "2.0.1", features = ["serde"] } +ring = { version = "0.17.14" } sha2 = { version = "0.10.9", features = ["compress"] } -hex = "0.4.3" +hex = { version = "0.4.3" } tracing = { version = "0.1.41" } -minreq = { version = "2.14.1", features = ["https", "json-using-serde"]} +minreq = { version = "2.14.1", features = ["https", "json-using-serde"] } -simplicityhl = { version = "0.4.0" } -simplicityhl-core = { version = "0.4.2", features = ["encoding"] } +simplicityhl = { git = "https://github.com/ikripaka/SimplicityHL/", branch = "feature/rich-params" } [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 c52f23b..e970c91 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # Simplex SDK +This collection of useful crates provides useful utilities for working with Simplicity on Elements. +- `simplex-macro` - provides common macros related utitiles which would provide and thrive DX. +- `simplex-core` - provides useful utilities. +- `simplex-cli` - provides common cli interface and ability to setup your contract development environment. ## License diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index a2c01eb..b8183f5 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "cli" +name = "simplex-cli" version = "0.1.0" edition = "2024" description = "Simplicity helper CLI for Liquid testnet" @@ -9,23 +9,20 @@ publish = false [[bin]] name = "simplex" -path = "src/main.rs" +path = "src/bin/main.rs" [lints] workspace = true [dependencies] anyhow = "1" -thiserror = "2" - -sled = "0.34.7" - dotenvy = "0.15" -clap = { version = "4.5", features = ["derive"] } +clap = { version = "4", features = ["derive", "env"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } config = { version = "0.15.16", default-features = true } - -minreq = { version = "2.14", features = ["https"] } -tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "sync"] } - -hex = { workspace = true } -simplex-simplicity = { path = "../sdk" } \ No newline at end of file +toml = { version = "0.9.8" } +serde = { version = "1", features = ["derive"] } +simplicityhl = { workspace = true } +tracing = { version = "0.1.44" } +thiserror = { version = "2.0.18" } +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } \ No newline at end of file diff --git a/crates/cli/README.md b/crates/cli/README.md new file mode 100644 index 0000000..d2dff83 --- /dev/null +++ b/crates/cli/README.md @@ -0,0 +1,11 @@ +# Simplex-CLI + +CLI instrument to support easy building and creation of contracts on SimplicityHL. + +## License + +Dual-licensed under either of: +- Apache License, Version 2.0 (Apache-2.0) +- MIT license (MIT) + +at your option. \ No newline at end of file diff --git a/crates/cli/src/bin/main.rs b/crates/cli/src/bin/main.rs new file mode 100644 index 0000000..78d30c5 --- /dev/null +++ b/crates/cli/src/bin/main.rs @@ -0,0 +1,14 @@ +#![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/commands.rs b/crates/cli/src/cli/commands.rs new file mode 100644 index 0000000..ec56e8b --- /dev/null +++ b/crates/cli/src/cli/commands.rs @@ -0,0 +1,7 @@ +use clap::Subcommand; + +#[derive(Debug, Subcommand)] +pub enum Command { + /// Show current configuration + Config, +} diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs new file mode 100644 index 0000000..1260bea --- /dev/null +++ b/crates/cli/src/cli/mod.rs @@ -0,0 +1,43 @@ +pub mod commands; + +use crate::error::Error; + +use crate::config::{Config, default_config_path}; + +use clap::Parser; +use std::path::PathBuf; + +pub use commands::Command; + +#[derive(Debug, Parser)] +#[command(name = "simplicity-dex")] +#[command(about = "CLI for Simplicity Options trading on Liquid")] +pub struct Cli { + #[arg(short, long, default_value_os_t = default_config_path(), env = "SIMPLEX_CONFIG")] + pub config: PathBuf, + + #[command(subcommand)] + pub command: Command, +} + +impl Cli { + #[must_use] + pub fn load_config(&self) -> Config { + Config::load_or_default(&self.config) + } + + /// Runs the CLI command. + /// + /// # Errors + /// Returns an error if the command execution fails. + pub async fn run(&self) -> Result<(), Error> { + let config = self.load_config(); + + match &self.command { + Command::Config => { + println!("{config:#?}"); + Ok(()) + } + } + } +} diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs new file mode 100644 index 0000000..19ba234 --- /dev/null +++ b/crates/cli/src/config.rs @@ -0,0 +1,75 @@ +use crate::error::Error; +use serde::{Deserialize, Serialize}; +use simplicityhl::elements::AddressParams; +use std::path::{Path, PathBuf}; + +const DEFAULT_CONFIG_PATH: &str = "config.toml"; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Config { + #[serde(default)] + pub network: NetworkConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkConfig { + #[serde(default = "default_network")] + pub name: NetworkName, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum NetworkName { + #[default] + Testnet, + Mainnet, +} + +impl NetworkName { + #[must_use] + pub const fn address_params(self) -> &'static AddressParams { + match self { + Self::Testnet => &AddressParams::LIQUID_TESTNET, + Self::Mainnet => &AddressParams::LIQUID, + } + } +} + +impl Config { + /// Loads configuration from the specified path. + /// + /// # Errors + /// Returns `Error::Io` if the file cannot be read, or `Error::TomlParse` if the content + /// is not valid TOML. + pub fn load(path: impl AsRef) -> Result { + let content = std::fs::read_to_string(path)?; + let config: Self = toml::from_str(&content)?; + Ok(config) + } + + pub fn load_or_default(path: impl AsRef) -> Self { + Self::load(path).unwrap_or_default() + } + + #[must_use] + pub const fn address_params(&self) -> &'static AddressParams { + self.network.name.address_params() + } +} + +impl Default for NetworkConfig { + fn default() -> Self { + Self { + name: default_network(), + } + } +} + +const fn default_network() -> NetworkName { + NetworkName::Testnet +} + +#[must_use] +pub fn default_config_path() -> PathBuf { + PathBuf::from(DEFAULT_CONFIG_PATH) +} diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs new file mode 100644 index 0000000..e6a70ef --- /dev/null +++ b/crates/cli/src/error.rs @@ -0,0 +1,25 @@ +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 when parsing TOML configuration files. + #[error("TOML parse error: {0}")] + TomlParse(#[from] toml::de::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), +} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs new file mode 100644 index 0000000..c7a1415 --- /dev/null +++ b/crates/cli/src/lib.rs @@ -0,0 +1,4 @@ +pub mod cli; +pub mod config; +pub mod error; +pub mod logging; diff --git a/crates/cli/src/logging.rs b/crates/cli/src/logging.rs new file mode 100644 index 0000000..6247051 --- /dev/null +++ b/crates/cli/src/logging.rs @@ -0,0 +1,10 @@ +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/crates/cli/src/main.rs b/crates/cli/src/main.rs deleted file mode 100644 index 623ab5f..0000000 --- a/crates/cli/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello world!") -} diff --git a/crates/sdk/Cargo.toml b/crates/core/Cargo.toml similarity index 84% rename from crates/sdk/Cargo.toml rename to crates/core/Cargo.toml index 29943f7..fd2dcf0 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "simplex-simplicity" +name = "simplex-core" version = "0.1.0" edition = "2024" rust-version = "1.90" @@ -19,14 +19,10 @@ workspace = true encoding = ["dep:bincode"] [dependencies] -thiserror = "2" - -bincode = { version = "2.0.1", optional = true } - +thiserror = { version = "2.0.18" } +bincode = { workspace = true, optional = true } sha2 = { workspace = true } - hex = { workspace = true } - simplicityhl = { workspace = true } - minreq = { workspace = true } +serde = { version = "1.0.228" } \ No newline at end of file diff --git a/crates/core/README.md b/crates/core/README.md new file mode 100644 index 0000000..caf7a9d --- /dev/null +++ b/crates/core/README.md @@ -0,0 +1,20 @@ +# Simpelex HL Core + +This crate provides useful utilities for working with Simplicity on Elements. + +- `blinder.rs` — derives deterministic blinder keypair from a "public secret" +- `constants.rs` — Liquid network constants (policy asset IDs, genesis hashes) +- `explorer.rs` — explorer API utilities (behind `explorer` feature) +- `runner.rs` — program execution helpers with logging +- `scripts.rs` — P2TR address creation, Taproot control block, and asset entropy utilities +- `lib.rs` — P2PK program helpers and transaction finalization + +Consider this more like a test helper tool rather than a production-ready version. + +## License + +Dual-licensed under either of: +- Apache License, Version 2.0 (Apache-2.0) +- MIT license (MIT) + +at your option. diff --git a/crates/core/src/assets/test-tx-incl-block.hex b/crates/core/src/assets/test-tx-incl-block.hex new file mode 100644 index 0000000..e957c2b --- /dev/null +++ b/crates/core/src/assets/test-tx-incl-block.hex @@ -0,0 +1 @@ +000000207e3dba98460e4136659f0fccf3e59338dfe53ed5f094fb0bb94d771c48341854d875900105c87e5dd46c740cb1129c06f8f4007e868f61b25e37cffa946c718d8742805b01000000015100030200000001010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0201230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000009b64001976a914608c0ea8194a8ceb57f0196f44a6b48a54fc065988ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000000000266a24aa21a9ed8f8a98e5623643b24167266c2648ead4a50d18b0491c6f34e11398aaee0ca6e8000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000020000000001eb04b68e9a26d116046c76e8ff47332fb71dda90ff4bef5370f25226d3bc09fc0000000000feffffff0201230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b20100000002540bd71c001976a91448633e2c0ee9495dd3f9c43732c47f4702a362c888ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000000ce4000000000000020000000101f23ceddac67cfbbc997199daa651384d0746fb2a5482b8c8629ba8df4b788f75000000006b483045022100e0feb3e2f292000d67e24b821d87c9532230dac1de428d6a0068c9f416583abf02200e76f072788dd411b2327267cd91c6b1659809598cd4fae35be475efe1e4bbad01210201e15c23c021652d07c1557b607ea0379fca0462aca840d6c33c4d4927524547feffffff030b60424a423335923c15ae387d95d4f80d944722020bfa55b9f0a0e67579e3c13c081c4f215239c77456d121eb73bd9914a9a6398fe369b4eb8f88a5f78e257fcaa303301ee46349950886ae115c9556607fcda9381c2f72368f4b5286488c62aa0b081976a9148bb6c4d5814d43fefb9e330575e326632136389c88ac0bd436b0539f5497af792d7cb281f09b73d8a5abc198b3ce6239d79e68893e5e5d0923899fd35071ba8a209d85b556d5747b6c35539c3b2f8631a27c0d477a1f45a603d1d350b8cbf900f7666da66541bf6252fc4c162141ad49c670884c93c57db6ba1976a9148c7ab6e0fca387d03643d4846f708bf39d47c1e988ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000008e800000000000000000000043010001dc65ae13f76fde4a7172e0fb380b1a5cc8dc88eaa0659e638a25eac8ae30d79bf93eb7e487eeee323e4ac8e3a2fe6523bdeba6acce32b9b085f2286174c04655fd6c0a6020000000000000000178ad016b3e5d8165423e56d8b37e3eaee96009b2f970043ccf65d61b5c3c1e1ef343e0c479bdba442717dc861c9591566010240b9d4607efb9252a5fcef05edf640e0bb6b606729246ad07baa49d0d3b52042c65a03ca737744e45b2d2d6d177c36569ae9d6eb4437305b169bbc59f85cabff3bc49a2d6d08c177cce3121a509d3c47961bd22e35c932b79d4ec5ccaf913fac04034bfebdadbc4ff3127af96344b02ee6b967bb08326cbe6a4e1c924485e64a8c0fdf70b98c99f38acaa15aa0adb2b5b7335ed5502443891bcd657310347cbd928f40f38f1dec087a2b947c9cf7d304798f77bbc4a2c843796b2d49acce91de4e88a0a9c261277df28ffc3320d7f7d64790f592ddded48a1068ef88271395fa5606389ef90856ddd6bd6710a8d27e0147983b5dde2a7efae44e83ad02a3c3da04be43d5f2c05c205f1e17b48554c2177670f46dbb6600bd2e6c75dd5ea2e1072c5f22483dcf05d8124e3f9063a5ddb179a29c23a2d15d6e89f2192f03dae5938f66fcdcff000c5a96ffd2920f23881880af72153c96a56dd80c218bb48b44a18e54a8050ff32c869c1264ee574cdb4002f86e0779c724d11dc4a768dbec1bd22054886f1fdf2e7347e4c247b829159d1375f881c6ce0a5c4da8534000e7fec3a980afb1edc99b725c29de80f260dcf144c873bf589ae1812ef6cb05f2234f9c66c23e874a0d5d0dc52f2209e015bbcf74ee449a397f6b0318c915b7e58dea5904abbe35285e90ccf548ad1f3f52f60c3b19b3cd67644d633e68aef42d8ef1782f22a8edd0620f55f29070720ca7a078ac83e87b9ebd2783ecad17dd854ef1bbd319f1a6d3a1e4931f9097422f5a3c4af037b99e06c7610ee61102c6eea763af108e9a16b93b2dc0891658d5c6a197df6aae9b306b2c895d21c79cb6cb6dd85b4018b0a9fe7468336e3907eb4adcaf930cacc97e8e951d2d6b25744a4143679bad1f31b210c9a2ed54b80d8f5d7dc1f1c985681534c1926920cd683d95dca7e8ea285f9906d2e89cd8bfa76a98e38ee4b5152522d55f79610fe8d5278fe6ed5866b5da4dcf330ea84307c34f30e1a66eb1934dafebb0074fc27c2ff73d8c0bae8416cc87bf611f81119aba9e2a911beaf3ac9507e621fc1ed1cf15dfb31408cf55e2bfdd2880db2d3489a336d6f8348347648d882f9f376331e469e809115c6cc82468f363c910673e9ded172ded90a369e1cdd135676f623e11a1531ed221177812b1ef0c65e5ca92c0df8de7fe664710f3228a226e019c99607fe1395ecd5643e1c7ad8a132bf5131737cb970a7f0dabc00029755bf71b3f47bd69ba39b3ab104c74f04239f4919dca1dfce7c9c41cba9d449073e106ebabe3c313b598ee8b11702ec46e9ee53fb9422f0326371898b8fa4c21a951684c687398e0bebd6f6fd91b829e8666b9a19a4273cfda0f34b8ecb902f7adc6539fb9a0cba6f87a63a957acfb2dfa18973f4a3063668767b2be780311513c63f1814f082176f6a953f2ffaa49ec9b39fecc2eab603be7a969bb4c1dbebf8d39fa90f802d5ea52378b5025a19b64a8c2c2dd6a6133bd8d29730bd5724b5bf50c158b238d1137082937ad91a176aaf91577868db7581b457c917e612b242ce0065ad47e11dcdc1fc6158687142249bcf312497a547b6f43e795af7d4ae8cd022e44e417987e35e83de21e39dcdf86b97bd421e6e61881a432fa2284f20be80e32459443736b875d9036468ceb881589394441e2d10aa10b6c93332951e8ba56f89fac70baf415b4511873c0f3e418ca4fe8954a28f1f7b5f590d34470119f694e2712f184882d90396c8e6aa850eaa3c2ae51990543638c46c59512167a2c5ad593532dc2142ffb6560476e4159213b9ef017ec75310d2e4624a405bb26f7192a485a94890674928c9caa4a5819ca4ddcba8fa71afc1a6baf63f039452c8fe994f8b63d58c876dfddd61a476345eaed4f66bdc0fcfc38d485c6a5b0e27d0fbc50427ff591ba38d63445c01642cfbd7d4c032f2546a6fe80bc3b598362502c552049523fe360c3bcf1cc572feb04386f97d55871dd8cea0393cdd964e724082adc98126e6f2fe1d576be4bf911e9aca70e35538175f8382bbcd614bbecc97c9607ef25da2ff08a6e5b6f76cbe9ccb0e0fdc3528e3e2c3675a5c897d295bb76524ec8a73a70b97909368f44d92f9aceaef0b03f3dafa1faa89fc663a92da3c19b4952463fac0e825e78cf046e266cfb9975af72e9d50d2c2cafee88fe2cecae2b1465fc07b280d83b66062dc9e7a372f81aec8e0bb9e97877814a5a6813c67746e35cd068d45d8664528bd00d5a306a5319e1bea7f38345da92d3a10d91476a26aed6b8441f0f72fbbad5d5e0f8ae5cabc9f4f08e6be7902b5c53632db5264afee7422c87b3237a32d5213ad0eb807b61977d9d90666cbb0c70500526b0eb762c99351796db41166b0aa2f221b5607e0d629fac4e938488245c11557381a4f8addcc49913b11d42481cf8668e37bacbad4a20509e4fe4ccbcee7aea2909a2abe59052f7f28b9340cd92f69729d615b8d3b530941c0b30506498cd4e561a9c82d915266bb7115967bc76c5593c06d094bdf4294b868afc5fa52742d3bdbd5932df599f0e1187c49f0dba8679c771a514cc9da75e03506957800bf470d4a07c4bb8918d6085499bb8ceeaba23c0b465863327e9ab8b6b8cf8b3ca530ca7b02cfadf85437b750f305e8fbc8855c95bee8595a7e9e1f0993a03adbadc68665a18936cc99b6530b4518c0754990d7bfdfdac76f88cfcbcb7b3d9a71ee10cbd3a1bdbc2e50b642c1fef56511962f845bbec6eab727b1d4add335db8d80c4c07e8356ad05adad68b012489fa5bb5d9019a667778ddf7f5edd80f1d3c4abd64397a89e554c8007809336ddc2b2e7d5219c39fdf39aad33b9350f6b18fe3b98c690b9068f36d4b7669530fd216373842fbf70fe9bbe80854b31eed4bd515d6caeb065d6c609846c9bfae1b3fce3db70b5bfb448ec69512e7f25019c789301b77a75f2a0f81c65ec29f41bf96d597a00c310e8ba4b48ac82b5a735c1e83f22394eb2fc9b35d42a35533c938f26290a5860175637982f1733c99be39c44ac4a09187406306bde2fd3d28e4e7bda73719912c338804dea03987757dac4d73def665e11da126f9414f71624a3b753797eb0472bd334094515c4f9fe57fdd8d185f22b4bf82e4b5f6b800870cce19a0c8174dc11ee9f1cb9ffe0ac6f6fff1ebf7c915c7ae20172bb70390e3759912e0e0a4e83a0a2d2318f4386314a89f6438ccb331f89377ff7947fe4b24f788aef85c1656ca87ee41c959f1b09bde09f20c2a51ac481646b28e9b0fc2ff49cfe8cf28577bf5bf6f261f54f97fcd2875da4210c6dfe685450280b68e378d9a486243cc682ed4ec747c37de1fde848e4a8f70498d22e40c462c469c884cd67330e77b694e759232313f31a1624e0e1960f23ddae47b68ff553d0de0910c8abe2e8e5fb063aa744ff77465fc731c7af79a84dcaa9b3f741a46dd3c932877d49242c6d883e14392b8c4530986605812b636a73590ef437f27e40d1af37ed1cbd68fb4e9ca5b0b41e5daee0142c1bf59c9d71f6c19b25e6148dfbb9fb142107aabe3701e36611a7e0b13ea32d3c5f8a51f63c5f34415baa15f6ca77300eb323241ffe73c5acd97fcb682c21dc8911392979e9cb81be5218acf452b5b93f6681d323b7989fdd10efe6fe9e2ac88d0d76a4cf3ee45e3b5c430100014142c1fc7e8a658eff437594a25cf34d269556d8511918f27fdc7e9d6dd73f0e4790b91f225e9d131e6abb3dbfb66549a9aa57948fbd2f183fcd951b1d2305bffd6c0a602000000000000000016f5cdf9fb6c1b5e98a36befdc2c55bd4fd8793d554b2506f51c909362495e1216ee83cd270ddb0a00785600ba23bd3363f0798e3a7a117990415adec88e61be65170bd587ab4d2ee38edb22a91e5c29afa397dd5a73465c51c6263f5fbde47fa801ce84464acc32589acaafadfe44d6558774b7085612a88f3424b6dca3c6f07217d1cbd5c41bda46a6a492a0119c1de4d25b58c94250bee3fba6b8223777535673a2f4da6af27598030f88144f408120f07ca9c98d5d9edcdf6cdc9073f118fce55e6c9d0be80b5e87992ddaa9c22053b3a00d42bdedc9768de25c0b37a5c4fb4e86710b33cebed5588d88adde607f6bca14f0279ce35126d403ffa50f288c87f528c19749ed43bd846c513fcd92c173fe76d8f2e69770439d3d075cb19b1094a42ee07ae1de197e8c136e2bc688a75a74db24adb0fbb73872dc80074f61c9cce9bd33861bdd921ee3edacab1d6e7cec325c172b6b6e82ada11687e4fc931225074dd1f20a0f9342dbce1fc3fdbf5bb6cb74ab6475e574e9f5f247a2f7e4fcfcc354d4da8c8066e574642c7fccbbb9ef0aa592ecab5366fe87eb8e14cd64aee34578aa48f68f8f4c5372df2c3fc429f5a3e39ef6c034c87f9c52b2ea35e28c7bf3be737c3817efd6569466dc859e8ff8965c5249b6f045934d3d08b0ffd388aec58df8194ac2c4fec2152942d2626595e65664b1fa33b5dae8ee796a840a56d885cbf7ae6483fad05e507ada3f075ebce0d791b626c6dfe93f8492c4dd3b34aafc33d7644c5c8e38bfd8c19194f65be88fcb4538778632e489a626896372fdd2498b16e64daa7d3c5cfac688d6f9cdf3717261b0a1f25be1bdd6be6558ddb826fa04b5f668810a291aea51a6f05ff7c34dcf81c74849a8015bad5e4e416989b10ef01de304775db725fa0b665f4330dc9c540dc29aab144837362a97d6bb0165cb3272338c2d32386cd95ee3e66d876b591a25a6907237523cf908f736d2fdc8e54ea8d9c7562697161d1f72fc4d7b775052415cd0e5ae5bdf6edfab5776b6ff75ce5e1f8f2beea6ec74252b63966cca58abd638279dc5c998a1068079f3e5dcc8a69165c304c3d8c362ccfadab05ad12208a5655ab389eb727e8ed5f86b300331a13be26e2fbabf89fbfd2b98481dd5edb52ed456a0e03a84b6f89761f91ff251412f5cfa286e35fb9f48ef0e044c4742b6e860a08767ecb80548c2f3df3b371cdb40e86dbe118f64e84faf45ecb78d73364e9e31e3412ca2a3fad0a35983370ea9e6264a222edd1fd4aca30e3c169d7ca2d07609262e786ecd019c1417a06b7dfa32a54e0897afdc6492f26611555cbff47dba3b76381f239d597a8f687669333e0b47b53d5bcc4fea1919490bad3c6f0b6a58a50aca7ddeb9745ead454e0a38d9486fb52aefe0dbb92bf7fd6c215078aba3482b11274ec8cddff92c359bbc6d20bd823ad0bbf859cfaadf8e775b3d37b3078319f46c6d2a112cf60a673fee467538c70f1687d97fbe9d9f8a0856061592a4e00b6d10e979e674dd2cd0ba8b853f733877cd508062d5f723d58d215ad69c2be6be742496aef54eb87338622eb36a9bbc5a7a602d280a45e095b1e078dab54479e783a513c722066acaae44ccc15f9560da91ed053ec05c36d82f6809766876c45c4fbeb2321d50f48f7995437d0c5fc365974a571fb0352d28cb1cdbd21d69fab576a2e68d6b881776027bcdb7f01be22b1c847d91f26e680ef6ab2c128a89b59432383d9bd661b0b01432cf8a25319426d38ac2e2114825f59b4250569c798b1094920bb31130728313ff56a6eef2e6c4b275215dce3786d0f9024952b5f572566c53597e7ef4ab1f75743e605a564054d667f48906b5481d924769ef65751e349891d725a2c1bf8b102fea4c25c874d2fc2ce1bfec4b39bea76fbf7a28855725d52b595a4fc96892c3f1f961d46310ebd5221df729c02060035c559baf0fd7efa73a2213ca29642857aeb8ebf7efdf9d2f5c84746b6fc35ab355a8dca56e7dde4831e47ca1be6b62af30cfcf807c384e56ab84ff03bbe786251e6c4b932c9217bf671046217bd0511fdc06aa69050c1480281e4843eb73d80095a2fb8e68a2c0c98c9aea637b99d87ad847a3a76d59ea308c751f9cb4a4fce2989822bd6ba2f901f09df647536dc30730ea3160dd35b8c6dcc9aa815b79ed492a8a299a298ccdf784b9b0211ca877ec1723817c98529acaa4d3727162b5740b0fc9b498dfb2212a3cbf0c63dc4f7663fafad7905643a792862b651e8497b0f0da632b897ecf9ee63f2b20b54fa5eb2f2e424dcce5a075f50b856af266655be3a815fc83ed8027508b2536976982196b160e2219ffdb5c7a56dd3e6b700860c711f4439dbf72973f4f26fe3260ec43a3446fe14444b9787d877e107be610147eec4a3574745e95a1f424aff062f84c559d13b1e6b59e8dc2221515c229f07db8eb39c515a321d8bd07b1bd6c9a79dac6d951c04415553c7a2ce1eb77495c7f89c4d5b4cffd289435b69bc53585095083cc5a1b191781342266e204e1566aca8175e2ae84a8bd711d188b666dfb65a6442776d3e23c1b5192af09ec712537f2157d0ccbc1bb3b3a1969d9705671f16bdc266e615ad2e50a8cbd666f3ee7465cc430c6cd69d30c91e717b12f7094b6f0ef89134d6c1620d28d8f238c181146448b348e4ca2e93c737210350f18fb878fb91b70ecc5689e5b6101ecfc545f6a1c903115b0c6419c91a50fb2dbe2edd362f2815f0c75070974507c34130ac9b29747ff7efbe6e37ee4c62be3ecfedfa817fdf3309163aaff677775b77f0d288c9858cfe59cb0fa18afa591e7d574eaef43c82e79d71542c4177de4e5bd724b18cfd33c68530665728a9d5ef192772094acbf3d885d5146c1634e74754e3fbcb94fa349eac8280cfd7d1f46a0813b57a83bd078b1f7cb5a60a59b59380fe04e1c600c33b33d1add69a9ff1be546f0ec5c0083979fce940b23711f382ac0d011c1103f02cb6082c18e39cf7a9c3bf4c081f905ae7b87951a7880b57e934465ccd634e5a17fd8d8866abfdfebd33b2c3d2c5be58144900c04e9c18de0c80270660e62a3c185277555f89da4c41bd33cec1359f4ed21abdb586e1d97f720a92d16014d7f1822f1836f74c97cb7f7b38e073477c6ab064fde835916c1e624de81f2ad90f6260073c5e1848582860f033630bde225821b39c2572b30c36adf8fdb8317c33df05f6413447f4985d12e9012629df09dc8f43373a6d0db4b0048453a6f1ec662472c77a30d5cf4ac7084f736d0d598c251f2aefc986052fbf12a657885d7140ad36b07c63ab86388a2be12d943747f3f29ef9f2e11e1444cc873df0ed7826eef675389a0d5a0388a8504fe89c4791ea4a572bfd406d5f01418b4f888c9a7a566e32811936bf6950bbf786b86c41c28f2045d31953fcd15f179e7bc00c72870890537921f7deff82270b0e44b88720aa738f60a85567deb7c90b0c2444467621e53e1c079436d31d3d0b34dd237fc281eb9d87175237a9a433142db4bb7f8c4cb6a34e2dc73f074045d216695ce88ef68e18564c935c9cbd902e939655c258de2ab78def8746bffd972083afce3b6881b7147262e1a44e0224689fafa1a3cb823c8da6eb7df091bec0638bf728b7b10aa95f2bce512ec8d3252938d2eb77b44ace7a2f976588032cac5af670f9e5ca25cb0721bc1baec26f9c3a9f41b02fb62997d6cb0a01314845e9d0e78139ea49f2ead8736e0000 \ No newline at end of file diff --git a/crates/sdk/src/simplicityhl_core/blinder.rs b/crates/core/src/blinder.rs similarity index 89% rename from crates/sdk/src/simplicityhl_core/blinder.rs rename to crates/core/src/blinder.rs index 96af5b3..8754bdc 100644 --- a/crates/sdk/src/simplicityhl_core/blinder.rs +++ b/crates/core/src/blinder.rs @@ -1,4 +1,4 @@ -use crate::simplicityhl_core::PUBLIC_SECRET_BLINDER_KEY; +use crate::PUBLIC_SECRET_BLINDER_KEY; use simplicityhl::elements::bitcoin::secp256k1; use simplicityhl::elements::secp256k1_zkp::SecretKey; diff --git a/crates/sdk/src/simplicityhl_core/constants.rs b/crates/core/src/constants.rs similarity index 66% rename from crates/sdk/src/simplicityhl_core/constants.rs rename to crates/core/src/constants.rs index 541d6f7..70f8f20 100644 --- a/crates/sdk/src/simplicityhl_core/constants.rs +++ b/crates/core/src/constants.rs @@ -22,60 +22,48 @@ pub const PUBLIC_SECRET_BLINDER_KEY: [u8; 32] = [1; 32]; pub const PLACEHOLDER_ISSUANCE_VALUE: u64 = 0; /// Policy asset id (hex, BE) for Liquid mainnet. -pub const LIQUID_POLICY_ASSET_STR: &str = - "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"; +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"; +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"; +pub const LIQUID_DEFAULT_REGTEST_ASSET_STR: &str = "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"; /// Example test asset id (hex, BE) on Liquid testnet. -pub static LIQUID_TESTNET_TEST_ASSET_ID_STR: &str = - "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; +pub static LIQUID_TESTNET_TEST_ASSET_ID_STR: &str = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; /// LBTC asset id for Liquid testnet. -pub static LIQUID_TESTNET_BITCOIN_ASSET: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - elements::AssetId::from_inner(sha256::Midstate([ - 0x49, 0x9a, 0x81, 0x85, 0x45, 0xf6, 0xba, 0xe3, 0x9f, 0xc0, 0x3b, 0x63, 0x7f, 0x2a, - 0x4e, 0x1e, 0x64, 0xe5, 0x90, 0xca, 0xc1, 0xbc, 0x3a, 0x6f, 0x6d, 0x71, 0xaa, 0x44, - 0x43, 0x65, 0x4c, 0x14, - ])) - }); +pub static LIQUID_TESTNET_BITCOIN_ASSET: std::sync::LazyLock = std::sync::LazyLock::new(|| { + elements::AssetId::from_inner(sha256::Midstate([ + 0x49, 0x9a, 0x81, 0x85, 0x45, 0xf6, 0xba, 0xe3, 0x9f, 0xc0, 0x3b, 0x63, 0x7f, 0x2a, 0x4e, 0x1e, 0x64, 0xe5, + 0x90, 0xca, 0xc1, 0xbc, 0x3a, 0x6f, 0x6d, 0x71, 0xaa, 0x44, 0x43, 0x65, 0x4c, 0x14, + ])) +}); /// Genesis block hash for Liquid mainnet. -pub static LIQUID_MAINNET_GENESIS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - elements::BlockHash::from_byte_array([ - 0x03, 0x60, 0x20, 0x8a, 0x88, 0x96, 0x92, 0x37, 0x2c, 0x8d, 0x68, 0xb0, 0x84, 0xa6, - 0x2e, 0xfd, 0xf6, 0x0e, 0xa1, 0xa3, 0x59, 0xa0, 0x4c, 0x94, 0xb2, 0x0d, 0x22, 0x36, - 0x58, 0x27, 0x66, 0x14, - ]) - }); +pub static LIQUID_MAINNET_GENESIS: std::sync::LazyLock = std::sync::LazyLock::new(|| { + elements::BlockHash::from_byte_array([ + 0x03, 0x60, 0x20, 0x8a, 0x88, 0x96, 0x92, 0x37, 0x2c, 0x8d, 0x68, 0xb0, 0x84, 0xa6, 0x2e, 0xfd, 0xf6, 0x0e, + 0xa1, 0xa3, 0x59, 0xa0, 0x4c, 0x94, 0xb2, 0x0d, 0x22, 0x36, 0x58, 0x27, 0x66, 0x14, + ]) +}); /// Genesis block hash for Liquid testnet. -pub static LIQUID_TESTNET_GENESIS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - elements::BlockHash::from_byte_array([ - 0xc1, 0xb1, 0x6a, 0xe2, 0x4f, 0x24, 0x23, 0xae, 0xa2, 0xea, 0x34, 0x55, 0x22, 0x92, - 0x79, 0x3b, 0x5b, 0x5e, 0x82, 0x99, 0x9a, 0x1e, 0xed, 0x81, 0xd5, 0x6a, 0xee, 0x52, - 0x8e, 0xda, 0x71, 0xa7, - ]) - }); +pub static LIQUID_TESTNET_GENESIS: std::sync::LazyLock = std::sync::LazyLock::new(|| { + elements::BlockHash::from_byte_array([ + 0xc1, 0xb1, 0x6a, 0xe2, 0x4f, 0x24, 0x23, 0xae, 0xa2, 0xea, 0x34, 0x55, 0x22, 0x92, 0x79, 0x3b, 0x5b, 0x5e, + 0x82, 0x99, 0x9a, 0x1e, 0xed, 0x81, 0xd5, 0x6a, 0xee, 0x52, 0x8e, 0xda, 0x71, 0xa7, + ]) +}); /// Genesis block hash for Liquid regtest. -pub static LIQUID_REGTEST_GENESIS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - elements::BlockHash::from_byte_array([ - 0x21, 0xca, 0xb1, 0xe5, 0xda, 0x47, 0x18, 0xea, 0x14, 0x0d, 0x97, 0x16, 0x93, 0x17, - 0x02, 0x42, 0x2f, 0x0e, 0x6a, 0xd9, 0x15, 0xc8, 0xd9, 0xb5, 0x83, 0xca, 0xc2, 0x70, - 0x6b, 0x2a, 0x90, 0x00, - ]) - }); +pub static LIQUID_REGTEST_GENESIS: std::sync::LazyLock = std::sync::LazyLock::new(|| { + elements::BlockHash::from_byte_array([ + 0x21, 0xca, 0xb1, 0xe5, 0xda, 0x47, 0x18, 0xea, 0x14, 0x0d, 0x97, 0x16, 0x93, 0x17, 0x02, 0x42, 0x2f, 0x0e, + 0x6a, 0xd9, 0x15, 0xc8, 0xd9, 0xb5, 0x83, 0xca, 0xc2, 0x70, 0x6b, 0x2a, 0x90, 0x00, + ]) +}); /// The network of the elements blockchain. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -113,9 +101,7 @@ impl SimplicityNetwork { pub fn policy_asset(&self) -> elements::AssetId { match self { Self::Liquid => elements::AssetId::from_str(LIQUID_POLICY_ASSET_STR).unwrap(), - Self::LiquidTestnet => { - elements::AssetId::from_str(LIQUID_TESTNET_POLICY_ASSET_STR).unwrap() - } + Self::LiquidTestnet => elements::AssetId::from_str(LIQUID_TESTNET_POLICY_ASSET_STR).unwrap(), Self::ElementsRegtest { policy_asset } => *policy_asset, } } diff --git a/crates/sdk/src/simplicityhl_core/error.rs b/crates/core/src/error.rs similarity index 92% rename from crates/sdk/src/simplicityhl_core/error.rs rename to crates/core/src/error.rs index a8b4179..22b1afb 100644 --- a/crates/sdk/src/simplicityhl_core/error.rs +++ b/crates/core/src/error.rs @@ -41,17 +41,11 @@ pub enum ProgramError { Execution(simplicityhl::simplicity::bit_machine::ExecutionError), #[error("UTXO index {input_index} out of bounds (have {utxo_count} UTXOs)")] - UtxoIndexOutOfBounds { - input_index: usize, - utxo_count: usize, - }, + UtxoIndexOutOfBounds { input_index: usize, utxo_count: usize }, /// Returned when the UTXO's script does not match the expected program address. #[error("Script pubkey mismatch: expected hash {expected_hash}, got {actual_hash}")] - ScriptPubkeyMismatch { - expected_hash: String, - actual_hash: String, - }, + ScriptPubkeyMismatch { expected_hash: String, actual_hash: String }, #[error("Input index exceeds u32 maximum: {0}")] InputIndexOverflow(#[from] std::num::TryFromIntError), diff --git a/crates/sdk/src/simplicityhl_core/fee_rate_fetcher.rs b/crates/core/src/fee_rate_fetcher.rs similarity index 96% rename from crates/sdk/src/simplicityhl_core/fee_rate_fetcher.rs rename to crates/core/src/fee_rate_fetcher.rs index 663c84a..1036740 100644 --- a/crates/sdk/src/simplicityhl_core/fee_rate_fetcher.rs +++ b/crates/core/src/fee_rate_fetcher.rs @@ -65,8 +65,7 @@ pub trait SyncFeeFetcher { // Fall back to higher targets (lower fee rates) // Available targets: 1-25, 144, 504, 1008 let fallback_targets = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, 144, 504, 1008, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 144, 504, 1008, ]; for &target in fallback_targets.iter().filter(|&&t| t >= target_blocks) { @@ -84,9 +83,7 @@ pub trait SyncFeeFetcher { } } - Err(FeeFetcherError::Request( - "No fee estimates available".to_string(), - )) + Err(FeeFetcherError::Request("No fee estimates available".to_string())) } } diff --git a/crates/sdk/src/simplicityhl_core/mod.rs b/crates/core/src/lib.rs similarity index 90% rename from crates/sdk/src/simplicityhl_core/mod.rs rename to crates/core/src/lib.rs index 2c698ed..2a59260 100644 --- a/crates/sdk/src/simplicityhl_core/mod.rs +++ b/crates/core/src/lib.rs @@ -8,13 +8,13 @@ mod error; mod fee_rate_fetcher; mod runner; mod scripts; +mod tx_inclusion; #[cfg(feature = "encoding")] pub mod encoding { + use crate::EncodingError; pub use bincode::{Decode, Encode}; - use crate::simplicityhl_core::error::EncodingError; - /// Trait for binary encoding/decoding with hex string support. pub trait Encodable { /// Encode to binary bytes. @@ -23,9 +23,9 @@ pub mod encoding { /// Returns error if encoding fails. fn encode(&self) -> Result, EncodingError> where - Self: Encode, + Self: serde::Serialize, { - Ok(bincode::encode_to_vec(self, bincode::config::standard())?) + Ok(bincode::serde::encode_to_vec(self, bincode::config::standard())?) } /// Decode from binary bytes. @@ -34,9 +34,9 @@ pub mod encoding { /// Returns error if decoding fails. fn decode(buf: &[u8]) -> Result where - Self: Sized + Decode<()>, + Self: Sized + serde::de::DeserializeOwned, { - Ok(bincode::decode_from_slice(buf, bincode::config::standard())?.0) + Ok(bincode::serde::decode_from_slice(buf, bincode::config::standard())?.0) } /// Encode to hex string. @@ -45,7 +45,7 @@ pub mod encoding { /// Returns error if encoding fails. fn to_hex(&self) -> Result where - Self: Encode, + Self: serde::Serialize, { Ok(hex::encode(Encodable::encode(self)?)) } @@ -56,7 +56,7 @@ pub mod encoding { /// Returns error if hex decoding or binary decoding fails. fn from_hex(hex: &str) -> Result where - Self: bincode::Decode<()>, + Self: serde::de::DeserializeOwned, { Encodable::decode(&hex::decode(hex)?) } @@ -72,6 +72,7 @@ pub use error::EncodingError; pub use runner::*; pub use scripts::*; +pub use tx_inclusion::*; pub use fee_rate_fetcher::*; @@ -116,9 +117,7 @@ pub fn get_p2pk_address( /// /// # Errors /// Returns error if program compilation fails. -pub fn get_p2pk_program( - account_public_key: &XOnlyPublicKey, -) -> Result { +pub fn get_p2pk_program(account_public_key: &XOnlyPublicKey) -> Result { let arguments = simplicityhl::Arguments::from(HashMap::from([( WitnessName::from_str_unchecked("PUBLIC_KEY"), Value::u256(U256::from_byte_array(account_public_key.serialize())), @@ -169,17 +168,9 @@ pub fn create_p2pk_signature( let x_only_public_key = keypair.x_only_public_key().0; let p2pk_program = get_p2pk_program(&x_only_public_key)?; - let env = get_and_verify_env( - tx, - &p2pk_program, - &x_only_public_key, - utxos, - network, - input_index, - )?; - - let sighash_all = - elements::secp256k1_zkp::Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); + let env = get_and_verify_env(tx, &p2pk_program, &x_only_public_key, utxos, network, input_index)?; + + let sighash_all = elements::secp256k1_zkp::Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); Ok(keypair.sign_schnorr(sighash_all)) } @@ -209,14 +200,7 @@ pub fn finalize_p2pk_transaction( ) -> Result { let p2pk_program = get_p2pk_program(x_only_public_key)?; - let env = get_and_verify_env( - &tx, - &p2pk_program, - x_only_public_key, - utxos, - network, - input_index, - )?; + let env = get_and_verify_env(&tx, &p2pk_program, x_only_public_key, utxos, network, input_index)?; let pruned = execute_p2pk_program(&p2pk_program, schnorr_signature, &env, log_level)?; @@ -253,14 +237,7 @@ pub fn finalize_transaction( network: SimplicityNetwork, log_level: TrackerLogLevel, ) -> Result { - let env = get_and_verify_env( - &tx, - program, - program_public_key, - utxos, - network, - input_index, - )?; + let env = get_and_verify_env(&tx, program, program_public_key, utxos, network, input_index)?; let pruned = run_program(program, witness_values, &env, log_level)?.0; diff --git a/crates/sdk/src/simplicityhl_core/runner.rs b/crates/core/src/runner.rs similarity index 96% rename from crates/sdk/src/simplicityhl_core/runner.rs rename to crates/core/src/runner.rs index 7ee797e..5695e45 100644 --- a/crates/sdk/src/simplicityhl_core/runner.rs +++ b/crates/core/src/runner.rs @@ -12,7 +12,7 @@ use simplicityhl::simplicity::{BitMachine, RedeemNode, Value}; use simplicityhl::tracker::{DefaultTracker, TrackerLogLevel}; use simplicityhl::{CompiledProgram, WitnessValues}; -use crate::simplicityhl_core::error::ProgramError; +use crate::error::ProgramError; /// Satisfy and execute a compiled program in the provided environment. /// Returns the pruned program and the resulting value. diff --git a/crates/sdk/src/simplicityhl_core/scripts.rs b/crates/core/src/scripts.rs similarity index 83% rename from crates/sdk/src/simplicityhl_core/scripts.rs rename to crates/core/src/scripts.rs index 8ecc896..0b82ac6 100644 --- a/crates/sdk/src/simplicityhl_core/scripts.rs +++ b/crates/core/src/scripts.rs @@ -2,23 +2,20 @@ use sha2::{Digest, Sha256}; -use simplicityhl::elements::{ - Address, AddressParams, AssetId, ContractHash, OutPoint, Script, script, taproot, -}; +use simplicityhl::elements::{Address, AddressParams, AssetId, ContractHash, OutPoint, Script, script, taproot}; use simplicityhl::simplicity::bitcoin::{XOnlyPublicKey, secp256k1}; use simplicityhl::simplicity::hashes::{Hash, sha256}; use simplicityhl::{Arguments, CompiledProgram}; -use crate::simplicityhl_core::error::ProgramError; +use crate::error::ProgramError; /// Load program source and compile it to a Simplicity program. /// /// # Errors /// Returns error if the program fails to compile. pub fn load_program(source: &str, arguments: Arguments) -> Result { - let compiled = - CompiledProgram::new(source, arguments, true).map_err(ProgramError::Compilation)?; + let compiled = CompiledProgram::new(source, arguments, true).map_err(ProgramError::Compilation)?; Ok(compiled) } @@ -64,14 +61,10 @@ fn taproot_spending_info( /// # Panics /// Panics if the taproot tree is invalid (should never happen with valid CMR). #[must_use] -pub fn control_block( - cmr: simplicityhl::simplicity::Cmr, - internal_key: XOnlyPublicKey, -) -> taproot::ControlBlock { +pub fn control_block(cmr: simplicityhl::simplicity::Cmr, internal_key: XOnlyPublicKey) -> taproot::ControlBlock { let info = taproot_spending_info(cmr, internal_key); let script_ver = script_version(cmr); - info.control_block(&script_ver) - .expect("control block should exist") + info.control_block(&script_ver).expect("control block should exist") } /// SHA256 hash of an address's scriptPubKey bytes. diff --git a/crates/sdk/src/simplicityhl_core/source_simf/p2pk.simf b/crates/core/src/source_simf/p2pk.simf similarity index 100% rename from crates/sdk/src/simplicityhl_core/source_simf/p2pk.simf rename to crates/core/src/source_simf/p2pk.simf diff --git a/crates/core/src/tx_inclusion.rs b/crates/core/src/tx_inclusion.rs new file mode 100644 index 0000000..80cc7fa --- /dev/null +++ b/crates/core/src/tx_inclusion.rs @@ -0,0 +1,173 @@ +//! Transaction inclusion verification using Merkle proofs for Liquid/Elements blocks. +//! +//! This module provides SPV (Simplified Payment Verification) functionality to prove +//! a transaction exists in a block without downloading all transactions. + +use simplicityhl::elements::hashes::{Hash, HashEngine}; +use simplicityhl::elements::{Block, TxMerkleNode, Txid}; + +/// Merkle proof: (`transaction_index`, `sibling_hashes`) +pub type MerkleProof = (usize, Vec); + +/// Constructs a Merkle inclusion proof (Merkle branch). +/// +/// For a transaction TXID in a block, using Bitcoin consensus Merkle tree construction rules +/// (pairwise double-SHA256 hashing with odd-hash duplication). +/// +/// Liquid inherits the same Merkle tree semantics via the Elements codebase: +/// +/// +/// Returns `None` if the transaction is not present in the block. +#[must_use] +pub fn merkle_branch(tx: &Txid, block: &Block) -> Option { + if block.txdata.is_empty() { + return None; + } + + let tx_index = block.txdata.iter().position(|t| &t.txid() == tx)?; + + Some((tx_index, build_merkle_branch(tx_index, block))) +} + +/// Verifies a Merkle inclusion proof (Merkle branch). +/// +/// For a transaction TXID against the given Merkle root using Bitcoin consensus Merkle tree rules +/// (pairwise double-SHA256 hashing with left/right ordering). +/// +/// Liquid inherits the same Merkle tree semantics via the Elements codebase: +/// +/// +/// Returns `true` if the proof commits the transaction to the given root. +#[must_use] +pub fn verify_tx(tx: &Txid, root: &TxMerkleNode, proof: &MerkleProof) -> bool { + root.eq(&compute_merkle_root_from_branch(tx, proof.0, &proof.1)) +} + +fn build_merkle_branch(tx_index: usize, block: &Block) -> Vec { + if block.txdata.is_empty() || block.txdata.len() == 1 { + return vec![]; + } + + let mut branch = vec![]; + let mut layer = block + .txdata + .iter() + .map(|tx| TxMerkleNode::from_raw_hash(*tx.txid().as_raw_hash())) + .collect::>(); + let mut index = tx_index; + + // Bottom-up traversal: pair nodes, hash parents, collect siblings along path to root + while layer.len() > 1 { + let mut next_layer = vec![]; + + for i in (0..layer.len()).step_by(2) { + let left = layer[i]; + let right = if i + 1 < layer.len() { layer[i + 1] } else { layer[i] }; + + let mut eng = TxMerkleNode::engine(); + eng.input(left.as_raw_hash().as_byte_array()); + eng.input(right.as_raw_hash().as_byte_array()); + + next_layer.push(TxMerkleNode::from_engine(eng)); + + if index / 2 == i / 2 { + let sibling = if index.is_multiple_of(2) { right } else { left }; + branch.push(sibling); + } + } + + index /= 2; + layer = next_layer; + } + + branch +} + +fn compute_merkle_root_from_branch(tx: &Txid, tx_index: usize, branch: &[TxMerkleNode]) -> TxMerkleNode { + let mut res = TxMerkleNode::from_raw_hash(*tx.as_raw_hash()); + let mut pos = tx_index; + + for leaf in branch { + let mut eng = TxMerkleNode::engine(); + + if pos & 1 == 0 { + eng.input(res.as_raw_hash().as_byte_array()); + eng.input(leaf.as_raw_hash().as_byte_array()); + } else { + eng.input(leaf.as_raw_hash().as_byte_array()); + eng.input(res.as_raw_hash().as_byte_array()); + } + res = TxMerkleNode::from_engine(eng); + + pos >>= 1; + } + + res +} + +#[cfg(test)] +mod test { + + use super::*; + + /// Taken from rust-elements + /// + macro_rules! hex_deserialize( + ($e:expr) => ({ + use simplicityhl::elements::encode::deserialize; + + fn hex_char(c: char) -> u8 { + match c { + '0' => 0, + '1' => 1, + '2' => 2, + '3' => 3, + '4' => 4, + '5' => 5, + '6' => 6, + '7' => 7, + '8' => 8, + '9' => 9, + 'a' | 'A' => 10, + 'b' | 'B' => 11, + 'c' | 'C' => 12, + 'd' | 'D' => 13, + 'e' | 'E' => 14, + 'f' | 'F' => 15, + x => panic!("Invalid character {} in hex string", x), + } + } + + let mut ret = Vec::with_capacity($e.len() / 2); + let mut byte = 0; + for (ch, store) in $e.chars().zip([false, true].iter().cycle()) { + byte = (byte << 4) + hex_char(ch); + if *store { + ret.push(byte); + byte = 0; + } + } + deserialize(&ret).expect("deserialize object") + }); + ); + + // Unfortunately, `hex_deserialize` macro aforehead returns error trying deserialize + // blocks from elements-cli regtest, so this block, taken from `elements::Block::block`, is + // the only test case I have found so far. + const BLOCK_STR: &str = include_str!("./assets/test-tx-incl-block.hex"); + + #[test] + fn test_merkle_branch_construction() { + let block: Block = hex_deserialize!(BLOCK_STR); + + assert_eq!(block.txdata.len(), 3); + + let tx = block.txdata[1].txid(); + let proof = merkle_branch(&tx, &block).expect("Failed to find tx in block"); + + assert!( + verify_tx(&tx, &block.header.merkle_root, &proof), + "Invalid merkle proof" + ); + } +} diff --git a/crates/explorer/Cargo.toml b/crates/explorer/Cargo.toml new file mode 100644 index 0000000..174d99f --- /dev/null +++ b/crates/explorer/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "simplex-explorer" +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 + +[features] + + +[dependencies] +async-trait = { version = "0.1.89" } +simplicityhl = { workspace = true } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +thiserror = { version = "2.0.18" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +url = { version = "2.5.8" } +lazy_static = { version = "1.5.0", features = ["spin_no_std"] } +hex-simd = "0.8.0" +bitcoin_hashes = "0.14.1" + +[dev-dependencies] +tokio = {version = "1.49.0", features = ["full"]} diff --git a/crates/explorer/README.md b/crates/explorer/README.md new file mode 100644 index 0000000..40ed1c5 --- /dev/null +++ b/crates/explorer/README.md @@ -0,0 +1,7 @@ +# Simplex-explorer + +Waterfall API doc - [link][1]. +Esplora API doc - [link][2]. + +[1]: https://github.com/RCasatta/waterfalls/blob/master/API.md +[1]: https://github.com/Blockstream/esplora/blob/master/API.md \ No newline at end of file diff --git a/crates/explorer/api-esplora.md b/crates/explorer/api-esplora.md new file mode 100644 index 0000000..4d909f8 --- /dev/null +++ b/crates/explorer/api-esplora.md @@ -0,0 +1,442 @@ +# 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/crates/explorer/api-waterfall.md b/crates/explorer/api-waterfall.md new file mode 100644 index 0000000..a9fdf5d --- /dev/null +++ b/crates/explorer/api-waterfall.md @@ -0,0 +1,364 @@ +# 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/crates/explorer/src/error.rs b/crates/explorer/src/error.rs new file mode 100644 index 0000000..a8d7ab2 --- /dev/null +++ b/crates/explorer/src/error.rs @@ -0,0 +1,89 @@ +use reqwest::StatusCode; +use url::Url; + +#[derive(thiserror::Error, Debug, Clone)] +pub enum ExplorerError { + #[error("Failed to type to Url, {0}")] + UrlConversion(String), + + #[error("url")] + UrlParsing(#[from] url::ParseError), + + #[error("Failed to send request, [url: '{url:?}', code: {status:?}, text: '{text}']")] + Request { + url: Option, + status: Option, + text: String, + }, + + #[error("Erroneous response, [url: '{url:?}', code: {status:?}, text: '{text}']")] + ErroneousRequest { + url: Option, + status: Option, + text: String, + }, + + #[error("Failed to deserialize response, [url: '{url:?}', code: {status:?}, text: '{text}']")] + Deserialize { + url: Option, + status: Option, + text: String, + }, + + #[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: String, + }, + + #[error("Failed to decode hex string using hex_simd, error: {0}")] + HexSimdDecode(String), + + #[error("Failed to deserialize Transaction from hex, error: {0}")] + TransactionDecode(String), +} + +#[derive(Debug, Clone)] +pub enum CommitmentType { + Asset, + Nonce, + Value, +} + +impl ExplorerError { + #[inline] + pub(crate) fn response_failed(e: &reqwest::Error) -> Self { + ExplorerError::Request { + url: e.url().cloned(), + status: e.status(), + text: e.to_string(), + } + } + + #[inline] + pub(crate) fn erroneous_response(e: &reqwest::Response) -> Self { + ExplorerError::ErroneousRequest { + url: Some(e.url().clone()), + status: Some(e.status()), + text: String::new(), + } + } + + #[inline] + pub(crate) fn deserialize(e: &reqwest::Error) -> Self { + ExplorerError::Deserialize { + url: e.url().cloned(), + status: e.status(), + text: e.to_string(), + } + } +} diff --git a/crates/explorer/src/esplora/mod.rs b/crates/explorer/src/esplora/mod.rs new file mode 100644 index 0000000..8db8a26 --- /dev/null +++ b/crates/explorer/src/esplora/mod.rs @@ -0,0 +1,1073 @@ +mod types; + +// TODO(Illia): remove #[allow(dead_code)] + +use crate::error::ExplorerError; +use crate::esplora::deserializable::TypeConversion; +use simplicityhl::elements::pset::serialize::Deserialize; +use simplicityhl::elements::{BlockHash, Txid}; +use std::str::FromStr; + +const ESPLORA_LIQUID_TESTNET: &str = "https://blockstream.info/liquidtestnet/api"; +const ESPLORA_LIQUID: &str = "https://blockstream.info/liquid/api"; + +pub struct EsploraClient { + base_url: String, + client: reqwest::Client, +} + +#[derive(Debug, Clone)] +pub struct EsploraClientBuilder { + url: Option, +} + +#[allow(dead_code)] +pub struct EsploraConfig { + url: String, +} + +// TODO: Illia add caching as optional parameter +// TODO: Add api backend trait implementation +impl EsploraClientBuilder { + fn default_url() -> String { + ESPLORA_LIQUID_TESTNET.to_string() + } + + pub fn liquid_testnet() -> Self { + Self { + url: Some(ESPLORA_LIQUID_TESTNET.to_string()), + } + } + + pub fn liquid_mainnet() -> Self { + Self { + url: Some(ESPLORA_LIQUID.to_string()), + } + } + + pub fn custom(url: impl Into) -> Self { + // todo: remove trailling slash + EsploraClientBuilder { url: Some(url.into()) } + } + + pub fn build(self) -> EsploraClient { + EsploraClient { + base_url: self.url.unwrap_or(Self::default_url()), + client: reqwest::Client::new(), + } + } +} + +impl Default for EsploraClientBuilder { + fn default() -> Self { + EsploraClientBuilder::liquid_testnet() + } +} + +impl Default for EsploraClient { + fn default() -> Self { + EsploraClientBuilder::default().build() + } +} + +mod deserializable { + use crate::error::{CommitmentType, ExplorerError}; + use crate::esplora::types; + use crate::esplora::types::Stats; + use bitcoin_hashes::sha256d::Hash; + use simplicityhl::elements::confidential::{Asset, Nonce, Value}; + use simplicityhl::elements::{Address, AssetId, BlockHash, OutPoint, Script, TxMerkleNode, Txid}; + use std::str::FromStr; + + pub(crate) trait TypeConversion { + fn convert(self) -> Result; + } + + #[derive(serde::Deserialize)] + pub struct EsploraTransaction { + pub txid: String, + pub version: u32, + pub locktime: u32, + pub size: u64, + pub weight: u64, + pub fee: u64, + pub vin: Vec, + pub vout: Vec, + pub status: TxStatus, + pub discount_vsize: u64, + pub discount_weight: u64, + } + + #[allow(dead_code)] + #[derive(serde::Deserialize)] + pub struct Vin { + pub txid: String, + pub vout: u32, + pub is_coinbase: bool, + pub scriptsig: String, + pub scriptsig_asm: String, + pub inner_redeemscript_asm: Option, + pub inner_witnessscript_asm: Option, + pub sequence: u32, + #[serde(default)] + pub witness: Vec, + pub prevout: Option, + } + + #[derive(serde::Deserialize)] + pub struct Vout { + pub scriptpubkey: String, + pub scriptpubkey_asm: String, + pub scriptpubkey_type: String, + pub scriptpubkey_address: Option, + pub value: Option, + } + + #[derive(serde::Deserialize)] + pub struct TxStatus { + pub confirmed: bool, + pub block_height: Option, + pub block_hash: Option, + pub block_time: Option, + } + + #[derive(serde::Deserialize)] + pub struct AddressUtxo { + pub txid: String, + pub vout: u32, + pub status: TxStatus, + #[serde(flatten)] + pub utxo_info: UtxoInfo, + } + + #[derive(serde::Deserialize)] + #[serde(untagged)] + pub enum UtxoInfo { + Confidential { + valuecommitment: String, + assetcommitment: String, + noncecommitment: String, + }, + Explicit { + value: u64, + asset: String, + }, + } + + #[derive(serde::Deserialize)] + pub struct AddressInfo { + pub address: String, + pub chain_stats: types::ChainStats, + pub mempool_stats: types::MempoolStats, + } + + #[derive(serde::Deserialize)] + pub struct MerkleProof { + pub block_height: u64, + pub merkle: Vec, + pub pos: u64, + } + + #[derive(serde::Deserialize)] + pub struct Outspend { + pub spent: bool, + pub txid: Option, + pub vin: Option, + pub status: Option, + } + + #[allow(dead_code)] + #[derive(serde::Deserialize)] + pub struct MempoolRecent { + pub txid: String, + pub fee: u64, + pub vsize: u64, + pub discount_vsize: u64, + } + + #[derive(serde::Deserialize)] + pub struct ScripthashInfo { + pub scripthash: String, + pub chain_stats: Stats, + pub mempool_stats: Stats, + } + + #[derive(serde::Deserialize)] + pub struct Block { + pub id: String, + pub height: u64, + pub version: u32, + pub timestamp: u64, + pub mediantime: u64, + pub merkle_root: String, + pub tx_count: u64, + pub size: u64, + pub weight: u64, + pub previousblockhash: String, + pub ext: Option, + } + + #[allow(dead_code)] + #[derive(serde::Deserialize)] + #[serde(untagged)] + pub enum BlockExtDataRaw { + Proof { + challenge: String, + solution: String, + }, + Dynafed { + current: DynafedParamsRaw, + proposed: DynafedParamsRaw, + signblock_witness: Vec>, + }, + } + + #[allow(dead_code)] + #[derive(serde::Deserialize)] + #[serde(untagged)] + pub enum DynafedParamsRaw { + Null {}, + Compact { + signblockscript: String, + signblock_witness_limit: u32, + elided_root: String, + }, + } + + impl TypeConversion for TxStatus { + fn convert(self) -> Result { + let block_hash = match self.block_hash { + None => None, + Some(val) => match BlockHash::from_str(&val) { + Ok(x) => Some(x), + Err(e) => return Err(ExplorerError::BitcoinHashesHex(e)), + }, + }; + Ok(types::TxStatus { + confirmed: self.confirmed, + block_height: self.block_height, + block_hash, + block_time: self.block_time, + }) + } + } + + impl TypeConversion for AddressUtxo { + fn convert(self) -> Result { + let block_hash = self.status.block_hash.map(|hash| BlockHash::from_str(&hash)); + let block_hash = match block_hash { + None => None, + Some(Err(err)) => return Err(ExplorerError::BitcoinHashesHex(err)), + Some(Ok(x)) => Some(x), + }; + let utxo_info = match self.utxo_info { + UtxoInfo::Confidential { + assetcommitment, + noncecommitment, + valuecommitment, + } => types::UtxoInfo::Confidential { + asset_comm: Asset::from_commitment( + &hex_simd::decode_to_vec(assetcommitment) + .map_err(|e| ExplorerError::HexSimdDecode(e.to_string()))?, + ) + .map_err(|e| ExplorerError::CommitmentDecode { + commitment_type: CommitmentType::Asset, + error: e.to_string(), + })?, + value_comm: Value::from_commitment( + &hex_simd::decode_to_vec(valuecommitment) + .map_err(|e| ExplorerError::HexSimdDecode(e.to_string()))?, + ) + .map_err(|e| ExplorerError::CommitmentDecode { + commitment_type: CommitmentType::Asset, + error: e.to_string(), + })?, + nonce_comm: Nonce::from_commitment( + &hex_simd::decode_to_vec(noncecommitment) + .map_err(|e| ExplorerError::HexSimdDecode(e.to_string()))?, + ) + .map_err(|e| ExplorerError::CommitmentDecode { + commitment_type: CommitmentType::Asset, + error: e.to_string(), + })?, + }, + UtxoInfo::Explicit { asset, value } => types::UtxoInfo::Explicit { + value, + asset: AssetId::from_str(&asset).map_err(ExplorerError::BitcoinHashesHex)?, + }, + }; + + Ok(types::AddressUtxo { + outpoint: OutPoint::new(Txid::from_str(&self.txid)?, self.vout), + status: types::TxStatus { + confirmed: self.status.confirmed, + block_height: self.status.block_height, + block_hash, + block_time: self.status.block_time, + }, + utxo_info, + }) + } + } + + impl TypeConversion for MerkleProof { + fn convert(self) -> Result { + let hashes = self + .merkle + .into_iter() + .map(|x| Hash::from_str(&x)) + .collect::, bitcoin_hashes::hex::HexToArrayError>>()?; + let merkle_proofs = hashes.into_iter().map(TxMerkleNode::from_raw_hash).collect(); + Ok(types::MerkleProof { + block_height: self.block_height, + merkle: merkle_proofs, + pos: self.pos, + }) + } + } + + impl TypeConversion for AddressInfo { + fn convert(self) -> Result { + Ok(types::AddressInfo { + address: Address::from_str(&self.address) + .map_err(|e| ExplorerError::AddressConversion(e.to_string()))?, + chain_stats: self.chain_stats, + mempool_stats: self.mempool_stats, + }) + } + } + + impl TypeConversion for EsploraTransaction { + fn convert(self) -> Result { + let status = self.status.convert()?; + let vin = self.vin.into_iter().map(|x| x.convert()).collect::>()?; + let vout = self.vout.into_iter().map(|x| x.convert()).collect::>()?; + + Ok(types::EsploraTransaction { + txid: Txid::from_str(&self.txid)?, + version: self.version, + locktime: self.locktime, + size: self.size, + weight: self.weight, + fee: self.fee, + vin, + vout, + status, + discount_vsize: self.discount_vsize, + discount_weight: self.discount_weight, + }) + } + } + + impl TypeConversion for Vout { + fn convert(self) -> Result { + Ok(types::Vout { + scriptpubkey: Script::from_str(&self.scriptpubkey).map_err(ExplorerError::ElementsHex)?, + scriptpubkey_asm: self.scriptpubkey_asm, + scriptpubkey_type: self.scriptpubkey_type, + scriptpubkey_address: self.scriptpubkey_address, + value: self.value, + }) + } + } + impl TypeConversion for Vin { + fn convert(self) -> Result { + let prevout = match self.prevout { + None => None, + Some(val) => Some(val.convert()?), + }; + + Ok(types::Vin { + out_point: Default::default(), + is_coinbase: self.is_coinbase, + scriptsig: self.scriptsig, + scriptsig_asm: self.scriptsig_asm, + inner_redeemscript_asm: self.inner_redeemscript_asm, + inner_witnessscript_asm: self.inner_witnessscript_asm, + sequence: self.sequence, + witness: self.witness, + prevout, + }) + } + } + + impl TypeConversion for Outspend { + fn convert(self) -> Result { + let status = match self.status { + None => None, + Some(val) => Some(val.convert()?), + }; + let txid = match self.txid { + None => None, + Some(val) => Some(Txid::from_str(&val)?), + }; + + Ok(types::Outspend { + spent: self.spent, + txid, + vin: self.vin, + status, + }) + } + } + + impl TypeConversion for MempoolRecent { + fn convert(self) -> Result { + Ok(types::MempoolRecent { + txid: Txid::from_str(&self.txid)?, + fee: 0, + vsize: 0, + discount_vsize: 0, + }) + } + } + + impl TypeConversion for ScripthashInfo { + fn convert(self) -> Result { + Ok(types::ScripthashInfo { + scripthash: Script::from_str(&self.scripthash).map_err(ExplorerError::ElementsHex)?, + chain_stats: self.chain_stats, + mempool_stats: self.mempool_stats, + }) + } + } + + impl TypeConversion for Block { + fn convert(self) -> Result { + let ext = match self.ext { + None => None, + Some(val) => Some(val.convert()?), + }; + Ok(types::Block { + id: self.id, + height: self.height, + version: self.version, + timestamp: self.timestamp, + tx_count: self.tx_count, + size: self.size, + weight: self.weight, + merkle_root: TxMerkleNode::from_str(&self.merkle_root)?, + mediantime: self.mediantime, + previousblockhash: BlockHash::from_str(&self.previousblockhash)?, + ext, + }) + } + } + + impl TypeConversion for BlockExtDataRaw { + fn convert(self) -> Result { + todo!() + } + } +} + +impl EsploraClient { + #[inline] + fn join_url(&self, str: impl AsRef) -> Result { + Ok(format!("{}/{}", self.base_url, str.as_ref())) + } + + #[inline] + fn filter_resp(resp: &reqwest::Response) -> Result<(), ExplorerError> { + if !(200..300).contains(&resp.status().as_u16()) { + return Err(ExplorerError::erroneous_response(resp)); + } + Ok(()) + } + + pub async fn get_tx(&self, txid: &str) -> Result { + let url = self.join_url(format!("/tx/{txid}"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::deserialize(&e))?; + let resp = resp.convert()?; + + Ok(resp) + } + + pub async fn get_tx_status(&self, txid: &str) -> Result { + let url = self.join_url(format!("tx/{txid}/status"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::deserialize(&e))?; + let resp = resp.convert()?; + Ok(resp) + } + + pub async fn get_tx_hex(&self, txid: &str) -> Result { + let url = self.join_url(format!("tx/{txid}/hex"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + resp.text().await.map_err(|e| ExplorerError::deserialize(&e)) + } + + pub async fn get_tx_raw(&self, txid: &str) -> Result, ExplorerError> { + let url = self.join_url(format!("tx/{txid}/raw"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + resp.bytes() + .await + .map(|b| b.to_vec()) + .map_err(|e| ExplorerError::deserialize(&e)) + } + + pub async fn get_tx_elements(&self, txid: &str) -> Result { + let bytes = self.get_tx_raw(txid).await?; + simplicityhl::elements::Transaction::deserialize(&bytes) + .map_err(|e| ExplorerError::TransactionDecode(e.to_string())) + } + + pub async fn get_tx_merkle_proof(&self, txid: &str) -> Result { + let url = self.join_url(format!("tx/{txid}/merkle-proof"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.convert()?; + Ok(resp) + } + + pub async fn get_tx_outspend(&self, txid: &str, vout: u32) -> Result { + let url = self.join_url(format!("tx/{txid}/outspend/{vout}"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.convert()?; + Ok(resp) + } + + pub async fn get_tx_outspends(&self, txid: &str) -> Result, ExplorerError> { + let url = self.join_url(format!("tx/{txid}/outspends"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + resp.into_iter().map(|x| x.convert()).collect::, _>>() + } + + pub async fn broadcast_tx(&self, tx: &simplicityhl::elements::Transaction) -> Result { + let tx_hex = simplicityhl::elements::encode::serialize_hex(tx); + let url = self.join_url("tx")?; + let resp = self + .client + .post(url) + .body(tx_hex) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) + } + + // TODO: add batch execution with 10 elements + pub async fn broadcast_tx_package( + &self, + txs: &[simplicityhl::elements::Transaction], + ) -> Result { + let url = self.join_url("txs/package")?; + let tx_hexes = txs + .iter() + .map(simplicityhl::elements::encode::serialize_hex) + .collect::>(); + + let resp = self + .client + .post(url) + .json(&tx_hexes) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + resp.json().await.map_err(|e| ExplorerError::response_failed(&e)) + } + + pub async fn get_address(&self, address: &str) -> Result { + let url = self.join_url(format!("address/{address}"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.convert()?; + + Ok(resp) + } + + pub async fn get_address_txs(&self, address: &str) -> Result, ExplorerError> { + let url = self.join_url(format!("address/{address}/txs"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + Ok(resp) + } + + pub async fn get_address_txs_chain( + &self, + address: &str, + last_seen_txid: Option<&str>, + ) -> Result, ExplorerError> { + let url = if let Some(txid) = last_seen_txid { + self.join_url(format!("address/{address}/txs/chain/{txid}"))? + } else { + self.join_url(format!("address/{address}/txs/chain"))? + }; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + Ok(resp) + } + + pub async fn get_address_txs_mempool( + &self, + address: &str, + ) -> Result, ExplorerError> { + let url = self.join_url(format!("address/{address}/txs/mempool"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + Ok(resp) + } + + pub async fn get_address_utxo(&self, address: &str) -> Result, ExplorerError> { + let url = self.join_url(format!("address/{address}/utxo"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + resp.into_iter().map(|x| x.convert()).collect::, _>>() + } + + pub async fn get_scripthash(&self, hash: &str) -> Result { + let url = self.join_url(format!("scripthash/{hash}"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.convert()?; + Ok(resp) + } + + // TODO: check output + pub async fn get_scripthash_txs(&self, hash: &str) -> Result { + let url = self.join_url(format!("scripthash/{hash}/txs"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) + } + + // TODO: check output + pub async fn get_scripthash_txs_chain( + &self, + hash: &str, + last_seen_txid: Option<&str>, + ) -> Result { + let url = if let Some(txid) = last_seen_txid { + self.join_url(format!("scripthash/{hash}/txs/chain/{txid}"))? + } else { + self.join_url(format!("scripthash/{hash}/txs/chain"))? + }; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) + } + + // TODO: check output + pub async fn get_scripthash_txs_mempool(&self, hash: &str) -> Result { + let url = self.join_url(format!("scripthash/{hash}/txs/mempool"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) + } + + // TODO: check output + pub async fn get_scripthash_utxo(&self, hash: &str) -> Result { + let url = self.join_url(format!("scripthash/{hash}/utxo"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + resp.text().await.map_err(|e| ExplorerError::response_failed(&e)) + } + + pub async fn get_block(&self, hash: &str) -> Result { + let url = self.join_url(format!("block/{hash}"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.convert()?; + Ok(resp) + } + + // TODO: decode hex into elements::BlockHeader (no method to do this) + pub async fn get_block_header(&self, hash: &str) -> Result { + let url = self.join_url(format!("block/{hash}/header"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.text().await.map_err(|e| ExplorerError::response_failed(&e))?; + Ok(resp) + } + + pub async fn get_block_status(&self, hash: &str) -> Result { + let url = self.join_url(format!("block/{hash}/status"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + resp.json::() + .await + .map_err(|e| ExplorerError::response_failed(&e)) + } + + pub async fn get_block_txs( + &self, + hash: &str, + start_index: Option, + ) -> Result, ExplorerError> { + let url = if let Some(index) = start_index { + self.join_url(format!("block/{hash}/txs/{index}"))? + } else { + self.join_url(format!("block/{hash}/txs"))? + }; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.into_iter().map(|val| val.convert()).collect::>()?; + Ok(resp) + } + + pub async fn get_block_txids(&self, hash: &str) -> Result, ExplorerError> { + let url = self.join_url(format!("block/{hash}/txids"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + + let resp = resp + .into_iter() + .map(|val| Txid::from_str(&val)) + .collect::>()?; + Ok(resp) + } + + pub async fn get_block_txid(&self, hash: &str, index: u32) -> Result { + let url = self.join_url(format!("block/{hash}/txid/{index}"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.text().await.map_err(|e| ExplorerError::response_failed(&e))?; + + Ok(Txid::from_str(&resp)?) + } + + pub async fn get_block_raw(&self, hash: &str) -> Result, ExplorerError> { + let url = self.join_url(format!("block/{hash}/raw"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + resp.bytes() + .await + .map(|b| b.to_vec()) + .map_err(|e| ExplorerError::response_failed(&e)) + } + + pub async fn get_block_height(&self, height: u64) -> Result { + let url = self.join_url(format!("block-height/{height}"))?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.text().await.map_err(|e| ExplorerError::response_failed(&e))?; + let resp = BlockHash::from_str(&resp)?; + Ok(resp) + } + + pub async fn get_blocks(&self, start_height: Option) -> Result, ExplorerError> { + let url = if let Some(height) = start_height { + self.join_url(format!("blocks/{}", height))? + } else { + self.join_url("blocks")? + }; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.into_iter().map(|val| val.convert()).collect::>()?; + Ok(resp) + } + + pub async fn get_blocks_tip_height(&self) -> Result { + let url = self.join_url("blocks/tip/height")?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Ok(resp) + } + + pub async fn get_blocks_tip_hash(&self) -> Result { + let url = self.join_url("blocks/tip/hash")?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp.text().await.map_err(|e| ExplorerError::response_failed(&e))?; + let resp = BlockHash::from_str(&resp)?; + Ok(resp) + } + + pub async fn get_mempool(&self) -> Result { + let url = self.join_url("mempool")?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + resp.json().await.map_err(|e| ExplorerError::response_failed(&e)) + } + + pub async fn get_mempool_txids(&self) -> Result, ExplorerError> { + let url = self.join_url("mempool/txids")?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp + .into_iter() + .map(|val| Txid::from_str(&val)) + .collect::>()?; + Ok(resp) + } + + pub async fn get_mempool_recent(&self) -> Result, ExplorerError> { + let url = self.join_url("mempool/recent")?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + let resp = resp + .json::>() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + let resp = resp.into_iter().map(|x| x.convert()).collect::>()?; + Ok(resp) + } + + pub async fn get_fee_estimates(&self) -> Result { + let url = self.join_url("fee-estimates")?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(|e| ExplorerError::response_failed(&e))?; + Self::filter_resp(&resp)?; + + resp.json::() + .await + .map_err(|e| ExplorerError::response_failed(&e)) + } +} diff --git a/crates/explorer/src/esplora/types.rs b/crates/explorer/src/esplora/types.rs new file mode 100644 index 0000000..95dfda9 --- /dev/null +++ b/crates/explorer/src/esplora/types.rs @@ -0,0 +1,161 @@ +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 { + pub funded_txo_count: u64, + pub spent_txo_count: u64, + pub tx_count: u64, +} + +#[derive(Debug, Clone, Deserialize, Hash, Eq, PartialEq)] +pub struct Stats { + pub tx_count: u64, + pub funded_txo_count: u64, + pub spent_txo_count: u64, +} + +#[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/crates/explorer/src/lib.rs b/crates/explorer/src/lib.rs new file mode 100644 index 0000000..dd55acf --- /dev/null +++ b/crates/explorer/src/lib.rs @@ -0,0 +1,7 @@ +pub mod error; +pub mod esplora; +mod traits; +pub mod waterfall; + +pub use esplora::EsploraClient; +// pub use waterfall::WaterfallClient; diff --git a/crates/explorer/src/traits.rs b/crates/explorer/src/traits.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/explorer/src/traits.rs @@ -0,0 +1 @@ + diff --git a/crates/explorer/src/waterfall/mod.rs b/crates/explorer/src/waterfall/mod.rs new file mode 100644 index 0000000..3a02aba --- /dev/null +++ b/crates/explorer/src/waterfall/mod.rs @@ -0,0 +1,319 @@ +mod types; + +// pub struct WaterfallClient { +// base_url: String, +// client: reqwest::Client, +// } + +// impl WaterfallClient { +// pub fn new(base_url: impl Into) -> Self { +// Self { +// base_url: base_url.into(), +// client: reqwest::Client::new(), +// } +// } +// +// fn url(&self, path: &str) -> String { +// format!( +// "{}/{}", +// self.base_url.trim_end_matches('/'), +// path.trim_start_matches('/') +// ) +// } +// +// // Waterfalls v2 endpoints (JSON) +// pub async fn waterfalls_v2( +// &self, +// descriptor: &str, +// page: Option, +// to_index: Option, +// utxo_only: bool, +// ) -> Result<(WaterfallResponse, reqwest::header::HeaderMap), reqwest::Error> { +// let mut url = self.url(&format!("v2/waterfalls?descriptor={}", urlencoding::encode(descriptor))); +// +// if let Some(p) = page { +// url.push_str(&format!("&page={}", p)); +// } +// if let Some(idx) = to_index { +// url.push_str(&format!("&to_index={}", idx)); +// } +// if utxo_only { +// url.push_str("&utxo_only=true"); +// } +// +// let response = self.client.get(&url).send().await?; +// let headers = response.headers().clone(); +// let data = response.json().await?; +// Ok((data, headers)) +// } +// +// pub async fn waterfalls_v2_addresses( +// &self, +// addresses: &[String], +// page: Option, +// utxo_only: bool, +// ) -> Result<(WaterfallResponse, reqwest::header::HeaderMap), reqwest::Error> { +// let mut url = self.url(&format!("v2/waterfalls?addresses={}", addresses.join(","))); +// +// if let Some(p) = page { +// url.push_str(&format!("&page={}", p)); +// } +// if utxo_only { +// url.push_str("&utxo_only=true"); +// } +// +// let response = self.client.get(&url).send().await?; +// let headers = response.headers().clone(); +// let data = response.json().await?; +// Ok((data, headers)) +// } +// +// pub async fn waterfalls_v2_utxo_only( +// &self, +// descriptor: &str, +// to_index: Option, +// ) -> Result<(WaterfallResponse, reqwest::header::HeaderMap), reqwest::Error> { +// self.waterfalls_v2(descriptor, None, to_index, true).await +// } +// +// // Waterfalls v4 endpoints (JSON with extended tip metadata) +// pub async fn waterfalls_v4( +// &self, +// descriptor: &str, +// page: Option, +// to_index: Option, +// utxo_only: bool, +// ) -> Result<(WaterfallResponseV4, reqwest::header::HeaderMap), reqwest::Error> { +// let mut url = self.url(&format!("v4/waterfalls?descriptor={}", urlencoding::encode(descriptor))); +// +// if let Some(p) = page { +// url.push_str(&format!("&page={}", p)); +// } +// if let Some(idx) = to_index { +// url.push_str(&format!("&to_index={}", idx)); +// } +// if utxo_only { +// url.push_str("&utxo_only=true"); +// } +// +// let response = self.client.get(&url).send().await?; +// let headers = response.headers().clone(); +// let data = response.json().await?; +// Ok((data, headers)) +// } +// +// pub async fn waterfalls_v4_addresses( +// &self, +// addresses: &[String], +// page: Option, +// utxo_only: bool, +// ) -> Result<(WaterfallResponseV4, reqwest::header::HeaderMap), reqwest::Error> { +// let mut url = self.url(&format!("v4/waterfalls?addresses={}", addresses.join(","))); +// +// if let Some(p) = page { +// url.push_str(&format!("&page={}", p)); +// } +// if utxo_only { +// url.push_str("&utxo_only=true"); +// } +// +// let response = self.client.get(&url).send().await?; +// let headers = response.headers().clone(); +// let data = response.json().await?; +// Ok((data, headers)) +// } +// +// pub async fn waterfalls_v4_utxo_only( +// &self, +// descriptor: &str, +// to_index: Option, +// ) -> Result<(WaterfallResponseV4, reqwest::header::HeaderMap), reqwest::Error> { +// self.waterfalls_v4(descriptor, None, to_index, true).await +// } +// +// // Waterfalls v1 endpoint (for compatibility) +// pub async fn waterfalls_v1( +// &self, +// descriptor: &str, +// page: Option, +// to_index: Option, +// utxo_only: bool, +// ) -> Result<(WaterfallResponse, reqwest::header::HeaderMap), reqwest::Error> { +// let mut url = self.url(&format!("v1/waterfalls?descriptor={}", urlencoding::encode(descriptor))); +// +// if let Some(p) = page { +// url.push_str(&format!("&page={}", p)); +// } +// if let Some(idx) = to_index { +// url.push_str(&format!("&to_index={}", idx)); +// } +// if utxo_only { +// url.push_str("&utxo_only=true"); +// } +// +// let response = self.client.get(&url).send().await?; +// let headers = response.headers().clone(); +// let data = response.json().await?; +// Ok((data, headers)) +// } +// +// // CBOR endpoints +// pub async fn waterfalls_v2_cbor( +// &self, +// descriptor: &str, +// page: Option, +// to_index: Option, +// utxo_only: bool, +// ) -> Result<(Vec, reqwest::header::HeaderMap), reqwest::Error> { +// let mut url = self.url(&format!( +// "v2/waterfalls.cbor?descriptor={}", +// urlencoding::encode(descriptor) +// )); +// +// if let Some(p) = page { +// url.push_str(&format!("&page={}", p)); +// } +// if let Some(idx) = to_index { +// url.push_str(&format!("&to_index={}", idx)); +// } +// if utxo_only { +// url.push_str("&utxo_only=true"); +// } +// +// let response = self.client.get(&url).send().await?; +// let headers = response.headers().clone(); +// let data = response.bytes().await?.to_vec(); +// Ok((data, headers)) +// } +// +// pub async fn waterfalls_v4_cbor( +// &self, +// descriptor: &str, +// page: Option, +// to_index: Option, +// utxo_only: bool, +// ) -> Result<(Vec, reqwest::header::HeaderMap), reqwest::Error> { +// let mut url = self.url(&format!( +// "v4/waterfalls.cbor?descriptor={}", +// urlencoding::encode(descriptor) +// )); +// +// if let Some(p) = page { +// url.push_str(&format!("&page={}", p)); +// } +// if let Some(idx) = to_index { +// url.push_str(&format!("&to_index={}", idx)); +// } +// if utxo_only { +// url.push_str("&utxo_only=true"); +// } +// +// let response = self.client.get(&url).send().await?; +// let headers = response.headers().clone(); +// let data = response.bytes().await?.to_vec(); +// Ok((data, headers)) +// } +// +// // Last used index endpoint +// pub async fn last_used_index(&self, descriptor: &str) -> Result { +// self.client +// .get(&self.url(&format!( +// "v1/last_used_index?descriptor={}", +// urlencoding::encode(descriptor) +// ))) +// .send() +// .await? +// .json() +// .await +// } +// +// // Server information endpoints +// pub async fn server_recipient(&self) -> Result { +// self.client +// .get(&self.url("v1/server_recipient")) +// .send() +// .await? +// .text() +// .await +// } +// +// pub async fn server_address(&self) -> Result { +// self.client +// .get(&self.url("v1/server_address")) +// .send() +// .await? +// .text() +// .await +// } +// +// pub async fn time_since_last_block(&self) -> Result { +// self.client +// .get(&self.url("v1/time_since_last_block")) +// .send() +// .await? +// .text() +// .await +// } +// +// pub async fn build_info(&self) -> Result { +// self.client.get(&self.url("v1/build_info")).send().await?.json().await +// } +// +// // Blockchain data endpoints +// pub async fn tip_hash(&self) -> Result { +// self.client.get(&self.url("blocks/tip/hash")).send().await?.text().await +// } +// +// pub async fn block_hash_by_height(&self, height: u64) -> Result { +// self.client +// .get(&self.url(&format!("block-height/{}", height))) +// .send() +// .await? +// .text() +// .await +// } +// +// pub async fn block_header(&self, hash: &str) -> Result { +// self.client +// .get(&self.url(&format!("block/{}/header", hash))) +// .send() +// .await? +// .text() +// .await +// } +// +// pub async fn tx_raw(&self, txid: &str) -> Result, reqwest::Error> { +// self.client +// .get(&self.url(&format!("tx/{}/raw", txid))) +// .send() +// .await? +// .bytes() +// .await +// .map(|b| b.to_vec()) +// } +// +// pub async fn address_txs(&self, address: &str) -> Result, reqwest::Error> { +// self.client +// .get(&self.url(&format!("address/{}/txs", address))) +// .send() +// .await? +// .json() +// .await +// } +// +// // Transaction broadcasting +// pub async fn broadcast(&self, tx_hex: &str) -> Result { +// self.client +// .post(&self.url("tx")) +// .body(tx_hex.to_string()) +// .send() +// .await? +// .text() +// .await +// } +// +// // Prometheus metrics +// pub async fn metrics(&self) -> Result { +// self.client.get(&self.url("metrics")).send().await?.text().await +// } +// } diff --git a/crates/explorer/src/waterfall/types.rs b/crates/explorer/src/waterfall/types.rs new file mode 100644 index 0000000..3c1650a --- /dev/null +++ b/crates/explorer/src/waterfall/types.rs @@ -0,0 +1,62 @@ +// use serde::{Deserialize, Serialize}; +// use std::collections::HashMap; +// +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct WaterfallResponse { +// pub txs_seen: HashMap>, +// pub page: u32, +// #[serde(skip_serializing_if = "Option::is_none")] +// pub tip: Option, +// } +// +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct WaterfallResponseV4 { +// pub txs_seen: HashMap>, +// pub page: u32, +// pub tip_meta: TipMeta, +// } +// +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct TxSeen { +// pub txid: String, +// pub height: u64, +// pub block_hash: String, +// pub block_timestamp: u64, +// pub v: u32, +// } +// +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct TipMeta { +// pub b: String, // block hash +// pub t: u64, // timestamp +// pub h: u64, // height +// } +// +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct LastUsedIndex { +// #[serde(skip_serializing_if = "Option::is_none")] +// pub external: Option, +// #[serde(skip_serializing_if = "Option::is_none")] +// pub internal: Option, +// #[serde(skip_serializing_if = "Option::is_none")] +// pub tip: Option, +// } +// +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct BuildInfo { +// pub version: String, +// pub git_commit: String, +// } +// +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct AddressTxs { +// pub txid: String, +// pub status: AddressTxStatus, +// } +// +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct AddressTxStatus { +// pub block_height: u64, +// #[serde(skip_serializing_if = "Option::is_none")] +// pub block_hash: Option, +// } diff --git a/crates/macros-core/Cargo.toml b/crates/macros-core/Cargo.toml new file mode 100644 index 0000000..964592d --- /dev/null +++ b/crates/macros-core/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "simplex-macro-core" +description = "Macro support core for Simplex, the Rust SimplicityHl toolkit. Not intended to be used directly." +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[features] +bincode = [] +serde = ["bincode"] +default = ["bincode", "serde"] + + +[dependencies] +proc-macro-error = { version = "1.0" } +proc-macro2 = { version = "1.0.106", features = ["span-locations"] } +syn = { version = "2.0.114", default-features = false, features = ["proc-macro", "full", "parsing", "derive", "clone-impls", "extra-traits", "printing"] } +thiserror = { version = "2.0.18" } +quote = { version = "1.0.44" } + +simplicityhl = { workspace = true } diff --git a/crates/macros-core/src/attr/codegen.rs b/crates/macros-core/src/attr/codegen.rs new file mode 100644 index 0000000..c1625db --- /dev/null +++ b/crates/macros-core/src/attr/codegen.rs @@ -0,0 +1,339 @@ +use crate::attr::SimfContent; +use crate::attr::types::RustType; +use quote::{format_ident, quote}; +use simplicityhl::str::WitnessName; +use simplicityhl::{AbiMeta, Parameters, ResolvedType, WitnessTypes}; + +pub struct SimfContractMeta { + pub contract_source_const_name: proc_macro2::Ident, + pub args_struct: WitnessStruct, + pub witness_struct: WitnessStruct, + pub simf_content: SimfContent, + pub abi_meta: AbiMeta, +} + +pub struct GeneratedArgumentTokens { + pub imports: proc_macro2::TokenStream, + pub struct_token_stream: proc_macro2::TokenStream, + pub struct_impl: proc_macro2::TokenStream, +} + +pub struct GeneratedWitnessTokens { + pub imports: proc_macro2::TokenStream, + pub struct_token_stream: proc_macro2::TokenStream, + pub struct_impl: proc_macro2::TokenStream, +} + +pub struct WitnessField { + witness_simf_name: String, + struct_rust_field: proc_macro2::Ident, + rust_type: RustType, +} + +pub struct WitnessStruct { + pub struct_name: proc_macro2::Ident, + pub witness_values: Vec, +} + +impl SimfContractMeta { + /// Try to create a new `SimfContractMeta` from `SimfContent` and `AbiMeta`. + /// + /// # Errors + /// Returns a `syn::Result` with an error if the arguments or witness structure cannot be generated. + pub fn try_from(simf_content: SimfContent, abi_meta: AbiMeta) -> syn::Result { + let args_struct = WitnessStruct::generate_args_struct(&simf_content.contract_name, &abi_meta.param_types)?; + let witness_struct = + WitnessStruct::generate_witness_struct(&simf_content.contract_name, &abi_meta.witness_types)?; + let contract_source_const_name = format_ident!("{}_CONTRACT_SOURCE", simf_content.contract_name.to_uppercase()); + Ok(SimfContractMeta { + contract_source_const_name, + args_struct, + witness_struct, + simf_content, + abi_meta, + }) + } +} + +impl WitnessField { + fn new(witness_name: &WitnessName, resolved_type: &ResolvedType) -> syn::Result { + let (witness_simf_name, struct_rust_field) = { + let w_name = witness_name.to_string(); + let r_name = format_ident!("{}", w_name.to_lowercase()); + (w_name, r_name) + }; + + let rust_type = RustType::from_resolved_type(resolved_type)?; + + Ok(Self { + witness_simf_name, + struct_rust_field, + rust_type, + }) + } + + /// Generate the conversion code from Rust value to Simplicity Value + fn to_token_stream(&self) -> proc_macro2::TokenStream { + let witness_name = &self.witness_simf_name; + let field_name = &self.struct_rust_field; + let conversion = self + .rust_type + .generate_to_simplicity_conversion("e! { self.#field_name }); + + quote! { + ( + ::simplicityhl::str::WitnessName::from_str_unchecked(#witness_name), + #conversion + ) + } + } +} + +impl WitnessStruct { + /// Generate the implementation for the arguments struct. + /// + /// # Errors + /// Returns a `syn::Result` with an error if the conversion from arguments map fails. + pub fn generate_arguments_impl(&self) -> syn::Result { + let generated_struct = self.generate_struct_token_stream(); + let struct_name = &self.struct_name; + let tuples: Vec = self.construct_witness_tuples(); + let (arguments_conversion_from_args_map, struct_to_return): ( + proc_macro2::TokenStream, + proc_macro2::TokenStream, + ) = self.generate_from_args_conversion_with_param_name("args"); + + Ok(GeneratedArgumentTokens { + imports: quote! { + use std::collections::HashMap; + use simplicityhl::{Arguments, Value, ResolvedType}; + use simplicityhl::value::{UIntValue, ValueInner}; + use simplicityhl::num::U256; + use simplicityhl::str::WitnessName; + use simplicityhl::types::TypeConstructible; + use simplicityhl::value::ValueConstructible; + use bincode::*; + }, + struct_token_stream: quote! { + #generated_struct + }, + struct_impl: quote! { + impl #struct_name { + /// Build Simplicity arguments for contract instantiation. + #[must_use] + pub fn build_arguments(&self) -> ::simplicityhl::Arguments { + ::simplicityhl::Arguments::from(HashMap::from([ + #(#tuples),* + ])) + } + + /// Build struct from Simplicity Arguments. + /// + /// # Errors + /// + /// Returns error if any required witness is missing, has wrong type, or has invalid value. + pub fn from_arguments(args: &Arguments) -> Result { + #arguments_conversion_from_args_map + + Ok(#struct_to_return) + } + + } + + impl simplex::serde::Serialize for #struct_name { + fn serialize(&self, serializer: S) -> Result + where + S: simplex::serde::Serializer, + { + self.build_arguments().serialize(serializer) + } + } + impl<'de> simplex::serde::Deserialize<'de> for #struct_name { + fn deserialize(deserializer: D) -> Result + where + 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 {} + }, + }) + } + + /// Generate the implementation for the witness struct. + /// + /// # Errors + /// Returns a `syn::Result` with an error if the conversion from witness values fails. + pub fn generate_witness_impl(&self) -> syn::Result { + let generated_struct = self.generate_struct_token_stream(); + let struct_name = &self.struct_name; + let tuples: Vec = self.construct_witness_tuples(); + let (arguments_conversion_from_args_map, struct_to_return): ( + proc_macro2::TokenStream, + proc_macro2::TokenStream, + ) = self.generate_from_args_conversion_with_param_name("witness"); + + 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; + }, + struct_token_stream: quote! { + #generated_struct + }, + struct_impl: quote! { + impl #struct_name { + /// Build Simplicity witness values for contract execution. + #[must_use] + pub fn build_witness(&self) -> ::simplicityhl::WitnessValues { + ::simplicityhl::WitnessValues::from(HashMap::from([ + #(#tuples),* + ])) + } + + /// Build struct from Simplicity WitnessValues. + /// + /// # Errors + /// + /// Returns error if any required witness is missing, has wrong type, or has invalid value. + pub fn from_witness(witness: &WitnessValues) -> Result { + #arguments_conversion_from_args_map + + Ok(#struct_to_return) + } + } + + impl simplex::serde::Serialize for #struct_name { + fn serialize(&self, serializer: S) -> Result + where + S: simplex::serde::Serializer, + { + self.build_witness().serialize(serializer) + } + } + + impl<'de> simplex::serde::Deserialize<'de> for #struct_name { + fn deserialize(deserializer: D) -> Result + where + 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 {} + }, + }) + } + + fn convert_contract_name_to_struct_name(contract_name: &str) -> String { + let words: Vec = contract_name + .split('_') + .filter(|w| !w.is_empty()) + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + } + }) + .collect(); + words.join("") + } + + fn generate_args_struct(contract_name: &str, meta: &Parameters) -> syn::Result { + let base_name = Self::convert_contract_name_to_struct_name(contract_name); + Ok(WitnessStruct { + struct_name: format_ident!("{}Arguments", base_name), + witness_values: WitnessStruct::generate_witness_fields(meta.iter())?, + }) + } + + fn generate_witness_struct(contract_name: &str, meta: &WitnessTypes) -> syn::Result { + let base_name = Self::convert_contract_name_to_struct_name(contract_name); + Ok(WitnessStruct { + struct_name: format_ident!("{}Witness", base_name), + witness_values: WitnessStruct::generate_witness_fields(meta.iter())?, + }) + } + fn generate_witness_fields<'a>( + iter: impl Iterator, + ) -> syn::Result> { + iter.map(|(name, resolved_type)| WitnessField::new(name, resolved_type)) + .collect() + } + + fn generate_struct_token_stream(&self) -> proc_macro2::TokenStream { + let name = format_ident!("{}", self.struct_name); + let fields: Vec = self + .witness_values + .iter() + .map(|field| { + let field_name = format_ident!("{}", field.struct_rust_field); + let field_type = field.rust_type.to_type_token_stream(); + quote! { pub #field_name: #field_type } + }) + .collect(); + quote! { + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct #name { + #(#fields),* + } + } + } + + #[inline] + fn construct_witness_tuples(&self) -> Vec { + self.witness_values.iter().map(WitnessField::to_token_stream).collect() + } + + /// Generate conversion code from Arguments/WitnessValues back to struct fields. + /// Returns a tuple of (`extraction_code`, `struct_initialization_code`). + fn generate_from_args_conversion_with_param_name( + &self, + param_name: &str, + ) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { + let param_ident = format_ident!("{}", param_name); + let field_extractions: Vec = self + .witness_values + .iter() + .map(|field| { + let field_name = &field.struct_rust_field; + let witness_name = &field.witness_simf_name; + let extraction = field + .rust_type + .generate_from_value_extraction(¶m_ident, witness_name); + quote! { + let #field_name = #extraction; + } + }) + .collect(); + + let field_names: Vec = self + .witness_values + .iter() + .map(|field| format_ident!("{}", field.struct_rust_field)) + .collect(); + + let extractions = quote! { + #(#field_extractions)* + }; + + let struct_init = quote! { + Self { + #(#field_names),* + } + }; + + (extractions, struct_init) + } +} diff --git a/crates/macros-core/src/attr/mod.rs b/crates/macros-core/src/attr/mod.rs new file mode 100644 index 0000000..55faded --- /dev/null +++ b/crates/macros-core/src/attr/mod.rs @@ -0,0 +1,153 @@ +pub mod codegen; +pub mod parse; +mod types; + +pub use parse::SimfContent; + +use crate::attr::codegen::{GeneratedArgumentTokens, GeneratedWitnessTokens, SimfContractMeta}; +use proc_macro2::Span; +use quote::{format_ident, 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. +/// +/// # Errors +/// Returns a `syn::Result` with an error if code generation fails. +pub fn expand_helpers(simf_content: SimfContent, meta: AbiMeta) -> syn::Result { + gen_helpers_inner(simf_content, meta).map_err(|e| syn::Error::new(Span::call_site(), e)) +} + +fn gen_helpers_inner(simf_content: SimfContent, meta: AbiMeta) -> Result> { + let mod_ident = format_ident!("derived_{}", simf_content.contract_name); + + let derived_meta = SimfContractMeta::try_from(simf_content, meta)?; + + let program_helpers = construct_program_helpers(&derived_meta); + let witness_helpers = construct_witness_helpers(&derived_meta)?; + let arguments_helpers = construct_argument_helpers(&derived_meta)?; + + Ok(quote! { + pub mod #mod_ident{ + #program_helpers + + #witness_helpers + + #arguments_helpers + } + }) +} + +fn construct_program_helpers(derived_meta: &SimfContractMeta) -> proc_macro2::TokenStream { + let contract_content = &derived_meta.simf_content.content; + let error_msg = format!( + "INTERNAL: expected '{}' Program to compile successfully.", + derived_meta.simf_content.contract_name + ); + let contract_source_name = &derived_meta.contract_source_const_name; + let contract_arguments_struct_name = &derived_meta.args_struct.struct_name; + + quote! { + use simplicityhl::elements::Address; + use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; + use simplex::simplex_core::{create_p2tr_address, load_program, ProgramError, SimplicityNetwork}; + use simplicityhl::CompiledProgram; + + 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) + } + + /// Derive P2TR address for an option offer contract. + /// + /// # Errors + /// + /// Returns error if program compilation fails. + pub fn get_option_offer_address( + x_only_public_key: &XOnlyPublicKey, + arguments: &#contract_arguments_struct_name, + network: SimplicityNetwork, + ) -> Result { + Ok(create_p2tr_address( + get_loaded_program(arguments)?.commit().cmr(), + x_only_public_key, + network.address_params(), + )) + } + + /// Compile option offer program with the given arguments. + /// + /// # Errors + /// + /// 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() + } + } +} + +fn construct_witness_helpers(derived_meta: &SimfContractMeta) -> syn::Result { + let GeneratedWitnessTokens { + imports, + struct_token_stream, + struct_impl, + } = derived_meta.witness_struct.generate_witness_impl()?; + + Ok(quote! { + pub use build_witness::*; + mod build_witness { + #imports + + #struct_token_stream + + #struct_impl + } + }) +} + +fn construct_argument_helpers(derived_meta: &SimfContractMeta) -> syn::Result { + let GeneratedArgumentTokens { + imports, + struct_token_stream, + struct_impl, + } = derived_meta.args_struct.generate_arguments_impl()?; + + Ok(quote! { + pub use build_arguments::*; + mod build_arguments { + #imports + + #struct_token_stream + + #struct_impl + } + }) +} diff --git a/crates/macros-core/src/attr/parse.rs b/crates/macros-core/src/attr/parse.rs new file mode 100644 index 0000000..680ce28 --- /dev/null +++ b/crates/macros-core/src/attr/parse.rs @@ -0,0 +1,188 @@ +use proc_macro2::Span; +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +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, +} + +impl Parse for SynFilePath { + fn parse(input: ParseStream) -> syn::Result { + let expr = input.parse::()?; + + let span_file = expr.span().file(); + let path_literal = match expr { + Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) => Ok(s.value()), + _ => Err(syn::Error::new(expr.span(), "Expected string literal")), + }?; + Ok(Self { + _span_file: span_file, + path_literal, + }) + } +} + +impl SynFilePath { + #[inline] + fn validate_path(&self) -> syn::Result { + let mut path = PathBuf::from_str(&self.path_literal).unwrap(); + + if !path.is_absolute() { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| { + syn::Error::new( + proc_macro2::Span::call_site(), + "CARGO_MANIFEST_DIR not set - macro must be used within a Cargo workspace", + ) + })?; + + let mut path_local = PathBuf::from(manifest_dir); + path_local.push(&self.path_literal); + + path = path_local; + } + + if is_not_a_file(&path) { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + format!( + "File not found, look path: '{}', is file: '{}', canonical: '{:?}'", + path.display(), + path.is_file(), + path.canonicalize() + ), + )); + } + Ok(path) + } +} + +pub struct SimfContent { + pub content: String, + pub contract_name: String, +} + +impl SimfContent { + /// Prepares a contract name for use as a Rust module/identifier. + /// + /// Converts the input to a valid lowercase Rust identifier by: + /// - Trimming whitespace + /// - Converting to lowercase + /// - Replacing invalid characters with underscores + /// - Ensuring it starts with a letter or underscore (not a digit) + /// - Validating it's not a reserved keyword + /// + /// # Errors + /// Returns an `std::io::Error` if: + /// - The contract name is empty after trimming. + /// - The contract name is a reserved Rust keyword. + /// - The contract name is not a valid Rust identifier. + /// + /// # Examples + /// - `"MyContract"` → `"mycontract"` + /// - `"My-Contract-V2"` → `"my_contract_v2"` + /// - `"123Invalid"` → Error (starts with digit) + /// - `"valid_name"` → `"valid_name"` + pub fn prepare_contract_name(name: &str) -> std::io::Result { + let trimmed = name.trim_matches(|c: char| c.is_whitespace()); + if trimmed.is_empty() { + return Err(std::io::Error::other("Contract name cannot be empty")); + } + + let mut result = trimmed.to_lowercase(); + + result = result + .chars() + .map(|c| if c.is_alphanumeric() || c == '_' { c } else { '_' }) + .collect(); + + while result.contains("__") { + result = result.replace("__", "_"); + } + + result = result.trim_matches('_').to_string(); + + if result.chars().next().is_some_and(|c| c.is_ascii_digit()) { + result = format!("_{result}"); + } + + if Self::is_reserved_keyword(&result) { + return Err(std::io::Error::other(format!( + "Contract name '{result}' is a reserved Rust keyword" + ))); + } + + if !Self::is_valid_rust_identifier(&result) { + return Err(std::io::Error::other(format!( + "Contract name '{result}' is not a valid Rust identifier" + ))); + } + + Ok(result) + } + + /// Checks if a string is a valid Rust identifier + #[inline] + fn is_valid_rust_identifier(s: &str) -> bool { + if s.is_empty() { + return false; + } + + let first = s.chars().next().unwrap(); + // First char must be letter or underscore + if !first.is_alphabetic() && first != '_' { + return false; + } + + s.chars().all(|c| c.is_alphanumeric() || c == '_') + } + + /// Checks if a string is a Rust reserved keyword (only checks keywords, not format) + /// + /// This function validates against Rust's actual reserved keywords. + /// Valid identifiers like "hello" will return false (not a keyword).#[inline] + fn is_reserved_keyword(s: &str) -> bool { + syn::parse_str::(s).is_err() + } + + fn extract_content_from_path(path: &PathBuf) -> std::io::Result { + let contract_name = { + let name = path + .file_prefix() + .ok_or(std::io::Error::other(format!( + "No file prefix in file: '{}'", + path.display() + )))? + .to_string_lossy(); + Self::prepare_contract_name(name.as_ref())? + }; + + let mut content = String::new(); + let mut x = File::open(path)?; + x.read_to_string(&mut content)?; + Ok(SimfContent { content, contract_name }) + } + + /// Evaluates the path expression and extracts Simf content. + /// + /// # Errors + /// Returns a `syn::Error` if the path is invalid or the file cannot be read. + pub fn eval_path_expr(syn_file_path: &SynFilePath) -> syn::Result { + let path = syn_file_path.validate_path()?; + Self::extract_content_from_path(&path).map_err(|e| syn::Error::new(Span::call_site(), e)) + } +} + +#[inline] +fn is_not_a_file(path: &Path) -> bool { + !path.is_file() +} diff --git a/crates/macros-core/src/attr/types.rs b/crates/macros-core/src/attr/types.rs new file mode 100644 index 0000000..18625f2 --- /dev/null +++ b/crates/macros-core/src/attr/types.rs @@ -0,0 +1,619 @@ +use quote::quote; +use simplicityhl::ResolvedType; +use std::fmt::Display; + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum RustType { + Bool, + U8, + U16, + U32, + U64, + U128, + U256Array, + Array(Box, usize), + Tuple(Vec), + Either(Box, Box), + Option(Box), +} + +#[derive(Debug, Clone, Copy)] +enum RustTypeContext { + Array, + Tuple, + Either, + EitherLeft, + EitherRight, + Option, +} + +impl Display for RustTypeContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + RustTypeContext::Array => "array element".to_string(), + RustTypeContext::Tuple => "tuple element".to_string(), + RustTypeContext::EitherLeft => "left either branch".to_string(), + RustTypeContext::EitherRight => "right either branch".to_string(), + RustTypeContext::Option => "option element".to_string(), + RustTypeContext::Either => "either element".to_string(), + }; + write!(f, "{str}") + } +} + +impl RustType { + pub fn from_resolved_type(ty: &ResolvedType) -> syn::Result { + use simplicityhl::types::{TypeInner, UIntType}; + + match ty.as_inner() { + TypeInner::Boolean => Ok(RustType::Bool), + TypeInner::UInt(uint_ty) => match uint_ty { + UIntType::U1 => Ok(RustType::Bool), + UIntType::U2 | UIntType::U4 | UIntType::U8 => Ok(RustType::U8), + UIntType::U16 => Ok(RustType::U16), + UIntType::U32 => Ok(RustType::U32), + UIntType::U64 => Ok(RustType::U64), + UIntType::U128 => Ok(RustType::U128), + UIntType::U256 => Ok(RustType::U256Array), + }, + TypeInner::Either(left, right) => { + let left_ty = Self::from_resolved_type(left)?; + let right_ty = Self::from_resolved_type(right)?; + Ok(RustType::Either(Box::new(left_ty), Box::new(right_ty))) + } + TypeInner::Option(inner) => { + let inner_ty = Self::from_resolved_type(inner)?; + Ok(RustType::Option(Box::new(inner_ty))) + } + TypeInner::Tuple(elements) => { + let element_types: syn::Result> = elements.iter().map(|e| Self::from_resolved_type(e)).collect(); + Ok(RustType::Tuple(element_types?)) + } + TypeInner::Array(element, size) => { + let element_ty = Self::from_resolved_type(element)?; + Ok(RustType::Array(Box::new(element_ty), *size)) + } + TypeInner::List(_, _) => Err(syn::Error::new( + proc_macro2::Span::call_site(), + "List types are not yet supported in macro conversions", + )), + _ => Err(syn::Error::new( + proc_macro2::Span::call_site(), + "Unsupported type in macro conversions", + )), + } + } + + /// Generate the Rust type as a `TokenStream` for struct field declarations + pub fn to_type_token_stream(&self) -> proc_macro2::TokenStream { + match self { + RustType::Bool => quote! { bool }, + RustType::U8 => quote! { u8 }, + RustType::U16 => quote! { u16 }, + RustType::U32 => quote! { u32 }, + RustType::U64 => quote! { u64 }, + RustType::U128 => quote! { u128 }, + RustType::U256Array => quote! { [u8; 32] }, + RustType::Array(element, size) => { + let element_ty = element.to_type_token_stream(); + quote! { [#element_ty; #size] } + } + RustType::Tuple(elements) => { + let element_types: Vec<_> = elements.iter().map(RustType::to_type_token_stream).collect(); + quote! { (#(#element_types),*) } + } + RustType::Either(left, right) => { + let left_ty = left.to_type_token_stream(); + let right_ty = right.to_type_token_stream(); + quote! { ::simplex::either::Either<#left_ty, #right_ty> } + } + RustType::Option(inner) => { + let inner_ty = inner.to_type_token_stream(); + quote! { Option<#inner_ty> } + } + } + } + + pub fn generate_to_simplicity_conversion(&self, value_expr: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { + match self { + RustType::Bool => { + quote! { Value::from(#value_expr) } + } + RustType::U8 => { + quote! { Value::from(UIntValue::U8(#value_expr)) } + } + RustType::U16 => { + quote! { Value::from(UIntValue::U16(#value_expr)) } + } + RustType::U32 => { + quote! { Value::from(UIntValue::U32(#value_expr)) } + } + RustType::U64 => { + quote! { Value::from(UIntValue::U64(#value_expr)) } + } + RustType::U128 => { + quote! { Value::from(UIntValue::U128(#value_expr)) } + } + RustType::U256Array => { + quote! { Value::from(UIntValue::U256(U256::from_byte_array(#value_expr))) } + } + RustType::Array(element, size) => { + let indices: Vec<_> = (0..*size).map(syn::Index::from).collect(); + let element_conversions: Vec<_> = indices + .iter() + .map(|idx| { + let elem_expr = quote! { #value_expr[#idx] }; + element.generate_to_simplicity_conversion(&elem_expr) + }) + .collect(); + + let elem_ty_generation = element.generate_simplicity_type_construction(); + + quote! { + { + let elements = [#(#element_conversions),*]; + Value::array(elements, #elem_ty_generation) + } + } + } + RustType::Tuple(elements) => { + if elements.is_empty() { + quote! { Value::unit() } + } else { + let tuple_conversions = elements.iter().enumerate().map(|(i, elem_ty)| { + let idx = syn::Index::from(i); + let elem_expr = quote! { #value_expr.#idx }; + elem_ty.generate_to_simplicity_conversion(&elem_expr) + }); + + quote! { + Value::tuple([#(#tuple_conversions),*]) + } + } + } + RustType::Either(left, right) => { + let left_conv = left.generate_to_simplicity_conversion("e! { left_val }); + let right_conv = right.generate_to_simplicity_conversion("e! { right_val }); + let left_ty = left.generate_simplicity_type_construction(); + let right_ty = right.generate_simplicity_type_construction(); + + quote! { + match &#value_expr { + ::simplex::either::Either::Left(left_val) => { + Value::left( + #left_conv, + #right_ty + ) + } + ::simplex::either::Either::Right(right_val) => { + Value::right( + #left_ty, + #right_conv + ) + } + } + } + } + RustType::Option(inner) => { + let inner_conv = inner.generate_to_simplicity_conversion("e! { inner_val }); + let inner_ty = inner.generate_simplicity_type_construction(); + + quote! { + match &#value_expr { + None => { + Value::none(#inner_ty) + } + Some(inner_val) => { + Value::some(#inner_conv) + } + } + } + } + } + } + + pub fn generate_simplicity_type_construction(&self) -> proc_macro2::TokenStream { + match self { + RustType::Bool => { + quote! { ResolvedType::boolean() } + } + RustType::U8 => { + quote! { ResolvedType::u8() } + } + RustType::U16 => { + quote! { ResolvedType::u16() } + } + RustType::U32 => { + quote! { ResolvedType::u32() } + } + RustType::U64 => { + quote! { ResolvedType::u64() } + } + RustType::U128 => { + quote! { ResolvedType::u128() } + } + RustType::U256Array => { + quote! { ResolvedType::u256() } + } + RustType::Array(element, size) => { + let elem_ty = element.generate_simplicity_type_construction(); + quote! { ResolvedType::array(#elem_ty, #size) } + } + RustType::Tuple(elements) => { + let elem_types: Vec<_> = elements + .iter() + .map(RustType::generate_simplicity_type_construction) + .collect(); + quote! { ResolvedType::tuple([#(#elem_types),*]) } + } + RustType::Either(left, right) => { + let left_ty = left.generate_simplicity_type_construction(); + let right_ty = right.generate_simplicity_type_construction(); + quote! { ResolvedType::either(#left_ty, #right_ty) } + } + RustType::Option(inner) => { + let inner_ty = inner.generate_simplicity_type_construction(); + quote! { ResolvedType::option(#inner_ty) } + } + } + } + + #[allow(clippy::too_many_lines)] + pub fn generate_from_value_extraction( + &self, + args_expr: &proc_macro2::Ident, + witness_name: &str, + ) -> proc_macro2::TokenStream { + match self { + RustType::Bool => { + quote! { + { + let witness_name = WitnessName::from_str_unchecked(#witness_name); + let value = #args_expr + .get(&witness_name) + .ok_or_else(|| format!("Missing witness: {}", #witness_name))?; + match value.inner() { + simplicityhl::value::ValueInner::Boolean(b) => *b, + _ => return Err(format!("Wrong type for {}: expected bool", #witness_name)), + } + } + } + } + RustType::U8 => { + quote! { + { + let witness_name = WitnessName::from_str_unchecked(#witness_name); + let value = #args_expr + .get(&witness_name) + .ok_or_else(|| format!("Missing witness: {}", #witness_name))?; + match value.inner() { + simplicityhl::value::ValueInner::UInt(UIntValue::U8(v)) => *v, + _ => return Err(format!("Wrong type for {}: expected U8", #witness_name)), + } + } + } + } + RustType::U16 => { + quote! { + { + let witness_name = WitnessName::from_str_unchecked(#witness_name); + let value = #args_expr + .get(&witness_name) + .ok_or_else(|| format!("Missing witness: {}", #witness_name))?; + match value.inner() { + simplicityhl::value::ValueInner::UInt(UIntValue::U16(v)) => *v, + _ => return Err(format!("Wrong type for {}: expected U16", #witness_name)), + } + } + } + } + RustType::U32 => { + quote! { + { + let witness_name = WitnessName::from_str_unchecked(#witness_name); + let value = #args_expr + .get(&witness_name) + .ok_or_else(|| format!("Missing witness: {}", #witness_name))?; + match value.inner() { + simplicityhl::value::ValueInner::UInt(UIntValue::U32(v)) => *v, + _ => return Err(format!("Wrong type for {}: expected U32", #witness_name)), + } + } + } + } + RustType::U64 => { + quote! { + { + let witness_name = WitnessName::from_str_unchecked(#witness_name); + let value = #args_expr + .get(&witness_name) + .ok_or_else(|| format!("Missing witness: {}", #witness_name))?; + match value.inner() { + simplicityhl::value::ValueInner::UInt(UIntValue::U64(v)) => *v, + _ => return Err(format!("Wrong type for {}: expected U64", #witness_name)), + } + } + } + } + RustType::U128 => { + quote! { + { + let witness_name = WitnessName::from_str_unchecked(#witness_name); + let value = #args_expr + .get(&witness_name) + .ok_or_else(|| format!("Missing witness: {}", #witness_name))?; + match value.inner() { + simplicityhl::value::ValueInner::UInt(UIntValue::U128(v)) => *v, + _ => return Err(format!("Wrong type for {}: expected U128", #witness_name)), + } + } + } + } + RustType::U256Array => { + quote! { + { + let witness_name = WitnessName::from_str_unchecked(#witness_name); + let value = #args_expr + .get(&witness_name) + .ok_or_else(|| format!("Missing witness: {}", #witness_name))?; + match value.inner() { + simplicityhl::value::ValueInner::UInt(UIntValue::U256(u256)) => u256.to_byte_array(), + _ => return Err(format!("Wrong type for {}: expected U256", #witness_name)), + } + } + } + } + RustType::Array(element, size) => { + let elem_extraction = + (0..*size).map(|i| element.generate_inline_array_element_extraction("e! { arr_value }, i)); + + quote! { + { + let witness_name = WitnessName::from_str_unchecked(#witness_name); + let value = #args_expr + .get(&witness_name) + .ok_or_else(|| format!("Missing witness: {}", #witness_name))?; + match value.inner() { + simplicityhl::value::ValueInner::Array(arr_value) => { + if arr_value.len() != #size { + return Err(format!("Wrong array length for {}: expected {}, got {}", #witness_name, #size, arr_value.len())); + } + [#(#elem_extraction),*] + } + _ => return Err(format!("Wrong type for {}: expected Array", #witness_name)), + } + } + } + } + RustType::Tuple(elements) => { + let elem_extractions: Vec<_> = elements + .iter() + .enumerate() + .map(|(i, elem_ty)| elem_ty.generate_inline_tuple_element_extraction("e! { tuple_value }, i)) + .collect(); + let elements_len = elements.len(); + + quote! { + { + let witness_name = WitnessName::from_str_unchecked(#witness_name); + let value = #args_expr + .get(&witness_name) + .ok_or_else(|| format!("Missing witness: {}", #witness_name))?; + match value.inner() { + simplicityhl::value::ValueInner::Tuple(tuple_value) => { + if tuple_value.len() != #elements_len { + return Err(format!("Wrong tuple length for {}", #witness_name)); + } + (#(#elem_extractions),*) + } + _ => return Err(format!("Wrong type for {}: expected Tuple", #witness_name)), + } + } + } + } + RustType::Either(left, right) => { + let left_extraction = left.generate_inline_either_extraction("e! { left_val }); + let right_extraction = right.generate_inline_either_extraction("e! { right_val }); + + quote! { + { + let witness_name = WitnessName::from_str_unchecked(#witness_name); + let value = #args_expr + .get(&witness_name) + .ok_or_else(|| format!("Missing witness: {}", #witness_name))?; + match value.inner() { + simplicityhl::value::ValueInner::Either(either_val) => { + match either_val { + ::simplex::either::Either::Left(left_val) => { + ::simplex::either::Either::Left(#left_extraction) + } + ::simplex::either::Either::Right(right_val) => { + ::simplex::either::Either::Right(#right_extraction) + } + } + } + _ => return Err(format!("Wrong type for {}: expected Either", #witness_name)), + } + } + } + } + RustType::Option(inner) => { + let inner_extraction = inner.generate_inline_either_extraction("e! { some_val }); + + quote! { + { + let witness_name = WitnessName::from_str_unchecked(#witness_name); + let value = #args_expr + .get(&witness_name) + .ok_or_else(|| format!("Missing witness: {}", #witness_name))?; + match value.inner() { + simplicityhl::value::ValueInner::Option(opt_val) => { + match opt_val { + None => None, + Some(some_val) => Some(#inner_extraction), + } + } + _ => return Err(format!("Wrong type for {}: expected Option", #witness_name)), + } + } + } + } + } + } + + #[allow(clippy::too_many_lines)] + fn generate_value_extraction_from_expr( + &self, + value_expr: &proc_macro2::TokenStream, + context: RustTypeContext, + ) -> proc_macro2::TokenStream { + let context = format!("{context:?}"); + match self { + RustType::Bool => quote! { + match #value_expr.inner() { + simplicityhl::value::ValueInner::Boolean(b) => *b, + _ => return Err(format!("Wrong type for {}: expected bool", #context)), + } + }, + RustType::U8 => quote! { + match #value_expr.inner() { + simplicityhl::value::ValueInner::UInt(UIntValue::U8(v)) => *v, + _ => return Err(format!("Wrong type for {}: expected U8", #context)), + } + }, + RustType::U16 => quote! { + match #value_expr.inner() { + simplicityhl::value::ValueInner::UInt(UIntValue::U16(v)) => *v, + _ => return Err(format!("Wrong type for {}: expected U16", #context)), + } + }, + RustType::U32 => quote! { + match #value_expr.inner() { + simplicityhl::value::ValueInner::UInt(UIntValue::U32(v)) => *v, + _ => return Err(format!("Wrong type for {}: expected U32", #context)), + } + }, + RustType::U64 => quote! { + match #value_expr.inner() { + simplicityhl::value::ValueInner::UInt(UIntValue::U64(v)) => *v, + _ => return Err(format!("Wrong type for {}: expected U64", #context)), + } + }, + RustType::U128 => quote! { + match #value_expr.inner() { + simplicityhl::value::ValueInner::UInt(UIntValue::U128(v)) => *v, + _ => return Err(format!("Wrong type for {}: expected U128", #context)), + } + }, + RustType::U256Array => quote! { + match #value_expr.inner() { + simplicityhl::value::ValueInner::UInt(UIntValue::U256(u256)) => u256.to_byte_array(), + _ => return Err(format!("Wrong type for {}: expected U256", #context)), + } + }, + RustType::Array(element, size) => { + let elem_extractions: Vec<_> = (0..*size) + .map(|i| { + element.generate_value_extraction_from_expr("e! { arr_val[#i] }, RustTypeContext::Array) + }) + .collect(); + + quote! { + match #value_expr.inner() { + simplicityhl::value::ValueInner::Array(arr_val) => { + if arr_val.len() != #size { + return Err(format!("Wrong array length for {}: expected {}, got {}", #context, #size, arr_val.len())); + } + [#(#elem_extractions),*] + } + _ => return Err(format!("Wrong type for {}: expected Array", #context)), + } + } + } + RustType::Tuple(elements) => { + let tuple_len = elements.len(); + let elem_extractions: Vec<_> = elements + .iter() + .enumerate() + .map(|(i, elem_ty)| { + elem_ty.generate_value_extraction_from_expr("e! { tuple_val[#i] }, RustTypeContext::Tuple) + }) + .collect(); + + quote! { + match #value_expr.inner() { + simplicityhl::value::ValueInner::Tuple(tuple_val) => { + if tuple_val.len() != #tuple_len { + return Err(format!("Wrong tuple length for {}", #context)); + } + (#(#elem_extractions),*) + } + _ => return Err(format!("Wrong type for {}: expected Tuple", #context)), + } + } + } + RustType::Either(left, right) => { + let left_extraction = + left.generate_value_extraction_from_expr("e! { left_val }, RustTypeContext::EitherLeft); + let right_extraction = + right.generate_value_extraction_from_expr("e! { right_val }, RustTypeContext::EitherRight); + + quote! { + match #value_expr.inner() { + simplicityhl::value::ValueInner::Either(either_val) => { + match either_val { + ::simplex::either::Either::Left(left_val) => { + ::simplex::either::Either::Left(#left_extraction) + } + ::simplex::either::Either::Right(right_val) => { + ::simplex::either::Either::Right(#right_extraction) + } + } + } + _ => return Err(format!("Wrong type for {}: expected Either", #context)), + } + } + } + RustType::Option(inner) => { + let inner_extraction = + inner.generate_value_extraction_from_expr("e! { some_val }, RustTypeContext::Option); + + quote! { + match #value_expr.inner() { + simplicityhl::value::ValueInner::Option(opt_val) => { + match opt_val { + None => None, + Some(some_val) => Some(#inner_extraction), + } + } + _ => return Err(format!("Wrong type for {}: expected Option", #context)), + } + } + } + } + } + + fn generate_inline_array_element_extraction( + &self, + arr_expr: &proc_macro2::TokenStream, + index: usize, + ) -> proc_macro2::TokenStream { + self.generate_value_extraction_from_expr("e! { #arr_expr[#index] }, RustTypeContext::Array) + } + + fn generate_inline_tuple_element_extraction( + &self, + tuple_expr: &proc_macro2::TokenStream, + index: usize, + ) -> proc_macro2::TokenStream { + self.generate_value_extraction_from_expr("e! { #tuple_expr[#index] }, RustTypeContext::Tuple) + } + + fn generate_inline_either_extraction(&self, val_expr: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { + let extraction = self.generate_value_extraction_from_expr(val_expr, RustTypeContext::Either); + quote! { + #extraction + } + } +} diff --git a/crates/macros-core/src/lib.rs b/crates/macros-core/src/lib.rs new file mode 100644 index 0000000..17cc241 --- /dev/null +++ b/crates/macros-core/src/lib.rs @@ -0,0 +1,25 @@ +#![warn(clippy::all, clippy::pedantic)] + +pub mod attr; + +pub(crate) mod program; + +/// Expands the `include_simf` macro. +/// +/// # Errors +/// Returns a `syn::Result` with an error if parsing, compilation, or expansion fails. +pub fn expand_include_simf(input: &attr::parse::SynFilePath) -> syn::Result { + let simf_content = attr::SimfContent::eval_path_expr(input)?; + let abi_meta = program::compile_simf(&simf_content)?; + let generated = attr::expand_helpers(simf_content, abi_meta)?; + + Ok(generated) +} + +/// Expands the `test` macro. +/// +/// # Errors +/// Returns a `syn::Result` with an error if expansion fails. +pub fn expand_test(_args: &proc_macro2::TokenStream, _input: &syn::ItemFn) -> syn::Result { + todo!() +} diff --git a/crates/macros-core/src/program.rs b/crates/macros-core/src/program.rs new file mode 100644 index 0000000..67cd275 --- /dev/null +++ b/crates/macros-core/src/program.rs @@ -0,0 +1,13 @@ +use crate::attr::parse::SimfContent; +use proc_macro2::Span; +use simplicityhl::{AbiMeta, TemplateProgram}; +use std::error::Error; + +pub fn compile_simf(content: &SimfContent) -> syn::Result { + compile_program_inner(content).map_err(|e| syn::Error::new(Span::call_site(), e)) +} + +fn compile_program_inner(content: &SimfContent) -> Result> { + let program = content.content.as_str(); + Ok(TemplateProgram::new(program)?.generate_abi_meta()?) +} diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml new file mode 100644 index 0000000..f15fbc3 --- /dev/null +++ b/crates/macros/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "simplex-macros" +version = "0.1.0" +edition = "2024" +rust-version = "1.90.0" +description = "High-level helpers for delivering boilerplate code for SimplicityHL contracts" +license = "MIT OR Apache-2.0" +repository = "https://github.com/BlockstreamResearch/simplicity-contracts" +homepage = "https://github.com/BlockstreamResearch/simplicity-contracts/tree/dev/crates/simplicityhl-core" +readme = "README.md" +documentation = "https://docs.rs/simplicityhl-core" +keywords = ["simplicity", "liquid", "bitcoin", "elements", "macro"] +categories = ["cryptography::cryptocurrencies"] + +[lib] +proc-macro = true + +[features] +macros = [] +derive = [] +default = ["macros", "derive",] +serde = ["macros", "dep:serde"] + +[lints] +workspace = true + + +[dependencies] +simplex-macro-core = { path = "../macros-core", features = ["bincode", "serde"] } +serde = { version = "1.0.228", optional = true } +syn = { version = "2.0.114", default-features = false, features = ["parsing", "proc-macro"] } + diff --git a/crates/sdk/README.md b/crates/macros/README.md similarity index 85% rename from crates/sdk/README.md rename to crates/macros/README.md index d31ac58..c52f23b 100644 --- a/crates/sdk/README.md +++ b/crates/macros/README.md @@ -1,4 +1,4 @@ -# Simplex simplicity +# Simplex SDK diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs new file mode 100644 index 0000000..8179b38 --- /dev/null +++ b/crates/macros/src/lib.rs @@ -0,0 +1,26 @@ +use proc_macro::TokenStream; + +// TODO(Illia): add path to exported crates to be able users to use their own https://stackoverflow.com/questions/79595543/rust-how-to-re-export-3rd-party-crate +// #[serde(crate = "exporter::reexports::serde")] +// simplicityhl, either + +#[cfg(feature = "macros")] +#[proc_macro] +pub fn include_simf(tokenstream: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(tokenstream as simplex_macro_core::attr::parse::SynFilePath); + match simplex_macro_core::expand_include_simf(&input) { + Ok(ts) => ts.into(), + Err(e) => e.to_compile_error().into(), + } +} + +#[cfg(feature = "macros")] +#[proc_macro_attribute] +pub fn test(args: TokenStream, input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as syn::ItemFn); + + match simplex_macro_core::expand_test(&args.into(), &input) { + Ok(ts) => ts.into(), + Err(e) => e.to_compile_error().into(), + } +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs deleted file mode 100644 index eb5a76d..0000000 --- a/crates/sdk/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod simplicityhl_core; diff --git a/crates/simplex/Cargo.toml b/crates/simplex/Cargo.toml new file mode 100644 index 0000000..e185769 --- /dev/null +++ b/crates/simplex/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "simplex" +version = "0.1.0" +edition = "2024" +rust-version = "1.90" +description = "High-level helpers for compiling and executing Simplicity programs on Liquid" +license = "MIT OR Apache-2.0" +repository = "https://github.com/BlockstreamResearch/simplex" +readme = "README.md" +documentation = "https://docs.rs/simplex" +keywords = ["simplicity", "liquid", "bitcoin", "elements", "taproot"] +categories = ["cryptography::cryptocurrencies"] + +[lints] +workspace = true + +[features] +default = ["macros", "encoding", "core"] +encoding = ["dep:bincode"] +macros = ["dep:simplex-macros"] +core = ["dep:simplex-core"] + +[dependencies] +simplex-macros = { path = "../macros", features = [], optional = true } +simplex-core = { path = "../core", features = ["encoding"], optional = true } + +bincode = { workspace = true, optional = true } +simplicityhl = { workspace = true, features = ["serde"] } +serde = { version = "1.0.228" } +either = { version = "1.15.0", features = ["serde"] } + +[dev-dependencies] +trybuild = { version = "1.0.115" } diff --git a/crates/simplex/README.md b/crates/simplex/README.md new file mode 100644 index 0000000..caf7a9d --- /dev/null +++ b/crates/simplex/README.md @@ -0,0 +1,20 @@ +# Simpelex HL Core + +This crate provides useful utilities for working with Simplicity on Elements. + +- `blinder.rs` — derives deterministic blinder keypair from a "public secret" +- `constants.rs` — Liquid network constants (policy asset IDs, genesis hashes) +- `explorer.rs` — explorer API utilities (behind `explorer` feature) +- `runner.rs` — program execution helpers with logging +- `scripts.rs` — P2TR address creation, Taproot control block, and asset entropy utilities +- `lib.rs` — P2PK program helpers and transaction finalization + +Consider this more like a test helper tool rather than a production-ready version. + +## License + +Dual-licensed under either of: +- Apache License, Version 2.0 (Apache-2.0) +- MIT license (MIT) + +at your option. diff --git a/crates/simplex/examples/example.rs b/crates/simplex/examples/example.rs new file mode 100644 index 0000000..d5f6da7 --- /dev/null +++ b/crates/simplex/examples/example.rs @@ -0,0 +1,35 @@ +use simplex_macros::include_simf; + +include_simf!("examples/source_simf/options.simf"); + +fn main() -> Result<(), String> { + let original_witness = derived_options::OptionsWitness { + path: simplicityhl::either::Either::Right(simplicityhl::either::Either::Left((true, 100, 200))), + }; + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_options::OptionsWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + let original_arguments = derived_options::OptionsArguments { + start_time: 0, + expiry_time: 0, + grantor_reissuance_token_asset: Default::default(), + grantor_token_asset: Default::default(), + settlement_per_contract: Default::default(), + settlement_asset_id: Default::default(), + collateral_per_contract: Default::default(), + collateral_asset_id: Default::default(), + option_reissuance_token_asset: Default::default(), + option_token_asset: Default::default(), + }; + + let witness_values = original_arguments.build_arguments(); + let recovered_witness = derived_options::OptionsArguments::from_arguments(&witness_values)?; + assert_eq!(original_arguments, recovered_witness); + + let _template = derived_options::get_template_program(); + let _compiled = derived_options::get_compiled_program(&original_arguments); + + Ok(()) +} diff --git a/crates/simplex/examples/source_simf/array_tr_storage.simf b/crates/simplex/examples/source_simf/array_tr_storage.simf new file mode 100644 index 0000000..4918cf3 --- /dev/null +++ b/crates/simplex/examples/source_simf/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/examples/source_simf/bytes32_tr_storage.simf b/crates/simplex/examples/source_simf/bytes32_tr_storage.simf new file mode 100644 index 0000000..0d11b5f --- /dev/null +++ b/crates/simplex/examples/source_simf/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/examples/source_simf/dual_currency_deposit.simf b/crates/simplex/examples/source_simf/dual_currency_deposit.simf new file mode 100644 index 0000000..e1a460a --- /dev/null +++ b/crates/simplex/examples/source_simf/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/examples/source_simf/option_offer.simf b/crates/simplex/examples/source_simf/option_offer.simf new file mode 100644 index 0000000..5cb2108 --- /dev/null +++ b/crates/simplex/examples/source_simf/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/crates/simplex/examples/source_simf/options.simf b/crates/simplex/examples/source_simf/options.simf new file mode 100644 index 0000000..e7da014 --- /dev/null +++ b/crates/simplex/examples/source_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/crates/simplex/examples/source_simf/simple_storage.simf b/crates/simplex/examples/source_simf/simple_storage.simf new file mode 100644 index 0000000..7ae6c41 --- /dev/null +++ b/crates/simplex/examples/source_simf/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/crates/simplex/src/lib.rs b/crates/simplex/src/lib.rs new file mode 100644 index 0000000..249cb3f --- /dev/null +++ b/crates/simplex/src/lib.rs @@ -0,0 +1,12 @@ +#![warn(clippy::all, clippy::pedantic)] + +//! High-level helpers for building and executing Simplicity programs on Liquid. + +pub extern crate either; +pub extern crate serde; + +#[cfg(feature = "macros")] +pub extern crate simplex_macros; + +#[cfg(feature = "core")] +pub extern crate simplex_core; diff --git a/crates/simplex/tests/compiletest.rs b/crates/simplex/tests/compiletest.rs new file mode 100644 index 0000000..db7b77e --- /dev/null +++ b/crates/simplex/tests/compiletest.rs @@ -0,0 +1,7 @@ +// TODO: all files has to pass + +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.pass("tests/ui/*.rs"); +} diff --git a/crates/simplex/tests/ui/array_tr_storage.rs b/crates/simplex/tests/ui/array_tr_storage.rs new file mode 100644 index 0000000..93dd077 --- /dev/null +++ b/crates/simplex/tests/ui/array_tr_storage.rs @@ -0,0 +1,25 @@ +use simplex_macros::*; + +include_simf!("../../../../crates/simplex/tests/ui/array_tr_storage.simf"); + +fn main() -> Result<(), String> { + let original_witness = derived_array_tr_storage::ArrayTrStorageWitness { + changed_index: 0, + state: Default::default(), + }; + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_array_tr_storage::ArrayTrStorageWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + let original_arguments = derived_array_tr_storage::ArrayTrStorageArguments {}; + + let witness_values = original_arguments.build_arguments(); + let recovered_witness = derived_array_tr_storage::ArrayTrStorageArguments::from_arguments(&witness_values)?; + assert_eq!(original_arguments, recovered_witness); + + let _template = derived_array_tr_storage::get_template_program(); + let _compiled = derived_array_tr_storage::get_compiled_program(&original_arguments); + + Ok(()) +} \ No newline at end of file diff --git a/crates/simplex/tests/ui/array_tr_storage.simf b/crates/simplex/tests/ui/array_tr_storage.simf new file mode 100644 index 0000000..4918cf3 --- /dev/null +++ b/crates/simplex/tests/ui/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/bytes32_tr_storage.rs b/crates/simplex/tests/ui/bytes32_tr_storage.rs new file mode 100644 index 0000000..463266c --- /dev/null +++ b/crates/simplex/tests/ui/bytes32_tr_storage.rs @@ -0,0 +1,24 @@ +use simplex_macros::*; + +include_simf!("../../../../crates/simplex/tests/ui/bytes32_tr_storage.simf"); + +fn main() -> Result<(), String> { + let original_witness = derived_bytes32_tr_storage::Bytes32TrStorageWitness { + state: Default::default(), + }; + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_bytes32_tr_storage::Bytes32TrStorageWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + let original_arguments = derived_bytes32_tr_storage::Bytes32TrStorageArguments {}; + + let witness_values = original_arguments.build_arguments(); + let recovered_witness = derived_bytes32_tr_storage::Bytes32TrStorageArguments::from_arguments(&witness_values)?; + assert_eq!(original_arguments, recovered_witness); + + let _template = derived_bytes32_tr_storage::get_template_program(); + let _compiled = derived_bytes32_tr_storage::get_compiled_program(&original_arguments); + + Ok(()) +} \ No newline at end of file diff --git a/crates/simplex/tests/ui/bytes32_tr_storage.simf b/crates/simplex/tests/ui/bytes32_tr_storage.simf new file mode 100644 index 0000000..0d11b5f --- /dev/null +++ b/crates/simplex/tests/ui/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/dual_currency_deposit.rs b/crates/simplex/tests/ui/dual_currency_deposit.rs new file mode 100644 index 0000000..92d4997 --- /dev/null +++ b/crates/simplex/tests/ui/dual_currency_deposit.rs @@ -0,0 +1,50 @@ +use simplex_macros::*; + +include_simf!("../../../../crates/simplex/tests/ui/dual_currency_deposit.simf"); + +fn main() -> Result<(), String> { + let original_witness = derived_dual_currency_deposit::DualCurrencyDepositWitness { + merge_branch: simplex::either::Left(simplex::either::Right(())), + token_branch: simplex::either::Left(()), + path: simplex::either::Left(simplex::either::Left(simplex::either::Left((0, 1, 2, 3)))), + }; + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_dual_currency_deposit::DualCurrencyDepositWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + let original_arguments = derived_dual_currency_deposit::DualCurrencyDepositArguments { + grantor_per_settlement_asset: 0, + settlement_asset_id: [1; 32], + grantor_settlement_token_asset: [1; 32], + strike_price: 0, + incentive_basis_points: 0, + grantor_collateral_token_asset: [1; 32], + contract_expiry_time: 0, + filler_per_settlement_asset: 0, + filler_per_principal_collateral: 0, + filler_token_asset: [1; 32], + grantor_per_settlement_collateral: 0, + grantor_settlement_per_deposited_asset: 0, + fee_script_hash: [1; 32], + taker_funding_end_time: 0, + settlement_height: 0, + collateral_asset_id: [1; 32], + taker_funding_start_time: 0, + filler_per_settlement_collateral: 0, + oracle_pk: [1; 32], + fee_basis_points: 0, + grantor_collateral_per_deposited_collateral: 0, + early_termination_end_time: 0, + }; + + let witness_values = original_arguments.build_arguments(); + let recovered_witness = + derived_dual_currency_deposit::DualCurrencyDepositArguments::from_arguments(&witness_values)?; + assert_eq!(original_arguments, recovered_witness); + + let _template = derived_dual_currency_deposit::get_template_program(); + let _compiled = derived_dual_currency_deposit::get_compiled_program(&original_arguments); + + Ok(()) +} \ No newline at end of file diff --git a/crates/simplex/tests/ui/dual_currency_deposit.simf b/crates/simplex/tests/ui/dual_currency_deposit.simf new file mode 100644 index 0000000..e1a460a --- /dev/null +++ b/crates/simplex/tests/ui/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/dual_currency_deposit.stderr b/crates/simplex/tests/ui/dual_currency_deposit.stderr new file mode 100644 index 0000000..6b486b3 --- /dev/null +++ b/crates/simplex/tests/ui/dual_currency_deposit.stderr @@ -0,0 +1,21 @@ +warning: unreachable expression + --> tests/ui/dual_currency_deposit.rs:3:1 + | +3 | include_simf!("../../../../crates/core/tests/ui/dual_currency_deposit.simf"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | | + | unreachable expression + | any code following this expression is unreachable + | + = note: `#[warn(unreachable_code)]` (part of `#[warn(unused)]`) on by default + = note: this warning originates in the macro `include_simf` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0308]: mismatched types + --> tests/ui/dual_currency_deposit.rs:3:1 + | +3 | include_simf!("../../../../crates/core/tests/ui/dual_currency_deposit.simf"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `[u8; 64]`, found `!` + | + = note: expected enum `simplicityhl::either::Either, (u64, [u8; 64], u64, u64, bool)>, simplicityhl::either::Either, ()>>` + found enum `simplicityhl::either::Either, (u64, !, u64, u64, bool)>, simplicityhl::either::Either, ()>>` + = note: this error originates in the macro `include_simf` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/simplex/tests/ui/option_offer.rs b/crates/simplex/tests/ui/option_offer.rs new file mode 100644 index 0000000..9277639 --- /dev/null +++ b/crates/simplex/tests/ui/option_offer.rs @@ -0,0 +1,30 @@ +use simplex_macros::*; + +include_simf!("../../../../crates/simplex/tests/ui/option_offer.simf"); + +fn main() -> Result<(), String> { + let original_witness = derived_option_offer::OptionOfferWitness { path: simplex::either::Left((0, false)) }; + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_option_offer::OptionOfferWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + let original_arguments = derived_option_offer::OptionOfferArguments { + user_pubkey: [1; 32], + premium_per_collateral: 0, + premium_asset_id: [1; 32], + settlement_asset_id: [1; 32], + collateral_asset_id: [1; 32], + collateral_per_contract: 0, + expiry_time: 0, + }; + + let witness_values = original_arguments.build_arguments(); + let recovered_witness = derived_option_offer::OptionOfferArguments::from_arguments(&witness_values)?; + assert_eq!(original_arguments, recovered_witness); + + let _template = derived_option_offer::get_template_program(); + let _compiled = derived_option_offer::get_compiled_program(&original_arguments); + + Ok(()) +} \ No newline at end of file diff --git a/crates/simplex/tests/ui/option_offer.simf b/crates/simplex/tests/ui/option_offer.simf new file mode 100644 index 0000000..5cb2108 --- /dev/null +++ b/crates/simplex/tests/ui/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/crates/simplex/tests/ui/options.rs b/crates/simplex/tests/ui/options.rs new file mode 100644 index 0000000..3f7c182 --- /dev/null +++ b/crates/simplex/tests/ui/options.rs @@ -0,0 +1,34 @@ +use simplex_macros::*; +include_simf!("../../../../crates/simplex/tests/ui/options.simf"); + +fn main() -> Result<(), String> { + let original_witness = derived_options::OptionsWitness { + path: simplicityhl::either::Either::Right(simplicityhl::either::Either::Left((true, 100, 200))), + }; + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_options::OptionsWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + let original_arguments = derived_options::OptionsArguments { + start_time: 0, + expiry_time: 0, + grantor_reissuance_token_asset: Default::default(), + grantor_token_asset: Default::default(), + settlement_per_contract: Default::default(), + settlement_asset_id: Default::default(), + collateral_per_contract: Default::default(), + collateral_asset_id: Default::default(), + option_reissuance_token_asset: Default::default(), + option_token_asset: Default::default(), + }; + + let witness_values = original_arguments.build_arguments(); + let recovered_witness = derived_options::OptionsArguments::from_arguments(&witness_values)?; + assert_eq!(original_arguments, recovered_witness); + + let _template = derived_options::get_template_program(); + let _compiled = derived_options::get_compiled_program(&original_arguments); + + Ok(()) +} \ No newline at end of file diff --git a/crates/simplex/tests/ui/options.simf b/crates/simplex/tests/ui/options.simf new file mode 100644 index 0000000..e7da014 --- /dev/null +++ b/crates/simplex/tests/ui/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/crates/simplex/tests/ui/simple_storage.rs b/crates/simplex/tests/ui/simple_storage.rs new file mode 100644 index 0000000..27d6f57 --- /dev/null +++ b/crates/simplex/tests/ui/simple_storage.rs @@ -0,0 +1,25 @@ +use simplex_macros::*; + +include_simf!("../../../../crates/simplex/tests/ui/simple_storage.simf"); + +fn main() -> Result<(), String> { + let original_witness = derived_simple_storage::SimpleStorageWitness { + new_value: 0, + user_signature: [1; 64], + }; + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_simple_storage::SimpleStorageWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + let original_arguments = derived_simple_storage::SimpleStorageArguments { user: Default::default(), slot_id: Default::default() }; + + let witness_values = original_arguments.build_arguments(); + let recovered_witness = derived_simple_storage::SimpleStorageArguments::from_arguments(&witness_values)?; + assert_eq!(original_arguments, recovered_witness); + + let _template = derived_simple_storage::get_template_program(); + let _compiled = derived_simple_storage::get_compiled_program(&original_arguments); + + Ok(()) +} \ No newline at end of file diff --git a/crates/simplex/tests/ui/simple_storage.simf b/crates/simplex/tests/ui/simple_storage.simf new file mode 100644 index 0000000..7ae6c41 --- /dev/null +++ b/crates/simplex/tests/ui/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/example.config.toml b/example.config.toml new file mode 100644 index 0000000..c90b221 --- /dev/null +++ b/example.config.toml @@ -0,0 +1,2 @@ +[network] +name = "testnet" \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..5e4210e --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,27 @@ +# WARNING: Formatting in this project is non-standard and unfortunetely `cargo fmt` does not support "out of the box" formatting. +# Here you can find the closest possible set of settings for `cargo fmt`, but it is not even close to desirable. +# use '+nightly' option for formatting (cargo +nightly fmt) + +edition = "2024" +style_edition = "2024" +max_width = 120 +tab_spaces = 4 +newline_style = "Unix" +fn_params_layout = "Tall" +match_arm_leading_pipes = "Preserve" +reorder_imports = true +reorder_modules = true +# unstable features below +# unstable_features = true +# format_code_in_doc_comments = true +# imports_granularity = "Crate" +# group_imports = "StdExternalCrate" + +#wrap_comments = true +#where_single_line = false +#blank_lines_upper_bound = 2 +#brace_style = "AlwaysNextLine" +#control_brace_style = "AlwaysNextLine" +#empty_item_single_line = true +#use_small_heuristics = "Off" +