diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7764816..1e795ec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # These owners will be requested for review when someone opens a pull request. -* @ikripaka @ +* @arvolear diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 083f574..63f4e92 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -2,7 +2,7 @@ name: Bug Report description: File a bug report labels: ['bug'] assignees: - - KyrylR + - Arvolear body: - type: markdown attributes: @@ -10,8 +10,8 @@ body: - type: input id: version attributes: - label: "Project version" - placeholder: "1.2.3" + label: "Simplex version" + placeholder: "0.1.2" validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 8f12f98..fc4aaf6 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -2,12 +2,12 @@ name: Feature request description: Suggest a new feature labels: ['feature'] assignees: - - KyrylR + - Arvolear body: - type: textarea id: feature-description attributes: label: "Describe the feature" - description: "A description of what you would like to see in the project" + description: "A description of what you would like to see in Simplex" validations: required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c3317f1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,25 @@ + + +- [ ] This PR suggests a **bug fix** and I've added the necessary tests. +- [ ] This PR introduces a **new feature** and I've discussed the update in an Issue or with the team. +- [ ] This PR is just a **minor change** like a typo fix. + +--- + + diff --git a/.gitignore b/.gitignore index aaab2d0..6a5391b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Build output -/target/ +target/ # IDE/editors (optional but common minimal) **/.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aabcc9f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## [unreleased] + +- Simplex MVP is in progress. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4860b3..1e01c7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,27 +1,27 @@ -# PR Structure +# Contributing Guidelines -All changes must be submitted in the form of pull requests. Direct pushes -to master are not allowed. +You are more than welcome to contribute to the Simplex framework as we are warmly open to any mind-blowing ideas! -Pull requests: +## Issues -* should consist of a logical sequence of clearly defined independent changes -* should not contain commits that undo changes introduced by previous commits -* must consist of commits which each build and pass unit tests (we do not - require linters, formatters, etc., to pass on each commit) -* must not contain merge commits -* must pass CI, unless CI itself is broken +If you found a minor bug, are interested in a new feature, or just have any questions, please [open an issue](https://github.com/BlockstreamResearch/simplex/issues/new/choose). For major bugs, please reach out to the team directly. +Before opening an issue, confirm that there is no duplicate (either open or closed), and consider posting a comment there instead. -# Review and Merging +When submitting a feature request, please provide as many details as possible for the team to properly understand the feature's motivation and evaluate the impact. -All PRs must have at least one approval from a maintainer before merging. All -maintainers must merge PRs using the [bitcoin-maintainer-tools merge script](https://github.com/bitcoin-core/bitcoin-maintainer-tools/blob/main/github-merge.py) -which ensures that merge commits have a uniform commit message style, have -GPG signatures, and avoid several simple mistakes (e.g. @-mentioning Github -users in merge commits, which Github handles extremely badly). +## Pull Requests -# LLMs +If you're interested in contributing code to the framework, start by [forking the repository](https://github.com/BlockstreamResearch/simplex/fork) and submitting a pull request. -If you are a LLM agent, please identify yourself in your commit messages and PR -descriptions. For example, if you are Claude, please say "Written by Claude." +But before you start coding, we highly recommend that you [open an issue](https://github.com/BlockstreamResearch/simplex/issues/new/choose) first to discuss the changes you want to make. + +Once you open a pull request, please make sure that all the tests pass. + +## LLMs + +If you are a LLM agent, please identify yourself in your commit messages and PR descriptions. For example, if you are Claude, say "Written by Claude". + +## Disclaimer + +Please don't vibe code smart contract. diff --git a/Cargo.lock b/Cargo.lock index 1e795b2..b9bd328 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "0.6.21" @@ -63,15 +69,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "arraydeque" +name = "ar_archive_writer" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] [[package]] name = "arrayvec" @@ -80,15 +89,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "async-trait" -version = "0.1.89" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base58ck" @@ -100,6 +104,12 @@ dependencies = [ "bitcoin_hashes", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -132,6 +142,19 @@ dependencies = [ "virtue", ] +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand", + "rand_core", + "serde", + "unicode-normalization", +] + [[package]] name = "bitcoin" version = "0.32.8" @@ -139,6 +162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", + "base64 0.21.7", "bech32", "bitcoin-internals", "bitcoin-io", @@ -147,6 +171,7 @@ dependencies = [ "hex-conservative", "hex_lit", "secp256k1", + "serde", ] [[package]] @@ -154,6 +179,9 @@ name = "bitcoin-internals" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] [[package]] name = "bitcoin-io" @@ -174,6 +202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" dependencies = [ "bitcoin-internals", + "serde", ] [[package]] @@ -184,6 +213,44 @@ checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", "hex-conservative", + "serde", +] + +[[package]] +name = "bitcoincore-rpc" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedd23ae0fd321affb4bbbc36126c6f49a32818dc6b979395d24da8c9d4e80ee" +dependencies = [ + "bitcoincore-rpc-json", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoincore-rpc-json" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8909583c5fab98508e80ef73e5592a651c954993dc6b7739963257d19f0e71a" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoind" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce6620b7c942dbe28cc49c21d95e792feb9ffd95a093205e7875ccfa69c2925" +dependencies = [ + "anyhow", + "bitcoincore-rpc", + "log", + "tempfile", + "which", ] [[package]] @@ -194,12 +261,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -210,11 +274,30 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -224,9 +307,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -238,11 +321,31 @@ 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 = "chumsky" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acc17a6284abccac6e50db35c1cee87f605474a72939b959a3a67d9371800efd" +dependencies = [ + "hashbrown 0.15.5", + "regex-automata 0.3.9", + "serde", + "stacker", + "unicode-ident", + "unicode-segmentation", +] + [[package]] name = "clap" -version = "4.5.57" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -250,9 +353,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.57" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -274,25 +377,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.0.0" 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", -] +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" @@ -300,55 +387,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "config" -version = "0.15.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" -dependencies = [ - "async-trait", - "convert_case", - "json5", - "pathdiff", - "ron", - "rust-ini", - "serde-untagged", - "serde_core", - "serde_json", - "toml", - "winnow", - "yaml-rust2", -] - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -359,12 +397,13 @@ dependencies = [ ] [[package]] -name = "crc32fast" -version = "1.5.0" +name = "crossbeam-deque" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", ] [[package]] @@ -382,12 +421,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-common" version = "0.1.7" @@ -398,6 +431,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix 0.31.2", + "windows-sys 0.61.2", +] + [[package]] name = "digest" version = "0.10.7" @@ -409,12 +453,15 @@ dependencies = [ ] [[package]] -name = "dlv-list" -version = "0.5.2" +name = "dispatch2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "const-random", + "bitflags 2.11.0", + "block2", + "libc", + "objc2", ] [[package]] @@ -423,11 +470,45 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "electrsd" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91435161fb2ad5098e7ac7a4b793bf9c34723b0208a3fcf6f33707489e771396" +dependencies = [ + "bitcoind", + "electrum-client", + "log", + "nix 0.25.1", + "which", +] + +[[package]] +name = "electrum-client" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0bd443023f9f5c4b7153053721939accc7113cbdf810a024434eed454b3db1" +dependencies = [ + "bitcoin", + "log", + "serde", + "serde_json", +] [[package]] name = "elements" @@ -438,28 +519,44 @@ dependencies = [ "bech32", "bitcoin", "secp256k1-zkp", + "serde", + "serde_json", ] [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "elements-miniscript" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "571fa105690f83c7833df2109eb2e14ca0e62d633d2624ffcb166ff18a3da870" dependencies = [ - "cfg-if", + "bitcoin", + "elements", + "miniscript", + "serde", ] [[package]] -name = "erased-serde" -version = "0.4.9" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "serde", - "serde_core", - "typeid", + "libc", + "windows-sys 0.61.2", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -472,25 +569,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -514,6 +592,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "ghost-cell" version = "0.2.6" @@ -521,10 +612,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8449d342b1c67f49169e92e71deb7b9b27f30062301a16dbc27a4cc8d2351b7" [[package]] -name = "hashbrown" -version = "0.14.5" +name = "glob" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.14", + "regex-syntax 0.8.10", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.11.0", + "ignore", + "walkdir", +] [[package]] name = "hashbrown" @@ -532,17 +647,16 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] [[package]] -name = "hashlink" -version = "0.10.0" +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -572,12 +686,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" [[package]] -name = "instant" -version = "0.1.13" +name = "home" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.14", + "same-file", + "walkdir", + "winapi-util", +] + +[[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", + "serde", + "serde_core", ] [[package]] @@ -603,39 +751,49 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] -name = "json5" -version = "0.4.1" +name = "jsonrpc" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf" dependencies = [ - "pest", - "pest_derive", + "base64 0.13.1", + "minreq", "serde", + "serde_json", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.180" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] -name = "lock_api" -version = "0.4.14" +name = "linux-raw-sys" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" @@ -645,9 +803,18 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] [[package]] name = "miniscript" @@ -673,106 +840,84 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "ordered-multimap" -version = "0.7.3" +name = "nix" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ - "dlv-list", - "hashbrown 0.14.5", + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", ] [[package]] -name = "parking_lot" -version = "0.11.2" +name = "nix" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "instant", - "lock_api", - "parking_lot_core", + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", ] [[package]] -name = "parking_lot_core" -version = "0.8.6" +name = "objc2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", + "objc2-encode", ] [[package]] -name = "pathdiff" -version = "0.2.3" +name = "objc2-encode" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] -name = "pest" -version = "2.8.5" +name = "object" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", - "ucd-trie", ] [[package]] -name = "pest_derive" -version = "2.8.5" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" -dependencies = [ - "pest", - "pest_generator", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "pest_generator" -version = "2.8.5" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "pest_meta" -version = "2.8.5" +name = "pathdiff" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" -dependencies = [ - "pest", - "sha2", -] +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "ppv-lite86" @@ -783,6 +928,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -792,15 +947,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -828,28 +999,30 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] [[package]] -name = "redox_syscall" -version = "0.2.16" +name = "regex" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ - "bitflags 1.3.2", + "aho-corasick", + "memchr", + "regex-automata 0.4.14", + "regex-syntax 0.8.10", ] [[package]] -name = "regex" -version = "1.12.3" +name = "regex-automata" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-syntax 0.7.5", ] [[package]] @@ -860,14 +1033,20 @@ checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.10", ] [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "regex-syntax" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "ring" @@ -877,34 +1056,36 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] -name = "ron" -version = "0.12.0" +name = "rustix" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", - "once_cell", - "serde", - "serde_derive", - "typeid", - "unicode-ident", + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", ] [[package]] -name = "rust-ini" -version = "0.21.3" +name = "rustix" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "cfg-if", - "ordered-multimap", + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] @@ -935,6 +1116,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "santiago" version = "1.3.1" @@ -944,12 +1134,6 @@ dependencies = [ "regex", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "sct" version = "0.7.1" @@ -969,6 +1153,7 @@ dependencies = [ "bitcoin_hashes", "rand", "secp256k1-sys", + "serde", ] [[package]] @@ -990,6 +1175,7 @@ dependencies = [ "rand", "secp256k1", "secp256k1-zkp-sys", + "serde", ] [[package]] @@ -1002,6 +1188,12 @@ dependencies = [ "secp256k1-sys", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1012,18 +1204,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -1084,17 +1264,108 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "simplex-simplicity" +name = "simplex" version = "0.1.0" dependencies = [ "bincode", + "either", + "serde", + "simplex-macros", + "simplex-sdk", + "simplex-test", + "simplicityhl", + "trybuild", +] + +[[package]] +name = "simplex-build" +version = "0.1.0" +dependencies = [ + "glob", + "globwalk", + "pathdiff", + "prettyplease", + "proc-macro2", + "quote", + "serde", + "simplicityhl", + "syn", + "thiserror", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "simplex-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "ctrlc", + "dotenvy", + "electrsd", + "serde", + "simplex-build", + "simplex-regtest", + "simplex-sdk", + "simplex-test", + "simplicityhl", + "thiserror", + "tokio", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "simplex-macros" +version = "0.1.0" +dependencies = [ + "simplex-build", + "simplex-test", + "syn", +] + +[[package]] +name = "simplex-regtest" +version = "0.1.0" +dependencies = [ + "electrsd", + "simplex-sdk", + "thiserror", +] + +[[package]] +name = "simplex-sdk" +version = "0.1.0" +dependencies = [ + "bip39", + "bitcoin_hashes", + "dyn-clone", + "electrsd", + "elements-miniscript", "hex", "minreq", + "serde", + "serde_json", "sha2", "simplicityhl", "thiserror", ] +[[package]] +name = "simplex-test" +version = "0.1.0" +dependencies = [ + "electrsd", + "proc-macro2", + "quote", + "serde", + "simplex-regtest", + "simplex-sdk", + "simplicityhl", + "syn", + "thiserror", + "toml 0.9.12+spec-1.1.0", +] + [[package]] name = "simplicity-lang" version = "0.7.0" @@ -1105,7 +1376,7 @@ dependencies = [ "bitcoin_hashes", "byteorder", "elements", - "getrandom", + "getrandom 0.2.17", "ghost-cell", "hex-conservative", "miniscript", @@ -1125,44 +1396,33 @@ 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/BlockstreamResearch/SimplicityHL.git?rev=568b462#568b4621d6145cd97dce68a3f3428c7eb85306b6" dependencies = [ - "base64", + "base64 0.21.7", + "chumsky", "clap", "either", - "getrandom", + "getrandom 0.2.17", "itertools", "miniscript", - "pest", - "pest_derive", "serde", "serde_json", "simplicity-lang", ] [[package]] -name = "sled" -version = "0.34.7" +name = "stacker" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", + "cc", + "cfg-if", "libc", - "log", - "parking_lot", + "psm", + "windows-sys 0.59.0", ] -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - [[package]] name = "strsim" version = "0.11.1" @@ -1171,15 +1431,43 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "target-triple" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -1201,19 +1489,25 @@ dependencies = [ ] [[package]] -name = "tiny-keccak" -version = "2.0.2" +name = "tinyvec" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ - "crunchy", + "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" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "pin-project-lite", "tokio-macros", @@ -1221,9 +1515,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -1232,14 +1526,31 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml" +version = "1.0.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "c94c3321114413476740df133f0d8862c61d87c8d26f04c6841e033c8c80db47" dependencies = [ + "indexmap", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", + "toml_writer", "winnow", ] @@ -1252,20 +1563,44 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] [[package]] -name = "typeid" -version = "1.0.3" +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 = "trybuild" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +checksum = "47c635f0191bd3a2941013e5062667100969f8c4e9cd787c14f977265d73616e" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml 1.0.4+spec-1.1.0", +] [[package]] name = "typenum" @@ -1274,16 +1609,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] -name = "ucd-trie" -version = "0.1.7" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "unicode-ident" -version = "1.0.22" +name = "unicode-normalization" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] [[package]] name = "unicode-segmentation" @@ -1291,6 +1629,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -1321,17 +1665,45 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[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 = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -1342,9 +1714,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1352,9 +1724,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -1365,40 +1737,73 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] -name = "webpki-roots" -version = "0.25.4" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] [[package]] -name = "winapi" -version = "0.3.9" +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] [[package]] -name = "winapi-x86_64-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 = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +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" @@ -1415,6 +1820,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1490,38 +1904,112 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "memchr", + "wit-bindgen-rust-macro", ] [[package]] -name = "yaml-rust2" -version = "0.10.4" +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink", + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] name = "zerocopy" -version = "0.8.38" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.38" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", @@ -1530,6 +2018,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.19" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 103cb79..9c0cdd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,27 +1,35 @@ [workspace] resolver = "3" members = [ - "crates/*" + "crates/*", ] +exclude = ["examples/basic"] [workspace.package] -license = "MIT OR Apache-2.0" +license = "MIT" edition = "2024" [workspace.lints.clippy] multiple_crate_versions = "allow" [workspace.dependencies] -ring = "0.17.14" -sha2 = { version = "0.10.9", features = ["compress"] } - -hex = "0.4.3" -tracing = { version = "0.1.41" } +simplex-macros = { path = "./crates/macros" } +simplex-build = { path = "./crates/build" } +simplex-test = { path = "./crates/test" } +simplex-regtest = { path = "./crates/regtest" } +simplex-sdk = { path = "./crates/sdk" } +simplex = { path = "./crates/simplex" } -minreq = { version = "2.14.1", features = ["https", "json-using-serde"]} +serde = { version = "1.0.228", features = ["derive"]} +bincode = { version = "2.0.1", features = ["serde"] } +hex = { version = "0.4.3" } +sha2 = { version = "0.10.9", features = ["compress"] } +thiserror = { version = "2.0.18" } +toml = { version = "0.9.8" } +minreq = { version = "2.14.1", features = ["https", "json-using-serde"] } +electrsd = { version = "0.29.0", features = ["legacy"] } -simplicityhl = { version = "0.4.0" } -simplicityhl-core = { version = "0.4.2", features = ["encoding"] } +simplicityhl = { git = "https://github.com/BlockstreamResearch/SimplicityHL.git", rev = "568b462" } [patch.crates-io] simplicity-sys = { git = "https://github.com/BlockstreamResearch/rust-simplicity", tag = "simplicity-sys-0.6.1" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..99784d9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Blockstream + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c52f23b..7eace42 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,101 @@ -# Simplex SDK +![](https://github.com/user-attachments/assets/7d7ca314-b706-47b3-a0be-2d64fc409fab) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +# Simplex -## License +**A blazingly-fast, ux-first simplicity development framework.** + +## What + +Simplex is a Rust-based, comprehensive development framework for [simplicity](https://github.com/BlockstreamResearch/SimplicityHL) smart contracts, aiming to provide a rich tooling suite for implementing, testing, and deploying smart contract on [Liquid](https://liquid.net/). + +- CLI for managing simplicity-based projects. +- SDK with essential simplicity utilities. +- Liquid regtest for local integration testing. +- Extensive framework configuration. + +> [!WARNING] +> The framework is in the extremely early stage of development, unforeseen breaking changes and critical bugs are expected. + +## Installation + +```bash +cargo install --path ./crates/cli +``` + +*The proper installer will be provided soon.* + +## Usage + +Simplex is a zero-config framework. However, it requires a `simplex.toml` file to exist in the project root. The default configuration is the following: + +```toml +# Simplex config + +[build] +src_dir = "./simf" +simf_files = ["*.simf"] +out_dir = "./src/artifacts" + +[test] +mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" + +[test.esplora] +url = "" +network = "" -Dual-licensed under either of: -- Apache License, Version 2.0 (Apache-2.0) -- MIT license (MIT) +[test.rpc] +url = "" +username = "" +password = "" +``` + +Where: + +- `build` (`simplex build` config) + - `src_dir` - The simplicity contracts source directory. + - `simf_files` - A glob pattern incidating which contracts are in scope. + - `out_dir` - The output directory where contracts artifacts are generated. +- `test` (`simplex test` config) + - `esplora` + - `url` - Esplora API endpoint url + - `network` - Esplora network type (`Liquid`, `LiquidTestnet`, `LiquidRegtest`). + - `rpc` + - `url` - Elements RPC endpoint url + - `username` - Elements RPC username + - `password` - Elements RPC password + +### CLI + +Simplex CLI provides the following commands: + +- `simplex init` - Initializes a Simplex project. +- `simplex config` - Prints the current config. +- `simplex build` - Generates simplicity artifacts. +- `simplex regtest` - Spins up local Electrs + Elements nodes. +- `simplex test` - Runs Simplex tests. +- `simplex clean` - Cleans up the project. + +To view the available options, run the help command: + +```bash +simplex -h +``` + +### Example + +Check out the complete project examples in the `examples` directory to learn more. + +## Future work + +- [ ] Custom signer setup with `simplex regtest`. +- [ ] SDK support for confidential assets. +- [ ] `simplex init` and `simplex clean` tasks. +- [ ] Proper installation scripts. +- [ ] Simplicity dependencies management once the language adds [support for modules](https://github.com/BlockstreamResearch/SimplicityHL/issues/155). +- [ ] Comprehensive documentation. + +## License -at your option. \ No newline at end of file +The framework is released under the MIT License. diff --git a/assets/electrs b/assets/electrs new file mode 100755 index 0000000..4dd5a9d Binary files /dev/null and b/assets/electrs differ diff --git a/assets/elementsd b/assets/elementsd new file mode 100755 index 0000000..93c13ca Binary files /dev/null and b/assets/elementsd differ diff --git a/crates/build/Cargo.toml b/crates/build/Cargo.toml new file mode 100644 index 0000000..b1d7196 --- /dev/null +++ b/crates/build/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "simplex-build" +version = "0.1.0" +description = "Simplex build command internal implementation" +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +thiserror = { workspace = true } +toml = { workspace = true } +serde = { workspace = true } +simplicityhl = { workspace = true } + +syn = { version = "2.0.114", default-features = false, features = ["proc-macro", "full", "parsing", "derive", "clone-impls", "extra-traits", "printing"] } +proc-macro2 = { version = "1.0.106", features = ["span-locations"] } +quote = { version = "1.0.44" } +pathdiff = { version = "0.2.3" } +prettyplease = { version = "0.2.37" } +glob = { version = "0.3.3"} +globwalk = { version = "0.9.1"} diff --git a/crates/build/src/config.rs b/crates/build/src/config.rs new file mode 100644 index 0000000..3395e75 --- /dev/null +++ b/crates/build/src/config.rs @@ -0,0 +1,40 @@ +use std::fs::OpenOptions; +use std::io::Read; +use std::path::Path; + +use serde::Deserialize; + +use super::error::BuildError; + +pub const DEFAULT_OUT_DIR_NAME: &str = "src/artifacts"; +pub const DEFAULT_INCLUDE_PATH: &str = "**/*.simf"; +pub const DEFAULT_SRC_DIR_NAME: &str = "simf"; + +#[derive(Debug, Clone, Deserialize)] +#[serde(default)] +pub struct BuildConfig { + pub simf_files: Vec, + pub src_dir: String, + pub out_dir: String, +} + +impl BuildConfig { + pub fn from_file(path: impl AsRef) -> Result { + let mut content = String::new(); + let mut file = OpenOptions::new().read(true).open(path)?; + + file.read_to_string(&mut content)?; + + Ok(toml::from_str(&content)?) + } +} + +impl Default for BuildConfig { + fn default() -> Self { + Self { + simf_files: vec![DEFAULT_INCLUDE_PATH.into()], + src_dir: DEFAULT_SRC_DIR_NAME.into(), + out_dir: DEFAULT_OUT_DIR_NAME.into(), + } + } +} diff --git a/crates/build/src/error.rs b/crates/build/src/error.rs new file mode 100644 index 0000000..7659748 --- /dev/null +++ b/crates/build/src/error.rs @@ -0,0 +1,33 @@ +use std::io; +use std::path::PathBuf; + +use globwalk::GlobError; + +#[derive(thiserror::Error, Debug)] +pub enum BuildError { + #[error("IO error: {0}")] + Io(#[from] io::Error), + + #[error("Glob error: {0}")] + Glob(#[from] GlobError), + + #[error("Occurred config deserialization error: '{0}'")] + ConfigDeserialize(#[from] toml::de::Error), + + #[error("Invalid generation path: '{0}'")] + GenerationPath(String), + + #[error("Failed to extract content from path, err: '{0}'")] + FailedToExtractContent(io::Error), + + #[error("Failed to generate file: {0}")] + GenerationFailed(String), + + #[error( + "Failed to resolve correct relative path for include_simf! macro, cwd: '{cwd:?}', simf_file: '{simf_file:?}'" + )] + FailedToFindCorrectRelativePath { cwd: PathBuf, simf_file: PathBuf }, + + #[error("Failed to find prefix for a file: {0}")] + NoBasePathForGeneration(#[from] std::path::StripPrefixError), +} diff --git a/crates/build/src/generator.rs b/crates/build/src/generator.rs new file mode 100644 index 0000000..a714735 --- /dev/null +++ b/crates/build/src/generator.rs @@ -0,0 +1,207 @@ +use std::collections::HashMap; +use std::env; +use std::fs; +use std::io::Write; +use std::path::{Component, Path, PathBuf}; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use crate::macros::parse::SimfContent; +use crate::macros::codegen::{ + convert_contract_name_to_contract_module, convert_contract_name_to_contract_source_const, + convert_contract_name_to_struct_name, +}; + +use super::error::BuildError; + +pub struct ArtifactsGenerator {} + +#[derive(Default)] +struct TreeNode { + files: Vec, + dirs: HashMap, +} + +impl ArtifactsGenerator { + pub fn generate_artifacts( + out_dir: impl AsRef, + base_dir: impl AsRef, + simfs: &[impl AsRef], + ) -> Result<(), BuildError> { + let tree = Self::build_directory_tree(&base_dir, simfs)?; + + Self::generate_bindings(out_dir.as_ref(), tree)?; + + Ok(()) + } + + fn build_directory_tree(base_dir: impl AsRef, paths: &[impl AsRef]) -> Result { + let mut root = TreeNode::default(); + + for path in paths { + let path = path.as_ref(); + + let relative_path = path + .strip_prefix(base_dir.as_ref()) + .map_err(BuildError::NoBasePathForGeneration)?; + + let components: Vec<_> = relative_path + .components() + .filter_map(|c| { + if let Component::Normal(name) = c { + Some(name) + } else { + None + } + }) + .collect(); + + let mut current_node = &mut root; + let components_len = components.len(); + + for (i, name) in components.into_iter().enumerate() { + let is_file = i == components_len - 1; + + if is_file { + current_node.files.push(path.to_path_buf()); + } else { + let dir_name = name.to_string_lossy().into_owned(); + + current_node = current_node.dirs.entry(dir_name).or_default(); + } + } + } + + Ok(root) + } + + fn generate_bindings(out_dir: &Path, path_tree: TreeNode) -> Result, BuildError> { + fs::create_dir_all(out_dir)?; + + let mut mod_filenames = Self::generate_simfs(&out_dir, &path_tree.files)?; + + for (dir_name, tree_node) in path_tree.dirs.into_iter() { + Self::generate_bindings(&out_dir.join(&dir_name), tree_node)?; + mod_filenames.push(dir_name); + } + + Self::generate_mod_rs(&out_dir, &mod_filenames)?; + + Ok(mod_filenames) + } + + fn generate_simfs(out_dir: impl AsRef, simfs: &[impl AsRef]) -> Result, BuildError> { + let mut module_files = Vec::with_capacity(simfs.len()); + + for simf_file_path in simfs { + let mod_name = Self::generate_simf_file(out_dir.as_ref(), simf_file_path)?; + module_files.push(mod_name); + } + + Ok(module_files) + } + + fn generate_simf_file(out_dir: impl AsRef, simf_file_path: impl AsRef) -> Result { + let simf_file_buf = PathBuf::from(simf_file_path.as_ref()); + let simf_content = + SimfContent::extract_content_from_path(&simf_file_buf).map_err(BuildError::FailedToExtractContent)?; + + let contract_name = simf_content.contract_name.clone(); + let output_file = out_dir.as_ref().join(format!("{}.rs", &contract_name)); + + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&output_file)?; + let code = Self::generate_simf_binding_code(simf_content, simf_file_buf)?; + + Self::expand_file(code, &mut file)?; + + Ok(contract_name) + } + + fn generate_mod_rs(out_dir: impl AsRef, simfs_mod_names: &[String]) -> Result<(), BuildError> { + let output_file = out_dir.as_ref().join("mod.rs"); + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&output_file)?; + let code = Self::generate_mod_binding_code(simfs_mod_names)?; + + Self::expand_file(code, &mut file)?; + + Ok(()) + } + + fn expand_file(code: TokenStream, buf: &mut dyn Write) -> Result<(), BuildError> { + let file: syn::File = syn::parse2(code).map_err(|e| BuildError::GenerationFailed(e.to_string()))?; + let prettystr = prettyplease::unparse(&file); + + buf.write_all(prettystr.as_bytes())?; + buf.flush()?; + + Ok(()) + } + + fn generate_simf_binding_code(simf_content: SimfContent, simf_file: PathBuf) -> Result { + let cwd = env::current_dir()?; + let contract_name = &simf_content.contract_name; + let program_name = { + let base_name = convert_contract_name_to_struct_name(contract_name); + format_ident!("{base_name}Program") + }; + let include_simf_source_const = convert_contract_name_to_contract_source_const(contract_name); + let include_simf_module = convert_contract_name_to_contract_module(contract_name); + + let pathdiff = pathdiff::diff_paths(&simf_file, &cwd).ok_or(BuildError::FailedToFindCorrectRelativePath { + cwd: cwd, + simf_file: simf_file, + })?; + let pathdiff = pathdiff.to_string_lossy().into_owned(); + + let code = quote! { + use simplex::include_simf; + use simplex::simplex_sdk::program::{ArgumentsTrait, Program}; + use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; + + pub struct #program_name { + program: Program, + } + + impl #program_name { + pub const SOURCE: &'static str = #include_simf_module::#include_simf_source_const; + + pub fn new(public_key: XOnlyPublicKey, arguments: impl ArgumentsTrait + 'static) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + + pub fn get_program(&self) -> &Program { + &self.program + } + + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } + } + + include_simf!(#pathdiff); + }; + + Ok(code) + } + + fn generate_mod_binding_code(mod_names: &[String]) -> Result { + let mod_names = mod_names.iter().map(|x| format_ident!("{x}")).collect::>(); + + let code = quote! { + #(pub mod #mod_names);*; + }; + + Ok(code) + } +} diff --git a/crates/build/src/lib.rs b/crates/build/src/lib.rs new file mode 100644 index 0000000..03e8477 --- /dev/null +++ b/crates/build/src/lib.rs @@ -0,0 +1,9 @@ +pub mod config; +pub mod error; +pub mod generator; +pub mod resolver; +pub mod macros; + +pub use config::BuildConfig; +pub use generator::ArtifactsGenerator; +pub use resolver::ArtifactsResolver; diff --git a/crates/build/src/macros/codegen.rs b/crates/build/src/macros/codegen.rs new file mode 100644 index 0000000..1a32515 --- /dev/null +++ b/crates/build/src/macros/codegen.rs @@ -0,0 +1,353 @@ +use quote::{format_ident, quote}; + +use simplicityhl::str::WitnessName; +use simplicityhl::{AbiMeta, Parameters, ResolvedType, WitnessTypes}; + +use crate::macros::parse::SimfContent; +use crate::macros::types::RustType; + +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 = convert_contract_name_to_contract_source_const(&simf_content.contract_name); + 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 ::simplex::simplex_sdk::program::ArgumentsTrait; + use ::simplex::bincode::*; + }, + struct_token_stream: quote! { + #generated_struct + }, + struct_impl: quote! { + impl #struct_name { + /// 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::simplex_sdk::program::ArgumentsTrait for #struct_name { + /// Build Simplicity arguments for contract instantiation. + #[must_use] + fn build_arguments(&self) -> ::simplicityhl::Arguments { + ::simplicityhl::Arguments::from(HashMap::from([ + #(#tuples),* + ])) + } + } + + impl ::simplex::serde::Serialize for #struct_name { + fn serialize(&self, serializer: S) -> Result + where + 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) + } + } + }, + }) + } + + /// 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; + use ::simplex::simplex_sdk::program::WitnessTrait; + }, + struct_token_stream: quote! { + #generated_struct + }, + struct_impl: quote! { + impl #struct_name { + /// Build struct from Simplicity WitnessValues. + /// + /// # Errors + /// + /// Returns error if any required witness is missing, has the wrong type, or has an invalid value. + pub fn from_witness(witness: &WitnessValues) -> Result { + #arguments_conversion_from_args_map + + Ok(#struct_to_return) + } + } + + impl ::simplex::simplex_sdk::program::WitnessTrait for #struct_name { + /// Build Simplicity witness values for contract execution. + #[must_use] + fn build_witness(&self) -> ::simplicityhl::WitnessValues { + ::simplicityhl::WitnessValues::from(HashMap::from([ + #(#tuples),* + ])) + } + } + + impl ::simplex::serde::Serialize for #struct_name { + fn serialize(&self, serializer: S) -> Result + where + 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) + } + } + }, + }) + } + + fn generate_args_struct(contract_name: &str, meta: &Parameters) -> syn::Result { + let base_name = convert_contract_name_to_struct_name(contract_name); + Ok(WitnessStruct { + struct_name: format_ident!("{}Arguments", base_name), + witness_values: WitnessStruct::generate_witness_fields(meta.iter())?, + }) + } + + fn generate_witness_struct(contract_name: &str, meta: &WitnessTypes) -> syn::Result { + let base_name = convert_contract_name_to_struct_name(contract_name); + Ok(WitnessStruct { + struct_name: format_ident!("{}Witness", base_name), + witness_values: WitnessStruct::generate_witness_fields(meta.iter())?, + }) + } + + 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) + } +} + +pub fn convert_contract_name_to_struct_name(contract_name: &str) -> String { + let words: Vec = contract_name + .split('_') + .filter(|w| !w.is_empty()) + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + } + }) + .collect(); + words.join("") +} + +pub fn convert_contract_name_to_contract_source_const(contract_name: &str) -> proc_macro2::Ident { + format_ident!("{}_CONTRACT_SOURCE", contract_name.to_uppercase()) +} + +pub fn convert_contract_name_to_contract_module(contract_name: &str) -> proc_macro2::Ident { + format_ident!("derived_{}", contract_name) +} diff --git a/crates/build/src/macros/macros.rs b/crates/build/src/macros/macros.rs new file mode 100644 index 0000000..1b539c3 --- /dev/null +++ b/crates/build/src/macros/macros.rs @@ -0,0 +1,91 @@ +use std::error::Error; + +use proc_macro2::Span; +use quote::quote; + +use simplicityhl::AbiMeta; + +use super::codegen::{ + GeneratedArgumentTokens, GeneratedWitnessTokens, SimfContractMeta, convert_contract_name_to_contract_module, +}; +use super::parse::{SimfContent, SynFilePath}; +use super::program; + +pub fn expand(input: &SynFilePath) -> syn::Result { + let simf_content = SimfContent::eval_path_expr(input)?; + let abi_meta = program::compile_simf(&simf_content)?; + let generated = expand_helpers(simf_content, abi_meta)?; + + Ok(generated) +} + +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 = convert_contract_name_to_contract_module(&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 contract_source_name = &derived_meta.contract_source_const_name; + + quote! { + pub const #contract_source_name: &str = #contract_content; + } +} + +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/build/src/macros/mod.rs b/crates/build/src/macros/mod.rs new file mode 100644 index 0000000..584b7a8 --- /dev/null +++ b/crates/build/src/macros/mod.rs @@ -0,0 +1,7 @@ +pub mod codegen; +pub mod parse; +pub mod program; +pub mod types; +pub mod macros; + +pub use macros::expand; diff --git a/crates/build/src/macros/parse.rs b/crates/build/src/macros/parse.rs new file mode 100644 index 0000000..3cdd34d --- /dev/null +++ b/crates/build/src/macros/parse.rs @@ -0,0 +1,185 @@ +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use proc_macro2::Span; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; +use syn::{Expr, ExprLit, Lit}; + +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() + } + + pub 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/build/src/macros/program.rs b/crates/build/src/macros/program.rs new file mode 100644 index 0000000..279d4c7 --- /dev/null +++ b/crates/build/src/macros/program.rs @@ -0,0 +1,17 @@ +use std::error::Error; + +use proc_macro2::Span; + +use simplicityhl::{AbiMeta, TemplateProgram}; + +use super::parse::SimfContent; + +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/build/src/macros/types.rs b/crates/build/src/macros/types.rs new file mode 100644 index 0000000..d6dda53 --- /dev/null +++ b/crates/build/src/macros/types.rs @@ -0,0 +1,621 @@ +use std::fmt::Display; + +use quote::quote; + +use simplicityhl::ResolvedType; + +#[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/build/src/resolver.rs b/crates/build/src/resolver.rs new file mode 100644 index 0000000..4051d0d --- /dev/null +++ b/crates/build/src/resolver.rs @@ -0,0 +1,60 @@ +use std::env; +use std::path::{Path, PathBuf}; + +use globwalk::FileType; + +use super::error::BuildError; + +pub struct ArtifactsResolver {} + +impl ArtifactsResolver { + pub fn resolve_files_to_build(src_dir: &String, simfs: &Vec) -> Result, BuildError> { + let cwd = env::current_dir()?; + let base = cwd.join(src_dir); + + let mut paths = Vec::new(); + + let walker = globwalk::GlobWalkerBuilder::from_patterns(base, &simfs) + .follow_links(true) + .file_type(FileType::FILE) + .build()? + .into_iter() + .filter_map(Result::ok); + + for img in walker { + paths.push(img.path().to_path_buf().canonicalize()?); + } + + Ok(paths) + } + + pub fn resolve_local_dir(path: &impl AsRef) -> Result { + let mut path_outer = PathBuf::from(path.as_ref()); + + if !path_outer.is_absolute() { + let manifest_dir = env::current_dir()?; + + let mut path_local = PathBuf::from(manifest_dir); + path_local.push(path_outer); + + path_outer = path_local; + } + + if path_outer.extension().is_some() { + return Err(BuildError::GenerationPath(format!( + "Directories can't have an extension, path: '{}'", + path_outer.display() + ))); + } + + if path_outer.is_file() { + return Err(BuildError::GenerationPath(format!( + "Directory can't be a path, path: '{}'", + path_outer.display() + ))); + } + + // TODO: canonicalize? but this path may not exist + Ok(path_outer) + } +} diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index a2c01eb..fa17bf3 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -1,31 +1,31 @@ [package] -name = "cli" +name = "simplex-cli" version = "0.1.0" -edition = "2024" -description = "Simplicity helper CLI for Liquid testnet" -license = "MIT OR Apache-2.0" -readme = "README.md" -publish = false +description = "Simplex cli with various utilities to manage a simplicity project" +license.workspace = true +edition.workspace = true [[bin]] name = "simplex" -path = "src/main.rs" +path = "src/bin/main.rs" [lints] workspace = true [dependencies] -anyhow = "1" -thiserror = "2" +simplex-regtest = { workspace = true } +simplex-test = { workspace = true } +simplex-build = { workspace = true } +simplex-sdk = { workspace = true } -sled = "0.34.7" +simplicityhl = { workspace = true } +electrsd = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } +toml = { workspace = true } +anyhow = "1" dotenvy = "0.15" -clap = { version = "4.5", features = ["derive"] } -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 +clap = { version = "4", features = ["derive", "env"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +ctrlc = { version = "3.5.2", features = ["termination"] } diff --git a/crates/cli/Simplex.default.toml b/crates/cli/Simplex.default.toml new file mode 100644 index 0000000..b63c7a5 --- /dev/null +++ b/crates/cli/Simplex.default.toml @@ -0,0 +1,18 @@ +# TEST CONFIG + +# [build] +# simf_files = ["*.simf"] +# out_dir = "./src/artifacts" +# src_dir = "./simf" + +# [test] +# mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" + +# [test.esplora] +# url = "https://blockstream.info/liquidtestnet/api" +# network = "LiquidTestnet" + +# [test.rpc] +# url = "" +# username = "" +# password = "" diff --git a/crates/cli/src/bin/main.rs b/crates/cli/src/bin/main.rs new file mode 100644 index 0000000..c23114a --- /dev/null +++ b/crates/cli/src/bin/main.rs @@ -0,0 +1,10 @@ +use clap::Parser; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + + Box::pin(simplex_cli::cli::Cli::parse().run()).await?; + + Ok(()) +} diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs new file mode 100644 index 0000000..3c8d1e6 --- /dev/null +++ b/crates/cli/src/cli.rs @@ -0,0 +1,61 @@ +use std::path::PathBuf; + +use clap::Parser; + +use crate::commands::build::Build; +use crate::commands::commands::Command; +use crate::commands::regtest::Regtest; +use crate::commands::test::Test; +use crate::config::{Config, INIT_CONFIG}; +use crate::error::CliError; + +#[derive(Debug, Parser)] +#[command(name = "Simplex")] +#[command(about = "Simplicity development framework")] +pub struct Cli { + pub config: Option, + + #[command(subcommand)] + pub command: Command, +} + +impl Cli { + pub async fn run(&self) -> Result<(), CliError> { + match &self.command { + Command::Init => { + let config_path = Config::get_default_path()?; + std::fs::write(&config_path, INIT_CONFIG)?; + + println!("Config written to: '{}'", config_path.display()); + + Ok(()) + } + Command::Config => { + let config_path = Config::get_default_path()?; + let loaded_config = Config::load(config_path)?; + + println!("{loaded_config:#?}"); + + Ok(()) + } + Command::Test { command } => { + let config_path = Config::get_default_path()?; + let loaded_config = Config::load(config_path)?; + + let test_config = loaded_config.test.unwrap_or_default(); + + Ok(Test::run(test_config, command)?) + } + Command::Regtest => { + // TODO: pass config + Ok(Regtest::run()?) + } + Command::Build => { + let config_path = Config::get_default_path()?; + let loaded_config = Config::load(config_path)?; + + Ok(Build::run(loaded_config.build)?) + } + } + } +} diff --git a/crates/cli/src/commands/build.rs b/crates/cli/src/commands/build.rs new file mode 100644 index 0000000..e5d4011 --- /dev/null +++ b/crates/cli/src/commands/build.rs @@ -0,0 +1,19 @@ +use simplex_build::{ArtifactsGenerator, ArtifactsResolver, BuildConfig}; + +use super::error::CommandError; + +pub struct Build {} + +impl Build { + pub fn run(config: BuildConfig) -> Result<(), CommandError> { + let output_dir = ArtifactsResolver::resolve_local_dir(&config.out_dir)?; + let src_dir = ArtifactsResolver::resolve_local_dir(&config.src_dir)?; + let files_to_build = ArtifactsResolver::resolve_files_to_build(&config.src_dir, &config.simf_files)?; + + Ok(ArtifactsGenerator::generate_artifacts( + &output_dir, + &src_dir, + &files_to_build, + )?) + } +} diff --git a/crates/cli/src/commands/commands.rs b/crates/cli/src/commands/commands.rs new file mode 100644 index 0000000..62a73c1 --- /dev/null +++ b/crates/cli/src/commands/commands.rs @@ -0,0 +1,37 @@ +use clap::{Args, Subcommand}; + +#[derive(Debug, Subcommand)] +pub enum Command { + Init, + Config, + Regtest, + Test { + #[command(subcommand)] + command: TestCommand, + }, + Build, +} + +#[derive(Debug, Subcommand)] +pub enum TestCommand { + Integration { + #[command(flatten)] + additional_flags: TestFlags, + }, + Run { + #[arg(long)] + tests: Vec, + #[command(flatten)] + additional_flags: TestFlags, + }, +} + +#[derive(Debug, Args, Copy, Clone)] +pub struct TestFlags { + #[arg(long)] + pub nocapture: bool, + #[arg(long = "show-output")] + pub show_output: bool, + #[arg(long)] + pub ignored: bool, +} diff --git a/crates/cli/src/commands/error.rs b/crates/cli/src/commands/error.rs new file mode 100644 index 0000000..03e2f40 --- /dev/null +++ b/crates/cli/src/commands/error.rs @@ -0,0 +1,14 @@ +#[derive(thiserror::Error, Debug)] +pub enum CommandError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error(transparent)] + Client(#[from] simplex_regtest::error::ClientError), + + #[error(transparent)] + Test(#[from] simplex_test::error::TestError), + + #[error(transparent)] + Build(#[from] simplex_build::error::BuildError), +} diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs new file mode 100644 index 0000000..312b28f --- /dev/null +++ b/crates/cli/src/commands/mod.rs @@ -0,0 +1,5 @@ +pub mod commands; +pub mod test; +pub mod regtest; +pub mod build; +pub mod error; diff --git a/crates/cli/src/commands/regtest.rs b/crates/cli/src/commands/regtest.rs new file mode 100644 index 0000000..d6afe2a --- /dev/null +++ b/crates/cli/src/commands/regtest.rs @@ -0,0 +1,34 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use simplex_regtest::TestClient; + +use crate::commands::error::CommandError; + +pub struct Regtest {} + +impl Regtest { + pub fn run() -> Result<(), CommandError> { + let mut client = TestClient::new(); + + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) + .expect("Error setting Ctrl-C handler"); + + println!("======================================"); + println!("Waiting for Ctrl-C..."); + println!("rpc: {}", client.rpc_url()); + println!("esplora: {}", client.esplora_url()); + let auth = client.auth().get_user_pass().unwrap(); + println!("user: {:?}, password: {:?}", auth.0.unwrap(), auth.1.unwrap()); + println!("======================================"); + + while running.load(Ordering::SeqCst) {} + + Ok(client.kill()?) + } +} diff --git a/crates/cli/src/commands/test.rs b/crates/cli/src/commands/test.rs new file mode 100644 index 0000000..359c3da --- /dev/null +++ b/crates/cli/src/commands/test.rs @@ -0,0 +1,118 @@ +use std::path::PathBuf; +use std::process::Stdio; + +use simplex_test::TestConfig; + +use super::commands::{TestCommand, TestFlags}; +use super::error::CommandError; + +pub struct Test {} + +impl Test { + pub fn run(config: TestConfig, command: &TestCommand) -> Result<(), CommandError> { + let cache_path = Self::get_test_config_cache_name()?; + config.to_file(&cache_path)?; + + let mut cargo_test_command = Self::build_cargo_test_command(&cache_path, command); + + let output = cargo_test_command.output()?; + + match output.status.code() { + Some(code) => { + println!("Exit Status: {}", code); + + if code == 0 { + println!("{}", String::from_utf8(output.stdout).unwrap()); + } + } + None => { + println!("Process terminated."); + } + } + + Ok(()) + } + + fn build_cargo_test_command(cache_path: &PathBuf, command: &TestCommand) -> std::process::Command { + let mut command_as_arg = String::new(); + + match command { + TestCommand::Integration { additional_flags } => { + command_as_arg.push_str("cargo test --tests"); + + let flag_args = Self::build_test_flags(&additional_flags); + + if !flag_args.is_empty() { + command_as_arg.push_str(" --"); + command_as_arg.push_str(&flag_args); + } + } + TestCommand::Run { + tests, + additional_flags, + } => { + // TODO: check this behavior + if tests.is_empty() { + command_as_arg.push_str("cargo test --tests"); + } else { + let mut arg = "cargo test".to_string(); + + for test_name in tests { + arg.push_str(&format!(" --test {test_name}")); + } + + command_as_arg.push_str(&arg); + } + + let flag_args = Self::build_test_flags(&additional_flags); + + if !flag_args.is_empty() { + command_as_arg.push_str(" --"); + command_as_arg.push_str(&flag_args); + } + } + } + + let mut cargo_test_command = std::process::Command::new("sh"); + cargo_test_command.args(["-c".to_string(), command_as_arg]); + + cargo_test_command + .env(simplex_test::TEST_ENV_NAME, cache_path) + .stdin(Stdio::inherit()) + .stderr(Stdio::inherit()) + .stdout(Stdio::inherit()); + + cargo_test_command + } + + fn build_test_flags(flags: &TestFlags) -> String { + let mut opt_params = String::new(); + + if flags.nocapture { + opt_params.push_str(" --nocapture"); + } + + if flags.show_output { + opt_params.push_str(" --show-output"); + } + + if flags.ignored { + opt_params.push_str(" --ignored"); + } + + opt_params + } + + fn get_test_config_cache_name() -> Result { + const TARGET_DIR_NAME: &str = "target"; + const SIMPLEX_CACHE_DIR_NAME: &str = "simplex"; + const SIMPLEX_TEST_CONFIG_NAME: &str = "simplex_test_config.toml"; + + let cwd = std::env::current_dir()?; + + Ok(cwd + .join(TARGET_DIR_NAME) + .join(SIMPLEX_CACHE_DIR_NAME) + .join(SIMPLEX_TEST_CONFIG_NAME)) + } +} diff --git a/crates/cli/src/config/config.rs b/crates/cli/src/config/config.rs new file mode 100644 index 0000000..168ca69 --- /dev/null +++ b/crates/cli/src/config/config.rs @@ -0,0 +1,70 @@ +use serde::Deserialize; +use std::path::{Path, PathBuf}; + +use simplex_build::BuildConfig; +use simplex_test::TestConfig; + +use super::error::ConfigError; + +pub const INIT_CONFIG: &str = include_str!("../../Simplex.default.toml"); +pub const CONFIG_FILENAME: &str = "Simplex.toml"; + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(default)] +pub struct Config { + pub test: Option, + pub build: BuildConfig, +} + +impl Config { + pub fn get_default_path() -> Result { + let cwd = std::env::current_dir()?; + + Ok(cwd.join(CONFIG_FILENAME)) + } + + pub fn load(path_buf: impl AsRef) -> Result { + let path = path_buf.as_ref().to_path_buf(); + + if !path.is_file() { + return Err(ConfigError::PathIsNotFile(path)); + } + + if !path.exists() { + return Err(ConfigError::PathNotExists(path)); + } + + let conf_str = std::fs::read_to_string(path)?; + let config: Config = toml::from_str(conf_str.as_str()).map_err(ConfigError::UnableToDeserialize)?; + + Self::validate(&config)?; + + Ok(config) + } + + fn validate(config: &Config) -> Result<(), ConfigError> { + match config.test.clone() { + Some(test_config) => match test_config.esplora { + Some(esplora_config) => { + Self::validate_network(&esplora_config.network)?; + + if test_config.rpc.is_some() && esplora_config.network != "ElementsRegtest" { + return Err(ConfigError::NetworkNameUnmatched(esplora_config.network.clone())); + } + + Ok(()) + } + None => Ok(()), + }, + None => Ok(()), + } + } + + fn validate_network(network: &String) -> Result<(), ConfigError> { + if network != "Liquid" && network != "LiquidTestnet" && network != "ElementsRegtest" { + return Err(ConfigError::BadNetworkName(network.clone())); + } + + Ok(()) + } +} diff --git a/crates/cli/src/config/error.rs b/crates/cli/src/config/error.rs new file mode 100644 index 0000000..d76ca7b --- /dev/null +++ b/crates/cli/src/config/error.rs @@ -0,0 +1,28 @@ +use std::path::PathBuf; + +#[derive(thiserror::Error, Debug)] +pub enum ConfigError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("TOML parse error: {0}")] + TomlParse(#[from] toml::de::Error), + + #[error("Network name should either be `Liquid`, `LiquidTestnet` or `ElementsRegtest`, got: {0}")] + BadNetworkName(String), + + #[error("Network name should be `ElementsRegtest` when RPC is specified, got: {0}")] + NetworkNameUnmatched(String), + + #[error("Unable to deserialize config: {0}")] + UnableToDeserialize(toml::de::Error), + + #[error("Unable to get env variable: {0}")] + UnableToGetEnv(#[from] std::env::VarError), + + #[error("Path doesn't a file: '{0}'")] + PathIsNotFile(PathBuf), + + #[error("Path doesn't exist: '{0}'")] + PathNotExists(PathBuf), +} diff --git a/crates/cli/src/config/mod.rs b/crates/cli/src/config/mod.rs new file mode 100644 index 0000000..ce90ebd --- /dev/null +++ b/crates/cli/src/config/mod.rs @@ -0,0 +1,4 @@ +pub mod error; +pub mod config; + +pub use config::{Config, INIT_CONFIG}; diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs new file mode 100644 index 0000000..885a3e1 --- /dev/null +++ b/crates/cli/src/error.rs @@ -0,0 +1,13 @@ +use crate::{commands, config}; + +#[derive(thiserror::Error, Debug)] +pub enum CliError { + #[error(transparent)] + Config(#[from] config::error::ConfigError), + + #[error(transparent)] + Command(#[from] commands::error::CommandError), + + #[error("IO error: '{0}'")] + Io(#[from] std::io::Error), +} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs new file mode 100644 index 0000000..9a253d5 --- /dev/null +++ b/crates/cli/src/lib.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod error; +pub mod cli; +pub mod commands; diff --git a/crates/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/macros/Cargo.toml b/crates/macros/Cargo.toml new file mode 100644 index 0000000..b4f4638 --- /dev/null +++ b/crates/macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "simplex-macros" +version = "0.1.0" +description = "Simplex macros re-export package" +license.workspace = true +edition.workspace = true + +[lib] +proc-macro = true + +[lints] +workspace = true + +[dependencies] +simplex-build = { workspace = true } +simplex-test = { workspace = true } + +syn = { version = "2.0.114", default-features = false, features = ["parsing", "proc-macro"] } diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs new file mode 100644 index 0000000..4758f85 --- /dev/null +++ b/crates/macros/src/lib.rs @@ -0,0 +1,21 @@ +use proc_macro::TokenStream; + +#[proc_macro] +pub fn include_simf(tokenstream: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(tokenstream as simplex_build::macros::parse::SynFilePath); + + match simplex_build::macros::expand(&input) { + Ok(ts) => ts.into(), + Err(e) => e.to_compile_error().into(), + } +} + +#[proc_macro_attribute] +pub fn test(args: TokenStream, input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as syn::ItemFn); + + match simplex_test::macros::expand(args.into(), input) { + Ok(ts) => ts.into(), + Err(e) => e.to_compile_error().into(), + } +} diff --git a/crates/regtest/Cargo.toml b/crates/regtest/Cargo.toml new file mode 100644 index 0000000..cddb69c --- /dev/null +++ b/crates/regtest/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "simplex-regtest" +version = "0.1.0" +description = "Simplex regtest command internal implementation" +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +simplex-sdk = { workspace = true } + +thiserror = { workspace = true } +electrsd = { workspace = true } diff --git a/crates/regtest/src/args.rs b/crates/regtest/src/args.rs new file mode 100644 index 0000000..23fcbb1 --- /dev/null +++ b/crates/regtest/src/args.rs @@ -0,0 +1,21 @@ +pub fn get_elementsd_bin_args() -> Vec { + vec![ + "-fallbackfee=0.0001".to_string(), + "-dustrelayfee=0.00000001".to_string(), + "-acceptdiscountct=1".to_string(), + "-rest".to_string(), + "-evbparams=simplicity:-1:::".to_string(), + "-minrelaytxfee=0".to_string(), + "-blockmintxfee=0".to_string(), + "-chain=liquidregtest".to_string(), + "-txindex=1".to_string(), + "-validatepegin=0".to_string(), + "-initialfreecoins=2100000000000000".to_string(), + "-listen=1".to_string(), + "-txindex=1".to_string(), + ] +} + +pub fn get_electrs_bin_args() -> Vec { + vec![] +} diff --git a/crates/regtest/src/client.rs b/crates/regtest/src/client.rs new file mode 100644 index 0000000..a5ab4bb --- /dev/null +++ b/crates/regtest/src/client.rs @@ -0,0 +1,104 @@ +use std::net::TcpListener; +use std::path::{Path, PathBuf}; + +use electrsd::ElectrsD; +use electrsd::bitcoind; +use electrsd::bitcoind::bitcoincore_rpc::Auth; +use electrsd::bitcoind::{BitcoinD, Conf}; + +use super::error::ClientError; +use crate::args::{get_electrs_bin_args, get_elementsd_bin_args}; + +pub struct TestClient { + pub electrs: ElectrsD, + pub elements: BitcoinD, +} + +impl TestClient { + // TODO: pass custom config + pub fn new() -> Self { + let (electrs_path, elementsd_path) = Self::default_bin_paths(); + let zmq_addr = Self::get_zmq_addr(); + let elements = Self::create_bitcoind_node(elementsd_path, &zmq_addr); + let electrs = Self::create_electrs_node(electrs_path, &elements, &zmq_addr); + + Self { + electrs: electrs, + elements: elements, + } + } + + pub fn default_bin_paths() -> (PathBuf, PathBuf) { + // TODO: change binary into installed one in $PATH dir + const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); + const ELEMENTSD_BIN_PATH: &str = "../../assets/elementsd"; + const ELECTRS_BIN_PATH: &str = "../../assets/electrs"; + + ( + Path::new(MANIFEST_DIR).join(ELECTRS_BIN_PATH), + Path::new(MANIFEST_DIR).join(ELEMENTSD_BIN_PATH), + ) + } + + pub fn rpc_url(&self) -> String { + self.elements.rpc_url() + } + + pub fn esplora_url(&self) -> String { + let url = self.electrs.esplora_url.clone().unwrap(); + let port = url.split_once(":").unwrap().1; + + format!("http://127.0.0.1:{}", port) + } + + pub fn auth(&self) -> Auth { + let cookie = self.elements.params.get_cookie_values().unwrap().unwrap(); + + Auth::UserPass(cookie.user, cookie.password) + } + + pub fn kill(&mut self) -> Result<(), ClientError> { + // electrs stops elements automatically + self.electrs.kill().map_err(|_| ClientError::ElectrsTermination())?; + + Ok(()) + } + + fn get_zmq_addr() -> String { + TcpListener::bind("127.0.0.1:0") + .unwrap() + .local_addr() + .unwrap() + .to_string() + } + + fn create_bitcoind_node(bin_path: impl AsRef, zmq_addr: &String) -> BitcoinD { + let mut conf = Conf::default(); + let mut bin_args = get_elementsd_bin_args(); + + bin_args.push(format!("-zmqpubrawtx=tcp://{zmq_addr}")); + bin_args.push(format!("-zmqpubrawblock=tcp://{zmq_addr}")); + bin_args.push(format!("-zmqpubhashtx=tcp://{zmq_addr}")); + bin_args.push(format!("-zmqpubhashblock=tcp://{zmq_addr}")); + bin_args.push(format!("-zmqpubsequence=tcp://{zmq_addr}")); + + conf.args = bin_args.iter().map(|x| x.as_ref()).collect::>(); + conf.network = "liquidregtest"; + conf.p2p = bitcoind::P2P::Yes; + + BitcoinD::with_conf(bin_path.as_ref(), &conf).unwrap() + } + + fn create_electrs_node(bin_path: impl AsRef, elementsd: &BitcoinD, zmq_addr: &String) -> ElectrsD { + let mut conf = electrsd::Conf::default(); + let mut bin_args = get_electrs_bin_args(); + + bin_args.push(format!("--zmq-addr={zmq_addr}")); + + conf.args = bin_args.iter().map(|x| x.as_ref()).collect::>(); + conf.http_enabled = true; + conf.network = "liquidregtest"; + + ElectrsD::with_conf(bin_path.as_ref(), &elementsd, &conf).unwrap() + } +} diff --git a/crates/regtest/src/error.rs b/crates/regtest/src/error.rs new file mode 100644 index 0000000..966185e --- /dev/null +++ b/crates/regtest/src/error.rs @@ -0,0 +1,8 @@ +#[derive(thiserror::Error, Debug)] +pub enum ClientError { + #[error("Failed to terminate elements")] + ElementsTermination(), + + #[error("Failed to terminate electrs")] + ElectrsTermination(), +} diff --git a/crates/regtest/src/lib.rs b/crates/regtest/src/lib.rs new file mode 100644 index 0000000..7a0cd31 --- /dev/null +++ b/crates/regtest/src/lib.rs @@ -0,0 +1,5 @@ +mod args; +pub mod client; +pub mod error; + +pub use client::TestClient; diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 29943f7..197a116 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -1,32 +1,24 @@ [package] -name = "simplex-simplicity" +name = "simplex-sdk" version = "0.1.0" -edition = "2024" -rust-version = "1.90" -description = "High-level helpers for compiling and executing Simplicity programs on Liquid" -license = "MIT OR Apache-2.0" -repository = "https://github.com/BlockstreamResearch/simplicity-contracts" -homepage = "https://github.com/BlockstreamResearch/simplicity-contracts/tree/dev/crates/simplicityhl-core" -readme = "README.md" -documentation = "https://docs.rs/simplicityhl-core" -keywords = ["simplicity", "liquid", "bitcoin", "elements", "taproot"] -categories = ["cryptography::cryptocurrencies"] +description = "Simplex sdk to simplify the development with simplicity" +license.workspace = true +edition.workspace = true [lints] workspace = true -[features] -encoding = ["dep:bincode"] - [dependencies] -thiserror = "2" - -bincode = { version = "2.0.1", optional = true } - +thiserror = { workspace = true } sha2 = { workspace = true } - -hex = { workspace = true } - +minreq = { workspace = true } simplicityhl = { workspace = true } +electrsd = { workspace = true } +serde = { workspace = true } +hex = { workspace = true } -minreq = { workspace = true } +serde_json = { version = "1.0" } +elements-miniscript = { version = "0.4", features = ["base64", "serde"] } +bip39 = { version = "2.0.0", features = ["rand"] } +dyn-clone = { version = "1.0.20" } +bitcoin_hashes = { version = "0.14.1" } diff --git a/crates/sdk/README.md b/crates/sdk/README.md deleted file mode 100644 index d31ac58..0000000 --- a/crates/sdk/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Simplex simplicity - - - -## 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/sdk/src/constants.rs b/crates/sdk/src/constants.rs new file mode 100644 index 0000000..b9c3b45 --- /dev/null +++ b/crates/sdk/src/constants.rs @@ -0,0 +1,16 @@ +pub const PUBLIC_SECRET_BLINDER_KEY: [u8; 32] = [1; 32]; +pub const DUMMY_SIGNATURE: [u8; 64] = [1; 64]; + +pub const MIN_FEE: u64 = 10; + +/// Policy asset id (hex, BE) for Liquid mainnet. +pub const LIQUID_POLICY_ASSET_STR: &str = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"; + +/// Policy asset id (hex, BE) for Liquid testnet. +pub const LIQUID_TESTNET_POLICY_ASSET_STR: &str = "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; + +/// Policy asset id (hex, BE) for Elements regtest. +pub const LIQUID_DEFAULT_REGTEST_ASSET_STR: &str = "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"; + +/// Example test asset id (hex, BE) on Liquid testnet. +pub const LIQUID_TESTNET_TEST_ASSET_ID_STR: &str = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index eb5a76d..5dc4e44 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1 +1,7 @@ -pub mod simplicityhl_core; +pub mod constants; +pub mod presets; +pub mod program; +pub mod provider; +pub mod signer; +pub mod transaction; +pub mod utils; diff --git a/crates/sdk/src/presets/mod.rs b/crates/sdk/src/presets/mod.rs new file mode 100644 index 0000000..90c5352 --- /dev/null +++ b/crates/sdk/src/presets/mod.rs @@ -0,0 +1,4 @@ +pub mod p2pk; + +pub use p2pk::P2PK; +pub use p2pk::p2pk_build::{P2PKArguments, P2PKWitness}; diff --git a/crates/sdk/src/presets/p2pk.rs b/crates/sdk/src/presets/p2pk.rs new file mode 100644 index 0000000..7f70a04 --- /dev/null +++ b/crates/sdk/src/presets/p2pk.rs @@ -0,0 +1,66 @@ +use crate::program::ArgumentsTrait; +use crate::program::Program; + +use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; + +// TODO macro +pub struct P2PK { + program: Program, +} + +impl P2PK { + pub const SOURCE: &'static str = include_str!("./simf/p2pk.simf"); + + pub fn new(public_key: XOnlyPublicKey, arguments: impl ArgumentsTrait + 'static) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + + pub fn get_program(&self) -> &Program { + &self.program + } + + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } +} + +pub mod p2pk_build { + use crate::program::ArgumentsTrait; + use crate::program::WitnessTrait; + use simplicityhl::num::U256; + use simplicityhl::str::WitnessName; + use simplicityhl::value::UIntValue; + use simplicityhl::value::ValueConstructible; + use simplicityhl::{Arguments, Value, WitnessValues}; + use std::collections::HashMap; + + #[derive(Clone)] + pub struct P2PKWitness { + pub signature: [u8; 64usize], + } + + #[derive(Clone)] + pub struct P2PKArguments { + pub public_key: [u8; 32], + } + + impl WitnessTrait for P2PKWitness { + fn build_witness(&self) -> WitnessValues { + WitnessValues::from(HashMap::from([( + WitnessName::from_str_unchecked("SIGNATURE"), + Value::byte_array(self.signature), + )])) + } + } + + impl ArgumentsTrait for P2PKArguments { + fn build_arguments(&self) -> Arguments { + Arguments::from(HashMap::from([( + WitnessName::from_str_unchecked("PUBLIC_KEY"), + Value::from(UIntValue::U256(U256::from_byte_array(self.public_key))), + )])) + } + } +} diff --git a/crates/sdk/src/presets/simf/p2pk.simf b/crates/sdk/src/presets/simf/p2pk.simf new file mode 100644 index 0000000..f6a75e6 --- /dev/null +++ b/crates/sdk/src/presets/simf/p2pk.simf @@ -0,0 +1,3 @@ +fn main() { + jet::bip_0340_verify((param::PUBLIC_KEY, jet::sig_all_hash()), witness::SIGNATURE) +} diff --git a/crates/sdk/src/program/arguments.rs b/crates/sdk/src/program/arguments.rs new file mode 100644 index 0000000..60799cd --- /dev/null +++ b/crates/sdk/src/program/arguments.rs @@ -0,0 +1,8 @@ +use dyn_clone::DynClone; +use simplicityhl::Arguments; + +pub trait ArgumentsTrait: DynClone { + fn build_arguments(&self) -> Arguments; +} + +dyn_clone::clone_trait_object!(ArgumentsTrait); diff --git a/crates/sdk/src/program/error.rs b/crates/sdk/src/program/error.rs new file mode 100644 index 0000000..b80e8e1 --- /dev/null +++ b/crates/sdk/src/program/error.rs @@ -0,0 +1,29 @@ +#[derive(Debug, thiserror::Error)] +pub enum ProgramError { + #[error("Failed to compile Simplicity program: {0}")] + Compilation(String), + + #[error("Failed to satisfy witness: {0}")] + WitnessSatisfaction(String), + + #[error("Failed to prune program: {0}")] + Pruning(#[from] simplicityhl::simplicity::bit_machine::ExecutionError), + + #[error("Failed to construct a Bit Machine with enough space: {0}")] + BitMachineCreation(#[from] simplicityhl::simplicity::bit_machine::LimitError), + + #[error("Failed to execute program on the Bit Machine: {0}")] + Execution(simplicityhl::simplicity::bit_machine::ExecutionError), + + #[error("UTXO index {input_index} out of bounds (have {utxo_count} UTXOs)")] + UtxoIndexOutOfBounds { input_index: usize, utxo_count: usize }, + + #[error("Script pubkey mismatch: expected hash {expected_hash}, got {actual_hash}")] + ScriptPubkeyMismatch { expected_hash: String, actual_hash: String }, + + #[error("Failed to extract tx from pst: {0}")] + TxExtraction(#[from] simplicityhl::elements::pset::Error), + + #[error("Input index exceeds u32 maximum: {0}")] + InputIndexOverflow(#[from] std::num::TryFromIntError), +} diff --git a/crates/sdk/src/program/mod.rs b/crates/sdk/src/program/mod.rs new file mode 100644 index 0000000..7d924e5 --- /dev/null +++ b/crates/sdk/src/program/mod.rs @@ -0,0 +1,9 @@ +pub mod arguments; +pub mod error; +pub mod program; +pub mod witness; + +pub use arguments::ArgumentsTrait; +pub use error::ProgramError; +pub use program::{Program, ProgramTrait}; +pub use witness::WitnessTrait; diff --git a/crates/sdk/src/program/program.rs b/crates/sdk/src/program/program.rs new file mode 100644 index 0000000..da9c97c --- /dev/null +++ b/crates/sdk/src/program/program.rs @@ -0,0 +1,211 @@ +use std::sync::Arc; + +use sha2::{Digest, Sha256}; + +use dyn_clone::DynClone; + +use simplicityhl::CompiledProgram; +use simplicityhl::WitnessValues; +use simplicityhl::elements::pset::PartiallySignedTransaction; +use simplicityhl::elements::{Address, Script, Transaction, TxOut, script, taproot}; +use simplicityhl::simplicity::bitcoin::{XOnlyPublicKey, secp256k1}; +use simplicityhl::simplicity::jet::Elements; +use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; +use simplicityhl::simplicity::{BitMachine, RedeemNode, Value}; +use simplicityhl::tracker::{DefaultTracker, TrackerLogLevel}; + +use super::arguments::ArgumentsTrait; +use super::error::ProgramError; +use crate::provider::SimplicityNetwork; + +pub trait ProgramTrait: DynClone { + fn get_env( + &self, + pst: &PartiallySignedTransaction, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result>, ProgramError>; + + fn execute( + &self, + pst: &PartiallySignedTransaction, + witness: &WitnessValues, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result<(Arc>, Value), ProgramError>; + + fn finalize( + &self, + pst: &PartiallySignedTransaction, + witness: &WitnessValues, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result>, ProgramError>; +} + +#[derive(Clone)] +pub struct Program { + source: &'static str, + pub_key: XOnlyPublicKey, + arguments: Box, +} + +dyn_clone::clone_trait_object!(ProgramTrait); + +impl ProgramTrait for Program { + fn get_env( + &self, + pst: &PartiallySignedTransaction, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result>, ProgramError> { + let genesis_hash = network.genesis_block_hash(); + let cmr = self.load()?.commit().cmr(); + let utxos: Vec = pst.inputs().iter().filter_map(|x| x.witness_utxo.clone()).collect(); + + if utxos.len() <= input_index { + return Err(ProgramError::UtxoIndexOutOfBounds { + input_index, + utxo_count: utxos.len(), + }); + } + + let target_utxo = &utxos[input_index]; + let script_pubkey = self.get_tr_address(network.clone())?.script_pubkey(); + + if target_utxo.script_pubkey != script_pubkey { + return Err(ProgramError::ScriptPubkeyMismatch { + expected_hash: script_pubkey.script_hash().to_string(), + actual_hash: target_utxo.script_pubkey.script_hash().to_string(), + }); + } + + Ok(ElementsEnv::new( + Arc::new(pst.extract_tx()?), + utxos + .iter() + .map(|utxo| ElementsUtxo { + script_pubkey: utxo.script_pubkey.clone(), + asset: utxo.asset, + value: utxo.value, + }) + .collect(), + u32::try_from(input_index)?, + cmr, + self.control_block()?, + None, + genesis_hash, + )) + } + + fn execute( + &self, + pst: &PartiallySignedTransaction, + witness: &WitnessValues, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result<(Arc>, Value), ProgramError> { + let satisfied = self + .load()? + .satisfy(witness.clone()) + .map_err(ProgramError::WitnessSatisfaction)?; + + let mut tracker = DefaultTracker::new(satisfied.debug_symbols()).with_log_level(TrackerLogLevel::Debug); + + let env = self.get_env(pst, input_index, network)?; + + let pruned = satisfied.redeem().prune_with_tracker(&env, &mut tracker)?; + let mut mac = BitMachine::for_program(&pruned)?; + + let result = mac.exec(&pruned, &env)?; + + Ok((pruned, result)) + } + + fn finalize( + &self, + pst: &PartiallySignedTransaction, + witness: &WitnessValues, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result>, ProgramError> { + let pruned = self.execute(&pst, witness, input_index, network)?.0; + + let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); + let cmr = pruned.cmr(); + + Ok(vec![ + simplicity_witness_bytes, + simplicity_program_bytes, + cmr.as_ref().to_vec(), + self.control_block()?.serialize(), + ]) + } +} + +impl Program { + pub fn new(source: &'static str, pub_key: XOnlyPublicKey, arguments: Box) -> Self { + Self { + source: source, + pub_key: pub_key, + arguments: arguments, + } + } + + pub fn get_tr_address(&self, network: SimplicityNetwork) -> Result { + let spend_info = self.taproot_spending_info()?; + + Ok(Address::p2tr( + secp256k1::SECP256K1, + spend_info.internal_key(), + spend_info.merkle_root(), + None, + network.address_params(), + )) + } + + pub fn get_script_pubkey(&self, network: SimplicityNetwork) -> Result { + Ok(self.get_tr_address(network)?.script_pubkey()) + } + + pub fn get_script_hash(&self, network: SimplicityNetwork) -> Result<[u8; 32], ProgramError> { + let script = self.get_script_pubkey(network)?; + let mut hasher = Sha256::new(); + + sha2::digest::Update::update(&mut hasher, script.as_bytes()); + Ok(hasher.finalize().into()) + } + + fn load(&self) -> Result { + let compiled = CompiledProgram::new(self.source, self.arguments.build_arguments(), true) + .map_err(ProgramError::Compilation)?; + Ok(compiled) + } + + fn script_version(&self) -> Result<(Script, taproot::LeafVersion), ProgramError> { + let cmr = self.load()?.commit().cmr(); + let script = script::Script::from(cmr.as_ref().to_vec()); + + Ok((script, simplicityhl::simplicity::leaf_version())) + } + + fn taproot_spending_info(&self) -> Result { + let builder = taproot::TaprootBuilder::new(); + let (script, version) = self.script_version()?; + + let builder = builder + .add_leaf_with_ver(0, script, version) + .expect("tap tree should be valid"); + + Ok(builder + .finalize(secp256k1::SECP256K1, self.pub_key) + .expect("tap tree should be valid")) + } + + fn control_block(&self) -> Result { + let info = self.taproot_spending_info()?; + let script_ver = self.script_version()?; + + Ok(info.control_block(&script_ver).expect("control block should exist")) + } +} diff --git a/crates/sdk/src/program/witness.rs b/crates/sdk/src/program/witness.rs new file mode 100644 index 0000000..374f8f6 --- /dev/null +++ b/crates/sdk/src/program/witness.rs @@ -0,0 +1,8 @@ +use dyn_clone::DynClone; +use simplicityhl::WitnessValues; + +pub trait WitnessTrait: DynClone { + fn build_witness(&self) -> WitnessValues; +} + +dyn_clone::clone_trait_object!(WitnessTrait); diff --git a/crates/sdk/src/provider/error.rs b/crates/sdk/src/provider/error.rs new file mode 100644 index 0000000..40a994b --- /dev/null +++ b/crates/sdk/src/provider/error.rs @@ -0,0 +1,22 @@ +use crate::provider::rpc::error::RpcError; + +#[derive(Debug, thiserror::Error)] +pub enum ProviderError { + #[error(transparent)] + Rpc(#[from] RpcError), + + #[error("HTTP request failed: {0}")] + Request(String), + + #[error("Couldn't wait for the transaction to be confirmed")] + Confirmation(), + + #[error("Broadcast failed with HTTP {status} for {url}: {message}")] + BroadcastRejected { status: u16, url: String, message: String }, + + #[error("Failed to deserialize response: {0}")] + Deserialize(String), + + #[error("Invalid txid format: {0}")] + InvalidTxid(String), +} diff --git a/crates/sdk/src/provider/esplora.rs b/crates/sdk/src/provider/esplora.rs new file mode 100644 index 0000000..fbe640d --- /dev/null +++ b/crates/sdk/src/provider/esplora.rs @@ -0,0 +1,252 @@ +use std::collections::HashMap; +use std::collections::HashSet; +use std::str::FromStr; +use std::time::Duration; + +use simplicityhl::elements::hashes::{Hash, sha256}; + +use simplicityhl::elements::encode; +use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid}; + +use serde::Deserialize; + +use crate::provider::SimplicityNetwork; + +use super::error::ProviderError; +use super::provider::{DEFAULT_ESPLORA_TIMEOUT_SECS, ProviderTrait}; + +pub struct EsploraProvider { + pub esplora_url: String, + pub network: SimplicityNetwork, + pub timeout: Duration, +} + +#[derive(Deserialize)] +#[allow(dead_code)] +struct TxStatus { + confirmed: bool, + block_height: Option, +} + +#[derive(Clone, Deserialize)] +#[allow(dead_code)] +struct UtxoStatus { + pub confirmed: bool, + pub block_height: Option, + pub block_hash: Option, + pub block_time: Option, +} + +#[derive(Clone, Deserialize)] +#[allow(dead_code)] +struct EsploraUtxo { + pub txid: String, + pub vout: u32, + pub value: Option, + pub valuecommitment: Option, + pub asset: Option, + pub assetcommitment: Option, + pub status: UtxoStatus, +} + +impl EsploraProvider { + pub fn new(url: String, network: SimplicityNetwork) -> Self { + Self { + esplora_url: url, + network: network, + timeout: Duration::from_secs(DEFAULT_ESPLORA_TIMEOUT_SECS), + } + } + + fn esplora_utxo_to_outpoint(&self, utxo: &EsploraUtxo) -> Result { + let txid = Txid::from_str(&utxo.txid).map_err(|e| ProviderError::InvalidTxid(e.to_string()))?; + + Ok(OutPoint::new(txid, utxo.vout)) + } + + fn populate_txouts_from_outpoints( + &self, + outpoints: &Vec, + ) -> Result, ProviderError> { + let set: HashSet<_> = outpoints.into_iter().collect(); + let mut map = HashMap::new(); + + // filter unique transactions + for point in set { + let tx = self.fetch_transaction(&point.txid)?; + map.insert(point.txid, tx); + } + + // populate TxOuts + Ok(outpoints + .iter() + .map(|point| { + ( + *point, + map.get(&point.txid).unwrap().output[point.vout as usize].clone(), + ) + }) + .collect()) + } +} + +impl ProviderTrait for EsploraProvider { + fn get_network(&self) -> &SimplicityNetwork { + &self.network + } + + fn broadcast_transaction(&self, tx: &Transaction) -> Result { + let tx_hex = encode::serialize_hex(tx); + let url = format!("{}/tx", self.esplora_url); + let timeout_secs = self.timeout.as_secs(); + + let response = minreq::post(&url) + .with_timeout(timeout_secs) + .with_body(tx_hex) + .send() + .map_err(|e| ProviderError::Request(e.to_string()))?; + + let status = response.status_code; + let body = response.as_str().unwrap_or("").trim().to_owned(); + + if !(200..300).contains(&status) { + return Err(ProviderError::BroadcastRejected { + status: status as u16, + url: format!("{}/tx", self.esplora_url), + message: body, + }); + } + + Ok(Txid::from_str(&body).map_err(|e| ProviderError::InvalidTxid(e.to_string()))?) + } + + fn wait(&self, txid: &Txid) -> Result<(), ProviderError> { + let url = format!("{}/tx/{}/status", self.esplora_url, txid); + let timeout_secs = self.timeout.as_secs(); + + let confirmation_poll = match self.network.clone() { + SimplicityNetwork::ElementsRegtest { .. } => Duration::from_millis(100), + _ => Duration::from_secs(10), + }; + + // polling needs to be > 1 min on mainnet/testnet + for _ in 1..10 { + let response = minreq::get(&url) + .with_timeout(timeout_secs) + .send() + .map_err(|e| ProviderError::Request(e.to_string()))?; + + if response.status_code != 200 { + std::thread::sleep(confirmation_poll); + continue; + } + + let status: TxStatus = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?; + + if status.confirmed { + return Ok(()); + } + + std::thread::sleep(confirmation_poll); + } + + Err(ProviderError::Confirmation()) + } + + fn fetch_transaction(&self, txid: &Txid) -> Result { + let url = format!("{}/tx/{}/raw", self.esplora_url, txid); + let timeout_secs = self.timeout.as_secs(); + + let response = minreq::get(&url) + .with_timeout(timeout_secs) + .send() + .map_err(|e| ProviderError::Request(e.to_string()))?; + + if response.status_code != 200 { + return Err(ProviderError::Request(format!( + "HTTP {}: {}", + response.status_code, response.reason_phrase + ))); + } + + let bytes = response.as_bytes(); + let tx: Transaction = encode::deserialize(bytes).map_err(|e| ProviderError::Deserialize(e.to_string()))?; + + Ok(tx) + } + + fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError> { + let url = format!("{}/address/{}/utxo", self.esplora_url, address); + let timeout_secs = self.timeout.as_secs(); + + let response = minreq::get(&url) + .with_timeout(timeout_secs) + .send() + .map_err(|e| ProviderError::Request(e.to_string()))?; + + if response.status_code != 200 { + return Err(ProviderError::Request(format!( + "HTTP {}: {}", + response.status_code, response.reason_phrase + ))); + } + + let utxos: Vec = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?; + let outpoints = utxos + .iter() + .map(|utxo| Ok(self.esplora_utxo_to_outpoint(&utxo)?)) + .collect::, ProviderError>>()?; + + Ok(self.populate_txouts_from_outpoints(&outpoints)?) + } + + fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError> { + let hash = sha256::Hash::hash(script.as_bytes()); + let hash_bytes = hash.to_byte_array(); + let scripthash = hex::encode(hash_bytes); + + let url = format!("{}/scripthash/{}/utxo", self.esplora_url, scripthash); + let timeout_secs = self.timeout.as_secs(); + + let response = minreq::get(&url) + .with_timeout(timeout_secs) + .send() + .map_err(|e| ProviderError::Request(e.to_string()))?; + + if response.status_code != 200 { + return Err(ProviderError::Request(format!( + "HTTP {}: {}", + response.status_code, response.reason_phrase + ))); + } + + let utxos: Vec = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?; + let outpoints = utxos + .iter() + .map(|utxo| Ok(self.esplora_utxo_to_outpoint(&utxo)?)) + .collect::, ProviderError>>()?; + + Ok(self.populate_txouts_from_outpoints(&outpoints)?) + } + + fn fetch_fee_estimates(&self) -> Result, ProviderError> { + let url = format!("{}/fee-estimates", self.esplora_url); + let timeout_secs = self.timeout.as_secs(); + + let response = minreq::get(&url) + .with_timeout(timeout_secs) + .send() + .map_err(|e| ProviderError::Request(e.to_string()))?; + + if response.status_code != 200 { + return Err(ProviderError::Request(format!( + "HTTP {}: {}", + response.status_code, response.reason_phrase + ))); + } + + let estimates: HashMap = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?; + + Ok(estimates) + } +} diff --git a/crates/sdk/src/provider/mod.rs b/crates/sdk/src/provider/mod.rs new file mode 100644 index 0000000..3ad48a2 --- /dev/null +++ b/crates/sdk/src/provider/mod.rs @@ -0,0 +1,16 @@ +pub mod error; +pub mod esplora; +pub mod network; +pub mod provider; +pub mod rpc; +pub mod simplex; + +pub use rpc::elements::ElementsRpc; +pub use esplora::EsploraProvider; +pub use simplex::SimplexProvider; +pub use provider::ProviderTrait; + +pub use network::*; + +pub use rpc::error::RpcError; +pub use error::ProviderError; diff --git a/crates/sdk/src/provider/network.rs b/crates/sdk/src/provider/network.rs new file mode 100644 index 0000000..c7e3cd3 --- /dev/null +++ b/crates/sdk/src/provider/network.rs @@ -0,0 +1,77 @@ + +use std::str::FromStr; + +use simplicityhl::simplicity::elements; +use simplicityhl::simplicity::hashes::{Hash, sha256}; + +use crate::constants::{LIQUID_DEFAULT_REGTEST_ASSET_STR, LIQUID_POLICY_ASSET_STR, LIQUID_TESTNET_POLICY_ASSET_STR}; + +pub static LIQUID_TESTNET_BITCOIN_ASSET: std::sync::LazyLock = std::sync::LazyLock::new(|| { + elements::AssetId::from_inner(sha256::Midstate([ + 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_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_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_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, + ]) +}); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SimplicityNetwork { + Liquid, + LiquidTestnet, + ElementsRegtest { policy_asset: elements::AssetId }, +} + +impl SimplicityNetwork { + pub fn default_regtest() -> Self { + let policy_asset = elements::AssetId::from_str(LIQUID_DEFAULT_REGTEST_ASSET_STR).unwrap(); + Self::ElementsRegtest { policy_asset } + } + + pub fn policy_asset(&self) -> elements::AssetId { + match self { + Self::Liquid => elements::AssetId::from_str(LIQUID_POLICY_ASSET_STR).unwrap(), + Self::LiquidTestnet => elements::AssetId::from_str(LIQUID_TESTNET_POLICY_ASSET_STR).unwrap(), + Self::ElementsRegtest { policy_asset } => *policy_asset, + } + } + + pub fn genesis_block_hash(&self) -> elements::BlockHash { + match self { + Self::Liquid => *LIQUID_MAINNET_GENESIS, + Self::LiquidTestnet => *LIQUID_TESTNET_GENESIS, + Self::ElementsRegtest { .. } => *LIQUID_REGTEST_GENESIS, + } + } + + pub fn is_mainnet(&self) -> bool { + self == &Self::Liquid + } + + pub const fn address_params(&self) -> &'static elements::AddressParams { + match self { + Self::Liquid => &elements::AddressParams::LIQUID, + Self::LiquidTestnet => &elements::AddressParams::LIQUID_TESTNET, + Self::ElementsRegtest { .. } => &elements::AddressParams::ELEMENTS, + } + } +} diff --git a/crates/sdk/src/provider/provider.rs b/crates/sdk/src/provider/provider.rs new file mode 100644 index 0000000..6bc187f --- /dev/null +++ b/crates/sdk/src/provider/provider.rs @@ -0,0 +1,57 @@ +use std::collections::HashMap; + +use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid}; + +use crate::provider::SimplicityNetwork; + +use super::error::ProviderError; + +pub const DEFAULT_FEE_RATE: f32 = 100.0; +pub const DEFAULT_ESPLORA_TIMEOUT_SECS: u64 = 10; + +pub trait ProviderTrait { + fn get_network(&self) -> &SimplicityNetwork; + + fn broadcast_transaction(&self, tx: &Transaction) -> Result; + + fn wait(&self, txid: &Txid) -> Result<(), ProviderError>; + + fn fetch_transaction(&self, txid: &Txid) -> Result; + + fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError>; + + fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError>; + + fn fetch_fee_estimates(&self) -> Result, ProviderError>; + + fn fetch_fee_rate(&self, target_blocks: u32) -> Result { + let estimates = self.fetch_fee_estimates()?; + let target_str = target_blocks.to_string(); + + if let Some(&rate) = estimates.get(&target_str) { + return Ok((rate * 1000.0) as f32); // Convert sat/vB to sats/kvb + } + + let fallback_targets = [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 144, 504, 1008, + ]; + + for &target in fallback_targets.iter().filter(|&&t| t >= target_blocks) { + let key = target.to_string(); + + if let Some(&rate) = estimates.get(&key) { + return Ok((rate * 1000.0) as f32); + } + } + + for &target in &fallback_targets { + let key = target.to_string(); + + if let Some(&rate) = estimates.get(&key) { + return Ok((rate * 1000.0) as f32); + } + } + + Ok(DEFAULT_FEE_RATE) + } +} diff --git a/crates/sdk/src/provider/rpc/elements.rs b/crates/sdk/src/provider/rpc/elements.rs new file mode 100644 index 0000000..1a7437e --- /dev/null +++ b/crates/sdk/src/provider/rpc/elements.rs @@ -0,0 +1,128 @@ +use std::str::FromStr; + +use electrsd::bitcoind::bitcoincore_rpc::{Auth, Client, RpcApi}; + +use serde_json::Value; + +use simplicityhl::elements::{Address, AssetId, BlockHash, Txid}; + +use super::error::RpcError; + +use crate::utils::sat2btc; + +pub struct ElementsRpc { + pub inner: Client, + pub auth: Auth, + pub url: String, +} + +impl ElementsRpc { + pub fn new(url: String, auth: Auth) -> Result { + let inner = Client::new(url.as_str(), auth.clone())?; + inner.ping()?; + + Ok(Self { + inner: inner, + auth: auth, + url: url, + }) + } + + pub fn height(&self) -> Result { + const METHOD: &str = "getblockcount"; + + self.inner + .call::(METHOD, &[])? + .as_u64() + .ok_or_else(|| RpcError::ElementsRpcUnexpectedReturn(METHOD.into())) + } + + pub fn block_hash(&self, height: u64) -> Result { + const METHOD: &str = "getblockhash"; + + let raw: Value = self.inner.call(METHOD, &[height.into()])?; + + Ok(BlockHash::from_str(raw.as_str().unwrap())?) + } + + pub fn sendtoaddress(&self, address: &Address, satoshi: u64, asset: Option) -> Result { + const METHOD: &str = "sendtoaddress"; + + let btc = sat2btc(satoshi); + let r = match asset { + Some(asset) => self.inner.call::( + METHOD, + &[ + address.to_string().into(), + btc.into(), + "".into(), + "".into(), + false.into(), + false.into(), + 1.into(), + "UNSET".into(), + false.into(), + asset.to_string().into(), + ], + )?, + None => self + .inner + .call::(METHOD, &[address.to_string().into(), btc.into()])?, + }; + + Ok(Txid::from_str(r.as_str().unwrap()).unwrap()) + } + + pub fn rescanblockchain(&self, start: Option, stop: Option) -> Result<(), RpcError> { + const METHOD: &str = "rescanblockchain"; + + let mut args = Vec::with_capacity(2); + + if start.is_some() { + args.push(start.into()); + } + + if stop.is_some() { + args.push(stop.into()); + } + + self.inner.call::(METHOD, &args)?; + + Ok(()) + } + + pub fn getnewaddress(&self, label: &str) -> Result { + const METHOD: &str = "getnewaddress"; + + let addr: Value = self.inner.call(METHOD, &[label.into(), "bech32".to_string().into()])?; + + Ok(Address::from_str(addr.as_str().unwrap()).unwrap()) + } + + pub fn generate_blocks(&self, block_num: u32) -> Result<(), RpcError> { + const METHOD: &str = "generatetoaddress"; + + let address = self.getnewaddress("")?.to_string(); + self.inner.call::(METHOD, &[block_num.into(), address.into()])?; + + Ok(()) + } + + pub fn sweep_initialfreecoins(&self) -> Result<(), RpcError> { + const METHOD: &str = "sendtoaddress"; + + let address = self.getnewaddress("")?; + self.inner.call::( + METHOD, + &[ + address.to_string().into(), + "21".into(), + "".into(), + "".into(), + true.into(), + ], + )?; + + Ok(()) + } +} diff --git a/crates/sdk/src/provider/rpc/error.rs b/crates/sdk/src/provider/rpc/error.rs new file mode 100644 index 0000000..0d76c06 --- /dev/null +++ b/crates/sdk/src/provider/rpc/error.rs @@ -0,0 +1,11 @@ +#[derive(thiserror::Error, Debug)] +pub enum RpcError { + #[error(transparent)] + ElementsRpcError(#[from] electrsd::bitcoind::bitcoincore_rpc::Error), + + #[error("Elements RPC returned an unexpected value for call {0}")] + ElementsRpcUnexpectedReturn(String), + + #[error("Failed to decode hex value to array, {0}")] + BitcoinHashesHex(#[from] bitcoin_hashes::hex::HexToArrayError), +} diff --git a/crates/sdk/src/provider/rpc/mod.rs b/crates/sdk/src/provider/rpc/mod.rs new file mode 100644 index 0000000..9201cc8 --- /dev/null +++ b/crates/sdk/src/provider/rpc/mod.rs @@ -0,0 +1,2 @@ +pub mod elements; +pub mod error; diff --git a/crates/sdk/src/provider/simplex.rs b/crates/sdk/src/provider/simplex.rs new file mode 100644 index 0000000..2fab759 --- /dev/null +++ b/crates/sdk/src/provider/simplex.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; + +use electrsd::bitcoind::bitcoincore_rpc::Auth; + +use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid}; + +use crate::provider::SimplicityNetwork; + +use super::error::ProviderError; +use super::provider::ProviderTrait; + +use super::{ElementsRpc, EsploraProvider}; + +pub struct SimplexProvider { + pub esplora: EsploraProvider, + pub elements: ElementsRpc, +} + +impl SimplexProvider { + pub fn new( + esplora_url: String, + elements_url: String, + auth: Auth, + network: SimplicityNetwork, + ) -> Result { + Ok(Self { + esplora: EsploraProvider::new(esplora_url, network), + elements: ElementsRpc::new(elements_url, auth)?, + }) + } +} + +impl ProviderTrait for SimplexProvider { + fn get_network(&self) -> &SimplicityNetwork { + self.esplora.get_network() + } + + fn broadcast_transaction(&self, tx: &Transaction) -> Result { + let txid = self.esplora.broadcast_transaction(tx)?; + + self.elements.generate_blocks(1)?; + + Ok(txid) + } + + fn wait(&self, txid: &Txid) -> Result<(), ProviderError> { + Ok(self.esplora.wait(txid)?) + } + + fn fetch_transaction(&self, txid: &Txid) -> Result { + Ok(self.esplora.fetch_transaction(txid)?) + } + + fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError> { + Ok(self.esplora.fetch_address_utxos(address)?) + } + + fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError> { + Ok(self.esplora.fetch_scripthash_utxos(script)?) + } + + fn fetch_fee_estimates(&self) -> Result, ProviderError> { + Ok(self.esplora.fetch_fee_estimates()?) + } +} diff --git a/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs new file mode 100644 index 0000000..6f91f07 --- /dev/null +++ b/crates/sdk/src/signer/error.rs @@ -0,0 +1,51 @@ +use crate::program::ProgramError; +use crate::provider::ProviderError; +use crate::transaction::TransactionError; + +#[derive(Debug, thiserror::Error)] +pub enum SignerError { + #[error(transparent)] + Program(#[from] ProgramError), + + #[error(transparent)] + Provider(#[from] ProviderError), + + #[error(transparent)] + Transaction(#[from] TransactionError), + + #[error("Failed to parse a mnemonic: {0}")] + Mnemonic(String), + + #[error("Failed to extract tx from pst: {0}")] + TxExtraction(#[from] simplicityhl::elements::pset::Error), + + #[error("Failed to construct a message for the input spending: {0}")] + SighashConstruction(#[from] elements_miniscript::psbt::SighashError), + + #[error("Fee amount is too low: {0}")] + DustAmount(i64), + + #[error("Not enough fee amount {0} to cover transaction costs: {1}")] + NotEnoughFeeAmount(i64, u64), + + #[error("Not enough funds on account to cover transaction costs: {0}")] + NotEnoughFunds(u64), + + #[error("Invalid secret key")] + InvalidSecretKey(#[from] simplicityhl::elements::secp256k1_zkp::UpstreamError), + + #[error("Failed to derive a private key: {0}")] + PrivateKeyDerivation(#[from] elements_miniscript::bitcoin::bip32::Error), + + #[error("Failed to construct a derivation path: {0}")] + DerivationPath(String), + + #[error("Failed to construct a wpkh descriptor: {0}")] + WpkhDescriptor(String), + + #[error("Failed to convert a descriptor: {0}")] + DescriptorConversion(#[from] elements_miniscript::descriptor::ConversionError), + + #[error("Failed to construct a wpkh address: {0}")] + WpkhAddressConstruction(#[from] elements_miniscript::Error), +} diff --git a/crates/sdk/src/signer/mod.rs b/crates/sdk/src/signer/mod.rs new file mode 100644 index 0000000..ae3f09a --- /dev/null +++ b/crates/sdk/src/signer/mod.rs @@ -0,0 +1,5 @@ +pub mod error; +pub mod signer; + +pub use error::SignerError; +pub use signer::{Signer, SignerTrait}; diff --git a/crates/sdk/src/signer/signer.rs b/crates/sdk/src/signer/signer.rs new file mode 100644 index 0000000..532854b --- /dev/null +++ b/crates/sdk/src/signer/signer.rs @@ -0,0 +1,416 @@ +use std::collections::{HashMap, HashSet}; +use std::str::FromStr; + +use elements_miniscript::Descriptor; +use elements_miniscript::bitcoin::PublicKey; +use elements_miniscript::descriptor::Wpkh; +use simplicityhl::Value; +use simplicityhl::WitnessValues; +use simplicityhl::elements::pset::PartiallySignedTransaction; +use simplicityhl::elements::secp256k1_zkp::{All, Keypair, Message, Secp256k1, ecdsa, schnorr}; +use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut}; +use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; +use simplicityhl::simplicity::hashes::Hash; +use simplicityhl::str::WitnessName; +use simplicityhl::value::ValueConstructible; + +use bip39::Mnemonic; + +use elements_miniscript::{ + DescriptorPublicKey, + bitcoin::{NetworkKind, PrivateKey, bip32::DerivationPath}, + elements::{ + EcdsaSighashType, + bitcoin::bip32::{Fingerprint, Xpriv, Xpub}, + sighash::SighashCache, + }, + elementssig_to_rawsig, + psbt::PsbtExt, +}; + +use super::error::SignerError; +use crate::constants::MIN_FEE; +use crate::program::ProgramTrait; +use crate::provider::ProviderTrait; +use crate::provider::SimplicityNetwork; +use crate::transaction::FinalTransaction; +use crate::transaction::PartialInput; +use crate::transaction::PartialOutput; +use crate::transaction::RequiredSignature; + +pub const PLACEHOLDER_FEE: u64 = 1; + +pub trait SignerTrait { + fn sign_program( + &self, + pst: &PartiallySignedTransaction, + program: &Box, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result; + + fn sign_input( + &self, + pst: &PartiallySignedTransaction, + input_index: usize, + ) -> Result<(PublicKey, ecdsa::Signature), SignerError>; +} + +pub struct Signer { + xprv: Xpriv, + provider: Box, + network: SimplicityNetwork, + secp: Secp256k1, +} + +impl SignerTrait for Signer { + fn sign_program( + &self, + pst: &PartiallySignedTransaction, + program: &Box, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result { + let env = program.get_env(&pst, input_index, network)?; + let msg = Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); + + let private_key = self.get_private_key()?; + let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner); + + Ok(self.secp.sign_schnorr(&msg, &keypair)) + } + + fn sign_input( + &self, + pst: &PartiallySignedTransaction, + input_index: usize, + ) -> Result<(PublicKey, ecdsa::Signature), SignerError> { + let tx = pst.extract_tx()?; + + let mut sighash_cache = SighashCache::new(&tx); + let genesis_hash = elements_miniscript::elements::BlockHash::all_zeros(); + + let message = pst + .sighash_msg(input_index, &mut sighash_cache, None, genesis_hash)? + .to_secp_msg(); + + let private_key = self.get_private_key()?; + let public_key = private_key.public_key(&self.secp); + + let signature = self.secp.sign_ecdsa_low_r(&message, &private_key.inner); + + Ok((public_key, signature)) + } +} + +enum Estimate { + Success(Transaction, u64), + Failure(u64), +} + +impl Signer { + pub fn new(mnemonic: &str, provider: Box) -> Result { + let secp = Secp256k1::new(); + let mnemonic: Mnemonic = mnemonic + .parse() + .map_err(|e: bip39::Error| SignerError::Mnemonic(e.to_string()))?; + let seed = mnemonic.to_seed(""); + let xprv = Xpriv::new_master(NetworkKind::Test, &seed)?; + + let network = provider.get_network().clone(); + + Ok(Self { + xprv, + provider: provider, + network: network, + secp: secp, + }) + } + + pub fn finalize(&self, tx: &FinalTransaction, target_blocks: u32) -> Result<(Transaction, u64), SignerError> { + let mut signer_utxos = self.get_wpkh_utxos()?; + let mut set = HashSet::new(); + + for input in tx.inputs() { + set.insert(OutPoint { + txid: input.partial_input.witness_txid.clone(), + vout: input.partial_input.witness_output_index, + }); + } + + signer_utxos + .retain(|utxo| utxo.1.asset.explicit().unwrap() == self.network.policy_asset() && !set.contains(&utxo.0)); + signer_utxos.sort_by(|a, b| b.1.value.cmp(&a.1.value)); + + let mut fee_tx = tx.clone(); + let mut curr_fee = MIN_FEE; + let fee_rate = self.provider.fetch_fee_rate(target_blocks)?; + + for utxo in signer_utxos { + let policy_amount_delta = fee_tx.calculate_fee_delta(); + + if policy_amount_delta >= curr_fee as i64 { + match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta as u64)? { + Estimate::Success(tx, fee) => return Ok((tx, fee)), + Estimate::Failure(required_fee) => curr_fee = required_fee, + } + } + + fee_tx.add_input(PartialInput::new(utxo.0, utxo.1), RequiredSignature::NativeEcdsa)?; + } + + // need to try one more time after the loop + let policy_amount_delta = fee_tx.calculate_fee_delta(); + + if policy_amount_delta >= curr_fee as i64 { + match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta as u64)? { + Estimate::Success(tx, fee) => return Ok((tx, fee)), + Estimate::Failure(required_fee) => curr_fee = required_fee, + } + } + + Err(SignerError::NotEnoughFunds(curr_fee)) + } + + pub fn finalize_strict( + &self, + tx: &FinalTransaction, + target_blocks: u32, + ) -> Result<(Transaction, u64), SignerError> { + let policy_amount_delta = tx.calculate_fee_delta(); + + if policy_amount_delta < MIN_FEE as i64 { + return Err(SignerError::DustAmount(policy_amount_delta)); + } + + let fee_rate = self.provider.fetch_fee_rate(target_blocks)?; + + // policy_amount_delta will be > 0 + match self.estimate_tx(tx.clone(), fee_rate, policy_amount_delta as u64)? { + Estimate::Success(tx, fee) => Ok((tx, fee)), + Estimate::Failure(required_fee) => Err(SignerError::NotEnoughFeeAmount(policy_amount_delta, required_fee)), + } + } + + pub fn get_provider(&self) -> &Box { + &self.provider + } + + pub fn get_wpkh_address(&self) -> Result { + let fingerprint = self.fingerprint()?; + let path = self.get_derivation_path()?; + let xpub = self.derive_xpub(&path)?; + + let desc = format!("elwpkh([{fingerprint}/{path}]{xpub}/<0;1>/*)"); + + let descriptor: Descriptor = + Descriptor::Wpkh(Wpkh::from_str(&desc).map_err(|e| SignerError::WpkhDescriptor(e.to_string()))?); + + Ok(descriptor.clone().into_single_descriptors()?[0] + .at_derivation_index(1)? + .address(self.network.address_params())?) + } + + pub fn get_wpkh_utxos(&self) -> Result, SignerError> { + Ok(self.provider.fetch_address_utxos(&self.get_wpkh_address()?)?) + } + + pub fn get_schnorr_public_key(&self) -> Result { + let private_key = self.get_private_key()?; + let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner); + + Ok(keypair.x_only_public_key().0) + } + + pub fn get_ecdsa_public_key(&self) -> Result { + Ok(self.get_private_key()?.public_key(&self.secp)) + } + + pub fn get_private_key(&self) -> Result { + let master_xprv = self.master_xpriv()?; + let full_path = self.get_derivation_path()?; + + let derived = + full_path.extend(DerivationPath::from_str("0/1").map_err(|e| SignerError::DerivationPath(e.to_string()))?); + + let ext_derived = master_xprv.derive_priv(&self.secp, &derived)?; + + Ok(PrivateKey::new(ext_derived.private_key, NetworkKind::Test)) + } + + fn estimate_tx( + &self, + mut fee_tx: FinalTransaction, + fee_rate: f32, + available_delta: u64, + ) -> Result { + // estimate the tx fee with the change + // use this wpkh address as a change script + fee_tx.add_output(PartialOutput::new( + self.get_wpkh_address()?.script_pubkey(), + PLACEHOLDER_FEE, + self.network.policy_asset(), + )); + + fee_tx.add_output(PartialOutput::new( + Script::new(), + PLACEHOLDER_FEE, + self.network.policy_asset(), + )); + + let final_tx = self.sign_tx(&fee_tx)?; + let fee = fee_tx.calculate_fee(final_tx.weight(), fee_rate); + + if available_delta > fee && available_delta - fee >= MIN_FEE { + // we have enough funds to cover the change UTXO + let outputs = fee_tx.outputs_mut(); + + outputs[outputs.len() - 2].amount = available_delta - fee; + outputs[outputs.len() - 1].amount = fee; + + let final_tx = self.sign_tx(&fee_tx)?; + + return Ok(Estimate::Success(final_tx, fee)); + } + + // not enough funds, so we need to estimate without the change + fee_tx.remove_output(fee_tx.n_outputs() - 2); + + let final_tx = self.sign_tx(&fee_tx)?; + let fee = fee_tx.calculate_fee(final_tx.weight(), fee_rate); + + if available_delta < fee { + return Ok(Estimate::Failure(fee)); + } + + let outputs = fee_tx.outputs_mut(); + + // change the fee output amount + outputs[outputs.len() - 1].amount = available_delta; + + // finalize the tx with fee and without the change + let final_tx = self.sign_tx(&fee_tx)?; + + return Ok(Estimate::Success(final_tx, fee)); + } + + fn sign_tx(&self, tx: &FinalTransaction) -> Result { + let mut pst = tx.extract_pst(); + let inputs = tx.inputs(); + + for index in 0..inputs.len() { + let input = inputs[index].clone(); + + // we need to prune the program + if input.program_input.is_some() { + let program = input.program_input.unwrap(); + let signed_witness: Result = match input.required_sig { + // sign the program and insert the signature into the witness + RequiredSignature::Witness(witness_name) => Ok(self.get_signed_program_witness( + &pst, + &program.program, + &program.witness.build_witness(), + &witness_name, + index, + )?), + // just build the passed witness + _ => Ok(program.witness.build_witness()), + }; + let pruned_witness = program + .program + .finalize(&pst, &signed_witness.unwrap(), index, &self.network) + .unwrap(); + + pst.inputs_mut()[index].final_script_witness = Some(pruned_witness); + } else { + // we need to sign the UTXO as is + // TODO: do we always sign? + let signed_witness = self.sign_input(&pst, index)?; + let raw_sig = elementssig_to_rawsig(&(signed_witness.1, EcdsaSighashType::All)); + + pst.inputs_mut()[index].final_script_witness = Some(vec![raw_sig, signed_witness.0.to_bytes()]); + } + } + + Ok(pst.extract_tx()?) + } + + fn get_signed_program_witness( + &self, + pst: &PartiallySignedTransaction, + program: &Box, + witness: &WitnessValues, + witness_name: &String, + index: usize, + ) -> Result { + let signature = self.sign_program(pst, program, index, &self.network)?; + + let mut hm = HashMap::new(); + + witness.iter().for_each(|el| { + hm.insert(el.0.clone(), el.1.clone()); + }); + + hm.insert( + WitnessName::from_str_unchecked(witness_name.as_str()), + Value::byte_array(signature.serialize()), + ); + + Ok(WitnessValues::from(hm)) + } + + fn derive_xpriv(&self, path: &DerivationPath) -> Result { + Ok(self.xprv.derive_priv(&self.secp, &path)?) + } + + fn master_xpriv(&self) -> Result { + Ok(self.derive_xpriv(&DerivationPath::master())?) + } + + fn derive_xpub(&self, path: &DerivationPath) -> Result { + let derived = self.derive_xpriv(path)?; + + Ok(Xpub::from_priv(&self.secp, &derived)) + } + + fn master_xpub(&self) -> Result { + Ok(self.derive_xpub(&DerivationPath::master())?) + } + + fn fingerprint(&self) -> Result { + Ok(self.master_xpub()?.fingerprint()) + } + + fn get_derivation_path(&self) -> Result { + let coin_type = if self.network.is_mainnet() { 1776 } else { 1 }; + let path = format!("84h/{coin_type}h/0h"); + + Ok(DerivationPath::from_str(&format!("m/{path}")).map_err(|e| SignerError::DerivationPath(e.to_string()))?) + } +} + +#[cfg(test)] +mod tests { + use crate::provider::EsploraProvider; + + use super::*; + + #[test] + fn keys_correspond_to_address() { + let url = "https://blockstream.info/liquidtestnet/api".to_string(); + let network = SimplicityNetwork::LiquidTestnet; + + let signer = Signer::new( + "exist carry drive collect lend cereal occur much tiger just involve mean", + Box::new(EsploraProvider::new(url, network.clone())), + ) + .unwrap(); + + let address = signer.get_wpkh_address().unwrap(); + let pubkey = signer.get_ecdsa_public_key().unwrap(); + + let derived_addr = Address::p2wpkh(&pubkey, None, network.address_params()); + + assert_eq!(derived_addr.to_string(), address.to_string()); + } +} diff --git a/crates/sdk/src/simplicityhl_core/blinder.rs b/crates/sdk/src/simplicityhl_core/blinder.rs deleted file mode 100644 index 96af5b3..0000000 --- a/crates/sdk/src/simplicityhl_core/blinder.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::simplicityhl_core::PUBLIC_SECRET_BLINDER_KEY; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::secp256k1_zkp::SecretKey; - -/// Derives a deterministic blinder keypair from the hardcoded public secret. -/// -/// # Panics -/// -/// Panics if the secret key bytes are invalid (should never happen with valid constant). -#[must_use] -pub fn derive_public_blinder_key() -> secp256k1::Keypair { - secp256k1::Keypair::from_secret_key( - secp256k1::SECP256K1, - &SecretKey::from_slice(&PUBLIC_SECRET_BLINDER_KEY).unwrap(), - ) -} diff --git a/crates/sdk/src/simplicityhl_core/constants.rs b/crates/sdk/src/simplicityhl_core/constants.rs deleted file mode 100644 index 541d6f7..0000000 --- a/crates/sdk/src/simplicityhl_core/constants.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! Common Liquid network constants and helpers. -//! -//! Exposes policy asset identifiers and the Liquid testnet genesis hash. -//! -//! These are used throughout the CLI and examples to ensure consistent -//! parameters when constructing Elements transactions. - -use simplicityhl::simplicity::elements; -use simplicityhl::simplicity::hashes::{Hash, sha256}; -use std::str::FromStr; - -pub const PUBLIC_SECRET_BLINDER_KEY: [u8; 32] = [1; 32]; - -/// `PLACEHOLDER_ISSUANCE_VALUE` constant is used for issuance and reissuance tokens during the blinding process. -/// -/// During blinding, the PSET collects surjection proof inputs from all outputs with matching asset IDs. -/// For issuance tokens, only the `asset_id` from `TxOutSectet` is copied into surjection proofs while the `value` is set to `0`, -/// as the exact value inserted for the issuance token is irrelevant to the proof computation. -/// This is because issuance and reissuance tokens surjection proofs only care about the `asset_id` or `token_id`, not the token value. -/// -/// See: `` -pub const PLACEHOLDER_ISSUANCE_VALUE: u64 = 0; - -/// Policy asset id (hex, BE) for Liquid mainnet. -pub const LIQUID_POLICY_ASSET_STR: &str = - "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"; - -/// Policy asset id (hex, BE) for Liquid testnet. -pub const LIQUID_TESTNET_POLICY_ASSET_STR: &str = - "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; - -/// Policy asset id (hex, BE) for Elements regtest. -pub const LIQUID_DEFAULT_REGTEST_ASSET_STR: &str = - "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"; - -/// Example test asset id (hex, BE) on Liquid testnet. -pub static LIQUID_TESTNET_TEST_ASSET_ID_STR: &str = - "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; - -/// LBTC asset id for Liquid testnet. -pub static LIQUID_TESTNET_BITCOIN_ASSET: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - elements::AssetId::from_inner(sha256::Midstate([ - 0x49, 0x9a, 0x81, 0x85, 0x45, 0xf6, 0xba, 0xe3, 0x9f, 0xc0, 0x3b, 0x63, 0x7f, 0x2a, - 0x4e, 0x1e, 0x64, 0xe5, 0x90, 0xca, 0xc1, 0xbc, 0x3a, 0x6f, 0x6d, 0x71, 0xaa, 0x44, - 0x43, 0x65, 0x4c, 0x14, - ])) - }); - -/// Genesis block hash for Liquid mainnet. -pub static LIQUID_MAINNET_GENESIS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - elements::BlockHash::from_byte_array([ - 0x03, 0x60, 0x20, 0x8a, 0x88, 0x96, 0x92, 0x37, 0x2c, 0x8d, 0x68, 0xb0, 0x84, 0xa6, - 0x2e, 0xfd, 0xf6, 0x0e, 0xa1, 0xa3, 0x59, 0xa0, 0x4c, 0x94, 0xb2, 0x0d, 0x22, 0x36, - 0x58, 0x27, 0x66, 0x14, - ]) - }); - -/// Genesis block hash for Liquid testnet. -pub static LIQUID_TESTNET_GENESIS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - elements::BlockHash::from_byte_array([ - 0xc1, 0xb1, 0x6a, 0xe2, 0x4f, 0x24, 0x23, 0xae, 0xa2, 0xea, 0x34, 0x55, 0x22, 0x92, - 0x79, 0x3b, 0x5b, 0x5e, 0x82, 0x99, 0x9a, 0x1e, 0xed, 0x81, 0xd5, 0x6a, 0xee, 0x52, - 0x8e, 0xda, 0x71, 0xa7, - ]) - }); - -/// Genesis block hash for Liquid regtest. -pub static LIQUID_REGTEST_GENESIS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - elements::BlockHash::from_byte_array([ - 0x21, 0xca, 0xb1, 0xe5, 0xda, 0x47, 0x18, 0xea, 0x14, 0x0d, 0x97, 0x16, 0x93, 0x17, - 0x02, 0x42, 0x2f, 0x0e, 0x6a, 0xd9, 0x15, 0xc8, 0xd9, 0xb5, 0x83, 0xca, 0xc2, 0x70, - 0x6b, 0x2a, 0x90, 0x00, - ]) - }); - -/// The network of the elements blockchain. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum SimplicityNetwork { - // Liquid mainnet policy asset - Liquid, - // Liquid testnet policy asset - LiquidTestnet, - /// Liquid regtest with a custom policy asset. - ElementsRegtest { - /// The policy asset to use for this regtest network. - /// You can use the default one using [`SimplicityNetwork::default_regtest()`]. - policy_asset: elements::AssetId, - }, -} - -impl SimplicityNetwork { - /// Return the default policy asset for regtest network. - /// - /// # Panics - /// - /// Doesn't panic as constants are defined correctly. - #[must_use] - pub fn default_regtest() -> Self { - let policy_asset = elements::AssetId::from_str(LIQUID_DEFAULT_REGTEST_ASSET_STR).unwrap(); - Self::ElementsRegtest { policy_asset } - } - - /// Return the policy asset for specific network. - /// - /// # Panics - /// - /// Doesn't panic as constants are defined correctly. - #[must_use] - pub fn policy_asset(&self) -> elements::AssetId { - match self { - Self::Liquid => elements::AssetId::from_str(LIQUID_POLICY_ASSET_STR).unwrap(), - Self::LiquidTestnet => { - elements::AssetId::from_str(LIQUID_TESTNET_POLICY_ASSET_STR).unwrap() - } - Self::ElementsRegtest { policy_asset } => *policy_asset, - } - } - - /// Return the genesis block hash for this network. - #[must_use] - pub fn genesis_block_hash(&self) -> elements::BlockHash { - match self { - Self::Liquid => *LIQUID_MAINNET_GENESIS, - Self::LiquidTestnet => *LIQUID_TESTNET_GENESIS, - Self::ElementsRegtest { .. } => *LIQUID_REGTEST_GENESIS, - } - } - - /// Return the address parameters for this network to generate addresses compatible for this network. - #[must_use] - pub const fn address_params(&self) -> &'static elements::AddressParams { - match self { - Self::Liquid => &elements::AddressParams::LIQUID, - Self::LiquidTestnet => &elements::AddressParams::LIQUID_TESTNET, - Self::ElementsRegtest { .. } => &elements::AddressParams::ELEMENTS, - } - } -} diff --git a/crates/sdk/src/simplicityhl_core/error.rs b/crates/sdk/src/simplicityhl_core/error.rs deleted file mode 100644 index a8b4179..0000000 --- a/crates/sdk/src/simplicityhl_core/error.rs +++ /dev/null @@ -1,58 +0,0 @@ -/// Errors that occur during binary or hex encoding/decoding operations. -/// -/// These errors are returned by the [`Encodable`](crate::Encodable) trait methods -/// when serializing or deserializing data. -#[cfg(feature = "encoding")] -#[derive(Debug, thiserror::Error)] -pub enum EncodingError { - #[error("Failed to encode to binary: {0}")] - BinaryEncode(#[from] bincode::error::EncodeError), - - #[error("Failed to decode from binary: {0}")] - BinaryDecode(#[from] bincode::error::DecodeError), - - /// Returned when a hex string cannot be parsed. - #[error("Failed to decode hex string: {0}")] - HexDecode(#[from] hex::FromHexError), -} - -/// Errors that occur during Simplicity program compilation, execution, or environment setup. -/// -/// These errors cover the full lifecycle of working with Simplicity programs: -/// loading source, satisfying witnesses, running on the Bit Machine, and -/// validating transaction environments. -#[derive(Debug, thiserror::Error)] -pub enum ProgramError { - #[error("Failed to compile Simplicity program: {0}")] - Compilation(String), - - /// Returned when witness values cannot satisfy the program's requirements. - #[error("Failed to satisfy witness: {0}")] - WitnessSatisfaction(String), - - /// Returned when the program cannot be pruned against the transaction environment. - #[error("Failed to prune program: {0}")] - Pruning(#[from] simplicityhl::simplicity::bit_machine::ExecutionError), - - #[error("Failed to construct a Bit Machine with enough space: {0}")] - BitMachineCreation(#[from] simplicityhl::simplicity::bit_machine::LimitError), - - #[error("Failed to execute program on the Bit Machine: {0}")] - Execution(simplicityhl::simplicity::bit_machine::ExecutionError), - - #[error("UTXO index {input_index} out of bounds (have {utxo_count} UTXOs)")] - UtxoIndexOutOfBounds { - input_index: usize, - utxo_count: usize, - }, - - /// Returned when the UTXO's script does not match the expected program address. - #[error("Script pubkey mismatch: expected hash {expected_hash}, got {actual_hash}")] - ScriptPubkeyMismatch { - expected_hash: String, - actual_hash: String, - }, - - #[error("Input index exceeds u32 maximum: {0}")] - InputIndexOverflow(#[from] std::num::TryFromIntError), -} diff --git a/crates/sdk/src/simplicityhl_core/fee_rate_fetcher.rs b/crates/sdk/src/simplicityhl_core/fee_rate_fetcher.rs deleted file mode 100644 index 663c84a..0000000 --- a/crates/sdk/src/simplicityhl_core/fee_rate_fetcher.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::collections::HashMap; - -/// Fee estimates response from Esplora. -/// Key: confirmation target (in blocks as string), Value: fee rate (sat/vB). -pub type FeeEstimates = HashMap; - -/// Default Target blocks value for using `DEFAULT_FEE_RATE` later -pub const DEFAULT_TARGET_BLOCKS: u32 = 0; - -/// Default fallback fee rate in sats/kvb (0.10 sat/vB). -/// Higher than LWK default to meet Liquid minimum relay fee requirements. -pub const DEFAULT_FEE_RATE: f32 = 100.0; - -/// Error type for Esplora sync operations. -#[derive(thiserror::Error, Debug)] -pub enum FeeFetcherError { - #[error("HTTP request failed: {0}")] - Request(String), - - #[error("Failed to deserialize response: {0}")] - Deserialize(String), - - #[error("Invalid txid format: {0}")] - InvalidTxid(String), -} - -pub trait SyncFeeFetcher { - /// Fetch fee estimates for various confirmation targets. - /// - /// # Errors - /// - /// Returns error if the HTTP request fails or response body cannot be parsed. - fn fetch_fee_estimates() -> Result; - - /// Get fee rate for a specific confirmation target. - /// - /// Fetches fee estimates from Esplora and returns the rate for the given target. - /// If the exact target is not available, falls back to higher targets. - /// - /// # Arguments - /// - /// * `target_blocks` - Desired confirmation target in blocks (1-25, 144, 504, 1008) - /// - /// # Returns - /// - /// Fee rate in sats/kvb (satoshis per 1000 virtual bytes). - /// Multiply Esplora's sat/vB value by 1000. - /// - /// # Errors - /// - /// Returns an error if the `fetch_fee_estimates()` fails or no suitable fee rate is found. - #[allow(clippy::cast_possible_truncation)] - fn get_fee_rate(target_blocks: u32) -> Result { - if target_blocks == 0 { - return Ok(DEFAULT_FEE_RATE); - } - - let estimates = Self::fetch_fee_estimates()?; - - let target_str = target_blocks.to_string(); - if let Some(&rate) = estimates.get(&target_str) { - return Ok((rate * 1000.0) as f32); // Convert sat/vB to sats/kvb - } - - // Fall back to higher targets (lower fee rates) - // Available targets: 1-25, 144, 504, 1008 - let fallback_targets = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, 144, 504, 1008, - ]; - - for &target in fallback_targets.iter().filter(|&&t| t >= target_blocks) { - let key = target.to_string(); - if let Some(&rate) = estimates.get(&key) { - return Ok((rate * 1000.0) as f32); - } - } - - // If no higher target found, try any available rate (use lowest target = highest rate) - for &target in &fallback_targets { - let key = target.to_string(); - if let Some(&rate) = estimates.get(&key) { - return Ok((rate * 1000.0) as f32); - } - } - - Err(FeeFetcherError::Request( - "No fee estimates available".to_string(), - )) - } -} - -pub struct EsploraFeeFetcher; - -impl SyncFeeFetcher for EsploraFeeFetcher { - /// Fetch fee estimates for various confirmation targets. - /// - /// Uses the `GET /fee-estimates` endpoint. - /// Note: Liquid testnet typically returns empty results, so callers should - /// use a fallback rate (see `config.fee.fallback_rate`). - /// - /// Returns a map where key is confirmation target (blocks) and value is fee rate (sat/vB). - /// - /// Example response: `{ "1": 87.882, "2": 87.882, ..., "144": 1.027, "1008": 1.027 }` - fn fetch_fee_estimates() -> Result { - const ESPLORA_URL: &str = "https://blockstream.info/liquidtestnet/api"; - - let url = format!("{ESPLORA_URL}/fee-estimates"); - let response = minreq::get(&url) - .send() - .map_err(|e| FeeFetcherError::Request(e.to_string()))?; - - if response.status_code != 200 { - return Err(FeeFetcherError::Request(format!( - "HTTP {}: {}", - response.status_code, response.reason_phrase - ))); - } - - let estimates: FeeEstimates = response - .json() - .map_err(|e| FeeFetcherError::Deserialize(e.to_string()))?; - - Ok(estimates) - } -} diff --git a/crates/sdk/src/simplicityhl_core/mod.rs b/crates/sdk/src/simplicityhl_core/mod.rs deleted file mode 100644 index 2c698ed..0000000 --- a/crates/sdk/src/simplicityhl_core/mod.rs +++ /dev/null @@ -1,334 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] - -//! High-level helpers for building and executing Simplicity programs on Liquid. - -mod blinder; -mod constants; -mod error; -mod fee_rate_fetcher; -mod runner; -mod scripts; - -#[cfg(feature = "encoding")] -pub mod encoding { - 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. - /// - /// # Errors - /// Returns error if encoding fails. - fn encode(&self) -> Result, EncodingError> - where - Self: Encode, - { - Ok(bincode::encode_to_vec(self, bincode::config::standard())?) - } - - /// Decode from binary bytes. - /// - /// # Errors - /// Returns error if decoding fails. - fn decode(buf: &[u8]) -> Result - where - Self: Sized + Decode<()>, - { - Ok(bincode::decode_from_slice(buf, bincode::config::standard())?.0) - } - - /// Encode to hex string. - /// - /// # Errors - /// Returns error if encoding fails. - fn to_hex(&self) -> Result - where - Self: Encode, - { - Ok(hex::encode(Encodable::encode(self)?)) - } - - /// Decode from hex string. - /// - /// # Errors - /// Returns error if hex decoding or binary decoding fails. - fn from_hex(hex: &str) -> Result - where - Self: bincode::Decode<()>, - { - Encodable::decode(&hex::decode(hex)?) - } - } -} - -pub use blinder::*; -pub use constants::*; -pub use error::ProgramError; - -#[cfg(feature = "encoding")] -pub use error::EncodingError; - -pub use runner::*; -pub use scripts::*; - -pub use fee_rate_fetcher::*; - -#[cfg(feature = "encoding")] -pub use encoding::Encodable; - -use simplicityhl::elements::secp256k1_zkp::schnorr::Signature; - -use std::collections::HashMap; -use std::sync::Arc; - -use simplicityhl::num::U256; -use simplicityhl::simplicity::RedeemNode; -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; -use simplicityhl::simplicity::elements::{Address, Transaction, TxInWitness, TxOut}; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; -use simplicityhl::str::WitnessName; -use simplicityhl::tracker::TrackerLogLevel; -use simplicityhl::value::ValueConstructible; -use simplicityhl::{CompiledProgram, Value, WitnessValues, elements}; - -/// Embedded Simplicity source for a basic P2PK program used to sign a single input. -pub const P2PK_SOURCE: &str = include_str!("source_simf/p2pk.simf"); - -/// Construct a P2TR address for the embedded P2PK program and the provided public key. -/// -/// # Errors -/// Returns error if the P2PK program fails to compile. -pub fn get_p2pk_address( - x_only_public_key: &XOnlyPublicKey, - network: SimplicityNetwork, -) -> Result { - Ok(create_p2tr_address( - get_p2pk_program(x_only_public_key)?.commit().cmr(), - x_only_public_key, - network.address_params(), - )) -} - -/// Compile the embedded P2PK program with the given X-only public key as argument. -/// -/// # Errors -/// Returns error if program compilation fails. -pub fn get_p2pk_program( - account_public_key: &XOnlyPublicKey, -) -> Result { - let arguments = simplicityhl::Arguments::from(HashMap::from([( - WitnessName::from_str_unchecked("PUBLIC_KEY"), - Value::u256(U256::from_byte_array(account_public_key.serialize())), - )])); - - load_program(P2PK_SOURCE, arguments) -} - -/// Execute the compiled P2PK program against the provided env, producing a pruned redeem node. -/// -/// The `schnorr_signature` should be created by signing the `sighash_all` from the environment: -/// ```ignore -/// let sighash_all = secp256k1::Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); -/// let schnorr_signature = keypair.sign_schnorr(sighash_all); -/// ``` -/// -/// # Errors -/// Returns error if program execution fails. -pub fn execute_p2pk_program( - compiled_program: &CompiledProgram, - schnorr_signature: &Signature, - env: &ElementsEnv>, - runner_log_level: TrackerLogLevel, -) -> Result>, ProgramError> { - let witness_values = WitnessValues::from(HashMap::from([( - WitnessName::from_str_unchecked("SIGNATURE"), - Value::byte_array(schnorr_signature.serialize()), - )])); - - Ok(run_program(compiled_program, witness_values, env, runner_log_level)?.0) -} - -/// Create a Schnorr signature for the P2PK program by signing the `sighash_all` of the transaction. -/// -/// This is a convenience function that builds the environment and signs the transaction hash. -/// -/// # Errors -/// Returns error if program compilation or environment verification fails. -pub fn create_p2pk_signature( - tx: &Transaction, - utxos: &[TxOut], - keypair: &elements::schnorr::Keypair, - input_index: usize, - network: SimplicityNetwork, -) -> Result { - use simplicityhl::simplicity::hashes::Hash as _; - - let x_only_public_key = keypair.x_only_public_key().0; - let p2pk_program = get_p2pk_program(&x_only_public_key)?; - - let env = get_and_verify_env( - tx, - &p2pk_program, - &x_only_public_key, - utxos, - network, - input_index, - )?; - - let sighash_all = - elements::secp256k1_zkp::Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); - Ok(keypair.sign_schnorr(sighash_all)) -} - -/// Finalize the given transaction by attaching a Simplicity witness for the specified P2PK input. -/// -/// The `schnorr_signature` should be created by signing the `sighash_all` from the environment. -/// Use [`create_p2pk_signature`] to create the signature if you have access to the secret key: -/// ```ignore -/// let signature = create_p2pk_signature(&tx, &utxos, &keypair, input_index, params, genesis_hash)?; -/// let tx = finalize_p2pk_transaction(tx, &utxos, &public_key, &signature, input_index, params, genesis_hash, TrackerLogLevel::None)?; -/// ``` -/// -/// Preconditions: -/// - `utxos[input_index]` must match the P2PK address derived from `x_only_public_key` and program CMR. -/// -/// # Errors -/// Returns error if program compilation, execution, or environment verification fails. -#[allow(clippy::too_many_arguments)] -pub fn finalize_p2pk_transaction( - mut tx: Transaction, - utxos: &[TxOut], - x_only_public_key: &XOnlyPublicKey, - schnorr_signature: &Signature, - input_index: usize, - network: SimplicityNetwork, - log_level: TrackerLogLevel, -) -> Result { - let p2pk_program = get_p2pk_program(x_only_public_key)?; - - let env = get_and_verify_env( - &tx, - &p2pk_program, - x_only_public_key, - utxos, - network, - input_index, - )?; - - let pruned = execute_p2pk_program(&p2pk_program, schnorr_signature, &env, log_level)?; - - let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); - let cmr = pruned.cmr(); - - tx.input[input_index].witness = TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, - script_witness: vec![ - simplicity_witness_bytes, - simplicity_program_bytes, - cmr.as_ref().to_vec(), - control_block(cmr, *x_only_public_key).serialize(), - ], - pegin_witness: vec![], - }; - - Ok(tx) -} - -/// Finalize transaction with a Simplicity witness for the specified input. -/// -/// # Errors -/// Returns error if environment verification or program execution fails. -#[allow(clippy::too_many_arguments)] -pub fn finalize_transaction( - mut tx: Transaction, - program: &CompiledProgram, - program_public_key: &XOnlyPublicKey, - utxos: &[TxOut], - input_index: usize, - witness_values: WitnessValues, - network: SimplicityNetwork, - log_level: TrackerLogLevel, -) -> Result { - let env = get_and_verify_env( - &tx, - program, - program_public_key, - utxos, - network, - input_index, - )?; - - let pruned = run_program(program, witness_values, &env, log_level)?.0; - - let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); - let cmr = pruned.cmr(); - - tx.input[input_index].witness = TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, - script_witness: vec![ - simplicity_witness_bytes, - simplicity_program_bytes, - cmr.as_ref().to_vec(), - control_block(cmr, *program_public_key).serialize(), - ], - pegin_witness: vec![], - }; - - Ok(tx) -} - -/// Build and verify an Elements environment for program execution. -/// -/// # Errors -/// Returns error if UTXO index is invalid or script pubkey doesn't match. -pub fn get_and_verify_env( - tx: &Transaction, - program: &CompiledProgram, - program_public_key: &XOnlyPublicKey, - utxos: &[TxOut], - network: SimplicityNetwork, - input_index: usize, -) -> Result>, ProgramError> { - let params = network.address_params(); - let genesis_hash = network.genesis_block_hash(); - let cmr = program.commit().cmr(); - - if utxos.len() <= input_index { - return Err(ProgramError::UtxoIndexOutOfBounds { - input_index, - utxo_count: utxos.len(), - }); - } - - let target_utxo = &utxos[input_index]; - let script_pubkey = create_p2tr_address(cmr, program_public_key, params).script_pubkey(); - - if target_utxo.script_pubkey != script_pubkey { - return Err(ProgramError::ScriptPubkeyMismatch { - expected_hash: script_pubkey.script_hash().to_string(), - actual_hash: target_utxo.script_pubkey.script_hash().to_string(), - }); - } - - Ok(ElementsEnv::new( - Arc::new(tx.clone()), - utxos - .iter() - .map(|utxo| ElementsUtxo { - script_pubkey: utxo.script_pubkey.clone(), - asset: utxo.asset, - value: utxo.value, - }) - .collect(), - u32::try_from(input_index)?, - cmr, - control_block(cmr, *program_public_key), - None, - genesis_hash, - )) -} diff --git a/crates/sdk/src/simplicityhl_core/runner.rs b/crates/sdk/src/simplicityhl_core/runner.rs deleted file mode 100644 index 7ee797e..0000000 --- a/crates/sdk/src/simplicityhl_core/runner.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Program execution helpers and logging levels for Simplicity programs. -//! -//! Provides `run_program` which satisfies and executes a compiled program -//! against an `ElementsEnv`, with optional debug and jet-trace logging. - -use std::sync::Arc; - -use simplicityhl::simplicity::elements::Transaction; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::ElementsEnv; -use simplicityhl::simplicity::{BitMachine, RedeemNode, Value}; -use simplicityhl::tracker::{DefaultTracker, TrackerLogLevel}; -use simplicityhl::{CompiledProgram, WitnessValues}; - -use crate::simplicityhl_core::error::ProgramError; - -/// Satisfy and execute a compiled program in the provided environment. -/// Returns the pruned program and the resulting value. -/// -/// # Errors -/// Returns error if witness satisfaction or program execution fails. -pub fn run_program( - program: &CompiledProgram, - witness_values: WitnessValues, - env: &ElementsEnv>, - log_level: TrackerLogLevel, -) -> Result<(Arc>, Value), ProgramError> { - let satisfied = program - .satisfy(witness_values) - .map_err(ProgramError::WitnessSatisfaction)?; - - let mut tracker = DefaultTracker::new(satisfied.debug_symbols()).with_log_level(log_level); - - let pruned = satisfied.redeem().prune_with_tracker(env, &mut tracker)?; - let mut mac = BitMachine::for_program(&pruned)?; - - let result = mac.exec(&pruned, env).map_err(ProgramError::Execution)?; - - Ok((pruned, result)) -} diff --git a/crates/sdk/src/simplicityhl_core/scripts.rs b/crates/sdk/src/simplicityhl_core/scripts.rs deleted file mode 100644 index 8ecc896..0000000 --- a/crates/sdk/src/simplicityhl_core/scripts.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! Script and Taproot utilities plus minor helpers around Elements types. - -use sha2::{Digest, Sha256}; - -use simplicityhl::elements::{ - Address, AddressParams, AssetId, ContractHash, OutPoint, Script, script, taproot, -}; - -use simplicityhl::simplicity::bitcoin::{XOnlyPublicKey, secp256k1}; -use simplicityhl::simplicity::hashes::{Hash, sha256}; -use simplicityhl::{Arguments, CompiledProgram}; - -use crate::simplicityhl_core::error::ProgramError; - -/// Load program source and compile it to a Simplicity program. -/// -/// # Errors -/// Returns error if the program fails to compile. -pub fn load_program(source: &str, arguments: Arguments) -> Result { - let compiled = - CompiledProgram::new(source, arguments, true).map_err(ProgramError::Compilation)?; - Ok(compiled) -} - -/// Generate a non-confidential P2TR address for the given program CMR and key. -#[must_use] -pub fn create_p2tr_address( - cmr: simplicityhl::simplicity::Cmr, - x_only_public_key: &XOnlyPublicKey, - params: &'static AddressParams, -) -> Address { - let spend_info = taproot_spending_info(cmr, *x_only_public_key); - - Address::p2tr( - secp256k1::SECP256K1, - spend_info.internal_key(), - spend_info.merkle_root(), - None, - params, - ) -} - -fn script_version(cmr: simplicityhl::simplicity::Cmr) -> (Script, taproot::LeafVersion) { - let script = script::Script::from(cmr.as_ref().to_vec()); - (script, simplicityhl::simplicity::leaf_version()) -} - -fn taproot_spending_info( - cmr: simplicityhl::simplicity::Cmr, - internal_key: XOnlyPublicKey, -) -> taproot::TaprootSpendInfo { - let builder = taproot::TaprootBuilder::new(); - let (script, version) = script_version(cmr); - let builder = builder - .add_leaf_with_ver(0, script, version) - .expect("tap tree should be valid"); - builder - .finalize(secp256k1::SECP256K1, internal_key) - .expect("tap tree should be valid") -} - -/// Compute the Taproot control block for script-path spending. -/// -/// # Panics -/// Panics if the taproot tree is invalid (should never happen with valid CMR). -#[must_use] -pub fn control_block( - cmr: simplicityhl::simplicity::Cmr, - internal_key: XOnlyPublicKey, -) -> taproot::ControlBlock { - let info = taproot_spending_info(cmr, internal_key); - let script_ver = script_version(cmr); - info.control_block(&script_ver) - .expect("control block should exist") -} - -/// SHA256 hash of an address's scriptPubKey bytes. -#[must_use] -pub fn hash_script(script: &Script) -> [u8; 32] { - let mut hasher = Sha256::new(); - sha2::digest::Update::update(&mut hasher, script.as_bytes()); - hasher.finalize().into() -} - -/// Compute issuance entropy for a new asset given an outpoint and contract hash entropy. -#[must_use] -pub fn get_new_asset_entropy(outpoint: &OutPoint, entropy: [u8; 32]) -> sha256::Midstate { - let contract_hash = ContractHash::from_byte_array(entropy); - AssetId::generate_asset_entropy(*outpoint, contract_hash) -} diff --git a/crates/sdk/src/transaction/error.rs b/crates/sdk/src/transaction/error.rs new file mode 100644 index 0000000..1591dfc --- /dev/null +++ b/crates/sdk/src/transaction/error.rs @@ -0,0 +1,5 @@ +#[derive(Debug, thiserror::Error)] +pub enum TransactionError { + #[error("Invalid signature type requested: {0}")] + SignatureRequest(String), +} diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs new file mode 100644 index 0000000..968b94e --- /dev/null +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -0,0 +1,204 @@ +use simplicityhl::elements::AssetId; +use simplicityhl::elements::pset::PartiallySignedTransaction; + +use crate::provider::SimplicityNetwork; +use crate::utils::asset_entropy; + +use super::error::TransactionError; +use super::partial_input::{IssuanceInput, PartialInput, ProgramInput, RequiredSignature}; +use super::partial_output::PartialOutput; + +pub const WITNESS_SCALE_FACTOR: usize = 4; + +#[derive(Clone)] +pub struct FinalInput { + pub partial_input: PartialInput, + pub program_input: Option, + pub issuance_input: Option, + pub required_sig: RequiredSignature, +} + +#[derive(Clone)] +pub struct FinalTransaction { + pub network: SimplicityNetwork, + inputs: Vec, + outputs: Vec, +} + +impl FinalTransaction { + pub fn new(network: SimplicityNetwork) -> Self { + Self { + network: network, + inputs: Vec::new(), + outputs: Vec::new(), + } + } + + pub fn add_input( + &mut self, + partial_input: PartialInput, + required_sig: RequiredSignature, + ) -> Result<(), TransactionError> { + match required_sig { + RequiredSignature::Witness(_) => { + return Err(TransactionError::SignatureRequest( + "Requested signature is not NativeEcdsa or None".to_string(), + )); + } + _ => {} + } + + self.inputs.push(FinalInput { + partial_input: partial_input, + program_input: None, + issuance_input: None, + required_sig: required_sig, + }); + + Ok(()) + } + + pub fn add_issuance_input( + &mut self, + partial_input: PartialInput, + issuance_input: IssuanceInput, + required_sig: RequiredSignature, + ) -> Result { + match required_sig { + RequiredSignature::Witness(_) => { + return Err(TransactionError::SignatureRequest( + "Requested signature is not NativeEcdsa or None".to_string(), + )); + } + _ => {} + } + + let asset_id = AssetId::from_entropy(asset_entropy(&partial_input.outpoint(), issuance_input.asset_entropy)); + + self.inputs.push(FinalInput { + partial_input: partial_input, + program_input: None, + issuance_input: Some(issuance_input), + required_sig: required_sig, + }); + + Ok(asset_id) + } + + pub fn add_program_input( + &mut self, + partial_input: PartialInput, + program_input: ProgramInput, + required_sig: RequiredSignature, + ) -> Result<(), TransactionError> { + match required_sig { + RequiredSignature::NativeEcdsa => { + return Err(TransactionError::SignatureRequest( + "Requested signature is not Witness or None".to_string(), + )); + } + _ => {} + } + + self.inputs.push(FinalInput { + partial_input: partial_input, + program_input: Some(program_input), + issuance_input: None, + required_sig: required_sig, + }); + + Ok(()) + } + + pub fn remove_input(&mut self, index: usize) -> Option { + if self.inputs.get(index).is_some() { + return Some(self.inputs.remove(index)); + } + + None + } + + pub fn add_output(&mut self, partial_output: PartialOutput) { + self.outputs.push(partial_output); + } + + pub fn remove_output(&mut self, index: usize) -> Option { + if self.outputs.get(index).is_some() { + return Some(self.outputs.remove(index)); + } + + None + } + + pub fn inputs(&self) -> &[FinalInput] { + &self.inputs + } + + pub fn inputs_mut(&mut self) -> &mut [FinalInput] { + &mut self.inputs + } + + pub fn outputs(&self) -> &[PartialOutput] { + &self.outputs + } + + pub fn outputs_mut(&mut self) -> &mut [PartialOutput] { + &mut self.outputs + } + + pub fn n_inputs(&self) -> usize { + self.inputs.len() + } + + pub fn n_outputs(&self) -> usize { + self.outputs.len() + } + + pub fn calculate_fee_delta(&self) -> i64 { + let available_amount = self + .inputs + .iter() + .filter(|input| input.partial_input.asset.clone().unwrap() == self.network.policy_asset()) + .fold(0 as u64, |acc, input| acc + input.partial_input.amount.clone().unwrap()); + + let consumed_amount = self + .outputs + .iter() + .filter(|output| output.asset == self.network.policy_asset()) + .fold(0 as u64, |acc, output| acc + output.amount); + + available_amount as i64 - consumed_amount as i64 + } + + pub fn calculate_fee(&self, weight: usize, fee_rate: f32) -> u64 { + let vsize = weight.div_ceil(WITNESS_SCALE_FACTOR); + + (vsize as f32 * fee_rate / 1000.0).ceil() as u64 + } + + pub fn extract_pst(&self) -> PartiallySignedTransaction { + let mut pst = PartiallySignedTransaction::new_v2(); + + self.inputs.iter().for_each(|el| { + let mut input = el.partial_input.input(); + + // populate the input manually since `input.merge` is private + if el.issuance_input.is_some() { + let issue = el.issuance_input.clone().unwrap().input(); + + input.issuance_value_amount = issue.issuance_value_amount; + input.issuance_asset_entropy = issue.issuance_asset_entropy; + input.issuance_inflation_keys = issue.issuance_inflation_keys; + input.blinded_issuance = issue.blinded_issuance; + } + + pst.add_input(input); + }); + + self.outputs.iter().for_each(|el| { + pst.add_output(el.to_output()); + }); + + pst + } +} diff --git a/crates/sdk/src/transaction/mod.rs b/crates/sdk/src/transaction/mod.rs new file mode 100644 index 0000000..41c0ba3 --- /dev/null +++ b/crates/sdk/src/transaction/mod.rs @@ -0,0 +1,9 @@ +pub mod error; +pub mod final_transaction; +pub mod partial_input; +pub mod partial_output; + +pub use error::TransactionError; +pub use final_transaction::{FinalInput, FinalTransaction}; +pub use partial_input::{PartialInput, ProgramInput, RequiredSignature}; +pub use partial_output::PartialOutput; diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs new file mode 100644 index 0000000..f4e54a7 --- /dev/null +++ b/crates/sdk/src/transaction/partial_input.rs @@ -0,0 +1,106 @@ +use simplicityhl::elements::confidential::{Asset, Value}; +use simplicityhl::elements::pset::Input; +use simplicityhl::elements::{AssetId, OutPoint, Sequence, TxOut, Txid}; + +use crate::program::ProgramTrait; +use crate::program::WitnessTrait; + +#[derive(Clone)] +pub enum RequiredSignature { + None, + NativeEcdsa, + Witness(String), +} + +#[derive(Clone)] +pub struct PartialInput { + pub witness_txid: Txid, + pub witness_output_index: u32, + pub witness_utxo: TxOut, + pub sequence: Option, + pub amount: Option, + pub asset: Option, +} + +#[derive(Clone)] +pub struct ProgramInput { + pub program: Box, + pub witness: Box, +} + +#[derive(Clone)] +pub struct IssuanceInput { + pub issuance_amount: u64, + pub asset_entropy: [u8; 32], +} + +impl PartialInput { + pub fn new(outpoint: OutPoint, txout: TxOut) -> Self { + let amount = match txout.value { + Value::Explicit(value) => Some(value), + _ => None, + }; + let asset = match txout.asset { + Asset::Explicit(asset) => Some(asset), + _ => None, + }; + + Self { + witness_txid: outpoint.txid, + witness_output_index: outpoint.vout, + witness_utxo: txout, + sequence: Default::default(), + amount: amount, + asset: asset, + } + } + + pub fn outpoint(&self) -> OutPoint { + OutPoint { + txid: self.witness_txid.clone(), + vout: self.witness_output_index, + } + } + + pub fn input(&self) -> Input { + let mut input = Input::default(); + + input.previous_txid = self.witness_txid.clone(); + input.previous_output_index = self.witness_output_index; + input.witness_utxo = Some(self.witness_utxo.clone()); + input.sequence = self.sequence.clone(); + input.amount = self.amount.clone(); + input.asset = self.asset.clone(); + + input + } +} + +impl ProgramInput { + pub fn new(program: Box, witness: Box) -> Self { + Self { + program: program, + witness: witness, + } + } +} + +impl IssuanceInput { + pub fn new(issuance_amount: u64, asset_entropy: [u8; 32]) -> Self { + Self { + issuance_amount: issuance_amount, + asset_entropy: asset_entropy, + } + } + + pub fn input(&self) -> Input { + let mut input = Input::default(); + + input.issuance_value_amount = Some(self.issuance_amount); + input.issuance_asset_entropy = Some(self.asset_entropy); + input.issuance_inflation_keys = None; + input.blinded_issuance = Some(0x00); + + input + } +} diff --git a/crates/sdk/src/transaction/partial_output.rs b/crates/sdk/src/transaction/partial_output.rs new file mode 100644 index 0000000..27235bb --- /dev/null +++ b/crates/sdk/src/transaction/partial_output.rs @@ -0,0 +1,23 @@ +use simplicityhl::elements::pset::Output; +use simplicityhl::elements::{AssetId, Script}; + +#[derive(Clone)] +pub struct PartialOutput { + pub script_pubkey: Script, + pub amount: u64, + pub asset: AssetId, +} + +impl PartialOutput { + pub fn new(script: Script, amount: u64, asset: AssetId) -> Self { + Self { + script_pubkey: script, + amount: amount, + asset: asset, + } + } + + pub fn to_output(&self) -> Output { + Output::new_explicit(self.script_pubkey.clone(), self.amount, self.asset.clone(), None) + } +} diff --git a/crates/sdk/src/utils.rs b/crates/sdk/src/utils.rs new file mode 100644 index 0000000..0175f08 --- /dev/null +++ b/crates/sdk/src/utils.rs @@ -0,0 +1,23 @@ +use simplicityhl::simplicity::bitcoin; +use simplicityhl::simplicity::bitcoin::secp256k1; + +use simplicityhl::elements::{AssetId, ContractHash, OutPoint}; +use simplicityhl::simplicity::hashes::{Hash, sha256}; + +pub fn tr_unspendable_key() -> secp256k1::XOnlyPublicKey { + secp256k1::XOnlyPublicKey::from_slice(&[ + 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e, 0x07, 0x8a, + 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0, + ]) + .expect("key should be valid") +} + +pub fn asset_entropy(outpoint: &OutPoint, entropy: [u8; 32]) -> sha256::Midstate { + let contract_hash = ContractHash::from_byte_array(entropy); + AssetId::generate_asset_entropy(*outpoint, contract_hash) +} + +pub fn sat2btc(sat: u64) -> String { + let amount = bitcoin::Amount::from_sat(sat); + amount.to_string_in(bitcoin::amount::Denomination::Bitcoin) +} diff --git a/crates/simplex/Cargo.toml b/crates/simplex/Cargo.toml new file mode 100644 index 0000000..588e054 --- /dev/null +++ b/crates/simplex/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "simplex" +version = "0.1.0" +description = "A blazingly-fast, ux-first simplicity development framework" +license.workspace = true +edition.workspace = true +repository = "https://github.com/BlockstreamResearch/simplex" +readme = "../README.md" +documentation = "https://docs.rs/simplex" +keywords = ["liquid", "framework", "simplicity", "simplex"] +categories = ["cryptography::cryptocurrencies"] + +[lints] +workspace = true + +[dependencies] +simplex-macros = { workspace = true } +simplex-test = { workspace = true } +simplex-sdk = { workspace = true } + +bincode = { workspace = true } +serde = { workspace = true } +simplicityhl = { workspace = true, features = ["serde"] } + +either = { version = "1.15.0", features = ["serde"] } + +[dev-dependencies] +trybuild = { version = "1.0.115" } diff --git a/crates/simplex/src/lib.rs b/crates/simplex/src/lib.rs new file mode 100644 index 0000000..aaea9f2 --- /dev/null +++ b/crates/simplex/src/lib.rs @@ -0,0 +1,12 @@ +pub use bincode; +pub use either; +pub use serde; +pub use simplicityhl; + +pub use simplex_sdk; + +pub use simplex_test::config::TestConfig; +pub use simplex_test::context::TestContext; + +pub use simplex_macros; +pub use simplex_macros::{include_simf, test}; diff --git a/crates/simplex/tests/compile_test.rs b/crates/simplex/tests/compile_test.rs new file mode 100644 index 0000000..01ebee1 --- /dev/null +++ b/crates/simplex/tests/compile_test.rs @@ -0,0 +1,12 @@ +const SLOW_TEST_ENV: &str = "RUN_UI_TESTS"; + +#[test] +fn ui() { + if let Err(_) = std::env::var(SLOW_TEST_ENV) { + eprintln!("Set '{SLOW_TEST_ENV}' to true in order to run a test"); + return; + } + + let tests = trybuild::TestCases::new(); + tests.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..7ac76cc --- /dev/null +++ b/crates/simplex/tests/ui/array_tr_storage.rs @@ -0,0 +1,23 @@ +use simplex::include_simf; +use simplex::simplex_sdk::program::{ArgumentsTrait, WitnessTrait}; + +include_simf!("../../../../crates/simplex/tests/ui_simfs/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); + + Ok(()) +} 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..0a0868d --- /dev/null +++ b/crates/simplex/tests/ui/bytes32_tr_storage.rs @@ -0,0 +1,22 @@ +use simplex::include_simf; +use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; + +include_simf!("../../../../crates/simplex/tests/ui_simfs/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); + + Ok(()) +} 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..35a5179 --- /dev/null +++ b/crates/simplex/tests/ui/dual_currency_deposit.rs @@ -0,0 +1,48 @@ +use simplex::include_simf; +use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; + +include_simf!("../../../../crates/simplex/tests/ui_simfs/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); + + Ok(()) +} diff --git a/crates/simplex/tests/ui/option_offer.rs b/crates/simplex/tests/ui/option_offer.rs new file mode 100644 index 0000000..d22bd85 --- /dev/null +++ b/crates/simplex/tests/ui/option_offer.rs @@ -0,0 +1,28 @@ +use simplex::include_simf; +use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; + +include_simf!("../../../../crates/simplex/tests/ui_simfs/option_offer.simf"); + +fn main() -> Result<(), String> { + let original_witness = derived_option_offer::OptionOfferWitness { path: simplex::either::Left((0, false)) }; + + 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); + + Ok(()) +} diff --git a/crates/simplex/tests/ui/options.rs b/crates/simplex/tests/ui/options.rs new file mode 100644 index 0000000..97126e8 --- /dev/null +++ b/crates/simplex/tests/ui/options.rs @@ -0,0 +1,33 @@ +use simplex::include_simf; +use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; + +include_simf!("../../../../crates/simplex/tests/ui_simfs/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); + + Ok(()) +} diff --git a/crates/simplex/tests/ui/simple_storage.rs b/crates/simplex/tests/ui/simple_storage.rs new file mode 100644 index 0000000..55e13ab --- /dev/null +++ b/crates/simplex/tests/ui/simple_storage.rs @@ -0,0 +1,23 @@ +use simplex::include_simf; +use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; + +include_simf!("../../../../crates/simplex/tests/ui_simfs/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); + + Ok(()) +} diff --git a/crates/simplex/tests/ui_simfs/array_tr_storage.simf b/crates/simplex/tests/ui_simfs/array_tr_storage.simf new file mode 100644 index 0000000..4918cf3 --- /dev/null +++ b/crates/simplex/tests/ui_simfs/array_tr_storage.simf @@ -0,0 +1,81 @@ +/* + * Extends `bytes32_tr_storage` using `array_fold` for larger buffers. + * Optimized for small, fixed-size states where linear hashing is more efficient + * than Merkle Trees. By avoiding proof overhead like sibling hashes, we reduce + * witness size and simplify contract logic for small N. + * This approach is particularly advantageous when updating all slots within every transaction. + */ + +fn hash_array_tr_storage(elem: u256, ctx: Ctx8) -> Ctx8 { + jet::sha_256_ctx_8_add_32(ctx, elem) +} + +fn hash_array_tr_storage_with_update(elem: u256, triplet: (Ctx8, u16, u16)) -> (Ctx8, u16, u16) { + let (ctx, i, changed_index): (Ctx8, u16, u16) = triplet; + + match jet::eq_16(i, changed_index) { + true => { + let (_, val): (bool, u16) = jet::increment_16(i); + + // There may be arbitrary logic here + let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(elem); + let new_state4: u64 = 20; + + let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); + ( + jet::sha_256_ctx_8_add_32(ctx, new_state), + val, + changed_index, + ) + }, + false => { + let (_, val): (bool, u16) = jet::increment_16(i); + ( + jet::sha_256_ctx_8_add_32(ctx, elem), + val, + changed_index, + ) + } + } +} + +fn script_hash_for_input_script(state: [u256; 3], changed_index: Option) -> u256 { + let tap_leaf: u256 = jet::tapleaf_hash(); + let ctx: Ctx8 = jet::tapdata_init(); + + let (ctx, _, _): (Ctx8, u16, u16) = match changed_index { + Some(ind: u16) => { + array_fold::(state, (ctx, 0, ind)) + }, + None => { + (array_fold::(state, ctx), 0, 0) + } + }; + + let computed: u256 = jet::sha_256_ctx_8_finalize(ctx); + let tap_node: u256 = jet::build_tapbranch(tap_leaf, computed); + + let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; + let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); + + let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); + let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 + let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); + jet::sha_256_ctx_8_finalize(hash_ctx3) +} + +fn main() { + let state: [u256; 3] = witness::STATE; + + // Assert that the input is correct, i.e. "load". + assert!(jet::eq_256( + script_hash_for_input_script(state, None), + unwrap(jet::input_script_hash(jet::current_index())) + )); + + // Assert that the output is correct, i.e. "store". + assert!(jet::eq_256( + script_hash_for_input_script(state, Some(witness::CHANGED_INDEX)), + unwrap(jet::output_script_hash(jet::current_index())) + )); +} \ No newline at end of file diff --git a/crates/simplex/tests/ui_simfs/bytes32_tr_storage.simf b/crates/simplex/tests/ui_simfs/bytes32_tr_storage.simf new file mode 100644 index 0000000..0d11b5f --- /dev/null +++ b/crates/simplex/tests/ui_simfs/bytes32_tr_storage.simf @@ -0,0 +1,66 @@ +/* + * Computes the "State Commitment" — the expected Script PubKey (address) + * for a specific state value. + * + * HOW IT WORKS: + * In Simplicity/Liquid, state is not stored in a dedicated database. Instead, + * it is verified via a "Commitment Scheme" inside the Taproot tree of the UTXO. + * + * This function reconstructs the Taproot structure to validate that the provided + * witness data (state_data) was indeed cryptographically embedded into the + * transaction output that is currently being spent. + * + * LOGIC FLOW: + * 1. Takes state_data (passed via witness at runtime). + * 2. Hashes it as a non-executable TapData leaf. + * 3. Combines it with the current program's CMR (tapleaf_hash). + * 4. Derives the tweaked_key (Internal Key + Merkle Root). + * 5. Returns the final SHA256 script hash (SegWit v1). + * + * USAGE: + * - In main, we verify: CalculatedHash(witness::STATE) == input_script_hash. + * - This assertion proves that the UTXO is "locked" not just by the code, + * but specifically by THIS instance of the state data. + */ + +fn script_hash_for_input_script(state_data: u256) -> u256 { + // This is the bulk of our "compute state commitment" logic from above. + let tap_leaf: u256 = jet::tapleaf_hash(); + let state_ctx1: Ctx8 = jet::tapdata_init(); + let state_ctx2: Ctx8 = jet::sha_256_ctx_8_add_32(state_ctx1, state_data); + let state_leaf: u256 = jet::sha_256_ctx_8_finalize(state_ctx2); + let tap_node: u256 = jet::build_tapbranch(tap_leaf, state_leaf); + + // Compute a taptweak using this. + let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; + let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); + + // Turn the taptweak into a script hash + let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); + let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 + let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); + jet::sha_256_ctx_8_finalize(hash_ctx3) +} + +fn main() { + let state_data: u256 = witness::STATE; + let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(state_data); + + // Assert that the input is correct, i.e. "load". + assert!(jet::eq_256( + script_hash_for_input_script(state_data), + unwrap(jet::input_script_hash(jet::current_index())) + )); + + // Do a state update (and fail on 64-bit overflow even though we've got 192 other + // bits we could be using..) + let (carry, new_state4): (bool, u64) = jet::increment_64(state4); + assert!(jet::eq_1(::into(carry), 0)); + + let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); + // Assert that the output is correct, i.e. "store". + assert!(jet::eq_256( + script_hash_for_input_script(new_state), + unwrap(jet::output_script_hash(jet::current_index())) + )); +} \ No newline at end of file diff --git a/crates/simplex/tests/ui_simfs/dual_currency_deposit.simf b/crates/simplex/tests/ui_simfs/dual_currency_deposit.simf new file mode 100644 index 0000000..e1a460a --- /dev/null +++ b/crates/simplex/tests/ui_simfs/dual_currency_deposit.simf @@ -0,0 +1,592 @@ +/* + * DCD: Dual Currency Deposit – price-attested settlement and funding windows + * + * Flows implemented: + * - Maker funding: deposit settlement asset and collateral, issue grantor tokens + * - Taker funding: deposit collateral in window and receive filler tokens + * - Settlement: at SETTLEMENT_HEIGHT, oracle Schnorr signature over (height, price) + * selects LBTC vs ALT branch based on price <= STRIKE_PRICE + * - Early/post-expiry termination: taker returns filler; maker burns grantor tokens + * - Merge: consolidate 2/3/4 token UTXOs + * + * All amounts and asset/script invariants are enforced on-chain; time guards use + * fallback locktime and height checks. + * + * Batching discussion: https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 + */ + +// Verify Schnorr signature against SHA256 of (u32 || u64) +fn checksig_priceblock(pk: Pubkey, current_block_height: u32, price_at_current_block_height: u64, sig: Signature) { + let hasher: Ctx8 = jet::sha_256_ctx_8_init(); + let hasher: Ctx8 = jet::sha_256_ctx_8_add_4(hasher, current_block_height); + let hasher: Ctx8 = jet::sha_256_ctx_8_add_8(hasher, price_at_current_block_height); + let msg: u256 = jet::sha_256_ctx_8_finalize(hasher); + jet::bip_0340_verify((pk, msg), sig); +} + +// Signed <= using XOR with 0x8000.. bias: a<=b (signed) iff (a^bias) <= (b^bias) (unsigned) +fn signed_le_u64(a_bits: u64, b_bits: u64) -> bool { + let bias: u64 = 0x8000000000000000; + jet::le_64(jet::xor_64(a_bits, bias), jet::xor_64(b_bits, bias)) +} + +fn signed_lt_u64(a: u64, b: u64) -> bool { + let bias: u64 = 0x8000000000000000; + jet::lt_64(jet::xor_64(a, bias), jet::xor_64(b, bias)) +} + +/// Assert: a == b * expected_q, via divmod +fn divmod_eq(a: u64, b: u64, expected_q: u64) { + let (q, r): (u64, u64) = jet::div_mod_64(a, b); + assert!(jet::eq_64(q, expected_q)); + assert!(jet::eq_64(r, 0)); +} + +/// Assert: base_amount * basis_point_percentage == provided_amount * MAX_BASIS_POINTS +fn constraint_percentage(base_amount: u64, basis_point_percentage: u64, provided_amount: u64) { + let MAX_BASIS_POINTS: u64 = 10000; + + let arg1: u256 = <(u128, u128)>::into((0, jet::multiply_64(base_amount, basis_point_percentage))); + let arg2: u256 = <(u128, u128)>::into((0, jet::multiply_64(provided_amount, MAX_BASIS_POINTS))); + + assert!(jet::eq_256(arg1, arg2)); +} + +fn get_output_script_hash(index: u32) -> u256 { + unwrap(jet::output_script_hash(index)) +} + +fn get_input_script_hash(index: u32) -> u256 { + unwrap(jet::input_script_hash(index)) +} + +fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } +fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } + +fn ensure_one_bit_or(bit1: bool, bit2: bool) { + assert!( + jet::eq_1( + ::into(jet::or_1(::into(bit1), ::into(bit2))), + 1 + ) + ); +} + +fn increment_by(index: u32, amount: u32) -> u32 { + let (carry, result): (bool, u32) = jet::add_32(index, amount); + ensure_zero_bit(carry); + result +} + +fn ensure_input_and_output_script_hash_eq(index: u32) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); +} + +fn ensure_output_is_op_return(index: u32) { + match jet::output_null_datum(index, 0) { + Some(entry: Option>>) => (), + None => panic!(), + } +} + +fn ensure_input_asset_eq(index: u32, expected_bits: u256) { + let asset: Asset1 = unwrap(jet::input_asset(index)); + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + assert!(jet::eq_256(asset_bits, expected_bits)); +} + +fn ensure_output_asset_eq(index: u32, expected_bits: u256) { + let asset: Asset1 = unwrap(jet::output_asset(index)); + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + assert!(jet::eq_256(asset_bits, expected_bits)); +} + +fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { + let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); + assert!(jet::eq_256(asset, expected_bits)); + assert!(jet::eq_64(amount, expected_amount)); +} + +fn ensure_input_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected)); +} + +fn ensure_output_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); +} + +fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { + let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); + assert!(jet::eq_32(jet::current_index(), index)); + + match is_change_needed { + true => { + ensure_input_and_output_script_hash_eq(index); + + let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); + ensure_zero_bit(carry); + ensure_output_asset_with_amount_eq(index, asset_id, collateral_change); + }, + false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), + } +} + +fn merge_2_tokens() { + // 2 tokens to merge + 1 input as fee + assert!(jet::eq_32(jet::num_inputs(), 3)); + // 3 outputs: 1 merged token + 1 change + 1 fee + assert!(jet::eq_32(jet::num_outputs(), 3)); + assert!(jet::le_32(jet::current_index(), 1)); + + ensure_input_and_output_script_hash_eq(0); + let script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(script_hash, get_input_script_hash(1))); +} + +fn merge_3_tokens() { + // 3 tokens to merge + 1 input as fee + assert!(jet::eq_32(jet::num_inputs(), 4)); + // 3 outputs: 1 merged token + 1 change + 1 fee + assert!(jet::eq_32(jet::num_outputs(), 3)); + assert!(jet::le_32(jet::current_index(), 2)); + + ensure_input_and_output_script_hash_eq(0); + let script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(script_hash, get_input_script_hash(1))); + assert!(jet::eq_256(script_hash, get_input_script_hash(2))); +} + +fn merge_4_tokens() { + // 4 tokens to merge + 1 input as fee + assert!(jet::eq_32(jet::num_inputs(), 5)); + // 3 outputs: 1 merged token + 1 change + 1 fee + assert!(jet::eq_32(jet::num_outputs(), 3)); + assert!(jet::le_32(jet::current_index(), 3)); + + ensure_input_and_output_script_hash_eq(0); + let script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(script_hash, get_input_script_hash(1))); + assert!(jet::eq_256(script_hash, get_input_script_hash(2))); + assert!(jet::eq_256(script_hash, get_input_script_hash(3))); +} + +/* +* Maker funding path +* Params: +* 1. FILLER_PER_SETTLEMENT_COLLATERAL +* 2. FILLER_PER_SETTLEMENT_ASSET +* 3. FILLER_PER_PRINCIPAL_COLLATERAL +* 4. GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET +* 5. GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL +* 6. GRANTOR_PER_SETTLEMENT_COLLATERAL +* 7. GRANTOR_PER_SETTLEMENT_ASSET +*/ +fn maker_funding_path(principal_collateral_amount: u64, principal_asset_amount: u64, interest_collateral_amount: u64, interest_asset_amount: u64) { + assert!(jet::eq_32(jet::num_inputs(), 5)); + assert!(jet::eq_32(jet::num_outputs(), 11)); + + let current_time: u32 = ::into(jet::lock_time()); + assert!(jet::lt_32(current_time, param::TAKER_FUNDING_START_TIME)); + + ensure_input_and_output_script_hash_eq(0); + ensure_input_and_output_script_hash_eq(1); + ensure_input_and_output_script_hash_eq(2); + + assert!(jet::le_32(jet::current_index(), 2)); + + let script_hash: u256 = get_output_script_hash(0); + ensure_output_script_hash_eq(1, script_hash); + ensure_output_script_hash_eq(2, script_hash); + ensure_output_script_hash_eq(3, script_hash); + ensure_output_script_hash_eq(4, script_hash); + ensure_output_script_hash_eq(5, script_hash); + + let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(3); + let (settlement_asset_bits, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(4); + let filler_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); + let grantor_collateral_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); + let grantor_settlement_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(2)))); + assert!(jet::eq_64(filler_token_amount, grantor_collateral_token_amount)); + assert!(jet::eq_64(filler_token_amount, grantor_settlement_token_amount)); + + divmod_eq(principal_asset_amount, param::STRIKE_PRICE, principal_collateral_amount); + + assert!(jet::eq_64(collateral_amount, interest_collateral_amount)); + constraint_percentage(principal_collateral_amount, param::INCENTIVE_BASIS_POINTS, collateral_amount); + + let MAX_BASIS_POINTS: u64 = 10000; + let (carry, asset_incentive_percentage): (bool, u64) = jet::add_64(param::INCENTIVE_BASIS_POINTS, MAX_BASIS_POINTS); + ensure_zero_bit(carry); + + constraint_percentage(principal_asset_amount, asset_incentive_percentage, settlement_amount); + + let (carry, calculated_total_asset_amount): (bool, u64) = jet::add_64(principal_asset_amount, interest_asset_amount); + ensure_zero_bit(carry); + assert!(jet::eq_64(calculated_total_asset_amount, settlement_amount)); + + let (carry, calculated_total_collateral_amount): (bool, u64) = jet::add_64(principal_collateral_amount, interest_collateral_amount); + ensure_zero_bit(carry); + + // Filler token constraints + divmod_eq(calculated_total_collateral_amount, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_token_amount); + divmod_eq(calculated_total_asset_amount, param::FILLER_PER_SETTLEMENT_ASSET, filler_token_amount); + divmod_eq(principal_collateral_amount, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount); + + // Grantor token constraints + divmod_eq(calculated_total_asset_amount, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_token_amount); + divmod_eq(interest_collateral_amount, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_token_amount); + + divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_collateral_token_amount); + // divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_settlement_token_amount); // duplicated because of lines 203-204 + + divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_collateral_token_amount); + // divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_settlement_token_amount); // duplicated because of lines 203-204 + + assert!(jet::eq_256(param::COLLATERAL_ASSET_ID, collateral_asset_bits)); + assert!(jet::eq_256(param::SETTLEMENT_ASSET_ID, settlement_asset_bits)); + + ensure_output_asset_with_amount_eq(5, param::FILLER_TOKEN_ASSET, filler_token_amount); + ensure_output_asset_with_amount_eq(6, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_token_amount); + ensure_output_asset_with_amount_eq(7, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_token_amount); + + ensure_input_asset_eq(3, param::SETTLEMENT_ASSET_ID); + ensure_input_asset_eq(4, param::COLLATERAL_ASSET_ID); + + ensure_output_asset_eq(8, param::COLLATERAL_ASSET_ID); + ensure_output_asset_eq(9, param::SETTLEMENT_ASSET_ID); + ensure_output_asset_eq(10, param::COLLATERAL_ASSET_ID); +} + +fn taker_funding_path(collateral_amount_to_deposit: u64, filler_token_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + assert!(jet::le_32(param::TAKER_FUNDING_START_TIME, current_time)); + assert!(jet::lt_32(current_time, param::TAKER_FUNDING_END_TIME)); + assert!(jet::lt_32(current_time, param::CONTRACT_EXPIRY_TIME)); + + let filler_token_input_index: u32 = 0; + let collateral_input_index: u32 = 1; + + let (collateral_to_covenant_output_index, filler_to_user_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(filler_token_input_index); + + // Check and ensure filler token change + ensure_correct_change_at_index(0, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(collateral_amount_to_deposit, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_get); + + // Ensure collateral asset and script hash are correct + ensure_output_asset_with_amount_eq(collateral_to_covenant_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_deposit); + ensure_output_script_hash_eq(collateral_to_covenant_output_index, expected_current_script_hash); + + ensure_output_asset_with_amount_eq(filler_to_user_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get); +} + +fn taker_early_termination_path(filler_token_amount_to_return: u64, collateral_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); + + let collateral_input_index: u32 = 0; + let filler_token_input_index: u32 = 1; + + let (return_filler_output_index, return_collateral_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(collateral_amount_to_get, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_return); + + // Ensure filler token transferred to covenant + ensure_output_asset_with_amount_eq(return_filler_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_return); + ensure_output_script_hash_eq(return_filler_output_index, expected_current_script_hash); + + // Ensure collateral transferred to user + ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); +} + +fn maker_collateral_termination_path(grantor_collateral_amount_to_burn: u64, collateral_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); + + let collateral_input_index: u32 = 0; + let grantor_collateral_token_input_index: u32 = 1; + + let (burn_grantor_collateral_output_index, return_collateral_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(collateral_amount_to_get, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_amount_to_burn); + + // Burn grantor collateral token + ensure_output_is_op_return(burn_grantor_collateral_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_amount_to_burn); + + // Ensure collateral transferred to user + ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); +} + +fn maker_settlement_termination_path(grantor_settlement_amount_to_burn: u64, settlement_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); + + let settlement_asset_input_index: u32 = 0; + let grantor_settlement_token_input_index: u32 = 1; + + let (burn_grantor_settlement_output_index, return_settlement_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset amount is correct + divmod_eq(settlement_amount_to_get, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_amount_to_burn); + + // Burn grantor settlement token + ensure_output_is_op_return(burn_grantor_settlement_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_amount_to_burn); + + // Ensure settlement asset transferred to user + ensure_output_asset_with_amount_eq(return_settlement_output_index, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get); +} + +fn ensure_correct_return_at(user_output_index: u32, asset_id: u256, amount_to_get: u64, fee_basis_points: u64) { + match jet::eq_64(fee_basis_points, 0) { + true => ensure_output_asset_with_amount_eq(user_output_index, asset_id, amount_to_get), + false => { + let fee_output_index: u32 = increment_by(user_output_index, 1); + + let (user_asset_bits, user_amount): (u256, u64) = get_output_explicit_asset_amount(user_output_index); + assert!(jet::eq_256(user_asset_bits, asset_id)); + + let (fee_asset_bits, fee_amount): (u256, u64) = get_output_explicit_asset_amount(fee_output_index); + assert!(jet::eq_256(fee_asset_bits, asset_id)); + + let (carry, calculated_total_amount): (bool, u64) = jet::add_64(user_amount, fee_amount); + ensure_zero_bit(carry); + + constraint_percentage(calculated_total_amount, fee_basis_points, fee_amount); + + ensure_output_script_hash_eq(fee_output_index, param::FEE_SCRIPT_HASH); + }, + }; +} + +fn maker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, grantor_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { + jet::check_lock_height(param::SETTLEMENT_HEIGHT); + checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); + + match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { + true => { + // Maker gets ALT + let settlement_asset_input_index: u32 = 0; + + let (burn_grantor_settlement_output_index, burn_grantor_collateral_output_index, settlement_output_index): (u32, u32, u32) = match is_change_needed { + true => (1, 2, 3), + false => (0, 1, 2), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset amount is correct + divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_amount_to_burn); + + // Burn grantor settlement and collateral tokens + ensure_output_is_op_return(burn_grantor_settlement_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); + ensure_output_is_op_return(burn_grantor_collateral_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); + + // Ensure settlement asset transferred to user + ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + false => { + // Maker gets the LBTC + let collateral_input_index: u32 = 0; + + let (burn_grantor_collateral_output_index, burn_grantor_settlement_output_index, collateral_output_index): (u32, u32, u32) = match is_change_needed { + true => (1, 2, 3), + false => (0, 1, 2), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_amount_to_burn); + + // Burn grantor collateral and settlement tokens + ensure_output_is_op_return(burn_grantor_collateral_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); + ensure_output_is_op_return(burn_grantor_settlement_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); + + // Ensure collateral transferred to user + ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + } +} + +fn taker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, filler_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { + jet::check_lock_height(param::SETTLEMENT_HEIGHT); + checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); + + match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { + true => { + // Taker receives LBTC principal+interest + let collateral_input_index: u32 = 0; + + let (burn_filler_output_index, collateral_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_amount_to_burn); + + // Burn filler token + ensure_output_is_op_return(burn_filler_output_index); + ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); + + // Ensure collateral transferred to user + ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + false => { + // Taker receives ALT + let settlement_asset_input_index: u32 = 0; + + let (burn_filler_output_index, settlement_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset amount is correct + divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_ASSET, filler_amount_to_burn); + + // Burn filler token + ensure_output_is_op_return(burn_filler_output_index); + ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); + + // Ensure filler token transferred to user + ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + } +} + +fn main() { + let token_branch: Either<(), ()> = witness::TOKEN_BRANCH; + let merge_branch: Either, ()> = witness::MERGE_BRANCH; + + match witness::PATH { + Left(funding_or_settlement: Either, (u64, Signature, u64, u64, bool)>) => match funding_or_settlement { + // Funding branches + Left(funding_params: Either<(u64, u64, u64, u64), (u64, u64, bool)>) => match funding_params { + // Maker funding: (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) + Left(params: (u64, u64, u64, u64)) => { + let (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount): (u64, u64, u64, u64) = params; + maker_funding_path(principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) + }, + // Taker funding: (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) + Right(params: (u64, u64, bool)) => { + let (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed): (u64, u64, bool) = params; + taker_funding_path(collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) + }, + }, + // Settlement branches (oracle price attested) + Right(params: (u64, Signature, u64, u64, bool)) => { + let (price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed): (u64, Signature, u64, u64, bool) = params; + + match token_branch { + // Maker settlement: burn grantor token + Left(u: ()) => maker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), + // Taker settlement: burn filler token + Right(u: ()) => taker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), + } + }, + }, + // Termination flows (early termination or post-expiry) or Merge flows + Right(termination_or_maker_or_merge: Either, ()>) => match termination_or_maker_or_merge { + Left(termination_or_maker: Either<(bool, u64, u64), (bool, u64, u64)>) => match termination_or_maker { + // Taker early termination: (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get) + Left(params: (bool, u64, u64)) => { + let (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get): (bool, u64, u64) = params; + taker_early_termination_path(filler_token_amount_to_return, collateral_amount_to_get, is_change_needed) + }, + // Maker termination (burn grantor token): choose collateral vs settlement token via token_branch + Right(params: (bool, u64, u64)) => { + let (is_change_needed, grantor_token_amount_to_burn, amount_to_get): (bool, u64, u64) = params; + + match token_branch { + // Burn grantor collateral token -> receive collateral + Left(u: ()) => maker_collateral_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), + // Burn grantor settlement token -> receive settlement asset + Right(u: ()) => maker_settlement_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), + } + }, + }, + Right(u: ()) => { + // Merge tokens based on MERGE_BRANCH discriminator + match merge_branch { + Left(left_or_right: Either<(), ()>) => match left_or_right { + Left(u: ()) => merge_2_tokens(), + Right(u: ()) => merge_3_tokens(), + }, + Right(u: ()) => merge_4_tokens(), + } + }, + }, + } + +} diff --git a/crates/simplex/tests/ui_simfs/option_offer.simf b/crates/simplex/tests/ui_simfs/option_offer.simf new file mode 100644 index 0000000..5cb2108 --- /dev/null +++ b/crates/simplex/tests/ui_simfs/option_offer.simf @@ -0,0 +1,213 @@ +/* + * Option Offer + * + * A covenant that allows a user to deposit collateral and premium assets, + * and have a counterparty swap settlement asset for both. + * The user can withdraw accumulated settlement asset at any time (with signature). + * After expiry, the user can reclaim any remaining collateral and premium (with signature). + * + * Paths: + * 1. Exercise: Counterparty swaps settlement asset for collateral + premium (no time restriction, optional change) + * 2. Withdraw: User withdraws settlement asset (no time restriction, signature required, full amount) + * 3. Expiry: User reclaims collateral + premium (after expiry, signature required, full amount) + * + * Constraints: + * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount + * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount + */ + +/// Assert: a == b * expected_q, via divmod +fn divmod_eq(a: u64, b: u64, expected_q: u64) { + let (q, r): (u64, u64) = jet::div_mod_64(a, b); + assert!(jet::eq_64(q, expected_q)); + assert!(jet::eq_64(r, 0)); +} + +fn get_input_script_hash(index: u32) -> u256 { + unwrap(jet::input_script_hash(index)) +} + +fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn ensure_zero_bit(bit: bool) { + assert!(jet::eq_1(::into(bit), 0)); +} + +fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { + let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); + assert!(jet::eq_256(asset, expected_bits)); + assert!(jet::eq_64(amount, expected_amount)); +} + +fn ensure_output_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); +} + +fn ensure_input_and_output_script_hash_eq(index: u32) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); +} + +fn check_user_signature(sig: Signature) { + let msg: u256 = jet::sig_all_hash(); + jet::bip_0340_verify((param::USER_PUBKEY, msg), sig); +} + +fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { + let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); + + match is_change_needed { + true => { + ensure_input_and_output_script_hash_eq(index); + + let (carry, asset_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); + ensure_zero_bit(carry); + ensure_output_asset_with_amount_eq(index, asset_id, asset_change); + }, + false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), + } +} + +/* + * Exercise Path + * + * Counterparty swaps settlement asset for collateral + premium. + * No time restriction - works before and after expiry. + * + * Constraints: + * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount + * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount + * + * Layout: + * + * Both: + * Input[0]: Collateral from covenant + * Input[1]: Premium from covenant + * + * With change (partial swap): + * Output[0]: Collateral change → covenant + * Output[1]: Premium change → covenant + * Output[2]: Settlement asset → covenant + * Output[3]: Collateral → counterparty + * Output[4]: Premium → counterparty + * + * Without change (full swap): + * Output[0]: Settlement asset → covenant + * Output[1]: Collateral → counterparty + * Output[2]: Premium → counterparty + */ +fn exercise_path(collateral_amount: u64, is_change_needed: bool) { + assert!(jet::le_32(jet::current_index(), 1)); + + let expected_covenant_script_hash: u256 = get_input_script_hash(0); + + assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); + + let premium_amount_u128: u128 = jet::multiply_64(collateral_amount, param::PREMIUM_PER_COLLATERAL); + let (left_part, premium_amount): (u64, u64) = dbg!(::into(premium_amount_u128)); + assert!(jet::eq_64(left_part, 0)); + + // Check collateral changes + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_covenant_script_hash, is_change_needed); + ensure_correct_change_at_index(1, param::PREMIUM_ASSET_ID, premium_amount, expected_covenant_script_hash, is_change_needed); + + let (settlement_output_index, collateral_output_index, premium_output_index): (u32, u32, u32) = match is_change_needed { + true => (2, 3, 4), + false => (0, 1, 2), + }; + + ensure_output_script_hash_eq(settlement_output_index, expected_covenant_script_hash); + + let (output_asset, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(settlement_output_index); + assert!(jet::eq_256(output_asset, param::SETTLEMENT_ASSET_ID)); + + divmod_eq(settlement_amount, param::COLLATERAL_PER_CONTRACT, collateral_amount); + + ensure_output_asset_with_amount_eq(collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(premium_output_index, param::PREMIUM_ASSET_ID, premium_amount); +} + +/* + * Withdraw Path + * + * User withdraws accumulated settlement asset. + * No time restriction. + * Requires signature from USER_PUBKEY. + * No change - full withdrawal only. + * + * Layout: + * Input[0]: Settlement asset from covenant + * Output[0]: Settlement asset → user (any address) + */ +fn withdraw_path(sig: Signature) { + assert!(jet::eq_32(jet::current_index(), 0)); + + let (input_asset, input_amount): (u256, u64) = get_input_explicit_asset_amount(0); + assert!(jet::eq_256(input_asset, param::SETTLEMENT_ASSET_ID)); + + check_user_signature(sig); + + ensure_output_asset_with_amount_eq(0, param::SETTLEMENT_ASSET_ID, input_amount); +} + +/* + * Expiry Path + * + * User reclaims remaining collateral and premium after expiry. + * Only allowed after EXPIRY_TIME. + * Requires signature from USER_PUBKEY. + * No change - full reclaim only. + * + * Layout: + * Input[0]: Collateral from covenant + * Input[1]: Premium from covenant + * Output[0]: Collateral → user (any address) + * Output[1]: Premium → user (any address) + */ +fn expiry_path(sig: Signature) { + jet::check_lock_time(param::EXPIRY_TIME); + + assert!(jet::le_32(jet::current_index(), 1)); + + let expected_covenant_script_hash: u256 = get_input_script_hash(0); + + assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); + + let (collateral_asset, collateral_amount): (u256, u64) = get_input_explicit_asset_amount(0); + assert!(jet::eq_256(collateral_asset, param::COLLATERAL_ASSET_ID)); + + let (premium_asset, premium_amount): (u256, u64) = get_input_explicit_asset_amount(1); + assert!(jet::eq_256(premium_asset, param::PREMIUM_ASSET_ID)); + + check_user_signature(sig); + + ensure_output_asset_with_amount_eq(0, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(1, param::PREMIUM_ASSET_ID, premium_amount); +} + +fn main() { + match witness::PATH { + Left(params: (u64, bool)) => { + let (collateral_amount, is_change_needed): (u64, bool) = params; + exercise_path(collateral_amount, is_change_needed) + }, + Right(withdraw_or_expiry: Either) => match withdraw_or_expiry { + Left(sig: Signature) => withdraw_path(sig), + Right(sig: Signature) => expiry_path(sig), + }, + } +} diff --git a/crates/simplex/tests/ui_simfs/options.simf b/crates/simplex/tests/ui_simfs/options.simf new file mode 100644 index 0000000..e7da014 --- /dev/null +++ b/crates/simplex/tests/ui_simfs/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_simfs/simple_storage.simf b/crates/simplex/tests/ui_simfs/simple_storage.simf new file mode 100644 index 0000000..7ae6c41 --- /dev/null +++ b/crates/simplex/tests/ui_simfs/simple_storage.simf @@ -0,0 +1,102 @@ +/* + * Simple Storage Program for Liquid + * + * Only the owner of the storage can modify the value. + * + * ==== IMPORTANT ==== + * + * Based on the following resources: + * https://github.com/ElementsProject/elements/blob/master/src/consensus/amount.h + * https://github.com/ElementsProject/rust-elements/blob/f6ffc7800df14b81c0f5ae1c94368a78b99612b9/src/blind.rs#L471 + * + * The maximum allowed amount is 2,100,000,000,000,000 + * (i.e., 21,000,000 × 10^8), which is approximately 51 bits. + */ + +fn checksig(pk: Pubkey, sig: Signature) { + let msg: u256 = jet::sig_all_hash(); + jet::bip_0340_verify((pk, msg), sig); +} + +fn ensure_current_index_eq(expected_index: u32){ + assert!(jet::eq_32(jet::current_index(), expected_index)); +} + +fn ensure_input_and_output_script_hash_eq(index: u32) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); +} + +fn ensure_output_is_op_return(index: u32) { + match jet::output_null_datum(index, 0) { + Some(entry: Option>>) => (), + None => panic!(), + } +} + +fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + + +fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { + let (asset, amount): (u256, u64) = dbg!(get_output_explicit_asset_amount(index)); + assert!(jet::eq_256(asset, expected_bits)); + assert!(jet::eq_64(amount, expected_amount)); +} + +fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } +fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } + +fn increment_by(index: u32, amount: u32) -> u32 { + let (carry, result): (bool, u32) = jet::add_32(index, amount); + ensure_zero_bit(carry); + result +} + +fn enforce_stage_checks(index: u32, new_value: u64) { + ensure_input_and_output_script_hash_eq(index); + + let (asset_bits, old_value): (u256, u64) = get_input_explicit_asset_amount(index); + assert!(jet::eq_256(asset_bits, param::SLOT_ID)); + + ensure_output_asset_with_amount_eq(index, param::SLOT_ID, new_value); + + match jet::lt_64(new_value, old_value) { + // burn + true => { + let burn_output_index: u32 = increment_by(index, 1); + + let (carry, amount_to_burn): (bool, u64) = jet::subtract_64(old_value, new_value); + ensure_zero_bit(carry); + + ensure_output_is_op_return(burn_output_index); + ensure_output_asset_with_amount_eq(burn_output_index, param::SLOT_ID, amount_to_burn); + }, + // mint + false => { + let reissuance_output_index: u32 = increment_by(index, 1); + ensure_input_and_output_script_hash_eq(reissuance_output_index); + }, + }; +} + +fn main() { + let index: u32 = 0; + enforce_stage_checks(index, witness::NEW_VALUE); + + checksig(param::USER, witness::USER_SIGNATURE) +} diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml new file mode 100644 index 0000000..a3795f1 --- /dev/null +++ b/crates/test/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "simplex-test" +version = "0.1.0" +description = "Simplex test command internal implementation" +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +simplex-sdk = { workspace = true } +simplex-regtest = { workspace = true } + +thiserror = { workspace = true } +simplicityhl = { workspace = true } +electrsd = { workspace = true } +serde = { workspace = true } +toml = { workspace = true } + +syn = { version = "2.0.114", default-features = false, features = ["proc-macro", "full", "parsing", "derive", "clone-impls", "extra-traits", "printing"] } +proc-macro2 = { version = "1.0.106", features = ["span-locations"] } +quote = { version = "1.0.44" } diff --git a/crates/test/src/config.rs b/crates/test/src/config.rs new file mode 100644 index 0000000..301e6ac --- /dev/null +++ b/crates/test/src/config.rs @@ -0,0 +1,67 @@ +use std::fs; +use std::fs::OpenOptions; +use std::io::Read; +use std::io::Write; +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use super::error::TestError; + +pub const TEST_ENV_NAME: &str = "SIMPLEX_TEST_ENV"; +pub const TEST_MNEMONIC: &str = "exist carry drive collect lend cereal occur much tiger just involve mean"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TestConfig { + pub mnemonic: String, + pub esplora: Option, + pub rpc: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct EsploraConfig { + pub url: String, + pub network: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct RpcConfig { + pub url: String, + pub username: String, + pub password: String, +} + +impl TestConfig { + pub fn to_file(&self, path: &impl AsRef) -> Result<(), TestError> { + if let Some(parent_dir) = path.as_ref().parent() { + fs::create_dir_all(parent_dir)?; + } + + let mut file = OpenOptions::new().create(true).write(true).open(&path)?; + + file.write(toml::to_string_pretty(&self).unwrap().as_bytes())?; + file.flush()?; + + Ok(()) + } + + pub fn from_file(path: impl AsRef) -> Result { + let mut content = String::new(); + let mut file = OpenOptions::new().read(true).open(path)?; + + file.read_to_string(&mut content)?; + + // TODO: check that network name is correct + Ok(toml::from_str(&content)?) + } +} + +impl Default for TestConfig { + fn default() -> Self { + Self { + mnemonic: TEST_MNEMONIC.to_string(), + esplora: None, + rpc: None, + } + } +} diff --git a/crates/test/src/context.rs b/crates/test/src/context.rs new file mode 100644 index 0000000..5ece65d --- /dev/null +++ b/crates/test/src/context.rs @@ -0,0 +1,142 @@ +use std::path::PathBuf; +use std::time::Duration; + +use electrsd::bitcoind::bitcoincore_rpc::Auth; + +use simplex_regtest::TestClient; +use simplex_sdk::provider::ElementsRpc; +use simplex_sdk::provider::{EsploraProvider, ProviderTrait, SimplexProvider, SimplicityNetwork}; +use simplex_sdk::signer::Signer; + +use crate::config::TestConfig; +use crate::error::TestError; + +#[allow(dead_code)] +pub struct TestContext { + _client: Option, + config: TestConfig, + signer: Signer, +} + +impl TestContext { + pub fn new(config_path: PathBuf) -> Result { + let config = TestConfig::from_file(&config_path)?; + + let (provider, client) = Self::setup_provider(&config)?; + let signer = Self::setup_signer(provider, &client, &config.mnemonic)?; + + Ok(Self { + _client: client, + config: config, + signer: signer, + }) + } + + pub fn get_provider(&self) -> &Box { + &self.signer.get_provider() + } + + pub fn get_config(&self) -> &TestConfig { + &self.config + } + + pub fn get_network(&self) -> &SimplicityNetwork { + &self.signer.get_provider().get_network() + } + + pub fn get_signer(&self) -> &Signer { + &self.signer + } + + fn setup_provider(config: &TestConfig) -> Result<(Box, Option), TestError> { + let provider: Box; + let client: Option; + + match config.esplora.clone() { + Some(esplora) => match config.rpc.clone() { + Some(rpc) => { + // custom regtest case + let auth = Auth::UserPass(rpc.username, rpc.password); + + provider = Box::new(SimplexProvider::new( + esplora.url, + rpc.url, + auth, + SimplicityNetwork::default_regtest(), + )?); + client = None; + } + None => { + // external esplora network + let network = match esplora.network.as_str() { + "Liquid" => SimplicityNetwork::Liquid, + "LiquidTestnet" => SimplicityNetwork::LiquidTestnet, + _ => panic!("Impossible branch reached, please report a bug"), + }; + + provider = Box::new(EsploraProvider::new(esplora.url, network)); + client = None; + } + }, + None => { + // simplex inner network + let client_inner = TestClient::new(); + + provider = Box::new(SimplexProvider::new( + client_inner.esplora_url(), + client_inner.rpc_url(), + client_inner.auth(), + SimplicityNetwork::default_regtest(), + )?); + + // need to save the client so that rust doesn't kill it + client = Some(client_inner); + } + } + + Ok((provider, client)) + } + + fn setup_signer( + provider: Box, + client: &Option, + mnemonic: &String, + ) -> Result { + let signer = Signer::new(mnemonic, provider)?; + + match client { + // if client exists, we are using inner simplex network + Some(client_inner) => { + let rpc_provider = ElementsRpc::new(client_inner.rpc_url(), client_inner.auth())?; + + rpc_provider.generate_blocks(1)?; + rpc_provider.rescanblockchain(None, None)?; + rpc_provider.sweep_initialfreecoins()?; + rpc_provider.generate_blocks(100)?; + + // 20 million BTC + rpc_provider.sendtoaddress(&signer.get_wpkh_address()?, 20_000_000 * u64::pow(10, 8), None)?; + + // wait for electrs to index + let mut attempts = 0; + + loop { + if !(signer.get_wpkh_utxos()?).is_empty() { + break; + } + + attempts += 1; + + if attempts > 100 { + panic!("Electrs failed to index the sweep after 10 seconds"); + } + + std::thread::sleep(Duration::from_millis(100)); + } + } + None => {} + }; + + Ok(signer) + } +} diff --git a/crates/test/src/error.rs b/crates/test/src/error.rs new file mode 100644 index 0000000..bcabf1e --- /dev/null +++ b/crates/test/src/error.rs @@ -0,0 +1,23 @@ +use std::io; + +use simplex_sdk::provider::ProviderError; +use simplex_sdk::provider::RpcError; +use simplex_sdk::signer::SignerError; + +#[derive(thiserror::Error, Debug)] +pub enum TestError { + #[error("Occurred io error: '{0}'")] + Io(#[from] io::Error), + + #[error(transparent)] + Provider(#[from] ProviderError), + + #[error(transparent)] + Rpc(#[from] RpcError), + + #[error(transparent)] + Signer(#[from] SignerError), + + #[error("Occurred config deserialization error: '{0}'")] + ConfigDeserialize(#[from] toml::de::Error), +} diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs new file mode 100644 index 0000000..21a3d3f --- /dev/null +++ b/crates/test/src/lib.rs @@ -0,0 +1,6 @@ +pub mod config; +pub mod context; +pub mod error; +pub mod macros; + +pub use config::{RpcConfig, TestConfig, TEST_ENV_NAME}; diff --git a/crates/test/src/macros/macros.rs b/crates/test/src/macros/macros.rs new file mode 100644 index 0000000..3b31fd8 --- /dev/null +++ b/crates/test/src/macros/macros.rs @@ -0,0 +1,50 @@ +use proc_macro2::TokenStream; +use syn::parse::Parser; + +use crate::TEST_ENV_NAME; + +type AttributeArgs = syn::punctuated::Punctuated; + +pub fn expand(args: TokenStream, input: syn::ItemFn) -> syn::Result { + let parser = AttributeArgs::parse_terminated; + let args = parser.parse2(args)?; + + expand_inner(&input, args) +} + +// TODO: args? +fn expand_inner(input: &syn::ItemFn, _args: AttributeArgs) -> syn::Result { + let ret = &input.sig.output; + let name = &input.sig.ident; + let inputs = &input.sig.inputs; + let body = &input.block; + let attrs = &input.attrs; + + let simplex_test_env = TEST_ENV_NAME; + + let expansion = quote::quote! { + #[::core::prelude::v1::test] + #(#attrs)* + fn #name() #ret { + use std::path::PathBuf; + use simplex::TestContext; + + fn #name(#inputs) #ret { + #body + } + + let test_context = match std::env::var(#simplex_test_env) { + Err(e) => { + panic!("Failed to run this test, required to use `simplex test`"); + }, + Ok(path) => { + TestContext::new(PathBuf::from(path)).unwrap() + } + }; + + #name(test_context) + } + }; + + Ok(expansion) +} diff --git a/crates/test/src/macros/mod.rs b/crates/test/src/macros/mod.rs new file mode 100644 index 0000000..eb7bb1d --- /dev/null +++ b/crates/test/src/macros/mod.rs @@ -0,0 +1,3 @@ +pub mod macros; + +pub use macros::expand; diff --git a/examples/basic/Cargo.lock b/examples/basic/Cargo.lock new file mode 100644 index 0000000..c82e810 --- /dev/null +++ b/examples/basic/Cargo.lock @@ -0,0 +1,1847 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand", + "rand_core", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" +dependencies = [ + "base58ck", + "base64 0.21.7", + "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin-units" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +dependencies = [ + "bitcoin-internals", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "serde", +] + +[[package]] +name = "bitcoincore-rpc" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedd23ae0fd321affb4bbbc36126c6f49a32818dc6b979395d24da8c9d4e80ee" +dependencies = [ + "bitcoincore-rpc-json", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoincore-rpc-json" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8909583c5fab98508e80ef73e5592a651c954993dc6b7739963257d19f0e71a" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoind" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce6620b7c942dbe28cc49c21d95e792feb9ffd95a093205e7875ccfa69c2925" +dependencies = [ + "anyhow", + "bitcoincore-rpc", + "log", + "tempfile", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chumsky" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acc17a6284abccac6e50db35c1cee87f605474a72939b959a3a67d9371800efd" +dependencies = [ + "hashbrown 0.15.5", + "regex-automata 0.3.9", + "serde", + "stacker", + "unicode-ident", + "unicode-segmentation", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "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 = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "draft_example" +version = "0.1.0" +dependencies = [ + "anyhow", + "simplex", + "simplicityhl", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "electrsd" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91435161fb2ad5098e7ac7a4b793bf9c34723b0208a3fcf6f33707489e771396" +dependencies = [ + "bitcoind", + "electrum-client", + "log", + "nix", + "which", +] + +[[package]] +name = "electrum-client" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0bd443023f9f5c4b7153053721939accc7113cbdf810a024434eed454b3db1" +dependencies = [ + "bitcoin", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "elements" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b2569d3495bfdfce36c504fd4d78752ff4a7699f8a33e6f3ee523bddf9f6ad" +dependencies = [ + "bech32", + "bitcoin", + "secp256k1-zkp", + "serde", + "serde_json", +] + +[[package]] +name = "elements-miniscript" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571fa105690f83c7833df2109eb2e14ca0e62d633d2624ffcb166ff18a3da870" +dependencies = [ + "bitcoin", + "elements", + "miniscript", + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[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 = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[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 = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.14", + "regex-syntax 0.8.10", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.11.0", + "ignore", + "walkdir", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.14", + "same-file", + "walkdir", + "winapi-util", +] + +[[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", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf" +dependencies = [ + "base64 0.13.1", + "minreq", + "serde", + "serde_json", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniscript" +version = "12.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" +dependencies = [ + "bech32", + "bitcoin", +] + +[[package]] +name = "minreq" +version = "2.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" +dependencies = [ + "rustls", + "rustls-webpki", + "serde", + "serde_json", + "webpki-roots", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[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 = "psm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.14", + "regex-syntax 0.8.10", +] + +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.10", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "santiago" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de36022292bc2086eb8f55bffa460fef3475e4459b478820711f4c421feb87ec" +dependencies = [ + "regex", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "rand", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-zkp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a44aed3002b5ae975f8624c5df3a949cfbf00479e18778b6058fcd213b76e3" +dependencies = [ + "bitcoin-private", + "rand", + "secp256k1", + "secp256k1-zkp-sys", + "serde", +] + +[[package]] +name = "secp256k1-zkp-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f08b2d0b143a22e07f798ae4f0ab20d5590d7c68e0d090f2088a48a21d1654" +dependencies = [ + "cc", + "secp256k1-sys", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simplex" +version = "0.1.0" +dependencies = [ + "bincode", + "either", + "serde", + "simplex-macros", + "simplex-sdk", + "simplex-test", + "simplicityhl", +] + +[[package]] +name = "simplex-build" +version = "0.1.0" +dependencies = [ + "glob", + "globwalk", + "pathdiff", + "prettyplease", + "proc-macro2", + "quote", + "serde", + "simplicityhl", + "syn", + "thiserror", + "toml", +] + +[[package]] +name = "simplex-macros" +version = "0.1.0" +dependencies = [ + "simplex-build", + "simplex-test", + "syn", +] + +[[package]] +name = "simplex-regtest" +version = "0.1.0" +dependencies = [ + "electrsd", + "simplex-sdk", + "thiserror", +] + +[[package]] +name = "simplex-sdk" +version = "0.1.0" +dependencies = [ + "bip39", + "bitcoin_hashes", + "dyn-clone", + "electrsd", + "elements-miniscript", + "hex", + "minreq", + "serde", + "serde_json", + "sha2", + "simplicityhl", + "thiserror", +] + +[[package]] +name = "simplex-test" +version = "0.1.0" +dependencies = [ + "electrsd", + "proc-macro2", + "quote", + "serde", + "simplex-regtest", + "simplex-sdk", + "simplicityhl", + "syn", + "thiserror", + "toml", +] + +[[package]] +name = "simplicity-lang" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e57bd4d84853974a212eab24ed89da54f49fbccf5e33e93bcd29f0a6591cd5" +dependencies = [ + "bitcoin", + "bitcoin_hashes", + "byteorder", + "elements", + "getrandom 0.2.17", + "ghost-cell", + "hex-conservative", + "miniscript", + "santiago", + "simplicity-sys", +] + +[[package]] +name = "simplicity-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bcb4e5bfc15080d67e0ce2c17d1c31bfb7521d65c86ea26ed0de72d5119d119" +dependencies = [ + "bitcoin_hashes", + "cc", +] + +[[package]] +name = "simplicityhl" +version = "0.4.1" +source = "git+https://github.com/BlockstreamResearch/SimplicityHL.git?rev=568b462#568b4621d6145cd97dce68a3f3428c7eb85306b6" +dependencies = [ + "base64 0.21.7", + "chumsky", + "clap", + "either", + "getrandom 0.2.17", + "itertools", + "miniscript", + "serde", + "serde_json", + "simplicity-lang", +] + +[[package]] +name = "stacker" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +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 = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[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 = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +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", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml new file mode 100644 index 0000000..a6c9551 --- /dev/null +++ b/examples/basic/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "draft_example" +edition = "2024" +rust-version = "1.90.0" +version = "0.1.0" + +[dependencies] +simplex = { path = "../../crates/simplex" } + +simplicityhl = { git = "https://github.com/BlockstreamResearch/SimplicityHL.git", rev = "568b462" } +anyhow = { version = "1.0.101" } diff --git a/examples/basic/Simplex.toml b/examples/basic/Simplex.toml new file mode 100644 index 0000000..b63c7a5 --- /dev/null +++ b/examples/basic/Simplex.toml @@ -0,0 +1,18 @@ +# TEST CONFIG + +# [build] +# simf_files = ["*.simf"] +# out_dir = "./src/artifacts" +# src_dir = "./simf" + +# [test] +# mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" + +# [test.esplora] +# url = "https://blockstream.info/liquidtestnet/api" +# network = "LiquidTestnet" + +# [test.rpc] +# url = "" +# username = "" +# password = "" diff --git a/examples/basic/simf/another_dir/another_module/bytes32_tr_storage.simf b/examples/basic/simf/another_dir/another_module/bytes32_tr_storage.simf new file mode 100644 index 0000000..0d11b5f --- /dev/null +++ b/examples/basic/simf/another_dir/another_module/bytes32_tr_storage.simf @@ -0,0 +1,66 @@ +/* + * Computes the "State Commitment" — the expected Script PubKey (address) + * for a specific state value. + * + * HOW IT WORKS: + * In Simplicity/Liquid, state is not stored in a dedicated database. Instead, + * it is verified via a "Commitment Scheme" inside the Taproot tree of the UTXO. + * + * This function reconstructs the Taproot structure to validate that the provided + * witness data (state_data) was indeed cryptographically embedded into the + * transaction output that is currently being spent. + * + * LOGIC FLOW: + * 1. Takes state_data (passed via witness at runtime). + * 2. Hashes it as a non-executable TapData leaf. + * 3. Combines it with the current program's CMR (tapleaf_hash). + * 4. Derives the tweaked_key (Internal Key + Merkle Root). + * 5. Returns the final SHA256 script hash (SegWit v1). + * + * USAGE: + * - In main, we verify: CalculatedHash(witness::STATE) == input_script_hash. + * - This assertion proves that the UTXO is "locked" not just by the code, + * but specifically by THIS instance of the state data. + */ + +fn script_hash_for_input_script(state_data: u256) -> u256 { + // This is the bulk of our "compute state commitment" logic from above. + let tap_leaf: u256 = jet::tapleaf_hash(); + let state_ctx1: Ctx8 = jet::tapdata_init(); + let state_ctx2: Ctx8 = jet::sha_256_ctx_8_add_32(state_ctx1, state_data); + let state_leaf: u256 = jet::sha_256_ctx_8_finalize(state_ctx2); + let tap_node: u256 = jet::build_tapbranch(tap_leaf, state_leaf); + + // Compute a taptweak using this. + let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; + let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); + + // Turn the taptweak into a script hash + let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); + let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 + let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); + jet::sha_256_ctx_8_finalize(hash_ctx3) +} + +fn main() { + let state_data: u256 = witness::STATE; + let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(state_data); + + // Assert that the input is correct, i.e. "load". + assert!(jet::eq_256( + script_hash_for_input_script(state_data), + unwrap(jet::input_script_hash(jet::current_index())) + )); + + // Do a state update (and fail on 64-bit overflow even though we've got 192 other + // bits we could be using..) + let (carry, new_state4): (bool, u64) = jet::increment_64(state4); + assert!(jet::eq_1(::into(carry), 0)); + + let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); + // Assert that the output is correct, i.e. "store". + assert!(jet::eq_256( + script_hash_for_input_script(new_state), + unwrap(jet::output_script_hash(jet::current_index())) + )); +} \ No newline at end of file diff --git a/examples/basic/simf/another_dir/another_module/dual_currency_deposit.simf b/examples/basic/simf/another_dir/another_module/dual_currency_deposit.simf new file mode 100644 index 0000000..e1a460a --- /dev/null +++ b/examples/basic/simf/another_dir/another_module/dual_currency_deposit.simf @@ -0,0 +1,592 @@ +/* + * DCD: Dual Currency Deposit – price-attested settlement and funding windows + * + * Flows implemented: + * - Maker funding: deposit settlement asset and collateral, issue grantor tokens + * - Taker funding: deposit collateral in window and receive filler tokens + * - Settlement: at SETTLEMENT_HEIGHT, oracle Schnorr signature over (height, price) + * selects LBTC vs ALT branch based on price <= STRIKE_PRICE + * - Early/post-expiry termination: taker returns filler; maker burns grantor tokens + * - Merge: consolidate 2/3/4 token UTXOs + * + * All amounts and asset/script invariants are enforced on-chain; time guards use + * fallback locktime and height checks. + * + * Batching discussion: https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 + */ + +// Verify Schnorr signature against SHA256 of (u32 || u64) +fn checksig_priceblock(pk: Pubkey, current_block_height: u32, price_at_current_block_height: u64, sig: Signature) { + let hasher: Ctx8 = jet::sha_256_ctx_8_init(); + let hasher: Ctx8 = jet::sha_256_ctx_8_add_4(hasher, current_block_height); + let hasher: Ctx8 = jet::sha_256_ctx_8_add_8(hasher, price_at_current_block_height); + let msg: u256 = jet::sha_256_ctx_8_finalize(hasher); + jet::bip_0340_verify((pk, msg), sig); +} + +// Signed <= using XOR with 0x8000.. bias: a<=b (signed) iff (a^bias) <= (b^bias) (unsigned) +fn signed_le_u64(a_bits: u64, b_bits: u64) -> bool { + let bias: u64 = 0x8000000000000000; + jet::le_64(jet::xor_64(a_bits, bias), jet::xor_64(b_bits, bias)) +} + +fn signed_lt_u64(a: u64, b: u64) -> bool { + let bias: u64 = 0x8000000000000000; + jet::lt_64(jet::xor_64(a, bias), jet::xor_64(b, bias)) +} + +/// Assert: a == b * expected_q, via divmod +fn divmod_eq(a: u64, b: u64, expected_q: u64) { + let (q, r): (u64, u64) = jet::div_mod_64(a, b); + assert!(jet::eq_64(q, expected_q)); + assert!(jet::eq_64(r, 0)); +} + +/// Assert: base_amount * basis_point_percentage == provided_amount * MAX_BASIS_POINTS +fn constraint_percentage(base_amount: u64, basis_point_percentage: u64, provided_amount: u64) { + let MAX_BASIS_POINTS: u64 = 10000; + + let arg1: u256 = <(u128, u128)>::into((0, jet::multiply_64(base_amount, basis_point_percentage))); + let arg2: u256 = <(u128, u128)>::into((0, jet::multiply_64(provided_amount, MAX_BASIS_POINTS))); + + assert!(jet::eq_256(arg1, arg2)); +} + +fn get_output_script_hash(index: u32) -> u256 { + unwrap(jet::output_script_hash(index)) +} + +fn get_input_script_hash(index: u32) -> u256 { + unwrap(jet::input_script_hash(index)) +} + +fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } +fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } + +fn ensure_one_bit_or(bit1: bool, bit2: bool) { + assert!( + jet::eq_1( + ::into(jet::or_1(::into(bit1), ::into(bit2))), + 1 + ) + ); +} + +fn increment_by(index: u32, amount: u32) -> u32 { + let (carry, result): (bool, u32) = jet::add_32(index, amount); + ensure_zero_bit(carry); + result +} + +fn ensure_input_and_output_script_hash_eq(index: u32) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); +} + +fn ensure_output_is_op_return(index: u32) { + match jet::output_null_datum(index, 0) { + Some(entry: Option>>) => (), + None => panic!(), + } +} + +fn ensure_input_asset_eq(index: u32, expected_bits: u256) { + let asset: Asset1 = unwrap(jet::input_asset(index)); + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + assert!(jet::eq_256(asset_bits, expected_bits)); +} + +fn ensure_output_asset_eq(index: u32, expected_bits: u256) { + let asset: Asset1 = unwrap(jet::output_asset(index)); + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + assert!(jet::eq_256(asset_bits, expected_bits)); +} + +fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { + let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); + assert!(jet::eq_256(asset, expected_bits)); + assert!(jet::eq_64(amount, expected_amount)); +} + +fn ensure_input_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected)); +} + +fn ensure_output_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); +} + +fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { + let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); + assert!(jet::eq_32(jet::current_index(), index)); + + match is_change_needed { + true => { + ensure_input_and_output_script_hash_eq(index); + + let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); + ensure_zero_bit(carry); + ensure_output_asset_with_amount_eq(index, asset_id, collateral_change); + }, + false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), + } +} + +fn merge_2_tokens() { + // 2 tokens to merge + 1 input as fee + assert!(jet::eq_32(jet::num_inputs(), 3)); + // 3 outputs: 1 merged token + 1 change + 1 fee + assert!(jet::eq_32(jet::num_outputs(), 3)); + assert!(jet::le_32(jet::current_index(), 1)); + + ensure_input_and_output_script_hash_eq(0); + let script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(script_hash, get_input_script_hash(1))); +} + +fn merge_3_tokens() { + // 3 tokens to merge + 1 input as fee + assert!(jet::eq_32(jet::num_inputs(), 4)); + // 3 outputs: 1 merged token + 1 change + 1 fee + assert!(jet::eq_32(jet::num_outputs(), 3)); + assert!(jet::le_32(jet::current_index(), 2)); + + ensure_input_and_output_script_hash_eq(0); + let script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(script_hash, get_input_script_hash(1))); + assert!(jet::eq_256(script_hash, get_input_script_hash(2))); +} + +fn merge_4_tokens() { + // 4 tokens to merge + 1 input as fee + assert!(jet::eq_32(jet::num_inputs(), 5)); + // 3 outputs: 1 merged token + 1 change + 1 fee + assert!(jet::eq_32(jet::num_outputs(), 3)); + assert!(jet::le_32(jet::current_index(), 3)); + + ensure_input_and_output_script_hash_eq(0); + let script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(script_hash, get_input_script_hash(1))); + assert!(jet::eq_256(script_hash, get_input_script_hash(2))); + assert!(jet::eq_256(script_hash, get_input_script_hash(3))); +} + +/* +* Maker funding path +* Params: +* 1. FILLER_PER_SETTLEMENT_COLLATERAL +* 2. FILLER_PER_SETTLEMENT_ASSET +* 3. FILLER_PER_PRINCIPAL_COLLATERAL +* 4. GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET +* 5. GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL +* 6. GRANTOR_PER_SETTLEMENT_COLLATERAL +* 7. GRANTOR_PER_SETTLEMENT_ASSET +*/ +fn maker_funding_path(principal_collateral_amount: u64, principal_asset_amount: u64, interest_collateral_amount: u64, interest_asset_amount: u64) { + assert!(jet::eq_32(jet::num_inputs(), 5)); + assert!(jet::eq_32(jet::num_outputs(), 11)); + + let current_time: u32 = ::into(jet::lock_time()); + assert!(jet::lt_32(current_time, param::TAKER_FUNDING_START_TIME)); + + ensure_input_and_output_script_hash_eq(0); + ensure_input_and_output_script_hash_eq(1); + ensure_input_and_output_script_hash_eq(2); + + assert!(jet::le_32(jet::current_index(), 2)); + + let script_hash: u256 = get_output_script_hash(0); + ensure_output_script_hash_eq(1, script_hash); + ensure_output_script_hash_eq(2, script_hash); + ensure_output_script_hash_eq(3, script_hash); + ensure_output_script_hash_eq(4, script_hash); + ensure_output_script_hash_eq(5, script_hash); + + let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(3); + let (settlement_asset_bits, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(4); + let filler_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); + let grantor_collateral_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); + let grantor_settlement_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(2)))); + assert!(jet::eq_64(filler_token_amount, grantor_collateral_token_amount)); + assert!(jet::eq_64(filler_token_amount, grantor_settlement_token_amount)); + + divmod_eq(principal_asset_amount, param::STRIKE_PRICE, principal_collateral_amount); + + assert!(jet::eq_64(collateral_amount, interest_collateral_amount)); + constraint_percentage(principal_collateral_amount, param::INCENTIVE_BASIS_POINTS, collateral_amount); + + let MAX_BASIS_POINTS: u64 = 10000; + let (carry, asset_incentive_percentage): (bool, u64) = jet::add_64(param::INCENTIVE_BASIS_POINTS, MAX_BASIS_POINTS); + ensure_zero_bit(carry); + + constraint_percentage(principal_asset_amount, asset_incentive_percentage, settlement_amount); + + let (carry, calculated_total_asset_amount): (bool, u64) = jet::add_64(principal_asset_amount, interest_asset_amount); + ensure_zero_bit(carry); + assert!(jet::eq_64(calculated_total_asset_amount, settlement_amount)); + + let (carry, calculated_total_collateral_amount): (bool, u64) = jet::add_64(principal_collateral_amount, interest_collateral_amount); + ensure_zero_bit(carry); + + // Filler token constraints + divmod_eq(calculated_total_collateral_amount, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_token_amount); + divmod_eq(calculated_total_asset_amount, param::FILLER_PER_SETTLEMENT_ASSET, filler_token_amount); + divmod_eq(principal_collateral_amount, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount); + + // Grantor token constraints + divmod_eq(calculated_total_asset_amount, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_token_amount); + divmod_eq(interest_collateral_amount, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_token_amount); + + divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_collateral_token_amount); + // divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_settlement_token_amount); // duplicated because of lines 203-204 + + divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_collateral_token_amount); + // divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_settlement_token_amount); // duplicated because of lines 203-204 + + assert!(jet::eq_256(param::COLLATERAL_ASSET_ID, collateral_asset_bits)); + assert!(jet::eq_256(param::SETTLEMENT_ASSET_ID, settlement_asset_bits)); + + ensure_output_asset_with_amount_eq(5, param::FILLER_TOKEN_ASSET, filler_token_amount); + ensure_output_asset_with_amount_eq(6, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_token_amount); + ensure_output_asset_with_amount_eq(7, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_token_amount); + + ensure_input_asset_eq(3, param::SETTLEMENT_ASSET_ID); + ensure_input_asset_eq(4, param::COLLATERAL_ASSET_ID); + + ensure_output_asset_eq(8, param::COLLATERAL_ASSET_ID); + ensure_output_asset_eq(9, param::SETTLEMENT_ASSET_ID); + ensure_output_asset_eq(10, param::COLLATERAL_ASSET_ID); +} + +fn taker_funding_path(collateral_amount_to_deposit: u64, filler_token_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + assert!(jet::le_32(param::TAKER_FUNDING_START_TIME, current_time)); + assert!(jet::lt_32(current_time, param::TAKER_FUNDING_END_TIME)); + assert!(jet::lt_32(current_time, param::CONTRACT_EXPIRY_TIME)); + + let filler_token_input_index: u32 = 0; + let collateral_input_index: u32 = 1; + + let (collateral_to_covenant_output_index, filler_to_user_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(filler_token_input_index); + + // Check and ensure filler token change + ensure_correct_change_at_index(0, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(collateral_amount_to_deposit, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_get); + + // Ensure collateral asset and script hash are correct + ensure_output_asset_with_amount_eq(collateral_to_covenant_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_deposit); + ensure_output_script_hash_eq(collateral_to_covenant_output_index, expected_current_script_hash); + + ensure_output_asset_with_amount_eq(filler_to_user_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get); +} + +fn taker_early_termination_path(filler_token_amount_to_return: u64, collateral_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); + + let collateral_input_index: u32 = 0; + let filler_token_input_index: u32 = 1; + + let (return_filler_output_index, return_collateral_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(collateral_amount_to_get, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_return); + + // Ensure filler token transferred to covenant + ensure_output_asset_with_amount_eq(return_filler_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_return); + ensure_output_script_hash_eq(return_filler_output_index, expected_current_script_hash); + + // Ensure collateral transferred to user + ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); +} + +fn maker_collateral_termination_path(grantor_collateral_amount_to_burn: u64, collateral_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); + + let collateral_input_index: u32 = 0; + let grantor_collateral_token_input_index: u32 = 1; + + let (burn_grantor_collateral_output_index, return_collateral_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(collateral_amount_to_get, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_amount_to_burn); + + // Burn grantor collateral token + ensure_output_is_op_return(burn_grantor_collateral_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_amount_to_burn); + + // Ensure collateral transferred to user + ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); +} + +fn maker_settlement_termination_path(grantor_settlement_amount_to_burn: u64, settlement_amount_to_get: u64, is_change_needed: bool) { + let current_time: u32 = ::into(jet::lock_time()); + ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); + + let settlement_asset_input_index: u32 = 0; + let grantor_settlement_token_input_index: u32 = 1; + + let (burn_grantor_settlement_output_index, return_settlement_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset amount is correct + divmod_eq(settlement_amount_to_get, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_amount_to_burn); + + // Burn grantor settlement token + ensure_output_is_op_return(burn_grantor_settlement_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_amount_to_burn); + + // Ensure settlement asset transferred to user + ensure_output_asset_with_amount_eq(return_settlement_output_index, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get); +} + +fn ensure_correct_return_at(user_output_index: u32, asset_id: u256, amount_to_get: u64, fee_basis_points: u64) { + match jet::eq_64(fee_basis_points, 0) { + true => ensure_output_asset_with_amount_eq(user_output_index, asset_id, amount_to_get), + false => { + let fee_output_index: u32 = increment_by(user_output_index, 1); + + let (user_asset_bits, user_amount): (u256, u64) = get_output_explicit_asset_amount(user_output_index); + assert!(jet::eq_256(user_asset_bits, asset_id)); + + let (fee_asset_bits, fee_amount): (u256, u64) = get_output_explicit_asset_amount(fee_output_index); + assert!(jet::eq_256(fee_asset_bits, asset_id)); + + let (carry, calculated_total_amount): (bool, u64) = jet::add_64(user_amount, fee_amount); + ensure_zero_bit(carry); + + constraint_percentage(calculated_total_amount, fee_basis_points, fee_amount); + + ensure_output_script_hash_eq(fee_output_index, param::FEE_SCRIPT_HASH); + }, + }; +} + +fn maker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, grantor_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { + jet::check_lock_height(param::SETTLEMENT_HEIGHT); + checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); + + match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { + true => { + // Maker gets ALT + let settlement_asset_input_index: u32 = 0; + + let (burn_grantor_settlement_output_index, burn_grantor_collateral_output_index, settlement_output_index): (u32, u32, u32) = match is_change_needed { + true => (1, 2, 3), + false => (0, 1, 2), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset amount is correct + divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_amount_to_burn); + + // Burn grantor settlement and collateral tokens + ensure_output_is_op_return(burn_grantor_settlement_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); + ensure_output_is_op_return(burn_grantor_collateral_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); + + // Ensure settlement asset transferred to user + ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + false => { + // Maker gets the LBTC + let collateral_input_index: u32 = 0; + + let (burn_grantor_collateral_output_index, burn_grantor_settlement_output_index, collateral_output_index): (u32, u32, u32) = match is_change_needed { + true => (1, 2, 3), + false => (0, 1, 2), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_amount_to_burn); + + // Burn grantor collateral and settlement tokens + ensure_output_is_op_return(burn_grantor_collateral_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); + ensure_output_is_op_return(burn_grantor_settlement_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); + + // Ensure collateral transferred to user + ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + } +} + +fn taker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, filler_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { + jet::check_lock_height(param::SETTLEMENT_HEIGHT); + checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); + + match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { + true => { + // Taker receives LBTC principal+interest + let collateral_input_index: u32 = 0; + + let (burn_filler_output_index, collateral_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_amount_to_burn); + + // Burn filler token + ensure_output_is_op_return(burn_filler_output_index); + ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); + + // Ensure collateral transferred to user + ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + false => { + // Taker receives ALT + let settlement_asset_input_index: u32 = 0; + + let (burn_filler_output_index, settlement_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset amount is correct + divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_ASSET, filler_amount_to_burn); + + // Burn filler token + ensure_output_is_op_return(burn_filler_output_index); + ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); + + // Ensure filler token transferred to user + ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); + }, + } +} + +fn main() { + let token_branch: Either<(), ()> = witness::TOKEN_BRANCH; + let merge_branch: Either, ()> = witness::MERGE_BRANCH; + + match witness::PATH { + Left(funding_or_settlement: Either, (u64, Signature, u64, u64, bool)>) => match funding_or_settlement { + // Funding branches + Left(funding_params: Either<(u64, u64, u64, u64), (u64, u64, bool)>) => match funding_params { + // Maker funding: (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) + Left(params: (u64, u64, u64, u64)) => { + let (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount): (u64, u64, u64, u64) = params; + maker_funding_path(principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) + }, + // Taker funding: (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) + Right(params: (u64, u64, bool)) => { + let (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed): (u64, u64, bool) = params; + taker_funding_path(collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) + }, + }, + // Settlement branches (oracle price attested) + Right(params: (u64, Signature, u64, u64, bool)) => { + let (price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed): (u64, Signature, u64, u64, bool) = params; + + match token_branch { + // Maker settlement: burn grantor token + Left(u: ()) => maker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), + // Taker settlement: burn filler token + Right(u: ()) => taker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), + } + }, + }, + // Termination flows (early termination or post-expiry) or Merge flows + Right(termination_or_maker_or_merge: Either, ()>) => match termination_or_maker_or_merge { + Left(termination_or_maker: Either<(bool, u64, u64), (bool, u64, u64)>) => match termination_or_maker { + // Taker early termination: (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get) + Left(params: (bool, u64, u64)) => { + let (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get): (bool, u64, u64) = params; + taker_early_termination_path(filler_token_amount_to_return, collateral_amount_to_get, is_change_needed) + }, + // Maker termination (burn grantor token): choose collateral vs settlement token via token_branch + Right(params: (bool, u64, u64)) => { + let (is_change_needed, grantor_token_amount_to_burn, amount_to_get): (bool, u64, u64) = params; + + match token_branch { + // Burn grantor collateral token -> receive collateral + Left(u: ()) => maker_collateral_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), + // Burn grantor settlement token -> receive settlement asset + Right(u: ()) => maker_settlement_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), + } + }, + }, + Right(u: ()) => { + // Merge tokens based on MERGE_BRANCH discriminator + match merge_branch { + Left(left_or_right: Either<(), ()>) => match left_or_right { + Left(u: ()) => merge_2_tokens(), + Right(u: ()) => merge_3_tokens(), + }, + Right(u: ()) => merge_4_tokens(), + } + }, + }, + } + +} diff --git a/examples/basic/simf/another_dir/array_tr_storage.simf b/examples/basic/simf/another_dir/array_tr_storage.simf new file mode 100644 index 0000000..4918cf3 --- /dev/null +++ b/examples/basic/simf/another_dir/array_tr_storage.simf @@ -0,0 +1,81 @@ +/* + * Extends `bytes32_tr_storage` using `array_fold` for larger buffers. + * Optimized for small, fixed-size states where linear hashing is more efficient + * than Merkle Trees. By avoiding proof overhead like sibling hashes, we reduce + * witness size and simplify contract logic for small N. + * This approach is particularly advantageous when updating all slots within every transaction. + */ + +fn hash_array_tr_storage(elem: u256, ctx: Ctx8) -> Ctx8 { + jet::sha_256_ctx_8_add_32(ctx, elem) +} + +fn hash_array_tr_storage_with_update(elem: u256, triplet: (Ctx8, u16, u16)) -> (Ctx8, u16, u16) { + let (ctx, i, changed_index): (Ctx8, u16, u16) = triplet; + + match jet::eq_16(i, changed_index) { + true => { + let (_, val): (bool, u16) = jet::increment_16(i); + + // There may be arbitrary logic here + let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(elem); + let new_state4: u64 = 20; + + let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); + ( + jet::sha_256_ctx_8_add_32(ctx, new_state), + val, + changed_index, + ) + }, + false => { + let (_, val): (bool, u16) = jet::increment_16(i); + ( + jet::sha_256_ctx_8_add_32(ctx, elem), + val, + changed_index, + ) + } + } +} + +fn script_hash_for_input_script(state: [u256; 3], changed_index: Option) -> u256 { + let tap_leaf: u256 = jet::tapleaf_hash(); + let ctx: Ctx8 = jet::tapdata_init(); + + let (ctx, _, _): (Ctx8, u16, u16) = match changed_index { + Some(ind: u16) => { + array_fold::(state, (ctx, 0, ind)) + }, + None => { + (array_fold::(state, ctx), 0, 0) + } + }; + + let computed: u256 = jet::sha_256_ctx_8_finalize(ctx); + let tap_node: u256 = jet::build_tapbranch(tap_leaf, computed); + + let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; + let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); + + let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); + let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 + let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); + jet::sha_256_ctx_8_finalize(hash_ctx3) +} + +fn main() { + let state: [u256; 3] = witness::STATE; + + // Assert that the input is correct, i.e. "load". + assert!(jet::eq_256( + script_hash_for_input_script(state, None), + unwrap(jet::input_script_hash(jet::current_index())) + )); + + // Assert that the output is correct, i.e. "store". + assert!(jet::eq_256( + script_hash_for_input_script(state, Some(witness::CHANGED_INDEX)), + unwrap(jet::output_script_hash(jet::current_index())) + )); +} \ No newline at end of file diff --git a/examples/basic/simf/module/option_offer.simf b/examples/basic/simf/module/option_offer.simf new file mode 100644 index 0000000..5cb2108 --- /dev/null +++ b/examples/basic/simf/module/option_offer.simf @@ -0,0 +1,213 @@ +/* + * Option Offer + * + * A covenant that allows a user to deposit collateral and premium assets, + * and have a counterparty swap settlement asset for both. + * The user can withdraw accumulated settlement asset at any time (with signature). + * After expiry, the user can reclaim any remaining collateral and premium (with signature). + * + * Paths: + * 1. Exercise: Counterparty swaps settlement asset for collateral + premium (no time restriction, optional change) + * 2. Withdraw: User withdraws settlement asset (no time restriction, signature required, full amount) + * 3. Expiry: User reclaims collateral + premium (after expiry, signature required, full amount) + * + * Constraints: + * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount + * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount + */ + +/// Assert: a == b * expected_q, via divmod +fn divmod_eq(a: u64, b: u64, expected_q: u64) { + let (q, r): (u64, u64) = jet::div_mod_64(a, b); + assert!(jet::eq_64(q, expected_q)); + assert!(jet::eq_64(r, 0)); +} + +fn get_input_script_hash(index: u32) -> u256 { + unwrap(jet::input_script_hash(index)) +} + +fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn ensure_zero_bit(bit: bool) { + assert!(jet::eq_1(::into(bit), 0)); +} + +fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { + let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); + assert!(jet::eq_256(asset, expected_bits)); + assert!(jet::eq_64(amount, expected_amount)); +} + +fn ensure_output_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); +} + +fn ensure_input_and_output_script_hash_eq(index: u32) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); +} + +fn check_user_signature(sig: Signature) { + let msg: u256 = jet::sig_all_hash(); + jet::bip_0340_verify((param::USER_PUBKEY, msg), sig); +} + +fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { + let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); + + match is_change_needed { + true => { + ensure_input_and_output_script_hash_eq(index); + + let (carry, asset_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); + ensure_zero_bit(carry); + ensure_output_asset_with_amount_eq(index, asset_id, asset_change); + }, + false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), + } +} + +/* + * Exercise Path + * + * Counterparty swaps settlement asset for collateral + premium. + * No time restriction - works before and after expiry. + * + * Constraints: + * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount + * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount + * + * Layout: + * + * Both: + * Input[0]: Collateral from covenant + * Input[1]: Premium from covenant + * + * With change (partial swap): + * Output[0]: Collateral change → covenant + * Output[1]: Premium change → covenant + * Output[2]: Settlement asset → covenant + * Output[3]: Collateral → counterparty + * Output[4]: Premium → counterparty + * + * Without change (full swap): + * Output[0]: Settlement asset → covenant + * Output[1]: Collateral → counterparty + * Output[2]: Premium → counterparty + */ +fn exercise_path(collateral_amount: u64, is_change_needed: bool) { + assert!(jet::le_32(jet::current_index(), 1)); + + let expected_covenant_script_hash: u256 = get_input_script_hash(0); + + assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); + + let premium_amount_u128: u128 = jet::multiply_64(collateral_amount, param::PREMIUM_PER_COLLATERAL); + let (left_part, premium_amount): (u64, u64) = dbg!(::into(premium_amount_u128)); + assert!(jet::eq_64(left_part, 0)); + + // Check collateral changes + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_covenant_script_hash, is_change_needed); + ensure_correct_change_at_index(1, param::PREMIUM_ASSET_ID, premium_amount, expected_covenant_script_hash, is_change_needed); + + let (settlement_output_index, collateral_output_index, premium_output_index): (u32, u32, u32) = match is_change_needed { + true => (2, 3, 4), + false => (0, 1, 2), + }; + + ensure_output_script_hash_eq(settlement_output_index, expected_covenant_script_hash); + + let (output_asset, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(settlement_output_index); + assert!(jet::eq_256(output_asset, param::SETTLEMENT_ASSET_ID)); + + divmod_eq(settlement_amount, param::COLLATERAL_PER_CONTRACT, collateral_amount); + + ensure_output_asset_with_amount_eq(collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(premium_output_index, param::PREMIUM_ASSET_ID, premium_amount); +} + +/* + * Withdraw Path + * + * User withdraws accumulated settlement asset. + * No time restriction. + * Requires signature from USER_PUBKEY. + * No change - full withdrawal only. + * + * Layout: + * Input[0]: Settlement asset from covenant + * Output[0]: Settlement asset → user (any address) + */ +fn withdraw_path(sig: Signature) { + assert!(jet::eq_32(jet::current_index(), 0)); + + let (input_asset, input_amount): (u256, u64) = get_input_explicit_asset_amount(0); + assert!(jet::eq_256(input_asset, param::SETTLEMENT_ASSET_ID)); + + check_user_signature(sig); + + ensure_output_asset_with_amount_eq(0, param::SETTLEMENT_ASSET_ID, input_amount); +} + +/* + * Expiry Path + * + * User reclaims remaining collateral and premium after expiry. + * Only allowed after EXPIRY_TIME. + * Requires signature from USER_PUBKEY. + * No change - full reclaim only. + * + * Layout: + * Input[0]: Collateral from covenant + * Input[1]: Premium from covenant + * Output[0]: Collateral → user (any address) + * Output[1]: Premium → user (any address) + */ +fn expiry_path(sig: Signature) { + jet::check_lock_time(param::EXPIRY_TIME); + + assert!(jet::le_32(jet::current_index(), 1)); + + let expected_covenant_script_hash: u256 = get_input_script_hash(0); + + assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); + + let (collateral_asset, collateral_amount): (u256, u64) = get_input_explicit_asset_amount(0); + assert!(jet::eq_256(collateral_asset, param::COLLATERAL_ASSET_ID)); + + let (premium_asset, premium_amount): (u256, u64) = get_input_explicit_asset_amount(1); + assert!(jet::eq_256(premium_asset, param::PREMIUM_ASSET_ID)); + + check_user_signature(sig); + + ensure_output_asset_with_amount_eq(0, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(1, param::PREMIUM_ASSET_ID, premium_amount); +} + +fn main() { + match witness::PATH { + Left(params: (u64, bool)) => { + let (collateral_amount, is_change_needed): (u64, bool) = params; + exercise_path(collateral_amount, is_change_needed) + }, + Right(withdraw_or_expiry: Either) => match withdraw_or_expiry { + Left(sig: Signature) => withdraw_path(sig), + Right(sig: Signature) => expiry_path(sig), + }, + } +} diff --git a/examples/basic/simf/options.simf b/examples/basic/simf/options.simf new file mode 100644 index 0000000..e7da014 --- /dev/null +++ b/examples/basic/simf/options.simf @@ -0,0 +1,395 @@ +/* + * Options + * + * Important: Currently only the LBTC collateral is supported. + * + * Based on the https://blockstream.com/assets/downloads/pdf/options-whitepaper.pdf + * + * This contract implements cash-settled European-style options using covenant-locked collateral. + * + * Room for optimization: + * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/2 (Use input asset to determine option covenent type) + * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/3 (Simplify match token_branch in funding_path.) + * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 (why batching is hard to implement) + * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/5 (Reduce Contract Parameters) + * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/21 (explains why funding is limited) + */ + +/// Assert: a == b * expected_q, via divmod +fn divmod_eq(a: u64, b: u64, expected_q: u64) { + let (q, r): (u64, u64) = jet::div_mod_64(a, b); + assert!(jet::eq_64(q, expected_q)); + assert!(jet::eq_64(r, 0)); +} + +fn get_output_script_hash(index: u32) -> u256 { + unwrap(jet::output_script_hash(index)) +} + +fn get_input_script_hash(index: u32) -> u256 { + unwrap(jet::input_script_hash(index)) +} + +fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } +fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } + +fn increment_by(index: u32, amount: u32) -> u32 { + let (carry, result): (bool, u32) = jet::add_32(index, amount); + ensure_zero_bit(carry); + result +} + +fn ensure_input_and_output_script_hash_eq(index: u32) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); +} + +fn ensure_output_is_op_return(index: u32) { + match jet::output_null_datum(index, 0) { + Some(entry: Option>>) => (), + None => panic!(), + } +} + +fn ensure_input_asset_eq(index: u32, expected_bits: u256) { + let asset: Asset1 = unwrap(jet::input_asset(index)); + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + assert!(jet::eq_256(asset_bits, expected_bits)); +} + +fn ensure_output_asset_eq(index: u32, expected_bits: u256) { + let asset: Asset1 = unwrap(jet::output_asset(index)); + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + assert!(jet::eq_256(asset_bits, expected_bits)); +} + +fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { + let (asset, amount): (u256, u64) = dbg!(get_output_explicit_asset_amount(index)); + assert!(jet::eq_256(asset, expected_bits)); + assert!(jet::eq_64(amount, expected_amount)); +} + +fn ensure_input_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected)); +} + +fn ensure_output_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); +} + +fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { + let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); + assert!(jet::eq_32(jet::current_index(), index)); + + match is_change_needed { + true => { + ensure_input_and_output_script_hash_eq(index); + + let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); + ensure_zero_bit(carry); + ensure_output_asset_with_amount_eq(index, asset_id, collateral_change); + }, + false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), + } +} + +fn check_y(expected_y: Fe, actual_y: Fe) { + match jet::eq_256(expected_y, actual_y) { + true => {}, + false => { + assert!(jet::eq_256(expected_y, jet::fe_negate(actual_y))); + } + }; +} + +fn ensure_input_and_output_reissuance_token_eq(index: u32) { + let (input_asset, input_amount): (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (output_asset, output_amount): (Asset1, Amount1) = unwrap(jet::output_amount(index)); + + match (input_asset) { + Left(in_conf: Point) => { + let (input_asset_parity, input_asset_x): (u1, u256) = in_conf; + let (output_asset_parity, output_asset_x): (u1, u256) = unwrap_left::(output_asset); + + assert!(jet::eq_1(input_asset_parity, output_asset_parity)); + assert!(jet::eq_256(input_asset_x, output_asset_x)); + }, + Right(in_expl: u256) => { + let out_expl: u256 = unwrap_right::(output_asset); + assert!(jet::eq_256(in_expl, out_expl)); + } + }; + + match (input_amount) { + Left(in_conf: Point) => { + let (input_amount_parity, input_amount_x): (u1, u256) = in_conf; + let (output_amount_parity, output_amount_x): (u1, u256) = unwrap_left::(output_amount); + + assert!(jet::eq_1(input_amount_parity, output_amount_parity)); + assert!(jet::eq_256(input_amount_x, output_amount_x)); + }, + Right(in_expl: u64) => { + let out_expl: u64 = unwrap_right::(output_amount); + assert!(jet::eq_64(in_expl, out_expl)); + } + }; +} + +// Verify that a reissuance token commitment matches the expected token ID using provided blinding factors. +// Reissuance tokens are confidential because, in Elements, +// the asset must be provided in blinded form in order to reissue tokens. +// https://github.com/BlockstreamResearch/simplicity-contracts/issues/21#issuecomment-3691599583 +fn verify_token_commitment(actual_asset: Asset1, actual_amount: Amount1, expected_token_id: u256, abf: u256, vbf: u256) { + match actual_asset { + Left(conf_token: Point) => { + let amount_scalar: u256 = 1; + let (actual_ax, actual_ay): Ge = unwrap(jet::decompress(conf_token)); + + let gej_point: Gej = (jet::hash_to_curve(expected_token_id), 1); + let asset_blind_point: Gej = jet::generate(abf); + + let asset_generator: Gej = jet::gej_add(gej_point, asset_blind_point); + let (ax, ay): Ge = unwrap(jet::gej_normalize(asset_generator)); + + assert!(jet::eq_256(actual_ax, ax)); + check_y(actual_ay, ay); + + // Check amount + let conf_val: Point = unwrap_left::(actual_amount); + let (actual_vx, actual_vy): Ge = unwrap(jet::decompress(conf_val)); + + let amount_part: Gej = jet::scale(amount_scalar, asset_generator); + let vbf_part: Gej = jet::generate(vbf); + + let value_generator: Gej = jet::gej_add(amount_part, vbf_part); + let (vx, vy): Ge = unwrap(jet::gej_normalize(value_generator)); + + assert!(jet::eq_256(actual_vx, vx)); + check_y(actual_vy, vy); + }, + Right(reissuance_token: u256) => { + let expected_amount: u64 = 1; + let actual_amount: u64 = unwrap_right::(actual_amount); + + assert!(jet::eq_64(expected_amount, actual_amount)); + assert!(jet::eq_256(reissuance_token, expected_token_id)); + } + }; +} + +fn verify_output_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) { + let (asset, amount): (Asset1, Amount1) = unwrap(jet::output_amount(index)); + verify_token_commitment(asset, amount, expected_token_id, abf, vbf); +} + +fn verify_input_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) { + let (asset, amount): (Asset1, Amount1) = unwrap(jet::input_amount(index)); + verify_token_commitment(asset, amount, expected_token_id, abf, vbf); +} + +/* + * Funding Path + */ +fn funding_path( + expected_asset_amount: u64, + input_option_abf: u256, + input_option_vbf: u256, + input_grantor_abf: u256, + input_grantor_vbf: u256, + output_option_abf: u256, + output_option_vbf: u256, + output_grantor_abf: u256, + output_grantor_vbf: u256 +) { + ensure_input_and_output_script_hash_eq(0); + ensure_input_and_output_script_hash_eq(1); + + verify_input_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, input_option_abf, input_option_vbf); + verify_input_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, input_grantor_abf, input_grantor_vbf); + + verify_output_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, output_option_abf, output_option_vbf); + verify_output_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, output_grantor_abf, output_grantor_vbf); + + assert!(dbg!(jet::eq_256(get_output_script_hash(0), get_output_script_hash(1)))); + + assert!(jet::le_32(jet::current_index(), 1)); + + ensure_output_script_hash_eq(2, get_output_script_hash(0)); + + let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(2); + let option_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); + let grantor_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); + assert!(jet::eq_64(option_token_amount, grantor_token_amount)); + + divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, option_token_amount); + divmod_eq(expected_asset_amount, param::SETTLEMENT_PER_CONTRACT, option_token_amount); + + ensure_output_asset_with_amount_eq(2, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(3, param::OPTION_TOKEN_ASSET, option_token_amount); + ensure_output_asset_with_amount_eq(4, param::GRANTOR_TOKEN_ASSET, grantor_token_amount); +} + +/* + * Cancellation Path + */ +fn cancellation_path(amount_to_burn: u64, collateral_amount_to_withdraw: u64, is_change_needed: bool) { + let collateral_input_index: u32 = 0; + let option_input_index: u32 = 1; + let grantor_input_index: u32 = 2; + + let (burn_option_output_index, burn_grantor_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_withdraw, expected_current_script_hash, is_change_needed); + + // Burn option and grantor tokens + ensure_output_is_op_return(burn_option_output_index); + ensure_output_is_op_return(burn_grantor_output_index); + + ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, amount_to_burn); + ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, amount_to_burn); + + // Ensure returned collateral amount is correct + divmod_eq(collateral_amount_to_withdraw, param::COLLATERAL_PER_CONTRACT, amount_to_burn); +} + +/* + * Exercise Path + */ +fn exercise_path(option_amount_to_burn: u64, collateral_amount_to_get: u64, asset_amount_to_pay: u64, is_change_needed: bool) { + jet::check_lock_time(param::START_TIME); + + let collateral_input_index: u32 = 0; + + let (burn_option_output_index, asset_to_covenant_output_index): (u32, u32) = match is_change_needed { + true => (1, 2), + false => (0, 1), + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); + + // Ensure collateral and asset amounts are correct + divmod_eq(collateral_amount_to_get, param::COLLATERAL_PER_CONTRACT, option_amount_to_burn); + divmod_eq(asset_amount_to_pay, param::SETTLEMENT_PER_CONTRACT, option_amount_to_burn); + + // Burn option token + ensure_output_is_op_return(burn_option_output_index); + ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, option_amount_to_burn); + + // Ensure settlement asset and script hash are correct + ensure_output_asset_with_amount_eq(asset_to_covenant_output_index, param::SETTLEMENT_ASSET_ID, asset_amount_to_pay); + ensure_output_script_hash_eq(asset_to_covenant_output_index, expected_current_script_hash); +} + +/* + * Settlement Path + */ +fn settlement_path(grantor_token_amount_to_burn: u64, asset_amount: u64, is_change_needed: bool) { + jet::check_lock_time(param::START_TIME); + + let target_asset_input_index: u32 = 0; + + let burn_grantor_output_index: u32 = match is_change_needed { + true => 1, + false => 0, + }; + + let expected_current_script_hash: u256 = get_input_script_hash(target_asset_input_index); + + // Check and ensure settlement asset change + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, asset_amount, expected_current_script_hash, is_change_needed); + + // Ensure settlement asset and grantor token amounts are correct + divmod_eq(asset_amount, param::SETTLEMENT_PER_CONTRACT, grantor_token_amount_to_burn); + + // Burn grantor token + ensure_output_is_op_return(burn_grantor_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn); +} + +/* + * Expiry Path + */ +fn expiry_path(grantor_token_amount_to_burn: u64, collateral_amount: u64, is_change_needed: bool) { + jet::check_lock_time(param::EXPIRY_TIME); + + let collateral_input_index: u32 = 0; + + let burn_grantor_output_index: u32 = match is_change_needed { + true => 1, + false => 0, + }; + + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + + // Check and ensure collateral change + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_current_script_hash, is_change_needed); + + // Ensure collateral amount is correct + divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, grantor_token_amount_to_burn); + + // Burn grantor token + ensure_output_is_op_return(burn_grantor_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn); +} + +fn main() { + match witness::PATH { + Left(left_or_right: Either<(u64, u256, u256, u256, u256, u256, u256, u256, u256), Either<(bool, u64, u64, u64), (bool, u64, u64)>>) => match left_or_right { + Left(params: (u64, u256, u256, u256, u256, u256, u256, u256, u256)) => { + let (expected_asset_amount, input_option_abf, input_option_vbf, input_grantor_abf, input_grantor_vbf, output_option_abf, output_option_vbf, output_grantor_abf, output_grantor_vbf): (u64, u256, u256, u256, u256, u256, u256, u256, u256) = params; + funding_path( + expected_asset_amount, + input_option_abf, input_option_vbf, + input_grantor_abf, input_grantor_vbf, + output_option_abf, output_option_vbf, + output_grantor_abf, output_grantor_vbf + ); + }, + Right(exercise_or_settlement: Either<(bool, u64, u64, u64), (bool, u64, u64)>) => match exercise_or_settlement { + Left(params: (bool, u64, u64, u64)) => { + let (is_change_needed, amount_to_burn, collateral_amount, asset_amount): (bool, u64, u64, u64) = dbg!(params); + exercise_path(amount_to_burn, collateral_amount, asset_amount, is_change_needed) + }, + Right(params: (bool, u64, u64)) => { + let (is_change_needed, amount_to_burn, asset_amount): (bool, u64, u64) = dbg!(params); + settlement_path(amount_to_burn, asset_amount, is_change_needed) + }, + }, + }, + Right(left_or_right: Either<(bool, u64, u64), (bool, u64, u64)>) => match left_or_right { + Left(params: (bool, u64, u64)) => { + let (is_change_needed, grantor_token_amount_to_burn, collateral_amount): (bool, u64, u64) = params; + expiry_path(grantor_token_amount_to_burn, collateral_amount, is_change_needed) + }, + Right(params: (bool, u64, u64)) => { + let (is_change_needed, amount_to_burn, collateral_amount): (bool, u64, u64) = params; + cancellation_path(amount_to_burn, collateral_amount, is_change_needed) + }, + }, + } +} diff --git a/crates/sdk/src/simplicityhl_core/source_simf/p2pk.simf b/examples/basic/simf/p2pk.simf similarity index 100% rename from crates/sdk/src/simplicityhl_core/source_simf/p2pk.simf rename to examples/basic/simf/p2pk.simf diff --git a/examples/basic/src/artifacts/another_dir/another_module/bytes32_tr_storage.rs b/examples/basic/src/artifacts/another_dir/another_module/bytes32_tr_storage.rs new file mode 100644 index 0000000..70abfcf --- /dev/null +++ b/examples/basic/src/artifacts/another_dir/another_module/bytes32_tr_storage.rs @@ -0,0 +1,24 @@ +use simplex::include_simf; +use simplex::simplex_sdk::program::{ArgumentsTrait, Program}; +use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +pub struct Bytes32TrStorageProgram { + program: Program, +} +impl Bytes32TrStorageProgram { + pub const SOURCE: &'static str = derived_bytes32_tr_storage::BYTES32_TR_STORAGE_CONTRACT_SOURCE; + pub fn new( + public_key: XOnlyPublicKey, + arguments: impl ArgumentsTrait + 'static, + ) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + pub fn get_program(&self) -> &Program { + &self.program + } + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } +} +include_simf!("simf/another_dir/another_module/bytes32_tr_storage.simf"); diff --git a/examples/basic/src/artifacts/another_dir/another_module/dual_currency_deposit.rs b/examples/basic/src/artifacts/another_dir/another_module/dual_currency_deposit.rs new file mode 100644 index 0000000..4a2c288 --- /dev/null +++ b/examples/basic/src/artifacts/another_dir/another_module/dual_currency_deposit.rs @@ -0,0 +1,24 @@ +use simplex::include_simf; +use simplex::simplex_sdk::program::{ArgumentsTrait, Program}; +use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +pub struct DualCurrencyDepositProgram { + program: Program, +} +impl DualCurrencyDepositProgram { + pub const SOURCE: &'static str = derived_dual_currency_deposit::DUAL_CURRENCY_DEPOSIT_CONTRACT_SOURCE; + pub fn new( + public_key: XOnlyPublicKey, + arguments: impl ArgumentsTrait + 'static, + ) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + pub fn get_program(&self) -> &Program { + &self.program + } + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } +} +include_simf!("simf/another_dir/another_module/dual_currency_deposit.simf"); diff --git a/examples/basic/src/artifacts/another_dir/another_module/mod.rs b/examples/basic/src/artifacts/another_dir/another_module/mod.rs new file mode 100644 index 0000000..121de82 --- /dev/null +++ b/examples/basic/src/artifacts/another_dir/another_module/mod.rs @@ -0,0 +1,2 @@ +pub mod dual_currency_deposit; +pub mod bytes32_tr_storage; diff --git a/examples/basic/src/artifacts/another_dir/array_tr_storage.rs b/examples/basic/src/artifacts/another_dir/array_tr_storage.rs new file mode 100644 index 0000000..373f444 --- /dev/null +++ b/examples/basic/src/artifacts/another_dir/array_tr_storage.rs @@ -0,0 +1,24 @@ +use simplex::include_simf; +use simplex::simplex_sdk::program::{ArgumentsTrait, Program}; +use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +pub struct ArrayTrStorageProgram { + program: Program, +} +impl ArrayTrStorageProgram { + pub const SOURCE: &'static str = derived_array_tr_storage::ARRAY_TR_STORAGE_CONTRACT_SOURCE; + pub fn new( + public_key: XOnlyPublicKey, + arguments: impl ArgumentsTrait + 'static, + ) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + pub fn get_program(&self) -> &Program { + &self.program + } + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } +} +include_simf!("simf/another_dir/array_tr_storage.simf"); diff --git a/examples/basic/src/artifacts/another_dir/mod.rs b/examples/basic/src/artifacts/another_dir/mod.rs new file mode 100644 index 0000000..f7bff3b --- /dev/null +++ b/examples/basic/src/artifacts/another_dir/mod.rs @@ -0,0 +1,2 @@ +pub mod array_tr_storage; +pub mod another_module; diff --git a/examples/basic/src/artifacts/mod.rs b/examples/basic/src/artifacts/mod.rs new file mode 100644 index 0000000..b78b75d --- /dev/null +++ b/examples/basic/src/artifacts/mod.rs @@ -0,0 +1,4 @@ +pub mod p2pk; +pub mod options; +pub mod module; +pub mod another_dir; diff --git a/examples/basic/src/artifacts/module/mod.rs b/examples/basic/src/artifacts/module/mod.rs new file mode 100644 index 0000000..920b31d --- /dev/null +++ b/examples/basic/src/artifacts/module/mod.rs @@ -0,0 +1 @@ +pub mod option_offer; diff --git a/examples/basic/src/artifacts/module/option_offer.rs b/examples/basic/src/artifacts/module/option_offer.rs new file mode 100644 index 0000000..603e7be --- /dev/null +++ b/examples/basic/src/artifacts/module/option_offer.rs @@ -0,0 +1,24 @@ +use simplex::include_simf; +use simplex::simplex_sdk::program::{ArgumentsTrait, Program}; +use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +pub struct OptionOfferProgram { + program: Program, +} +impl OptionOfferProgram { + pub const SOURCE: &'static str = derived_option_offer::OPTION_OFFER_CONTRACT_SOURCE; + pub fn new( + public_key: XOnlyPublicKey, + arguments: impl ArgumentsTrait + 'static, + ) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + pub fn get_program(&self) -> &Program { + &self.program + } + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } +} +include_simf!("simf/module/option_offer.simf"); diff --git a/examples/basic/src/artifacts/options.rs b/examples/basic/src/artifacts/options.rs new file mode 100644 index 0000000..9631c0a --- /dev/null +++ b/examples/basic/src/artifacts/options.rs @@ -0,0 +1,24 @@ +use simplex::include_simf; +use simplex::simplex_sdk::program::{ArgumentsTrait, Program}; +use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +pub struct OptionsProgram { + program: Program, +} +impl OptionsProgram { + pub const SOURCE: &'static str = derived_options::OPTIONS_CONTRACT_SOURCE; + pub fn new( + public_key: XOnlyPublicKey, + arguments: impl ArgumentsTrait + 'static, + ) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + pub fn get_program(&self) -> &Program { + &self.program + } + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } +} +include_simf!("simf/options.simf"); diff --git a/examples/basic/src/artifacts/p2pk.rs b/examples/basic/src/artifacts/p2pk.rs new file mode 100644 index 0000000..3cf11bf --- /dev/null +++ b/examples/basic/src/artifacts/p2pk.rs @@ -0,0 +1,24 @@ +use simplex::include_simf; +use simplex::simplex_sdk::program::{ArgumentsTrait, Program}; +use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +pub struct P2pkProgram { + program: Program, +} +impl P2pkProgram { + pub const SOURCE: &'static str = derived_p2pk::P2PK_CONTRACT_SOURCE; + pub fn new( + public_key: XOnlyPublicKey, + arguments: impl ArgumentsTrait + 'static, + ) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), + } + } + pub fn get_program(&self) -> &Program { + &self.program + } + pub fn get_program_mut(&mut self) -> &mut Program { + &mut self.program + } +} +include_simf!("simf/p2pk.simf"); diff --git a/examples/basic/src/lib.rs b/examples/basic/src/lib.rs new file mode 100644 index 0000000..91946c7 --- /dev/null +++ b/examples/basic/src/lib.rs @@ -0,0 +1 @@ +pub mod artifacts; diff --git a/examples/basic/tests/draft_test.rs b/examples/basic/tests/draft_test.rs new file mode 100644 index 0000000..6cefbcf --- /dev/null +++ b/examples/basic/tests/draft_test.rs @@ -0,0 +1,92 @@ +use simplicityhl::elements::{Script, Txid}; + +use simplex::simplex_sdk::constants::DUMMY_SIGNATURE; +use simplex::simplex_sdk::provider::ProviderTrait; +use simplex::simplex_sdk::signer::Signer; +use simplex::simplex_sdk::transaction::{ + FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, +}; +use simplex::simplex_sdk::utils::tr_unspendable_key; + +use draft_example::artifacts::p2pk::P2pkProgram; +use draft_example::artifacts::p2pk::derived_p2pk::{P2pkArguments, P2pkWitness}; + +fn get_p2pk(context: &simplex::TestContext, signer: &Signer) -> (P2pkProgram, Script) { + let arguments = P2pkArguments { + public_key: signer.get_schnorr_public_key().unwrap().serialize(), + }; + + let p2pk = P2pkProgram::new(tr_unspendable_key(), arguments); + let p2pk_script = p2pk.get_program().get_script_pubkey(*context.get_network()).unwrap(); + + (p2pk, p2pk_script) +} + +fn spend_p2wpkh(context: &simplex::TestContext, signer: &Signer, provider: &dyn ProviderTrait) -> Txid { + let (_, p2pk_script) = get_p2pk(context, signer); + + let mut ft = FinalTransaction::new(*context.get_network()); + + ft.add_output(PartialOutput::new( + p2pk_script.clone(), + 50, + context.get_network().policy_asset(), + )); + + let (tx, _) = signer.finalize(&ft, 1).unwrap(); + let res = provider.broadcast_transaction(&tx).unwrap(); + + println!("Broadcast: {}", res); + + res +} + +fn spend_p2pk(context: &simplex::TestContext, signer: &Signer, provider: &dyn ProviderTrait) -> Txid { + let (p2pk, p2pk_script) = get_p2pk(context, signer); + + let mut p2pk_utxos = provider.fetch_scripthash_utxos(&p2pk_script).unwrap(); + + p2pk_utxos.retain(|el| el.1.asset.explicit().unwrap() == context.get_network().policy_asset()); + + let mut ft = FinalTransaction::new(*context.get_network()); + + let witness = P2pkWitness { + signature: DUMMY_SIGNATURE, + }; + + ft.add_program_input( + PartialInput::new(p2pk_utxos[0].0, p2pk_utxos[0].1.clone()), + ProgramInput::new(Box::new(p2pk.get_program().clone()), Box::new(witness.clone())), + RequiredSignature::Witness("SIGNATURE".to_string()), + ) + .unwrap(); + + let (tx, _) = signer.finalize(&ft, 1).unwrap(); + let res = provider.broadcast_transaction(&tx).unwrap(); + + println!("Broadcast: {}", res); + + res +} + +#[simplex::test] +fn dummy_test(context: simplex::TestContext) -> anyhow::Result<()> { + let signer = context.get_signer(); + let provider = context.get_provider(); + + let tx = spend_p2wpkh(&context, &signer, provider.as_ref()); + + provider.wait(&tx)?; + + println!("Confirmed"); + + let tx = spend_p2pk(&context, &signer, provider.as_ref()); + + provider.wait(&tx)?; + + println!("Confirmed"); + + println!("OK"); + + Ok(()) +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..74b8276 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,25 @@ +# 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"