From 10beb3f0dcdc75757960973cceeea76a701db319 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Mon, 11 May 2026 12:34:00 +0300 Subject: [PATCH 1/5] general: add Simplex.toml --- Simplex.toml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Simplex.toml diff --git a/Simplex.toml b/Simplex.toml new file mode 100644 index 0000000..7bb6be4 --- /dev/null +++ b/Simplex.toml @@ -0,0 +1,4 @@ +[build] +src_dir = "crates/contracts/simf" +simf_files = ["**/*.simf"] +out_dir = "crates/contracts/src/artifacts" From 1484e1b5c13b4ac7059a6eb89bdcb93c2d08b20c Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Mon, 11 May 2026 12:34:42 +0300 Subject: [PATCH 2/5] general: remove cli, wallet-abi. Move to Simplex for testing finance related contracts --- Cargo.lock | 2934 +++-------------- Cargo.toml | 15 +- clippy.toml | 1 + crates/cli/.env.example | 5 - crates/cli/Cargo.toml | 35 - crates/cli/README.md | 72 - crates/cli/src/commands/basic.rs | 426 --- crates/cli/src/commands/mod.rs | 4 - crates/cli/src/commands/option_offer.rs | 513 --- crates/cli/src/lib.rs | 4 - crates/cli/src/main.rs | 92 - crates/cli/src/modules/mod.rs | 2 - crates/cli/src/modules/store.rs | 76 - crates/cli/src/modules/utils.rs | 42 - crates/contracts/.gitignore | 1 + crates/contracts/Cargo.toml | 22 +- crates/contracts/README.md | 21 +- crates/contracts/Simplex.toml | 0 .../source_simf => simf}/option_offer.simf | 0 .../options/source_simf => simf}/options.simf | 0 crates/contracts/src/error.rs | 106 - crates/contracts/src/finance/mod.rs | 4 - .../finance/option_offer/build_arguments.rs | 284 -- .../src/finance/option_offer/build_witness.rs | 61 - .../contracts/src/finance/option_offer/mod.rs | 641 ---- .../src/finance/options/build_arguments.rs | 382 --- .../src/finance/options/build_witness.rs | 125 - crates/contracts/src/finance/options/mod.rs | 67 - crates/contracts/src/lib.rs | 19 +- crates/contracts/src/programs/mod.rs | 3 + crates/contracts/src/programs/option_offer.rs | 118 + crates/contracts/src/programs/options.rs | 196 ++ crates/contracts/src/programs/program.rs | 17 + .../contracts/src/simplicityhl_core/error.rs | 79 + crates/contracts/src/simplicityhl_core/mod.rs | 3 + .../contracts/src/simplicityhl_core/runner.rs | 38 + .../src/simplicityhl_core/scripts.rs | 105 + .../array_tr_storage/build_witness.rs | 22 +- .../state_management/array_tr_storage/mod.rs | 37 +- .../bytes32_tr_storage/build_witness.rs | 8 +- .../bytes32_tr_storage/mod.rs | 49 +- .../simple_storage/build_arguments.rs | 17 +- .../simple_storage/build_witness.rs | 12 +- .../state_management/simple_storage/mod.rs | 66 +- .../smt_storage/build_witness.rs | 32 +- .../src/state_management/smt_storage/mod.rs | 44 +- .../src/state_management/smt_storage/smt.rs | 8 +- .../contracts/src/utils/arguments_helpers.rs | 72 - crates/contracts/src/utils/mod.rs | 4 - crates/contracts/src/utils/test_setup.rs | 191 -- crates/contracts/tests/common/filters.rs | 137 + crates/contracts/tests/common/issuance.rs | 59 + crates/contracts/tests/common/mod.rs | 3 + crates/contracts/tests/common/signer.rs | 129 + crates/contracts/tests/mod.rs | 3 + crates/contracts/tests/program_builder/mod.rs | 2 + .../tests/program_builder/option_offer.rs | 166 + .../tests/program_builder/options.rs | 591 ++++ crates/contracts/tests/regtest/mod.rs | 2 + .../tests/regtest/option_offer/deposit.rs | 72 + .../tests/regtest/option_offer/exercise.rs | 497 +++ .../tests/regtest/option_offer/expiry.rs | 221 ++ .../tests/regtest/option_offer/mod.rs | 4 + .../tests/regtest/option_offer/withdraw.rs | 532 +++ .../contracts/tests/regtest/options/cancel.rs | 245 ++ .../tests/regtest/options/exercise.rs | 412 +++ .../contracts/tests/regtest/options/expiry.rs | 325 ++ .../contracts/tests/regtest/options/fund.rs | 117 + crates/contracts/tests/regtest/options/mod.rs | 5 + .../tests/regtest/options/settlement.rs | 545 +++ crates/wallet-abi/Cargo.toml | 31 - crates/wallet-abi/src/encoding.rs | 83 - crates/wallet-abi/src/error.rs | 75 - .../wallet-abi/src/issuance_validation/mod.rs | 647 ---- crates/wallet-abi/src/lib.rs | 27 - .../src/runtime/input_resolution.rs | 1294 -------- crates/wallet-abi/src/runtime/mod.rs | 624 ---- .../src/runtime/output_resolution.rs | 590 ---- crates/wallet-abi/src/runtime/utils.rs | 10 - crates/wallet-abi/src/schema/mod.rs | 4 - .../wallet-abi/src/schema/runtime_params.rs | 275 -- crates/wallet-abi/src/schema/tx_create.rs | 121 - crates/wallet-abi/src/schema/types.rs | 9 - crates/wallet-abi/src/schema/values.rs | 176 - crates/wallet-abi/src/scripts.rs | 24 - crates/wallet-abi/src/simplicity/mod.rs | 1 - crates/wallet-abi/src/simplicity/p2pk.rs | 61 - crates/wallet-abi/src/source_simf/p2pk.simf | 3 - crates/wallet-abi/src/taproot_pubkey_gen.rs | 448 --- crates/wallet-abi/src/tx_inclusion.rs | 200 -- .../tests/data/test-tx-incl-block.hex | 1 - .../tests/data/tx_with_issuance_token.hex | 1 - 92 files changed, 5253 insertions(+), 10594 deletions(-) create mode 100644 clippy.toml delete mode 100644 crates/cli/.env.example delete mode 100644 crates/cli/Cargo.toml delete mode 100644 crates/cli/README.md delete mode 100644 crates/cli/src/commands/basic.rs delete mode 100644 crates/cli/src/commands/mod.rs delete mode 100644 crates/cli/src/commands/option_offer.rs delete mode 100644 crates/cli/src/lib.rs delete mode 100644 crates/cli/src/main.rs delete mode 100644 crates/cli/src/modules/mod.rs delete mode 100644 crates/cli/src/modules/store.rs delete mode 100644 crates/cli/src/modules/utils.rs create mode 100644 crates/contracts/.gitignore create mode 100644 crates/contracts/Simplex.toml rename crates/contracts/{src/finance/option_offer/source_simf => simf}/option_offer.simf (100%) rename crates/contracts/{src/finance/options/source_simf => simf}/options.simf (100%) delete mode 100644 crates/contracts/src/error.rs delete mode 100644 crates/contracts/src/finance/mod.rs delete mode 100644 crates/contracts/src/finance/option_offer/build_arguments.rs delete mode 100644 crates/contracts/src/finance/option_offer/build_witness.rs delete mode 100644 crates/contracts/src/finance/option_offer/mod.rs delete mode 100644 crates/contracts/src/finance/options/build_arguments.rs delete mode 100644 crates/contracts/src/finance/options/build_witness.rs delete mode 100644 crates/contracts/src/finance/options/mod.rs create mode 100644 crates/contracts/src/programs/mod.rs create mode 100644 crates/contracts/src/programs/option_offer.rs create mode 100644 crates/contracts/src/programs/options.rs create mode 100644 crates/contracts/src/programs/program.rs create mode 100644 crates/contracts/src/simplicityhl_core/error.rs create mode 100644 crates/contracts/src/simplicityhl_core/mod.rs create mode 100644 crates/contracts/src/simplicityhl_core/runner.rs create mode 100644 crates/contracts/src/simplicityhl_core/scripts.rs delete mode 100644 crates/contracts/src/utils/arguments_helpers.rs delete mode 100644 crates/contracts/src/utils/mod.rs delete mode 100644 crates/contracts/src/utils/test_setup.rs create mode 100644 crates/contracts/tests/common/filters.rs create mode 100644 crates/contracts/tests/common/issuance.rs create mode 100644 crates/contracts/tests/common/mod.rs create mode 100644 crates/contracts/tests/common/signer.rs create mode 100644 crates/contracts/tests/mod.rs create mode 100644 crates/contracts/tests/program_builder/mod.rs create mode 100644 crates/contracts/tests/program_builder/option_offer.rs create mode 100644 crates/contracts/tests/program_builder/options.rs create mode 100644 crates/contracts/tests/regtest/mod.rs create mode 100644 crates/contracts/tests/regtest/option_offer/deposit.rs create mode 100644 crates/contracts/tests/regtest/option_offer/exercise.rs create mode 100644 crates/contracts/tests/regtest/option_offer/expiry.rs create mode 100644 crates/contracts/tests/regtest/option_offer/mod.rs create mode 100644 crates/contracts/tests/regtest/option_offer/withdraw.rs create mode 100644 crates/contracts/tests/regtest/options/cancel.rs create mode 100644 crates/contracts/tests/regtest/options/exercise.rs create mode 100644 crates/contracts/tests/regtest/options/expiry.rs create mode 100644 crates/contracts/tests/regtest/options/fund.rs create mode 100644 crates/contracts/tests/regtest/options/mod.rs create mode 100644 crates/contracts/tests/regtest/options/settlement.rs delete mode 100644 crates/wallet-abi/Cargo.toml delete mode 100644 crates/wallet-abi/src/encoding.rs delete mode 100644 crates/wallet-abi/src/error.rs delete mode 100644 crates/wallet-abi/src/issuance_validation/mod.rs delete mode 100644 crates/wallet-abi/src/lib.rs delete mode 100644 crates/wallet-abi/src/runtime/input_resolution.rs delete mode 100644 crates/wallet-abi/src/runtime/mod.rs delete mode 100644 crates/wallet-abi/src/runtime/output_resolution.rs delete mode 100644 crates/wallet-abi/src/runtime/utils.rs delete mode 100644 crates/wallet-abi/src/schema/mod.rs delete mode 100644 crates/wallet-abi/src/schema/runtime_params.rs delete mode 100644 crates/wallet-abi/src/schema/tx_create.rs delete mode 100644 crates/wallet-abi/src/schema/types.rs delete mode 100644 crates/wallet-abi/src/schema/values.rs delete mode 100644 crates/wallet-abi/src/scripts.rs delete mode 100644 crates/wallet-abi/src/simplicity/mod.rs delete mode 100644 crates/wallet-abi/src/simplicity/p2pk.rs delete mode 100644 crates/wallet-abi/src/source_simf/p2pk.simf delete mode 100644 crates/wallet-abi/src/taproot_pubkey_gen.rs delete mode 100644 crates/wallet-abi/src/tx_inclusion.rs delete mode 100644 crates/wallet-abi/tests/data/test-tx-incl-block.hex delete mode 100644 crates/wallet-abi/tests/data/tx_with_issuance_token.hex diff --git a/Cargo.lock b/Cargo.lock index a8c61f1..c578f92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,49 +38,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "age" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf640be7658959746f1f0f2faab798f6098a9436a8e18e148d18bc9875e13c4b" -dependencies = [ - "age-core", - "base64 0.21.7", - "bech32 0.9.1", - "chacha20poly1305", - "cookie-factory", - "hmac", - "i18n-embed", - "i18n-embed-fl", - "lazy_static", - "nom", - "pin-project", - "rand 0.8.5", - "rust-embed", - "scrypt", - "sha2", - "subtle", - "x25519-dalek", - "zeroize", -] - -[[package]] -name = "age-core" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99" -dependencies = [ - "base64 0.21.7", - "chacha20poly1305", - "cookie-factory", - "hkdf", - "io_tee", - "nom", - "rand 0.8.5", - "secrecy", - "sha2", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -91,19 +48,16 @@ dependencies = [ ] [[package]] -name = "android_system_properties" -version = "0.1.5" +name = "allocator-api2" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -116,15 +70,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -151,17 +105,17 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "arc-swap" -version = "1.8.2" +name = "ar_archive_writer" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ - "rustversion", + "object", ] [[package]] @@ -170,12 +124,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.5.0" @@ -204,53 +152,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "basic-toml" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" -dependencies = [ - "serde", -] - -[[package]] -name = "bech32" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" - [[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" @@ -258,8 +165,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ "bitcoin_hashes", - "rand 0.8.5", - "rand_core 0.6.4", + "rand", + "rand_core", "serde", "unicode-normalization", ] @@ -272,7 +179,7 @@ checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", "base64 0.21.7", - "bech32 0.11.1", + "bech32", "bitcoin-internals", "bitcoin-io", "bitcoin-units", @@ -370,9 +277,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -390,21 +297,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829a082bd3761fde7476dc2ed85ca56c11628948460ece621e4f56fef5046567" [[package]] -name = "bollard-stubs" -version = "1.41.0" +name = "bstr" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2f2e73fffe9455141e170fb9c1feb0ac521ec7e7dcd47a7cab72a658490fb8" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ - "chrono", + "memchr", "serde", - "serde_with", ] [[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" @@ -412,17 +318,11 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - [[package]] name = "cc" -version = "1.2.56" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", @@ -435,47 +335,17 @@ 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 = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - -[[package]] -name = "chrono" -version = "0.4.43" +name = "chumsky" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "acc17a6284abccac6e50db35c1cee87f605474a72939b959a3a67d9371800efd" dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", + "hashbrown 0.15.5", + "regex-automata 0.3.9", "serde", - "wasm-bindgen", - "windows-link", + "stacker", + "unicode-ident", + "unicode-segmentation", ] [[package]] @@ -486,115 +356,53 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", - "zeroize", ] [[package]] name = "clap" -version = "4.5.59" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", - "clap_derive", ] [[package]] name = "clap_builder" -version = "4.5.59" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", -] - -[[package]] -name = "clap_derive" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.116", + "strsim", ] [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" - -[[package]] -name = "cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "contracts", - "dotenvy", - "hex", - "lwk_common", - "simplicityhl", - "sled", - "tokio", - "tracing-subscriber", - "wallet-abi", -] +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "contracts" version = "0.1.0" dependencies = [ "anyhow", - "bincode", - "bitcoincore-rpc", - "hex", "lwk_common", - "lwk_test_util", "serde", "serde_json", - "simplicityhl", - "thiserror 2.0.18", - "tokio", - "wallet-abi", -] - -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" -dependencies = [ - "futures", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", + "smplx-std", + "thiserror", ] -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -605,12 +413,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]] @@ -635,7 +444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "typenum", ] @@ -648,67 +457,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", -] - -[[package]] -name = "darling" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" -dependencies = [ - "darling_core", - "quote", - "syn 1.0.109", -] - [[package]] name = "digest" version = "0.10.7" @@ -721,27 +469,19 @@ dependencies = [ ] [[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" +name = "dyn-clone" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +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" @@ -763,14 +503,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0bd443023f9f5c4b7153053721939accc7113cbdf810a024434eed454b3db1" dependencies = [ "bitcoin", - "byteorder", - "libc", "log", - "rustls", "serde", "serde_json", - "webpki-roots 0.25.4", - "winapi", ] [[package]] @@ -779,25 +514,13 @@ version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81b2569d3495bfdfce36c504fd4d78752ff4a7699f8a33e6f3ee523bddf9f6ad" dependencies = [ - "bech32 0.11.1", + "bech32", "bitcoin", "secp256k1-zkp", "serde", "serde_json", ] -[[package]] -name = "elements" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d562b364c5d2aced40b01b3f73fc968311787e6813957593d4ffa94cd8733e3" -dependencies = [ - "bech32 0.11.1", - "bitcoin", - "secp256k1-zkp", - "serde_json", -] - [[package]] name = "elements-miniscript" version = "0.4.0" @@ -805,43 +528,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "571fa105690f83c7833df2109eb2e14ca0e62d633d2624ffcb166ff18a3da870" dependencies = [ "bitcoin", - "elements 0.25.2", + "elements", "miniscript", "serde", ] -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "env_filter" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -860,24 +551,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "find-crate" -version = "0.6.3" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" -dependencies = [ - "toml", -] +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" @@ -886,259 +562,81 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] -name = "fluent" -version = "0.16.1" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" -dependencies = [ - "fluent-bundle", - "unic-langid", -] +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "fluent-bundle" -version = "0.15.3" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "fluent-langneg", - "fluent-syntax", - "intl-memoizer", - "intl_pluralrules", - "rustc-hash 1.1.0", - "self_cell 0.10.3", - "smallvec", - "unic-langid", + "typenum", + "version_check", ] [[package]] -name = "fluent-langneg" -version = "0.13.1" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "unic-langid", + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", ] [[package]] -name = "fluent-syntax" -version = "0.11.1" +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ - "thiserror 1.0.69", + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "ghost-cell" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "d8449d342b1c67f49169e92e71deb7b9b27f30062301a16dbc27a4cc8d2351b7" [[package]] -name = "foldhash" -version = "0.1.5" +name = "glob" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[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 = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width", -] - -[[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.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.1" +name = "globset" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.14", + "regex-syntax 0.8.10", ] [[package]] -name = "ghost-cell" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8449d342b1c67f49169e92e71deb7b9b27f30062301a16dbc27a4cc8d2351b7" - -[[package]] -name = "h2" -version = "0.4.13" +name = "globwalk" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", + "bitflags 2.11.1", + "ignore", + "walkdir", ] [[package]] @@ -1147,14 +645,16 @@ 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" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -1183,15 +683,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - [[package]] name = "hmac" version = "0.12.1" @@ -1210,280 +701,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.6", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "system-configuration", - "tokio", - "tower-service", - "tracing", - "windows-registry", -] - -[[package]] -name = "i18n-config" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" -dependencies = [ - "basic-toml", - "log", - "serde", - "serde_derive", - "thiserror 1.0.69", - "unic-langid", -] - -[[package]] -name = "i18n-embed" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4" -dependencies = [ - "arc-swap", - "fluent", - "fluent-langneg", - "fluent-syntax", - "i18n-embed-impl", - "intl-memoizer", - "log", - "parking_lot 0.12.5", - "rust-embed", - "thiserror 1.0.69", - "unic-langid", - "walkdir", -] - -[[package]] -name = "i18n-embed-fl" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2d" -dependencies = [ - "find-crate", - "fluent", - "fluent-syntax", - "i18n-config", - "i18n-embed", - "proc-macro-error2", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.116", - "unic-langid", -] - -[[package]] -name = "i18n-embed-impl" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" -dependencies = [ - "find-crate", - "i18n-config", - "proc-macro2", - "quote", - "syn 2.0.116", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - [[package]] name = "id-arena" version = "2.3.0" @@ -1491,40 +708,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] -name = "ident_case" -version = "1.0.1" +name = "ignore" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ - "icu_normalizer", - "icu_properties", + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.14", + "same-file", + "walkdir", + "winapi-util", ] [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -1538,56 +744,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "intl-memoizer" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" -dependencies = [ - "type-map", - "unic-langid", -] - -[[package]] -name = "intl_pluralrules" -version = "7.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" -dependencies = [ - "unic-langid", -] - -[[package]] -name = "io_tee" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1605,39 +761,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jiff" -version = "0.2.20" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", -] +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", @@ -1655,12 +787,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "leb128fmt" version = "0.1.0" @@ -1669,9 +795,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "linux-raw-sys" @@ -1681,24 +807,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lock_api" -version = "0.4.14" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" @@ -1706,374 +817,109 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "lwk_common" -version = "0.14.0" -source = "git+https://github.com/Blockstream/lwk?rev=4074c527fcc1268eb26953af397eecc6c1444293#4074c527fcc1268eb26953af397eecc6c1444293" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c72efef1d108bbfeb135d0b6617729165532533d362ae4c9c93b2699b5132b" dependencies = [ "aes-gcm-siv", "base64 0.21.7", - "elements 0.25.2", + "elements", "elements-miniscript", "getrandom 0.2.17", "qr_code", - "rand 0.8.5", + "rand", "serde", "serde_json", "tempfile", - "thiserror 1.0.69", -] - -[[package]] -name = "lwk_containers" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb8dd8521dc2613c494b5ad4856d864499b694a83e3bd33c4aec2c30e084462" -dependencies = [ - "elements 0.25.2", - "rand 0.8.5", - "tempfile", - "testcontainers", -] - -[[package]] -name = "lwk_signer" -version = "0.14.0" -source = "git+https://github.com/Blockstream/lwk?rev=4074c527fcc1268eb26953af397eecc6c1444293#4074c527fcc1268eb26953af397eecc6c1444293" -dependencies = [ - "base64 0.21.7", - "bip39", - "elements-miniscript", - "lwk_common", - "thiserror 1.0.69", -] - -[[package]] -name = "lwk_simplicity" -version = "0.1.0" -source = "git+https://github.com/Blockstream/lwk?rev=4074c527fcc1268eb26953af397eecc6c1444293#4074c527fcc1268eb26953af397eecc6c1444293" -dependencies = [ - "lwk_common", - "simplicityhl", - "thiserror 2.0.18", -] - -[[package]] -name = "lwk_test_util" -version = "0.14.0" -source = "git+https://github.com/Blockstream/lwk?rev=4074c527fcc1268eb26953af397eecc6c1444293#4074c527fcc1268eb26953af397eecc6c1444293" -dependencies = [ - "bip39", - "electrsd", - "elements-miniscript", - "env_logger", - "log", - "lwk_common", - "lwk_containers", - "pulldown-cmark", - "rand 0.8.5", - "reqwest", - "serde_json", - "tempfile", -] - -[[package]] -name = "lwk_wollet" -version = "0.14.0" -source = "git+https://github.com/Blockstream/lwk?rev=4074c527fcc1268eb26953af397eecc6c1444293#4074c527fcc1268eb26953af397eecc6c1444293" -dependencies = [ - "aes-gcm-siv", - "age", - "base64 0.21.7", - "bip39", - "electrum-client", - "elements 0.25.2", - "elements 0.26.1", - "elements-miniscript", - "futures", - "fxhash", - "idna", - "js-sys", - "log", - "lwk_common", - "once_cell", - "rand 0.8.5", - "regex-lite", - "reqwest", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "url", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "memchr" -version = "2.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 = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniscript" -version = "12.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" -dependencies = [ - "bech32 0.11.1", - "bitcoin", -] - -[[package]] -name = "minreq" -version = "2.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "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 = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[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 = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.12", + "thiserror", ] [[package]] -name = "parking_lot_core" -version = "0.8.6" +name = "memchr" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", -] +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "memoffset" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", + "autocfg", ] [[package]] -name = "pbkdf2" -version = "0.12.2" +name = "miniscript" +version = "12.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" dependencies = [ - "digest", - "hmac", + "bech32", + "bitcoin", ] [[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.6" +name = "minreq" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" dependencies = [ - "memchr", - "ucd-trie", + "rustls", + "rustls-webpki", + "serde", + "serde_json", + "webpki-roots", ] [[package]] -name = "pest_derive" -version = "2.8.6" +name = "nix" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ - "pest", - "pest_generator", + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", ] [[package]] -name = "pest_generator" -version = "2.8.6" +name = "object" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.116", + "memchr", ] [[package]] -name = "pest_meta" -version = "2.8.6" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "pin-project" -version = "1.1.10" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "pin-project-internal" -version = "1.1.10" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", -] +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "pathdiff" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pin-utils" @@ -2081,17 +927,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", -] - [[package]] name = "polyval" version = "0.6.2" @@ -2104,30 +939,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2144,29 +955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.116", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.116", + "syn", ] [[package]] @@ -2179,15 +968,13 @@ dependencies = [ ] [[package]] -name = "pulldown-cmark" -version = "0.9.6" +name = "psm" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" dependencies = [ - "bitflags 2.11.0", - "getopts", - "memchr", - "unicase", + "ar_archive_writer", + "cc", ] [[package]] @@ -2199,95 +986,30 @@ dependencies = [ "bmp-monochrome", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.1", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash 2.1.1", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" -version = "1.0.44" +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 = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", + "rand_chacha", + "rand_core", ] [[package]] @@ -2297,17 +1019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", + "rand_core", ] [[package]] @@ -2320,42 +1032,26 @@ dependencies = [ ] [[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" +name = "regex" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ - "bitflags 2.11.0", + "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]] @@ -2366,63 +1062,20 @@ checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.10", ] -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] -name = "reqwest" -version = "0.12.28" +name = "regex-syntax" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 1.0.6", -] +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "ring" @@ -2438,120 +1091,51 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rust-embed" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn 2.0.116", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" -dependencies = [ - "sha2", - "walkdir", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.36" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "once_cell", "ring", - "rustls-pki-types", "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", + "sct", ] [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring", - "rustls-pki-types", "untrusted", ] @@ -2561,21 +1145,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - [[package]] name = "same-file" version = "1.0.6" @@ -2595,20 +1164,13 @@ dependencies = [ ] [[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "scrypt" -version = "0.11.0" +name = "sct" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "pbkdf2", - "salsa20", - "sha2", + "ring", + "untrusted", ] [[package]] @@ -2618,7 +1180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "bitcoin_hashes", - "rand 0.8.5", + "rand", "secp256k1-sys", "serde", ] @@ -2639,51 +1201,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a44aed3002b5ae975f8624c5df3a949cfbf00479e18778b6058fcd213b76e3" dependencies = [ "bitcoin-private", - "rand 0.8.5", + "rand", "secp256k1", "secp256k1-zkp-sys", "serde", ] [[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 = "secrecy" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" -dependencies = [ - "zeroize", -] - -[[package]] -name = "self_cell" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" -dependencies = [ - "self_cell 1.2.2", -] - -[[package]] -name = "self_cell" -version = "1.2.2" +name = "secp256k1-zkp-sys" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" +checksum = "57f08b2d0b143a22e07f798ae4f0ab20d5590d7c68e0d090f2088a48a21d1654" +dependencies = [ + "cc", + "secp256k1-sys", +] [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -2712,7 +1250,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn", ] [[package]] @@ -2729,37 +1267,12 @@ dependencies = [ ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" -dependencies = [ - "serde", - "serde_with_macros", -] - -[[package]] -name = "serde_with_macros" -version = "1.5.2" +name = "serde_spanned" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 1.0.109", + "serde_core", ] [[package]] @@ -2773,15 +1286,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shlex" version = "1.3.0" @@ -2797,7 +1301,7 @@ dependencies = [ "bitcoin", "bitcoin_hashes", "byteorder", - "elements 0.25.2", + "elements", "getrandom 0.2.17", "ghost-cell", "hex-conservative", @@ -2808,9 +1312,9 @@ dependencies = [ [[package]] name = "simplicity-sys" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bcb4e5bfc15080d67e0ce2c17d1c31bfb7521d65c86ea26ed0de72d5119d119" +checksum = "e3401ee7331f183a5458c0f5a4b3d5d00bde0fd12e2e03728c537df34efae289" dependencies = [ "bitcoin_hashes", "cc", @@ -2818,456 +1322,242 @@ dependencies = [ [[package]] name = "simplicityhl" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa7477fc9bfef4cc53ae969db00539f0e67af38156822ac79662513d04f6fee" +checksum = "25de8990174fe3e1a843df138cacc4265d05839ebd2550c18b9196f567d55e81" dependencies = [ "base64 0.21.7", + "chumsky", "clap", "either", "getrandom 0.2.17", "itertools", "miniscript", - "pest", - "pest_derive", "serde", "serde_json", "simplicity-lang", ] [[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot 0.11.2", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.116" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +name = "smplx-build" +version = "0.0.4" +source = "git+https://github.com/BlockstreamResearch/smplx?branch=dev#3330f15249234ab91e3243443de81fc1a5b19ae7" dependencies = [ + "glob", + "globwalk", + "pathdiff", + "prettyplease", "proc-macro2", "quote", - "syn 2.0.116", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags 2.11.0", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", + "serde", + "simplicityhl", + "syn", + "thiserror", + "toml", ] [[package]] -name = "tempfile" -version = "3.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +name = "smplx-macros" +version = "0.0.4" +source = "git+https://github.com/BlockstreamResearch/smplx?branch=dev#3330f15249234ab91e3243443de81fc1a5b19ae7" dependencies = [ - "fastrand", - "getrandom 0.4.1", - "once_cell", - "rustix 1.1.3", - "windows-sys 0.61.2", + "smplx-build", + "smplx-test", + "syn", ] [[package]] -name = "testcontainers" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e2b1567ca8a2b819ea7b28c92be35d9f76fb9edb214321dcc86eb96023d1f87" +name = "smplx-regtest" +version = "0.0.4" +source = "git+https://github.com/BlockstreamResearch/smplx?branch=dev#3330f15249234ab91e3243443de81fc1a5b19ae7" dependencies = [ - "bollard-stubs", - "futures", + "electrsd", "hex", "hmac", - "log", - "rand 0.8.5", "serde", - "serde_json", "sha2", + "smplx-sdk", + "thiserror", + "toml", ] [[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +name = "smplx-sdk" +version = "0.0.4" +source = "git+https://github.com/BlockstreamResearch/smplx?branch=dev#3330f15249234ab91e3243443de81fc1a5b19ae7" dependencies = [ - "thiserror-impl 2.0.18", + "bip39", + "bitcoin_hashes", + "dyn-clone", + "electrsd", + "elements-miniscript", + "hex", + "minreq", + "serde", + "serde_json", + "sha2", + "simplicityhl", + "thiserror", ] [[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +name = "smplx-std" +version = "0.0.4" +source = "git+https://github.com/BlockstreamResearch/smplx?branch=dev#3330f15249234ab91e3243443de81fc1a5b19ae7" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", + "either", + "serde", + "simplicityhl", + "smplx-macros", + "smplx-sdk", + "smplx-test", ] [[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +name = "smplx-test" +version = "0.0.4" +source = "git+https://github.com/BlockstreamResearch/smplx?branch=dev#3330f15249234ab91e3243443de81fc1a5b19ae7" dependencies = [ + "electrsd", "proc-macro2", "quote", - "syn 2.0.116", + "serde", + "simplicityhl", + "smplx-regtest", + "smplx-sdk", + "syn", + "thiserror", + "toml", ] [[package]] -name = "thread_local" -version = "1.1.9" +name = "stacker" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" dependencies = [ + "cc", "cfg-if", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "serde_core", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", + "psm", "windows-sys 0.61.2", ] [[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags 2.11.0", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "tower-service" -version = "0.3.3" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "tracing" -version = "0.1.44" +name = "syn" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ - "pin-project-lite", - "tracing-core", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "tracing-core" -version = "0.1.36" +name = "tempfile" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ + "fastrand", + "getrandom 0.4.2", "once_cell", - "valuable", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] -name = "tracing-log" -version = "0.2.0" +name = "thiserror" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "log", - "once_cell", - "tracing-core", + "thiserror-impl", ] [[package]] -name = "tracing-subscriber" -version = "0.3.22" +name = "thiserror-impl" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "type-map" -version = "0.5.1" +name = "tinyvec" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ - "rustc-hash 2.1.1", + "tinyvec_macros", ] [[package]] -name = "typenum" -version = "1.19.0" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "ucd-trie" -version = "0.1.7" +name = "toml" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] [[package]] -name = "unic-langid" -version = "0.9.6" +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "unic-langid-impl", + "serde_core", ] [[package]] -name = "unic-langid-impl" -version = "0.9.6" +name = "toml_parser" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "serde", - "tinystr", + "winnow 1.0.2", ] [[package]] -name = "unicase" -version = "2.9.0" +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "typenum" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" @@ -3285,10 +1575,10 @@ dependencies = [ ] [[package]] -name = "unicode-width" -version = "0.2.2" +name = "unicode-segmentation" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-xid" @@ -3312,54 +1602,18 @@ 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 = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "version_check" version = "0.9.5" 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" @@ -3370,34 +1624,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wallet-abi" -version = "0.1.0" -dependencies = [ - "bincode", - "hex", - "lwk_common", - "lwk_signer", - "lwk_simplicity", - "lwk_wollet", - "ring", - "serde", - "serde_json", - "sha2", - "simplicityhl", - "thiserror 2.0.18", - "tokio", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3406,11 +1632,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -3419,14 +1645,14 @@ 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", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -3435,25 +1661,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3461,22 +1673,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.116", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -3509,47 +1721,18 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", ] -[[package]] -name = "web-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "webpki-roots" version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "which" version = "4.4.2" @@ -3562,22 +1745,6 @@ dependencies = [ "rustix 0.38.44", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.11" @@ -3587,98 +1754,28 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -3696,31 +1793,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -3729,84 +1809,42 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3814,10 +1852,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "winnow" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "wit-bindgen" @@ -3828,6 +1872,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -3849,7 +1899,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.116", + "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3865,7 +1915,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.116", + "syn", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3877,7 +1927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.11.1", "indexmap", "log", "serde", @@ -3907,86 +1957,24 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "x25519-dalek" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" -dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "serde", - "zeroize", -] - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", - "synstructure", -] - [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", - "synstructure", + "syn", ] [[package]] @@ -3994,54 +1982,6 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "serde", - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", -] [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index d1e115e..17e773d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,7 @@ [workspace] resolver = "3" members = [ - "crates/cli", "crates/contracts", - "crates/wallet-abi", ] [workspace.package] @@ -14,18 +12,13 @@ edition = "2024" multiple_crate_versions = "allow" [workspace.dependencies] -ring = "0.17.14" -sha2 = { version = "0.10.9", features = ["compress"] } +serde = "1" +serde_json = "1" -hex = "0.4.3" tracing = { version = "0.1.41" } minreq = { version = "2.14.1", features = ["https", "json-using-serde"]} -simplicityhl = { version = "0.4.0" } +smplx-std = { git = "https://github.com/BlockstreamResearch/smplx", branch = "dev" } -[patch.crates-io] -lwk_common = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_common" } -lwk_signer = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_signer" } -lwk_simplicity = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_simplicity" } -lwk_wollet = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_wollet" } +lwk_common = "0.17.0" \ No newline at end of file diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..bcebf47 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +too-many-lines-threshold = 250 diff --git a/crates/cli/.env.example b/crates/cli/.env.example deleted file mode 100644 index 9333b00..0000000 --- a/crates/cli/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# Optional mnemonic consumed by `--mnemonic` / `MNEMONIC`. -MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - -# Optional wallet data root override. -# SIMPLICITY_CLI_WALLET_DATA_DIR=".cache/wallet" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml deleted file mode 100644 index ebabc76..0000000 --- a/crates/cli/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "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 - -[[bin]] -name = "simplicity-cli" -path = "src/main.rs" - -[lints] -workspace = true - -[dependencies] -anyhow = "1" -dotenvy = "0.15.7" - -sled = "0.34.7" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -clap = { version = "4", features = ["derive", "env"] } - -tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "sync"] } - -hex = { workspace = true } - -wallet-abi = { path = "../wallet-abi" } -contracts = { path = "../contracts" } - -simplicityhl = { workspace = true } - -lwk_common = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_common", default-features = false } diff --git a/crates/cli/README.md b/crates/cli/README.md deleted file mode 100644 index 3d1f576..0000000 --- a/crates/cli/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Simplicity Contracts CLI - -This crate provides `simplicity-cli`, a helper CLI for basic wallet flows and option-offer flows. - -## Command Topology - -- `basic ...` -- `option-offer ...` - -`--network` is required for every command invocation. - -Use `--help` at each level: - -```bash -cargo run -p cli -- --help -cargo run -p cli -- --network testnet-liquid basic --help -cargo run -p cli -- --network testnet-liquid option-offer --help -``` - -Top-level command groups currently exposed: - -- `basic`: address/balance/transfer/split/issue/reissue flows -- `option-offer`: create/import/export/exercise/withdraw/expiry flows - -## Runtime Configuration - -Current behavior: - -- `--network` is required on every invocation. - - supported: `liquid`, `testnet-liquid`, `localtest-liquid` -- `--mnemonic` is optional and also supports env var `MNEMONIC`. -- if mnemonic is not provided, a built-in test mnemonic is used. -- Esplora URL is derived from network in code (no env override in current build): - - `liquid` -> `https://blockstream.info/liquid/api` - - `testnet-liquid` -> `https://blockstream.info/liquidtestnet/api` - - `localtest-liquid` -> `http://127.0.0.1:3001` -- wallet data dir defaults to `.cache/wallet` and can be overridden with `SIMPLICITY_CLI_WALLET_DATA_DIR`. - -Example: - -```bash -export MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" -cargo run -p cli -- --network localtest-liquid basic address -``` - -## Local Store Behavior - -Option-offer arguments are stored in `.cache/store`. - -- `option-offer create` rejects duplicate keys. -- `option-offer import` overwrites existing keys. -- `option-offer export` reads by key. - -Import/export examples: - -```bash -cargo run -p cli -- --network testnet-liquid option-offer import \ - --option-offer-taproot-pubkey-gen \ - --encoded-option-offer-arguments - -cargo run -p cli -- --network testnet-liquid option-offer export \ - --option-offer-taproot-pubkey-gen -``` - -## License - -Dual-licensed under either of: - -- Apache License, Version 2.0 (Apache-2.0) -- MIT license (MIT) - -at your option. diff --git a/crates/cli/src/commands/basic.rs b/crates/cli/src/commands/basic.rs deleted file mode 100644 index 03cfd77..0000000 --- a/crates/cli/src/commands/basic.rs +++ /dev/null @@ -1,426 +0,0 @@ -use crate::modules::store::Store; - -use crate::modules::utils::execute_request; - -use anyhow::{Context, anyhow}; - -use clap::Subcommand; - -use simplicityhl::elements::encode; -use simplicityhl::elements::hashes::sha256::Midstate; -use simplicityhl::elements::hex::ToHex; -use simplicityhl::elements::pset::serialize::Serialize; -use simplicityhl::elements::{Address, AssetId, Sequence, Transaction}; - -use wallet_abi::runtime::WalletRuntimeConfig; -use wallet_abi::schema::tx_create::{TX_CREATE_ABI_VERSION, TxCreateRequest}; -use wallet_abi::taproot_pubkey_gen::get_random_seed; -use wallet_abi::{ - AmountFilter, AssetFilter, AssetVariant, BlinderVariant, FinalizerSpec, InputBlinder, - InputIssuance, InputIssuanceKind, InputSchema, LockFilter, LockVariant, OutputSchema, - RuntimeParams, UTXOSource, WalletSourceFilter, get_new_asset_entropy, -}; - -fn decode_transaction(tx_hex: &str) -> anyhow::Result { - let tx_bytes = hex::decode(tx_hex).context("failed to decode transaction hex")?; - encode::deserialize(&tx_bytes).context("failed to decode transaction bytes") -} - -#[derive(Subcommand, Debug)] -pub enum Basic { - /// Print a deterministic address - Address, - /// Print wallet balances grouped by asset id - Balance, - /// Build tx transferring an asset to recipient - Transfer { - /// Recipient Liquid address - #[arg(long = "to-address")] - to_address: Address, - /// Asset to send, LBTC by default - #[arg(long = "asset")] - asset: Option, - /// Amount to send to the recipient in satoshis - #[arg(long = "send-sats")] - amount_to_send: u64, - /// When set, broadcast the built transaction via Esplora and print txid - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// Build tx splitting funds into multiple outputs. - Split { - /// Asset to split, LBTC by default - #[arg(long = "asset")] - asset: Option, - /// Number of UTXOs to split the UTXO into - #[arg(long = "split-parts")] - split_parts: u64, - /// Value of single split - #[arg(long = "part-amount")] - part_amount: u64, - /// When set, broadcast the built transaction via Esplora and print txid - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// Build tx issuing an asset - IssueAsset { - /// Asset name (this will be stored in the CLI's database only, so it will be not shown on the Esplora UI) - #[arg(long = "asset-name")] - asset_name: String, - /// Amount to issue of the asset in its satoshi units - #[arg(long = "issue-sats")] - issue_amount: u64, - /// When set, broadcast the built transaction via Esplora and print txid - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// Reissue an asset - ReissueAsset { - /// Asset name (this will be stored in the CLI's database only, so it will be not shown on the Esplora UI) - #[arg(long = "asset-name")] - asset_name: String, - /// Amount to reissue of the asset in its satoshi units - #[arg(long = "reissue-sats")] - reissue_amount: u64, - /// When set, broadcast the built transaction via Esplora and print txid - #[arg(long = "broadcast")] - broadcast: bool, - }, -} - -impl Basic { - /// Handle basic CLI subcommand execution. - /// - /// # Errors - /// Returns error if the subcommand operation fails. - /// - /// # Panics - /// Panics if asset entropy conversion fails. - #[expect(clippy::too_many_lines)] - pub async fn handle(&self, runtime: WalletRuntimeConfig) -> anyhow::Result<()> { - let mut runtime = runtime; - - match self { - Self::Address => { - let receiver_address = runtime.signer_receive_address()?; - - let signer_address = runtime.signer_x_only_public_key()?; - - println!("Receiver Address: {receiver_address}"); - println!("Signer X Only Public Key: {signer_address}"); - - Ok(()) - } - Self::Balance => { - runtime.sync_wallet().await?; - - let mut balances: std::collections::BTreeMap = - std::collections::BTreeMap::new(); - for utxo in runtime.wollet.utxos()? { - let entry = balances.entry(utxo.unblinded.asset).or_insert(0); - *entry = entry - .checked_add(utxo.unblinded.value) - .ok_or_else(|| anyhow!("balance overflow while summing wallet UTXOs"))?; - } - - if balances.is_empty() { - println!("No available assets"); - return Ok(()); - } - - for (asset_id, amount_sat) in balances { - println!("{asset_id}: {amount_sat}"); - } - - Ok(()) - } - Self::Transfer { - asset, - to_address, - amount_to_send, - broadcast, - } => { - let asset_to_send = asset.unwrap_or(*runtime.network.policy_asset()); - - let blinder = to_address - .blinding_pubkey - .map_or(BlinderVariant::Explicit, |blinder| { - BlinderVariant::Provided { pubkey: blinder } - }); - - let request = TxCreateRequest { - abi_version: TX_CREATE_ABI_VERSION.to_string(), - request_id: "request-basic.transfer".to_string(), - network: runtime.network, - params: RuntimeParams { - inputs: vec![InputSchema { - id: "input0".to_string(), - utxo_source: UTXOSource::Wallet { - filter: WalletSourceFilter { - asset: AssetFilter::Exact { - asset_id: asset_to_send, - }, - amount: AmountFilter::default(), - lock: LockFilter::default(), - }, - }, - blinder: InputBlinder::default(), - sequence: Sequence::default(), - issuance: None, - finalizer: FinalizerSpec::default(), - }], - outputs: vec![OutputSchema { - id: "to-recipient".to_string(), - amount_sat: *amount_to_send, - lock: LockVariant::Script { - script: to_address.script_pubkey(), - }, - asset: AssetVariant::AssetId { - asset_id: asset_to_send, - }, - blinder, - }], - fee_rate_sat_vb: Some(0.1), - locktime: None, - }, - broadcast: *broadcast, - }; - - let _ = execute_request(&mut runtime, request).await?; - - Ok(()) - } - Self::Split { - asset, - split_parts, - part_amount, - broadcast, - } => { - let asset_to_split = asset.unwrap_or(*runtime.network.policy_asset()); - - if *split_parts == 0 { - return Err(anyhow!("split-parts must be > 0")); - } - - let signer_address = runtime.signer_receive_address()?; - - let mut outputs = Vec::new(); - for output_index in 0..*split_parts { - outputs.push(OutputSchema { - id: format!("out{output_index}"), - amount_sat: *part_amount, - lock: LockVariant::Script { - script: signer_address.script_pubkey(), - }, - asset: AssetVariant::AssetId { - asset_id: asset_to_split, - }, - blinder: BlinderVariant::Wallet, - }); - } - - let request = TxCreateRequest { - abi_version: TX_CREATE_ABI_VERSION.to_string(), - request_id: "request-basic.split_native".to_string(), - network: runtime.network, - params: RuntimeParams { - inputs: vec![InputSchema { - id: "input0".to_string(), - utxo_source: UTXOSource::Wallet { - filter: WalletSourceFilter { - asset: AssetFilter::Exact { - asset_id: asset_to_split, - }, - amount: AmountFilter::default(), - lock: LockFilter::default(), - }, - }, - blinder: InputBlinder::default(), - sequence: Sequence::default(), - issuance: None, - finalizer: FinalizerSpec::default(), - }], - outputs, - fee_rate_sat_vb: Some(0.1), - locktime: None, - }, - broadcast: *broadcast, - }; - - let _ = execute_request(&mut runtime, request).await?; - - Ok(()) - } - Self::IssueAsset { - asset_name, - issue_amount, - broadcast, - } => { - let store = Store::load()?; - - if store.store.get(asset_name)?.is_some() { - return Err(anyhow!("Asset name already exists")); - } - let issuance_entropy = get_random_seed(); - - let policy_asset = *runtime.network.policy_asset(); - let signer_script = runtime.signer_receive_address()?.script_pubkey(); - - let request = TxCreateRequest { - abi_version: TX_CREATE_ABI_VERSION.to_string(), - request_id: "request-basic.issue_asset".to_string(), - network: runtime.network, - params: RuntimeParams { - inputs: vec![InputSchema { - id: "in0".to_string(), - utxo_source: UTXOSource::Wallet { - filter: WalletSourceFilter { - // TODO: really? - asset: AssetFilter::Exact { - asset_id: policy_asset, - }, - amount: AmountFilter::default(), - lock: LockFilter::default(), - }, - }, - blinder: InputBlinder::default(), - sequence: Sequence::default(), - issuance: Some(InputIssuance { - kind: InputIssuanceKind::New, - asset_amount_sat: *issue_amount, - token_amount_sat: 1, - entropy: issuance_entropy, - }), - finalizer: FinalizerSpec::default(), - }], - outputs: vec![ - OutputSchema { - id: "out0".to_string(), - amount_sat: 1, - lock: LockVariant::Script { - script: signer_script.clone(), - }, - asset: AssetVariant::NewIssuanceToken { input_index: 0 }, - blinder: BlinderVariant::Wallet, - }, - OutputSchema { - id: "out1".to_string(), - amount_sat: *issue_amount, - lock: LockVariant::Script { - script: signer_script, - }, - asset: AssetVariant::NewIssuanceAsset { input_index: 0 }, - blinder: BlinderVariant::Wallet, - }, - ], - fee_rate_sat_vb: Some(0.1), - locktime: None, - }, - broadcast: *broadcast, - }; - - let tx_info = execute_request(&mut runtime, request).await?; - - let tx = decode_transaction(&tx_info.tx_hex)?; - let input = tx - .input - .first() - .ok_or_else(|| anyhow!("issued transaction is missing input[0]"))?; - let (asset_id, reissuance_asset_id) = input.issuance_ids(); - - let asset_entropy = get_new_asset_entropy(&input.previous_output, issuance_entropy); - - println!( - "Asset id: {asset_id}, Reissuance asset: {reissuance_asset_id}, Asset entropy: {}", - asset_entropy.to_hex() - ); - - store - .store - .insert(asset_name, &asset_entropy.to_byte_array())?; - - store - .store - .insert(format!("re-{asset_name}"), reissuance_asset_id.serialize())?; - - Ok(()) - } - Self::ReissueAsset { - asset_name, - reissue_amount, - broadcast, - } => { - let store = Store::load()?; - - let Some(asset_entropy) = store.store.get(asset_name)? else { - return Err(anyhow!("Asset name not found")); - }; - let Some(reissue_token_id) = store.store.get(format!("re-{asset_name}"))? else { - return Err(anyhow!("Asset name not found")); - }; - let asset_entropy = Midstate::from_slice(&asset_entropy)?; - let reissue_token_id = AssetId::from_slice(&reissue_token_id)?; - - let signer_script = runtime.signer_receive_address()?.script_pubkey(); - - let request = TxCreateRequest { - abi_version: TX_CREATE_ABI_VERSION.to_string(), - request_id: "request-basic.reissue_asset".to_string(), - network: runtime.network, - params: RuntimeParams { - inputs: vec![InputSchema { - id: "input0".to_string(), - utxo_source: UTXOSource::Wallet { - filter: WalletSourceFilter { - asset: AssetFilter::Exact { - asset_id: reissue_token_id, - }, - amount: AmountFilter::Min { satoshi: 1 }, - lock: LockFilter::None, - }, - }, - blinder: InputBlinder::default(), - sequence: Sequence::default(), - issuance: Some(InputIssuance { - kind: InputIssuanceKind::Reissue, - asset_amount_sat: *reissue_amount, - token_amount_sat: 0, - entropy: asset_entropy.to_byte_array(), - }), - finalizer: FinalizerSpec::default(), - }], - outputs: vec![ - OutputSchema { - id: "out0".to_string(), - amount_sat: 1, - lock: LockVariant::Script { - script: signer_script.clone(), - }, - asset: AssetVariant::AssetId { - asset_id: reissue_token_id, - }, - blinder: BlinderVariant::Wallet, - }, - OutputSchema { - id: "out1".to_string(), - amount_sat: *reissue_amount, - lock: LockVariant::Script { - script: signer_script, - }, - asset: AssetVariant::ReIssuanceAsset { input_index: 0 }, - blinder: BlinderVariant::Wallet, - }, - ], - fee_rate_sat_vb: Some(0.1), - locktime: None, - }, - broadcast: *broadcast, - }; - - let _ = execute_request(&mut runtime, request).await?; - - Ok(()) - } - } - } -} diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs deleted file mode 100644 index df32184..0000000 --- a/crates/cli/src/commands/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod basic; -pub mod option_offer; -// pub mod options; -// pub mod smt_storage; diff --git a/crates/cli/src/commands/option_offer.rs b/crates/cli/src/commands/option_offer.rs deleted file mode 100644 index eb336ff..0000000 --- a/crates/cli/src/commands/option_offer.rs +++ /dev/null @@ -1,513 +0,0 @@ -#![allow(clippy::missing_errors_doc)] - -use std::convert::TryFrom; -use std::future::Future; - -use crate::modules::store::Store; -use crate::modules::utils::execute_request; - -use anyhow::{Context, anyhow}; -use clap::Subcommand; -use contracts::option_offer::{OptionOfferArguments, OptionOfferRuntime, get_option_offer_address}; -use simplicityhl::elements::{AssetId, OutPoint, Script, Txid}; -use wallet_abi::Encodable; -use wallet_abi::runtime::WalletRuntimeConfig; -use wallet_abi::schema::tx_create::TxCreateRequest; -use wallet_abi::taproot_pubkey_gen::TaprootPubkeyGen; - -/// Option-offer contract utilities. -#[derive(Subcommand, Debug)] -pub enum OptionOffer { - /// Create, store, and fund a new option-offer contract in one command. - Create { - /// Collateral asset id. - #[arg(long = "collateral-asset-id")] - collateral_asset_id: AssetId, - /// Premium asset id. - #[arg(long = "premium-asset-id")] - premium_asset_id: AssetId, - /// Settlement asset id. - #[arg(long = "settlement-asset-id")] - settlement_asset_id: AssetId, - /// Expected collateral amount user will deposit. - #[arg(long = "expected-to-deposit-collateral")] - expected_to_deposit_collateral: u64, - /// Expected premium amount user will deposit. - #[arg(long = "expected-to-deposit-premium")] - expected_to_deposit_premium: u64, - /// Expected settlement amount user will get on exercise. - #[arg(long = "expected-to-get-settlement")] - expected_to_get_settlement: u64, - /// Unix timestamp after which expiry path becomes valid. - #[arg(long = "expiry-time")] - expiry_time: u32, - /// When set, broadcast the built transaction via Esplora and print txid. - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// Import option-offer arguments into local store. - Import { - /// Option-offer taproot pubkey gen handle used as local store key. - #[arg(long = "option-offer-taproot-pubkey-gen")] - option_offer_taproot_pubkey_gen: String, - /// Encoded option-offer arguments (hex). - #[arg(long = "encoded-option-offer-arguments")] - encoded_option_offer_arguments: String, - }, - /// Export option-offer arguments from local store. - Export { - /// Option-offer taproot pubkey gen handle used as local store key. - #[arg(long = "option-offer-taproot-pubkey-gen")] - option_offer_taproot_pubkey_gen: String, - }, - /// Exercise path: swap settlement asset for collateral and premium. - Exercise { - /// Option-offer taproot pubkey gen handle. - #[arg(long = "option-offer-taproot-pubkey-gen")] - option_offer_taproot_pubkey_gen: String, - /// Creation txid containing covenant outputs (collateral at vout=0, premium at vout=1). - #[arg(long = "creation-txid")] - creation_tx_id: Txid, - /// Collateral amount to receive from covenant. - #[arg(long = "collateral-amount")] - collateral_amount: u64, - /// When set, broadcast the built transaction via Esplora and print txid. - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// Withdraw settlement from covenant after exercise. - Withdraw { - /// Option-offer taproot pubkey gen handle. - #[arg(long = "option-offer-taproot-pubkey-gen")] - option_offer_taproot_pubkey_gen: String, - /// Exercise txid that produced settlement output in covenant. - #[arg(long = "exercise-txid")] - exercise_tx_id: Txid, - /// When set, broadcast the built transaction via Esplora and print txid. - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// Expiry path: reclaim collateral and premium after expiry time. - Expiry { - /// Option-offer taproot pubkey gen handle. - #[arg(long = "option-offer-taproot-pubkey-gen")] - option_offer_taproot_pubkey_gen: String, - /// Creation txid containing covenant outputs (collateral at vout=0, premium at vout=1). - #[arg(long = "creation-txid")] - creation_tx_id: Txid, - /// When set, broadcast the built transaction via Esplora and print txid. - #[arg(long = "broadcast")] - broadcast: bool, - }, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct CandidateOutput { - vout: u32, - script_pubkey: Script, - asset_id: Option, - value_sat: Option, -} - -struct OptionOfferContext { - runtime: OptionOfferRuntime, -} - -impl OptionOfferContext { - const fn new(runtime: OptionOfferRuntime) -> Self { - Self { runtime } - } - - fn load(runtime: WalletRuntimeConfig, taproot_pubkey_gen: &str) -> anyhow::Result { - let store = Store::load().context("failed to open local store")?; - let args = store - .get_arguments::(taproot_pubkey_gen) - .with_context(|| { - format!("failed to load option-offer arguments for key '{taproot_pubkey_gen}'") - })?; - - let tap = TaprootPubkeyGen::build_from_str( - taproot_pubkey_gen, - &args, - runtime.network, - &get_option_offer_address, - ) - .with_context(|| format!("invalid option-offer taproot handle '{taproot_pubkey_gen}'"))?; - - let signer_pubkey = runtime.signer_x_only_public_key()?.serialize(); - if signer_pubkey != args.user_pubkey() { - return Err(anyhow!( - "signer x-only pubkey mismatch: signer={}, args.user_pubkey={}", - hex::encode(signer_pubkey), - hex::encode(args.user_pubkey()) - )); - } - - Ok(Self::new(OptionOfferRuntime::new(runtime, args, tap))) - } - - const fn runtime_mut(&mut self) -> &mut WalletRuntimeConfig { - self.runtime.runtime_mut() - } - - fn taproot_pubkey_gen(&self) -> String { - self.runtime.tap().to_string() - } - - fn build_deposit_request( - &self, - collateral_deposit_amount: u64, - broadcast: bool, - ) -> TxCreateRequest { - let mut request = self - .runtime - .build_deposit_request(collateral_deposit_amount); - request.broadcast = broadcast; - request - } - - async fn build_exercise_request( - &self, - creation_tx_id: Txid, - collateral_amount: u64, - broadcast: bool, - ) -> anyhow::Result { - let mut request = self - .runtime - .build_exercise_request(creation_tx_id, collateral_amount) - .await?; - request.broadcast = broadcast; - Ok(request) - } - - async fn build_withdraw_request( - &self, - exercise_tx_id: Txid, - broadcast: bool, - ) -> anyhow::Result { - let tx = { - let inner_esplora = self.runtime.runtime().esplora.lock().await; - inner_esplora.get_transaction(exercise_tx_id).await? - }; - - let covenant_script = self.runtime.tap().address.script_pubkey(); - let settlement_asset_id = self.runtime.args().get_settlement_asset_id(); - - let outputs = tx - .output - .iter() - .enumerate() - .filter(|(_, tx_out)| tx_out.script_pubkey.eq(&covenant_script)) - .map( - |(vout, tx_out)| -> anyhow::Result> { - if tx_out.asset.is_confidential() { - return Ok(None); - } - - let asset_id = tx_out.asset.explicit().ok_or_else(|| { - anyhow!( - "exercise transaction output at vout={vout} has non-explicit asset id" - ) - })?; - - if asset_id != settlement_asset_id { - return Ok(None); - } - - Ok(Some(CandidateOutput { - vout: u32::try_from(vout).context("exercise transaction vout overflow")?, - script_pubkey: tx_out.script_pubkey.clone(), - asset_id: Some(asset_id), - value_sat: tx_out.value.explicit(), - })) - }, - ) - .collect::>>()?; - - let outputs: Vec<_> = outputs.into_iter().flatten().collect(); - - let (settlement_vout, settlement_amount) = - select_settlement_output_for_withdraw(&outputs, &covenant_script, settlement_asset_id)?; - - let mut request = self.runtime.build_withdraw_request_for_outpoint( - OutPoint::new(exercise_tx_id, settlement_vout), - settlement_amount, - )?; - request.broadcast = broadcast; - - Ok(request) - } - - async fn build_expiry_request( - &self, - creation_tx_id: Txid, - broadcast: bool, - ) -> anyhow::Result { - let collateral_outpoint = OutPoint::new(creation_tx_id, 0); - let collateral_tx_out = self - .runtime - .runtime() - .fetch_tx_out(&collateral_outpoint) - .await?; - ensure_explicit_asset( - Option::from(&collateral_tx_out.asset.explicit()), - self.runtime.args().get_collateral_asset_id(), - "covenant collateral output", - )?; - let collateral_amount = explicit_amount( - collateral_tx_out.value.explicit(), - "covenant collateral output", - )?; - - let premium_outpoint = OutPoint::new(creation_tx_id, 1); - let premium_tx_out = self - .runtime - .runtime() - .fetch_tx_out(&premium_outpoint) - .await?; - ensure_explicit_asset( - Option::from(&premium_tx_out.asset.explicit()), - self.runtime.args().get_premium_asset_id(), - "covenant premium output", - )?; - let premium_amount = - explicit_amount(premium_tx_out.value.explicit(), "covenant premium output")?; - - let mut request = - self.runtime - .build_expiry_request(creation_tx_id, collateral_amount, premium_amount)?; - request.broadcast = broadcast; - - Ok(request) - } -} - -impl OptionOffer { - #[allow(clippy::too_many_lines)] - pub async fn handle(&self, runtime: WalletRuntimeConfig) -> anyhow::Result<()> { - match self { - Self::Create { - collateral_asset_id, - premium_asset_id, - settlement_asset_id, - expected_to_deposit_collateral, - expected_to_deposit_premium, - expected_to_get_settlement, - expiry_time, - broadcast, - } => { - let (collateral_per_contract, premium_per_collateral) = - derive_contract_terms_from_expected_amounts( - *expected_to_deposit_collateral, - *expected_to_deposit_premium, - *expected_to_get_settlement, - )?; - - let user_pubkey = runtime.signer_x_only_public_key()?.serialize(); - let args = OptionOfferArguments::new( - *collateral_asset_id, - *premium_asset_id, - *settlement_asset_id, - collateral_per_contract, - premium_per_collateral, - *expiry_time, - user_pubkey, - ); - - let tap = TaprootPubkeyGen::from(&args, runtime.network, &get_option_offer_address) - .context("failed to derive option-offer taproot handle")?; - let taproot_pubkey_gen = tap.to_string(); - let encoded = args.encode()?; - - let store = Store::load().context("failed to open local store")?; - if store.store.get(&taproot_pubkey_gen)?.is_some() { - return Err(anyhow!( - "option-offer key already exists in store: {taproot_pubkey_gen}" - )); - } - - let mut context = - OptionOfferContext::new(OptionOfferRuntime::new(runtime, args, tap)); - let request = - context.build_deposit_request(*expected_to_deposit_collateral, *broadcast); - execute_then_store_option_offer(&store, &taproot_pubkey_gen, &encoded, || async { - let _ = execute_request(context.runtime_mut(), request).await?; - Ok(()) - }) - .await?; - - println!( - "Option-offer taproot pubkey gen: {}", - context.taproot_pubkey_gen() - ); - println!("Option-offer address: {}", context.runtime.tap().address); - println!("Encoded option-offer arguments: {}", hex::encode(encoded)); - println!("Derived collateral-per-contract: {collateral_per_contract}"); - println!("Derived premium-per-collateral: {premium_per_collateral}"); - println!("Expiry-time: {expiry_time}"); - - Ok(()) - } - Self::Import { - option_offer_taproot_pubkey_gen, - encoded_option_offer_arguments, - } => Store::load()?.import_arguments::( - option_offer_taproot_pubkey_gen, - encoded_option_offer_arguments, - runtime.network, - &get_option_offer_address, - ), - Self::Export { - option_offer_taproot_pubkey_gen, - } => { - println!( - "{}", - Store::load()?.export_arguments(option_offer_taproot_pubkey_gen)? - ); - Ok(()) - } - Self::Exercise { - option_offer_taproot_pubkey_gen, - creation_tx_id, - collateral_amount, - broadcast, - } => { - let mut context = - OptionOfferContext::load(runtime, option_offer_taproot_pubkey_gen)?; - let request = context - .build_exercise_request(*creation_tx_id, *collateral_amount, *broadcast) - .await?; - let _ = execute_request(context.runtime_mut(), request).await?; - Ok(()) - } - Self::Withdraw { - option_offer_taproot_pubkey_gen, - exercise_tx_id, - broadcast, - } => { - let mut context = - OptionOfferContext::load(runtime, option_offer_taproot_pubkey_gen)?; - let request = context - .build_withdraw_request(*exercise_tx_id, *broadcast) - .await?; - let _ = execute_request(context.runtime_mut(), request).await?; - Ok(()) - } - Self::Expiry { - option_offer_taproot_pubkey_gen, - creation_tx_id, - broadcast, - } => { - let mut context = - OptionOfferContext::load(runtime, option_offer_taproot_pubkey_gen)?; - let request = context - .build_expiry_request(*creation_tx_id, *broadcast) - .await?; - let _ = execute_request(context.runtime_mut(), request).await?; - Ok(()) - } - } - } -} - -fn checked_div_exact_u64(numerator: u64, denominator: u64, label: &str) -> anyhow::Result { - if denominator == 0 { - return Err(anyhow!("{label} denominator must be > 0")); - } - if !numerator.is_multiple_of(denominator) { - return Err(anyhow!( - "{label} must divide exactly: numerator={numerator}, denominator={denominator}" - )); - } - Ok(numerator / denominator) -} - -async fn execute_then_store_option_offer( - store: &Store, - taproot_pubkey_gen: &str, - encoded: &[u8], - execute: Exec, -) -> anyhow::Result<()> -where - Exec: FnOnce() -> ExecFuture, - ExecFuture: Future>, -{ - execute().await?; - - store - .store - .insert(taproot_pubkey_gen, encoded) - .with_context(|| { - format!( - "transaction succeeded but failed to persist option-offer arguments; \ -taproot key: {taproot_pubkey_gen}; encoded args (hex): {}. \ -You can recover by re-importing this value with `option-offer import`.", - hex::encode(encoded) - ) - })?; - - Ok(()) -} - -fn derive_contract_terms_from_expected_amounts( - expected_to_deposit_collateral: u64, - expected_to_deposit_premium: u64, - expected_to_get_settlement: u64, -) -> anyhow::Result<(u64, u64)> { - if expected_to_deposit_collateral == 0 { - return Err(anyhow!("expected-to-deposit-collateral must be > 0")); - } - - let premium_per_collateral = checked_div_exact_u64( - expected_to_deposit_premium, - expected_to_deposit_collateral, - "expected-to-deposit-premium / expected-to-deposit-collateral", - )?; - let collateral_per_contract = checked_div_exact_u64( - expected_to_get_settlement, - expected_to_deposit_collateral, - "expected-to-get-settlement / expected-to-deposit-collateral", - )?; - - Ok((collateral_per_contract, premium_per_collateral)) -} - -fn ensure_explicit_asset( - actual: Option<&AssetId>, - expected: AssetId, - context_label: &str, -) -> anyhow::Result<()> { - match actual { - Some(asset_id) if *asset_id == expected => Ok(()), - Some(asset_id) => Err(anyhow!( - "{context_label} has wrong asset id: expected {expected}, got {asset_id}" - )), - None => Err(anyhow!("{context_label} must have explicit asset id")), - } -} - -fn explicit_amount(value: Option, context_label: &str) -> anyhow::Result { - value.ok_or_else(|| anyhow!("{context_label} must have explicit value")) -} - -fn select_settlement_output_for_withdraw( - outputs: &[CandidateOutput], - covenant_script: &Script, - settlement_asset_id: AssetId, -) -> anyhow::Result<(u32, u64)> { - let Some(selected) = outputs - .iter() - .rfind(|output| output.script_pubkey == *covenant_script) - else { - return Err(anyhow!("exercise transaction has no covenant outputs")); - }; - - ensure_explicit_asset( - Option::from(&selected.asset_id), - settlement_asset_id, - "selected covenant settlement output", - )?; - let amount = explicit_amount(selected.value_sat, "selected covenant settlement output")?; - - Ok((selected.vout, amount)) -} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs deleted file mode 100644 index 33e473b..0000000 --- a/crates/cli/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] - -pub mod commands; -pub mod modules; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs deleted file mode 100644 index c00fd0f..0000000 --- a/crates/cli/src/main.rs +++ /dev/null @@ -1,92 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] - -use cli::commands::basic::Basic; -use cli::commands::option_offer::OptionOffer; -use cli::modules::utils::{esplora_url_from_network, wallet_data_root}; - -use anyhow::Result; - -use clap::{Parser, Subcommand}; - -use wallet_abi::runtime::WalletRuntimeConfig; - -use tracing_subscriber::{EnvFilter, fmt, prelude::*}; - -/// Command-line entrypoint for the Simplicity helper CLI. -#[derive(Parser, Debug)] -#[command( - name = "simplicity-cli", - version, - about = "Simplicity helper CLI for Liquid testnet" -)] -struct Cli { - #[command(subcommand)] - command: Commands, - /// Network on which to send a transaction - #[arg(long = "network")] - network: lwk_common::Network, - #[arg(short, long, env = "MNEMONIC")] - mnemonic: Option, -} - -/// Top-level subcommand groups. -#[derive(Subcommand, Debug)] -enum Commands { - /// Simple transaction utilities - Basic { - #[command(subcommand)] - basic: Box, - }, - /// Option-offer contract utilities - OptionOffer { - #[command(subcommand)] - option_offer: Box, - }, - // /// Options contract utilities - // Options { - // #[command(subcommand)] - // options: Box, - // }, - // /// Storage utilities - // Storage { - // #[command(subcommand)] - // storage: Box, - // }, -} - -const TEST_MNEMONIC: &str = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - -#[tokio::main] -async fn main() -> Result<()> { - let _ = dotenvy::dotenv(); - - logging_init(); - - let parsed = Cli::parse(); - - let mnemonic = parsed.mnemonic.unwrap_or_else(|| TEST_MNEMONIC.to_string()); - - let runtime = WalletRuntimeConfig::from_mnemonic( - &mnemonic, - parsed.network, - &esplora_url_from_network(parsed.network), - wallet_data_root(), - )?; - - match parsed.command { - Commands::Basic { basic } => basic.handle(runtime).await, - Commands::OptionOffer { option_offer } => Box::pin(option_offer.handle(runtime)).await, - // TODO: Commands::Options { options } => options.handle().await, - // TODO: Commands::Storage { storage } => storage.handle().await, - } -} - -fn logging_init() { - let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); - - tracing_subscriber::registry() - .with(fmt::layer().with_target(true)) - .with(filter) - .init(); -} diff --git a/crates/cli/src/modules/mod.rs b/crates/cli/src/modules/mod.rs deleted file mode 100644 index ca7140d..0000000 --- a/crates/cli/src/modules/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod store; -pub mod utils; diff --git a/crates/cli/src/modules/store.rs b/crates/cli/src/modules/store.rs deleted file mode 100644 index 5ed74cc..0000000 --- a/crates/cli/src/modules/store.rs +++ /dev/null @@ -1,76 +0,0 @@ -#![allow(dead_code)] - -use wallet_abi::taproot_pubkey_gen::TaprootPubkeyGen; -use wallet_abi::{Encodable, ProgramError}; - -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; -use simplicityhl::simplicity::elements::Address; - -#[derive(Clone, Debug)] -pub struct Store { - pub store: sled::Db, -} - -impl Store { - /// Load or create the local argument store. - /// - /// # Errors - /// Returns error if the store database cannot be opened. - pub fn load() -> anyhow::Result { - Ok(Self { - store: sled::open(".cache/store")?, - }) - } - - /// Import and validate encoded arguments into the store. - /// - /// # Errors - /// Returns error if hex decoding, validation, or storage fails. - pub fn import_arguments( - &self, - taproot_pubkey_gen: &str, - encoded_data: &str, - network: lwk_common::Network, - get_address: &impl Fn(&XOnlyPublicKey, &A, lwk_common::Network) -> Result, - ) -> anyhow::Result<()> - where - A: Encodable + wallet_abi::encoding::Decode<()>, - { - let decoded_data = hex::decode(encoded_data)?; - - let arguments = Encodable::decode(&decoded_data)?; - let _ = - TaprootPubkeyGen::build_from_str(taproot_pubkey_gen, &arguments, network, get_address)?; - - self.store.insert(taproot_pubkey_gen, decoded_data)?; - - Ok(()) - } - - /// Export stored arguments as hex-encoded string. - /// - /// # Errors - /// Returns error if arguments are not found. - pub fn export_arguments(&self, taproot_pubkey_gen: &str) -> anyhow::Result { - if let Some(value) = self.store.get(taproot_pubkey_gen)? { - return Ok(hex::encode(value)); - } - - anyhow::bail!("Arguments not found"); - } - - /// Retrieve and decode arguments by name. - /// - /// # Errors - /// Returns error if arguments are not found or decoding fails. - pub fn get_arguments(&self, arg_name: &str) -> anyhow::Result - where - A: Encodable + wallet_abi::encoding::Decode<()>, - { - if let Some(value) = self.store.get(arg_name)? { - return Encodable::decode(&value).map_err(anyhow::Error::msg); - } - - anyhow::bail!("Arguments not found"); - } -} diff --git a/crates/cli/src/modules/utils.rs b/crates/cli/src/modules/utils.rs deleted file mode 100644 index c47f8f1..0000000 --- a/crates/cli/src/modules/utils.rs +++ /dev/null @@ -1,42 +0,0 @@ -#![allow(clippy::missing_errors_doc)] - -use anyhow::anyhow; - -use wallet_abi::runtime::WalletRuntimeConfig; -use wallet_abi::schema::tx_create::{TransactionInfo, TxCreateRequest}; - -pub async fn execute_request( - runtime: &mut WalletRuntimeConfig, - request: TxCreateRequest, -) -> anyhow::Result { - let response = runtime.process_request(&request).await?; - let tx_info = response - .transaction - .ok_or_else(|| anyhow!("Expected transaction info in runtime response"))?; - - if request.broadcast { - println!("Broadcasted txid: {}", tx_info.txid); - } else { - println!("{}", tx_info.tx_hex); - } - - Ok(tx_info) -} - -pub fn wallet_data_root() -> std::path::PathBuf { - std::env::var_os("SIMPLICITY_CLI_WALLET_DATA_DIR").map_or_else( - || std::path::PathBuf::from(".cache/wallet"), - std::path::PathBuf::from, - ) -} - -#[must_use] -pub fn esplora_url_from_network(network: lwk_common::Network) -> String { - match network { - lwk_common::Network::Liquid => "https://blockstream.info/liquid/api".to_string(), - lwk_common::Network::TestnetLiquid => { - "https://blockstream.info/liquidtestnet/api".to_string() - } - lwk_common::Network::LocaltestLiquid => "http://127.0.0.1:3001".to_string(), - } -} diff --git a/crates/contracts/.gitignore b/crates/contracts/.gitignore new file mode 100644 index 0000000..cbd40b0 --- /dev/null +++ b/crates/contracts/.gitignore @@ -0,0 +1 @@ +src/artifacts \ No newline at end of file diff --git a/crates/contracts/Cargo.toml b/crates/contracts/Cargo.toml index d559a9c..51600f8 100644 --- a/crates/contracts/Cargo.toml +++ b/crates/contracts/Cargo.toml @@ -10,9 +10,7 @@ keywords = ["simplicity", "liquid", "bitcoin", "elements", "contracts"] categories = ["cryptography::cryptocurrencies"] [features] -default = ["finance-options", "finance-option-offer", "simple-storage", "bytes32-tr-storage", "array-tr-storage", "smt-storage"] -finance-options = [] -finance-option-offer = [] +default = ["simple-storage", "bytes32-tr-storage", "array-tr-storage", "smt-storage"] simple-storage = [] bytes32-tr-storage = [] array-tr-storage = [] @@ -22,20 +20,14 @@ smt-storage = [] workspace = true [dependencies] -bincode = "2.0.1" - thiserror = "2" -hex = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } + +smplx-std = { workspace = true } -simplicityhl = { workspace = true } -wallet-abi = { path = "../wallet-abi" } -serde_json = "1" -serde = { version = "1", features = ["derive"] } +lwk_common = { workspace = true } [dev-dependencies] -anyhow = "1" -bitcoincore-rpc = "0.19.0" -tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "sync"] } -lwk_common = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_common" } -lwk_test_util = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_test_util" } +anyhow = "1" \ No newline at end of file diff --git a/crates/contracts/README.md b/crates/contracts/README.md index 5f263c9..94e8c53 100644 --- a/crates/contracts/README.md +++ b/crates/contracts/README.md @@ -1,25 +1,24 @@ # Simplicity Contracts -This crate is a collection of contracts showcasing core possibilities of [Elements](https://docs.rs/elements) and [Simplicity HL](https://github.com/BlockstreamResearch/simfony). +This crate is a collection of contracts showcasing core possibilities of [Elements](https://docs.rs/elements) +and [Simplicity HL](https://github.com/BlockstreamResearch/simfony). Current contract modules in this crate: -- Finance: - - [Options](src/finance/options) - - [Option Offer](src/finance/option_offer) - State management: - - [Simple Storage](src/state_management/simple_storage) - - [Bytes32 Taproot Storage](src/state_management/bytes32_tr_storage) - - [Array Taproot Storage](src/state_management/array_tr_storage) - - [Sparse Merkle Tree Storage](src/state_management/smt_storage) + - [Simple Storage](src/state_management/simple_storage) + - [Bytes32 Taproot Storage](src/state_management/bytes32_tr_storage) + - [Array Taproot Storage](src/state_management/array_tr_storage) + - [Sparse Merkle Tree Storage](src/state_management/smt_storage) -Wallet-facing transaction construction is schema-first and lives in the wallet ABI crate. -Contract-side modules in this crate focus on program compilation, argument/witness helpers, execution, and -transaction finalization primitives. +- Finance: + - [Options](src/programs/options.rs) + - [Options Offer](src/programs/option_offer.rs) ## License Dual-licensed under either of: + - Apache License, Version 2.0 (Apache-2.0) - MIT license (MIT) diff --git a/crates/contracts/Simplex.toml b/crates/contracts/Simplex.toml new file mode 100644 index 0000000..e69de29 diff --git a/crates/contracts/src/finance/option_offer/source_simf/option_offer.simf b/crates/contracts/simf/option_offer.simf similarity index 100% rename from crates/contracts/src/finance/option_offer/source_simf/option_offer.simf rename to crates/contracts/simf/option_offer.simf diff --git a/crates/contracts/src/finance/options/source_simf/options.simf b/crates/contracts/simf/options.simf similarity index 100% rename from crates/contracts/src/finance/options/source_simf/options.simf rename to crates/contracts/simf/options.simf diff --git a/crates/contracts/src/error.rs b/crates/contracts/src/error.rs deleted file mode 100644 index e7c75ac..0000000 --- a/crates/contracts/src/error.rs +++ /dev/null @@ -1,106 +0,0 @@ -/// Errors from UTXO validation operations. -#[derive(Debug, thiserror::Error)] -pub enum ValidationError { - #[error("UTXO {script_hash} has confidential value")] - ConfidentialValue { script_hash: String }, - - #[error("UTXO {script_hash} has confidential asset")] - ConfidentialAsset { script_hash: String }, - - #[error("UTXO {script_hash} has insufficient funds: have {available}, need {required}")] - InsufficientFunds { - script_hash: String, - available: u64, - required: u64, - }, - - #[error("Fee UTXO {script_hash} has wrong asset: expected {expected}, got {actual}")] - FeeAssetMismatch { - script_hash: String, - expected: String, - actual: String, - }, -} - -/// Errors from transaction building operations. -#[derive(Debug, thiserror::Error)] -pub enum TransactionBuildError { - #[error("parts_to_split must be greater than 0")] - InvalidSplitParts, - - #[error("Send amount {send_amount} exceeds asset UTXO value {available}")] - SendAmountExceedsUtxo { send_amount: u64, available: u64 }, - - #[error( - "Fee UTXOs must have the same asset: first ({first_script_hash}) has {first_asset}, second ({second_script_hash}) has {second_asset}" - )] - FeeUtxoAssetMismatch { - first_script_hash: String, - first_asset: String, - second_script_hash: String, - second_asset: String, - }, - - #[error( - "First issuance must produce option token: expected ({expected_token}, {expected_reissuance}), got ({actual_token}, {actual_reissuance})" - )] - OptionTokenMismatch { - expected_token: String, - expected_reissuance: String, - actual_token: String, - actual_reissuance: String, - }, - - #[error( - "Second issuance must produce grantor token: expected ({expected_token}, {expected_reissuance}), got ({actual_token}, {actual_reissuance})" - )] - GrantorTokenMismatch { - expected_token: String, - expected_reissuance: String, - actual_token: String, - actual_reissuance: String, - }, - - #[error("Insufficient collateral: need {required}, have {available}")] - InsufficientCollateral { required: u64, available: u64 }, - - #[error("Insufficient settlement asset: need {required}, have {available}")] - InsufficientSettlementAsset { required: u64, available: u64 }, - - #[error("Grantor asset UTXO has wrong token: expected {expected}, got {actual}")] - WrongGrantorToken { expected: String, actual: String }, - - #[error("Settlement asset UTXO has wrong asset: expected {expected}, got {actual}")] - WrongSettlementAsset { expected: String, actual: String }, - - #[error("Failed to blind transaction: {0}")] - Blinding(#[from] simplicityhl::elements::pset::Error), - - #[error("Failed to blind transaction outputs: {0}")] - BlindingOutputs(#[from] simplicityhl::elements::pset::PsetBlindError), - - #[error("Transaction amount proof verification failed: {0}")] - AmountProofVerification(#[from] simplicityhl::elements::VerificationError), - - #[error("Failed to unblind transaction output: {0}")] - Unblinding(#[from] simplicityhl::elements::UnblindError), - - #[error("Invalid lock time: {0}")] - InvalidLockTime(#[from] simplicityhl::elements::locktime::Error), - - #[error(transparent)] - Validation(#[from] ValidationError), -} - -/// Errors from extracting arguments from Arguments struct. -#[derive(Debug, thiserror::Error)] -pub enum FromArgumentsError { - #[error("Missing witness name: {name}")] - MissingWitness { name: String }, - - #[error("Wrong value type for {name}: expected {expected}")] - WrongValueType { name: String, expected: String }, - - #[error("Invalid asset ID bytes for {name}")] - InvalidAssetId { name: String }, -} diff --git a/crates/contracts/src/finance/mod.rs b/crates/contracts/src/finance/mod.rs deleted file mode 100644 index 77fa54e..0000000 --- a/crates/contracts/src/finance/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[cfg(feature = "finance-option-offer")] -pub mod option_offer; -#[cfg(feature = "finance-options")] -pub mod options; diff --git a/crates/contracts/src/finance/option_offer/build_arguments.rs b/crates/contracts/src/finance/option_offer/build_arguments.rs deleted file mode 100644 index ecf5357..0000000 --- a/crates/contracts/src/finance/option_offer/build_arguments.rs +++ /dev/null @@ -1,284 +0,0 @@ -#![allow(clippy::missing_errors_doc)] - -use crate::error::FromArgumentsError; -use crate::utils::arguments_helpers::{extract_u32, extract_u64, extract_u256_bytes}; -use serde_json::Value; -use simplicityhl::elements::AssetId; -use simplicityhl::num::U256; -use simplicityhl::{Arguments, str::WitnessName, value::UIntValue}; -use std::collections::HashMap; -use wallet_abi::WalletAbiError; -use wallet_abi::schema::values::SimfArguments; - -#[derive(Debug, Clone, bincode::Encode, bincode::Decode, PartialEq, Eq, Default)] -pub struct OptionOfferArguments { - /// Asset ID of collateral (the asset user deposits) - collateral_asset_id: [u8; 32], - /// Asset ID of premium (the second asset user deposits, linked to collateral) - premium_asset_id: [u8; 32], - /// Asset ID of settlement asset (the asset counterparty pays with) - settlement_asset_id: [u8; 32], - /// Settlement rate: `settlement_amount` = `COLLATERAL_PER_CONTRACT` * `collateral_amount` - collateral_per_contract: u64, - /// Premium rate: `premium_amount` = `PREMIUM_PER_COLLATERAL` * `collateral_amount` - premium_per_collateral: u64, - /// Unix timestamp after which user can reclaim collateral and premium - expiry_time: u32, - /// User's x-only public key for signature verification (32 bytes) - user_pubkey: [u8; 32], -} - -impl OptionOfferArguments { - /// Create new option offer arguments. - #[must_use] - #[allow(clippy::too_many_arguments)] - pub fn new( - collateral_asset_id: AssetId, - premium_asset_id: AssetId, - settlement_asset_id: AssetId, - collateral_per_contract: u64, - premium_per_collateral: u64, - expiry_time: u32, - user_pubkey: [u8; 32], - ) -> Self { - Self { - collateral_asset_id: collateral_asset_id.into_inner().0, - premium_asset_id: premium_asset_id.into_inner().0, - settlement_asset_id: settlement_asset_id.into_inner().0, - collateral_per_contract, - premium_per_collateral, - expiry_time, - user_pubkey, - } - } - - /// Build arguments for contract instantiation. - #[must_use] - pub fn build_arguments(&self) -> Arguments { - Arguments::from(HashMap::from([ - ( - WitnessName::from_str_unchecked("COLLATERAL_ASSET_ID"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.collateral_asset_id, - ))), - ), - ( - WitnessName::from_str_unchecked("PREMIUM_ASSET_ID"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.premium_asset_id, - ))), - ), - ( - WitnessName::from_str_unchecked("SETTLEMENT_ASSET_ID"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.settlement_asset_id, - ))), - ), - ( - WitnessName::from_str_unchecked("COLLATERAL_PER_CONTRACT"), - simplicityhl::Value::from(UIntValue::U64(self.collateral_per_contract)), - ), - ( - WitnessName::from_str_unchecked("PREMIUM_PER_COLLATERAL"), - simplicityhl::Value::from(UIntValue::U64(self.premium_per_collateral)), - ), - ( - WitnessName::from_str_unchecked("EXPIRY_TIME"), - simplicityhl::Value::from(UIntValue::U32(self.expiry_time)), - ), - ( - WitnessName::from_str_unchecked("USER_PUBKEY"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(self.user_pubkey))), - ), - ])) - } - - #[must_use] - pub fn build_simf_arguments(&self) -> SimfArguments { - SimfArguments { - resolved: self.build_arguments(), - runtime_arguments: HashMap::default(), - } - } - - pub fn to_json(&self) -> Result { - serde_json::to_value(self.build_arguments()).map_err(WalletAbiError::from) - } - - /// Returns the collateral per contract amount. - #[must_use] - pub const fn collateral_per_contract(&self) -> u64 { - self.collateral_per_contract - } - - /// Returns the premium per collateral amount. - #[must_use] - pub const fn premium_per_collateral(&self) -> u64 { - self.premium_per_collateral - } - - /// Returns the expiry time. - #[must_use] - pub const fn expiry_time(&self) -> u32 { - self.expiry_time - } - - /// Returns the user's public key. - #[must_use] - pub const fn user_pubkey(&self) -> [u8; 32] { - self.user_pubkey - } - - /// Returns the collateral asset ID. - /// - /// # Panics - /// - /// Panics if the collateral asset ID bytes are invalid. - #[must_use] - pub fn get_collateral_asset_id(&self) -> AssetId { - AssetId::from_slice(&self.collateral_asset_id).unwrap() - } - - /// Returns the premium asset ID. - /// - /// # Panics - /// - /// Panics if the premium asset ID bytes are invalid. - #[must_use] - pub fn get_premium_asset_id(&self) -> AssetId { - AssetId::from_slice(&self.premium_asset_id).unwrap() - } - - /// Returns the settlement asset ID. - /// - /// # Panics - /// - /// Panics if the settlement asset ID bytes are invalid. - #[must_use] - pub fn get_settlement_asset_id(&self) -> AssetId { - AssetId::from_slice(&self.settlement_asset_id).unwrap() - } - - /// 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 { - let collateral_asset_id_name = WitnessName::from_str_unchecked("COLLATERAL_ASSET_ID"); - let premium_asset_id_name = WitnessName::from_str_unchecked("PREMIUM_ASSET_ID"); - let settlement_asset_id_name = WitnessName::from_str_unchecked("SETTLEMENT_ASSET_ID"); - let collateral_per_contract_name = - WitnessName::from_str_unchecked("COLLATERAL_PER_CONTRACT"); - let premium_per_collateral_name = WitnessName::from_str_unchecked("PREMIUM_PER_COLLATERAL"); - let expiry_time_name = WitnessName::from_str_unchecked("EXPIRY_TIME"); - let user_pubkey_name = WitnessName::from_str_unchecked("USER_PUBKEY"); - - let collateral_asset_id = extract_u256_bytes(args, &collateral_asset_id_name)?; - let premium_asset_id = extract_u256_bytes(args, &premium_asset_id_name)?; - let settlement_asset_id = extract_u256_bytes(args, &settlement_asset_id_name)?; - let collateral_per_contract = extract_u64(args, &collateral_per_contract_name)?; - let premium_per_collateral = extract_u64(args, &premium_per_collateral_name)?; - let expiry_time = extract_u32(args, &expiry_time_name)?; - let user_pubkey = extract_u256_bytes(args, &user_pubkey_name)?; - - Ok(Self { - collateral_asset_id, - premium_asset_id, - settlement_asset_id, - collateral_per_contract, - premium_per_collateral, - expiry_time, - user_pubkey, - }) - } -} - -impl wallet_abi::Encodable for OptionOfferArguments {} - -#[cfg(test)] -mod tests { - use super::*; - use wallet_abi::Encodable; - - fn make_full_args() -> anyhow::Result { - Ok(OptionOfferArguments::new( - AssetId::from_slice(&[1u8; 32])?, - AssetId::from_slice(&[2u8; 32])?, - AssetId::from_slice(&[3u8; 32])?, - 1000, - 100, - 1_700_000_000, - [4u8; 32], - )) - } - - #[test] - fn test_serialize_deserialize_default() -> anyhow::Result<()> { - let args = OptionOfferArguments::default(); - - let serialized = args.encode()?; - let deserialized = OptionOfferArguments::decode(&serialized)?; - - assert_eq!(args, deserialized); - assert_eq!(deserialized.build_arguments().iter().count(), 7); - - Ok(()) - } - - #[test] - fn test_serialize_deserialize_full() -> anyhow::Result<()> { - let args = make_full_args()?; - - let serialized = args.encode()?; - let deserialized = OptionOfferArguments::decode(&serialized)?; - - assert_eq!(args, deserialized); - assert_eq!(deserialized.collateral_per_contract(), 1000); - assert_eq!(deserialized.premium_per_collateral(), 100); - assert_eq!(deserialized.expiry_time(), 1_700_000_000); - assert_eq!(deserialized.user_pubkey(), [4u8; 32]); - assert_eq!( - deserialized.get_collateral_asset_id(), - AssetId::from_slice(&[1u8; 32])? - ); - assert_eq!( - deserialized.get_premium_asset_id(), - AssetId::from_slice(&[2u8; 32])? - ); - assert_eq!( - deserialized.get_settlement_asset_id(), - AssetId::from_slice(&[3u8; 32])? - ); - - Ok(()) - } - - #[test] - fn test_arguments_roundtrip_default() -> anyhow::Result<()> { - let original = OptionOfferArguments::default(); - let arguments = original.build_arguments(); - - let recovered = OptionOfferArguments::from_arguments(&arguments)?; - - assert_eq!(original, recovered); - - Ok(()) - } - - #[test] - fn test_arguments_roundtrip_full() -> anyhow::Result<()> { - let original = make_full_args()?; - let arguments = original.build_arguments(); - - let recovered = OptionOfferArguments::from_arguments(&arguments)?; - - assert_eq!(original, recovered); - assert_eq!(arguments.iter().count(), 7); - let simf_arguments = original.build_simf_arguments(); - assert!(simf_arguments.runtime_arguments.is_empty()); - assert_eq!(simf_arguments.resolved.iter().count(), 7); - - Ok(()) - } -} diff --git a/crates/contracts/src/finance/option_offer/build_witness.rs b/crates/contracts/src/finance/option_offer/build_witness.rs deleted file mode 100644 index 74e12cd..0000000 --- a/crates/contracts/src/finance/option_offer/build_witness.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::collections::HashMap; - -use simplicityhl::elements::bitcoin::XOnlyPublicKey; -use simplicityhl::{ - ResolvedType, Value, WitnessValues, parse::ParseFromStr, str::WitnessName, - types::TypeConstructible, -}; -use wallet_abi::schema::values::{RuntimeSimfWitness, SimfWitness}; - -#[derive(Debug, Clone)] -pub enum OptionOfferBranch { - /// Exercise path: counterparty swaps settlement asset for collateral + premium - Exercise { - /// Amount of collateral the counterparty will receive (premium derived from ratio) - collateral_amount: u64, - /// Whether there's change (partial swap) - is_change_needed: bool, - }, - /// Withdraw path: user withdraws settlement asset - Withdraw, - /// Expiry path: user reclaims collateral + premium after expiry - Expiry, -} - -#[must_use] -/// Build runtime SIMF witness payload for option-offer branches. -/// -/// # Panics -/// -/// Panics if internal witness type/value parsing fails for static type descriptors. -pub fn build_option_offer_witness( - branch: &OptionOfferBranch, - to_sign_x_only: XOnlyPublicKey, -) -> SimfWitness { - let exercise_type = ResolvedType::parse_from_str("(u64, bool)").unwrap(); - let signature_type = ResolvedType::parse_from_str("()").unwrap(); - let withdraw_or_expiry_type = ResolvedType::either(signature_type.clone(), signature_type); - let path_type = ResolvedType::either(exercise_type, withdraw_or_expiry_type); - - let path = match branch { - OptionOfferBranch::Exercise { - collateral_amount, - is_change_needed, - } => { - format!("Left(({collateral_amount}, {is_change_needed}))") - } - OptionOfferBranch::Withdraw => "Right(Left(()))".to_string(), - OptionOfferBranch::Expiry => "Right(Right(()))".to_string(), - }; - - SimfWitness { - resolved: WitnessValues::from(HashMap::from([( - WitnessName::from_str_unchecked("PATH"), - Value::parse_from_str(&path, &path_type).unwrap(), - )])), - runtime_arguments: vec![RuntimeSimfWitness::SigHashAll { - name: "USER_SIGHASH_ALL".to_string(), - public_key: to_sign_x_only, - }], - } -} diff --git a/crates/contracts/src/finance/option_offer/mod.rs b/crates/contracts/src/finance/option_offer/mod.rs deleted file mode 100644 index d372cf3..0000000 --- a/crates/contracts/src/finance/option_offer/mod.rs +++ /dev/null @@ -1,641 +0,0 @@ -pub mod build_arguments; -pub mod build_witness; - -pub use build_arguments::OptionOfferArguments; - -use crate::option_offer::build_witness::{OptionOfferBranch, build_option_offer_witness}; - -use wallet_abi::runtime::WalletRuntimeConfig; -use wallet_abi::schema::tx_create::{TX_CREATE_ABI_VERSION, TxCreateRequest}; -use wallet_abi::schema::values::{serialize_arguments, serialize_witness}; -use wallet_abi::taproot_pubkey_gen::TaprootPubkeyGen; -use wallet_abi::{ - AssetVariant, BlinderVariant, FinalizerSpec, InputBlinder, InputSchema, LockVariant, Network, - OutputSchema, ProgramError, RuntimeParams, UTXOSource, WalletAbiError, create_p2tr_address, - load_program, -}; - -use simplicityhl::elements::{Address, LockTime, OutPoint, Sequence, Txid}; - -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; - -use simplicityhl::{CompiledProgram, TemplateProgram}; - -pub const OPTION_OFFER_SOURCE: &str = include_str!("source_simf/option_offer.simf"); - -/// Get the option offer template program for instantiation. -/// -/// # Panics -/// -/// Panics if the embedded source fails to compile (should never happen). -#[must_use] -pub fn get_option_offer_template_program() -> TemplateProgram { - TemplateProgram::new(OPTION_OFFER_SOURCE) - .expect("INTERNAL: expected Option Offer Program to compile successfully.") -} - -/// Derive P2TR address for an option offer contract. -/// -/// # Errors -/// -/// Returns error if program compilation fails. -pub fn get_option_offer_address( - x_only_public_key: &XOnlyPublicKey, - arguments: &OptionOfferArguments, - network: Network, -) -> Result { - Ok(create_p2tr_address( - get_option_offer_program(arguments)?.commit().cmr(), - x_only_public_key, - network.address_params(), - )) -} - -/// Compile option offer program with the given arguments. -/// -/// # Errors -/// -/// Returns error if compilation fails. -pub fn get_option_offer_program( - arguments: &OptionOfferArguments, -) -> Result { - load_program(OPTION_OFFER_SOURCE, arguments.build_arguments()) -} - -/// Get compiled option offer program, panicking on failure. -/// -/// # Panics -/// -/// Panics if program instantiation fails. -#[must_use] -pub fn get_compiled_option_offer_program(arguments: &OptionOfferArguments) -> CompiledProgram { - let program = get_option_offer_template_program(); - - program - .instantiate(arguments.build_arguments(), true) - .unwrap() -} - -pub struct OptionOfferRuntime { - runtime: WalletRuntimeConfig, - args: OptionOfferArguments, - tap: TaprootPubkeyGen, -} - -impl OptionOfferRuntime { - /// Create runtime helper from resolved runtime config, arguments, and taproot handle. - #[must_use] - pub const fn new( - runtime: WalletRuntimeConfig, - args: OptionOfferArguments, - tap: TaprootPubkeyGen, - ) -> Self { - Self { runtime, args, tap } - } - - /// Return immutable access to underlying wallet runtime. - #[must_use] - pub const fn runtime(&self) -> &WalletRuntimeConfig { - &self.runtime - } - - /// Return mutable access to underlying wallet runtime. - pub const fn runtime_mut(&mut self) -> &mut WalletRuntimeConfig { - &mut self.runtime - } - - /// Return option-offer arguments used by this runtime. - #[must_use] - pub const fn args(&self) -> &OptionOfferArguments { - &self.args - } - - /// Return taproot handle used by this runtime. - #[must_use] - pub const fn tap(&self) -> &TaprootPubkeyGen { - &self.tap - } - - /// Compute premium amount from collateral amount. - /// - /// # Panics - /// - /// Panics if multiplication overflows `u64`. - pub const fn premium_amount(&self, collateral_amount: u64) -> u64 { - collateral_amount - .checked_mul(self.args.premium_per_collateral()) - .expect("premium amount overflow") - } - - /// Compute settlement amount from collateral amount. - /// - /// # Panics - /// - /// Panics if multiplication overflows `u64`. - pub const fn settlement_amount(&self, collateral_amount: u64) -> u64 { - collateral_amount - .checked_mul(self.args.collateral_per_contract()) - .expect("settlement amount overflow") - } - - fn get_base_finalizer_spec( - &self, - witness: &OptionOfferBranch, - ) -> Result { - Ok(FinalizerSpec::Simf { - source_simf: OPTION_OFFER_SOURCE.to_string(), - internal_key: Box::new(self.tap.clone()), - arguments: serialize_arguments(&self.args.build_simf_arguments())?, - witness: serialize_witness(&build_option_offer_witness( - witness, - self.runtime.signer_x_only_public_key()?, - ))?, - }) - } - - /// Build the initial deposit transaction request. - /// - /// This constructor is currently infallible and always returns `Ok`. - /// - /// # Panics - /// - /// Panics if premium amount multiplication overflows `u64`. - pub fn build_deposit_request(&self, collateral_deposit_amount: u64) -> TxCreateRequest { - let premium_deposit_amount = self.premium_amount(collateral_deposit_amount); - - TxCreateRequest { - abi_version: TX_CREATE_ABI_VERSION.to_string(), - request_id: "request-option_offer.deposit".to_string(), - network: self.runtime.network, - params: RuntimeParams { - inputs: vec![InputSchema::new("input0"), InputSchema::new("input1")], - outputs: vec![ - OutputSchema::from_script( - "out0", - self.args.get_collateral_asset_id(), - collateral_deposit_amount, - self.tap.address.script_pubkey(), - ), - OutputSchema::from_script( - "out1", - self.args.get_premium_asset_id(), - premium_deposit_amount, - self.tap.address.script_pubkey(), - ), - ], - fee_rate_sat_vb: Some(0.1), - locktime: None, - }, - broadcast: true, - } - } - - /// Build the exercise transaction request. - /// - /// # Errors - /// - /// Returns an error if covenant inputs are not explicit or if requested amounts exceed - /// available covenant balances. - /// - /// # Panics - /// - /// Panics if premium/settlement amount multiplication overflows `u64`. - #[expect(clippy::too_many_lines)] - pub async fn build_exercise_request( - &self, - creation_tx_id: Txid, - collateral_amount: u64, - ) -> Result { - let premium_amount = self.premium_amount(collateral_amount); - let settlement_amount = self.settlement_amount(collateral_amount); - - let collateral_outpoint = OutPoint::new(creation_tx_id, 0); - let collateral_tx_out = self.runtime.fetch_tx_out(&collateral_outpoint).await?; - let available_collateral = collateral_tx_out.value.explicit().ok_or_else(|| { - WalletAbiError::InvalidRequest( - "covenant collateral output must be explicit".to_string(), - ) - })?; - - let premium_outpoint = OutPoint::new(creation_tx_id, 1); - let premium_tx_out = self.runtime.fetch_tx_out(&premium_outpoint).await?; - let available_premium = premium_tx_out.value.explicit().ok_or_else(|| { - WalletAbiError::InvalidRequest("covenant premium output must be explicit".to_string()) - })?; - - let collateral_change = available_collateral - .checked_sub(collateral_amount) - .ok_or_else(|| { - WalletAbiError::InvalidRequest( - "requested collateral exceeds covenant collateral balance".to_string(), - ) - })?; - let premium_change = available_premium - .checked_sub(premium_amount) - .ok_or_else(|| { - WalletAbiError::InvalidRequest( - "requested premium exceeds covenant premium balance".to_string(), - ) - })?; - - let finalizer = self.get_base_finalizer_spec(&OptionOfferBranch::Exercise { - collateral_amount, - is_change_needed: collateral_change != 0, - })?; - - let receiver = self.runtime.signer_receive_address()?; - - let mut outputs: Vec = Vec::new(); - if collateral_change != 0 { - outputs.push(OutputSchema { - id: "covenant-collateral-change".to_string(), - amount_sat: collateral_change, - lock: LockVariant::Script { - script: self.tap.address.script_pubkey(), - }, - asset: AssetVariant::AssetId { - asset_id: self.args.get_collateral_asset_id(), - }, - blinder: BlinderVariant::Explicit, - }); - outputs.push(OutputSchema { - id: "covenant-premium-change".to_string(), - amount_sat: premium_change, - lock: LockVariant::Script { - script: self.tap.address.script_pubkey(), - }, - asset: AssetVariant::AssetId { - asset_id: self.args.get_premium_asset_id(), - }, - blinder: BlinderVariant::Explicit, - }); - } - - outputs.extend(vec![ - OutputSchema { - id: "covenant-settlement-change".to_string(), - amount_sat: settlement_amount, - lock: LockVariant::Script { - script: self.tap.address.script_pubkey(), - }, - asset: AssetVariant::AssetId { - asset_id: self.args.get_settlement_asset_id(), - }, - blinder: BlinderVariant::Explicit, - }, - OutputSchema { - id: "user-collateral-requested".to_string(), - amount_sat: collateral_amount, - lock: LockVariant::Script { - script: receiver.script_pubkey(), - }, - asset: AssetVariant::AssetId { - asset_id: self.args.get_collateral_asset_id(), - }, - blinder: BlinderVariant::Explicit, - }, - OutputSchema { - id: "user-premium-requested".to_string(), - amount_sat: premium_amount, - lock: LockVariant::Script { - script: receiver.script_pubkey(), - }, - asset: AssetVariant::AssetId { - asset_id: self.args.get_premium_asset_id(), - }, - blinder: BlinderVariant::Explicit, - }, - ]); - - Ok(TxCreateRequest { - abi_version: TX_CREATE_ABI_VERSION.to_string(), - request_id: "request-option_offer.exercise".to_string(), - network: self.runtime.network, - params: RuntimeParams { - inputs: vec![ - InputSchema { - id: "input0".to_string(), - utxo_source: UTXOSource::Provided { - outpoint: OutPoint::new(creation_tx_id, 0), - }, - blinder: InputBlinder::Explicit, - sequence: Sequence::default(), - issuance: None, - finalizer: finalizer.clone(), - }, - InputSchema { - id: "input1".to_string(), - utxo_source: UTXOSource::Provided { - outpoint: OutPoint::new(creation_tx_id, 1), - }, - blinder: InputBlinder::Explicit, - sequence: Sequence::default(), - issuance: None, - finalizer, - }, - InputSchema::new("input2"), - ], - outputs, - fee_rate_sat_vb: Some(0.1), - locktime: None, - }, - broadcast: true, - }) - } - - /// Build the withdraw transaction request for an explicit covenant settlement outpoint. - /// - /// Settlement output index is not fixed. Resolve the concrete settlement outpoint from the - /// actual exercise transaction outputs (exercise with change can shift `vout` positions). - /// - /// # Errors - /// - /// Returns an error if runtime-derived metadata cannot be serialized. - pub fn build_withdraw_request_for_outpoint( - &self, - settlement_outpoint: OutPoint, - settlement_amount: u64, - ) -> Result { - let finalizer = self.get_base_finalizer_spec(&OptionOfferBranch::Withdraw)?; - - let receiver = self.runtime.signer_receive_address()?; - - Ok(TxCreateRequest { - abi_version: TX_CREATE_ABI_VERSION.to_string(), - request_id: "request-option_offer.withdraw".to_string(), - network: self.runtime.network, - params: RuntimeParams { - inputs: vec![InputSchema { - id: "input0".to_string(), - utxo_source: UTXOSource::Provided { - outpoint: settlement_outpoint, - }, - blinder: InputBlinder::Explicit, - sequence: Sequence::default(), - issuance: None, - finalizer, - }], - outputs: vec![OutputSchema::from_script( - "out0", - self.args.get_settlement_asset_id(), - settlement_amount, - receiver.script_pubkey(), - )], - fee_rate_sat_vb: Some(0.1), - locktime: None, - }, - broadcast: true, - }) - } - - /// Build the expiry transaction request. - /// - /// # Errors - /// - /// Returns an error if locktime conversion or runtime metadata serialization fails. - pub fn build_expiry_request( - &self, - creation_tx_id: Txid, - collateral_amount: u64, - premium_amount: u64, - ) -> Result { - let finalizer = self.get_base_finalizer_spec(&OptionOfferBranch::Expiry)?; - - let receiver = self.runtime.signer_receive_address()?; - - Ok(TxCreateRequest { - abi_version: TX_CREATE_ABI_VERSION.to_string(), - request_id: "request-option_offer.expiry".to_string(), - network: self.runtime.network, - params: RuntimeParams { - inputs: vec![ - InputSchema { - id: "input0".to_string(), - utxo_source: UTXOSource::Provided { - outpoint: OutPoint::new(creation_tx_id, 0), - }, - blinder: InputBlinder::Explicit, - sequence: Sequence::ENABLE_LOCKTIME_NO_RBF, - issuance: None, - finalizer: finalizer.clone(), - }, - InputSchema { - id: "input1".to_string(), - utxo_source: UTXOSource::Provided { - outpoint: OutPoint::new(creation_tx_id, 1), - }, - blinder: InputBlinder::Explicit, - sequence: Sequence::ENABLE_LOCKTIME_NO_RBF, - issuance: None, - finalizer, - }, - ], - outputs: vec![ - OutputSchema::from_script( - "out0", - self.args.get_collateral_asset_id(), - collateral_amount, - receiver.script_pubkey(), - ), - OutputSchema::from_script( - "out1", - self.args.get_premium_asset_id(), - premium_amount, - receiver.script_pubkey(), - ), - ], - fee_rate_sat_vb: Some(0.1), - locktime: Some(LockTime::from_time(self.args.expiry_time())?), - }, - broadcast: true, - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - - use crate::utils::test_setup::{ - RuntimeFundingAsset, ensure_node_running, fund_runtime, get_esplora_url, mine_blocks, - wallet_data_root, - }; - - use anyhow::anyhow; - use simplicityhl::elements::{OutPoint, Txid}; - - use wallet_abi::runtime::WalletRuntimeConfig; - use wallet_abi::schema::tx_create::TxCreateRequest; - use wallet_abi::taproot_pubkey_gen::TaprootPubkeyGen; - - const COLLATERAL_PER_CONTRACT: u64 = 100; - const PREMIUM_PER_COLLATERAL: u64 = 10; - const EXPIRY_TIME: u32 = 1_700_000_000; - - fn setup() -> anyhow::Result { - ensure_node_running()?; - - let runtime_config = WalletRuntimeConfig::build_random( - Network::LocaltestLiquid, - &get_esplora_url()?, - wallet_data_root(), - )?; - - let collateral_funding = fund_runtime(&runtime_config, RuntimeFundingAsset::Lbtc)?; - let premium_funding = fund_runtime(&runtime_config, RuntimeFundingAsset::NewAsset)?; - let settlement_funding = fund_runtime(&runtime_config, RuntimeFundingAsset::NewAsset)?; - - let args = OptionOfferArguments::new( - collateral_funding.funded_asset_id, - premium_funding.funded_asset_id, - settlement_funding.funded_asset_id, - COLLATERAL_PER_CONTRACT, - PREMIUM_PER_COLLATERAL, - EXPIRY_TIME, - runtime_config.signer_x_only_public_key()?.serialize(), - ); - let tap = - TaprootPubkeyGen::from(&args, Network::LocaltestLiquid, &get_option_offer_address)?; - - Ok(OptionOfferRuntime { - runtime: runtime_config, - args, - tap, - }) - } - - async fn assert_broadcast_happy_path( - runtime: &mut OptionOfferRuntime, - request: &TxCreateRequest, - ) -> anyhow::Result { - let response = runtime.runtime.process_request(request).await?; - - let Some(tx_info) = response.transaction else { - panic!("Expected a response broadcast info"); - }; - - Ok(tx_info.txid) - } - - async fn find_settlement_outpoint( - runtime: &OptionOfferRuntime, - exercise_tx_id: Txid, - ) -> anyhow::Result<(OutPoint, u64)> { - let tx = { - let inner_esplora = runtime.runtime.esplora.lock().await; - inner_esplora.get_transaction(exercise_tx_id).await? - }; - - let covenant_script = runtime.tap.address.script_pubkey(); - let settlement_asset_id = runtime.args.get_settlement_asset_id(); - - let Some((vout, value_sat)) = tx.output.iter().enumerate().find_map(|(vout, tx_out)| { - if tx_out.script_pubkey != covenant_script { - return None; - } - - let asset = tx_out.asset.explicit()?; - if asset != settlement_asset_id { - return None; - } - - let value_sat = tx_out.value.explicit()?; - Some((vout, value_sat)) - }) else { - return Err(anyhow!( - "exercise tx {exercise_tx_id} does not contain explicit settlement output for covenant script" - )); - }; - - Ok(( - OutPoint::new( - exercise_tx_id, - u32::try_from(vout).map_err(|_| anyhow!("exercise vout index overflow"))?, - ), - value_sat, - )) - } - - #[tokio::test] - async fn test_option_offer_deposit() -> anyhow::Result<()> { - let mut fixture = setup()?; - let request = fixture.build_deposit_request(1_000u64); - let _ = assert_broadcast_happy_path(&mut fixture, &request).await?; - Ok(()) - } - - #[tokio::test] - async fn test_option_offer_exercise() -> anyhow::Result<()> { - let mut fixture = setup()?; - let collateral_amount = 1_000u64; - let request = fixture.build_deposit_request(collateral_amount); - let creation_tx_id = assert_broadcast_happy_path(&mut fixture, &request).await?; - mine_blocks(1)?; - - let request = fixture - .build_exercise_request(creation_tx_id, collateral_amount) - .await?; - let _ = assert_broadcast_happy_path(&mut fixture, &request).await?; - Ok(()) - } - - #[tokio::test] - async fn test_option_offer_exercise_with_change() -> anyhow::Result<()> { - let mut fixture = setup()?; - let collateral_amount = 1_000u64; - let request = fixture.build_deposit_request(collateral_amount); - let creation_tx_id = assert_broadcast_happy_path(&mut fixture, &request).await?; - mine_blocks(1)?; - - let request = fixture - .build_exercise_request(creation_tx_id, collateral_amount - 500) - .await?; - let _ = assert_broadcast_happy_path(&mut fixture, &request).await?; - Ok(()) - } - - #[tokio::test] - async fn test_option_offer_withdraw() -> anyhow::Result<()> { - let mut fixture = setup()?; - let collateral_amount = 1_000u64; - let exercise_collateral_amount = collateral_amount - 500; - let expected_settlement_amount = fixture.settlement_amount(exercise_collateral_amount); - - let deposit_request = fixture.build_deposit_request(collateral_amount); - let creation_tx_id = assert_broadcast_happy_path(&mut fixture, &deposit_request).await?; - mine_blocks(1)?; - - let exercise_request = fixture - .build_exercise_request(creation_tx_id, exercise_collateral_amount) - .await?; - let exercise_tx_id = assert_broadcast_happy_path(&mut fixture, &exercise_request).await?; - mine_blocks(1)?; - - let (settlement_outpoint, settlement_amount) = - find_settlement_outpoint(&fixture, exercise_tx_id).await?; - assert_eq!(settlement_amount, expected_settlement_amount); - - let request = - fixture.build_withdraw_request_for_outpoint(settlement_outpoint, settlement_amount)?; - let _ = assert_broadcast_happy_path(&mut fixture, &request).await?; - - Ok(()) - } - - #[tokio::test] - async fn test_option_offer_expiry() -> anyhow::Result<()> { - let mut fixture = setup()?; - let collateral_amount = 1_000u64; - let premium_amount = fixture.premium_amount(collateral_amount); - - let deposit_request = fixture.build_deposit_request(collateral_amount); - let creation_tx_id = assert_broadcast_happy_path(&mut fixture, &deposit_request).await?; - mine_blocks(1)?; - - let request = - fixture.build_expiry_request(creation_tx_id, collateral_amount, premium_amount)?; - let _ = assert_broadcast_happy_path(&mut fixture, &request).await?; - - Ok(()) - } -} diff --git a/crates/contracts/src/finance/options/build_arguments.rs b/crates/contracts/src/finance/options/build_arguments.rs deleted file mode 100644 index d04d7e4..0000000 --- a/crates/contracts/src/finance/options/build_arguments.rs +++ /dev/null @@ -1,382 +0,0 @@ -use std::collections::HashMap; - -use crate::error::FromArgumentsError; -use crate::utils::arguments_helpers::{extract_u32, extract_u64, extract_u256_bytes}; - -use simplicityhl::elements::AssetId; -use simplicityhl::num::U256; -use simplicityhl::{Arguments, str::WitnessName, value::UIntValue}; - -#[derive(Debug, Clone, bincode::Encode, bincode::Decode, PartialEq, Eq, Default)] -pub struct OptionsArguments { - /// Unix timestamp (seconds) when exercise/settlement becomes valid. - start_time: u32, - /// Unix timestamp (seconds) when expiry path becomes valid. - expiry_time: u32, - /// Collateral units locked per option contract. - collateral_per_contract: u64, - /// Settlement units paid per option contract. - settlement_per_contract: u64, - /// Collateral asset ID committed in covenant parameters. - collateral_asset_id: [u8; 32], - /// Settlement asset ID committed in covenant parameters. - settlement_asset_id: [u8; 32], - /// Option token asset ID committed in covenant parameters. - option_token_asset: [u8; 32], - /// Option reissuance token ID committed in covenant parameters. - option_reissuance_token_asset: [u8; 32], - /// Grantor token asset ID committed in covenant parameters. - grantor_token_asset: [u8; 32], - /// Grantor reissuance token ID committed in covenant parameters. - grantor_reissuance_token_asset: [u8; 32], -} - -impl OptionsArguments { - #[allow(clippy::too_many_arguments)] - #[must_use] - pub fn new( - start_time: u32, - expiry_time: u32, - collateral_per_contract: u64, - settlement_per_contract: u64, - collateral_asset_id: AssetId, - settlement_asset_id: AssetId, - option_token_asset: AssetId, - option_reissuance_token_asset: AssetId, - grantor_token_asset: AssetId, - grantor_reissuance_token_asset: AssetId, - ) -> Self { - Self { - start_time, - expiry_time, - collateral_per_contract, - settlement_per_contract, - collateral_asset_id: collateral_asset_id.into_inner().0, - settlement_asset_id: settlement_asset_id.into_inner().0, - option_token_asset: option_token_asset.into_inner().0, - option_reissuance_token_asset: option_reissuance_token_asset.into_inner().0, - grantor_token_asset: grantor_token_asset.into_inner().0, - grantor_reissuance_token_asset: grantor_reissuance_token_asset.into_inner().0, - } - } - - #[must_use] - pub const fn start_time(&self) -> u32 { - self.start_time - } - - #[must_use] - pub const fn expiry_time(&self) -> u32 { - self.expiry_time - } - - #[must_use] - pub const fn collateral_per_contract(&self) -> u64 { - self.collateral_per_contract - } - - #[must_use] - pub const fn settlement_per_contract(&self) -> u64 { - self.settlement_per_contract - } - - #[must_use] - /// # Panics - /// - /// Panics if internal bytes are not a valid `AssetId`. - pub fn option_token(&self) -> AssetId { - AssetId::from_slice(&self.option_token_asset) - .expect("option_token_asset must be a valid 32-byte asset id") - } - - #[must_use] - /// # Panics - /// - /// Panics if internal bytes are not a valid `AssetId`. - pub fn option_reissuance_token(&self) -> AssetId { - AssetId::from_slice(&self.option_reissuance_token_asset) - .expect("option_reissuance_token_asset must be a valid 32-byte asset id") - } - - #[must_use] - /// # Panics - /// - /// Panics if internal bytes are not a valid `AssetId`. - pub fn grantor_token(&self) -> AssetId { - AssetId::from_slice(&self.grantor_token_asset) - .expect("grantor_token_asset must be a valid 32-byte asset id") - } - - #[must_use] - /// # Panics - /// - /// Panics if internal bytes are not a valid `AssetId`. - pub fn grantor_reissuance_token(&self) -> AssetId { - AssetId::from_slice(&self.grantor_reissuance_token_asset) - .expect("grantor_reissuance_token_asset must be a valid 32-byte asset id") - } - - #[must_use] - pub fn option_token_ids(&self) -> (AssetId, AssetId) { - (self.option_token(), self.option_reissuance_token()) - } - - #[must_use] - pub fn grantor_token_ids(&self) -> (AssetId, AssetId) { - (self.grantor_token(), self.grantor_reissuance_token()) - } - - #[must_use] - pub fn build_arguments(&self) -> Arguments { - Arguments::from(HashMap::from([ - ( - WitnessName::from_str_unchecked("START_TIME"), - simplicityhl::Value::from(UIntValue::U32(self.start_time)), - ), - ( - WitnessName::from_str_unchecked("EXPIRY_TIME"), - simplicityhl::Value::from(UIntValue::U32(self.expiry_time)), - ), - ( - WitnessName::from_str_unchecked("COLLATERAL_PER_CONTRACT"), - simplicityhl::Value::from(UIntValue::U64(self.collateral_per_contract)), - ), - ( - WitnessName::from_str_unchecked("SETTLEMENT_PER_CONTRACT"), - simplicityhl::Value::from(UIntValue::U64(self.settlement_per_contract)), - ), - ( - WitnessName::from_str_unchecked("COLLATERAL_ASSET_ID"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.collateral_asset_id, - ))), - ), - ( - WitnessName::from_str_unchecked("SETTLEMENT_ASSET_ID"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.settlement_asset_id, - ))), - ), - ( - WitnessName::from_str_unchecked("OPTION_TOKEN_ASSET"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.option_token_asset, - ))), - ), - ( - WitnessName::from_str_unchecked("OPTION_REISSUANCE_TOKEN_ASSET"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.option_reissuance_token_asset, - ))), - ), - ( - WitnessName::from_str_unchecked("GRANTOR_TOKEN_ASSET"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.grantor_token_asset, - ))), - ), - ( - WitnessName::from_str_unchecked("GRANTOR_REISSUANCE_TOKEN_ASSET"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.grantor_reissuance_token_asset, - ))), - ), - ])) - } - - /// Build struct from Simplicity arguments. - /// - /// # Errors - /// - /// Returns error if any required parameter is missing or has wrong type. - pub fn from_arguments(args: &Arguments) -> Result { - let start_time_name = WitnessName::from_str_unchecked("START_TIME"); - let expiry_time_name = WitnessName::from_str_unchecked("EXPIRY_TIME"); - let collateral_per_contract_name = - WitnessName::from_str_unchecked("COLLATERAL_PER_CONTRACT"); - let settlement_per_contract_name = - WitnessName::from_str_unchecked("SETTLEMENT_PER_CONTRACT"); - let collateral_asset_id_name = WitnessName::from_str_unchecked("COLLATERAL_ASSET_ID"); - let settlement_asset_id_name = WitnessName::from_str_unchecked("SETTLEMENT_ASSET_ID"); - let option_token_asset_name = WitnessName::from_str_unchecked("OPTION_TOKEN_ASSET"); - let option_reissuance_token_asset_name = - WitnessName::from_str_unchecked("OPTION_REISSUANCE_TOKEN_ASSET"); - let grantor_token_asset_name = WitnessName::from_str_unchecked("GRANTOR_TOKEN_ASSET"); - let grantor_reissuance_token_asset_name = - WitnessName::from_str_unchecked("GRANTOR_REISSUANCE_TOKEN_ASSET"); - - let start_time = extract_u32(args, &start_time_name)?; - let expiry_time = extract_u32(args, &expiry_time_name)?; - let collateral_per_contract = extract_u64(args, &collateral_per_contract_name)?; - let settlement_per_contract = extract_u64(args, &settlement_per_contract_name)?; - let collateral_asset_id = extract_u256_bytes(args, &collateral_asset_id_name)?; - let settlement_asset_id = extract_u256_bytes(args, &settlement_asset_id_name)?; - let option_token_asset = extract_u256_bytes(args, &option_token_asset_name)?; - let option_reissuance_token_asset = - extract_u256_bytes(args, &option_reissuance_token_asset_name)?; - let grantor_token_asset = extract_u256_bytes(args, &grantor_token_asset_name)?; - let grantor_reissuance_token_asset = - extract_u256_bytes(args, &grantor_reissuance_token_asset_name)?; - - Ok(Self { - start_time, - expiry_time, - collateral_per_contract, - settlement_per_contract, - collateral_asset_id, - settlement_asset_id, - option_token_asset, - option_reissuance_token_asset, - grantor_token_asset, - grantor_reissuance_token_asset, - }) - } - - #[must_use] - /// # Panics - /// - /// Panics if internal bytes are not a valid `AssetId`. - pub fn settlement_asset_id(&self) -> AssetId { - AssetId::from_slice(&self.settlement_asset_id) - .expect("settlement_asset_id must be a valid 32-byte asset id") - } - - #[must_use] - /// # Panics - /// - /// Panics if internal bytes are not a valid `AssetId`. - pub fn collateral_asset_id(&self) -> AssetId { - AssetId::from_slice(&self.collateral_asset_id) - .expect("collateral_asset_id must be a valid 32-byte asset id") - } -} - -impl wallet_abi::Encodable for OptionsArguments {} - -#[cfg(test)] -mod tests { - use super::*; - use simplicityhl::value::{UIntValue, ValueInner}; - use wallet_abi::Encodable; - - const NETWORK: ::wallet_abi::Network = ::wallet_abi::Network::TestnetLiquid; - - fn make_full_args() -> anyhow::Result { - Ok(OptionsArguments::new( - 10, - 50, - 100, - 1000, - *NETWORK.policy_asset(), - AssetId::from_slice(&[2; 32])?, - AssetId::from_slice(&[4; 32])?, - AssetId::from_slice(&[5; 32])?, - AssetId::from_slice(&[6; 32])?, - AssetId::from_slice(&[7; 32])?, - )) - } - - fn assert_is_u32(arguments: &Arguments, name: &str) { - let value = arguments - .get(&WitnessName::from_str_unchecked(name)) - .unwrap_or_else(|| panic!("{name} is missing")); - assert!( - matches!(value.inner(), ValueInner::UInt(UIntValue::U32(_))), - "{name} should be U32" - ); - } - - fn assert_is_u64(arguments: &Arguments, name: &str) { - let value = arguments - .get(&WitnessName::from_str_unchecked(name)) - .unwrap_or_else(|| panic!("{name} is missing")); - assert!( - matches!(value.inner(), ValueInner::UInt(UIntValue::U64(_))), - "{name} should be U64" - ); - } - - fn assert_is_u256(arguments: &Arguments, name: &str) { - let value = arguments - .get(&WitnessName::from_str_unchecked(name)) - .unwrap_or_else(|| panic!("{name} is missing")); - assert!( - matches!(value.inner(), ValueInner::UInt(UIntValue::U256(_))), - "{name} should be U256" - ); - } - - #[test] - fn test_serialize_deserialize_default() -> anyhow::Result<()> { - let args = OptionsArguments::default(); - let serialized = args.encode()?; - let deserialized = OptionsArguments::decode(&serialized)?; - assert_eq!(args, deserialized); - assert_eq!(deserialized.build_arguments().iter().count(), 10); - Ok(()) - } - - #[test] - fn test_serialize_deserialize_full() -> anyhow::Result<()> { - let args = make_full_args()?; - let serialized = args.encode()?; - let deserialized = OptionsArguments::decode(&serialized)?; - assert_eq!(args, deserialized); - assert_eq!(deserialized.start_time(), 10); - assert_eq!(deserialized.expiry_time(), 50); - assert_eq!(deserialized.collateral_per_contract(), 100); - assert_eq!(deserialized.settlement_per_contract(), 1000); - assert_eq!(deserialized.collateral_asset_id(), *NETWORK.policy_asset()); - assert_eq!( - deserialized.settlement_asset_id(), - AssetId::from_slice(&[2; 32])? - ); - assert_eq!(deserialized.option_token(), AssetId::from_slice(&[4; 32])?); - assert_eq!( - deserialized.option_reissuance_token(), - AssetId::from_slice(&[5; 32])? - ); - assert_eq!(deserialized.grantor_token(), AssetId::from_slice(&[6; 32])?); - assert_eq!( - deserialized.grantor_reissuance_token(), - AssetId::from_slice(&[7; 32])? - ); - Ok(()) - } - - #[test] - fn test_arguments_roundtrip_default() -> anyhow::Result<()> { - let original = OptionsArguments::default(); - let recovered = OptionsArguments::from_arguments(&original.build_arguments())?; - assert_eq!(original, recovered); - Ok(()) - } - - #[test] - fn test_arguments_roundtrip_full() -> anyhow::Result<()> { - let original = make_full_args()?; - let recovered = OptionsArguments::from_arguments(&original.build_arguments())?; - assert_eq!(original, recovered); - Ok(()) - } - - #[test] - fn test_build_arguments_keys_and_value_types() -> anyhow::Result<()> { - let arguments = make_full_args()?.build_arguments(); - - assert_eq!(arguments.iter().count(), 10); - - assert_is_u32(&arguments, "START_TIME"); - assert_is_u32(&arguments, "EXPIRY_TIME"); - assert_is_u64(&arguments, "COLLATERAL_PER_CONTRACT"); - assert_is_u64(&arguments, "SETTLEMENT_PER_CONTRACT"); - assert_is_u256(&arguments, "COLLATERAL_ASSET_ID"); - assert_is_u256(&arguments, "SETTLEMENT_ASSET_ID"); - assert_is_u256(&arguments, "OPTION_TOKEN_ASSET"); - assert_is_u256(&arguments, "OPTION_REISSUANCE_TOKEN_ASSET"); - assert_is_u256(&arguments, "GRANTOR_TOKEN_ASSET"); - assert_is_u256(&arguments, "GRANTOR_REISSUANCE_TOKEN_ASSET"); - - Ok(()) - } -} diff --git a/crates/contracts/src/finance/options/build_witness.rs b/crates/contracts/src/finance/options/build_witness.rs deleted file mode 100644 index 7bc6a90..0000000 --- a/crates/contracts/src/finance/options/build_witness.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::collections::HashMap; - -use simplicityhl::{ - ResolvedType, WitnessValues, elements::TxOutSecrets, num::U256, parse::ParseFromStr, - str::WitnessName, types::TypeConstructible, -}; - -/// Extract (`asset_bf`, `value_bf`) as U256 from `TxOutSecrets`. -#[must_use] -pub fn blinding_factors_from_secrets(secrets: &TxOutSecrets) -> (U256, U256) { - ( - U256::from_byte_array(*secrets.asset_bf.into_inner().as_ref()), - U256::from_byte_array(*secrets.value_bf.into_inner().as_ref()), - ) -} - -#[derive(Debug, Clone)] -#[allow(clippy::large_enum_variant)] -pub enum OptionBranch { - Funding { - 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, - }, - Exercise { - is_change_needed: bool, - amount_to_burn: u64, - collateral_amount_to_get: u64, - asset_amount: u64, - }, - Settlement { - is_change_needed: bool, - grantor_token_amount_to_burn: u64, - asset_amount: u64, - }, - Expiry { - is_change_needed: bool, - grantor_token_amount_to_burn: u64, - collateral_amount_to_withdraw: u64, - }, - Cancellation { - is_change_needed: bool, - amount_to_burn: u64, - collateral_amount_to_withdraw: u64, - }, -} - -/// Build witness values for options program execution. -/// -/// # Panics -/// -/// Panics if internal static type descriptors or generated witness values fail -/// to parse. -#[must_use] -#[allow(clippy::too_many_lines)] -pub fn build_option_witness(branch: &OptionBranch) -> WitnessValues { - let single = - ResolvedType::parse_from_str("(u64, u256, u256, u256, u256, u256, u256, u256, u256)") - .unwrap(); - let quadruple = ResolvedType::parse_from_str("(bool, u64, u64, u64)").unwrap(); - let triple = ResolvedType::parse_from_str("(bool, u64, u64)").unwrap(); - - let exercise_or_settlement_type = ResolvedType::either(quadruple, triple.clone()); - let left_type = ResolvedType::either(single, exercise_or_settlement_type); - let right_type = ResolvedType::either(triple.clone(), triple); - let path_type = ResolvedType::either(left_type, right_type); - - let branch_str = match branch { - OptionBranch::Funding { - 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, - } => format!( - "Left(Left(({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})))" - ), - OptionBranch::Exercise { - is_change_needed, - amount_to_burn, - collateral_amount_to_get: collateral_amount, - asset_amount, - } => format!( - "Left(Right(Left(({is_change_needed}, {amount_to_burn}, {collateral_amount}, {asset_amount}))))" - ), - OptionBranch::Settlement { - is_change_needed, - grantor_token_amount_to_burn, - asset_amount, - } => format!( - "Left(Right(Right(({is_change_needed}, {grantor_token_amount_to_burn}, {asset_amount}))))" - ), - OptionBranch::Expiry { - is_change_needed, - grantor_token_amount_to_burn, - collateral_amount_to_withdraw: collateral_amount, - } => format!( - "Right(Left(({is_change_needed}, {grantor_token_amount_to_burn}, {collateral_amount})))" - ), - OptionBranch::Cancellation { - is_change_needed, - amount_to_burn, - collateral_amount_to_withdraw: collateral_amount, - } => format!("Right(Right(({is_change_needed}, {amount_to_burn}, {collateral_amount})))"), - }; - - let mut witness_map = HashMap::new(); - - witness_map.insert( - WitnessName::from_str_unchecked("PATH"), - simplicityhl::Value::parse_from_str(&branch_str, &path_type).unwrap(), - ); - - simplicityhl::WitnessValues::from(witness_map) -} diff --git a/crates/contracts/src/finance/options/mod.rs b/crates/contracts/src/finance/options/mod.rs deleted file mode 100644 index 2fdb1c2..0000000 --- a/crates/contracts/src/finance/options/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -#![allow(clippy::similar_names)] - -use wallet_abi::{Network, ProgramError, create_p2tr_address, load_program}; - -use simplicityhl::elements::Address; - -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; - -use simplicityhl::{CompiledProgram, TemplateProgram}; - -pub mod build_arguments; -pub mod build_witness; - -pub use build_arguments::OptionsArguments; - -pub const OPTIONS_SOURCE: &str = include_str!("source_simf/options.simf"); - -/// Get the options template program for instantiation. -/// -/// # Panics -/// -/// Panics if the embedded source fails to compile. -#[must_use] -pub fn get_options_template_program() -> TemplateProgram { - TemplateProgram::new(OPTIONS_SOURCE) - .expect("INTERNAL: expected Options Program to compile successfully.") -} - -/// Derive P2TR address for an options contract. -/// -/// # Errors -/// -/// Returns error if program compilation fails. -pub fn get_options_address( - x_only_public_key: &XOnlyPublicKey, - arguments: &OptionsArguments, - network: Network, -) -> Result { - Ok(create_p2tr_address( - get_options_program(arguments)?.commit().cmr(), - x_only_public_key, - network.address_params(), - )) -} - -/// Compile options program with the given arguments. -/// -/// # Errors -/// -/// Returns error if compilation fails. -pub fn get_options_program(arguments: &OptionsArguments) -> Result { - load_program(OPTIONS_SOURCE, arguments.build_arguments()) -} - -/// Get compiled options program, panicking on failure. -/// -/// # Panics -/// -/// Panics if program instantiation fails. -#[must_use] -pub fn get_compiled_options_program(arguments: &OptionsArguments) -> CompiledProgram { - let program = get_options_template_program(); - - program - .instantiate(arguments.build_arguments(), true) - .expect("INTERNAL: expected Options Program instantiation to succeed") -} diff --git a/crates/contracts/src/lib.rs b/crates/contracts/src/lib.rs index b37a7e0..65a4ea3 100644 --- a/crates/contracts/src/lib.rs +++ b/crates/contracts/src/lib.rs @@ -1,19 +1,12 @@ #![warn(clippy::all, clippy::pedantic)] extern crate core; - -pub mod error; - -#[cfg(any(feature = "finance-option-offer", feature = "finance-options"))] -pub mod finance; +#[rustfmt::skip] +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] +pub mod artifacts; +pub mod programs; +pub mod simplicityhl_core; pub mod state_management; -mod utils; - -#[cfg(feature = "finance-option-offer")] -pub use finance::option_offer; -#[cfg(feature = "finance-options")] -pub use finance::options; - #[cfg(feature = "array-tr-storage")] pub use state_management::array_tr_storage; #[cfg(feature = "bytes32-tr-storage")] @@ -22,3 +15,5 @@ pub use state_management::bytes32_tr_storage; pub use state_management::simple_storage; #[cfg(feature = "smt-storage")] pub use state_management::smt_storage; + +pub use simplicityhl_core::*; diff --git a/crates/contracts/src/programs/mod.rs b/crates/contracts/src/programs/mod.rs new file mode 100644 index 0000000..5417bf7 --- /dev/null +++ b/crates/contracts/src/programs/mod.rs @@ -0,0 +1,3 @@ +pub mod option_offer; +pub mod options; +pub mod program; diff --git a/crates/contracts/src/programs/option_offer.rs b/crates/contracts/src/programs/option_offer.rs new file mode 100644 index 0000000..62130a0 --- /dev/null +++ b/crates/contracts/src/programs/option_offer.rs @@ -0,0 +1,118 @@ +use crate::artifacts::option_offer::OptionOfferProgram; +use crate::artifacts::option_offer::derived_option_offer::{ + OptionOfferArguments, OptionOfferWitness, +}; +use crate::programs::program::SimplexProgram; + +use simplex::constants::DUMMY_SIGNATURE; +use simplex::either::{Left, Right}; +use simplex::program::Program; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::{AssetId, secp256k1_zkp::XOnlyPublicKey}; + +#[derive(Debug, Clone, Copy)] +pub struct OptionOfferParameters { + pub collateral_asset_id: AssetId, + pub premium_asset_id: AssetId, + pub settlement_asset_id: AssetId, + pub collateral_per_contract: u64, + pub premium_per_collateral: u64, + pub expiry_time: u32, + pub user_pubkey: XOnlyPublicKey, + pub network: SimplicityNetwork, +} + +impl From for OptionOfferArguments { + fn from(value: OptionOfferParameters) -> Self { + Self { + collateral_asset_id: value.collateral_asset_id.into_inner().0, + premium_asset_id: value.premium_asset_id.into_inner().0, + settlement_asset_id: value.settlement_asset_id.into_inner().0, + collateral_per_contract: value.collateral_per_contract, + premium_per_collateral: value.premium_per_collateral, + expiry_time: value.expiry_time, + user_pubkey: value.user_pubkey.serialize(), + } + } +} + +pub struct OptionOffer { + program: OptionOfferProgram, + pub parameters: OptionOfferParameters, +} + +#[derive(Debug, Clone, Copy)] +pub enum OptionOfferBranch { + /// Exercise path: counterparty swaps settlement asset for collateral + premium + Exercise { + /// Amount of collateral the counterparty will receive (premium derived from ratio) + collateral_amount: u64, + /// Whether there's change (partial swap) + is_change_needed: bool, + }, + /// Withdraw path: user withdraws settlement asset + Withdraw, + /// Expiry path: user reclaims collateral + premium after expiry + Expiry, +} + +impl OptionOffer { + #[must_use] + pub fn new(parameters: OptionOfferParameters) -> Self { + Self { + program: OptionOfferProgram::new(OptionOfferArguments::from(parameters)), + parameters, + } + } + + #[must_use] + pub fn from_internal_key( + internal_key: XOnlyPublicKey, + parameters: OptionOfferParameters, + ) -> Self { + Self { + program: OptionOfferProgram::new(OptionOfferArguments::from(parameters)) + .with_pub_key(internal_key), + parameters, + } + } + + #[must_use] + pub const fn calculate_per_params( + collateral_amount_to_deposit: u64, + expected_settlement: u64, + expected_premium: u64, + ) -> (Option, Option) { + let collateral_per_contract = expected_settlement.checked_div(collateral_amount_to_deposit); + let premium_per_collateral = expected_premium.checked_div(collateral_amount_to_deposit); + + (collateral_per_contract, premium_per_collateral) + } + + #[must_use] + pub const fn get_witness(option_offer_branch: OptionOfferBranch) -> OptionOfferWitness { + let path = match option_offer_branch { + OptionOfferBranch::Exercise { + collateral_amount, + is_change_needed, + } => Left((collateral_amount, is_change_needed)), + OptionOfferBranch::Withdraw => Right(Left(())), + OptionOfferBranch::Expiry => Right(Right(())), + }; + + OptionOfferWitness { + user_sighash_all: DUMMY_SIGNATURE, + path, + } + } +} + +impl SimplexProgram for OptionOffer { + fn get_program(&self) -> &Program { + self.program.as_ref() + } + + fn get_network(&self) -> &SimplicityNetwork { + &self.parameters.network + } +} diff --git a/crates/contracts/src/programs/options.rs b/crates/contracts/src/programs/options.rs new file mode 100644 index 0000000..e06e456 --- /dev/null +++ b/crates/contracts/src/programs/options.rs @@ -0,0 +1,196 @@ +use simplex::either::{Left, Right}; +use simplex::program::Program; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::{AssetId, secp256k1_zkp::XOnlyPublicKey}; + +use crate::artifacts::options::OptionsProgram; +use crate::artifacts::options::derived_options::{OptionsArguments, OptionsWitness}; +use crate::programs::program::SimplexProgram; + +#[derive(Debug, Clone, Copy)] +pub struct OptionsParameters { + pub start_time: u32, + pub expiry_time: u32, + pub collateral_per_contract: u64, + pub settlement_per_contract: u64, + pub collateral_asset_id: AssetId, + pub settlement_asset_id: AssetId, + pub option_token_asset: AssetId, + pub option_reissuance_token_asset: AssetId, + pub grantor_token_asset: AssetId, + pub grantor_reissuance_token_asset: AssetId, + pub network: SimplicityNetwork, +} + +impl From for OptionsArguments { + fn from(value: OptionsParameters) -> Self { + Self { + start_time: value.start_time, + expiry_time: value.expiry_time, + collateral_per_contract: value.collateral_per_contract, + settlement_per_contract: value.settlement_per_contract, + collateral_asset_id: value.collateral_asset_id.into_inner().0, + settlement_asset_id: value.settlement_asset_id.into_inner().0, + option_token_asset: value.option_token_asset.into_inner().0, + option_reissuance_token_asset: value.option_reissuance_token_asset.into_inner().0, + grantor_token_asset: value.grantor_token_asset.into_inner().0, + grantor_reissuance_token_asset: value.grantor_reissuance_token_asset.into_inner().0, + } + } +} + +pub struct Options { + program: OptionsProgram, + pub parameters: OptionsParameters, +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct OptionsFundingBlinders { + pub input_option_abf: [u8; 32], + pub input_option_vbf: [u8; 32], + pub input_grantor_abf: [u8; 32], + pub input_grantor_vbf: [u8; 32], + pub output_option_abf: [u8; 32], + pub output_option_vbf: [u8; 32], + pub output_grantor_abf: [u8; 32], + pub output_grantor_vbf: [u8; 32], +} + +impl OptionsFundingBlinders { + #[must_use] + pub const fn zero() -> Self { + Self { + input_option_abf: [0; 32], + input_option_vbf: [0; 32], + input_grantor_abf: [0; 32], + input_grantor_vbf: [0; 32], + output_option_abf: [0; 32], + output_option_vbf: [0; 32], + output_grantor_abf: [0; 32], + output_grantor_vbf: [0; 32], + } + } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, Copy)] +pub enum OptionsBranch { + Fund { + expected_settlement_amount: u64, + blinders: OptionsFundingBlinders, + }, + Exercise { + is_change_needed: bool, + amount_to_burn: u64, + collateral_amount: u64, + settlement_amount: u64, + }, + Settlement { + is_change_needed: bool, + amount_to_burn: u64, + settlement_amount: u64, + }, + Expiry { + is_change_needed: bool, + amount_to_burn: u64, + collateral_amount: u64, + }, + Cancel { + is_change_needed: bool, + amount_to_burn: u64, + collateral_amount: u64, + }, +} + +impl Options { + #[must_use] + pub fn new(parameters: OptionsParameters) -> Self { + Self { + program: OptionsProgram::new(OptionsArguments::from(parameters)), + parameters, + } + } + + #[must_use] + pub fn from_internal_key(internal_key: XOnlyPublicKey, parameters: OptionsParameters) -> Self { + Self { + program: OptionsProgram::new(OptionsArguments::from(parameters)) + .with_pub_key(internal_key), + parameters, + } + } + + #[must_use] + pub const fn calculate_per_contract_params( + total_collateral: u64, + expected_settlement: u64, + contract_count: u64, + ) -> (Option, Option) { + let collateral_per_contract = total_collateral.checked_div(contract_count); + let settlement_per_contract = expected_settlement.checked_div(contract_count); + + (collateral_per_contract, settlement_per_contract) + } + + #[must_use] + pub const fn get_witness(branch: OptionsBranch) -> OptionsWitness { + let path = match branch { + OptionsBranch::Fund { + expected_settlement_amount, + blinders, + } => Left(Left(( + expected_settlement_amount, + blinders.input_option_abf, + blinders.input_option_vbf, + blinders.input_grantor_abf, + blinders.input_grantor_vbf, + blinders.output_option_abf, + blinders.output_option_vbf, + blinders.output_grantor_abf, + blinders.output_grantor_vbf, + ))), + OptionsBranch::Exercise { + is_change_needed, + amount_to_burn, + collateral_amount, + settlement_amount, + } => Left(Right(Left(( + is_change_needed, + amount_to_burn, + collateral_amount, + settlement_amount, + )))), + OptionsBranch::Settlement { + is_change_needed, + amount_to_burn, + settlement_amount, + } => Left(Right(Right(( + is_change_needed, + amount_to_burn, + settlement_amount, + )))), + OptionsBranch::Expiry { + is_change_needed, + amount_to_burn, + collateral_amount, + } => Right(Left((is_change_needed, amount_to_burn, collateral_amount))), + OptionsBranch::Cancel { + is_change_needed, + amount_to_burn, + collateral_amount, + } => Right(Right((is_change_needed, amount_to_burn, collateral_amount))), + }; + + OptionsWitness { path } + } +} + +impl SimplexProgram for Options { + fn get_program(&self) -> &Program { + self.program.as_ref() + } + + fn get_network(&self) -> &SimplicityNetwork { + &self.parameters.network + } +} diff --git a/crates/contracts/src/programs/program.rs b/crates/contracts/src/programs/program.rs new file mode 100644 index 0000000..0033062 --- /dev/null +++ b/crates/contracts/src/programs/program.rs @@ -0,0 +1,17 @@ +use simplex::program::Program; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::Script; + +pub trait SimplexProgram { + fn get_script_pubkey(&self) -> Script { + self.get_program().get_script_pubkey(self.get_network()) + } + + fn get_script_hash(&self) -> [u8; 32] { + self.get_program().get_script_hash(self.get_network()) + } + + fn get_program(&self) -> &Program; + + fn get_network(&self) -> &SimplicityNetwork; +} diff --git a/crates/contracts/src/simplicityhl_core/error.rs b/crates/contracts/src/simplicityhl_core/error.rs new file mode 100644 index 0000000..7822336 --- /dev/null +++ b/crates/contracts/src/simplicityhl_core/error.rs @@ -0,0 +1,79 @@ +#[derive(Debug, thiserror::Error)] +pub enum WalletAbiError { + #[error("Invalid request: {0}")] + InvalidRequest(String), + + #[error("JSON error: {0}")] + Serde(#[from] serde_json::Error), + + #[error("Program error: {0}")] + Program(#[from] ProgramError), + + #[error("Bitcoin derivation: {0}")] + Derivation(#[from] simplex::simplicityhl::elements::bitcoin::bip32::Error), + + #[error("Int conversion: {0}")] + TryFromInt(#[from] std::num::TryFromIntError), + + #[error("Failed to fund the request: {0}")] + Funding(String), + + #[error("Runtime signer configuration: {0}")] + InvalidSignerConfig(String), + + #[error("Runtime response construction: {0}")] + InvalidResponse(String), + + #[error("PSET error: {0}")] + Pset(#[from] simplex::simplicityhl::elements::pset::Error), + + #[error("PSET blinding error: {0}")] + PsetBlind(#[from] simplex::simplicityhl::elements::pset::PsetBlindError), + + #[error("Transaction amount proof verification failed: {0}")] + AmountProofVerification(#[from] simplex::simplicityhl::elements::VerificationError), + + #[error("Invalid finalization steps: {0}")] + InvalidFinalizationSteps(String), +} + +/// 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(simplex::simplicityhl::simplicity::bit_machine::ExecutionError), + + #[error("Failed to construct a Bit Machine with enough space: {0}")] + BitMachineCreation(#[from] simplex::simplicityhl::simplicity::bit_machine::LimitError), + + #[error("Failed to execute program on the Bit Machine: {0}")] + Execution(simplex::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/contracts/src/simplicityhl_core/mod.rs b/crates/contracts/src/simplicityhl_core/mod.rs new file mode 100644 index 0000000..4c632a7 --- /dev/null +++ b/crates/contracts/src/simplicityhl_core/mod.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod runner; +pub mod scripts; diff --git a/crates/contracts/src/simplicityhl_core/runner.rs b/crates/contracts/src/simplicityhl_core/runner.rs new file mode 100644 index 0000000..40d1a81 --- /dev/null +++ b/crates/contracts/src/simplicityhl_core/runner.rs @@ -0,0 +1,38 @@ +use std::sync::Arc; + +use simplex::simplicityhl::simplicity::elements::Transaction; +use simplex::simplicityhl::simplicity::jet::Elements; +use simplex::simplicityhl::simplicity::jet::elements::ElementsEnv; +use simplex::simplicityhl::simplicity::{BitMachine, RedeemNode, Value}; +use simplex::simplicityhl::tracker::{DefaultTracker, TrackerLogLevel}; +use simplex::simplicityhl::{CompiledProgram, WitnessValues}; + +use super::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) + .map_err(ProgramError::Pruning)?; + let mut mac = BitMachine::for_program(&pruned)?; + + let result = mac.exec(&pruned, env).map_err(ProgramError::Execution)?; + + Ok((pruned, result)) +} diff --git a/crates/contracts/src/simplicityhl_core/scripts.rs b/crates/contracts/src/simplicityhl_core/scripts.rs new file mode 100644 index 0000000..221f9a7 --- /dev/null +++ b/crates/contracts/src/simplicityhl_core/scripts.rs @@ -0,0 +1,105 @@ +use simplex::simplicityhl::elements::{Address, AddressParams, Script, taproot}; + +use simplex::simplicityhl::simplicity::bitcoin::{XOnlyPublicKey, secp256k1}; +use simplex::simplicityhl::simplicity::hashes::{Hash, HashEngine, sha256}; +use simplex::simplicityhl::{Arguments, CompiledProgram}; + +use super::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: simplex::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, + ) +} + +/// Return the version of Simplicity leaves inside a tap tree. +#[must_use] +pub fn simplicity_leaf_version() -> taproot::LeafVersion { + simplex::simplicityhl::simplicity::leaf_version() +} + +/// Create a SHA256 context, initialized with a "`TapData`" tag and data +/// +/// Based on the C implementation of the `tapdata_init` jet: +/// +#[must_use] +pub fn tap_data_hash(data: &[u8]) -> sha256::Hash { + let tag = sha256::Hash::hash(b"TapData"); + let mut eng = sha256::Hash::engine(); + eng.input(tag.as_byte_array()); + eng.input(tag.as_byte_array()); + eng.input(data); + sha256::Hash::from_engine(eng) +} + +/// 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: simplex::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") +} + +/// Returns pair (Script, `LeafVersion`) for the CMR of Simplicity program +fn script_version(cmr: simplex::simplicityhl::simplicity::Cmr) -> (Script, taproot::LeafVersion) { + let script = Script::from(cmr.as_ref().to_vec()); + (script, simplicity_leaf_version()) +} + +fn taproot_spending_info( + cmr: simplex::simplicityhl::simplicity::Cmr, + internal_key: XOnlyPublicKey, +) -> taproot::TaprootSpendInfo { + let (script, version) = script_version(cmr); + let builder = taproot::TaprootBuilder::new() + .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") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tap_data_hash() { + assert_eq!( + tap_data_hash([0u8; 32].as_ref()).to_string(), + "a33ad504fd45357a3909bf9dea8ce4aca38fe6e7d9c9d3e9e01211408990123f" + ); + } +} diff --git a/crates/contracts/src/state_management/array_tr_storage/build_witness.rs b/crates/contracts/src/state_management/array_tr_storage/build_witness.rs index 85efd84..6a074a9 100644 --- a/crates/contracts/src/state_management/array_tr_storage/build_witness.rs +++ b/crates/contracts/src/state_management/array_tr_storage/build_witness.rs @@ -1,16 +1,16 @@ use std::collections::HashMap; -use simplicityhl::num::U256; -use simplicityhl::types::UIntType; -use simplicityhl::value::{UIntValue, ValueConstructible}; -use simplicityhl::{WitnessValues, str::WitnessName}; +use simplex::simplicityhl::num::U256; +use simplex::simplicityhl::types::UIntType; +use simplex::simplicityhl::value::{UIntValue, ValueConstructible}; +use simplex::simplicityhl::{WitnessValues, str::WitnessName}; // Storage is represented as 3 u256 slots. // This is a constant because Simplicity cannot initialise an array using 'param'. // The value 3 enables us to demonstrate the efficiency of storage with a small number of elements. pub const MAX_VAL: usize = 3; -#[derive(Debug, Clone, bincode::Encode, bincode::Decode, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct State { pub limbs: [[u8; 32]; MAX_VAL], } @@ -24,10 +24,12 @@ impl State { } #[must_use] - pub fn to_simplicity_values(&self) -> Vec { + pub fn to_simplicity_values(&self) -> Vec { self.limbs .iter() - .map(|value| simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(*value)))) + .map(|value| { + simplex::simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(*value))) + }) .collect() } @@ -54,14 +56,14 @@ impl Default for State { pub fn build_array_tr_storage_witness(state: &State, changed_index: u16) -> WitnessValues { let values = state.to_simplicity_values(); - simplicityhl::WitnessValues::from(HashMap::from([ + simplex::simplicityhl::WitnessValues::from(HashMap::from([ ( WitnessName::from_str_unchecked("STATE"), - simplicityhl::Value::array(values, UIntType::U256.into()), + simplex::simplicityhl::Value::array(values, UIntType::U256.into()), ), ( WitnessName::from_str_unchecked("CHANGED_INDEX"), - simplicityhl::Value::from(UIntValue::U16(changed_index)), + simplex::simplicityhl::Value::from(UIntValue::U16(changed_index)), ), ])) } diff --git a/crates/contracts/src/state_management/array_tr_storage/mod.rs b/crates/contracts/src/state_management/array_tr_storage/mod.rs index 6b402b5..81f13f7 100644 --- a/crates/contracts/src/state_management/array_tr_storage/mod.rs +++ b/crates/contracts/src/state_management/array_tr_storage/mod.rs @@ -1,15 +1,20 @@ +use crate::error::ProgramError; +use crate::runner::run_program; +use crate::scripts::{simplicity_leaf_version, tap_data_hash}; + use std::sync::Arc; -use simplicityhl::simplicity::bitcoin::secp256k1; -use simplicityhl::simplicity::elements::taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo}; -use simplicityhl::simplicity::elements::{Script, Transaction}; -use simplicityhl::simplicity::hashes::sha256; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::ElementsEnv; -use simplicityhl::simplicity::{Cmr, RedeemNode}; -use simplicityhl::tracker::TrackerLogLevel; -use simplicityhl::{Arguments, CompiledProgram, TemplateProgram}; -use wallet_abi::{ProgramError, run_program, simplicity_leaf_version, tap_data_hash}; +use simplex::simplicityhl::simplicity::bitcoin::secp256k1; +use simplex::simplicityhl::simplicity::elements::taproot::{ + LeafVersion, TaprootBuilder, TaprootSpendInfo, +}; +use simplex::simplicityhl::simplicity::elements::{Script, Transaction}; +use simplex::simplicityhl::simplicity::hashes::sha256; +use simplex::simplicityhl::simplicity::jet::Elements; +use simplex::simplicityhl::simplicity::jet::elements::ElementsEnv; +use simplex::simplicityhl::simplicity::{Cmr, RedeemNode}; +use simplex::simplicityhl::tracker::TrackerLogLevel; +use simplex::simplicityhl::{Arguments, CompiledProgram, TemplateProgram}; mod build_witness; @@ -113,12 +118,12 @@ mod array_tr_storage_tests { use anyhow::Result; use std::sync::Arc; - use simplicityhl::elements::confidential::{Asset, Value}; - use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; - use simplicityhl::elements::{AssetId, BlockHash, OutPoint, Script, Txid}; - use simplicityhl::simplicity::elements::taproot::ControlBlock; - use simplicityhl::simplicity::hashes::Hash as _; - use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; + use simplex::simplicityhl::elements::confidential::{Asset, Value}; + use simplex::simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; + use simplex::simplicityhl::elements::{AssetId, BlockHash, OutPoint, Script, Txid}; + use simplex::simplicityhl::simplicity::elements::taproot::ControlBlock; + use simplex::simplicityhl::simplicity::hashes::Hash as _; + use simplex::simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; #[rustfmt::skip] // mangles byte vectors fn array_tr_storage_unspendable_internal_key() -> secp256k1::XOnlyPublicKey { diff --git a/crates/contracts/src/state_management/bytes32_tr_storage/build_witness.rs b/crates/contracts/src/state_management/bytes32_tr_storage/build_witness.rs index 995c1d9..d8e3992 100644 --- a/crates/contracts/src/state_management/bytes32_tr_storage/build_witness.rs +++ b/crates/contracts/src/state_management/bytes32_tr_storage/build_witness.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; -use simplicityhl::num::U256; -use simplicityhl::{WitnessValues, str::WitnessName, value::UIntValue}; +use simplex::simplicityhl::num::U256; +use simplex::simplicityhl::{WitnessValues, str::WitnessName, value::UIntValue}; #[must_use] pub fn build_bytes32_tr_witness(state: [u8; 32]) -> WitnessValues { - simplicityhl::WitnessValues::from(HashMap::from([( + simplex::simplicityhl::WitnessValues::from(HashMap::from([( WitnessName::from_str_unchecked("STATE"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(state))), + simplex::simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(state))), )])) } diff --git a/crates/contracts/src/state_management/bytes32_tr_storage/mod.rs b/crates/contracts/src/state_management/bytes32_tr_storage/mod.rs index 4dc6310..f8a1c16 100644 --- a/crates/contracts/src/state_management/bytes32_tr_storage/mod.rs +++ b/crates/contracts/src/state_management/bytes32_tr_storage/mod.rs @@ -1,14 +1,19 @@ use std::sync::Arc; -use simplicityhl::simplicity::bitcoin::secp256k1; -use simplicityhl::simplicity::elements::taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo}; -use simplicityhl::simplicity::elements::{Script, Transaction}; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::ElementsEnv; -use simplicityhl::simplicity::{Cmr, RedeemNode}; -use simplicityhl::tracker::TrackerLogLevel; -use simplicityhl::{CompiledProgram, TemplateProgram}; -use wallet_abi::{ProgramError, run_program, simplicity_leaf_version, tap_data_hash}; +use crate::error::ProgramError; +use crate::runner::run_program; +use crate::scripts::{simplicity_leaf_version, tap_data_hash}; + +use simplex::simplicityhl::simplicity::bitcoin::secp256k1; +use simplex::simplicityhl::simplicity::elements::taproot::{ + LeafVersion, TaprootBuilder, TaprootSpendInfo, +}; +use simplex::simplicityhl::simplicity::elements::{Script, Transaction}; +use simplex::simplicityhl::simplicity::jet::Elements; +use simplex::simplicityhl::simplicity::jet::elements::ElementsEnv; +use simplex::simplicityhl::simplicity::{Cmr, RedeemNode}; +use simplex::simplicityhl::tracker::TrackerLogLevel; +use simplex::simplicityhl::{CompiledProgram, TemplateProgram}; mod build_witness; @@ -36,7 +41,7 @@ pub fn get_bytes32_tr_compiled_program() -> CompiledProgram { let program = get_bytes32_tr_template_program(); program - .instantiate(simplicityhl::Arguments::default(), true) + .instantiate(simplex::simplicityhl::Arguments::default(), true) .unwrap() } @@ -115,12 +120,12 @@ mod bytes32_tr_tests { use anyhow::Result; use std::sync::Arc; - use simplicityhl::elements::confidential::{Asset, Value}; - use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; - use simplicityhl::elements::{self, AssetId, OutPoint, Script, Txid}; - use simplicityhl::simplicity::elements::taproot::ControlBlock; - use simplicityhl::simplicity::hashes::Hash as _; - use simplicityhl::simplicity::jet::elements::ElementsEnv; + use simplex::simplicityhl::elements::confidential::{Asset, Value}; + use simplex::simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; + use simplex::simplicityhl::elements::{self, AssetId, OutPoint, Script, Txid}; + use simplex::simplicityhl::simplicity::elements::taproot::ControlBlock; + use simplex::simplicityhl::simplicity::hashes::Hash as _; + use simplex::simplicityhl::simplicity::jet::elements::ElementsEnv; #[test] fn test_bytes32_tr_mint_path() -> Result<()> { @@ -160,11 +165,13 @@ mod bytes32_tr_tests { // Set up environment let env = ElementsEnv::new( Arc::new(pst.extract_tx()?), - vec![simplicityhl::simplicity::jet::elements::ElementsUtxo { - script_pubkey: old_script_pubkey, - asset: Asset::default(), - value: Value::default(), - }], + vec![ + simplex::simplicityhl::simplicity::jet::elements::ElementsUtxo { + script_pubkey: old_script_pubkey, + asset: Asset::default(), + value: Value::default(), + }, + ], 0, cmr, ControlBlock::from_slice(&control_block.serialize())?, // Real control block diff --git a/crates/contracts/src/state_management/simple_storage/build_arguments.rs b/crates/contracts/src/state_management/simple_storage/build_arguments.rs index 9ba23d6..af51a56 100644 --- a/crates/contracts/src/state_management/simple_storage/build_arguments.rs +++ b/crates/contracts/src/state_management/simple_storage/build_arguments.rs @@ -1,11 +1,10 @@ use std::collections::HashMap; -use hex::FromHex; +use simplex::simplicityhl::elements::hex::FromHex; +use simplex::simplicityhl::num::U256; +use simplex::simplicityhl::{Arguments, str::WitnessName, value::UIntValue}; -use simplicityhl::num::U256; -use simplicityhl::{Arguments, str::WitnessName, value::UIntValue}; - -#[derive(Debug, Clone, bincode::Encode, bincode::Decode, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct StorageArguments { public_key: [u8; 32], slot_asset: String, @@ -46,13 +45,13 @@ pub fn build_storage_arguments(args: &StorageArguments) -> Arguments { Arguments::from(HashMap::from([ ( WitnessName::from_str_unchecked("SLOT_ID"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(slot_id))), + simplex::simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(slot_id))), ), ( WitnessName::from_str_unchecked("USER"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(args.public_key))), + simplex::simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( + args.public_key, + ))), ), ])) } - -impl wallet_abi::Encodable for StorageArguments {} diff --git a/crates/contracts/src/state_management/simple_storage/build_witness.rs b/crates/contracts/src/state_management/simple_storage/build_witness.rs index bebab65..9049c2b 100644 --- a/crates/contracts/src/state_management/simple_storage/build_witness.rs +++ b/crates/contracts/src/state_management/simple_storage/build_witness.rs @@ -1,22 +1,22 @@ use std::collections::HashMap; -use simplicityhl::simplicity::bitcoin; -use simplicityhl::value::ValueConstructible; -use simplicityhl::{WitnessValues, str::WitnessName, value::UIntValue}; +use simplex::simplicityhl::simplicity::bitcoin; +use simplex::simplicityhl::value::ValueConstructible; +use simplex::simplicityhl::{WitnessValues, str::WitnessName, value::UIntValue}; #[must_use] pub fn build_storage_witness( new_value: u64, signature: &bitcoin::secp256k1::schnorr::Signature, ) -> WitnessValues { - simplicityhl::WitnessValues::from(HashMap::from([ + simplex::simplicityhl::WitnessValues::from(HashMap::from([ ( WitnessName::from_str_unchecked("NEW_VALUE"), - simplicityhl::Value::from(UIntValue::U64(new_value)), + simplex::simplicityhl::Value::from(UIntValue::U64(new_value)), ), ( WitnessName::from_str_unchecked("USER_SIGNATURE"), - simplicityhl::Value::byte_array(signature.serialize()), + simplex::simplicityhl::Value::byte_array(signature.serialize()), ), ])) } diff --git a/crates/contracts/src/state_management/simple_storage/mod.rs b/crates/contracts/src/state_management/simple_storage/mod.rs index 519e44b..b29bdb5 100644 --- a/crates/contracts/src/state_management/simple_storage/mod.rs +++ b/crates/contracts/src/state_management/simple_storage/mod.rs @@ -1,16 +1,19 @@ use std::sync::Arc; -use wallet_abi::{ProgramError, create_p2tr_address, load_program, run_program}; - -use simplicityhl::simplicity::RedeemNode; -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; -use simplicityhl::simplicity::bitcoin::key::Keypair; -use simplicityhl::simplicity::bitcoin::secp256k1; -use simplicityhl::simplicity::elements::{Address, AddressParams, Transaction}; -use simplicityhl::simplicity::hashes::Hash; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::ElementsEnv; -use simplicityhl::tracker::TrackerLogLevel; -use simplicityhl::{CompiledProgram, TemplateProgram}; + +use crate::error::ProgramError; +use crate::runner::run_program; +use crate::scripts::{create_p2tr_address, load_program}; + +use simplex::simplicityhl::simplicity::RedeemNode; +use simplex::simplicityhl::simplicity::bitcoin::XOnlyPublicKey; +use simplex::simplicityhl::simplicity::bitcoin::key::Keypair; +use simplex::simplicityhl::simplicity::bitcoin::secp256k1; +use simplex::simplicityhl::simplicity::elements::{Address, AddressParams, Transaction}; +use simplex::simplicityhl::simplicity::hashes::Hash as _; +use simplex::simplicityhl::simplicity::jet::Elements; +use simplex::simplicityhl::simplicity::jet::elements::ElementsEnv; +use simplex::simplicityhl::tracker::TrackerLogLevel; +use simplex::simplicityhl::{CompiledProgram, TemplateProgram}; mod build_arguments; mod build_witness; @@ -74,7 +77,8 @@ pub fn execute_storage_program( env: &ElementsEnv>, log_level: TrackerLogLevel, ) -> Result>, ProgramError> { - let sighash_all = secp256k1::Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); + let sighash_all = + secp256k1::Message::from_digest(*env.c_tx_env().sighash_all().as_byte_array()); let signature = keypair.sign_schnorr(sighash_all); let witness_values = build_storage_witness(new_value, &signature); @@ -89,15 +93,15 @@ mod simple_storage_tests { use std::sync::Arc; - use simplicityhl::elements::confidential::{Asset, Value}; - use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; - use simplicityhl::elements::{self, AssetId, OutPoint, Script, Txid}; - use simplicityhl::simplicity::bitcoin::key::Keypair; - use simplicityhl::simplicity::bitcoin::secp256k1; - use simplicityhl::simplicity::elements::taproot::ControlBlock; - use simplicityhl::simplicity::jet::elements::ElementsEnv; + use simplex::simplicityhl::elements::confidential::{Asset, Value}; + use simplex::simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; + use simplex::simplicityhl::elements::{self, AssetId, OutPoint, Script, Txid}; + use simplex::simplicityhl::simplicity::bitcoin::key::Keypair; + use simplex::simplicityhl::simplicity::bitcoin::secp256k1; + use simplex::simplicityhl::simplicity::elements::taproot::ControlBlock; + use simplex::simplicityhl::simplicity::jet::elements::ElementsEnv; - use wallet_abi::Network; + use lwk_common::Network; const NETWORK: Network = Network::TestnetLiquid; @@ -149,19 +153,19 @@ mod simple_storage_tests { let env = ElementsEnv::new( Arc::new(pst.extract_tx()?), vec![ - simplicityhl::simplicity::jet::elements::ElementsUtxo { + simplex::simplicityhl::simplicity::jet::elements::ElementsUtxo { script_pubkey: storage_address.script_pubkey(), asset: Asset::Explicit(*NETWORK.policy_asset()), value: Value::Explicit(old_value), }, - simplicityhl::simplicity::jet::elements::ElementsUtxo { + simplex::simplicityhl::simplicity::jet::elements::ElementsUtxo { script_pubkey: storage_address.script_pubkey(), asset: Asset::Explicit(AssetId::default()), value: Value::Explicit(1), }, ], 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), + simplex::simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), ControlBlock::from_slice(&[0xc0; 33])?, None, elements::BlockHash::all_zeros(), @@ -223,13 +227,15 @@ mod simple_storage_tests { let env = ElementsEnv::new( Arc::new(pst.extract_tx()?), - vec![simplicityhl::simplicity::jet::elements::ElementsUtxo { - script_pubkey: storage_address.script_pubkey(), - asset: Asset::Explicit(*NETWORK.policy_asset()), - value: Value::Explicit(old_value), - }], + vec![ + simplex::simplicityhl::simplicity::jet::elements::ElementsUtxo { + script_pubkey: storage_address.script_pubkey(), + asset: Asset::Explicit(*NETWORK.policy_asset()), + value: Value::Explicit(old_value), + }, + ], 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), + simplex::simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), ControlBlock::from_slice(&[0xc0; 33])?, None, elements::BlockHash::all_zeros(), diff --git a/crates/contracts/src/state_management/smt_storage/build_witness.rs b/crates/contracts/src/state_management/smt_storage/build_witness.rs index 7813490..1d2d043 100644 --- a/crates/contracts/src/state_management/smt_storage/build_witness.rs +++ b/crates/contracts/src/state_management/smt_storage/build_witness.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; -use simplicityhl::num::U256; -use simplicityhl::types::{ResolvedType, TypeConstructible, UIntType}; -use simplicityhl::value::{UIntValue, ValueConstructible}; -use simplicityhl::{WitnessValues, str::WitnessName}; +use simplex::simplicityhl::num::U256; +use simplex::simplicityhl::types::{ResolvedType, TypeConstructible, UIntType}; +use simplex::simplicityhl::value::{UIntValue, ValueConstructible}; +use simplex::simplicityhl::{WitnessValues, str::WitnessName}; #[allow(non_camel_case_types)] pub type u256 = [u8; 32]; @@ -14,7 +14,7 @@ pub type u256 = [u8; 32]; /// and cannot dynamically resolve array lengths using `param::LEN`. pub const DEPTH: usize = 8; -#[derive(Debug, Clone, bincode::Encode, bincode::Decode, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SMTWitness { /// The internal public key used for Taproot tweaking. /// @@ -73,39 +73,41 @@ impl Default for SMTWitness { #[must_use] pub fn build_smt_storage_witness(witness: &SMTWitness) -> WitnessValues { - let values: Vec = witness + let values: Vec = witness .merkle_data .iter() .map(|(value, is_right)| { let hash_val = - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(*value))); - let direction_val = simplicityhl::Value::from(*is_right); + simplex::simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(*value))); + let direction_val = simplex::simplicityhl::Value::from(*is_right); - simplicityhl::Value::product(hash_val, direction_val) + simplex::simplicityhl::Value::product(hash_val, direction_val) }) .collect(); - let element_type = simplicityhl::types::TypeConstructible::product( + let element_type = simplex::simplicityhl::types::TypeConstructible::product( UIntType::U256.into(), ResolvedType::boolean(), ); - simplicityhl::WitnessValues::from(HashMap::from([ + simplex::simplicityhl::WitnessValues::from(HashMap::from([ ( WitnessName::from_str_unchecked("KEY"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(witness.key))), + simplex::simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(witness.key))), ), ( WitnessName::from_str_unchecked("LEAF"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(witness.leaf))), + simplex::simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( + witness.leaf, + ))), ), ( WitnessName::from_str_unchecked("PATH_BITS"), - simplicityhl::Value::from(UIntValue::U8(witness.path_bits)), + simplex::simplicityhl::Value::from(UIntValue::U8(witness.path_bits)), ), ( WitnessName::from_str_unchecked("MERKLE_DATA"), - simplicityhl::Value::array(values, element_type), + simplex::simplicityhl::Value::array(values, element_type), ), ])) } diff --git a/crates/contracts/src/state_management/smt_storage/mod.rs b/crates/contracts/src/state_management/smt_storage/mod.rs index ecd48c9..f580dde 100644 --- a/crates/contracts/src/state_management/smt_storage/mod.rs +++ b/crates/contracts/src/state_management/smt_storage/mod.rs @@ -1,20 +1,24 @@ use std::sync::Arc; -use simplicityhl::elements::TxInWitness; -use simplicityhl::elements::TxOut; -use simplicityhl::elements::taproot::ControlBlock; -use simplicityhl::simplicity::bitcoin::secp256k1; -use simplicityhl::simplicity::elements::hashes::HashEngine as _; -use simplicityhl::simplicity::elements::taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo}; -use simplicityhl::simplicity::elements::{Script, Transaction}; -use simplicityhl::simplicity::hashes::{Hash, sha256}; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; -use simplicityhl::simplicity::{Cmr, RedeemNode}; -use simplicityhl::tracker::TrackerLogLevel; -use simplicityhl::{Arguments, CompiledProgram, TemplateProgram}; -use wallet_abi::{Network, ProgramError, run_program}; -use wallet_abi::{simplicity_leaf_version, tap_data_hash}; +use crate::error::ProgramError; +use crate::runner::run_program; +use crate::scripts::{simplicity_leaf_version, tap_data_hash}; +use lwk_common::Network; +use simplex::simplicityhl::elements::TxInWitness; +use simplex::simplicityhl::elements::TxOut; +use simplex::simplicityhl::elements::taproot::ControlBlock; +use simplex::simplicityhl::simplicity::bitcoin::secp256k1; +use simplex::simplicityhl::simplicity::elements::hashes::HashEngine as _; +use simplex::simplicityhl::simplicity::elements::taproot::{ + LeafVersion, TaprootBuilder, TaprootSpendInfo, +}; +use simplex::simplicityhl::simplicity::elements::{Script, Transaction}; +use simplex::simplicityhl::simplicity::hashes::{Hash, sha256}; +use simplex::simplicityhl::simplicity::jet::Elements; +use simplex::simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; +use simplex::simplicityhl::simplicity::{Cmr, RedeemNode}; +use simplex::simplicityhl::tracker::TrackerLogLevel; +use simplex::simplicityhl::{Arguments, CompiledProgram, TemplateProgram}; mod build_witness; mod smt; @@ -278,13 +282,13 @@ pub fn finalize_get_storage_transaction( mod smt_storage_tests { use super::*; use anyhow::Result; - use simplicityhl::elements::secp256k1_zkp::rand::{Rng, thread_rng}; + use simplex::simplicityhl::elements::secp256k1_zkp::rand::{Rng, thread_rng}; use std::sync::Arc; - use simplicityhl::elements::confidential::{Asset, Value}; - use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; - use simplicityhl::elements::{AssetId, BlockHash, OutPoint, Script, Txid}; - use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; + use simplex::simplicityhl::elements::confidential::{Asset, Value}; + use simplex::simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; + use simplex::simplicityhl::elements::{AssetId, BlockHash, OutPoint, Script, Txid}; + use simplex::simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; fn add_elements(smt: &mut SparseMerkleTree, num: u64) -> (u256, [u256; DEPTH], [bool; DEPTH]) { let mut rng = thread_rng(); diff --git a/crates/contracts/src/state_management/smt_storage/smt.rs b/crates/contracts/src/state_management/smt_storage/smt.rs index e2ce89c..f24f475 100644 --- a/crates/contracts/src/state_management/smt_storage/smt.rs +++ b/crates/contracts/src/state_management/smt_storage/smt.rs @@ -1,6 +1,6 @@ -use simplicityhl::simplicity::elements::hashes::HashEngine as _; -use simplicityhl::simplicity::hashes::{Hash, sha256}; -use wallet_abi::tap_data_hash; +use crate::scripts::tap_data_hash; +use simplex::simplicityhl::simplicity::elements::hashes::HashEngine as _; +use simplex::simplicityhl::simplicity::hashes::{Hash, sha256}; use crate::state_management::smt_storage::get_path_bits; @@ -11,7 +11,7 @@ use super::build_witness::{DEPTH, u256}; /// The tree is structured as a recursive binary tree where: /// - [`TreeNode::Leaf`] represents the bottom-most layer containing the actual data hash. /// - [`TreeNode::Branch`] represents an internal node containing the combined hash of its children. -#[derive(Debug, Clone, bincode::Encode, bincode::Decode, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] enum TreeNode { /// A leaf node at the bottom of the tree. /// diff --git a/crates/contracts/src/utils/arguments_helpers.rs b/crates/contracts/src/utils/arguments_helpers.rs deleted file mode 100644 index fbda166..0000000 --- a/crates/contracts/src/utils/arguments_helpers.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::error::FromArgumentsError; - -use simplicityhl::Arguments; -use simplicityhl::str::WitnessName; -use simplicityhl::value::{UIntValue, ValueInner}; - -fn extract_uint<'a>( - args: &'a Arguments, - name: &WitnessName, -) -> Result<&'a UIntValue, FromArgumentsError> { - let value = args - .get(name) - .ok_or_else(|| FromArgumentsError::MissingWitness { - name: name.as_inner().to_owned(), - })?; - - match value.inner() { - ValueInner::UInt(uint_value) => Ok(uint_value), - _ => Err(FromArgumentsError::WrongValueType { - name: name.as_inner().to_owned(), - expected: "UInt".to_owned(), - }), - } -} - -/// Extract a U256 value as `[u8; 32]` from `Arguments` for a witness name. -/// -/// # Errors -/// -/// Returns error if the witness is missing or has wrong type. -pub fn extract_u256_bytes( - args: &Arguments, - name: &WitnessName, -) -> Result<[u8; 32], FromArgumentsError> { - match extract_uint(args, name)? { - UIntValue::U256(u256) => Ok(u256.to_byte_array()), - _ => Err(FromArgumentsError::WrongValueType { - name: name.as_inner().to_owned(), - expected: "U256".to_owned(), - }), - } -} - -/// Extract a U64 value from `Arguments` for a witness name. -/// -/// # Errors -/// -/// Returns error if the witness is missing or has wrong type. -pub fn extract_u64(args: &Arguments, name: &WitnessName) -> Result { - match extract_uint(args, name)? { - UIntValue::U64(v) => Ok(*v), - _ => Err(FromArgumentsError::WrongValueType { - name: name.as_inner().to_owned(), - expected: "U64".to_owned(), - }), - } -} - -/// Extract a U32 value from `Arguments` for a witness name. -/// -/// # Errors -/// -/// Returns error if the witness is missing or has wrong type. -pub fn extract_u32(args: &Arguments, name: &WitnessName) -> Result { - match extract_uint(args, name)? { - UIntValue::U32(v) => Ok(*v), - _ => Err(FromArgumentsError::WrongValueType { - name: name.as_inner().to_owned(), - expected: "U32".to_owned(), - }), - } -} diff --git a/crates/contracts/src/utils/mod.rs b/crates/contracts/src/utils/mod.rs deleted file mode 100644 index a6b69c8..0000000 --- a/crates/contracts/src/utils/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod arguments_helpers; - -#[cfg(test)] -pub mod test_setup; diff --git a/crates/contracts/src/utils/test_setup.rs b/crates/contracts/src/utils/test_setup.rs deleted file mode 100644 index dda1d8c..0000000 --- a/crates/contracts/src/utils/test_setup.rs +++ /dev/null @@ -1,191 +0,0 @@ -#![allow(clippy::missing_errors_doc)] - -use std::str::FromStr; -use std::sync::{Arc, Mutex, OnceLock, RwLock}; - -use anyhow::{Context, anyhow}; -use bitcoincore_rpc::{Auth, Client, RpcApi}; -use lwk_test_util::{TestEnv, TestEnvBuilder}; -use serde_json::Value; -use simplicityhl::elements::AssetId; -use wallet_abi::Network; -use wallet_abi::runtime::WalletRuntimeConfig; - -const DEFAULT_FUND_AMOUNT_SAT: u64 = 1_000_000; - -static TEST_ENV: OnceLock>> = OnceLock::new(); -static TEST_ENV_INIT_LOCK: Mutex<()> = Mutex::new(()); - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RuntimeFundingAsset { - Lbtc, - NewAsset, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct RuntimeFundingResult { - pub funded_asset_id: AssetId, - pub issued_token_id: Option, - pub funded_amount_sat: u64, -} - -pub fn ensure_node_running() -> anyhow::Result<()> { - if TEST_ENV.get().is_some() { - return Ok(()); - } - - let _init_guard = TEST_ENV_INIT_LOCK - .lock() - .map_err(|_| anyhow!("test env init lock poisoned"))?; - if TEST_ENV.get().is_some() { - return Ok(()); - } - - for var in ["ELEMENTSD_EXEC", "ELECTRS_LIQUID_EXEC"] { - if std::env::var(var) - .map(|value| value.trim().is_empty()) - .unwrap_or(true) - { - return Err(anyhow!("{var} must be set")); - } - } - - let env = std::panic::catch_unwind(|| TestEnvBuilder::from_env().with_esplora().build()) - .map_err(|panic| { - panic.downcast_ref::().map_or_else( - || anyhow!("failed to start regtest test environment"), - |message| anyhow!("failed to start regtest test environment: {message}"), - ) - })?; - - let _ = TEST_ENV.set(Arc::new(RwLock::new(env))); - - Ok(()) -} - -/// Return the wallet data root path for integration tests. -/// -/// Uses `SIMPLICITY_CLI_WALLET_DATA_DIR` if set; otherwise falls back to a -/// deterministic workspace-relative path rooted at `CARGO_MANIFEST_DIR`. -pub fn wallet_data_root() -> std::path::PathBuf { - std::env::var_os("SIMPLICITY_CLI_WALLET_DATA_DIR").map_or_else( - || std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.cache/wallet"), - std::path::PathBuf::from, - ) -} - -fn test_env() -> anyhow::Result>> { - ensure_node_running()?; - TEST_ENV - .get() - .cloned() - .ok_or_else(|| anyhow!("test env failed to initialize")) -} - -#[allow(clippy::cast_precision_loss)] -fn sat_to_btc(satoshi: u64) -> f64 { - satoshi as f64 / 100_000_000.0 -} - -fn issue_asset_with_token(env: &TestEnv, amount_sat: u64) -> anyhow::Result<(AssetId, AssetId)> { - let rpc_url = env.elements_rpc_url(); - let (rpc_user, rpc_password) = env.elements_rpc_credentials(); - let client = Client::new(&rpc_url, Auth::UserPass(rpc_user, rpc_password)) - .context("failed to connect to elements RPC")?; - - let issued: Value = client - .call("issueasset", &[sat_to_btc(amount_sat).into(), 0.into()]) - .context("issueasset RPC failed")?; - - let asset_id = issued - .get("asset") - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("issueasset response missing 'asset'"))?; - let token_id = issued - .get("token") - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("issueasset response missing 'token'"))?; - - let asset_id = AssetId::from_str(asset_id).context("invalid issued asset id")?; - let token_id = AssetId::from_str(token_id).context("invalid issued token id")?; - - Ok((asset_id, token_id)) -} - -pub fn fund_runtime( - runtime: &WalletRuntimeConfig, - asset: RuntimeFundingAsset, -) -> anyhow::Result { - fund_runtime_with_amount(runtime, asset, DEFAULT_FUND_AMOUNT_SAT) -} - -pub fn fund_runtime_with_amount( - runtime: &WalletRuntimeConfig, - asset: RuntimeFundingAsset, - amount_sat: u64, -) -> anyhow::Result { - if runtime.network != Network::LocaltestLiquid { - return Err(anyhow!( - "fund_runtime supports only Network::LocaltestLiquid" - )); - } - - if amount_sat == 0 { - return Err(anyhow!("amount_sat must be > 0")); - } - - let test_env = test_env()?; - let env = test_env - .read() - .map_err(|_| anyhow!("test env lock poisoned"))?; - - let signer_address = runtime.signer_receive_address()?; - - let (asset_id, token_id) = match asset { - RuntimeFundingAsset::Lbtc => { - let lbtc = *runtime.network.policy_asset(); - env.elementsd_sendtoaddress(&signer_address, amount_sat, Some(lbtc)); - env.elementsd_generate(1); - drop(env); - (lbtc, None) - } - RuntimeFundingAsset::NewAsset => { - let (issued_asset, token_id) = issue_asset_with_token(&env, amount_sat)?; - env.elementsd_generate(1); - env.elementsd_sendtoaddress(&signer_address, amount_sat, Some(issued_asset)); - env.elementsd_generate(1); - drop(env); - (issued_asset, Some(token_id)) - } - }; - - Ok(RuntimeFundingResult { - funded_asset_id: asset_id, - issued_token_id: token_id, - funded_amount_sat: amount_sat, - }) -} - -pub fn get_esplora_url() -> anyhow::Result { - let test_env = test_env()?; - let env = test_env - .read() - .map_err(|_| anyhow!("test env lock poisoned"))?; - - Ok(env.esplora_url()) -} - -pub fn mine_blocks(blocks: usize) -> anyhow::Result<()> { - let test_env = test_env()?; - { - let env = test_env - .read() - .map_err(|_| anyhow!("test env lock poisoned"))?; - - for _ in 0..blocks { - env.elementsd_generate(1); - } - } - - Ok(()) -} diff --git a/crates/contracts/tests/common/filters.rs b/crates/contracts/tests/common/filters.rs new file mode 100644 index 0000000..a8d2ced --- /dev/null +++ b/crates/contracts/tests/common/filters.rs @@ -0,0 +1,137 @@ +#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] + +use simplex::signer::Signer; +use simplex::simplicityhl::elements::{AssetId, Script}; +use simplex::transaction::UTXO; + +#[derive(Clone, Copy)] +pub enum AmountFilter { + LessThan, + GreaterThan, + EqualTo, +} + +#[must_use] +pub fn filter_signer_utxos_by_asset_and_amount( + signer: &Signer, + asset_id: AssetId, + amount: u64, + amount_filter: AmountFilter, +) -> Vec { + signer + .get_utxos_filter( + &|utxo| { + utxo.explicit_asset() == asset_id + && matches_amount_filter(utxo.explicit_amount(), amount, amount_filter) + }, + &|utxo| { + utxo.unblinded_asset() == asset_id + && matches_amount_filter(utxo.unblinded_amount(), amount, amount_filter) + }, + ) + .unwrap() +} + +#[must_use] +pub fn find_signer_utxo_by_asset_and_amount( + signer: &Signer, + asset_id: AssetId, + amount: u64, + amount_filter: AmountFilter, +) -> Option { + filter_signer_utxos_by_asset_and_amount(signer, asset_id, amount, amount_filter) + .into_iter() + .next() +} + +#[must_use] +pub fn filter_signer_utxos_by_asset_id(signer: &Signer, asset_id: AssetId) -> Vec { + signer + .get_utxos_filter(&|utxo| utxo.explicit_asset() == asset_id, &|utxo| { + utxo.unblinded_asset() == asset_id + }) + .unwrap() +} + +#[must_use] +pub fn filter_signer_utxos_by_amount( + signer: &Signer, + amount: u64, + amount_filter: AmountFilter, +) -> Vec { + signer + .get_utxos_filter( + &|utxo| matches_amount_filter(utxo.explicit_amount(), amount, amount_filter), + &|utxo| matches_amount_filter(utxo.unblinded_amount(), amount, amount_filter), + ) + .unwrap() +} + +#[must_use] +pub fn find_utxo_by_asset_and_amount( + utxos: &[UTXO], + asset_id: AssetId, + amount: u64, +) -> Option { + utxos + .iter() + .find(|utxo| { + utxo.txout.asset.explicit() == Some(asset_id) + && utxo.txout.value.explicit() == Some(amount) + }) + .cloned() +} + +pub fn require_utxo_by_asset_and_amount( + utxos: &[UTXO], + asset_id: AssetId, + amount: u64, + missing_utxo_message: &str, +) -> anyhow::Result { + find_utxo_by_asset_and_amount(utxos, asset_id, amount) + .ok_or_else(|| anyhow::anyhow!("{missing_utxo_message}")) +} + +#[must_use] +pub fn find_utxo_by_asset_amount_and_script( + utxos: &[UTXO], + asset_id: AssetId, + amount: u64, + script_pubkey: &Script, +) -> Option { + utxos + .iter() + .find(|utxo| { + utxo.txout.asset.explicit() == Some(asset_id) + && utxo.txout.value.explicit() == Some(amount) + && utxo.txout.script_pubkey == *script_pubkey + }) + .cloned() +} + +pub fn assert_has_utxo_by_asset_and_amount(utxos: &[UTXO], asset_id: AssetId, amount: u64) { + assert!( + find_utxo_by_asset_and_amount(utxos, asset_id, amount).is_some(), + "missing utxo for asset {asset_id:?} and amount {amount}" + ); +} + +pub fn assert_has_utxo_by_asset_amount_and_script( + utxos: &[UTXO], + asset_id: AssetId, + amount: u64, + script_pubkey: &Script, +) { + assert!( + find_utxo_by_asset_amount_and_script(utxos, asset_id, amount, script_pubkey).is_some(), + "missing utxo for asset {asset_id:?}, amount {amount}, and expected script" + ); +} + +const fn matches_amount_filter(utxo_amount: u64, amount: u64, amount_filter: AmountFilter) -> bool { + match amount_filter { + AmountFilter::LessThan => utxo_amount < amount, + AmountFilter::GreaterThan => utxo_amount > amount, + AmountFilter::EqualTo => utxo_amount == amount, + } +} diff --git a/crates/contracts/tests/common/issuance.rs b/crates/contracts/tests/common/issuance.rs new file mode 100644 index 0000000..dff3383 --- /dev/null +++ b/crates/contracts/tests/common/issuance.rs @@ -0,0 +1,59 @@ +#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] + +use crate::common::signer::{finalize_and_broadcast, get_lbtc_utxo}; + +use simplex::transaction::partial_input::IssuanceInput; +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature}; + +use simplex::simplicityhl::elements::secp256k1_zkp::rand::RngCore; +use simplex::simplicityhl::elements::{AssetId, Txid}; + +/// System-random 32-byte seed. +/// +/// # Panics +/// Panics if the system random number generator fails. +#[must_use] +pub fn get_random_seed() -> [u8; 32] { + let mut bytes: [u8; 32] = [0; 32]; + simplex::simplicityhl::elements::secp256k1_zkp::rand::thread_rng().fill_bytes(&mut bytes); + bytes +} + +pub fn issue_asset( + context: &simplex::TestContext, + asset_amount: u64, +) -> anyhow::Result<(Txid, AssetId)> { + let signer = context.get_default_signer(); + + let mut ft = FinalTransaction::new(); + + let first_utxo = get_lbtc_utxo(context)?; + + let asset_entropy = get_random_seed(); + + let issuance_details = ft.add_issuance_input( + PartialInput::new(first_utxo.clone()), + IssuanceInput::new_issuance(asset_amount, 0, asset_entropy), + RequiredSignature::NativeEcdsa, + ); + + let signer_script_pubkey = signer.get_address().script_pubkey(); + + ft.add_output(PartialOutput::new( + signer_script_pubkey.clone(), + asset_amount, + issuance_details.asset_id, + )); + + let (utxo_asset_id, utxo_amount) = (first_utxo.asset(), first_utxo.amount()); + + ft.add_output(PartialOutput::new( + signer_script_pubkey, + utxo_amount, + utxo_asset_id, + )); + + let txid = finalize_and_broadcast(context, &ft)?; + + Ok((txid, issuance_details.asset_id)) +} diff --git a/crates/contracts/tests/common/mod.rs b/crates/contracts/tests/common/mod.rs new file mode 100644 index 0000000..a6e2a9e --- /dev/null +++ b/crates/contracts/tests/common/mod.rs @@ -0,0 +1,3 @@ +pub mod filters; +pub mod issuance; +pub mod signer; diff --git a/crates/contracts/tests/common/signer.rs b/crates/contracts/tests/common/signer.rs new file mode 100644 index 0000000..4e30232 --- /dev/null +++ b/crates/contracts/tests/common/signer.rs @@ -0,0 +1,129 @@ +#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] + +use crate::common::filters::{ + AmountFilter, filter_signer_utxos_by_asset_and_amount, filter_signer_utxos_by_asset_id, + find_signer_utxo_by_asset_and_amount, +}; +use simplex::provider::SimplicityNetwork; +use simplex::signer::Signer; +use simplex::simplicityhl::elements::{AssetId, Txid}; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO, +}; + +pub fn finalize_and_broadcast( + context: &simplex::TestContext, + ft: &FinalTransaction, +) -> anyhow::Result { + let provider = context.get_default_provider(); + let (tx, _) = context.get_default_signer().finalize(ft)?; + let txid = provider.broadcast_transaction(&tx)?; + txid.wait()?; + Ok(txid.txid()) +} + +pub fn ensure_exact_signer_utxo( + context: &simplex::TestContext, + asset_id: AssetId, + amount: u64, +) -> anyhow::Result { + let signer = context.get_default_signer(); + + if let Some(exact_utxo) = + find_signer_utxo_by_asset_and_amount(signer, asset_id, amount, AmountFilter::EqualTo) + { + return Ok(exact_utxo); + } + + let funding_utxo = + find_signer_utxo_by_asset_and_amount(signer, asset_id, amount, AmountFilter::GreaterThan) + .ok_or_else(|| { + anyhow::anyhow!( + "missing signer funding utxo for asset {asset_id:?} and amount {amount}" + ) + })?; + + let split_ft = get_split_utxo_ft(funding_utxo, vec![amount], signer, *context.get_network()); + let _ = finalize_and_broadcast(context, &split_ft)?; + + find_signer_utxo_by_asset_and_amount(signer, asset_id, amount, AmountFilter::EqualTo) + .ok_or_else(|| { + anyhow::anyhow!("missing exact signer utxo for asset {asset_id:?} and amount {amount}") + }) +} + +#[must_use] +pub fn get_split_utxo_ft( + utxo: UTXO, + amounts: Vec, + signer: &Signer, + network: SimplicityNetwork, +) -> FinalTransaction { + let (utxo_asset_id, utxo_amount) = (utxo.asset(), utxo.amount()); + + let mut ft = FinalTransaction::new(); + ft.add_input(PartialInput::new(utxo), RequiredSignature::NativeEcdsa); + + let signer_script_pubkey = signer.get_address().script_pubkey(); + let mut total_amount = 0; + + for amount in amounts { + ft.add_output(PartialOutput::new( + signer_script_pubkey.clone(), + amount, + utxo_asset_id, + )); + total_amount += amount; + } + + assert!( + total_amount <= utxo_amount, + "Total amounts after split must be less than the utxo amount" + ); + + if utxo_asset_id != network.policy_asset() && total_amount < utxo_amount { + ft.add_output(PartialOutput::new( + signer_script_pubkey, + utxo_amount - total_amount, + utxo_asset_id, + )); + } + + ft +} + +pub fn split_first_signer_utxo( + context: &simplex::TestContext, + amounts: Vec, +) -> anyhow::Result { + let signer = context.get_default_signer(); + let signer_utxos = signer.get_utxos()?; + let signer_utxo = signer_utxos + .first() + .expect("Signer does not have any utxos"); + + let ft = get_split_utxo_ft(signer_utxo.clone(), amounts, signer, *context.get_network()); + finalize_and_broadcast(context, &ft) +} + +pub fn get_lbtc_utxo(context: &simplex::TestContext) -> anyhow::Result { + let signer = context.get_default_signer(); + + let fee_sized_policy_utxos = filter_signer_utxos_by_asset_and_amount( + signer, + context.get_network().policy_asset(), + 100_000, + AmountFilter::LessThan, + ); + let first_utxo = fee_sized_policy_utxos + .first() + .cloned() + .or_else(|| { + filter_signer_utxos_by_asset_id(signer, context.get_network().policy_asset()) + .first() + .cloned() + }) + .ok_or_else(|| anyhow::anyhow!("Signer does not have any policy asset UTXOs"))?; + + Ok(first_utxo) +} diff --git a/crates/contracts/tests/mod.rs b/crates/contracts/tests/mod.rs new file mode 100644 index 0000000..7df4c2b --- /dev/null +++ b/crates/contracts/tests/mod.rs @@ -0,0 +1,3 @@ +pub mod common; +pub mod program_builder; +mod regtest; diff --git a/crates/contracts/tests/program_builder/mod.rs b/crates/contracts/tests/program_builder/mod.rs new file mode 100644 index 0000000..535f9c3 --- /dev/null +++ b/crates/contracts/tests/program_builder/mod.rs @@ -0,0 +1,2 @@ +pub mod option_offer; +pub mod options; diff --git a/crates/contracts/tests/program_builder/option_offer.rs b/crates/contracts/tests/program_builder/option_offer.rs new file mode 100644 index 0000000..7b2b786 --- /dev/null +++ b/crates/contracts/tests/program_builder/option_offer.rs @@ -0,0 +1,166 @@ +#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] + +use crate::common::filters::{AmountFilter, filter_signer_utxos_by_asset_and_amount}; +use crate::common::issuance::issue_asset; +use crate::common::signer::{ + ensure_exact_signer_utxo, finalize_and_broadcast, get_lbtc_utxo, split_first_signer_utxo, +}; + +use simplex::simplicityhl::elements::Txid; +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature}; + +use contracts::programs::option_offer::{OptionOffer, OptionOfferParameters}; +use contracts::programs::program::SimplexProgram; + +pub fn prepare_option_offer( + context: &simplex::TestContext, + deposit_lbtc_amount: u64, + expected_premium_amount: u64, + expected_settlement_amount: u64, + delta_timestamp: i32, +) -> anyhow::Result { + let _ = split_first_signer_utxo(context, vec![1_000])?; + + build_option_offer_program( + context, + deposit_lbtc_amount, + expected_premium_amount, + expected_settlement_amount, + delta_timestamp, + ) +} + +pub fn build_option_offer_program( + context: &simplex::TestContext, + deposit_lbtc_amount: u64, + expected_premium_amount: u64, + expected_settlement_amount: u64, + delta_timestamp: i32, +) -> anyhow::Result { + let signer = context.get_default_signer(); + let network = context.get_network(); + let policy_asset = network.policy_asset(); + let tip_timestamp = context.get_default_provider().fetch_tip_timestamp()?; + + let expiry_time = if delta_timestamp < 0 { + u32::try_from(tip_timestamp) + .map_err(|_| anyhow::anyhow!("tip timestamp {tip_timestamp} exceeds u32 range"))? + .checked_sub(delta_timestamp.unsigned_abs()) + .ok_or_else(|| anyhow::anyhow!("expiry timestamp overflow"))? + } else { + u32::try_from(tip_timestamp) + .map_err(|_| anyhow::anyhow!("tip timestamp {tip_timestamp} exceeds u32 range"))? + .checked_add(delta_timestamp.unsigned_abs()) + .ok_or_else(|| anyhow::anyhow!("expiry timestamp overflow"))? + }; + + let (_, premium_asset_id) = issue_asset(context, 5 * expected_premium_amount)?; + + let (_, settlement_asset_id) = issue_asset(context, 5 * expected_settlement_amount)?; + + let (collateral_per_contract, premium_per_collateral) = OptionOffer::calculate_per_params( + deposit_lbtc_amount, + expected_settlement_amount, + expected_premium_amount, + ); + + let option_offer_params: OptionOfferParameters = OptionOfferParameters { + collateral_asset_id: policy_asset, + premium_asset_id, + settlement_asset_id, + collateral_per_contract: collateral_per_contract.unwrap(), + premium_per_collateral: premium_per_collateral.unwrap(), + expiry_time, + user_pubkey: signer.get_schnorr_public_key(), + network: *network, + }; + + Ok(OptionOffer::new(option_offer_params)) +} + +pub fn create_option_offer_with_premium( + context: &simplex::TestContext, + option_offer: &OptionOffer, + collateral_amount: u64, + premium_amount: u64, +) -> anyhow::Result { + let collateral_input = get_lbtc_utxo(context)?; + let premium_input = ensure_exact_signer_utxo( + context, + option_offer.parameters.premium_asset_id, + premium_amount, + )?; + + let mut ft = FinalTransaction::new(); + ft.add_input( + PartialInput::new(collateral_input), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(premium_input), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + collateral_amount, + option_offer.parameters.collateral_asset_id, + )); + ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + premium_amount, + option_offer.parameters.premium_asset_id, + )); + + let txid = finalize_and_broadcast(context, &ft)?; + + Ok(txid) +} + +pub fn deposit_to_option_offer( + context: &simplex::TestContext, + deposit_lbtc_amount: u64, + expected_premium_amount: u64, + expected_settlement_amount: u64, + delta_timestamp: i32, +) -> anyhow::Result<(OptionOffer, Txid)> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + let network = context.get_network(); + let policy_asset = network.policy_asset(); + + let option_offer = prepare_option_offer( + context, + deposit_lbtc_amount, + expected_premium_amount, + expected_settlement_amount, + delta_timestamp, + )?; + + let mut ft = FinalTransaction::new(); + + let signer_utxos = filter_signer_utxos_by_asset_and_amount( + signer, + policy_asset, + deposit_lbtc_amount, + AmountFilter::GreaterThan, + ); + let first_utxo = signer_utxos.first().ok_or_else(|| { + anyhow::anyhow!("Signer does not have a policy UTXO large enough to deposit and pay fees") + })?; + + ft.add_input( + PartialInput::new(first_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + + ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + deposit_lbtc_amount, + policy_asset, + )); + + let txid = finalize_and_broadcast(context, &ft)?; + provider.wait(&txid)?; + + Ok((option_offer, txid)) +} diff --git a/crates/contracts/tests/program_builder/options.rs b/crates/contracts/tests/program_builder/options.rs new file mode 100644 index 0000000..a2660ec --- /dev/null +++ b/crates/contracts/tests/program_builder/options.rs @@ -0,0 +1,591 @@ +#![allow( + clippy::missing_errors_doc, + clippy::missing_panics_doc, + clippy::similar_names +)] + +use std::collections::HashMap; + +use crate::common::filters::{AmountFilter, filter_signer_utxos_by_asset_and_amount}; +use crate::common::issuance::issue_asset; +use crate::common::signer::{ensure_exact_signer_utxo, split_first_signer_utxo}; + +use anyhow::Context; +use contracts::programs::options::{ + Options, OptionsBranch, OptionsFundingBlinders, OptionsParameters, +}; +use contracts::programs::program::SimplexProgram; + +use simplex::program::{ProgramTrait, WitnessTrait}; +use simplex::signer::SignerTrait; +use simplex::simplicityhl::elements::secp256k1_zkp::Tweak; +use simplex::simplicityhl::elements::{ + AssetId, ContractHash, EcdsaSighashType, OutPoint, Script, TxOutSecrets, Txid, + confidential::{AssetBlindingFactor, ValueBlindingFactor}, + pset::PartiallySignedTransaction, + secp256k1_zkp::{Secp256k1, rand::thread_rng}, +}; +use simplex::simplicityhl::simplicity::hashes::Hash; +use simplex::transaction::partial_input::IssuanceInput; +use simplex::transaction::{PartialInput, PartialOutput, UTXO}; +const OPTION_ISSUANCE_CONTRACT_HASH: [u8; 32] = [1; 32]; +const GRANTOR_ISSUANCE_CONTRACT_HASH: [u8; 32] = [2; 32]; + +pub struct PreparedOptionsState { + pub options: Options, + pub option_issuance_entropy: [u8; 32], + pub grantor_issuance_entropy: [u8; 32], + pub option_issuance_source: UTXO, + pub grantor_issuance_source: UTXO, + pub creation_fee_source: UTXO, +} + +pub struct CreatedOptionsState { + pub options: Options, + pub option_issuance_entropy: [u8; 32], + pub grantor_issuance_entropy: [u8; 32], + pub creation_txid: Txid, + pub option_reissuance_token: UTXO, + pub grantor_reissuance_token: UTXO, +} + +pub struct FundedOptionsState { + pub options: Options, + pub option_issuance_entropy: [u8; 32], + pub grantor_issuance_entropy: [u8; 32], + pub funding_txid: Txid, + pub option_reissuance_token: UTXO, + pub grantor_reissuance_token: UTXO, + pub locked_collateral: UTXO, + pub option_token: UTXO, + pub grantor_token: UTXO, +} + +pub fn prepare_options( + context: &simplex::TestContext, + total_collateral_amount: u64, + expected_settlement_amount: u64, + contract_count: u64, + start_delta_timestamp: i32, + expiry_delta_timestamp: i32, +) -> anyhow::Result { + let network = context.get_network(); + let policy_asset = network.policy_asset(); + let tip_timestamp = context.get_default_provider().fetch_tip_timestamp()?; + + let start_time = offset_timestamp(tip_timestamp, start_delta_timestamp)?; + let expiry_time = offset_timestamp(tip_timestamp, expiry_delta_timestamp)?; + + let _ = split_first_signer_utxo(context, vec![1_000, 1_000, 1_000, 1_000]) + .context("prepare_options split policy setup utxos")?; + + let (_, settlement_asset_id) = issue_asset(context, 5 * expected_settlement_amount) + .context("prepare_options issue settlement asset")?; + + let signer = context.get_default_signer(); + let setup_policy_utxos = + filter_signer_utxos_by_asset_and_amount(signer, policy_asset, 1_000, AmountFilter::EqualTo); + let option_issuance_source = setup_policy_utxos + .first() + .cloned() + .ok_or_else(|| anyhow::anyhow!("missing option issuance source utxo"))?; + let grantor_issuance_source = setup_policy_utxos + .iter() + .find(|utxo| utxo.outpoint != option_issuance_source.outpoint) + .cloned() + .ok_or_else(|| anyhow::anyhow!("missing grantor issuance source utxo"))?; + let creation_fee_source = setup_policy_utxos + .iter() + .find(|utxo| { + utxo.outpoint != option_issuance_source.outpoint + && utxo.outpoint != grantor_issuance_source.outpoint + }) + .cloned() + .ok_or_else(|| anyhow::anyhow!("missing creation fee source utxo"))?; + + let (option_token_asset, option_reissuance_token_asset, option_issuance_entropy) = issuance_ids( + option_issuance_source.outpoint, + OPTION_ISSUANCE_CONTRACT_HASH, + ); + let (grantor_token_asset, grantor_reissuance_token_asset, grantor_issuance_entropy) = + issuance_ids( + grantor_issuance_source.outpoint, + GRANTOR_ISSUANCE_CONTRACT_HASH, + ); + + let (collateral_per_contract, settlement_per_contract) = Options::calculate_per_contract_params( + total_collateral_amount, + expected_settlement_amount, + contract_count, + ); + + let collateral_per_contract = collateral_per_contract + .ok_or_else(|| anyhow::anyhow!("failed to derive collateral_per_contract"))?; + let settlement_per_contract = settlement_per_contract + .ok_or_else(|| anyhow::anyhow!("failed to derive settlement_per_contract"))?; + + if collateral_per_contract + .checked_mul(contract_count) + .ok_or_else(|| anyhow::anyhow!("collateral amount overflow"))? + != total_collateral_amount + { + return Err(anyhow::anyhow!( + "total collateral must be an exact multiple of contract_count" + )); + } + + if settlement_per_contract + .checked_mul(contract_count) + .ok_or_else(|| anyhow::anyhow!("settlement amount overflow"))? + != expected_settlement_amount + { + return Err(anyhow::anyhow!( + "expected settlement must be an exact multiple of contract_count" + )); + } + + Ok(PreparedOptionsState { + options: Options::new(OptionsParameters { + start_time, + expiry_time, + collateral_per_contract, + settlement_per_contract, + collateral_asset_id: policy_asset, + settlement_asset_id, + option_token_asset, + option_reissuance_token_asset, + grantor_token_asset, + grantor_reissuance_token_asset, + network: *network, + }), + option_issuance_entropy, + grantor_issuance_entropy, + option_issuance_source, + grantor_issuance_source, + creation_fee_source, + }) +} + +pub fn create_options( + context: &simplex::TestContext, + prepared: PreparedOptionsState, +) -> anyhow::Result { + let signer = context.get_default_signer(); + let creation_fee = 500_u64; + let creation_input_total = prepared.option_issuance_source.amount() + + prepared.grantor_issuance_source.amount() + + prepared.creation_fee_source.amount(); + let creation_change = creation_input_total + .checked_sub(creation_fee) + .ok_or_else(|| anyhow::anyhow!("creation inputs do not cover the fee"))?; + + let mut option_input = PartialInput::new(prepared.option_issuance_source.clone()).to_input(); + option_input.issuance_asset_entropy = Some(OPTION_ISSUANCE_CONTRACT_HASH); + option_input.issuance_inflation_keys = Some(1); + option_input.blinded_issuance = Some(0x00); + + let mut grantor_input = PartialInput::new(prepared.grantor_issuance_source.clone()).to_input(); + grantor_input.issuance_asset_entropy = Some(GRANTOR_ISSUANCE_CONTRACT_HASH); + grantor_input.issuance_inflation_keys = Some(1); + grantor_input.blinded_issuance = Some(0x00); + + let mut pst = PartiallySignedTransaction::new_v2(); + pst.add_input(option_input); + pst.add_input(grantor_input); + pst.add_input(PartialInput::new(prepared.creation_fee_source.clone()).to_input()); + pst.add_output( + PartialOutput::new( + prepared.options.get_script_pubkey(), + 1, + prepared.options.parameters.option_reissuance_token_asset, + ) + .with_blinding_key(signer.get_blinding_public_key()) + .to_output(), + ); + pst.add_output( + PartialOutput::new( + prepared.options.get_script_pubkey(), + 1, + prepared.options.parameters.grantor_reissuance_token_asset, + ) + .with_blinding_key(signer.get_blinding_public_key()) + .to_output(), + ); + pst.add_output( + PartialOutput::new( + signer.get_address().script_pubkey(), + creation_change, + prepared.options.parameters.collateral_asset_id, + ) + .to_output(), + ); + pst.add_output( + PartialOutput::new( + Script::new(), + creation_fee, + prepared.options.parameters.collateral_asset_id, + ) + .to_output(), + ); + + let input_secrets = HashMap::from([ + ( + 0_usize, + TxOutSecrets::new( + prepared.option_issuance_source.asset(), + AssetBlindingFactor::zero(), + prepared.option_issuance_source.amount(), + ValueBlindingFactor::zero(), + ), + ), + ( + 1_usize, + TxOutSecrets::new( + prepared.grantor_issuance_source.asset(), + AssetBlindingFactor::zero(), + prepared.grantor_issuance_source.amount(), + ValueBlindingFactor::zero(), + ), + ), + ( + 2_usize, + TxOutSecrets::new( + prepared.creation_fee_source.asset(), + AssetBlindingFactor::zero(), + prepared.creation_fee_source.amount(), + ValueBlindingFactor::zero(), + ), + ), + ]); + pst.blind_last(&mut thread_rng(), &Secp256k1::new(), &input_secrets) + .context("create_options blind outputs")?; + + for input_index in 0..pst.inputs().len() { + let (public_key, signature) = signer + .sign_input(&pst, input_index) + .context("create_options sign input")?; + let mut raw_sig = signature.serialize_der().to_vec(); + raw_sig.push(EcdsaSighashType::All as u8); + pst.inputs_mut()[input_index].final_script_witness = + Some(vec![raw_sig, public_key.to_bytes()]); + } + + let creation_tx = pst.extract_tx().context("create_options extract tx")?; + let creation_txid = context + .get_default_provider() + .broadcast_transaction(&creation_tx) + .context("create_options broadcast")?; + + let secp = Secp256k1::new(); + let blinding_key = signer.get_blinding_private_key(); + let option_reissuance_txout = creation_tx + .output + .first() + .cloned() + .ok_or_else(|| anyhow::anyhow!("missing option reissuance creation output"))?; + let grantor_reissuance_txout = creation_tx + .output + .get(1) + .cloned() + .ok_or_else(|| anyhow::anyhow!("missing grantor reissuance creation output"))?; + let option_reissuance_secrets = option_reissuance_txout + .unblind(&secp, blinding_key.inner) + .context("create_options unblind option reissuance output")?; + let grantor_reissuance_secrets = grantor_reissuance_txout + .unblind(&secp, blinding_key.inner) + .context("create_options unblind grantor reissuance output")?; + + Ok(CreatedOptionsState { + options: prepared.options, + option_issuance_entropy: prepared.option_issuance_entropy, + grantor_issuance_entropy: prepared.grantor_issuance_entropy, + creation_txid: creation_txid.txid(), + option_reissuance_token: UTXO { + outpoint: OutPoint::new(creation_txid.txid(), 0), + txout: option_reissuance_txout, + secrets: Some(option_reissuance_secrets), + }, + grantor_reissuance_token: UTXO { + outpoint: OutPoint::new(creation_txid.txid(), 1), + txout: grantor_reissuance_txout, + secrets: Some(grantor_reissuance_secrets), + }, + }) +} + +#[allow(clippy::too_many_lines)] +pub fn fund_options( + context: &simplex::TestContext, + created: CreatedOptionsState, + collateral_amount: u64, + contract_count: u64, +) -> anyhow::Result { + let expected_settlement_amount = contract_count + .checked_mul(created.options.parameters.settlement_per_contract) + .ok_or_else(|| anyhow::anyhow!("settlement amount overflow"))?; + let expected_collateral_amount = contract_count + .checked_mul(created.options.parameters.collateral_per_contract) + .ok_or_else(|| anyhow::anyhow!("collateral amount overflow"))?; + + if collateral_amount != expected_collateral_amount { + return Err(anyhow::anyhow!( + "collateral amount does not match collateral_per_contract * contract_count" + )); + } + + let option_input_secrets = created + .option_reissuance_token + .secrets + .ok_or_else(|| anyhow::anyhow!("missing option reissuance input secrets"))?; + let grantor_input_secrets = created + .grantor_reissuance_token + .secrets + .ok_or_else(|| anyhow::anyhow!("missing grantor reissuance input secrets"))?; + let option_input_abf = tweak_to_bytes(option_input_secrets.asset_bf.into_inner()); + let option_input_vbf = tweak_to_bytes(option_input_secrets.value_bf.into_inner()); + let grantor_input_abf = tweak_to_bytes(grantor_input_secrets.asset_bf.into_inner()); + let grantor_input_vbf = tweak_to_bytes(grantor_input_secrets.value_bf.into_inner()); + + let signer = context.get_default_signer(); + let collateral_input = ensure_exact_signer_utxo( + context, + created.options.parameters.collateral_asset_id, + collateral_amount, + )?; + let fee_input = signer + .get_utxos_asset(created.options.parameters.collateral_asset_id) + .context("fund_options fetch fee utxos")? + .into_iter() + .find(|utxo| utxo.outpoint != collateral_input.outpoint) + .ok_or_else(|| anyhow::anyhow!("missing funding fee input"))?; + let funding_fee = 500_u64; + let funding_change = fee_input + .amount() + .checked_sub(funding_fee) + .ok_or_else(|| anyhow::anyhow!("funding fee input does not cover the fee"))?; + + let mut option_input = PartialInput::new(created.option_reissuance_token.clone()).to_input(); + let option_issuance = + IssuanceInput::new_reissuance(contract_count, created.option_issuance_entropy).to_input(); + option_input.issuance_blinding_nonce = Some(Tweak::from_inner(option_input_abf)?); + option_input.issuance_value_amount = option_issuance.issuance_value_amount; + option_input.issuance_asset_entropy = option_issuance.issuance_asset_entropy; + option_input.blinded_issuance = option_issuance.blinded_issuance; + + let mut grantor_input = PartialInput::new(created.grantor_reissuance_token.clone()).to_input(); + let grantor_issuance = + IssuanceInput::new_reissuance(contract_count, created.grantor_issuance_entropy).to_input(); + grantor_input.issuance_blinding_nonce = Some(Tweak::from_inner(grantor_input_abf)?); + grantor_input.issuance_value_amount = grantor_issuance.issuance_value_amount; + grantor_input.issuance_asset_entropy = grantor_issuance.issuance_asset_entropy; + grantor_input.blinded_issuance = grantor_issuance.blinded_issuance; + + let mut pst = PartiallySignedTransaction::new_v2(); + pst.add_input(option_input); + pst.add_input(grantor_input); + pst.add_input(PartialInput::new(collateral_input.clone()).to_input()); + pst.add_input(PartialInput::new(fee_input.clone()).to_input()); + pst.add_output( + PartialOutput::new( + created.options.get_script_pubkey(), + 1, + created.options.parameters.option_reissuance_token_asset, + ) + .with_blinding_key(signer.get_blinding_public_key()) + .to_output(), + ); + pst.add_output( + PartialOutput::new( + created.options.get_script_pubkey(), + 1, + created.options.parameters.grantor_reissuance_token_asset, + ) + .with_blinding_key(signer.get_blinding_public_key()) + .to_output(), + ); + pst.add_output( + PartialOutput::new( + created.options.get_script_pubkey(), + collateral_amount, + created.options.parameters.collateral_asset_id, + ) + .to_output(), + ); + pst.add_output( + PartialOutput::new( + signer_script(context), + contract_count, + created.options.parameters.option_token_asset, + ) + .to_output(), + ); + pst.add_output( + PartialOutput::new( + signer_script(context), + contract_count, + created.options.parameters.grantor_token_asset, + ) + .to_output(), + ); + pst.add_output( + PartialOutput::new( + signer_script(context), + funding_change, + created.options.parameters.collateral_asset_id, + ) + .to_output(), + ); + pst.add_output( + PartialOutput::new( + Script::new(), + funding_fee, + created.options.parameters.collateral_asset_id, + ) + .to_output(), + ); + + let input_secrets = HashMap::from([ + (0_usize, option_input_secrets), + (1_usize, grantor_input_secrets), + (2_usize, explicit_txout_secrets(&collateral_input)), + (3_usize, explicit_txout_secrets(&fee_input)), + ]); + pst.blind_last(&mut thread_rng(), &Secp256k1::new(), &input_secrets) + .context("fund_options blind outputs")?; + + let blinded_tx = pst + .extract_tx() + .context("fund_options extract blinded tx")?; + let blinding_key = signer.get_blinding_private_key(); + let secp = Secp256k1::new(); + let option_output_secrets = blinded_tx.output[0] + .unblind(&secp, blinding_key.inner) + .context("fund_options unblind option reissuance output")?; + let grantor_output_secrets = blinded_tx.output[1] + .unblind(&secp, blinding_key.inner) + .context("fund_options unblind grantor reissuance output")?; + let funding_witness = Options::get_witness(OptionsBranch::Fund { + expected_settlement_amount, + blinders: OptionsFundingBlinders { + input_option_abf: option_input_abf, + input_option_vbf: option_input_vbf, + input_grantor_abf: grantor_input_abf, + input_grantor_vbf: grantor_input_vbf, + output_option_abf: tweak_to_bytes(option_output_secrets.asset_bf.into_inner()), + output_option_vbf: tweak_to_bytes(option_output_secrets.value_bf.into_inner()), + output_grantor_abf: tweak_to_bytes(grantor_output_secrets.asset_bf.into_inner()), + output_grantor_vbf: tweak_to_bytes(grantor_output_secrets.value_bf.into_inner()), + }, + }) + .build_witness(); + + let program = created.options.get_program().clone(); + pst.inputs_mut()[0].final_script_witness = Some( + program + .finalize(&pst, &funding_witness, 0, context.get_network()) + .context("fund_options finalize option reissuance input")?, + ); + pst.inputs_mut()[1].final_script_witness = Some( + program + .finalize(&pst, &funding_witness, 1, context.get_network()) + .context("fund_options finalize grantor reissuance input")?, + ); + + for input_index in [2_usize, 3_usize] { + let (public_key, signature) = signer + .sign_input(&pst, input_index) + .context("fund_options sign input")?; + let mut raw_sig = signature.serialize_der().to_vec(); + raw_sig.push(EcdsaSighashType::All as u8); + pst.inputs_mut()[input_index].final_script_witness = + Some(vec![raw_sig, public_key.to_bytes()]); + } + + let funding_tx = pst.extract_tx().context("fund_options extract final tx")?; + let funding_txid = context + .get_default_provider() + .broadcast_transaction(&funding_tx) + .context("fund_options broadcast")?; + + Ok(FundedOptionsState { + options: created.options, + option_issuance_entropy: created.option_issuance_entropy, + grantor_issuance_entropy: created.grantor_issuance_entropy, + funding_txid: funding_txid.txid(), + option_reissuance_token: UTXO { + outpoint: OutPoint::new(funding_txid.txid(), 0), + txout: funding_tx.output[0].clone(), + secrets: Some(option_output_secrets), + }, + grantor_reissuance_token: UTXO { + outpoint: OutPoint::new(funding_txid.txid(), 1), + txout: funding_tx.output[1].clone(), + secrets: Some(grantor_output_secrets), + }, + locked_collateral: UTXO { + outpoint: OutPoint::new(funding_txid.txid(), 2), + txout: funding_tx.output[2].clone(), + secrets: None, + }, + option_token: UTXO { + outpoint: OutPoint::new(funding_txid.txid(), 3), + txout: funding_tx.output[3].clone(), + secrets: None, + }, + grantor_token: UTXO { + outpoint: OutPoint::new(funding_txid.txid(), 4), + txout: funding_tx.output[4].clone(), + secrets: None, + }, + }) +} + +fn issuance_ids( + issuance_outpoint: OutPoint, + contract_hash_bytes: [u8; 32], +) -> (AssetId, AssetId, [u8; 32]) { + let issuance_entropy = AssetId::generate_asset_entropy( + issuance_outpoint, + ContractHash::from_byte_array(contract_hash_bytes), + ); + + ( + AssetId::from_entropy(issuance_entropy), + AssetId::reissuance_token_from_entropy(issuance_entropy, false), + issuance_entropy.to_byte_array(), + ) +} + +fn offset_timestamp(tip_timestamp: u64, delta_timestamp: i32) -> anyhow::Result { + let tip_timestamp = u32::try_from(tip_timestamp) + .map_err(|_| anyhow::anyhow!("tip timestamp {tip_timestamp} exceeds u32 range"))?; + + if delta_timestamp < 0 { + tip_timestamp + .checked_sub(delta_timestamp.unsigned_abs()) + .ok_or_else(|| anyhow::anyhow!("timestamp underflow")) + } else { + tip_timestamp + .checked_add(delta_timestamp.unsigned_abs()) + .ok_or_else(|| anyhow::anyhow!("timestamp overflow")) + } +} + +fn signer_script(context: &simplex::TestContext) -> Script { + context.get_default_signer().get_address().script_pubkey() +} + +fn tweak_to_bytes(tweak: Tweak) -> [u8; 32] { + let mut bytes = [0_u8; 32]; + bytes.copy_from_slice(tweak.as_ref()); + bytes +} + +fn explicit_txout_secrets(utxo: &UTXO) -> TxOutSecrets { + TxOutSecrets::new( + utxo.asset(), + AssetBlindingFactor::zero(), + utxo.amount(), + ValueBlindingFactor::zero(), + ) +} diff --git a/crates/contracts/tests/regtest/mod.rs b/crates/contracts/tests/regtest/mod.rs new file mode 100644 index 0000000..535f9c3 --- /dev/null +++ b/crates/contracts/tests/regtest/mod.rs @@ -0,0 +1,2 @@ +pub mod option_offer; +pub mod options; diff --git a/crates/contracts/tests/regtest/option_offer/deposit.rs b/crates/contracts/tests/regtest/option_offer/deposit.rs new file mode 100644 index 0000000..e28e194 --- /dev/null +++ b/crates/contracts/tests/regtest/option_offer/deposit.rs @@ -0,0 +1,72 @@ +use crate::common::filters::assert_has_utxo_by_asset_and_amount; +use crate::program_builder::option_offer::{ + create_option_offer_with_premium, deposit_to_option_offer, prepare_option_offer, +}; + +use contracts::programs::program::SimplexProgram; + +#[simplex::test] +fn deposit_to_create_option_offer(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + + let deposit_lbtc_amount = 1000; + + let (option_offer, deposit_txid) = + deposit_to_option_offer(&context, deposit_lbtc_amount, 10000, 10000, 1000)?; + + let transaction = provider.fetch_transaction(&deposit_txid)?; + assert_eq!( + transaction.output[0].value.explicit(), + Some(deposit_lbtc_amount) + ); + assert_eq!( + transaction.output[0].asset.explicit(), + Some(option_offer.parameters.collateral_asset_id) + ); + assert_eq!( + transaction.output[0].script_pubkey, + option_offer.get_script_pubkey() + ); + + Ok(()) +} + +#[simplex::test] +fn deposit_to_create_option_offer_with_premium( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + + let deposit_lbtc_amount = 1_000_u64; + let expected_premium_amount = 10_000_u64; + let expected_settlement_amount = 10_000_u64; + + let option_offer = prepare_option_offer( + &context, + deposit_lbtc_amount, + expected_premium_amount, + expected_settlement_amount, + 1_000, + )?; + + create_option_offer_with_premium( + &context, + &option_offer, + deposit_lbtc_amount, + expected_premium_amount, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + ); + assert_has_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + ); + + Ok(()) +} diff --git a/crates/contracts/tests/regtest/option_offer/exercise.rs b/crates/contracts/tests/regtest/option_offer/exercise.rs new file mode 100644 index 0000000..91feab4 --- /dev/null +++ b/crates/contracts/tests/regtest/option_offer/exercise.rs @@ -0,0 +1,497 @@ +use crate::common::filters::{ + assert_has_utxo_by_asset_amount_and_script, assert_has_utxo_by_asset_and_amount, + require_utxo_by_asset_and_amount, +}; +use crate::common::signer::{ensure_exact_signer_utxo, finalize_and_broadcast}; +use crate::program_builder::option_offer::{ + create_option_offer_with_premium, prepare_option_offer, +}; + +use simplex::program::{ProgramError, ProgramTrait, WitnessTrait}; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, +}; + +use contracts::programs::option_offer::{OptionOffer, OptionOfferBranch}; +use contracts::programs::program::SimplexProgram; + +#[simplex::test] +fn exercise_option_offer(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let deposit_lbtc_amount = 1_000_u64; + let expected_premium_amount = 10_000_u64; + let expected_settlement_amount = 10_000_u64; + + let option_offer = prepare_option_offer( + &context, + deposit_lbtc_amount, + expected_premium_amount, + expected_settlement_amount, + 1_000, + )?; + + create_option_offer_with_premium( + &context, + &option_offer, + deposit_lbtc_amount, + expected_premium_amount, + )?; + + let receiver_script_pubkey = signer.get_address().script_pubkey(); + let settlement_input = ensure_exact_signer_utxo( + &context, + option_offer.parameters.settlement_asset_id, + expected_settlement_amount, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + let exercise_branch = OptionOfferBranch::Exercise { + collateral_amount: deposit_lbtc_amount, + is_change_needed: false, + }; + let collateral_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + "missing collateral covenant utxo", + )?; + let premium_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + "missing premium covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + + for utxo in [collateral_program_utxo, premium_program_utxo] { + ft.add_program_input( + PartialInput::new(utxo), + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(exercise_branch)), + ), + RequiredSignature::None, + ); + } + + ft.add_input( + PartialInput::new(settlement_input), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + expected_settlement_amount, + option_offer.parameters.settlement_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + deposit_lbtc_amount, + option_offer.parameters.collateral_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey, + expected_premium_amount, + option_offer.parameters.premium_asset_id, + )); + + let exercise_txid = finalize_and_broadcast(&context, &ft)?; + + let exercised_program_utxos = + provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &exercised_program_utxos, + option_offer.parameters.settlement_asset_id, + expected_settlement_amount, + ); + + let signer_utxos = signer.get_utxos_txid(exercise_txid)?; + let receiver_script_pubkey = signer.get_address().script_pubkey(); + assert_has_utxo_by_asset_amount_and_script( + &signer_utxos, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + &receiver_script_pubkey, + ); + assert_has_utxo_by_asset_amount_and_script( + &signer_utxos, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + &receiver_script_pubkey, + ); + + Ok(()) +} + +#[simplex::test] +fn exercise_option_offer_with_change(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let deposit_lbtc_amount = 1_000_u64; + let expected_premium_amount = 10_000_u64; + let expected_settlement_amount = 10_000_u64; + let partial_collateral_amount = 600_u64; + let remaining_collateral_amount = deposit_lbtc_amount - partial_collateral_amount; + let exercised_premium_amount = 6_000_u64; + let remaining_premium_amount = expected_premium_amount - exercised_premium_amount; + let exercised_settlement_amount = 6_000_u64; + + let option_offer = prepare_option_offer( + &context, + deposit_lbtc_amount, + expected_premium_amount, + expected_settlement_amount, + 0, + )?; + + create_option_offer_with_premium( + &context, + &option_offer, + deposit_lbtc_amount, + expected_premium_amount, + )?; + + let receiver_script_pubkey = signer.get_address().script_pubkey(); + let settlement_input = ensure_exact_signer_utxo( + &context, + option_offer.parameters.settlement_asset_id, + exercised_settlement_amount, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + let exercise_branch = OptionOfferBranch::Exercise { + collateral_amount: partial_collateral_amount, + is_change_needed: true, + }; + let collateral_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + "missing collateral covenant utxo", + )?; + let premium_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + "missing premium covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + + for utxo in [collateral_program_utxo, premium_program_utxo] { + ft.add_program_input( + PartialInput::new(utxo), + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(exercise_branch)), + ), + RequiredSignature::None, + ); + } + + ft.add_input( + PartialInput::new(settlement_input), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + remaining_collateral_amount, + option_offer.parameters.collateral_asset_id, + )); + ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + remaining_premium_amount, + option_offer.parameters.premium_asset_id, + )); + ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + exercised_settlement_amount, + option_offer.parameters.settlement_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + partial_collateral_amount, + option_offer.parameters.collateral_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey, + exercised_premium_amount, + option_offer.parameters.premium_asset_id, + )); + + let exercise_txid = finalize_and_broadcast(&context, &ft)?; + + let exercised_program_utxos = + provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &exercised_program_utxos, + option_offer.parameters.collateral_asset_id, + remaining_collateral_amount, + ); + assert_has_utxo_by_asset_and_amount( + &exercised_program_utxos, + option_offer.parameters.premium_asset_id, + remaining_premium_amount, + ); + assert_has_utxo_by_asset_and_amount( + &exercised_program_utxos, + option_offer.parameters.settlement_asset_id, + exercised_settlement_amount, + ); + + let signer_utxos = signer.get_utxos_txid(exercise_txid)?; + let receiver_script_pubkey = signer.get_address().script_pubkey(); + assert_has_utxo_by_asset_amount_and_script( + &signer_utxos, + option_offer.parameters.collateral_asset_id, + partial_collateral_amount, + &receiver_script_pubkey, + ); + assert_has_utxo_by_asset_amount_and_script( + &signer_utxos, + option_offer.parameters.premium_asset_id, + exercised_premium_amount, + &receiver_script_pubkey, + ); + + Ok(()) +} + +#[simplex::test] +fn exercise_option_offer_rejects_wrong_settlement_amount( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let deposit_lbtc_amount = 1_000_u64; + let expected_premium_amount = 10_000_u64; + let expected_settlement_amount = 10_000_u64; + + let option_offer = prepare_option_offer( + &context, + deposit_lbtc_amount, + expected_premium_amount, + expected_settlement_amount, + 1_000, + )?; + + create_option_offer_with_premium( + &context, + &option_offer, + deposit_lbtc_amount, + expected_premium_amount, + )?; + + let receiver_script_pubkey = signer.get_address().script_pubkey(); + let settlement_input = ensure_exact_signer_utxo( + &context, + option_offer.parameters.settlement_asset_id, + expected_settlement_amount, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + let exercise_branch = OptionOfferBranch::Exercise { + collateral_amount: deposit_lbtc_amount, + is_change_needed: false, + }; + let collateral_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + "missing collateral covenant utxo", + )?; + let premium_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + "missing premium covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + + for utxo in [collateral_program_utxo, premium_program_utxo] { + ft.add_program_input( + PartialInput::new(utxo), + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(exercise_branch)), + ), + RequiredSignature::None, + ); + } + + ft.add_input( + PartialInput::new(settlement_input), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + expected_settlement_amount - 1, + option_offer.parameters.settlement_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + deposit_lbtc_amount, + option_offer.parameters.collateral_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + expected_premium_amount, + option_offer.parameters.premium_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey, + 1, + option_offer.parameters.settlement_asset_id, + )); + + let (pst, _) = ft.extract_pst(); + let witness = OptionOffer::get_witness(exercise_branch).build_witness(); + let program_error = option_offer + .get_program() + .finalize(&pst, &witness, 0, context.get_network()) + .expect_err("exercise should reject short settlement"); + assert!(matches!(program_error, ProgramError::Pruning(_))); + + let covenant_utxos_after_rejection = + provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_rejection, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + ); + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_rejection, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + ); + + Ok(()) +} + +#[simplex::test] +fn exercise_option_offer_rejects_partial_exercise_without_change_flag( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let deposit_lbtc_amount = 1_000_u64; + let expected_premium_amount = 10_000_u64; + let expected_settlement_amount = 10_000_u64; + let partial_collateral_amount = 600_u64; + let remaining_collateral_amount = deposit_lbtc_amount - partial_collateral_amount; + let exercised_premium_amount = 6_000_u64; + let remaining_premium_amount = expected_premium_amount - exercised_premium_amount; + let exercised_settlement_amount = 6_000_u64; + + let option_offer = prepare_option_offer( + &context, + deposit_lbtc_amount, + expected_premium_amount, + expected_settlement_amount, + 1_000, + )?; + + create_option_offer_with_premium( + &context, + &option_offer, + deposit_lbtc_amount, + expected_premium_amount, + )?; + + let receiver_script_pubkey = signer.get_address().script_pubkey(); + let settlement_input = ensure_exact_signer_utxo( + &context, + option_offer.parameters.settlement_asset_id, + exercised_settlement_amount, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + let exercise_branch = OptionOfferBranch::Exercise { + collateral_amount: partial_collateral_amount, + is_change_needed: false, + }; + let collateral_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + "missing collateral covenant utxo", + )?; + let premium_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + "missing premium covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + + for utxo in [collateral_program_utxo, premium_program_utxo] { + ft.add_program_input( + PartialInput::new(utxo), + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(exercise_branch)), + ), + RequiredSignature::None, + ); + } + + ft.add_input( + PartialInput::new(settlement_input), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + remaining_collateral_amount, + option_offer.parameters.collateral_asset_id, + )); + ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + remaining_premium_amount, + option_offer.parameters.premium_asset_id, + )); + ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + exercised_settlement_amount, + option_offer.parameters.settlement_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + partial_collateral_amount, + option_offer.parameters.collateral_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey, + exercised_premium_amount, + option_offer.parameters.premium_asset_id, + )); + + let (pst, _) = ft.extract_pst(); + let witness = OptionOffer::get_witness(exercise_branch).build_witness(); + let program_error = option_offer + .get_program() + .finalize(&pst, &witness, 0, context.get_network()) + .expect_err("exercise should reject missing change flag"); + assert!(matches!(program_error, ProgramError::Pruning(_))); + + let covenant_utxos_after_rejection = + provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_rejection, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + ); + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_rejection, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + ); + + Ok(()) +} diff --git a/crates/contracts/tests/regtest/option_offer/expiry.rs b/crates/contracts/tests/regtest/option_offer/expiry.rs new file mode 100644 index 0000000..5f0425b --- /dev/null +++ b/crates/contracts/tests/regtest/option_offer/expiry.rs @@ -0,0 +1,221 @@ +use crate::common::filters::{ + assert_has_utxo_by_asset_amount_and_script, assert_has_utxo_by_asset_and_amount, + require_utxo_by_asset_and_amount, +}; +use crate::common::signer::{finalize_and_broadcast, get_lbtc_utxo}; +use crate::program_builder::option_offer::{ + create_option_offer_with_premium, prepare_option_offer, +}; + +use std::collections::HashMap; + +use simplex::program::{ProgramError, ProgramTrait, WitnessTrait}; +use simplex::signer::SignerTrait; +use simplex::simplicityhl::elements::{LockTime, Sequence}; +use simplex::simplicityhl::str::WitnessName; +use simplex::simplicityhl::value::ValueConstructible; +use simplex::simplicityhl::{Value, WitnessValues}; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, +}; + +use contracts::programs::option_offer::{OptionOffer, OptionOfferBranch}; +use contracts::programs::program::SimplexProgram; + +#[simplex::test] +fn expiry_option_offer(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let deposit_lbtc_amount = 1_000_u64; + let expected_premium_amount = 10_000_u64; + let expected_settlement_amount = 10_000_u64; + + let option_offer = prepare_option_offer( + &context, + deposit_lbtc_amount, + expected_premium_amount, + expected_settlement_amount, + -50, + )?; + + create_option_offer_with_premium( + &context, + &option_offer, + deposit_lbtc_amount, + expected_premium_amount, + )?; + + let locktime = LockTime::from_time(option_offer.parameters.expiry_time) + .map_err(|error| anyhow::anyhow!(error))?; + let program_utxos = provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + let collateral_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + "missing collateral covenant utxo", + )?; + let premium_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + "missing premium covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + for utxo in [collateral_program_utxo, premium_program_utxo] { + ft.add_program_input( + PartialInput::new(utxo) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(OptionOfferBranch::Expiry)), + ), + RequiredSignature::Witness("USER_SIGHASH_ALL".to_string()), + ); + } + ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + + let receiver_script_pubkey = signer.get_address().script_pubkey(); + ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + deposit_lbtc_amount, + option_offer.parameters.collateral_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + expected_premium_amount, + option_offer.parameters.premium_asset_id, + )); + + let expiry_txid = finalize_and_broadcast(&context, &ft)?; + + let signer_utxos = signer.get_utxos_txid(expiry_txid)?; + assert_has_utxo_by_asset_amount_and_script( + &signer_utxos, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + &receiver_script_pubkey, + ); + assert_has_utxo_by_asset_amount_and_script( + &signer_utxos, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + &receiver_script_pubkey, + ); + + Ok(()) +} + +#[simplex::test] +fn expiry_option_offer_rejects_missing_locktime( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let deposit_lbtc_amount = 1_000_u64; + let expected_premium_amount = 10_000_u64; + let expected_settlement_amount = 10_000_u64; + + let option_offer = prepare_option_offer( + &context, + deposit_lbtc_amount, + expected_premium_amount, + expected_settlement_amount, + -50, + )?; + + create_option_offer_with_premium( + &context, + &option_offer, + deposit_lbtc_amount, + expected_premium_amount, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + let collateral_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + "missing collateral covenant utxo", + )?; + let premium_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + "missing premium covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + for utxo in [collateral_program_utxo, premium_program_utxo] { + ft.add_program_input( + PartialInput::new(utxo).with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF), + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(OptionOfferBranch::Expiry)), + ), + RequiredSignature::Witness("USER_SIGHASH_ALL".to_string()), + ); + } + ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?).with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF), + RequiredSignature::NativeEcdsa, + ); + + let receiver_script_pubkey = signer.get_address().script_pubkey(); + ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + deposit_lbtc_amount, + option_offer.parameters.collateral_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey, + expected_premium_amount, + option_offer.parameters.premium_asset_id, + )); + + let (pst, _) = ft.extract_pst(); + let signature = + signer.sign_program(&pst, option_offer.get_program(), 0, context.get_network())?; + let mut signed_witness = HashMap::new(); + let witness = OptionOffer::get_witness(OptionOfferBranch::Expiry).build_witness(); + witness.iter().for_each(|(name, value)| { + signed_witness.insert(name.clone(), value.clone()); + }); + signed_witness.insert( + WitnessName::from_str_unchecked("USER_SIGHASH_ALL"), + Value::byte_array(signature.serialize()), + ); + let program_error = option_offer + .get_program() + .finalize( + &pst, + &WitnessValues::from(signed_witness), + 0, + context.get_network(), + ) + .expect_err("expiry should reject a missing absolute locktime"); + assert!(matches!(program_error, ProgramError::Pruning(_))); + + let program_utxos_after_rejection = + provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &program_utxos_after_rejection, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + ); + assert_has_utxo_by_asset_and_amount( + &program_utxos_after_rejection, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + ); + + Ok(()) +} diff --git a/crates/contracts/tests/regtest/option_offer/mod.rs b/crates/contracts/tests/regtest/option_offer/mod.rs new file mode 100644 index 0000000..3d1d16a --- /dev/null +++ b/crates/contracts/tests/regtest/option_offer/mod.rs @@ -0,0 +1,4 @@ +pub mod deposit; +pub mod exercise; +pub mod expiry; +pub mod withdraw; diff --git a/crates/contracts/tests/regtest/option_offer/withdraw.rs b/crates/contracts/tests/regtest/option_offer/withdraw.rs new file mode 100644 index 0000000..081f1ed --- /dev/null +++ b/crates/contracts/tests/regtest/option_offer/withdraw.rs @@ -0,0 +1,532 @@ +use crate::common::filters::{ + assert_has_utxo_by_asset_amount_and_script, assert_has_utxo_by_asset_and_amount, + require_utxo_by_asset_and_amount, +}; +use crate::common::signer::{ensure_exact_signer_utxo, finalize_and_broadcast, get_lbtc_utxo}; +use crate::program_builder::option_offer::{ + create_option_offer_with_premium, prepare_option_offer, +}; + +use std::collections::HashMap; + +use simplex::program::{ProgramError, ProgramTrait, WitnessTrait}; +use simplex::signer::SignerTrait; +use simplex::simplicityhl::str::WitnessName; +use simplex::simplicityhl::value::ValueConstructible; +use simplex::simplicityhl::{Value, WitnessValues}; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, +}; + +use contracts::programs::option_offer::{OptionOffer, OptionOfferBranch}; +use contracts::programs::program::SimplexProgram; + +#[simplex::test] +fn withdraw_option_offer(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let deposit_lbtc_amount = 1_000_u64; + let expected_premium_amount = 10_000_u64; + let expected_settlement_amount = 10_000_u64; + let partial_collateral_amount = 600_u64; + let remaining_collateral_amount = deposit_lbtc_amount - partial_collateral_amount; + let exercised_premium_amount = 6_000_u64; + let remaining_premium_amount = expected_premium_amount - exercised_premium_amount; + let exercised_settlement_amount = 6_000_u64; + + let option_offer = prepare_option_offer( + &context, + deposit_lbtc_amount, + expected_premium_amount, + expected_settlement_amount, + 1_000, + )?; + + create_option_offer_with_premium( + &context, + &option_offer, + deposit_lbtc_amount, + expected_premium_amount, + )?; + + let receiver_script_pubkey = signer.get_address().script_pubkey(); + let settlement_input = ensure_exact_signer_utxo( + &context, + option_offer.parameters.settlement_asset_id, + exercised_settlement_amount, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + let exercise_branch = OptionOfferBranch::Exercise { + collateral_amount: partial_collateral_amount, + is_change_needed: true, + }; + let collateral_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + "missing collateral covenant utxo", + )?; + let premium_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + "missing premium covenant utxo", + )?; + + let mut exercise_ft = FinalTransaction::new(); + + for utxo in [collateral_program_utxo, premium_program_utxo] { + exercise_ft.add_program_input( + PartialInput::new(utxo), + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(exercise_branch)), + ), + RequiredSignature::None, + ); + } + + exercise_ft.add_input( + PartialInput::new(settlement_input), + RequiredSignature::NativeEcdsa, + ); + exercise_ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + remaining_collateral_amount, + option_offer.parameters.collateral_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + remaining_premium_amount, + option_offer.parameters.premium_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + exercised_settlement_amount, + option_offer.parameters.settlement_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + partial_collateral_amount, + option_offer.parameters.collateral_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + exercised_premium_amount, + option_offer.parameters.premium_asset_id, + )); + + let _ = finalize_and_broadcast(&context, &exercise_ft)?; + + let exercised_program_utxos = + provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + let settlement_program_utxo = require_utxo_by_asset_and_amount( + &exercised_program_utxos, + option_offer.parameters.settlement_asset_id, + exercised_settlement_amount, + "missing settlement covenant utxo", + )?; + + let mut withdraw_ft = FinalTransaction::new(); + withdraw_ft.add_program_input( + PartialInput::new(settlement_program_utxo), + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(OptionOfferBranch::Withdraw)), + ), + RequiredSignature::Witness("USER_SIGHASH_ALL".to_string()), + ); + withdraw_ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?), + RequiredSignature::NativeEcdsa, + ); + withdraw_ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + exercised_settlement_amount, + option_offer.parameters.settlement_asset_id, + )); + + let withdraw_txid = finalize_and_broadcast(&context, &withdraw_ft)?; + + let program_utxos_after_withdraw = + provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &program_utxos_after_withdraw, + option_offer.parameters.collateral_asset_id, + remaining_collateral_amount, + ); + assert_has_utxo_by_asset_and_amount( + &program_utxos_after_withdraw, + option_offer.parameters.premium_asset_id, + remaining_premium_amount, + ); + + let signer_utxos = signer.get_utxos_txid(withdraw_txid)?; + assert_has_utxo_by_asset_amount_and_script( + &signer_utxos, + option_offer.parameters.settlement_asset_id, + exercised_settlement_amount, + &receiver_script_pubkey, + ); + + Ok(()) +} + +#[simplex::test] +fn withdraw_option_offer_rejects_invalid_signature( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let deposit_lbtc_amount = 1_000_u64; + let expected_premium_amount = 10_000_u64; + let expected_settlement_amount = 10_000_u64; + let partial_collateral_amount = 600_u64; + let remaining_collateral_amount = deposit_lbtc_amount - partial_collateral_amount; + let exercised_premium_amount = 6_000_u64; + let remaining_premium_amount = expected_premium_amount - exercised_premium_amount; + let exercised_settlement_amount = 6_000_u64; + + let prepared_option_offer = prepare_option_offer( + &context, + deposit_lbtc_amount, + expected_premium_amount, + expected_settlement_amount, + 1_000, + )?; + let mismatched_user_signer = context.create_signer( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ); + let mut option_offer_parameters = prepared_option_offer.parameters; + option_offer_parameters.user_pubkey = mismatched_user_signer.get_schnorr_public_key(); + let option_offer = OptionOffer::new(option_offer_parameters); + + create_option_offer_with_premium( + &context, + &option_offer, + deposit_lbtc_amount, + expected_premium_amount, + )?; + + let receiver_script_pubkey = signer.get_address().script_pubkey(); + let settlement_input = ensure_exact_signer_utxo( + &context, + option_offer.parameters.settlement_asset_id, + exercised_settlement_amount, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + let exercise_branch = OptionOfferBranch::Exercise { + collateral_amount: partial_collateral_amount, + is_change_needed: true, + }; + let collateral_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + "missing collateral covenant utxo", + )?; + let premium_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + "missing premium covenant utxo", + )?; + + let mut exercise_ft = FinalTransaction::new(); + + for utxo in [collateral_program_utxo, premium_program_utxo] { + exercise_ft.add_program_input( + PartialInput::new(utxo), + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(exercise_branch)), + ), + RequiredSignature::None, + ); + } + + exercise_ft.add_input( + PartialInput::new(settlement_input), + RequiredSignature::NativeEcdsa, + ); + exercise_ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + remaining_collateral_amount, + option_offer.parameters.collateral_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + remaining_premium_amount, + option_offer.parameters.premium_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + exercised_settlement_amount, + option_offer.parameters.settlement_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + partial_collateral_amount, + option_offer.parameters.collateral_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + receiver_script_pubkey, + exercised_premium_amount, + option_offer.parameters.premium_asset_id, + )); + + let _ = finalize_and_broadcast(&context, &exercise_ft)?; + + let exercised_program_utxos = + provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + let settlement_program_utxo = require_utxo_by_asset_and_amount( + &exercised_program_utxos, + option_offer.parameters.settlement_asset_id, + exercised_settlement_amount, + "missing settlement covenant utxo", + )?; + + let mut withdraw_ft = FinalTransaction::new(); + withdraw_ft.add_program_input( + PartialInput::new(settlement_program_utxo), + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(OptionOfferBranch::Withdraw)), + ), + RequiredSignature::Witness("USER_SIGHASH_ALL".to_string()), + ); + withdraw_ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?), + RequiredSignature::NativeEcdsa, + ); + withdraw_ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + exercised_settlement_amount, + option_offer.parameters.settlement_asset_id, + )); + + let (pst, _) = withdraw_ft.extract_pst(); + let signature = + signer.sign_program(&pst, option_offer.get_program(), 0, context.get_network())?; + let mut signed_witness = HashMap::new(); + let witness = OptionOffer::get_witness(OptionOfferBranch::Withdraw).build_witness(); + witness.iter().for_each(|(name, value)| { + signed_witness.insert(name.clone(), value.clone()); + }); + signed_witness.insert( + WitnessName::from_str_unchecked("USER_SIGHASH_ALL"), + Value::byte_array(signature.serialize()), + ); + let program_error = option_offer + .get_program() + .finalize( + &pst, + &WitnessValues::from(signed_witness), + 0, + context.get_network(), + ) + .expect_err("withdraw should reject a mismatched user signature"); + assert!(matches!(program_error, ProgramError::Pruning(_))); + + let program_utxos_after_rejection = + provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &program_utxos_after_rejection, + option_offer.parameters.collateral_asset_id, + remaining_collateral_amount, + ); + assert_has_utxo_by_asset_and_amount( + &program_utxos_after_rejection, + option_offer.parameters.premium_asset_id, + remaining_premium_amount, + ); + assert_has_utxo_by_asset_and_amount( + &program_utxos_after_rejection, + option_offer.parameters.settlement_asset_id, + exercised_settlement_amount, + ); + + Ok(()) +} + +#[simplex::test] +fn withdraw_option_offer_rejects_partial_output_amount( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let deposit_lbtc_amount = 1_000_u64; + let expected_premium_amount = 10_000_u64; + let expected_settlement_amount = 10_000_u64; + let partial_collateral_amount = 600_u64; + let remaining_collateral_amount = deposit_lbtc_amount - partial_collateral_amount; + let exercised_premium_amount = 6_000_u64; + let remaining_premium_amount = expected_premium_amount - exercised_premium_amount; + let exercised_settlement_amount = 6_000_u64; + + let option_offer = prepare_option_offer( + &context, + deposit_lbtc_amount, + expected_premium_amount, + expected_settlement_amount, + 1_000, + )?; + + create_option_offer_with_premium( + &context, + &option_offer, + deposit_lbtc_amount, + expected_premium_amount, + )?; + + let receiver_script_pubkey = signer.get_address().script_pubkey(); + let settlement_input = ensure_exact_signer_utxo( + &context, + option_offer.parameters.settlement_asset_id, + exercised_settlement_amount, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + let exercise_branch = OptionOfferBranch::Exercise { + collateral_amount: partial_collateral_amount, + is_change_needed: true, + }; + let collateral_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.collateral_asset_id, + deposit_lbtc_amount, + "missing collateral covenant utxo", + )?; + let premium_program_utxo = require_utxo_by_asset_and_amount( + &program_utxos, + option_offer.parameters.premium_asset_id, + expected_premium_amount, + "missing premium covenant utxo", + )?; + + let mut exercise_ft = FinalTransaction::new(); + + for utxo in [collateral_program_utxo, premium_program_utxo] { + exercise_ft.add_program_input( + PartialInput::new(utxo), + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(exercise_branch)), + ), + RequiredSignature::None, + ); + } + + exercise_ft.add_input( + PartialInput::new(settlement_input), + RequiredSignature::NativeEcdsa, + ); + exercise_ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + remaining_collateral_amount, + option_offer.parameters.collateral_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + remaining_premium_amount, + option_offer.parameters.premium_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + option_offer.get_script_pubkey(), + exercised_settlement_amount, + option_offer.parameters.settlement_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + partial_collateral_amount, + option_offer.parameters.collateral_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + exercised_premium_amount, + option_offer.parameters.premium_asset_id, + )); + + let _ = finalize_and_broadcast(&context, &exercise_ft)?; + + let exercised_program_utxos = + provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + let settlement_program_utxo = require_utxo_by_asset_and_amount( + &exercised_program_utxos, + option_offer.parameters.settlement_asset_id, + exercised_settlement_amount, + "missing settlement covenant utxo", + )?; + + let mut withdraw_ft = FinalTransaction::new(); + withdraw_ft.add_program_input( + PartialInput::new(settlement_program_utxo), + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(OptionOfferBranch::Withdraw)), + ), + RequiredSignature::Witness("USER_SIGHASH_ALL".to_string()), + ); + withdraw_ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?), + RequiredSignature::NativeEcdsa, + ); + withdraw_ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + exercised_settlement_amount - 1, + option_offer.parameters.settlement_asset_id, + )); + withdraw_ft.add_output(PartialOutput::new( + receiver_script_pubkey, + 1, + option_offer.parameters.settlement_asset_id, + )); + + let (pst, _) = withdraw_ft.extract_pst(); + let signature = + signer.sign_program(&pst, option_offer.get_program(), 0, context.get_network())?; + let mut signed_witness = HashMap::new(); + let witness = OptionOffer::get_witness(OptionOfferBranch::Withdraw).build_witness(); + witness.iter().for_each(|(name, value)| { + signed_witness.insert(name.clone(), value.clone()); + }); + signed_witness.insert( + WitnessName::from_str_unchecked("USER_SIGHASH_ALL"), + Value::byte_array(signature.serialize()), + ); + let program_error = option_offer + .get_program() + .finalize( + &pst, + &WitnessValues::from(signed_witness), + 0, + context.get_network(), + ) + .expect_err("withdraw should reject a partial settlement output at index 0"); + assert!(matches!(program_error, ProgramError::Pruning(_))); + + let program_utxos_after_rejection = + provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &program_utxos_after_rejection, + option_offer.parameters.collateral_asset_id, + remaining_collateral_amount, + ); + assert_has_utxo_by_asset_and_amount( + &program_utxos_after_rejection, + option_offer.parameters.premium_asset_id, + remaining_premium_amount, + ); + assert_has_utxo_by_asset_and_amount( + &program_utxos_after_rejection, + option_offer.parameters.settlement_asset_id, + exercised_settlement_amount, + ); + + Ok(()) +} diff --git a/crates/contracts/tests/regtest/options/cancel.rs b/crates/contracts/tests/regtest/options/cancel.rs new file mode 100644 index 0000000..3f4ece4 --- /dev/null +++ b/crates/contracts/tests/regtest/options/cancel.rs @@ -0,0 +1,245 @@ +use crate::common::filters::{ + assert_has_utxo_by_asset_and_amount, require_utxo_by_asset_and_amount, +}; +use crate::common::signer::{ensure_exact_signer_utxo, finalize_and_broadcast, get_lbtc_utxo}; +use crate::program_builder::options::{create_options, fund_options, prepare_options}; + +use contracts::programs::options::{Options, OptionsBranch}; +use contracts::programs::program::SimplexProgram; + +use simplex::simplicityhl::elements::Script; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, +}; + +#[simplex::test] +fn cancel_options(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let total_collateral_amount = 1_000_u64; + let expected_settlement_amount = 500_u64; + let contract_count = 10_u64; + + let prepared = prepare_options( + &context, + total_collateral_amount, + expected_settlement_amount, + contract_count, + -100, + 1_000, + )?; + let created = create_options(&context, prepared)?; + let funded = fund_options(&context, created, total_collateral_amount, contract_count)?; + + let option_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.option_token_asset, + contract_count, + )?; + let grantor_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.grantor_token_asset, + contract_count, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_collateral = require_utxo_by_asset_and_amount( + &program_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + "missing locked collateral covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + ft.add_program_input( + PartialInput::new(locked_collateral), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Cancel { + is_change_needed: false, + amount_to_burn: contract_count, + collateral_amount: total_collateral_amount, + })), + ), + RequiredSignature::None, + ); + ft.add_input( + PartialInput::new(option_token_input), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(grantor_token_input), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + contract_count, + funded.options.parameters.option_token_asset, + )); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + contract_count, + funded.options.parameters.grantor_token_asset, + )); + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + total_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + + let cancel_txid = finalize_and_broadcast(&context, &ft)?; + + let transaction = provider.fetch_transaction(&cancel_txid)?; + assert_eq!( + transaction.output[0].script_pubkey, + Script::new_op_return(b"burn") + ); + assert_eq!( + transaction.output[1].script_pubkey, + Script::new_op_return(b"burn") + ); + assert_eq!( + transaction.output[2].asset.explicit(), + Some(funded.options.parameters.collateral_asset_id) + ); + assert_eq!( + transaction.output[2].value.explicit(), + Some(total_collateral_amount) + ); + + let signer_utxos = signer.get_utxos_txid(cancel_txid)?; + assert_has_utxo_by_asset_and_amount( + &signer_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + ); + + Ok(()) +} + +#[simplex::test] +fn cancel_options_with_change(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + + let total_collateral_amount = 1_000_u64; + let expected_settlement_amount = 500_u64; + let contract_count = 10_u64; + let cancelled_contract_count = 6_u64; + let returned_collateral_amount = 600_u64; + let remaining_collateral_amount = total_collateral_amount - returned_collateral_amount; + + let prepared = prepare_options( + &context, + total_collateral_amount, + expected_settlement_amount, + contract_count, + -100, + 1_000, + )?; + let created = create_options(&context, prepared)?; + let funded = fund_options(&context, created, total_collateral_amount, contract_count)?; + + let option_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.option_token_asset, + cancelled_contract_count, + )?; + let grantor_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.grantor_token_asset, + cancelled_contract_count, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_collateral = require_utxo_by_asset_and_amount( + &program_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + "missing locked collateral covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + ft.add_program_input( + PartialInput::new(locked_collateral), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Cancel { + is_change_needed: true, + amount_to_burn: cancelled_contract_count, + collateral_amount: returned_collateral_amount, + })), + ), + RequiredSignature::None, + ); + ft.add_input( + PartialInput::new(option_token_input), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(grantor_token_input), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + funded.options.get_script_pubkey(), + remaining_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + cancelled_contract_count, + funded.options.parameters.option_token_asset, + )); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + cancelled_contract_count, + funded.options.parameters.grantor_token_asset, + )); + ft.add_output(PartialOutput::new( + context.get_default_signer().get_address().script_pubkey(), + returned_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + + let cancel_txid = finalize_and_broadcast(&context, &ft)?; + + let transaction = provider.fetch_transaction(&cancel_txid)?; + assert_eq!( + transaction.output[0].value.explicit(), + Some(remaining_collateral_amount) + ); + assert_eq!( + transaction.output[0].script_pubkey, + funded.options.get_script_pubkey() + ); + assert_eq!( + transaction.output[1].script_pubkey, + Script::new_op_return(b"burn") + ); + assert_eq!( + transaction.output[2].script_pubkey, + Script::new_op_return(b"burn") + ); + assert_eq!( + transaction.output[3].value.explicit(), + Some(returned_collateral_amount) + ); + + let covenant_utxos_after_cancel = + provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_cancel, + funded.options.parameters.collateral_asset_id, + remaining_collateral_amount, + ); + + Ok(()) +} diff --git a/crates/contracts/tests/regtest/options/exercise.rs b/crates/contracts/tests/regtest/options/exercise.rs new file mode 100644 index 0000000..63183cc --- /dev/null +++ b/crates/contracts/tests/regtest/options/exercise.rs @@ -0,0 +1,412 @@ +use crate::common::filters::{ + assert_has_utxo_by_asset_and_amount, require_utxo_by_asset_and_amount, +}; +use crate::common::signer::{ensure_exact_signer_utxo, finalize_and_broadcast, get_lbtc_utxo}; +use crate::program_builder::options::{create_options, fund_options, prepare_options}; + +use contracts::programs::options::{Options, OptionsBranch}; +use contracts::programs::program::SimplexProgram; + +use simplex::program::{ProgramError, ProgramTrait, WitnessTrait}; +use simplex::simplicityhl::elements::{LockTime, Script, Sequence}; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, +}; + +#[simplex::test] +fn exercise_options(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let total_collateral_amount = 1_000_u64; + let expected_settlement_amount = 500_u64; + let contract_count = 10_u64; + + let prepared = prepare_options( + &context, + total_collateral_amount, + expected_settlement_amount, + contract_count, + -100, + 1_000, + )?; + let created = create_options(&context, prepared)?; + let funded = fund_options(&context, created, total_collateral_amount, contract_count)?; + + let receiver_script_pubkey = signer.get_address().script_pubkey(); + let option_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.option_token_asset, + contract_count, + )?; + let settlement_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.settlement_asset_id, + expected_settlement_amount, + )?; + let locktime = LockTime::from_time(funded.options.parameters.start_time) + .map_err(|error| anyhow::anyhow!(error))?; + + let program_utxos = provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_collateral = require_utxo_by_asset_and_amount( + &program_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + "missing locked collateral covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + ft.add_program_input( + PartialInput::new(locked_collateral) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Exercise { + is_change_needed: false, + amount_to_burn: contract_count, + collateral_amount: total_collateral_amount, + settlement_amount: expected_settlement_amount, + })), + ), + RequiredSignature::None, + ); + ft.add_input( + PartialInput::new(option_token_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(settlement_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + contract_count, + funded.options.parameters.option_token_asset, + )); + ft.add_output(PartialOutput::new( + funded.options.get_script_pubkey(), + expected_settlement_amount, + funded.options.parameters.settlement_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + total_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + + let exercise_txid = finalize_and_broadcast(&context, &ft)?; + + let transaction = provider.fetch_transaction(&exercise_txid)?; + assert_eq!( + transaction.output[0].asset.explicit(), + Some(funded.options.parameters.option_token_asset) + ); + assert_eq!(transaction.output[0].value.explicit(), Some(contract_count)); + assert_eq!( + transaction.output[0].script_pubkey, + Script::new_op_return(b"burn") + ); + + assert_eq!( + transaction.output[1].asset.explicit(), + Some(funded.options.parameters.settlement_asset_id) + ); + assert_eq!( + transaction.output[1].value.explicit(), + Some(expected_settlement_amount) + ); + assert_eq!( + transaction.output[1].script_pubkey, + funded.options.get_script_pubkey() + ); + + assert_eq!( + transaction.output[2].asset.explicit(), + Some(funded.options.parameters.collateral_asset_id) + ); + assert_eq!( + transaction.output[2].value.explicit(), + Some(total_collateral_amount) + ); + assert_eq!(transaction.output[2].script_pubkey, receiver_script_pubkey); + + let covenant_utxos_after_exercise = + provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_exercise, + funded.options.parameters.settlement_asset_id, + expected_settlement_amount, + ); + + let signer_utxos = signer.get_utxos_txid(exercise_txid)?; + assert_has_utxo_by_asset_and_amount( + &signer_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + ); + + Ok(()) +} + +#[simplex::test] +fn exercise_options_with_change(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let total_collateral_amount = 1_000_u64; + let expected_settlement_amount = 500_u64; + let contract_count = 10_u64; + let exercised_contract_count = 6_u64; + let exercised_collateral_amount = 600_u64; + let exercised_settlement_amount = 300_u64; + let remaining_collateral_amount = total_collateral_amount - exercised_collateral_amount; + + let prepared = prepare_options( + &context, + total_collateral_amount, + expected_settlement_amount, + contract_count, + -100, + 1_000, + )?; + let created = create_options(&context, prepared)?; + let funded = fund_options(&context, created, total_collateral_amount, contract_count)?; + + let receiver_script_pubkey = signer.get_address().script_pubkey(); + let option_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.option_token_asset, + exercised_contract_count, + )?; + let settlement_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.settlement_asset_id, + exercised_settlement_amount, + )?; + let locktime = LockTime::from_time(funded.options.parameters.start_time) + .map_err(|error| anyhow::anyhow!(error))?; + + let program_utxos = provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_collateral = require_utxo_by_asset_and_amount( + &program_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + "missing locked collateral covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + ft.add_program_input( + PartialInput::new(locked_collateral) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Exercise { + is_change_needed: true, + amount_to_burn: exercised_contract_count, + collateral_amount: exercised_collateral_amount, + settlement_amount: exercised_settlement_amount, + })), + ), + RequiredSignature::None, + ); + ft.add_input( + PartialInput::new(option_token_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(settlement_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + funded.options.get_script_pubkey(), + remaining_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + exercised_contract_count, + funded.options.parameters.option_token_asset, + )); + ft.add_output(PartialOutput::new( + funded.options.get_script_pubkey(), + exercised_settlement_amount, + funded.options.parameters.settlement_asset_id, + )); + ft.add_output(PartialOutput::new( + receiver_script_pubkey.clone(), + exercised_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + + let exercise_txid = finalize_and_broadcast(&context, &ft)?; + + let transaction = provider.fetch_transaction(&exercise_txid)?; + assert_eq!( + transaction.output[0].value.explicit(), + Some(remaining_collateral_amount) + ); + assert_eq!( + transaction.output[0].script_pubkey, + funded.options.get_script_pubkey() + ); + assert_eq!( + transaction.output[1].script_pubkey, + Script::new_op_return(b"burn") + ); + assert_eq!( + transaction.output[2].value.explicit(), + Some(exercised_settlement_amount) + ); + assert_eq!( + transaction.output[2].script_pubkey, + funded.options.get_script_pubkey() + ); + assert_eq!( + transaction.output[3].value.explicit(), + Some(exercised_collateral_amount) + ); + assert_eq!(transaction.output[3].script_pubkey, receiver_script_pubkey); + + let covenant_utxos_after_exercise = + provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_exercise, + funded.options.parameters.collateral_asset_id, + remaining_collateral_amount, + ); + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_exercise, + funded.options.parameters.settlement_asset_id, + exercised_settlement_amount, + ); + + Ok(()) +} + +#[simplex::test] +fn exercise_options_rejects_missing_locktime(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + + let total_collateral_amount = 1_000_u64; + let expected_settlement_amount = 500_u64; + let contract_count = 10_u64; + + let prepared = prepare_options( + &context, + total_collateral_amount, + expected_settlement_amount, + contract_count, + -100, + 1_000, + )?; + let created = create_options(&context, prepared)?; + let funded = fund_options(&context, created, total_collateral_amount, contract_count)?; + + let option_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.option_token_asset, + contract_count, + )?; + let settlement_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.settlement_asset_id, + expected_settlement_amount, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_collateral = require_utxo_by_asset_and_amount( + &program_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + "missing locked collateral covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + ft.add_program_input( + PartialInput::new(locked_collateral).with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Exercise { + is_change_needed: false, + amount_to_burn: contract_count, + collateral_amount: total_collateral_amount, + settlement_amount: expected_settlement_amount, + })), + ), + RequiredSignature::None, + ); + ft.add_input( + PartialInput::new(option_token_input).with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(settlement_input).with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?).with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + contract_count, + funded.options.parameters.option_token_asset, + )); + ft.add_output(PartialOutput::new( + funded.options.get_script_pubkey(), + expected_settlement_amount, + funded.options.parameters.settlement_asset_id, + )); + ft.add_output(PartialOutput::new( + context.get_default_signer().get_address().script_pubkey(), + total_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + + let (pst, _) = ft.extract_pst(); + let witness = Options::get_witness(OptionsBranch::Exercise { + is_change_needed: false, + amount_to_burn: contract_count, + collateral_amount: total_collateral_amount, + settlement_amount: expected_settlement_amount, + }) + .build_witness(); + let program_error = funded + .options + .get_program() + .finalize(&pst, &witness, 0, context.get_network()) + .expect_err("exercise should reject a missing absolute locktime"); + assert!(matches!(program_error, ProgramError::Pruning(_))); + + let covenant_utxos_after_rejection = + provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_rejection, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + ); + + Ok(()) +} diff --git a/crates/contracts/tests/regtest/options/expiry.rs b/crates/contracts/tests/regtest/options/expiry.rs new file mode 100644 index 0000000..eae3038 --- /dev/null +++ b/crates/contracts/tests/regtest/options/expiry.rs @@ -0,0 +1,325 @@ +use crate::common::filters::{ + assert_has_utxo_by_asset_and_amount, require_utxo_by_asset_and_amount, +}; +use crate::common::signer::{ensure_exact_signer_utxo, finalize_and_broadcast, get_lbtc_utxo}; +use crate::program_builder::options::{create_options, fund_options, prepare_options}; + +use contracts::programs::options::{Options, OptionsBranch}; +use contracts::programs::program::SimplexProgram; + +use simplex::program::{ProgramError, ProgramTrait, WitnessTrait}; +use simplex::simplicityhl::elements::{LockTime, Script, Sequence}; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, +}; + +#[simplex::test] +fn expire_options(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let total_collateral_amount = 1_000_u64; + let expected_settlement_amount = 500_u64; + let contract_count = 10_u64; + + let prepared = prepare_options( + &context, + total_collateral_amount, + expected_settlement_amount, + contract_count, + -100, + -50, + )?; + let created = create_options(&context, prepared)?; + let funded = fund_options(&context, created, total_collateral_amount, contract_count)?; + + let grantor_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.grantor_token_asset, + contract_count, + )?; + let locktime = LockTime::from_time(funded.options.parameters.expiry_time) + .map_err(|error| anyhow::anyhow!(error))?; + + let program_utxos = provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_collateral = require_utxo_by_asset_and_amount( + &program_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + "missing locked collateral covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + ft.add_program_input( + PartialInput::new(locked_collateral) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Expiry { + is_change_needed: false, + amount_to_burn: contract_count, + collateral_amount: total_collateral_amount, + })), + ), + RequiredSignature::None, + ); + ft.add_input( + PartialInput::new(grantor_token_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + contract_count, + funded.options.parameters.grantor_token_asset, + )); + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + total_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + + let expiry_txid = finalize_and_broadcast(&context, &ft)?; + + let transaction = provider.fetch_transaction(&expiry_txid)?; + assert_eq!( + transaction.output[0].script_pubkey, + Script::new_op_return(b"burn") + ); + assert_eq!( + transaction.output[0].asset.explicit(), + Some(funded.options.parameters.grantor_token_asset) + ); + assert_eq!(transaction.output[0].value.explicit(), Some(contract_count)); + assert_eq!( + transaction.output[1].asset.explicit(), + Some(funded.options.parameters.collateral_asset_id) + ); + assert_eq!( + transaction.output[1].value.explicit(), + Some(total_collateral_amount) + ); + assert_eq!( + transaction.output[1].script_pubkey, + signer.get_address().script_pubkey() + ); + + let signer_utxos = signer.get_utxos_txid(expiry_txid)?; + assert_has_utxo_by_asset_and_amount( + &signer_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + ); + + Ok(()) +} + +#[simplex::test] +fn expire_options_with_change(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + + let total_collateral_amount = 1_000_u64; + let expected_settlement_amount = 500_u64; + let contract_count = 10_u64; + let expired_contract_count = 6_u64; + let returned_collateral_amount = 600_u64; + let remaining_collateral_amount = total_collateral_amount - returned_collateral_amount; + + let prepared = prepare_options( + &context, + total_collateral_amount, + expected_settlement_amount, + contract_count, + -100, + -50, + )?; + let created = create_options(&context, prepared)?; + let funded = fund_options(&context, created, total_collateral_amount, contract_count)?; + + let grantor_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.grantor_token_asset, + expired_contract_count, + )?; + let locktime = LockTime::from_time(funded.options.parameters.expiry_time) + .map_err(|error| anyhow::anyhow!(error))?; + + let program_utxos = provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_collateral = require_utxo_by_asset_and_amount( + &program_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + "missing locked collateral covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + ft.add_program_input( + PartialInput::new(locked_collateral) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Expiry { + is_change_needed: true, + amount_to_burn: expired_contract_count, + collateral_amount: returned_collateral_amount, + })), + ), + RequiredSignature::None, + ); + ft.add_input( + PartialInput::new(grantor_token_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + funded.options.get_script_pubkey(), + remaining_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + expired_contract_count, + funded.options.parameters.grantor_token_asset, + )); + ft.add_output(PartialOutput::new( + context.get_default_signer().get_address().script_pubkey(), + returned_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + + let expiry_txid = finalize_and_broadcast(&context, &ft)?; + + let transaction = provider.fetch_transaction(&expiry_txid)?; + assert_eq!( + transaction.output[0].value.explicit(), + Some(remaining_collateral_amount) + ); + assert_eq!( + transaction.output[0].script_pubkey, + funded.options.get_script_pubkey() + ); + assert_eq!( + transaction.output[1].script_pubkey, + Script::new_op_return(b"burn") + ); + assert_eq!( + transaction.output[2].value.explicit(), + Some(returned_collateral_amount) + ); + + let covenant_utxos_after_expiry = + provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_expiry, + funded.options.parameters.collateral_asset_id, + remaining_collateral_amount, + ); + + Ok(()) +} + +#[simplex::test] +fn expire_options_rejects_missing_locktime(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + + let total_collateral_amount = 1_000_u64; + let expected_settlement_amount = 500_u64; + let contract_count = 10_u64; + + let prepared = prepare_options( + &context, + total_collateral_amount, + expected_settlement_amount, + contract_count, + -100, + -50, + )?; + let created = create_options(&context, prepared)?; + let funded = fund_options(&context, created, total_collateral_amount, contract_count)?; + + let grantor_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.grantor_token_asset, + contract_count, + )?; + + let program_utxos = provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_collateral = require_utxo_by_asset_and_amount( + &program_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + "missing locked collateral covenant utxo", + )?; + + let mut ft = FinalTransaction::new(); + ft.add_program_input( + PartialInput::new(locked_collateral).with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Expiry { + is_change_needed: false, + amount_to_burn: contract_count, + collateral_amount: total_collateral_amount, + })), + ), + RequiredSignature::None, + ); + ft.add_input( + PartialInput::new(grantor_token_input).with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?).with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF), + RequiredSignature::NativeEcdsa, + ); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + contract_count, + funded.options.parameters.grantor_token_asset, + )); + ft.add_output(PartialOutput::new( + context.get_default_signer().get_address().script_pubkey(), + total_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + + let (pst, _) = ft.extract_pst(); + let witness = Options::get_witness(OptionsBranch::Expiry { + is_change_needed: false, + amount_to_burn: contract_count, + collateral_amount: total_collateral_amount, + }) + .build_witness(); + let program_error = funded + .options + .get_program() + .finalize(&pst, &witness, 0, context.get_network()) + .expect_err("expiry should reject a missing absolute locktime"); + assert!(matches!(program_error, ProgramError::Pruning(_))); + + let covenant_utxos_after_rejection = + provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_rejection, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + ); + + Ok(()) +} diff --git a/crates/contracts/tests/regtest/options/fund.rs b/crates/contracts/tests/regtest/options/fund.rs new file mode 100644 index 0000000..0d57777 --- /dev/null +++ b/crates/contracts/tests/regtest/options/fund.rs @@ -0,0 +1,117 @@ +use crate::common::filters::{ + assert_has_utxo_by_asset_amount_and_script, assert_has_utxo_by_asset_and_amount, +}; +use crate::program_builder::options::{create_options, fund_options, prepare_options}; + +use contracts::programs::program::SimplexProgram; + +#[simplex::test] +fn fund_options_contract(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let total_collateral_amount = 1_000_u64; + let expected_settlement_amount = 500_u64; + let contract_count = 10_u64; + + let prepared = prepare_options( + &context, + total_collateral_amount, + expected_settlement_amount, + contract_count, + -100, + 1_000, + )?; + let created = create_options(&context, prepared)?; + let funded = fund_options(&context, created, total_collateral_amount, contract_count)?; + + let funding_tx = provider.fetch_transaction(&funded.funding_txid)?; + assert_eq!(funded.option_reissuance_token.outpoint.vout, 0); + assert_eq!( + funded.option_reissuance_token.asset(), + funded.options.parameters.option_reissuance_token_asset + ); + assert_eq!(funded.option_reissuance_token.amount(), 1); + assert_eq!( + funded.option_reissuance_token.txout.script_pubkey, + funded.options.get_script_pubkey() + ); + + assert_eq!(funded.grantor_reissuance_token.outpoint.vout, 1); + assert_eq!( + funded.grantor_reissuance_token.asset(), + funded.options.parameters.grantor_reissuance_token_asset + ); + assert_eq!(funded.grantor_reissuance_token.amount(), 1); + assert_eq!( + funded.grantor_reissuance_token.txout.script_pubkey, + funded.options.get_script_pubkey() + ); + + assert_eq!( + funding_tx.output[2].asset.explicit(), + Some(funded.options.parameters.collateral_asset_id) + ); + assert_eq!( + funding_tx.output[2].value.explicit(), + Some(total_collateral_amount) + ); + assert_eq!( + funding_tx.output[2].script_pubkey, + funded.options.get_script_pubkey() + ); + + assert_eq!( + funding_tx.output[3].asset.explicit(), + Some(funded.options.parameters.option_token_asset) + ); + assert_eq!(funding_tx.output[3].value.explicit(), Some(contract_count)); + assert_eq!( + funding_tx.output[3].script_pubkey, + signer.get_address().script_pubkey() + ); + + assert_eq!( + funding_tx.output[4].asset.explicit(), + Some(funded.options.parameters.grantor_token_asset) + ); + assert_eq!(funding_tx.output[4].value.explicit(), Some(contract_count)); + assert_eq!( + funding_tx.output[4].script_pubkey, + signer.get_address().script_pubkey() + ); + + let program_utxos = provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + assert_eq!( + funded.option_reissuance_token.asset(), + funded.options.parameters.option_reissuance_token_asset + ); + assert_eq!(funded.option_reissuance_token.amount(), 1); + assert_eq!( + funded.grantor_reissuance_token.asset(), + funded.options.parameters.grantor_reissuance_token_asset + ); + assert_eq!(funded.grantor_reissuance_token.amount(), 1); + assert_has_utxo_by_asset_and_amount( + &program_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + ); + + let signer_utxos = signer.get_utxos_txid(funded.funding_txid)?; + let receiver_script_pubkey = signer.get_address().script_pubkey(); + assert_has_utxo_by_asset_amount_and_script( + &signer_utxos, + funded.options.parameters.option_token_asset, + contract_count, + &receiver_script_pubkey, + ); + assert_has_utxo_by_asset_amount_and_script( + &signer_utxos, + funded.options.parameters.grantor_token_asset, + contract_count, + &receiver_script_pubkey, + ); + + Ok(()) +} diff --git a/crates/contracts/tests/regtest/options/mod.rs b/crates/contracts/tests/regtest/options/mod.rs new file mode 100644 index 0000000..1a00ada --- /dev/null +++ b/crates/contracts/tests/regtest/options/mod.rs @@ -0,0 +1,5 @@ +pub mod cancel; +pub mod exercise; +pub mod expiry; +pub mod fund; +pub mod settlement; diff --git a/crates/contracts/tests/regtest/options/settlement.rs b/crates/contracts/tests/regtest/options/settlement.rs new file mode 100644 index 0000000..f337fb9 --- /dev/null +++ b/crates/contracts/tests/regtest/options/settlement.rs @@ -0,0 +1,545 @@ +use crate::common::filters::{ + assert_has_utxo_by_asset_and_amount, require_utxo_by_asset_and_amount, +}; +use crate::common::signer::{ensure_exact_signer_utxo, finalize_and_broadcast, get_lbtc_utxo}; +use crate::program_builder::options::{create_options, fund_options, prepare_options}; + +use contracts::programs::options::{Options, OptionsBranch}; +use contracts::programs::program::SimplexProgram; + +use simplex::program::{ProgramError, ProgramTrait, WitnessTrait}; +use simplex::simplicityhl::elements::{LockTime, Script, Sequence}; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, +}; + +#[simplex::test] +fn settle_options(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let total_collateral_amount = 1_000_u64; + let expected_settlement_amount = 500_u64; + let contract_count = 10_u64; + + let prepared = prepare_options( + &context, + total_collateral_amount, + expected_settlement_amount, + contract_count, + -100, + 1_000, + )?; + let created = create_options(&context, prepared)?; + let funded = fund_options(&context, created, total_collateral_amount, contract_count)?; + + let option_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.option_token_asset, + contract_count, + )?; + let settlement_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.settlement_asset_id, + expected_settlement_amount, + )?; + let locktime = LockTime::from_time(funded.options.parameters.start_time) + .map_err(|error| anyhow::anyhow!(error))?; + + let program_utxos = provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_collateral = require_utxo_by_asset_and_amount( + &program_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + "missing locked collateral covenant utxo", + )?; + + let mut exercise_ft = FinalTransaction::new(); + exercise_ft.add_program_input( + PartialInput::new(locked_collateral) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Exercise { + is_change_needed: false, + amount_to_burn: contract_count, + collateral_amount: total_collateral_amount, + settlement_amount: expected_settlement_amount, + })), + ), + RequiredSignature::None, + ); + exercise_ft.add_input( + PartialInput::new(option_token_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + exercise_ft.add_input( + PartialInput::new(settlement_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + exercise_ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + exercise_ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + contract_count, + funded.options.parameters.option_token_asset, + )); + exercise_ft.add_output(PartialOutput::new( + funded.options.get_script_pubkey(), + expected_settlement_amount, + funded.options.parameters.settlement_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + total_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + + let _ = finalize_and_broadcast(&context, &exercise_ft)?; + + let exercised_program_utxos = + provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_settlement = require_utxo_by_asset_and_amount( + &exercised_program_utxos, + funded.options.parameters.settlement_asset_id, + expected_settlement_amount, + "missing locked settlement covenant utxo", + )?; + let grantor_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.grantor_token_asset, + contract_count, + )?; + + let mut settlement_ft = FinalTransaction::new(); + settlement_ft.add_program_input( + PartialInput::new(locked_settlement) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Settlement { + is_change_needed: false, + amount_to_burn: contract_count, + settlement_amount: expected_settlement_amount, + })), + ), + RequiredSignature::None, + ); + settlement_ft.add_input( + PartialInput::new(grantor_token_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + settlement_ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + settlement_ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + contract_count, + funded.options.parameters.grantor_token_asset, + )); + settlement_ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + expected_settlement_amount, + funded.options.parameters.settlement_asset_id, + )); + + let settlement_txid = finalize_and_broadcast(&context, &settlement_ft)?; + + let transaction = provider.fetch_transaction(&settlement_txid)?; + assert_eq!( + transaction.output[0].script_pubkey, + Script::new_op_return(b"burn") + ); + assert_eq!( + transaction.output[0].asset.explicit(), + Some(funded.options.parameters.grantor_token_asset) + ); + assert_eq!(transaction.output[0].value.explicit(), Some(contract_count)); + assert_eq!( + transaction.output[1].asset.explicit(), + Some(funded.options.parameters.settlement_asset_id) + ); + assert_eq!( + transaction.output[1].value.explicit(), + Some(expected_settlement_amount) + ); + assert_eq!( + transaction.output[1].script_pubkey, + signer.get_address().script_pubkey() + ); + + let signer_utxos = signer.get_utxos_txid(settlement_txid)?; + assert_has_utxo_by_asset_and_amount( + &signer_utxos, + funded.options.parameters.settlement_asset_id, + expected_settlement_amount, + ); + + Ok(()) +} + +#[simplex::test] +fn settle_options_with_change(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let total_collateral_amount = 1_000_u64; + let expected_settlement_amount = 500_u64; + let contract_count = 10_u64; + let settled_contract_count = 6_u64; + let settled_settlement_amount = 300_u64; + let remaining_settlement_amount = expected_settlement_amount - settled_settlement_amount; + + let prepared = prepare_options( + &context, + total_collateral_amount, + expected_settlement_amount, + contract_count, + -100, + 1_000, + )?; + let created = create_options(&context, prepared)?; + let funded = fund_options(&context, created, total_collateral_amount, contract_count)?; + + let option_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.option_token_asset, + contract_count, + )?; + let settlement_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.settlement_asset_id, + expected_settlement_amount, + )?; + let locktime = LockTime::from_time(funded.options.parameters.start_time) + .map_err(|error| anyhow::anyhow!(error))?; + + let program_utxos = provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_collateral = require_utxo_by_asset_and_amount( + &program_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + "missing locked collateral covenant utxo", + )?; + + let mut exercise_ft = FinalTransaction::new(); + exercise_ft.add_program_input( + PartialInput::new(locked_collateral) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Exercise { + is_change_needed: false, + amount_to_burn: contract_count, + collateral_amount: total_collateral_amount, + settlement_amount: expected_settlement_amount, + })), + ), + RequiredSignature::None, + ); + exercise_ft.add_input( + PartialInput::new(option_token_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + exercise_ft.add_input( + PartialInput::new(settlement_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + exercise_ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + exercise_ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + contract_count, + funded.options.parameters.option_token_asset, + )); + exercise_ft.add_output(PartialOutput::new( + funded.options.get_script_pubkey(), + expected_settlement_amount, + funded.options.parameters.settlement_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + total_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + + let _ = finalize_and_broadcast(&context, &exercise_ft)?; + + let exercised_program_utxos = + provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_settlement = require_utxo_by_asset_and_amount( + &exercised_program_utxos, + funded.options.parameters.settlement_asset_id, + expected_settlement_amount, + "missing locked settlement covenant utxo", + )?; + let grantor_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.grantor_token_asset, + settled_contract_count, + )?; + + let mut settlement_ft = FinalTransaction::new(); + settlement_ft.add_program_input( + PartialInput::new(locked_settlement) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Settlement { + is_change_needed: true, + amount_to_burn: settled_contract_count, + settlement_amount: settled_settlement_amount, + })), + ), + RequiredSignature::None, + ); + settlement_ft.add_input( + PartialInput::new(grantor_token_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + settlement_ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + settlement_ft.add_output(PartialOutput::new( + funded.options.get_script_pubkey(), + remaining_settlement_amount, + funded.options.parameters.settlement_asset_id, + )); + settlement_ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + settled_contract_count, + funded.options.parameters.grantor_token_asset, + )); + settlement_ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + settled_settlement_amount, + funded.options.parameters.settlement_asset_id, + )); + + let settlement_txid = finalize_and_broadcast(&context, &settlement_ft)?; + + let transaction = provider.fetch_transaction(&settlement_txid)?; + assert_eq!( + transaction.output[0].value.explicit(), + Some(remaining_settlement_amount) + ); + assert_eq!( + transaction.output[0].script_pubkey, + funded.options.get_script_pubkey() + ); + assert_eq!( + transaction.output[1].script_pubkey, + Script::new_op_return(b"burn") + ); + assert_eq!( + transaction.output[2].value.explicit(), + Some(settled_settlement_amount) + ); + assert_eq!( + transaction.output[2].script_pubkey, + signer.get_address().script_pubkey() + ); + + let covenant_utxos_after_settlement = + provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_settlement, + funded.options.parameters.settlement_asset_id, + remaining_settlement_amount, + ); + + Ok(()) +} + +#[simplex::test] +fn settle_options_rejects_missing_locktime(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + + let total_collateral_amount = 1_000_u64; + let expected_settlement_amount = 500_u64; + let contract_count = 10_u64; + + let prepared = prepare_options( + &context, + total_collateral_amount, + expected_settlement_amount, + contract_count, + -100, + 1_000, + )?; + let created = create_options(&context, prepared)?; + let funded = fund_options(&context, created, total_collateral_amount, contract_count)?; + + let option_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.option_token_asset, + contract_count, + )?; + let settlement_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.settlement_asset_id, + expected_settlement_amount, + )?; + let locktime = LockTime::from_time(funded.options.parameters.start_time) + .map_err(|error| anyhow::anyhow!(error))?; + + let program_utxos = provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_collateral = require_utxo_by_asset_and_amount( + &program_utxos, + funded.options.parameters.collateral_asset_id, + total_collateral_amount, + "missing locked collateral covenant utxo", + )?; + + let mut exercise_ft = FinalTransaction::new(); + exercise_ft.add_program_input( + PartialInput::new(locked_collateral) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Exercise { + is_change_needed: false, + amount_to_burn: contract_count, + collateral_amount: total_collateral_amount, + settlement_amount: expected_settlement_amount, + })), + ), + RequiredSignature::None, + ); + exercise_ft.add_input( + PartialInput::new(option_token_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + exercise_ft.add_input( + PartialInput::new(settlement_input) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + exercise_ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime), + RequiredSignature::NativeEcdsa, + ); + exercise_ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + contract_count, + funded.options.parameters.option_token_asset, + )); + exercise_ft.add_output(PartialOutput::new( + funded.options.get_script_pubkey(), + expected_settlement_amount, + funded.options.parameters.settlement_asset_id, + )); + exercise_ft.add_output(PartialOutput::new( + context.get_default_signer().get_address().script_pubkey(), + total_collateral_amount, + funded.options.parameters.collateral_asset_id, + )); + + let _ = finalize_and_broadcast(&context, &exercise_ft)?; + + let exercised_program_utxos = + provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + let locked_settlement = require_utxo_by_asset_and_amount( + &exercised_program_utxos, + funded.options.parameters.settlement_asset_id, + expected_settlement_amount, + "missing locked settlement covenant utxo", + )?; + let grantor_token_input = ensure_exact_signer_utxo( + &context, + funded.options.parameters.grantor_token_asset, + contract_count, + )?; + + let mut settlement_ft = FinalTransaction::new(); + settlement_ft.add_program_input( + PartialInput::new(locked_settlement).with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF), + ProgramInput::new( + Box::new(funded.options.get_program().clone()), + Box::new(Options::get_witness(OptionsBranch::Settlement { + is_change_needed: false, + amount_to_burn: contract_count, + settlement_amount: expected_settlement_amount, + })), + ), + RequiredSignature::None, + ); + settlement_ft.add_input( + PartialInput::new(grantor_token_input).with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF), + RequiredSignature::NativeEcdsa, + ); + settlement_ft.add_input( + PartialInput::new(get_lbtc_utxo(&context)?).with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF), + RequiredSignature::NativeEcdsa, + ); + settlement_ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + contract_count, + funded.options.parameters.grantor_token_asset, + )); + settlement_ft.add_output(PartialOutput::new( + context.get_default_signer().get_address().script_pubkey(), + expected_settlement_amount, + funded.options.parameters.settlement_asset_id, + )); + + let (pst, _) = settlement_ft.extract_pst(); + let witness = Options::get_witness(OptionsBranch::Settlement { + is_change_needed: false, + amount_to_burn: contract_count, + settlement_amount: expected_settlement_amount, + }) + .build_witness(); + let program_error = funded + .options + .get_program() + .finalize(&pst, &witness, 0, context.get_network()) + .expect_err("settlement should reject a missing absolute locktime"); + assert!(matches!(program_error, ProgramError::Pruning(_))); + + let covenant_utxos_after_rejection = + provider.fetch_scripthash_utxos(&funded.options.get_script_pubkey())?; + assert_has_utxo_by_asset_and_amount( + &covenant_utxos_after_rejection, + funded.options.parameters.settlement_asset_id, + expected_settlement_amount, + ); + + Ok(()) +} diff --git a/crates/wallet-abi/Cargo.toml b/crates/wallet-abi/Cargo.toml deleted file mode 100644 index cec1ae2..0000000 --- a/crates/wallet-abi/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "wallet-abi" -version = "0.1.0" -edition = "2024" -description = "Schema-driven wallet create runtime for Simplicity contracts" -license = "MIT OR Apache-2.0" -publish = false - -[lints] -workspace = true - -[dependencies] -thiserror = "2" - -bincode = "2.0.1" - -serde = { version = "1", features = ["derive"] } -serde_json = "1" - -hex = { workspace = true } -sha2 = { workspace = true } -ring = { workspace = true } - -simplicityhl = { workspace = true } - -lwk_wollet = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_wollet", default-features = false, features = ["electrum", "esplora"] } -lwk_signer = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_signer", default-features = false } -lwk_common = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_common", default-features = false } -lwk_simplicity = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_simplicity" } - -tokio = { version = "1", features = ["sync"] } diff --git a/crates/wallet-abi/src/encoding.rs b/crates/wallet-abi/src/encoding.rs deleted file mode 100644 index f512f32..0000000 --- a/crates/wallet-abi/src/encoding.rs +++ /dev/null @@ -1,83 +0,0 @@ -pub use bincode::{Decode, Encode}; - -use crate::error::EncodingError; - -/// Trait for binary encoding/decoding with hex string support. -pub trait Encodable { - fn encode(&self) -> Result, EncodingError> - where - Self: Encode, - { - Ok(bincode::encode_to_vec(self, bincode::config::standard())?) - } - - /// Decode from binary and fail if trailing bytes remain. - fn decode(buf: &[u8]) -> Result - where - Self: Sized + Decode<()>, - { - let (decoded, consumed) = bincode::decode_from_slice(buf, bincode::config::standard())?; - if consumed != buf.len() { - return Err(EncodingError::TrailingBytes { - consumed, - total: buf.len(), - }); - } - - Ok(decoded) - } - - fn to_hex(&self) -> Result - where - Self: Encode, - { - Ok(hex::encode(Encodable::encode(self)?)) - } - - fn from_hex(hex: &str) -> Result - where - Self: bincode::Decode<()>, - { - Encodable::decode(&hex::decode(hex)?) - } -} - -#[cfg(test)] -mod tests { - use super::Encodable; - use crate::error::EncodingError; - - #[derive(Debug, PartialEq, bincode::Encode, bincode::Decode)] - struct TestPayload { - value: u32, - } - - impl Encodable for TestPayload {} - - #[test] - fn decode_rejects_trailing_bytes() { - let payload = TestPayload { value: 7 }; - let mut encoded = payload.encode().expect("encodes"); - encoded.push(0); - - let err = ::decode(&encoded).expect_err("must reject trailing"); - assert!(matches!( - err, - EncodingError::TrailingBytes { consumed, total } if consumed + 1 == total - )); - } - - #[test] - fn from_hex_rejects_trailing_bytes() { - let payload = TestPayload { value: 7 }; - let mut encoded_hex = payload.to_hex().expect("encodes"); - encoded_hex.push_str("00"); - - let err = - ::from_hex(&encoded_hex).expect_err("must reject trailing"); - assert!(matches!( - err, - EncodingError::TrailingBytes { consumed, total } if consumed + 1 == total - )); - } -} diff --git a/crates/wallet-abi/src/error.rs b/crates/wallet-abi/src/error.rs deleted file mode 100644 index 1e551f1..0000000 --- a/crates/wallet-abi/src/error.rs +++ /dev/null @@ -1,75 +0,0 @@ -#[derive(Debug, thiserror::Error)] -pub enum WalletAbiError { - #[error("Invalid request: {0}")] - InvalidRequest(String), - - #[error("Invalid response: {0}")] - InvalidResponse(String), - - #[error("Invalid finalization steps: {0}")] - InvalidFinalizationSteps(String), - - #[error("Invalid signer configuration: {0}")] - InvalidSignerConfig(String), - - #[error("Funding failed: {0}")] - Funding(String), - - #[error("I/O error: {0}")] - Io(#[from] std::io::Error), - - #[error("JSON error: {0}")] - Serde(#[from] serde_json::Error), - - #[error("Hex decode error: {0}")] - Hex(#[from] hex::FromHexError), - - #[error("PSET error: {0}")] - Pset(#[from] simplicityhl::elements::pset::Error), - - #[error("PSET blinding error: {0}")] - PsetBlind(#[from] simplicityhl::elements::pset::PsetBlindError), - - #[error("Transaction decode error: {0}")] - TxDecode(#[from] simplicityhl::elements::encode::Error), - - #[error("Transaction amount proof verification failed: {0}")] - AmountProofVerification(#[from] simplicityhl::elements::VerificationError), - - #[error("Program error: {0}")] - Program(#[from] lwk_simplicity::error::ProgramError), - - #[error("esplora mutex poisoned: {0}")] - EsploraPoisoned(String), - - #[error("LWK Signer error: {0}")] - LWKSigner(#[from] lwk_signer::NewError), - - #[error("LWK sign error: {0}")] - LWKSign(#[from] lwk_signer::SignError), - - #[error("LWK wollet error: {0}")] - LWKWollet(#[from] lwk_wollet::Error), - - #[error("TXOut unblinding error: {0}")] - Unblind(#[from] lwk_wollet::elements::UnblindError), - - #[error("Locktime error: {0}")] - Locktime(#[from] lwk_wollet::elements::locktime::Error), -} - -/// Errors that occur during binary or hex encoding/decoding operations. -#[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), - - #[error("Unexpected trailing bytes after decode: consumed {consumed} of {total}")] - TrailingBytes { consumed: usize, total: usize }, - - #[error("Failed to decode hex string: {0}")] - HexDecode(#[from] hex::FromHexError), -} diff --git a/crates/wallet-abi/src/issuance_validation/mod.rs b/crates/wallet-abi/src/issuance_validation/mod.rs deleted file mode 100644 index 79ba136..0000000 --- a/crates/wallet-abi/src/issuance_validation/mod.rs +++ /dev/null @@ -1,647 +0,0 @@ -use lwk_wollet::elements::Script; -use simplicityhl::elements::confidential::Value as ConfidentialValue; -use simplicityhl::elements::secp256k1_zkp::{SECP256K1, SecretKey, ZERO_TWEAK}; -use simplicityhl::elements::{AssetId, Transaction}; -use std::collections::HashSet; - -/// Constraints for verifying an issuance transaction. -#[derive(Clone, Debug, Default)] -pub struct IssuanceTxConstraints { - /// Per-issuance-input constraints. - pub inputs: Vec, - - /// If `false`, every issuance input in the transaction must be listed in [`Self::inputs`]. - pub allow_unconstrained_issuances: bool, -} - -/// Per-input constraints for a new issuance. -#[derive(Clone, Debug)] -pub struct IssuanceInputConstraints { - /// Index into `tx.input`. - pub input_idx: usize, - - /// Destination, amount, and optional blinding key for the issued asset. - /// - /// The tuple is `(script, amount, blinding_key)`. When `blinding_key` is `Some`, - /// confidential outputs are unblinded using it; if unblinding succeeds and the asset - /// matches, the output is accounted for, otherwise it is skipped. - pub issuance_destination: Option<(Script, u64, Option)>, - - /// Destination, amount, and optional blinding key for the reissuance token. - /// - /// The tuple is `(script, amount, blinding_key)`. When `blinding_key` is `Some`, - /// confidential outputs are unblinded using it; if unblinding succeeds and the asset - /// matches, the output is accounted for, otherwise it is skipped. - pub reissuance_destination: Option<(Script, u64, Option)>, -} - -#[derive(thiserror::Error, Debug, PartialEq, Eq)] -pub enum IssuanceVerificationError { - #[error("No asset issuances found in the transaction.")] - NoIssuancesFound, - - #[error("Constraint input index {input_idx} is out of bounds (tx inputs: {inputs_len}).")] - ConstraintInputOutOfBounds { input_idx: usize, inputs_len: usize }, - - #[error("Constraint input index {input_idx} appears more than once.")] - DuplicateConstraintInput { input_idx: usize }, - - #[error("Constraint input index {input_idx} is not an issuance input.")] - ConstraintInputNotAnIssuance { input_idx: usize }, - - #[error("Issuance input at index {input_idx} is a reissuance (not a new issuance).")] - ReissuanceInputFound { input_idx: usize }, - - #[error("Issuance input at index {input_idx} is not listed in constraints.")] - UnexpectedIssuanceInput { input_idx: usize }, - - #[error("Issuance input at index {input_idx} has a confidential issued amount.")] - ConfidentialIssuanceAmount { input_idx: usize }, - - #[error("Issuance input at index {input_idx} has confidential inflation keys.")] - ConfidentialInflationKeys { input_idx: usize }, - - #[error( - "Minted issued amount mismatch for input #{input_idx} (Asset ID: {asset_id}): expected {expected}, found {found}." - )] - MintedIssuanceAmountMismatch { - input_idx: usize, - asset_id: AssetId, - expected: u64, - found: u64, - }, - - #[error( - "Minted inflation keys mismatch for input #{input_idx} (Reissuance Token ID: {asset_id}): expected {expected}, found {found}." - )] - MintedInflationKeysMismatch { - input_idx: usize, - asset_id: AssetId, - expected: u64, - found: u64, - }, - - #[error( - "Output #{vout} has non-explicit value for constrained asset {asset_id} (cannot verify exact amounts)." - )] - OutputValueNotExplicitForConstrainedAsset { vout: usize, asset_id: AssetId }, - - #[error("Constrained asset {asset_id} appears in an unexpected output #{vout}.")] - AssetAppearsInUnexpectedOutput { vout: usize, asset_id: AssetId }, - - #[error("Amount mismatch for asset {asset_id}: expected {expected}, found {found}.")] - AmountMismatch { - asset_id: AssetId, - expected: u64, - found: u64, - }, -} - -/// Verifies that `tx` is a well-formed *new issuance* transaction and satisfies `constraints`. -/// -/// ## What gets checked -/// -/// - **New issuance inputs only**: every input with a non-null `asset_issuance` must be a *new* -/// issuance (`asset_blinding_nonce == ZERO_TWEAK`). Reissuances are rejected. -/// - **Coverage**: if `constraints.allow_unconstrained_issuances` is `false`, then every issuance -/// input in the transaction must be listed in `constraints.inputs`. -/// - **Minted amounts**: minted `asset_issuance.amount` and `asset_issuance.inflation_keys` must -/// be `Null` or `Explicit`; confidential issuance fields are rejected. -/// - **Destinations**: for each constrained input, both the issued asset and the reissuance token -/// must only appear in outputs spending to the provided `Script`, and the *sum of explicit -/// output values* must equal the constrained amount. -/// -/// ## Confidentiality policy -/// -/// - Each destination tuple carries an optional blinding key (the third element). When set, -/// confidential outputs are unblinded using the key. If unblinding succeeds and the asset -/// matches, the output is accounted for. If unblinding fails, the output is silently skipped. -/// - When no blinding key is provided for a destination, outputs whose **asset is confidential** -/// are ignored during verification (this verifier makes no claims about what may be hidden in -/// confidential-asset outputs). -/// - For constrained assets, any output with an **explicit matching asset** but a **non-explicit -/// value** fails verification (cannot check exact amounts). -/// -/// ## `None` destination semantics -/// -/// If `issuance_destination` or `reissuance_destination` is `None`, the corresponding minted amount -/// must be `0` and the asset must not appear in any *explicit* output (even with value `0`). -/// -/// # Errors -/// -/// Returns an [`IssuanceVerificationError`] if the transaction does not satisfy the constraints. -#[allow(clippy::too_many_lines)] -pub fn verify_issuance( - tx: &Transaction, - constraints: &IssuanceTxConstraints, -) -> Result<(), IssuanceVerificationError> { - let issuance_input_indices: Vec = tx - .input - .iter() - .enumerate() - .filter_map(|(i, inp)| (!inp.asset_issuance.is_null()).then_some(i)) - .collect(); - - if issuance_input_indices.is_empty() { - return Err(IssuanceVerificationError::NoIssuancesFound); - } - - // All issuance inputs must be *new* issuances. - for &input_idx in &issuance_input_indices { - if tx.input[input_idx].asset_issuance.asset_blinding_nonce != ZERO_TWEAK { - return Err(IssuanceVerificationError::ReissuanceInputFound { input_idx }); - } - } - - // Validate constraint indices and build a set for coverage checks. - let mut constrained_inputs = HashSet::::new(); - for issuance_input_constraint in &constraints.inputs { - if issuance_input_constraint.input_idx >= tx.input.len() { - return Err(IssuanceVerificationError::ConstraintInputOutOfBounds { - input_idx: issuance_input_constraint.input_idx, - inputs_len: tx.input.len(), - }); - } - - if !constrained_inputs.insert(issuance_input_constraint.input_idx) { - return Err(IssuanceVerificationError::DuplicateConstraintInput { - input_idx: issuance_input_constraint.input_idx, - }); - } - - if tx.input[issuance_input_constraint.input_idx] - .asset_issuance - .is_null() - { - return Err(IssuanceVerificationError::ConstraintInputNotAnIssuance { - input_idx: issuance_input_constraint.input_idx, - }); - } - } - - if !constraints.allow_unconstrained_issuances { - for &input_idx in &issuance_input_indices { - if !constrained_inputs.contains(&input_idx) { - return Err(IssuanceVerificationError::UnexpectedIssuanceInput { input_idx }); - } - } - } - - for constraint in &constraints.inputs { - let inp = &tx.input[constraint.input_idx]; - let (issued_asset_id, reissuance_token_id) = inp.issuance_ids(); - - let minted_issuance_amount = - issuance_value_to_u64(&inp.asset_issuance.amount, constraint.input_idx)?; - verify_constrained_asset( - tx, - issued_asset_id, - minted_issuance_amount, - Option::from(&constraint.issuance_destination), - constraint.input_idx, - MintedConstraintKind::IssuanceAmount, - )?; - - let minted_inflation_keys = - inflation_keys_to_u64(&inp.asset_issuance.inflation_keys, constraint.input_idx)?; - verify_constrained_asset( - tx, - reissuance_token_id, - minted_inflation_keys, - Option::from(&constraint.reissuance_destination), - constraint.input_idx, - MintedConstraintKind::InflationKeys, - )?; - } - - Ok(()) -} - -#[derive(Clone, Copy, Debug)] -enum MintedConstraintKind { - IssuanceAmount, - InflationKeys, -} - -fn issuance_value_to_u64( - amount: &ConfidentialValue, - input_idx: usize, -) -> Result { - if amount.is_null() { - return Ok(0); - } - - amount - .explicit() - .ok_or(IssuanceVerificationError::ConfidentialIssuanceAmount { input_idx }) -} - -fn inflation_keys_to_u64( - amount: &ConfidentialValue, - input_idx: usize, -) -> Result { - if amount.is_null() { - return Ok(0); - } - - amount - .explicit() - .ok_or(IssuanceVerificationError::ConfidentialInflationKeys { input_idx }) -} - -fn verify_constrained_asset( - tx: &Transaction, - asset_id: AssetId, - minted_amount: u64, - destination: Option<&(Script, u64, Option)>, - input_idx: usize, - kind: MintedConstraintKind, -) -> Result<(), IssuanceVerificationError> { - let (dest_script, expected_amount, blinder) = match destination { - Some((s, amt, blinder)) => (Some(s), *amt, Option::from(blinder)), - None => (None, 0, None), - }; - - if minted_amount != expected_amount { - return Err(match kind { - MintedConstraintKind::IssuanceAmount => { - IssuanceVerificationError::MintedIssuanceAmountMismatch { - input_idx, - asset_id, - expected: expected_amount, - found: minted_amount, - } - } - MintedConstraintKind::InflationKeys => { - IssuanceVerificationError::MintedInflationKeysMismatch { - input_idx, - asset_id, - expected: expected_amount, - found: minted_amount, - } - } - }); - } - - verify_asset_destination(tx, asset_id, expected_amount, dest_script, blinder) -} - -fn verify_asset_destination( - tx: &Transaction, - asset_id: AssetId, - expected_amount: u64, - destination_script: Option<&Script>, - blinding_key: Option<&SecretKey>, -) -> Result<(), IssuanceVerificationError> { - let mut sum_to_destination = 0u64; - - for (vout, output) in tx.output.iter().enumerate() { - let resolved = match output.asset.explicit() { - Some(out_asset) if out_asset == asset_id => { - let Some(value) = output.value.explicit() else { - return Err( - IssuanceVerificationError::OutputValueNotExplicitForConstrainedAsset { - vout, - asset_id, - }, - ); - }; - Some(value) - } - _ => blinding_key - .and_then(|key| output.unblind(SECP256K1, *key).ok()) - .filter(|secrets| secrets.asset == asset_id) - .map(|secrets| secrets.value), - }; - - if let Some(value) = resolved { - let Some(dest_script) = destination_script else { - return Err(IssuanceVerificationError::AssetAppearsInUnexpectedOutput { - vout, - asset_id, - }); - }; - - if output.script_pubkey != *dest_script { - return Err(IssuanceVerificationError::AssetAppearsInUnexpectedOutput { - vout, - asset_id, - }); - } - - sum_to_destination = sum_to_destination.saturating_add(value); - } - } - - if sum_to_destination != expected_amount { - return Err(IssuanceVerificationError::AmountMismatch { - asset_id, - expected: expected_amount, - found: sum_to_destination, - }); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use lwk_wollet::elements::hex::FromHex; - - use simplicityhl::elements::confidential::{Asset as ConfidentialAsset, Nonce, Value}; - use simplicityhl::elements::hashes::Hash; - use simplicityhl::elements::pset::serialize::Deserialize; - use simplicityhl::elements::secp256k1_zkp::{Generator, Secp256k1, Tweak, ZERO_TWEAK}; - use simplicityhl::elements::{ - AssetIssuance, LockTime, OutPoint, Sequence, Transaction, TxIn, TxOut, TxOutWitness, Txid, - }; - - fn outpoint(vout: u32) -> OutPoint { - OutPoint { - txid: Txid::all_zeros(), - vout, - } - } - - fn new_issuance_input(vout: u32, contract_hash: [u8; 32], amount: u64, keys: u64) -> TxIn { - TxIn { - previous_output: outpoint(vout), - sequence: Sequence::MAX, - asset_issuance: AssetIssuance { - asset_blinding_nonce: ZERO_TWEAK, - asset_entropy: contract_hash, - amount: if amount == 0 { - Value::Null - } else { - Value::Explicit(amount) - }, - inflation_keys: if keys == 0 { - Value::Null - } else { - Value::Explicit(keys) - }, - }, - ..Default::default() - } - } - - fn reissuance_input(vout: u32, entropy: [u8; 32]) -> TxIn { - TxIn { - previous_output: outpoint(vout), - asset_issuance: AssetIssuance { - asset_blinding_nonce: Tweak::from_inner([1u8; 32]).expect("valid tweak"), - asset_entropy: entropy, - amount: Value::Explicit(1), - inflation_keys: Value::Null, - }, - ..Default::default() - } - } - - fn tx_out_explicit(script: Script, asset_id: AssetId, value: u64) -> TxOut { - TxOut { - asset: ConfidentialAsset::Explicit(asset_id), - value: Value::Explicit(value), - nonce: Nonce::Null, - script_pubkey: script, - witness: TxOutWitness::default(), - } - } - - fn tx_out_confidential_asset(script: Script) -> TxOut { - // Create a valid confidential asset generator. - let secp = Secp256k1::new(); - let generator = Generator::new_unblinded(&secp, AssetId::LIQUID_BTC.into_tag()); - TxOut { - asset: ConfidentialAsset::Confidential(generator), - value: Value::Explicit(1), - nonce: Nonce::Null, - script_pubkey: script, - witness: TxOutWitness::default(), - } - } - - fn tx_with(inputs: Vec, outputs: Vec) -> Transaction { - Transaction { - version: 2, - lock_time: LockTime::ZERO, - input: inputs, - output: outputs, - } - } - - #[test] - fn verify_issuance_happy_path_sums_to_destinations() { - let issue_script = Script::from(vec![0x51]); - let token_script = Script::from(vec![0x52]); - - let inp0 = new_issuance_input(0, [7u8; 32], 50, 1); - let (asset_id, token_id) = inp0.issuance_ids(); - - let tx = tx_with( - vec![inp0], - vec![ - tx_out_explicit(issue_script.clone(), asset_id, 25), - tx_out_explicit(issue_script.clone(), asset_id, 25), - tx_out_explicit(token_script.clone(), token_id, 1), - tx_out_confidential_asset(Script::from(vec![0x6a])), - ], - ); - - let constraints = IssuanceTxConstraints { - inputs: vec![IssuanceInputConstraints { - input_idx: 0, - issuance_destination: Some((issue_script, 50, None)), - reissuance_destination: Some((token_script, 1, None)), - }], - ..Default::default() - }; - - assert_eq!(verify_issuance(&tx, &constraints), Ok(())); - } - - #[test] - fn verify_issuance_fails_on_wrong_script() { - let issue_script = Script::from(vec![0x51]); - let wrong_script = Script::from(vec![0x52]); - let token_script = Script::from(vec![0x53]); - - let inp0 = new_issuance_input(0, [9u8; 32], 10, 1); - let (asset_id, token_id) = inp0.issuance_ids(); - - let tx = tx_with( - vec![inp0], - vec![ - tx_out_explicit(wrong_script, asset_id, 10), - tx_out_explicit(token_script.clone(), token_id, 1), - ], - ); - - let constraints = IssuanceTxConstraints { - inputs: vec![IssuanceInputConstraints { - input_idx: 0, - issuance_destination: Some((issue_script, 10, None)), - reissuance_destination: Some((token_script, 1, None)), - }], - ..Default::default() - }; - - assert!(matches!( - verify_issuance(&tx, &constraints), - Err(IssuanceVerificationError::AssetAppearsInUnexpectedOutput { .. }) - )); - } - - #[test] - fn verify_issuance_fails_on_amount_mismatch() { - let issue_script = Script::from(vec![0x51]); - let token_script = Script::from(vec![0x52]); - - let inp0 = new_issuance_input(0, [11u8; 32], 10, 1); - let (asset_id, token_id) = inp0.issuance_ids(); - - let tx = tx_with( - vec![inp0], - vec![ - tx_out_explicit(issue_script.clone(), asset_id, 9), - tx_out_explicit(token_script.clone(), token_id, 1), - ], - ); - - let constraints = IssuanceTxConstraints { - inputs: vec![IssuanceInputConstraints { - input_idx: 0, - issuance_destination: Some((issue_script, 10, None)), - reissuance_destination: Some((token_script, 1, None)), - }], - ..Default::default() - }; - - assert!(matches!( - verify_issuance(&tx, &constraints), - Err(IssuanceVerificationError::AmountMismatch { .. }) - )); - } - - #[test] - fn verify_issuance_none_destination_requires_zero_and_no_appearances() { - let token_script = Script::from(vec![0x52]); - - let inp0 = new_issuance_input(0, [13u8; 32], 0, 1); - let (asset_id, token_id) = inp0.issuance_ids(); - - let tx = tx_with( - vec![inp0], - vec![ - // Value 0 still counts as an appearance. - tx_out_explicit(Script::from(vec![0x51]), asset_id, 0), - tx_out_explicit(token_script.clone(), token_id, 1), - ], - ); - - let constraints = IssuanceTxConstraints { - inputs: vec![IssuanceInputConstraints { - input_idx: 0, - issuance_destination: None, - reissuance_destination: Some((token_script, 1, None)), - }], - ..Default::default() - }; - - assert!(matches!( - verify_issuance(&tx, &constraints), - Err(IssuanceVerificationError::AssetAppearsInUnexpectedOutput { .. }) - )); - } - - #[test] - fn verify_issuance_fails_if_any_issuance_input_is_reissuance() { - let tx = tx_with(vec![reissuance_input(0, [0u8; 32])], vec![]); - let constraints = IssuanceTxConstraints { - inputs: vec![IssuanceInputConstraints { - input_idx: 0, - issuance_destination: None, - reissuance_destination: None, - }], - ..Default::default() - }; - - assert_eq!( - verify_issuance(&tx, &constraints), - Err(IssuanceVerificationError::ReissuanceInputFound { input_idx: 0 }) - ); - } - - #[test] - fn verify_issuance_coverage_policy_allows_extra_issuance_when_enabled() { - let issue_script = Script::from(vec![0x51]); - let token_script = Script::from(vec![0x52]); - - let inp0 = new_issuance_input(0, [21u8; 32], 10, 1); - let (asset_id, token_id) = inp0.issuance_ids(); - - let inp1 = new_issuance_input(1, [22u8; 32], 1, 1); - - let tx = tx_with( - vec![inp0, inp1], - vec![ - tx_out_explicit(issue_script.clone(), asset_id, 10), - tx_out_explicit(token_script.clone(), token_id, 1), - ], - ); - - let constraints_strict = IssuanceTxConstraints { - inputs: vec![IssuanceInputConstraints { - input_idx: 0, - issuance_destination: Some((issue_script, 10, None)), - reissuance_destination: Some((token_script, 1, None)), - }], - ..Default::default() - }; - - assert_eq!( - verify_issuance(&tx, &constraints_strict), - Err(IssuanceVerificationError::UnexpectedIssuanceInput { input_idx: 1 }) - ); - - let constraints_allow = IssuanceTxConstraints { - allow_unconstrained_issuances: true, - ..constraints_strict - }; - - assert_eq!(verify_issuance(&tx, &constraints_allow), Ok(())); - } - - #[test] - fn test_verify_issuance_valid() -> Result<(), String> { - let script_pubkey = Script::from_hex( - "51203451c2c04047ab5f2d3eb747773e5a9756c10eb9bf31326bd16a6533142f1ace", - ) - .unwrap(); - - let tx_hex = include_str!("../../tests/data/tx_with_issuance_token.hex"); - let tx_bytes = hex::decode(tx_hex.trim()).unwrap(); - let tx: Transaction = Deserialize::deserialize(&tx_bytes[..]).unwrap(); - - let constraints = IssuanceTxConstraints { - inputs: vec![ - IssuanceInputConstraints { - input_idx: 0, - issuance_destination: None, - reissuance_destination: Some((script_pubkey.clone(), 1, None)), - }, - IssuanceInputConstraints { - input_idx: 1, - issuance_destination: None, - reissuance_destination: Some((script_pubkey, 1, None)), - }, - ], - ..Default::default() - }; - - verify_issuance(&tx, &constraints).map_err(|e| format!("Verification failed: {e:?}"))?; - - Ok(()) - } -} diff --git a/crates/wallet-abi/src/lib.rs b/crates/wallet-abi/src/lib.rs deleted file mode 100644 index ab5a101..0000000 --- a/crates/wallet-abi/src/lib.rs +++ /dev/null @@ -1,27 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] -#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] - -pub mod encoding; -pub mod error; -pub mod issuance_validation; -pub mod runtime; -pub mod schema; -pub mod scripts; -pub mod simplicity; -pub mod taproot_pubkey_gen; -pub mod tx_inclusion; - -pub use encoding::Encodable; -pub use error::WalletAbiError; -pub use lwk_common::Network; -pub use lwk_simplicity::error::ProgramError; -pub use lwk_simplicity::runner::run_program; -pub use lwk_simplicity::signer::{finalize_transaction, get_and_verify_env, get_sighash_all}; -pub use schema::runtime_params::*; -pub use scripts::{ - control_block, create_p2tr_address, get_new_asset_entropy, hash_script, load_program, - simplicity_leaf_version, tap_data_hash, -}; -pub use simplicity::p2pk::{P2PK_SOURCE, execute_p2pk_program, get_p2pk_address, get_p2pk_program}; - -pub use tx_inclusion::*; diff --git a/crates/wallet-abi/src/runtime/input_resolution.rs b/crates/wallet-abi/src/runtime/input_resolution.rs deleted file mode 100644 index 17d0363..0000000 --- a/crates/wallet-abi/src/runtime/input_resolution.rs +++ /dev/null @@ -1,1294 +0,0 @@ -//! Input resolution for transaction construction. -//! -//! This module balances the input/output equation in four phases: -//! 1. Build demand from all requested outputs and inject implicit fee demand on policy asset. -//! 2. Resolve declared inputs in order. -//! 3. Materialize deferred issuance-linked output demand when referenced inputs are known. -//! 4. Add auxiliary wallet inputs until every positive asset deficit is closed. -//! -//! # Algorithm -//! -//! Auxiliary funding for each asset deficit uses a deterministic stack: -//! 1. Bounded Branch-and-Bound (`BnB`) for exact subset match. -//! 2. Deterministic single-input fallback (largest UTXO above target). -//! 3. Deterministic largest-first accumulation fallback. -//! -//! This mirrors formal coin-selection framing (subset-sum / knapsack) while keeping runtime -//! bounded by an explicit node cap. -//! -//! # Determinism -//! -//! Candidate order and tie-breaks are stable: -//! - primary sort: amount descending -//! - tie-break 1: `txid` lexicographic ascending -//! - tie-break 2: `vout` ascending -//! -//! For multiple exact `BnB` matches with equal input count, the lexicographically smaller -//! outpoint list is selected. -//! -//! # Complexity -//! -//! Let: -//! - `O` = number of outputs -//! - `I` = number of declared inputs -//! - `U` = wallet UTXO count in snapshot -//! - `A` = number of distinct demanded assets -//! - `K` = number of auxiliary inputs added -//! - `N` = max candidate UTXOs for one deficit asset -//! -//! Worst-case time is: -//! - declared-input selection: `O(I * U * A)` -//! - auxiliary selection per deficit asset: bounded Branch-and-Bound search -//! plus deterministic fallbacks, `O(MAX_BNB_NODES + N)` -//! - overall: `O(I * U * A + K * (MAX_BNB_NODES + N) + O + I)` -//! -//! Space is `O(U + A + O + N)` for used-outpoint tracking, equation state and -//! per-asset candidate working sets. -//! -//! # Failure modes -//! -//! - Duplicate `"fee"` output ids fail fast. -//! - Fee output with non-policy asset fails fast. -//! - Arithmetic overflow fails with `InvalidRequest`. -//! - Unclosable deficits fail with `Funding`. - -use crate::runtime::{WalletRuntimeConfig, get_finalizer_spec_key, get_secrets_spec_key}; -use crate::{ - AmountFilter, AssetFilter, AssetVariant, FinalizerSpec, InputBlinder, InputIssuance, - InputIssuanceKind, InputSchema, LockFilter, RuntimeParams, UTXOSource, WalletAbiError, - WalletSourceFilter, -}; - -use lwk_common::Bip::Bip84; -use lwk_common::Signer; -use lwk_wollet::bitcoin::bip32::{ChildNumber, DerivationPath}; -use lwk_wollet::elements::confidential::{Asset, AssetBlindingFactor, Value, ValueBlindingFactor}; -use lwk_wollet::elements::hashes::Hash; -use lwk_wollet::elements::pset::{Input, PartiallySignedTransaction}; -use lwk_wollet::elements::{AssetId, ContractHash, OutPoint, TxOut, TxOutSecrets, secp256k1_zkp}; -use lwk_wollet::{Chain, EC, WalletTxOut}; -use std::collections::{BTreeMap, HashMap, HashSet}; - -type CandidateScore = (u64, u64, u64, String, u32); - -type Midstate = lwk_wollet::elements::hashes::sha256::Midstate; -/// Upper bound on DFS nodes visited by `BnB` before deterministic fallback is used. -const MAX_BNB_NODES: usize = 100_000; - -/// Auxiliary `BnB` candidate projection used for deterministic subset search. -#[derive(Clone, Debug, Eq, PartialEq)] -struct BnbCandidate { - amount_sat: u64, - txid_lex: String, - vout: u32, -} - -/// Selected auxiliary strategy used for one deficit asset. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum BnbSelectionStatus { - Exact, - FallbackSingleLargestAboveTarget, - FallbackLargestFirstAccumulation, -} - -#[derive(Clone, Copy)] -struct WalletDerivationIndex { - ext_int: Chain, - wildcard_index: u32, -} - -#[derive(Clone, Copy)] -enum DeferredDemandKind { - NewIssuanceAsset, - NewIssuanceToken, - ReIssuanceAsset, -} - -#[derive(Default)] -struct ResolutionState { - used_outpoints: HashSet, - demand_by_asset: BTreeMap, - supply_by_asset: BTreeMap, - deferred_demands: HashMap>, -} - -struct ResolvedInputMaterial { - outpoint: OutPoint, - tx_out: TxOut, - secrets: TxOutSecrets, - wallet_derivation: Option, -} - -/// Add `amount_sat` to one asset bucket with overflow protection. -pub(super) fn add_balance( - map: &mut BTreeMap, - asset_id: AssetId, - amount_sat: u64, -) -> Result<(), WalletAbiError> { - let entry = map.entry(asset_id).or_insert(0); - *entry = entry.checked_add(amount_sat).ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "asset amount overflow while aggregating balances for {asset_id}" - )) - })?; - Ok(()) -} - -/// Compute positive deficits `(demand - supply)` per asset. -/// -/// ```rust,ignore -/// use std::collections::BTreeMap; -/// use lwk_wollet::elements::AssetId; -/// -/// let lbtc = AssetId::from_slice(&[1u8; 32]).unwrap(); -/// let usdt = AssetId::from_slice(&[2u8; 32]).unwrap(); -/// let demand = BTreeMap::from([(lbtc, 9_u64), (usdt, 5)]); -/// let supply = BTreeMap::from([(lbtc, 4_u64), (usdt, 5)]); -/// let d = current_deficits(&demand, &supply); -/// assert_eq!(d, BTreeMap::from([(lbtc, 5)])); -/// ``` -fn current_deficits( - demand_by_asset: &BTreeMap, - supply_by_asset: &BTreeMap, -) -> BTreeMap { - // Deficits are kept only for assets where demand is still strictly above supply. - let mut deficits = BTreeMap::new(); - for (asset_id, demand_sat) in demand_by_asset { - let supplied = supply_by_asset.get(asset_id).copied().unwrap_or(0); - if *demand_sat > supplied { - deficits.insert(*asset_id, demand_sat - supplied); - } - } - deficits -} - -/// Reserve an outpoint and fail if it was already used. -fn reserve_outpoint( - used_outpoints: &mut HashSet, - input_id: &str, - outpoint: OutPoint, -) -> Result<(), WalletAbiError> { - if used_outpoints.insert(outpoint) { - return Ok(()); - } - - Err(WalletAbiError::InvalidRequest(format!( - "duplicate input outpoint resolved for '{}': {}:{}", - input_id, outpoint.txid, outpoint.vout - ))) -} - -/// Validate that an output reference points to an existing declared input index. -fn validate_output_input_index( - output_id: &str, - input_index: u32, - input_count: usize, -) -> Result<(), WalletAbiError> { - let idx = usize::try_from(input_index).map_err(|_| { - WalletAbiError::InvalidRequest(format!( - "output '{output_id}' input_index overflow: {input_index}" - )) - })?; - - if idx >= input_count { - return Err(WalletAbiError::InvalidRequest(format!( - "output '{output_id}' references missing input_index {input_index}" - ))); - } - - Ok(()) -} - -/// Compute issuance entropy from input outpoint and issuance kind. -pub(super) fn derive_issuance_entropy(outpoint: OutPoint, issuance: &InputIssuance) -> Midstate { - match issuance.kind { - InputIssuanceKind::New => AssetId::generate_asset_entropy( - outpoint, - ContractHash::from_byte_array(issuance.entropy), - ), - InputIssuanceKind::Reissue => Midstate::from_byte_array(issuance.entropy), - } -} - -/// Resolve issuance token id for the current runtime issuance model. -/// -/// This mirrors `elements::pset::Input::issuance_ids()` token derivation semantics where -/// the token confidentiality flag tracks `issuance_value_comm.is_some()`. -/// -/// Runtime currently sets unblinded issuance amounts (`issuance_value_amount`) and does not -/// populate `issuance_value_comm`, so the confidentiality flag is intentionally fixed to `false`. -pub(super) fn issuance_token_from_entropy_for_unblinded_issuance( - issuance_entropy: Midstate, -) -> AssetId { - let issuance_value_commitment_present = false; - AssetId::reissuance_token_from_entropy(issuance_entropy, issuance_value_commitment_present) -} - -/// Resolve a deferred issuance-linked output demand into a concrete asset id. -fn demand_asset_from_deferred( - kind: DeferredDemandKind, - issuance: &InputIssuance, - material: &ResolvedInputMaterial, - input_id: &str, -) -> Result { - match (kind, &issuance.kind) { - (DeferredDemandKind::NewIssuanceAsset, InputIssuanceKind::New) - | (DeferredDemandKind::ReIssuanceAsset, InputIssuanceKind::Reissue) => Ok( - AssetId::from_entropy(derive_issuance_entropy(material.outpoint, issuance)), - ), - (DeferredDemandKind::NewIssuanceToken, InputIssuanceKind::New) => { - Ok(issuance_token_from_entropy_for_unblinded_issuance( - derive_issuance_entropy(material.outpoint, issuance), - )) - } - (DeferredDemandKind::NewIssuanceAsset, InputIssuanceKind::Reissue) => { - Err(WalletAbiError::InvalidRequest(format!( - "output asset variant new_issuance_asset references reissue input '{input_id}'" - ))) - } - (DeferredDemandKind::NewIssuanceToken, InputIssuanceKind::Reissue) => { - Err(WalletAbiError::InvalidRequest(format!( - "output asset variant new_issuance_token references reissue input '{input_id}'" - ))) - } - (DeferredDemandKind::ReIssuanceAsset, InputIssuanceKind::New) => { - Err(WalletAbiError::InvalidRequest(format!( - "output asset variant re_issuance_asset references new issuance input '{input_id}'" - ))) - } - } -} - -/// Populate issuance-related PSET input fields from request metadata. -fn apply_issuance_to_pset_input( - pset_input: &mut Input, - issuance: &InputIssuance, - secrets: &TxOutSecrets, -) -> Result<(), WalletAbiError> { - pset_input.issuance_value_amount = if issuance.asset_amount_sat == 0 { - None - } else { - Some(issuance.asset_amount_sat) - }; - pset_input.issuance_asset_entropy = Some(issuance.entropy); - pset_input.issuance_inflation_keys = if issuance.token_amount_sat == 0 { - None - } else { - Some(issuance.token_amount_sat) - }; - - if issuance.kind == InputIssuanceKind::Reissue { - // Runtime currently emits unblinded issuance amounts; for reissuance we still need a - // non-zero nonce and derive it from the input asset blinding factor. - let mut nonce = secrets.asset_bf.into_inner(); - if nonce == secp256k1_zkp::ZERO_TWEAK { - let mut one = [0u8; 32]; - one[0] = 1; - nonce = secp256k1_zkp::Tweak::from_slice(&one).map_err(|error| { - WalletAbiError::InvalidRequest(format!( - "failed to construct non-zero reissuance blinding nonce: {error}" - )) - })?; - } - pset_input.issuance_blinding_nonce = Some(nonce); - } - - pset_input.blinded_issuance = Some(0x00); - - Ok(()) -} - -/// Check whether a wallet UTXO candidate satisfies source filters and is unused. -fn matches_wallet_filter( - candidate: &WalletTxOut, - filter: &WalletSourceFilter, - used_outpoints: &HashSet, -) -> bool { - if used_outpoints.contains(&candidate.outpoint) { - return false; - } - - let asset_ok = match filter.asset { - AssetFilter::None => true, - AssetFilter::Exact { asset_id } => candidate.unblinded.asset == asset_id, - }; - if !asset_ok { - return false; - } - - let amount_ok = match filter.amount { - AmountFilter::None => true, - AmountFilter::Exact { satoshi } => candidate.unblinded.value == satoshi, - AmountFilter::Min { satoshi } => candidate.unblinded.value >= satoshi, - }; - if !amount_ok { - return false; - } - - match &filter.lock { - LockFilter::None => true, - LockFilter::Script { script } => candidate.script_pubkey == *script, - } -} - -/// Score one candidate by simulating its supply contribution. -/// -/// Lower score tuple is better. -fn score_candidate( - candidate: &WalletTxOut, - demand_by_asset: &BTreeMap, - supply_by_asset: &BTreeMap, -) -> Result { - // Simulate adding this candidate to the current supply map, then compute a - // deterministic lexicographic score that favors candidates which reduce deficits fastest. - let mut simulated_supply = supply_by_asset.clone(); - let current_supply = simulated_supply - .get(&candidate.unblinded.asset) - .copied() - .unwrap_or(0); - let updated_supply = current_supply - .checked_add(candidate.unblinded.value) - .ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "asset amount overflow while scoring candidate {}:{}", - candidate.outpoint.txid, candidate.outpoint.vout - )) - })?; - simulated_supply.insert(candidate.unblinded.asset, updated_supply); - - let mut total_remaining_deficit = 0u64; - for (asset_id, demand_sat) in demand_by_asset { - let supplied = simulated_supply.get(asset_id).copied().unwrap_or(0); - let remaining = demand_sat.saturating_sub(supplied); - total_remaining_deficit = - total_remaining_deficit - .checked_add(remaining) - .ok_or_else(|| { - WalletAbiError::InvalidRequest( - "deficit overflow while scoring wallet candidates".to_string(), - ) - })?; - } - - let candidate_demand = demand_by_asset - .get(&candidate.unblinded.asset) - .copied() - .unwrap_or(0); - let candidate_before_supply = supply_by_asset - .get(&candidate.unblinded.asset) - .copied() - .unwrap_or(0); - let candidate_after_supply = simulated_supply - .get(&candidate.unblinded.asset) - .copied() - .unwrap_or(0); - - let remaining_candidate_deficit = candidate_demand.saturating_sub(candidate_after_supply); - let needed_before = candidate_demand.saturating_sub(candidate_before_supply); - let overshoot_or_undershoot = candidate.unblinded.value.abs_diff(needed_before); - - Ok(( - total_remaining_deficit, - remaining_candidate_deficit, - overshoot_or_undershoot, - candidate.outpoint.txid.to_string(), - candidate.outpoint.vout, - )) -} - -/// Sort candidates using the canonical deterministic order for `BnB` and fallbacks. -/// -/// Order: -/// 1. amount descending -/// 2. txid lexicographic ascending -/// 3. vout ascending -fn sort_bnb_candidates(candidates: &mut [BnbCandidate]) { - candidates.sort_by(|a, b| { - b.amount_sat - .cmp(&a.amount_sat) - .then_with(|| a.txid_lex.cmp(&b.txid_lex)) - .then_with(|| a.vout.cmp(&b.vout)) - }); -} - -/// Build a comparable outpoint-key for one candidate subset. -/// -/// The key is sorted so subset order itself does not affect comparisons. -fn subset_lexicographic_key(indices: &[usize], candidates: &[BnbCandidate]) -> Vec<(String, u32)> { - let mut key = indices - .iter() - .map(|index| { - let candidate = &candidates[*index]; - (candidate.txid_lex.clone(), candidate.vout) - }) - .collect::>(); - key.sort(); - key -} - -/// Compare two exact-match subsets by deterministic tie-break rules. -/// -/// Preference: -/// 1. fewer selected inputs -/// 2. lexicographically smaller outpoint key -fn is_better_exact_subset( - proposed: &[usize], - current_best: Option<&[usize]>, - candidates: &[BnbCandidate], -) -> bool { - let Some(current_best) = current_best else { - return true; - }; - - if proposed.len() < current_best.len() { - return true; - } - if proposed.len() > current_best.len() { - return false; - } - - subset_lexicographic_key(proposed, candidates) - < subset_lexicographic_key(current_best, candidates) -} - -fn build_bnb_suffix_sums(candidates: &[BnbCandidate]) -> Result, WalletAbiError> { - let mut suffix_sum_sat = vec![0u64; candidates.len() + 1]; - for index in (0..candidates.len()).rev() { - suffix_sum_sat[index] = suffix_sum_sat[index + 1] - .checked_add(candidates[index].amount_sat) - .ok_or_else(|| { - WalletAbiError::InvalidRequest( - "asset amount overflow while computing BnB suffix sums".to_string(), - ) - })?; - } - - Ok(suffix_sum_sat) -} - -struct BnbSearch<'a> { - target_sat: u64, - candidates: &'a [BnbCandidate], - suffix_sum_sat: &'a [u64], - max_nodes: usize, - nodes_visited: usize, - node_limit_hit: bool, - current: Vec, - best: Option>, -} - -impl<'a> BnbSearch<'a> { - const fn new( - target_sat: u64, - candidates: &'a [BnbCandidate], - suffix_sum_sat: &'a [u64], - max_nodes: usize, - ) -> Self { - Self { - target_sat, - candidates, - suffix_sum_sat, - max_nodes, - nodes_visited: 0, - node_limit_hit: false, - current: Vec::new(), - best: None, - } - } - - const fn mark_node_visit(&mut self) -> bool { - self.nodes_visited = self.nodes_visited.saturating_add(1); - self.nodes_visited > self.max_nodes - } - - fn record_exact_if_better(&mut self) { - if is_better_exact_subset(&self.current, self.best.as_deref(), self.candidates) { - self.best = Some(self.current.clone()); - } - } - - fn can_reach_target(&self, index: usize, sum_sat: u64) -> Result { - let max_possible = sum_sat - .checked_add(self.suffix_sum_sat[index]) - .ok_or_else(|| { - WalletAbiError::InvalidRequest( - "asset amount overflow while evaluating BnB pruning bounds".to_string(), - ) - })?; - - Ok(max_possible >= self.target_sat) - } - - fn search(&mut self, index: usize, sum_sat: u64) -> Result<(), WalletAbiError> { - if self.node_limit_hit { - return Ok(()); - } - if self.mark_node_visit() { - self.node_limit_hit = true; - return Ok(()); - } - - if sum_sat == self.target_sat { - self.record_exact_if_better(); - return Ok(()); - } - if index >= self.candidates.len() || sum_sat > self.target_sat { - return Ok(()); - } - if !self.can_reach_target(index, sum_sat)? { - return Ok(()); - } - - let included_sum = sum_sat - .checked_add(self.candidates[index].amount_sat) - .ok_or_else(|| { - WalletAbiError::InvalidRequest( - "asset amount overflow while evaluating BnB include branch".to_string(), - ) - })?; - if included_sum <= self.target_sat { - self.current.push(index); - self.search(index + 1, included_sum)?; - self.current.pop(); - } - - self.search(index + 1, sum_sat) - } -} - -/// Bounded depth-first Branch-and-Bound search for an exact subset sum. -/// -/// Returns: -/// - `Some(indices)` on exact match -/// - `None` if no exact match or node bound is reached -/// -/// Pruning: -/// - stop include branch when `sum > target` -/// - stop branch when `sum + remaining < target` -fn bnb_exact_subset_indices( - candidates: &[BnbCandidate], - target_sat: u64, - max_nodes: usize, -) -> Result>, WalletAbiError> { - if target_sat == 0 { - return Ok(Some(Vec::new())); - } - if candidates.is_empty() { - return Ok(None); - } - - let suffix_sum_sat = build_bnb_suffix_sums(candidates)?; - let mut search = BnbSearch::new(target_sat, candidates, &suffix_sum_sat, max_nodes); - search.search(0, 0)?; - - if search.node_limit_hit { - return Ok(None); - } - - Ok(search.best) -} - -/// Deterministic fallback A: select one largest UTXO whose amount is `>= target`. -/// -/// Candidates are expected to be pre-sorted with `sort_bnb_candidates`. -fn select_single_largest_above_target( - candidates: &[BnbCandidate], - target_sat: u64, -) -> Option> { - candidates - .iter() - .position(|candidate| candidate.amount_sat >= target_sat) - .map(|index| vec![index]) -} - -/// Deterministic fallback B: accumulate largest-first until the target is reached. -/// -/// Candidates are expected to be pre-sorted with `sort_bnb_candidates`. -fn select_largest_first_accumulation( - candidates: &[BnbCandidate], - target_sat: u64, -) -> Result>, WalletAbiError> { - let mut selected_indices = Vec::new(); - let mut sum_sat = 0u64; - - for (index, candidate) in candidates.iter().enumerate() { - selected_indices.push(index); - sum_sat = sum_sat.checked_add(candidate.amount_sat).ok_or_else(|| { - WalletAbiError::InvalidRequest( - "asset amount overflow while running fallback accumulation".to_string(), - ) - })?; - if sum_sat >= target_sat { - return Ok(Some(selected_indices)); - } - } - - Ok(None) -} - -/// Sum selected candidate amounts with overflow checks. -fn sum_selected_amount( - candidates: &[BnbCandidate], - selected_indices: &[usize], -) -> Result { - selected_indices.iter().try_fold(0u64, |sum, index| { - sum.checked_add(candidates[*index].amount_sat) - .ok_or_else(|| { - WalletAbiError::InvalidRequest( - "asset amount overflow while summing selected auxiliary inputs".to_string(), - ) - }) - }) -} - -impl WalletRuntimeConfig { - /// Build demand from output specs and store issuance-linked entries as deferred. - /// - /// Rules: - /// - Non-fee outputs contribute demand directly (or deferred for issuance-derived assets). - /// - Exactly one implicit policy-asset demand entry is added for `fee_target_sat`. - /// - `"fee"` output, if present, is validated only for uniqueness and policy-asset type. - /// - Caller-provided fee output amount is ignored for demand accounting. - fn resolve_output_demands( - params: &RuntimeParams, - fee_target_sat: u64, - policy_asset: AssetId, - state: &mut ResolutionState, - ) -> Result<(), WalletAbiError> { - // Convert output-level asset requirements into equation demand. - // Issuance-derived outputs are deferred until their referenced input is resolved. - let mut fee_output_seen = false; - for output in ¶ms.outputs { - if output.id == "fee" { - if fee_output_seen { - return Err(WalletAbiError::InvalidRequest( - "duplicate output id 'fee' in params.outputs".to_string(), - )); - } - fee_output_seen = true; - - match output.asset { - AssetVariant::AssetId { asset_id } if asset_id == policy_asset => {} - AssetVariant::AssetId { asset_id } => { - return Err(WalletAbiError::InvalidRequest(format!( - "fee output must use policy asset {policy_asset}, found {asset_id}" - ))); - } - _ => { - return Err(WalletAbiError::InvalidRequest( - "fee output must use explicit asset_id policy asset variant" - .to_string(), - )); - } - } - - continue; - } - - match &output.asset { - AssetVariant::AssetId { asset_id } => { - add_balance(&mut state.demand_by_asset, *asset_id, output.amount_sat)?; - } - AssetVariant::NewIssuanceAsset { input_index } => { - validate_output_input_index(&output.id, *input_index, params.inputs.len())?; - state - .deferred_demands - .entry(*input_index) - .or_default() - .push((DeferredDemandKind::NewIssuanceAsset, output.amount_sat)); - } - AssetVariant::NewIssuanceToken { input_index } => { - validate_output_input_index(&output.id, *input_index, params.inputs.len())?; - state - .deferred_demands - .entry(*input_index) - .or_default() - .push((DeferredDemandKind::NewIssuanceToken, output.amount_sat)); - } - AssetVariant::ReIssuanceAsset { input_index } => { - validate_output_input_index(&output.id, *input_index, params.inputs.len())?; - state - .deferred_demands - .entry(*input_index) - .or_default() - .push((DeferredDemandKind::ReIssuanceAsset, output.amount_sat)); - } - } - } - - // Fee demand is always modeled from runtime target, independent of params fee amount. - add_balance(&mut state.demand_by_asset, policy_asset, fee_target_sat)?; - - Ok(()) - } - - /// Resolve input material from a provided outpoint and optional blinder hints. - async fn resolve_provided_input_material( - &self, - input: &InputSchema, - outpoint: OutPoint, - state: &mut ResolutionState, - ) -> Result { - reserve_outpoint(&mut state.used_outpoints, &input.id, outpoint)?; - - let tx_out = self.fetch_tx_out(&outpoint).await?; - - let secrets = match &input.blinder { - InputBlinder::Wallet => { - let (_, unblinded) = self.unblind_with_wallet(tx_out.clone())?; - - unblinded - } - InputBlinder::Provided { secret_key } => { - tx_out.unblind(&EC, *secret_key).map_err(|error| { - WalletAbiError::InvalidRequest(format!( - "unable to unblind input '{}' with provided blinder: {error}", - input.id - )) - })? - } - InputBlinder::Explicit => { - let (Asset::Explicit(asset), Value::Explicit(value)) = (tx_out.asset, tx_out.value) - else { - return Err(WalletAbiError::InvalidRequest(format!( - "marked input '{}' as explicit when the confidential was provided", - input.id - ))); - }; - - TxOutSecrets { - asset, - asset_bf: AssetBlindingFactor::zero(), - value, - value_bf: ValueBlindingFactor::zero(), - } - } - }; - - Ok(ResolvedInputMaterial { - outpoint, - tx_out, - secrets, - wallet_derivation: None, - }) - } - - /// Resolve input material from wallet snapshot using deficit-aware selection. - async fn resolve_wallet_input_material( - &self, - input: &InputSchema, - filter: &WalletSourceFilter, - wallet_snapshot: &[WalletTxOut], - state: &mut ResolutionState, - ) -> Result { - let selected = Self::filter_tx_out( - wallet_snapshot, - filter, - &state.used_outpoints, - &state.demand_by_asset, - &state.supply_by_asset, - )? - .ok_or_else(|| { - WalletAbiError::Funding(format!( - "no wallet UTXO matched contract input '{}' filter", - input.id - )) - })?; - - reserve_outpoint(&mut state.used_outpoints, &input.id, selected.outpoint)?; - - let tx_out = self.fetch_tx_out(&selected.outpoint).await?; - - Ok(ResolvedInputMaterial { - outpoint: selected.outpoint, - tx_out, - secrets: selected.unblinded, - wallet_derivation: Some(WalletDerivationIndex { - ext_int: selected.ext_int, - wildcard_index: selected.wildcard_index, - }), - }) - } - - /// Resolve one declared input from either provided or wallet source. - async fn resolve_declared_input_material( - &self, - input: &InputSchema, - wallet_snapshot: &[WalletTxOut], - state: &mut ResolutionState, - ) -> Result { - match &input.utxo_source { - UTXOSource::Wallet { filter } => { - self.resolve_wallet_input_material(input, filter, wallet_snapshot, state) - .await - } - UTXOSource::Provided { outpoint } => { - self.resolve_provided_input_material(input, *outpoint, state) - .await - } - } - } - - fn signer_origin_for_wallet_utxo( - &self, - index: WalletDerivationIndex, - ) -> Result<(lwk_wollet::elements::bitcoin::PublicKey, DerivationPath), WalletAbiError> { - let ext_int = match index.ext_int { - Chain::External => ChildNumber::from_normal_idx(0), - Chain::Internal => ChildNumber::from_normal_idx(1), - } - .map_err(|error| { - WalletAbiError::InvalidSignerConfig(format!( - "invalid change index for descriptor derivation: {error}" - )) - })?; - let wildcard = ChildNumber::from_normal_idx(index.wildcard_index).map_err(|error| { - WalletAbiError::InvalidRequest(format!( - "invalid wallet wildcard index {}: {error}", - index.wildcard_index - )) - })?; - - let derivation_path = self - .get_derivation_path(Bip84) - .child(ext_int) - .child(wildcard); - let pubkey = self.signer.derive_xpub(&derivation_path)?.public_key.into(); - - Ok((pubkey, derivation_path)) - } - - /// Append a resolved input to the PSET and attach sequence, prevout and witness UTXO. - fn add_resolved_input_to_pset( - &self, - pst: &mut PartiallySignedTransaction, - input: &InputSchema, - material: &ResolvedInputMaterial, - ) -> Result<(), WalletAbiError> { - let mut pset_input = Input::from_prevout(material.outpoint); - pset_input.sequence = Some(input.sequence); - pset_input.witness_utxo = Some(material.tx_out.clone()); - pset_input.amount = Some(material.secrets.value); - pset_input.asset = Some(material.secrets.asset); - - if let Some(issuance) = input.issuance.as_ref() { - apply_issuance_to_pset_input(&mut pset_input, issuance, &material.secrets)?; - } - - pset_input - .proprietary - .insert(get_finalizer_spec_key(), input.finalizer.try_encode()?); - pset_input.proprietary.insert( - get_secrets_spec_key(), - serde_json::to_vec(&material.secrets)?, - ); - if let Some(index) = material.wallet_derivation { - let (pubkey, derivation_path) = self.signer_origin_for_wallet_utxo(index)?; - pset_input - .bip32_derivation - .insert(pubkey, (self.signer.fingerprint(), derivation_path)); - } - pst.add_input(pset_input); - - Ok(()) - } - - /// Apply the resolved input contribution to equation supply (base + issuance minting). - fn apply_input_supply( - input: &InputSchema, - material: &ResolvedInputMaterial, - state: &mut ResolutionState, - ) -> Result<(), WalletAbiError> { - add_balance( - &mut state.supply_by_asset, - material.secrets.asset, - material.secrets.value, - )?; - - if let Some(issuance) = input.issuance.as_ref() { - let issuance_entropy = derive_issuance_entropy(material.outpoint, issuance); - let issuance_asset = AssetId::from_entropy(issuance_entropy); - add_balance( - &mut state.supply_by_asset, - issuance_asset, - issuance.asset_amount_sat, - )?; - - if issuance.token_amount_sat > 0 { - let token_asset = - issuance_token_from_entropy_for_unblinded_issuance(issuance_entropy); - add_balance( - &mut state.supply_by_asset, - token_asset, - issuance.token_amount_sat, - )?; - } - } - - Ok(()) - } - - /// Convert deferred issuance-linked demand into concrete asset demand for one input index. - fn activate_deferred_demands_for_input( - input_index: usize, - input: &InputSchema, - material: &ResolvedInputMaterial, - state: &mut ResolutionState, - ) -> Result<(), WalletAbiError> { - // Deferred demands become concrete once the referenced input is known, - // because issuance-derived asset ids depend on that input outpoint/entropy. - let input_index_u32 = u32::try_from(input_index).map_err(|_| { - WalletAbiError::InvalidRequest(format!( - "input index overflow while activating deferred demands: {input_index}" - )) - })?; - let Some(entries) = state.deferred_demands.remove(&input_index_u32) else { - return Ok(()); - }; - - let issuance = input.issuance.as_ref().ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "output asset references input {} but input '{}' has no issuance metadata", - input_index, input.id - )) - })?; - - for (kind, amount_sat) in entries { - let demand_asset = demand_asset_from_deferred(kind, issuance, material, &input.id)?; - add_balance(&mut state.demand_by_asset, demand_asset, amount_sat)?; - } - - Ok(()) - } - - /// Resolve all declared inputs in order and mutate both PSET and equation state. - async fn resolve_declared_inputs( - &self, - pst: &mut PartiallySignedTransaction, - params: &RuntimeParams, - wallet_snapshot: &[WalletTxOut], - state: &mut ResolutionState, - ) -> Result<(), WalletAbiError> { - // Main declared-input pass: - // resolve source -> append PSET input -> increase supply -> unlock deferred demands. - for (input_index, input) in params.inputs.iter().enumerate() { - let material = self - .resolve_declared_input_material(input, wallet_snapshot, state) - .await?; - - self.add_resolved_input_to_pset(pst, input, &material)?; - Self::apply_input_supply(input, &material, state)?; - Self::activate_deferred_demands_for_input(input_index, input, &material, state)?; - } - - Ok(()) - } - - /// Pick the currently largest positive deficit asset (tie-break by asset id ordering). - fn pick_largest_deficit_asset(state: &ResolutionState) -> Option<(AssetId, u64)> { - current_deficits(&state.demand_by_asset, &state.supply_by_asset) - .iter() - .fold( - None, - |best: Option<(AssetId, u64)>, (asset, missing)| match best { - None => Some((*asset, *missing)), - Some((best_asset, best_missing)) => { - if *missing > best_missing - || (*missing == best_missing && *asset < best_asset) - { - Some((*asset, *missing)) - } else { - Some((best_asset, best_missing)) - } - } - }, - ) - } - - /// Select deterministic auxiliary wallet inputs for one deficit asset. - /// - /// Strategy order: - /// 1. exact `BnB` - /// 2. single largest-above-target - /// 3. largest-first accumulation - fn select_auxiliary_inputs_for_asset( - wallet_snapshot: &[WalletTxOut], - used_outpoints: &HashSet, - target_asset: AssetId, - target_missing: u64, - ) -> Result<(Vec, BnbSelectionStatus), WalletAbiError> { - let mut wallet_candidates: Vec = wallet_snapshot - .iter() - .filter(|candidate| { - !used_outpoints.contains(&candidate.outpoint) - && candidate.unblinded.asset == target_asset - }) - .cloned() - .collect(); - if wallet_candidates.is_empty() { - return Err(WalletAbiError::Funding( - "unable to cover remaining deficits with wallet utxos".to_string(), - )); - } - - let mut bnb_candidates = wallet_candidates - .iter() - .map(|candidate| BnbCandidate { - amount_sat: candidate.unblinded.value, - txid_lex: candidate.outpoint.txid.to_string(), - vout: candidate.outpoint.vout, - }) - .collect::>(); - sort_bnb_candidates(&mut bnb_candidates); - wallet_candidates.sort_by(|a, b| { - b.unblinded - .value - .cmp(&a.unblinded.value) - .then_with(|| { - a.outpoint - .txid - .to_string() - .cmp(&b.outpoint.txid.to_string()) - }) - .then_with(|| a.outpoint.vout.cmp(&b.outpoint.vout)) - }); - - let (selected_indices, status) = if let Some(exact) = - bnb_exact_subset_indices(&bnb_candidates, target_missing, MAX_BNB_NODES)? - { - (exact, BnbSelectionStatus::Exact) - } else if let Some(single) = - select_single_largest_above_target(&bnb_candidates, target_missing) - { - (single, BnbSelectionStatus::FallbackSingleLargestAboveTarget) - } else if let Some(accumulated) = - select_largest_first_accumulation(&bnb_candidates, target_missing)? - { - ( - accumulated, - BnbSelectionStatus::FallbackLargestFirstAccumulation, - ) - } else { - return Err(WalletAbiError::Funding( - "unable to cover remaining deficits with wallet utxos".to_string(), - )); - }; - - let selected_total = sum_selected_amount(&bnb_candidates, &selected_indices)?; - if selected_total < target_missing { - return Err(WalletAbiError::Funding( - "unable to cover remaining deficits with wallet utxos".to_string(), - )); - } - - let selected = selected_indices - .iter() - .map(|index| wallet_candidates[*index].clone()) - .collect::>(); - - Ok((selected, status)) - } - - async fn add_auxiliary_wallet_input( - &self, - pst: &mut PartiallySignedTransaction, - state: &mut ResolutionState, - selected: &WalletTxOut, - ) -> Result<(), WalletAbiError> { - if !state.used_outpoints.insert(selected.outpoint) { - return Err(WalletAbiError::InvalidRequest(format!( - "duplicate auxiliary outpoint resolved: {}:{}", - selected.outpoint.txid, selected.outpoint.vout - ))); - } - - // TODO: use wollet cache here instead of fetching. - let tx_out = self.fetch_tx_out(&selected.outpoint).await?; - let mut pset_input = Input::from_prevout(selected.outpoint); - pset_input.witness_utxo = Some(tx_out); - pset_input.amount = Some(selected.unblinded.value); - pset_input.asset = Some(selected.unblinded.asset); - pset_input.proprietary.insert( - get_finalizer_spec_key(), - FinalizerSpec::Wallet.try_encode()?, - ); - pset_input.proprietary.insert( - get_secrets_spec_key(), - serde_json::to_vec(&selected.unblinded)?, - ); - let (pubkey, derivation_path) = - self.signer_origin_for_wallet_utxo(WalletDerivationIndex { - ext_int: selected.ext_int, - wildcard_index: selected.wildcard_index, - })?; - pset_input - .bip32_derivation - .insert(pubkey, (self.signer.fingerprint(), derivation_path)); - pst.add_input(pset_input); - - add_balance( - &mut state.supply_by_asset, - selected.unblinded.asset, - selected.unblinded.value, - )?; - - Ok(()) - } - - /// Add one or more auxiliary wallet inputs targeting one missing asset amount. - /// - /// The selected inputs are appended in deterministic order and each contribution updates - /// `supply_by_asset` immediately. - async fn add_auxiliary_input_for_asset( - &self, - pst: &mut PartiallySignedTransaction, - wallet_snapshot: &[WalletTxOut], - state: &mut ResolutionState, - target_asset: AssetId, - target_missing: u64, - ) -> Result<(), WalletAbiError> { - let (selected_inputs, _status) = Self::select_auxiliary_inputs_for_asset( - wallet_snapshot, - &state.used_outpoints, - target_asset, - target_missing, - )?; - - for selected in &selected_inputs { - self.add_auxiliary_wallet_input(pst, state, selected) - .await?; - } - - Ok(()) - } - - /// Repeatedly add auxiliary wallet inputs until there is no remaining positive deficit. - /// - /// Assets are processed by current largest deficit (asset-id tie-break). - async fn add_auxiliary_inputs_until_balanced( - &self, - pst: &mut PartiallySignedTransaction, - wallet_snapshot: &[WalletTxOut], - state: &mut ResolutionState, - ) -> Result<(), WalletAbiError> { - // Keep injecting auxiliary inputs until the equation has no remaining positive deficits. - while let Some((target_asset, target_missing)) = Self::pick_largest_deficit_asset(state) { - self.add_auxiliary_input_for_asset( - pst, - wallet_snapshot, - state, - target_asset, - target_missing, - ) - .await?; - } - - Ok(()) - } - - /// Resolve all inputs required to satisfy output demand, including issuance-derived demand. - /// - /// The algorithm first consumes declared inputs, then greedily appends auxiliary wallet - /// inputs until the equation has no positive deficits. - /// - /// Fee nuance: - /// - Fee demand is injected implicitly as policy-asset demand equal to `fee_target_sat`. - /// - Fee output amount in request params is ignored for funding demand purposes. - /// - Fee output id validation (`"fee"`) is still enforced for duplicates and asset type. - /// - /// Change nuance: - /// - This resolver does not create or place change outputs. - /// - It guarantees only `supply >= demand` per asset after resolution. - /// - Any surplus created by UTXO granularity/overshoot is left for the output stage - /// to materialize as explicit change. - /// - /// # Complexity - /// - /// Let `I` be declared inputs, `U` wallet UTXOs, `A` demanded assets, and `K` auxiliary - /// inputs added. Declared-input selection is `O(I * U * A)`. Auxiliary per-asset funding - /// is bounded by `MAX_BNB_NODES` search plus deterministic fallbacks. - pub(super) async fn resolve_inputs( - &self, - pst: PartiallySignedTransaction, - params: &RuntimeParams, - fee_target_sat: u64, - ) -> Result { - // Phase 1: initialize demand/supply state and load wallet snapshot once. - // We keep all equation state in a dedicated struct so each phase mutates a single object. - let mut pst = pst; - let wallet_snapshot = self.wollet.utxos()?; - let mut state = ResolutionState::default(); - - // Phase 2: build output demand from AssetVariant. - // AssetId contributes directly, while issuance-linked variants are deferred until their - // referenced input is resolved and its issuance entropy is known. - Self::resolve_output_demands( - params, - fee_target_sat, - *self.network.policy_asset(), - &mut state, - )?; - - // Phase 3: resolve declared inputs in order. - // Each input updates the PSET, contributes supply, and may unlock deferred output demand. - self.resolve_declared_inputs(&mut pst, params, &wallet_snapshot, &mut state) - .await?; - - // Safety check: all deferred output demands must have been activated by now. - if !state.deferred_demands.is_empty() { - return Err(WalletAbiError::InvalidRequest( - "unresolved deferred output demands remain after input resolution".to_string(), - )); - } - - // Phase 4: if the declared inputs do not close the equation, add auxiliary wallet inputs - // greedily by largest remaining deficit asset until fully balanced. - self.add_auxiliary_inputs_until_balanced(&mut pst, &wallet_snapshot, &mut state) - .await?; - - Ok(pst) - } - - /// Return the best wallet UTXO candidate under a deterministic, deficit-aware score. - /// - /// Candidates must pass `WalletSourceFilter`, then are ranked lexicographically by: - /// 1. total remaining deficit after simulated addition - /// 2. remaining deficit on candidate asset - /// 3. candidate overshoot/undershoot for that asset - /// 4. `txid`, then `vout` - /// - /// # Complexity - /// - /// With `U` wallet UTXOs and `A` demanded assets, selection is `O(U * A)` time and `O(A)` - /// temporary space per scored candidate simulation. - fn filter_tx_out( - snapshot: &[WalletTxOut], - filter: &WalletSourceFilter, - used_outpoints: &HashSet, - demand_by_asset: &BTreeMap, - supply_by_asset: &BTreeMap, - ) -> Result, WalletAbiError> { - // Candidate ranking is lexicographic and fully deterministic: - // 1) total remaining deficit after adding candidate - // 2) remaining deficit on candidate's asset - // 3) candidate overshoot/undershoot for that asset - // 4) txid + vout tie-break - let mut best: Option<(WalletTxOut, CandidateScore)> = None; - - for candidate in snapshot - .iter() - .filter(|x| matches_wallet_filter(x, filter, used_outpoints)) - { - let score = score_candidate(candidate, demand_by_asset, supply_by_asset)?; - - match &best { - Some((_, best_score)) if score >= *best_score => {} - _ => { - best = Some((candidate.clone(), score)); - } - } - } - - Ok(best.map(|(candidate, _)| candidate)) - } -} diff --git a/crates/wallet-abi/src/runtime/mod.rs b/crates/wallet-abi/src/runtime/mod.rs deleted file mode 100644 index 79d6fa8..0000000 --- a/crates/wallet-abi/src/runtime/mod.rs +++ /dev/null @@ -1,624 +0,0 @@ -//! Runtime transaction builder/finalizer. -//! -//! High-level flow: -//! 1. Build a fee-targeted PSET (`resolve_inputs` + `balance_out`). -//! 2. Estimate required fee from a finalized+blinded estimation transaction. -//! 3. Iterate fee target to fixed-point convergence (bounded). -//! 4. Build final PSET with converged fee, blind, finalize, and verify proofs. -//! -//! Fee convergence: -//! - initial target: `1 sat` -//! - max iterations: `MAX_FEE_ITERS` -//! - cycle handling: if oscillation is detected, escalate once to max cycle value -//! - failure mode: deterministic `Funding` error when convergence is not reached -//! -//! Formal references: -//! - Bitcoin Core coin selection context: -//! -//! - Murch, *An Evaluation of Coin Selection Strategies*: -//! -//! -pub mod utils; - -mod input_resolution; -mod output_resolution; - -use crate::error::WalletAbiError; -use crate::schema::tx_create::{TransactionInfo, TxCreateRequest, TxCreateResponse}; -use crate::{FinalizerSpec, InputSchema, LockFilter, RuntimeParams, UTXOSource}; -use std::collections::HashMap; - -use std::path::Path; -use std::str::FromStr; -use std::sync::Arc; - -use crate::runtime::utils::to_lwk_wollet_network; -use crate::schema::values::{resolve_arguments, resolve_witness}; -use lwk_common::{Bip, Network, Signer}; -use lwk_signer::SwSigner; -use lwk_signer::bip39::rand::thread_rng; -use lwk_simplicity::runner::run_program; -use lwk_simplicity::scripts::{control_block, load_program}; -use lwk_simplicity::signer::get_and_verify_env; -use lwk_wollet::asyncr::EsploraClient; -use lwk_wollet::bitcoin::bip32::{DerivationPath, Xpriv}; -use lwk_wollet::elements::hex::ToHex; -use lwk_wollet::elements::pset::PartiallySignedTransaction; -use lwk_wollet::elements::pset::raw::ProprietaryKey; -use lwk_wollet::elements::{Address, BlockHash, OutPoint, Script, TxOut, TxOutSecrets}; -use lwk_wollet::elements_miniscript::ToPublicKey; -use lwk_wollet::elements_miniscript::psbt::PsbtExt; -use lwk_wollet::hashes::Hash; -use lwk_wollet::secp256k1::{Keypair, XOnlyPublicKey}; -use lwk_wollet::{EC, Wollet, WolletDescriptor}; -use simplicityhl::elements::{Transaction, encode}; -use simplicityhl::tracker::TrackerLogLevel; -use tokio::sync::Mutex; - -/// Maximum number of fee fixed-point iterations before failing. -const MAX_FEE_ITERS: usize = 8; - -pub(crate) fn get_finalizer_spec_key() -> ProprietaryKey { - ProprietaryKey::from_pset_pair(1, b"finalizer-spec".to_vec()) -} - -pub(crate) fn get_secrets_spec_key() -> ProprietaryKey { - ProprietaryKey::from_pset_pair(1, b"secrets-spec".to_vec()) -} - -pub const DEFAULT_FEE_RATE_SAT_VB: f32 = 0.1; - -#[derive(Debug)] -pub struct WalletRuntimeConfig { - pub signer: SwSigner, - pub network: Network, - pub esplora: Arc>, - pub wollet: Wollet, -} - -impl WalletRuntimeConfig { - pub fn build_random( - network: Network, - esplora_url: &str, - wallet_data_dir: impl AsRef, - ) -> Result { - let (signer, _) = SwSigner::random(network.is_mainnet())?; - - Self::from_signer(signer, network, esplora_url, wallet_data_dir) - } - - pub fn from_mnemonic( - mnemonic: &str, - network: Network, - esplora_url: &str, - wallet_data_dir: impl AsRef, - ) -> Result { - let signer = SwSigner::new(mnemonic, network.is_mainnet())?; - - Self::from_signer(signer, network, esplora_url, wallet_data_dir) - } - - fn x_private(&self, bip: Bip) -> Result { - let x_private = self.signer.derive_xprv(&self.get_derivation_path(bip))?; - - Ok(x_private) - } - - pub(crate) fn get_derivation_path(&self, bip: Bip) -> DerivationPath { - let coin_type = if self.network.is_mainnet() { 1776 } else { 1 }; - let path = match bip { - Bip::Bip84 => format!("84h/{coin_type}h/0h"), - Bip::Bip49 => format!("49h/{coin_type}h/0h"), - Bip::Bip87 => format!("87h/{coin_type}h/0h"), - }; - - DerivationPath::from_str(&format!("m/{path}")).expect("static") - } - - pub fn from_signer( - signer: SwSigner, - network: Network, - esplora_url: &str, - wallet_data_dir: impl AsRef, - ) -> Result { - let descriptor = WolletDescriptor::from_str( - &signer - .wpkh_slip77_descriptor() - .map_err(WalletAbiError::InvalidSignerConfig)?, - )?; - - let lwk_wollet_network = to_lwk_wollet_network(network); - - let esplora = Arc::new(Mutex::new(EsploraClient::new( - lwk_wollet_network, - esplora_url, - ))); - let wollet = Wollet::with_fs_persist(lwk_wollet_network, descriptor, wallet_data_dir)?; - - Ok(Self { - signer, - network, - esplora, - wollet, - }) - } - - pub fn get_descriptor(&self) -> Result { - Ok(WolletDescriptor::from_str( - &self - .signer - .wpkh_slip77_descriptor() - .map_err(WalletAbiError::InvalidSignerConfig)?, - )?) - } - - pub fn signer_x_only_public_key(&self) -> Result { - Ok(self.signer_keypair()?.x_only_public_key().0) - } - - pub fn signer_receive_address(&self) -> Result { - let descriptor = self.get_descriptor()?; - - Ok(descriptor.address(1, self.network.address_params())?) - } - - pub(crate) fn signer_keypair(&self) -> Result { - Ok(self.x_private(Bip::Bip87)?.to_keypair(&EC)) - } - - pub async fn sync_wallet(&mut self) -> Result<(), WalletAbiError> { - self.sync_descriptor(self.get_descriptor()?).await - } - - /// TODO: (this is broken for now) Request a full scan using a script descriptor. - pub async fn sync_script_wollet( - &mut self, - script_pubkey: &Script, - ) -> Result<(), WalletAbiError> { - let spk_descriptor = format!(":{}", script_pubkey.to_hex()); - - self.sync_descriptor(WolletDescriptor::from_str(&spk_descriptor)?) - .await - } - - /// TODO: (this is broken for now) Request a full scan while validating an arbitrary descriptor shape. - pub async fn sync_descriptor( - &mut self, - wollet_descriptor: WolletDescriptor, - ) -> Result<(), WalletAbiError> { - // TODO: fix. - // The descriptor is validated by constructing a temporary wollet, while the full scan is - // currently executed against the runtime's persisted primary wallet. - let _wollet = - Wollet::without_persist(to_lwk_wollet_network(self.network), wollet_descriptor)?; - - if let Some(update) = { - let mut inner_esplora = self.esplora.lock().await; - inner_esplora.full_scan(&self.wollet).await? - } { - self.wollet.apply_update(update)?; - } - - Ok(()) - } - - pub async fn fetch_tx_out(&self, outpoint: &OutPoint) -> Result { - let tx = { - let inner_esplora = self.esplora.lock().await; - inner_esplora.get_transaction(outpoint.txid).await? - }; - let tx_out = tx.output.get(outpoint.vout as usize).ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "prevout transaction {} missing vout {}", - outpoint.txid, outpoint.vout - )) - })?; - - Ok(tx_out.clone()) - } - - pub async fn process_request( - &mut self, - request: &TxCreateRequest, - ) -> Result { - request.validate_for_runtime(self.network)?; - - let fee_rate_sat_vb = validate_fee_rate_sat_vb( - request - .params - .fee_rate_sat_vb - .unwrap_or(DEFAULT_FEE_RATE_SAT_VB), - )?; - - self.sync_wallet().await?; - self.pre_sync_inputs(&request.params.inputs).await?; - - let finalized_tx = self.finalize(&request.params, fee_rate_sat_vb).await?; - - let txid = finalized_tx.txid(); - - if request.broadcast { - let published_txid = { - let inner_esplora = self.esplora.lock().await; - inner_esplora.broadcast(&finalized_tx).await? - }; - if txid != published_txid { - return Err(WalletAbiError::InvalidResponse(format!( - "broadcast txid mismatch: locally built txid={txid}, esplora returned txid={published_txid}" - ))); - } - } - - let response = TxCreateResponse::ok( - request, - TransactionInfo { - tx_hex: encode::serialize_hex(&finalized_tx), - txid, - }, - None, - ); - - Ok(response) - } - - /// Build, blind and finalize a transaction with bounded fee fixed-point convergence. - /// - /// The output stage models fee as explicit policy-asset demand, so this method iterates - /// `fee_target_sat` until the estimated fee matches the target. - /// - /// Failure conditions: - /// - convergence not reached within `MAX_FEE_ITERS` - /// - any intermediate funding deficit raised by resolvers - async fn finalize( - &self, - params: &RuntimeParams, - fee_rate: f32, - ) -> Result { - // Bounded fixed-point fee convergence: - // fee_target -> build tx -> estimate fee -> repeat until stable or cap reached. - let mut fee_target_sat = 1u64; - let mut seen_targets = Vec::new(); - let mut escalated_cycle_once = false; - let mut converged_fee_target = None; - - for _ in 0..MAX_FEE_ITERS { - let estimated_fee_sat = self - .estimate_fee_target(params, fee_target_sat, fee_rate) - .await?; - - if estimated_fee_sat == fee_target_sat { - converged_fee_target = Some(estimated_fee_sat); - break; - } - - if let Some(cycle_start) = seen_targets - .iter() - .position(|previous| *previous == estimated_fee_sat) - { - let cycle_max = seen_targets[cycle_start..] - .iter() - .copied() - .chain(std::iter::once(estimated_fee_sat)) - .max() - .unwrap_or(estimated_fee_sat); - if !escalated_cycle_once { - escalated_cycle_once = true; - seen_targets.push(fee_target_sat); - fee_target_sat = cycle_max; - continue; - } - } - - seen_targets.push(fee_target_sat); - fee_target_sat = estimated_fee_sat; - } - - let converged_fee_target = converged_fee_target.ok_or_else(|| { - WalletAbiError::Funding(format!( - "fee convergence failed after {MAX_FEE_ITERS} iterations; last target={} sat, visited=[{}]", - fee_target_sat, - seen_targets - .iter() - .map(u64::to_string) - .collect::>() - .join(",") - )) - })?; - - let mut pst = self.build_transaction(params, converged_fee_target).await?; - let inp_txout_secrets = Self::input_blinding_secrets(&pst)?; - pst.blind_last(&mut thread_rng(), &EC, &inp_txout_secrets)?; - let pst = self.finalize_all_inputs(pst)?; - - let utxos: Vec = pst - .inputs() - .iter() - .filter_map(|x| x.witness_utxo.clone()) - .collect(); - - let tx = pst.extract_tx()?; - - tx.verify_tx_amt_proofs(&EC, &utxos)?; - - Ok(tx) - } - - /// Collect input secrets used by blinding and surjection-proof domain construction. - fn input_blinding_secrets( - pst: &PartiallySignedTransaction, - ) -> Result, WalletAbiError> { - let mut inp_txout_secrets: HashMap = HashMap::new(); - for (input_index, input) in pst.inputs().iter().enumerate() { - let encoded_secrets = - input - .proprietary - .get(&get_secrets_spec_key()) - .ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "missing input blinding secrets metadata for input index {input_index}" - )) - })?; - let secrets: TxOutSecrets = serde_json::from_slice(encoded_secrets)?; - inp_txout_secrets.insert(input_index, secrets); - } - - Ok(inp_txout_secrets) - } - - fn input_finalizer_spec( - pst: &PartiallySignedTransaction, - input_index: usize, - ) -> Result { - let finalizer_payload = pst - .inputs() - .get(input_index) - .ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "missing input index {input_index} while finalizing transaction" - )) - })? - .proprietary - .get(&get_finalizer_spec_key()) - .ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "missing finalizer metadata for input index {input_index}" - )) - })?; - - FinalizerSpec::decode(finalizer_payload) - } - - /// Estimate required fee for a candidate fee target using a finalized+blinded estimation tx. - /// - /// This is used inside the bounded fixed-point loop in `finalize`. - async fn estimate_fee_target( - &self, - params: &RuntimeParams, - fee_target_sat: u64, - fee_rate: f32, - ) -> Result { - let fee_estimation_build = self.build_transaction(params, fee_target_sat).await?; - let mut pst = self.finalize_all_inputs(fee_estimation_build)?; - let inp_txout_secrets = Self::input_blinding_secrets(&pst)?; - pst.blind_last(&mut thread_rng(), &EC, &inp_txout_secrets)?; - - Ok(calculate_fee(pst.extract_tx()?.discount_weight(), fee_rate)) - } - - fn unblind_with_wallet(&self, tx_out: TxOut) -> Result<(TxOut, TxOutSecrets), WalletAbiError> { - let blinding_private_key = self - .signer - .slip77_master_blinding_key()? - .blinding_private_key(&tx_out.script_pubkey); - - let secrets = tx_out.unblind(&EC, blinding_private_key)?; - - Ok((tx_out, secrets)) - } - - /// Build a fee-targeted PSET by running fee-aware input and output resolvers. - async fn build_transaction( - &self, - params: &RuntimeParams, - fee_target_sat: u64, - ) -> Result { - let mut pst = PartiallySignedTransaction::new_v2(); - pst.global.tx_data.fallback_locktime = params.locktime; - - // Input resolution is fee-aware and receives the current fee target. - pst = self.resolve_inputs(pst, params, fee_target_sat).await?; - - pst = self.balance_out(pst, params, fee_target_sat)?; - - Ok(pst) - } - - pub fn finalize_all_inputs( - &self, - mut pst: PartiallySignedTransaction, - ) -> Result { - let utxos: Vec = pst - .inputs() - .iter() - .filter_map(|x| x.witness_utxo.clone()) - .collect(); - - self.signer.sign(&mut pst)?; - - for input_index in 0..pst.inputs().len() { - let finalizer = Self::input_finalizer_spec(&pst, input_index)?; - - match finalizer { - FinalizerSpec::Wallet => { - pst.finalize_inp_mut(&EC, input_index, BlockHash::all_zeros()) - .map_err(|error| { - WalletAbiError::InvalidFinalizationSteps(format!( - "wallet finalization failed for input index {input_index}: {error}" - )) - })?; - } - FinalizerSpec::Simf { - source_simf, - internal_key, - arguments, - witness, - } => { - let arguments = resolve_arguments(&arguments, &pst)?; - - let program = load_program(&source_simf, arguments)?; - - let env = get_and_verify_env( - &pst.extract_tx()?, - &program, - &internal_key.pubkey.to_x_only_pubkey(), - &utxos, - self.network, - input_index, - )?; - - let witness = resolve_witness(&witness, self, &env)?; - - let pruned = run_program(&program, witness, &env, TrackerLogLevel::None)?.0; - - let (simplicity_program_bytes, simplicity_witness_bytes) = - pruned.to_vec_with_witness(); - let cmr = pruned.cmr(); - - pst.inputs_mut()[input_index].final_script_witness = Some(vec![ - simplicity_witness_bytes, - simplicity_program_bytes, - cmr.as_ref().to_vec(), - control_block(cmr, internal_key.pubkey.to_x_only_pubkey()).serialize(), - ]); - } - } - } - - Ok(pst) - } - - /// Pre-sync script-locked wallet filters before input resolution. - /// - /// Current behavior note: - /// this only triggers sync calls; input resolution still reads UTXOs from - /// `self.wollet.utxos()` and does not switch the resolver snapshot source. - async fn pre_sync_inputs(&mut self, inputs: &[InputSchema]) -> Result<(), WalletAbiError> { - for i in inputs { - match &i.utxo_source { - UTXOSource::Wallet { filter } => match &filter.lock { - LockFilter::None => {} - LockFilter::Script { script } => { - self.sync_script_wollet(script).await?; - } - }, - UTXOSource::Provided { .. } => {} - } - } - - Ok(()) - } -} - -fn validate_fee_rate_sat_vb(fee_rate_sat_vb: f32) -> Result { - if !fee_rate_sat_vb.is_finite() || fee_rate_sat_vb < 0.0 { - return Err(WalletAbiError::InvalidRequest(format!( - "fee_rate_sat_vb must be a finite non-negative value in sat/vB, got {fee_rate_sat_vb}" - ))); - } - - Ok(fee_rate_sat_vb) -} - -/// Calculate fee from weight and fee rate (sat/vB). -/// -/// Formula: `fee = ceil(vsize * fee_rate_sat_vb)` -/// where `vsize = ceil(weight / 4)` -/// -/// # Arguments -/// -/// * `weight` - Transaction weight in weight units (WU) -/// * `fee_rate` - Fee rate in satoshis per virtual byte (sat/vB) -/// -/// # Returns -/// -/// The calculated fee in satoshis. -#[must_use] -#[allow( - clippy::cast_precision_loss, - clippy::cast_possible_truncation, - clippy::cast_sign_loss -)] -pub fn calculate_fee(weight: usize, fee_rate: f32) -> u64 { - let vsize = weight.div_ceil(4); - (vsize as f32 * fee_rate).ceil() as u64 -} - -#[cfg(test)] -mod tests { - use super::*; - use lwk_wollet::elements::hashes::Hash; - use lwk_wollet::elements::pset::Input; - use lwk_wollet::elements::{OutPoint, Txid}; - - fn test_outpoint(tag: u8, vout: u32) -> OutPoint { - OutPoint::new( - Txid::from_slice(&[tag; 32]).expect("txid from fixed bytes"), - vout, - ) - } - - #[test] - fn calculate_fee_rounds_vsize_and_fee_up() { - assert_eq!(calculate_fee(4, 1.0), 1); - assert_eq!(calculate_fee(5, 1.0), 2); - assert_eq!(calculate_fee(8, 1.5), 3); - } - - #[test] - fn calculate_fee_allows_zero_fee_rate() { - assert_eq!(calculate_fee(123, 0.0), 0); - } - - #[test] - fn validate_fee_rate_rejects_negative_and_non_finite_values() { - for invalid in [-0.01, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { - let err = validate_fee_rate_sat_vb(invalid).expect_err("invalid fee rate must fail"); - match err { - WalletAbiError::InvalidRequest(message) => { - assert!(message.contains("fee_rate_sat_vb")); - } - other => panic!("unexpected error variant: {other}"), - } - } - } - - #[test] - fn input_blinding_secrets_missing_metadata_returns_error() { - let mut pst = PartiallySignedTransaction::new_v2(); - pst.add_input(Input::from_prevout(test_outpoint(1, 0))); - - let err = - WalletRuntimeConfig::input_blinding_secrets(&pst).expect_err("missing secrets key"); - match err { - WalletAbiError::InvalidRequest(message) => { - assert!(message.contains("missing input blinding secrets metadata")); - } - other => panic!("unexpected error variant: {other}"), - } - } - - #[test] - fn input_finalizer_spec_missing_metadata_returns_error() { - let mut pst = PartiallySignedTransaction::new_v2(); - pst.add_input(Input::from_prevout(test_outpoint(2, 0))); - - let err = WalletRuntimeConfig::input_finalizer_spec(&pst, 0) - .expect_err("missing finalizer key should fail"); - match err { - WalletAbiError::InvalidRequest(message) => { - assert!(message.contains("missing finalizer metadata")); - } - other => panic!("unexpected error variant: {other}"), - } - } -} diff --git a/crates/wallet-abi/src/runtime/output_resolution.rs b/crates/wallet-abi/src/runtime/output_resolution.rs deleted file mode 100644 index a956718..0000000 --- a/crates/wallet-abi/src/runtime/output_resolution.rs +++ /dev/null @@ -1,590 +0,0 @@ -//! Output resolution for transaction construction. -//! -//! This module balances the final output set through a deterministic, equation-first flow: -//! 1. Materialize requested outputs exactly as declared in `RuntimeParams`. -//! 2. Normalize or append the fee output to match the target policy-asset fee. -//! 3. Aggregate per-asset supply from resolved inputs plus issuance/reissuance minting. -//! 4. Aggregate per-asset demand from all outputs currently present in the PSET. -//! 5. Compute deficits and residuals per asset. -//! 6. Fail if any deficit remains; otherwise append one change output per residual asset. -//! 7. Assign output blinder indices and assert exact per-asset conservation. -//! -//! # Assumptions -//! -//! Input resolution is expected to be fee-aware (implicit fee demand injected on policy asset). -//! This module still enforces exact conservation independently and hard-fails on any deficit. -//! -//! # Complexity -//! -//! Let: -//! - `I` = number of inputs in the PSET -//! - `O` = number of outputs after fee/change materialization -//! - `A` = number of distinct assets across supply and demand -//! -//! Time complexity is `O(I + O + A)` and auxiliary space complexity is `O(A)`. - -use crate::runtime::input_resolution::{ - add_balance, derive_issuance_entropy, issuance_token_from_entropy_for_unblinded_issuance, -}; -use crate::runtime::{WalletRuntimeConfig, get_finalizer_spec_key}; -use crate::{ - AssetVariant, BlinderVariant, FinalizerSpec, InputIssuance, InputIssuanceKind, LockVariant, - RuntimeParams, WalletAbiError, -}; - -use std::collections::{BTreeMap, BTreeSet}; - -use lwk_wollet::bitcoin::PublicKey; -use lwk_wollet::elements::pset::{Output, PartiallySignedTransaction}; -use lwk_wollet::elements::{Address, AssetId, OutPoint, Script}; - -/// Aggregated amounts keyed by asset id. -type AssetBalances = BTreeMap; - -/// Result of one supply-vs-demand comparison pass. -#[derive(Debug, Default)] -struct BalanceDelta { - /// Positive entries where `demand > supply`. - deficit_by_asset: AssetBalances, - /// Positive entries where `supply > demand`. - residual_by_asset: AssetBalances, -} - -/// Validate one output-linked input index and return it as `usize`. -fn validate_output_input_index( - output_id: &str, - input_index: u32, - input_count: usize, -) -> Result { - let idx = usize::try_from(input_index).map_err(|_| { - WalletAbiError::InvalidRequest(format!( - "output '{output_id}' input_index overflow: {input_index}" - )) - })?; - - if idx >= input_count { - return Err(WalletAbiError::InvalidRequest(format!( - "output '{output_id}' references missing input_index {input_index}" - ))); - } - - Ok(idx) -} - -/// Return indices of inputs finalized by the wallet signer. -/// -/// These indices are later used as valid `blinder_index` anchors for blinded outputs. -fn wallet_input_indices(pst: &PartiallySignedTransaction) -> Result, WalletAbiError> { - let mut indices = Vec::new(); - - for (index, input) in pst.inputs().iter().enumerate() { - let finalizer = FinalizerSpec::decode( - input - .proprietary - .get(&get_finalizer_spec_key()) - .ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "missing finalizer metadata for input index {index}" - )) - })?, - )?; - - if matches!(finalizer, FinalizerSpec::Wallet) { - let index_u32 = u32::try_from(index).map_err(|_| { - WalletAbiError::InvalidRequest(format!( - "wallet input index overflow while balancing outputs: {index}" - )) - })?; - indices.push(index_u32); - } - } - - Ok(indices) -} - -/// Aggregate issuance/reissuance minting supply from declared inputs. -/// -/// For each declared input with issuance metadata: -/// - Add `asset_amount_sat` to the derived issuance asset id. -/// - Add `token_amount_sat` to the derived reissuance token id (if non-zero). -fn aggregate_issuance_supply( - pst: &PartiallySignedTransaction, - params: &RuntimeParams, -) -> Result { - let mut balances = AssetBalances::new(); - - for (input_index, input) in params.inputs.iter().enumerate() { - let Some(issuance) = input.issuance.as_ref() else { - continue; - }; - - let pset_input = pst.inputs().get(input_index).ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "input '{}' at index {input_index} missing from PSET while aggregating issuance supply", - input.id - )) - })?; - - let outpoint = OutPoint::new(pset_input.previous_txid, pset_input.previous_output_index); - let entropy = derive_issuance_entropy(outpoint, issuance); - let issuance_asset = AssetId::from_entropy(entropy); - add_balance(&mut balances, issuance_asset, issuance.asset_amount_sat)?; - - if issuance.token_amount_sat > 0 { - let token_asset = issuance_token_from_entropy_for_unblinded_issuance(entropy); - add_balance(&mut balances, token_asset, issuance.token_amount_sat)?; - } - } - - Ok(balances) -} - -/// Aggregate total per-asset input supply. -/// -/// Supply is the sum of: -/// - Base amounts from all PSET inputs. -/// - Minted issuance/reissuance amounts derived from declared input metadata. -/// -/// Overflow is rejected via checked arithmetic. -fn aggregate_input_supply( - pst: &PartiallySignedTransaction, - params: &RuntimeParams, -) -> Result { - let mut balances = AssetBalances::new(); - - for (input_index, input) in pst.inputs().iter().enumerate() { - let asset = input.asset.ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "input index {input_index} missing explicit asset while aggregating supply" - )) - })?; - let amount_sat = input.amount.ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "input index {input_index} missing explicit amount while aggregating supply" - )) - })?; - add_balance(&mut balances, asset, amount_sat)?; - } - - let issuance_supply = aggregate_issuance_supply(pst, params)?; - for (asset_id, amount_sat) in issuance_supply { - add_balance(&mut balances, asset_id, amount_sat)?; - } - - Ok(balances) -} - -/// Aggregate total per-asset output demand from current PSET outputs. -/// -/// Fee output (policy asset, empty script) is treated as ordinary demand and is not -/// special-cased in this aggregation. -fn aggregate_output_demand( - pst: &PartiallySignedTransaction, -) -> Result { - let mut balances = AssetBalances::new(); - - for (output_index, output) in pst.outputs().iter().enumerate() { - let asset = output.asset.ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "output index {output_index} missing explicit asset while aggregating demand" - )) - })?; - let amount_sat = output.amount.ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "output index {output_index} missing explicit amount while aggregating demand" - )) - })?; - add_balance(&mut balances, asset, amount_sat)?; - } - - Ok(balances) -} - -/// Compute per-asset deficits and residuals from supply/demand maps. -/// -/// Definitions: -/// - deficit: `max(demand - supply, 0)` -/// - residual: `max(supply - demand, 0)` -/// -/// Returns `Funding` error if any deficit is present, including deterministic asset-ordered -/// deficit details and the applied fee target context. -fn compute_balance_delta( - supply_by_asset: &AssetBalances, - demand_by_asset: &AssetBalances, - fee_target_sat: u64, -) -> Result { - let mut delta = BalanceDelta::default(); - let mut all_assets = BTreeSet::new(); - - all_assets.extend(supply_by_asset.keys().copied()); - all_assets.extend(demand_by_asset.keys().copied()); - - for asset_id in all_assets { - let supply_sat = supply_by_asset.get(&asset_id).copied().unwrap_or(0); - let demand_sat = demand_by_asset.get(&asset_id).copied().unwrap_or(0); - - if demand_sat > supply_sat { - delta - .deficit_by_asset - .insert(asset_id, demand_sat - supply_sat); - continue; - } - - if supply_sat > demand_sat { - delta - .residual_by_asset - .insert(asset_id, supply_sat - demand_sat); - } - } - - if !delta.deficit_by_asset.is_empty() { - let details = delta - .deficit_by_asset - .iter() - .map(|(asset_id, missing_sat)| format!("{asset_id}:{missing_sat}")) - .collect::>() - .join(", "); - return Err(WalletAbiError::Funding(format!( - "asset deficits after applying fee target {fee_target_sat}: {details}" - ))); - } - - Ok(delta) -} - -/// Resolve the issuance context required for one issuance-derived output asset. -/// -/// The returned tuple is `(issuance_metadata, prevout)`. -fn resolve_issuance_asset_context( - output_id: &str, - input_index: u32, - pst: &PartiallySignedTransaction, - params: &RuntimeParams, -) -> Result<(InputIssuance, OutPoint), WalletAbiError> { - let idx = validate_output_input_index(output_id, input_index, params.inputs.len())?; - let input = params.inputs.get(idx).ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "output '{output_id}' references missing input_index {input_index}" - )) - })?; - let issuance = input.issuance.as_ref().ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "output '{output_id}' references input {} but input '{}' has no issuance metadata", - input_index, input.id - )) - })?; - - let pset_input = pst.inputs().get(idx).ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "resolved PSET input index {input_index} missing while materializing output '{output_id}'" - )) - })?; - let outpoint = OutPoint { - txid: pset_input.previous_txid, - vout: pset_input.previous_output_index, - }; - Ok((issuance.clone(), outpoint)) -} - -/// Resolve one output `AssetVariant` into a concrete `AssetId`. -/// -/// Issuance-linked variants validate issuance-kind compatibility against the referenced input. -fn resolve_output_asset( - output_id: &str, - variant: &AssetVariant, - pst: &PartiallySignedTransaction, - params: &RuntimeParams, -) -> Result { - match variant { - AssetVariant::AssetId { asset_id } => Ok(*asset_id), - AssetVariant::NewIssuanceAsset { input_index } => { - let (issuance, outpoint) = - resolve_issuance_asset_context(output_id, *input_index, pst, params)?; - if issuance.kind != InputIssuanceKind::New { - return Err(WalletAbiError::InvalidRequest(format!( - "output '{output_id}' new_issuance_asset references non-new issuance input index {input_index}" - ))); - } - Ok(AssetId::from_entropy(derive_issuance_entropy( - outpoint, &issuance, - ))) - } - AssetVariant::NewIssuanceToken { input_index } => { - let (issuance, outpoint) = - resolve_issuance_asset_context(output_id, *input_index, pst, params)?; - if issuance.kind != InputIssuanceKind::New { - return Err(WalletAbiError::InvalidRequest(format!( - "output '{output_id}' new_issuance_token references non-new issuance input index {input_index}" - ))); - } - Ok(issuance_token_from_entropy_for_unblinded_issuance( - derive_issuance_entropy(outpoint, &issuance), - )) - } - AssetVariant::ReIssuanceAsset { input_index } => { - let (issuance, outpoint) = - resolve_issuance_asset_context(output_id, *input_index, pst, params)?; - if issuance.kind != InputIssuanceKind::Reissue { - return Err(WalletAbiError::InvalidRequest(format!( - "output '{output_id}' re_issuance_asset references non-reissue input index {input_index}" - ))); - } - Ok(AssetId::from_entropy(derive_issuance_entropy( - outpoint, &issuance, - ))) - } - } -} - -/// Resolve output locking script from request lock variant. -/// -/// - `Script` uses caller-provided script directly. -/// - `Finalizer::Wallet` uses signer receive script. -/// - `Finalizer::Simf` uses internal taproot script from finalizer metadata. -fn resolve_output_lock_script(lock: &LockVariant, signer_address: &Address) -> Script { - match lock { - LockVariant::Script { script } => script.clone(), - LockVariant::Finalizer { finalizer } => match finalizer.as_ref() { - FinalizerSpec::Wallet => signer_address.script_pubkey(), - FinalizerSpec::Simf { internal_key, .. } => internal_key.address.script_pubkey(), - }, - } -} - -/// Mutate or append fee output so it matches the requested fee target. -/// -/// If `fee_output_index` is `Some(i)`, validates and overwrites that output as explicit fee. -/// Otherwise appends a new explicit fee output. -fn apply_fee_target( - pst: &mut PartiallySignedTransaction, - fee_output_index: Option, - fee_target_sat: u64, - policy_asset: AssetId, -) -> Result<(), WalletAbiError> { - if let Some(index) = fee_output_index { - let fee_output = pst.outputs_mut().get_mut(index).ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "fee output index {index} missing while applying fee target" - )) - })?; - - let fee_asset = fee_output.asset.ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "fee output at index {index} is missing explicit asset metadata" - )) - })?; - if fee_asset != policy_asset { - return Err(WalletAbiError::InvalidRequest(format!( - "fee output must use policy asset {policy_asset}, found {fee_asset}" - ))); - } - - fee_output.script_pubkey = Script::new(); - fee_output.amount = Some(fee_target_sat); - fee_output.asset = Some(policy_asset); - fee_output.blinding_key = None; - fee_output.blinder_index = None; - - return Ok(()); - } - - pst.add_output(Output::new_explicit( - Script::new(), - fee_target_sat, - policy_asset, - None, - )); - - Ok(()) -} - -/// Materialize all user-requested outputs into the PSET in declared order. -/// -/// Returns the index of the output whose id is `"fee"` if present, and fails on duplicates. -fn materialize_requested_outputs( - pst: &mut PartiallySignedTransaction, - params: &RuntimeParams, - signer_address: &Address, -) -> Result, WalletAbiError> { - let mut fee_output_index = None; - - for output in ¶ms.outputs { - let asset_id = resolve_output_asset(&output.id, &output.asset, pst, params)?; - let script = resolve_output_lock_script(&output.lock, signer_address); - - let blinding_key: Option = match output.blinder { - BlinderVariant::Wallet => Some( - signer_address - .blinding_pubkey - .ok_or_else(|| { - WalletAbiError::InvalidSignerConfig( - "signer receive address missing blinding pubkey for wallet output blinder" - .to_string(), - ) - })? - .into(), - ), - BlinderVariant::Provided { pubkey } => Some(pubkey.into()), - BlinderVariant::Explicit => None, - }; - - pst.add_output(Output::new_explicit( - script, - output.amount_sat, - asset_id, - blinding_key, - )); - - if output.id == "fee" { - let inserted_index = pst.outputs().len() - 1; - if fee_output_index.replace(inserted_index).is_some() { - return Err(WalletAbiError::InvalidRequest( - "duplicate output id 'fee' in params.outputs".to_string(), - )); - } - } - } - - Ok(fee_output_index) -} - -/// Append one blinded change output per positive residual asset. -/// -/// Change outputs are deterministic because `residual_by_asset` is a `BTreeMap` and therefore -/// iterated in ascending `AssetId` order. -fn append_global_change_outputs( - pst: &mut PartiallySignedTransaction, - signer_address: &Address, - residual_by_asset: &AssetBalances, -) -> Result<(), WalletAbiError> { - let change_blinding_key = signer_address.blinding_pubkey.ok_or_else(|| { - WalletAbiError::InvalidSignerConfig( - "signer receive address missing blinding pubkey for change output".to_string(), - ) - })?; - - for (asset_id, residual_sat) in residual_by_asset { - if *residual_sat == 0 { - continue; - } - - pst.add_output(Output::new_explicit( - signer_address.script_pubkey(), - *residual_sat, - *asset_id, - Some(change_blinding_key.into()), - )); - } - - Ok(()) -} - -/// Apply output `blinder_index` values using the first wallet-finalized input as source. -/// -/// Unblinded outputs get `None`; blinded outputs require at least one wallet-finalized input. -fn apply_output_blinder_indices( - pst: &mut PartiallySignedTransaction, - wallet_input_indices: &[u32], -) -> Result<(), WalletAbiError> { - let wallet_blinder_index = wallet_input_indices.first().copied(); - for (output_index, output) in pst.outputs_mut().iter_mut().enumerate() { - if output.blinding_key.is_none() { - output.blinder_index = None; - continue; - } - - output.blinder_index = Some(wallet_blinder_index.ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "blinded output at index {output_index} requires at least one wallet-finalized input" - )) - })?); - } - - Ok(()) -} - -/// Final safety check asserting exact per-asset conservation after change materialization. -/// -/// This enforces `supply[a] == demand[a]` for every asset `a`. -/// -/// ```rust,ignore -/// assert_exact_asset_conservation(&pst, ¶ms)?; -/// ``` -fn assert_exact_asset_conservation( - pst: &PartiallySignedTransaction, - params: &RuntimeParams, -) -> Result<(), WalletAbiError> { - let supply_by_asset = aggregate_input_supply(pst, params)?; - let demand_by_asset = aggregate_output_demand(pst)?; - let mut all_assets = BTreeSet::new(); - let mut mismatches = Vec::new(); - - all_assets.extend(supply_by_asset.keys().copied()); - all_assets.extend(demand_by_asset.keys().copied()); - - for asset_id in all_assets { - let supply_sat = supply_by_asset.get(&asset_id).copied().unwrap_or(0); - let demand_sat = demand_by_asset.get(&asset_id).copied().unwrap_or(0); - if supply_sat != demand_sat { - mismatches.push(format!( - "{asset_id}:supply={supply_sat},demand={demand_sat}" - )); - } - } - - if mismatches.is_empty() { - return Ok(()); - } - - Err(WalletAbiError::InvalidRequest(format!( - "asset conservation violated after balancing: {}", - mismatches.join("; ") - ))) -} - -impl WalletRuntimeConfig { - /// Materialize and balance final outputs for an already input-resolved PSET. - /// - /// Pipeline: - /// 1. Materialize outputs from params in order. - /// 2. Apply fee target. - /// 3. Build supply/demand equations. - /// 4. Fail on deficits; otherwise append residual change. - /// 5. Assign blinder indices and assert exact conservation. - /// - /// Safety: - /// - This stage intentionally keeps hard deficit failure even when input selection is - /// fee-aware; it is the final conservation gate before signing. - /// - /// # Complexity - /// - /// With `I` inputs, `O` outputs and `A` distinct assets, runtime is `O(I + O + A)` and - /// additional space is `O(A)`. - pub(super) fn balance_out( - &self, - mut pst: PartiallySignedTransaction, - params: &RuntimeParams, - fee_target_sat: u64, - ) -> Result { - let signer_address = self.signer_receive_address()?; - let wallet_input_indices = wallet_input_indices(&pst)?; - let fee_output_index = materialize_requested_outputs(&mut pst, params, &signer_address)?; - - apply_fee_target( - &mut pst, - fee_output_index, - fee_target_sat, - *self.network.policy_asset(), - )?; - - let supply_by_asset = aggregate_input_supply(&pst, params)?; - let demand_by_asset = aggregate_output_demand(&pst)?; - let delta = compute_balance_delta(&supply_by_asset, &demand_by_asset, fee_target_sat)?; - - append_global_change_outputs(&mut pst, &signer_address, &delta.residual_by_asset)?; - apply_output_blinder_indices(&mut pst, &wallet_input_indices)?; - assert_exact_asset_conservation(&pst, params)?; - - Ok(pst) - } -} diff --git a/crates/wallet-abi/src/runtime/utils.rs b/crates/wallet-abi/src/runtime/utils.rs deleted file mode 100644 index d0e5eef..0000000 --- a/crates/wallet-abi/src/runtime/utils.rs +++ /dev/null @@ -1,10 +0,0 @@ -use lwk_common::Network; - -#[must_use] -pub fn to_lwk_wollet_network(network: Network) -> lwk_wollet::ElementsNetwork { - match network { - Network::Liquid => lwk_wollet::ElementsNetwork::Liquid, - Network::TestnetLiquid => lwk_wollet::ElementsNetwork::LiquidTestnet, - Network::LocaltestLiquid => lwk_wollet::ElementsNetwork::default_regtest(), - } -} diff --git a/crates/wallet-abi/src/schema/mod.rs b/crates/wallet-abi/src/schema/mod.rs deleted file mode 100644 index dfa1255..0000000 --- a/crates/wallet-abi/src/schema/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod runtime_params; -pub mod tx_create; -pub mod types; -pub mod values; diff --git a/crates/wallet-abi/src/schema/runtime_params.rs b/crates/wallet-abi/src/schema/runtime_params.rs deleted file mode 100644 index 8907208..0000000 --- a/crates/wallet-abi/src/schema/runtime_params.rs +++ /dev/null @@ -1,275 +0,0 @@ -//! Runtime transaction parameter schema used by `wallet-create-0.1`. -//! -//! Serialization note: -//! enum variants are serialized in `snake_case` across this schema. - -use crate::WalletAbiError; -use crate::taproot_pubkey_gen::TaprootPubkeyGen; - -use lwk_wollet::elements::LockTime; -use serde::{Deserialize, Serialize}; - -use simplicityhl::elements::secp256k1_zkp::{PublicKey, SecretKey}; -use simplicityhl::elements::{Address, AssetId, OutPoint, Script, Sequence}; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RuntimeParams { - #[serde(default)] - pub inputs: Vec, - #[serde(default)] - pub outputs: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub fee_rate_sat_vb: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub locktime: Option, -} - -impl RuntimeParams { - pub fn from_request_params(value: &serde_json::Value) -> Result { - serde_json::from_value(value.clone()) - .map_err(|e| WalletAbiError::InvalidRequest(format!("invalid request params: {e}"))) - } - - pub fn to_request_params_value(&self) -> Result { - serde_json::to_value(self).map_err(WalletAbiError::from) - } -} - -impl InputSchema { - #[must_use] - pub const fn with_issuance(mut self, issuance: InputIssuance) -> Self { - self.issuance = Some(issuance); - self - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "snake_case")] -pub enum AssetFilter { - #[default] - None, - Exact { - asset_id: AssetId, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "snake_case")] -pub enum AmountFilter { - #[default] - None, - Exact { - satoshi: u64, - }, - Min { - satoshi: u64, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "snake_case")] -pub enum LockFilter { - #[default] - None, - Script { - script: Script, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -pub struct WalletSourceFilter { - pub asset: AssetFilter, - pub amount: AmountFilter, - pub lock: LockFilter, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum UTXOSource { - Wallet { filter: WalletSourceFilter }, - Provided { outpoint: OutPoint }, -} - -impl Default for UTXOSource { - fn default() -> Self { - Self::Wallet { - filter: WalletSourceFilter::default(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum InputIssuanceKind { - New, - Reissue, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct InputIssuance { - pub kind: InputIssuanceKind, - pub asset_amount_sat: u64, - pub token_amount_sat: u64, - pub entropy: [u8; 32], -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum FinalizerSpec { - #[default] - Wallet, - Simf { - source_simf: String, - internal_key: Box, - arguments: Vec, - witness: Vec, - }, -} - -impl FinalizerSpec { - pub fn try_encode(&self) -> Result, WalletAbiError> { - serde_json::to_vec(self).map_err(Into::into) - } - - #[must_use] - pub fn encode(&self) -> Vec { - self.try_encode() - .expect("finalizer spec serialization should not fail") - } - - pub fn decode(bytes: &[u8]) -> Result { - serde_json::from_slice(bytes).map_err(Into::into) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "snake_case")] -pub enum InputBlinder { - #[default] - Wallet, - Provided { - secret_key: SecretKey, - }, - Explicit, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] -pub struct InputSchema { - pub id: String, - pub utxo_source: UTXOSource, - pub blinder: InputBlinder, - pub sequence: Sequence, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub issuance: Option, - pub finalizer: FinalizerSpec, -} - -impl InputSchema { - pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - ..Default::default() - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum LockVariant { - Script { script: Script }, - Finalizer { finalizer: Box }, -} - -impl Default for LockVariant { - fn default() -> Self { - Self::Finalizer { - finalizer: Box::new(FinalizerSpec::default()), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum AssetVariant { - AssetId { asset_id: AssetId }, - NewIssuanceAsset { input_index: u32 }, - NewIssuanceToken { input_index: u32 }, - ReIssuanceAsset { input_index: u32 }, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "snake_case")] -pub enum BlinderVariant { - #[default] - Wallet, - Provided { - pubkey: PublicKey, - }, - Explicit, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct OutputSchema { - pub id: String, - pub amount_sat: u64, - pub lock: LockVariant, - pub asset: AssetVariant, - pub blinder: BlinderVariant, -} - -impl OutputSchema { - #[must_use] - pub fn from_address( - id: impl Into, - asset_id: AssetId, - amount_sat: u64, - address: &Address, - ) -> Self { - let blinder = address - .blinding_pubkey - .map_or_else(BlinderVariant::default, |pubkey| BlinderVariant::Provided { - pubkey, - }); - - Self { - id: id.into(), - amount_sat, - lock: LockVariant::Script { - script: address.script_pubkey(), - }, - asset: AssetVariant::AssetId { asset_id }, - blinder, - } - } - - #[must_use] - pub fn from_script( - id: impl Into, - asset_id: AssetId, - amount_sat: u64, - script: Script, - ) -> Self { - Self { - id: id.into(), - amount_sat, - lock: LockVariant::Script { script }, - asset: AssetVariant::AssetId { asset_id }, - blinder: BlinderVariant::Explicit, - } - } - - #[must_use] - pub fn fee_placeholder(policy_asset: AssetId) -> Self { - Self { - id: "fee".to_string(), - amount_sat: 0, - lock: LockVariant::Script { - script: Script::new(), - }, - asset: AssetVariant::AssetId { - asset_id: policy_asset, - }, - blinder: BlinderVariant::Explicit, - } - } -} diff --git a/crates/wallet-abi/src/schema/tx_create.rs b/crates/wallet-abi/src/schema/tx_create.rs deleted file mode 100644 index 040223e..0000000 --- a/crates/wallet-abi/src/schema/tx_create.rs +++ /dev/null @@ -1,121 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use lwk_wollet::elements::Txid; - -use crate::schema::types::ErrorInfo; -use crate::{Network, RuntimeParams, WalletAbiError}; - -pub const TX_CREATE_ABI_VERSION: &str = "wallet-create-0.1"; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct TransactionInfo { - pub tx_hex: String, - pub txid: Txid, -} - -pub type TxCreateArtifacts = serde_json::Map; - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum Status { - Ok, - Error, -} - -/// Transaction-create request envelope for the `wallet-create-0.1` ABI. -/// -/// `abi_version` and `network` are contract-level fields and should be validated -/// by runtime entrypoints before any wallet/network side effects. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct TxCreateRequest { - pub abi_version: String, - pub request_id: String, - pub network: Network, - pub params: RuntimeParams, - pub broadcast: bool, -} - -impl TxCreateRequest { - /// Validate request-level contract fields against the active runtime context. - /// - /// # Errors - /// - /// Returns [`WalletAbiError::InvalidRequest`] when `abi_version` or `network` - /// does not match runtime expectations. - pub fn validate_for_runtime(&self, runtime_network: Network) -> Result<(), WalletAbiError> { - if self.abi_version != TX_CREATE_ABI_VERSION { - return Err(WalletAbiError::InvalidRequest(format!( - "request abi_version mismatch: expected '{TX_CREATE_ABI_VERSION}', got '{}'", - self.abi_version - ))); - } - - if self.network != runtime_network { - return Err(WalletAbiError::InvalidRequest(format!( - "request network mismatch: expected {:?}, got {:?}", - runtime_network, self.network - ))); - } - - Ok(()) - } -} - -/// Transaction-create response envelope for the `wallet-create-0.1` ABI. -/// -/// Runtime currently returns `Result`. -/// This type is still useful for adapters that always emit ABI envelopes. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct TxCreateResponse { - pub abi_version: String, - pub request_id: String, - pub network: Network, - pub status: Status, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub transaction: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub artifacts: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -impl TxCreateResponse { - /// Build a successful ABI response envelope. - #[must_use] - pub fn ok( - request: &TxCreateRequest, - transaction: TransactionInfo, - artifacts: Option, - ) -> Self { - Self { - abi_version: TX_CREATE_ABI_VERSION.to_string(), - request_id: request.request_id.clone(), - network: request.network, - status: Status::Ok, - transaction: Some(transaction), - artifacts, - error: None, - } - } - - /// Build an error ABI response envelope. - /// - /// Intended for transport/adapters that must always return ABI responses - /// instead of bubbling runtime errors. - #[must_use] - pub fn error(request: &TxCreateRequest, code: &str, message: &str) -> Self { - Self { - abi_version: TX_CREATE_ABI_VERSION.to_string(), - request_id: request.request_id.clone(), - network: request.network, - status: Status::Error, - transaction: None, - artifacts: None, - error: Some(ErrorInfo { - code: code.to_string(), - message: message.to_string(), - details: None, - }), - } - } -} diff --git a/crates/wallet-abi/src/schema/types.rs b/crates/wallet-abi/src/schema/types.rs deleted file mode 100644 index 14b499c..0000000 --- a/crates/wallet-abi/src/schema/types.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ErrorInfo { - pub code: String, - pub message: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub details: Option, -} diff --git a/crates/wallet-abi/src/schema/values.rs b/crates/wallet-abi/src/schema/values.rs deleted file mode 100644 index 07bb6fd..0000000 --- a/crates/wallet-abi/src/schema/values.rs +++ /dev/null @@ -1,176 +0,0 @@ -use crate::WalletAbiError; -use crate::runtime::WalletRuntimeConfig; - -use std::collections::HashMap; -use std::sync::Arc; - -use serde::{Deserialize, Serialize}; - -use lwk_wollet::elements::Transaction; -use lwk_wollet::elements::pset::PartiallySignedTransaction; -use lwk_wollet::hashes::Hash; -use lwk_wollet::secp256k1::{Message, XOnlyPublicKey}; - -use simplicityhl::num::U256; -use simplicityhl::parse::ParseFromStr; -use simplicityhl::simplicity::jet::elements::ElementsEnv; -use simplicityhl::str::WitnessName; -use simplicityhl::value::{UIntValue, ValueConstructible}; -use simplicityhl::{Arguments, Value, WitnessValues}; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum RuntimeSimfValue { - NewIssuanceAsset { input_index: u32 }, - NewIssuanceToken { input_index: u32 }, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct SimfArguments { - pub resolved: simplicityhl::Arguments, - pub runtime_arguments: HashMap, -} - -impl SimfArguments { - #[must_use] - pub fn new(static_arguments: simplicityhl::Arguments) -> Self { - Self { - resolved: static_arguments, - runtime_arguments: HashMap::new(), - } - } - - pub fn append_runtime_simf_value(&mut self, name: &str, runtime_simf_value: RuntimeSimfValue) { - self.runtime_arguments - .insert(name.to_string(), runtime_simf_value); - } -} - -/// Convert compiled Simplicity arguments into bytes. -pub fn serialize_arguments(arguments: &SimfArguments) -> Result, WalletAbiError> { - Ok(serde_json::to_vec(arguments)?) -} - -fn parse_witness_name(name: &str, source: &str) -> Result { - WitnessName::parse_from_str(name).map_err(|error| { - WalletAbiError::InvalidRequest(format!( - "invalid Simplicity witness name '{name}' in {source}: {error}" - )) - }) -} - -/// Deserialize and resolve compiled Simplicity arguments from bytes. -pub fn resolve_arguments( - bytes: &[u8], - pst: &PartiallySignedTransaction, -) -> Result { - let simf_arguments: SimfArguments = serde_json::from_slice(bytes)?; - - let mut final_arguments: HashMap = HashMap::::new(); - - for (name, value) in simf_arguments.runtime_arguments { - match value { - RuntimeSimfValue::NewIssuanceAsset { input_index } => { - let input = pst - .inputs() - .get(input_index as usize) - .ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "runtime Simplicity argument '{name}' references missing input_index {input_index} (pset inputs: {})", - pst.inputs().len() - )) - })?; - let (asset, _) = input.issuance_ids(); - let witness_name = parse_witness_name(&name, "runtime argument map")?; - - final_arguments.insert( - witness_name, - Value::from(UIntValue::U256(U256::from_byte_array(asset.into_inner().0))), - ); - } - RuntimeSimfValue::NewIssuanceToken { input_index } => { - let input = pst - .inputs() - .get(input_index as usize) - .ok_or_else(|| { - WalletAbiError::InvalidRequest(format!( - "runtime Simplicity argument '{name}' references missing input_index {input_index} (pset inputs: {})", - pst.inputs().len() - )) - })?; - let (_, token) = input.issuance_ids(); - let witness_name = parse_witness_name(&name, "runtime argument map")?; - - final_arguments.insert( - witness_name, - Value::from(UIntValue::U256(U256::from_byte_array(token.into_inner().0))), - ); - } - } - } - - for static_arg in simf_arguments.resolved.iter() { - final_arguments.insert(static_arg.0.clone(), static_arg.1.clone()); - } - - Ok(Arguments::from(final_arguments)) -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum RuntimeSimfWitness { - SigHashAll { - name: String, - public_key: XOnlyPublicKey, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct SimfWitness { - pub resolved: WitnessValues, - pub runtime_arguments: Vec, -} - -/// Convert compiled Simplicity witness into bytes. -pub fn serialize_witness(witness: &SimfWitness) -> Result, WalletAbiError> { - Ok(serde_json::to_vec(witness)?) -} - -/// Deserialize and resolve compiled Simplicity witness values from bytes. -pub fn resolve_witness( - bytes: &[u8], - runtime: &WalletRuntimeConfig, - env: &ElementsEnv>, -) -> Result { - let simf_arguments: SimfWitness = serde_json::from_slice(bytes)?; - - let mut final_witness: HashMap = HashMap::::new(); - - let keypair = runtime.signer_keypair()?; - let sighash_all = Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); - - for value in simf_arguments.runtime_arguments { - match value { - RuntimeSimfWitness::SigHashAll { name, public_key } => { - let signer_public_key = keypair.x_only_public_key().0; - if signer_public_key != public_key { - return Err(WalletAbiError::InvalidRequest(format!( - "sighash_all witness '{name}' public key mismatch: expected {public_key}, runtime signer is {signer_public_key}" - ))); - } - let witness_name = parse_witness_name(&name, "runtime witness map")?; - - final_witness.insert( - witness_name, - Value::byte_array(keypair.sign_schnorr(sighash_all).serialize()), - ); - } - } - } - - for static_arg in simf_arguments.resolved.iter() { - final_witness.insert(static_arg.0.clone(), static_arg.1.clone()); - } - - Ok(WitnessValues::from(final_witness)) -} diff --git a/crates/wallet-abi/src/scripts.rs b/crates/wallet-abi/src/scripts.rs deleted file mode 100644 index 593f7af..0000000 --- a/crates/wallet-abi/src/scripts.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Script and Taproot helpers shared by contract crates. - -use sha2::{Digest, Sha256}; - -pub use lwk_simplicity::scripts::{ - control_block, create_p2tr_address, load_program, simplicity_leaf_version, tap_data_hash, -}; -use simplicityhl::elements::{AssetId, ContractHash, OutPoint, Script}; -use simplicityhl::simplicity::hashes::{Hash, sha256}; - -/// SHA256 of a scriptPubKey byte payload. -#[must_use] -pub fn hash_script(script: &Script) -> [u8; 32] { - let mut hasher = Sha256::new(); - hasher.update(script.as_bytes()); - hasher.finalize().into() -} - -/// Compute issuance entropy for a new issuance from outpoint + 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/wallet-abi/src/simplicity/mod.rs b/crates/wallet-abi/src/simplicity/mod.rs deleted file mode 100644 index 5083e09..0000000 --- a/crates/wallet-abi/src/simplicity/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod p2pk; diff --git a/crates/wallet-abi/src/simplicity/p2pk.rs b/crates/wallet-abi/src/simplicity/p2pk.rs deleted file mode 100644 index 854a45d..0000000 --- a/crates/wallet-abi/src/simplicity/p2pk.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use lwk_common::Network; -use lwk_simplicity::runner::run_program; -use lwk_simplicity::scripts::{create_p2tr_address, load_program}; - -use simplicityhl::elements::secp256k1_zkp::schnorr::Signature; -use simplicityhl::num::U256; -use simplicityhl::simplicity::RedeemNode; -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::ElementsEnv; -use simplicityhl::str::WitnessName; -use simplicityhl::tracker::TrackerLogLevel; -use simplicityhl::value::ValueConstructible; -use simplicityhl::{CompiledProgram, Value, WitnessValues, elements}; - -use crate::ProgramError; - -/// Embedded Simplicity source for a basic P2PK program. -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. -pub fn get_p2pk_address( - x_only_public_key: &XOnlyPublicKey, - network: Network, -) -> 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. -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. -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) -} diff --git a/crates/wallet-abi/src/source_simf/p2pk.simf b/crates/wallet-abi/src/source_simf/p2pk.simf deleted file mode 100644 index db4f27c..0000000 --- a/crates/wallet-abi/src/source_simf/p2pk.simf +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - jet::bip_0340_verify((param::PUBLIC_KEY, jet::sig_all_hash()), witness::SIGNATURE) -} \ No newline at end of file diff --git a/crates/wallet-abi/src/taproot_pubkey_gen.rs b/crates/wallet-abi/src/taproot_pubkey_gen.rs deleted file mode 100644 index 698b578..0000000 --- a/crates/wallet-abi/src/taproot_pubkey_gen.rs +++ /dev/null @@ -1,448 +0,0 @@ -//! Ephemeral Taproot pubkey and address generator for argument-bound programs. -//! -//! Produces a deterministic X-only public key and corresponding address without -//! holding a private key, based on a random seed. The resulting trio -//! `::` can be printed and -//! later verified with the same arguments to prevent mismatches. -//! -//! Identity field formats: -//! - `seed_hex`: 32-byte random seed (legacy/current default) -//! - `ext-`: externally supplied 32-byte x-only key handle - -use sha2::{Digest, Sha256}; -use std::fmt::Display; -use std::str::FromStr; - -use lwk_common::Network; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use simplicityhl::elements::{Address, schnorr::XOnlyPublicKey}; - -use crate::ProgramError; -use simplicityhl::simplicity::ToXOnlyPubkey; -use simplicityhl::simplicity::bitcoin::PublicKey; -use simplicityhl::simplicity::bitcoin::key::Parity; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -enum TaprootIdentity { - Seed(Vec), - ExternalXOnly(XOnlyPublicKey), -} - -/// Errors from taproot pubkey generation and verification. -#[derive(Debug, thiserror::Error)] -pub enum TaprootPubkeyGenError { - #[error("Invalid pubkey recovered: expected {expected}, got {actual}")] - InvalidPubkey { expected: String, actual: String }, - - #[error("Invalid address recovered: expected {expected}, got {actual}")] - InvalidAddress { expected: String, actual: String }, - - #[error( - "Invalid taproot pubkey gen string: expected 3 parts separated by ':', got {parts_count}" - )] - InvalidFormat { parts_count: usize }, - - #[error("Failed to decode seed hex: {0}")] - SeedHexDecode(#[from] hex::FromHexError), - - #[error("Invalid seed length: expected 32 bytes, got {actual}")] - InvalidSeedLength { actual: usize }, - - #[error("Failed to parse public key: {0}")] - PublicKeyParse(#[from] simplicityhl::simplicity::bitcoin::key::ParsePublicKeyError), - - #[error("Failed to parse address: {0}")] - AddressParse(#[from] simplicityhl::elements::address::AddressError), - - #[error("Failed to create X-only public key from bytes: {0}")] - XOnlyPublicKey(#[from] simplicityhl::simplicity::bitcoin::secp256k1::Error), - - #[error("Invalid external x-only key: {0}")] - InvalidExternalKey(String), - - #[error("Failed to generate address: {0}")] - AddressGeneration(#[from] ProgramError), -} - -/// Container for the seed, public key and derived address. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct TaprootPubkeyGen { - identity: TaprootIdentity, - pub pubkey: PublicKey, - pub address: Address, -} - -impl TaprootPubkeyGen { - /// Build from current process randomness and compute the address given `arguments`. - /// - /// Kept as `from` for compatibility with existing callers. - /// - /// # Errors - /// Returns error if address generation fails. - pub fn from( - arguments: &A, - network: Network, - get_address: &impl Fn(&XOnlyPublicKey, &A, Network) -> Result, - ) -> Result { - let (not_existent_public_key, seed) = generate_public_key_without_private(); - - let address = get_address( - ¬_existent_public_key.to_x_only_pubkey(), - arguments, - network, - )?; - - Ok(Self { - identity: TaprootIdentity::Seed(seed), - pubkey: not_existent_public_key, - address, - }) - } - - /// Parse from string and verify that pubkey and address match the provided arguments. - /// - /// # Errors - /// Returns error if parsing fails or verification doesn't match. - pub fn build_from_str( - s: &str, - arguments: &A, - network: Network, - get_address: &impl Fn(&XOnlyPublicKey, &A, Network) -> Result, - ) -> Result { - let taproot_pubkey_gen = Self::parse_from_str(s)?; - - taproot_pubkey_gen.verify(arguments, network, get_address)?; - - Ok(taproot_pubkey_gen) - } - - /// Verify that the stored pubkey and address are consistent with `arguments`. - /// - /// # Errors - /// Returns error if pubkey or address doesn't match the expected values. - pub fn verify( - &self, - arguments: &A, - network: Network, - get_address: &impl Fn(&XOnlyPublicKey, &A, Network) -> Result, - ) -> Result<(), TaprootPubkeyGenError> { - match &self.identity { - TaprootIdentity::Seed(seed) => { - let rand_seed = seed.as_slice(); - - let mut hasher = Sha256::new(); - sha2::digest::Update::update(&mut hasher, rand_seed); - sha2::digest::Update::update(&mut hasher, rand_seed); - sha2::digest::Update::update(&mut hasher, rand_seed); - let potential_pubkey: [u8; 32] = hasher.finalize().into(); - - let expected_pubkey: PublicKey = XOnlyPublicKey::from_slice(&potential_pubkey)? - .public_key(Parity::Even) - .into(); - - if expected_pubkey != self.pubkey { - return Err(TaprootPubkeyGenError::InvalidPubkey { - expected: expected_pubkey.to_string(), - actual: self.pubkey.to_string(), - }); - } - } - TaprootIdentity::ExternalXOnly(xonly) => { - if &self.pubkey.to_x_only_pubkey() != xonly { - let expected_pubkey: PublicKey = xonly.public_key(Parity::Even).into(); - return Err(TaprootPubkeyGenError::InvalidPubkey { - expected: expected_pubkey.to_string(), - actual: self.pubkey.to_string(), - }); - } - } - } - - let expected_address = get_address(&self.pubkey.to_x_only_pubkey(), arguments, network)?; - if self.address != expected_address { - return Err(TaprootPubkeyGenError::InvalidAddress { - expected: expected_address.to_string(), - actual: self.address.to_string(), - }); - } - - Ok(()) - } - - /// Get the X-only public key. - #[must_use] - pub fn get_x_only_pubkey(&self) -> XOnlyPublicKey { - self.pubkey.to_x_only_pubkey() - } - - pub fn to_json(&self) -> serde_json::Result { - serde_json::to_value(self) - } - - /// Parse `::
` representation. - fn parse_from_str(s: &str) -> Result { - let parts = s.split(':').collect::>(); - - if parts.len() != 3 { - return Err(TaprootPubkeyGenError::InvalidFormat { - parts_count: parts.len(), - }); - } - - let identity = if let Some(xonly_hex) = parts[0].strip_prefix("ext-") { - let xonly_bytes = hex::decode(xonly_hex) - .map_err(|e| TaprootPubkeyGenError::InvalidExternalKey(e.to_string()))?; - if xonly_bytes.len() != 32 { - return Err(TaprootPubkeyGenError::InvalidExternalKey(format!( - "expected 32-byte x-only pubkey, got {} bytes", - xonly_bytes.len() - ))); - } - TaprootIdentity::ExternalXOnly( - XOnlyPublicKey::from_slice(&xonly_bytes) - .map_err(|e| TaprootPubkeyGenError::InvalidExternalKey(e.to_string()))?, - ) - } else { - let seed = hex::decode(parts[0])?; - if seed.len() != 32 { - return Err(TaprootPubkeyGenError::InvalidSeedLength { actual: seed.len() }); - } - TaprootIdentity::Seed(seed) - }; - - Ok(Self { - identity, - pubkey: PublicKey::from_str(parts[1])?, - address: Address::from_str(parts[2])?, - }) - } -} - -impl Display for TaprootPubkeyGen { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let id = match &self.identity { - TaprootIdentity::Seed(seed) => hex::encode(seed), - TaprootIdentity::ExternalXOnly(xonly) => { - format!("ext-{}", hex::encode(xonly.serialize())) - } - }; - write!(f, "{}:{}:{}", id, self.pubkey, self.address) - } -} - -/// Try to deterministically map a random seed into a valid X-only pubkey. -fn try_generate_public_key_without_private() -> Result<(PublicKey, Vec), TaprootPubkeyGenError> -{ - let rand_seed: [u8; 32] = get_random_seed(); - - let mut hasher = Sha256::new(); - sha2::digest::Update::update(&mut hasher, &rand_seed); - sha2::digest::Update::update(&mut hasher, &rand_seed); - sha2::digest::Update::update(&mut hasher, &rand_seed); - let potential_pubkey: [u8; 32] = hasher.finalize().into(); - - Ok(( - XOnlyPublicKey::from_slice(&potential_pubkey)? - .public_key(Parity::Even) - .into(), - rand_seed.to_vec(), - )) -} - -/// Generate a valid ephemeral public key and its seed; repeats until valid. -#[must_use] -pub fn generate_public_key_without_private() -> (PublicKey, Vec) { - let not_existent_public_key; - loop { - if let Ok(public_key) = try_generate_public_key_without_private() { - not_existent_public_key = public_key; - break; - } - } - - not_existent_public_key -} - -/// System-random 32-byte seed. -/// -/// # Panics -/// Panics if the system random number generator fails. -#[must_use] -pub fn get_random_seed() -> [u8; 32] { - ring::rand::generate(&ring::rand::SystemRandom::new()) - .unwrap() - .expose() -} - -#[cfg(test)] -mod tests { - use super::*; - use simplicityhl::elements::schnorr::Keypair; - use simplicityhl::elements::secp256k1_zkp::{SECP256K1, SecretKey}; - - struct TestArgs; - - fn address_for_key( - xonly: &XOnlyPublicKey, - _args: &TestArgs, - network: Network, - ) -> Result { - crate::get_p2pk_address(xonly, network) - } - - fn deterministic_test_keypair(byte: u8) -> (XOnlyPublicKey, PublicKey) { - let secret = SecretKey::from_slice(&[byte; 32]).expect("secret"); - let keypair = Keypair::from_secret_key(SECP256K1, &secret); - let xonly = keypair.x_only_public_key().0; - let pubkey: PublicKey = xonly.public_key(Parity::Even).into(); - (xonly, pubkey) - } - - #[test] - fn build_from_str_supports_legacy_seed_handle() { - let args = TestArgs; - let generated = TaprootPubkeyGen::from(&args, Network::TestnetLiquid, &address_for_key) - .expect("generated handle"); - let encoded = generated.to_string(); - let decoded = TaprootPubkeyGen::build_from_str( - &encoded, - &args, - Network::TestnetLiquid, - &address_for_key, - ) - .expect("decoded handle"); - assert_eq!(decoded.pubkey, generated.pubkey); - assert_eq!(decoded.address, generated.address); - } - - #[test] - fn build_from_str_supports_external_xonly_handle() { - let args = TestArgs; - let (xonly, pubkey) = deterministic_test_keypair(0x12); - let address = address_for_key(&xonly, &args, Network::TestnetLiquid).expect("address"); - - let encoded = format!( - "ext-{}:{}:{}", - hex::encode(xonly.serialize()), - pubkey, - address - ); - let decoded = TaprootPubkeyGen::build_from_str( - &encoded, - &args, - Network::TestnetLiquid, - &address_for_key, - ) - .expect("decoded handle"); - - assert_eq!(decoded.pubkey, pubkey); - assert_eq!(decoded.address, address); - } - - #[test] - fn build_from_str_rejects_non_32_byte_seed_handle() { - let args = TestArgs; - let generated = TaprootPubkeyGen::from(&args, Network::TestnetLiquid, &address_for_key) - .expect("generated handle"); - let encoded = generated.to_string(); - let parts = encoded.split(':').collect::>(); - - let invalid_seed_handle = format!("{}:{}:{}", hex::encode([0x11; 31]), parts[1], parts[2]); - let err = TaprootPubkeyGen::build_from_str( - &invalid_seed_handle, - &args, - Network::TestnetLiquid, - &address_for_key, - ) - .expect_err("must reject non-32-byte seed"); - - assert!(matches!( - err, - TaprootPubkeyGenError::InvalidSeedLength { actual } if actual == 31 - )); - } - - #[test] - fn build_from_str_rejects_external_identity_with_wrong_length() { - let args = TestArgs; - let (xonly, pubkey) = deterministic_test_keypair(0x22); - let address = address_for_key(&xonly, &args, Network::TestnetLiquid).expect("address"); - - let encoded = format!("ext-{}:{}:{}", hex::encode([0xAB; 31]), pubkey, address); - let err = TaprootPubkeyGen::build_from_str( - &encoded, - &args, - Network::TestnetLiquid, - &address_for_key, - ) - .expect_err("must reject external x-only keys with wrong size"); - - assert!(matches!(err, TaprootPubkeyGenError::InvalidExternalKey(_))); - } - - #[test] - fn build_from_str_rejects_external_identity_with_invalid_hex() { - let args = TestArgs; - let (xonly, pubkey) = deterministic_test_keypair(0x23); - let address = address_for_key(&xonly, &args, Network::TestnetLiquid).expect("address"); - - let encoded = format!("ext-not_hex:{pubkey}:{address}"); - let err = TaprootPubkeyGen::build_from_str( - &encoded, - &args, - Network::TestnetLiquid, - &address_for_key, - ) - .expect_err("must reject non-hex external x-only prefix"); - - assert!(matches!(err, TaprootPubkeyGenError::InvalidExternalKey(_))); - } - - #[test] - fn build_from_str_rejects_mismatched_pubkey_for_seed_identity() { - let args = TestArgs; - let generated = TaprootPubkeyGen::from(&args, Network::TestnetLiquid, &address_for_key) - .expect("generated handle"); - let encoded = generated.to_string(); - let parts = encoded.split(':').collect::>(); - let wrong_pubkey = deterministic_test_keypair(0x44).1; - - let tampered = format!("{}:{}:{}", parts[0], wrong_pubkey, parts[2]); - let err = TaprootPubkeyGen::build_from_str( - &tampered, - &args, - Network::TestnetLiquid, - &address_for_key, - ) - .expect_err("must reject mismatched seed-derived public key"); - - assert!(matches!(err, TaprootPubkeyGenError::InvalidPubkey { .. })); - } - - #[test] - fn build_from_str_rejects_mismatched_address() { - let args = TestArgs; - let generated = TaprootPubkeyGen::from(&args, Network::TestnetLiquid, &address_for_key) - .expect("generated handle"); - let encoded = generated.to_string(); - let parts = encoded.split(':').collect::>(); - let wrong_network_address = address_for_key( - &generated.get_x_only_pubkey(), - &args, - Network::LocaltestLiquid, - ) - .expect("address"); - - let tampered = format!("{}:{}:{}", parts[0], parts[1], wrong_network_address); - let err = TaprootPubkeyGen::build_from_str( - &tampered, - &args, - Network::TestnetLiquid, - &address_for_key, - ) - .expect_err("must reject mismatched address"); - - assert!(matches!(err, TaprootPubkeyGenError::InvalidAddress { .. })); - } -} diff --git a/crates/wallet-abi/src/tx_inclusion.rs b/crates/wallet-abi/src/tx_inclusion.rs deleted file mode 100644 index ea3951d..0000000 --- a/crates/wallet-abi/src/tx_inclusion.rs +++ /dev/null @@ -1,200 +0,0 @@ -//! Transaction inclusion verification using Merkle proofs for Liquid/Elements blocks. -//! -//! This module provides SPV (Simplified Payment Verification) functionality to prove -//! a transaction exists in a block without downloading all transactions. - -use simplicityhl::elements::hashes::{Hash, HashEngine}; -use simplicityhl::elements::{Block, TxMerkleNode, Txid}; - -/// Merkle proof: (`transaction_index`, `sibling_hashes`) -pub type MerkleProof = (usize, Vec); - -/// Constructs a Merkle inclusion proof (Merkle branch). -/// -/// For a transaction TXID in a block, using Bitcoin consensus Merkle tree construction rules -/// (pairwise double-SHA256 hashing with odd-hash duplication). -/// -/// Liquid inherits the same Merkle tree semantics via the Elements codebase: -/// -/// -/// Returns `None` if the transaction is not present in the block. -#[must_use] -pub fn merkle_branch(tx: &Txid, block: &Block) -> Option { - if block.txdata.is_empty() { - return None; - } - - let tx_index = block.txdata.iter().position(|t| &t.txid() == tx)?; - - Some((tx_index, build_merkle_branch(tx_index, block))) -} - -/// Verifies a Merkle inclusion proof (Merkle branch). -/// -/// For a transaction TXID against the given Merkle root using Bitcoin consensus Merkle tree rules -/// (pairwise double-SHA256 hashing with left/right ordering). -/// -/// Liquid inherits the same Merkle tree semantics via the Elements codebase: -/// -/// -/// Returns `true` if the proof commits the transaction to the given root. -#[must_use] -pub fn verify_tx(tx: &Txid, root: &TxMerkleNode, proof: &MerkleProof) -> bool { - root.eq(&compute_merkle_root_from_branch(tx, proof.0, &proof.1)) -} - -fn build_merkle_branch(tx_index: usize, block: &Block) -> Vec { - if block.txdata.is_empty() || block.txdata.len() == 1 { - return vec![]; - } - - let mut branch = vec![]; - let mut layer = block - .txdata - .iter() - .map(|tx| TxMerkleNode::from_raw_hash(*tx.txid().as_raw_hash())) - .collect::>(); - let mut index = tx_index; - - // Bottom-up traversal: pair nodes, hash parents, collect siblings along path to root - while layer.len() > 1 { - let mut next_layer = vec![]; - - for i in (0..layer.len()).step_by(2) { - let left = layer[i]; - let right = if i + 1 < layer.len() { - layer[i + 1] - } else { - layer[i] - }; - - let mut eng = TxMerkleNode::engine(); - eng.input(left.as_raw_hash().as_byte_array()); - eng.input(right.as_raw_hash().as_byte_array()); - - next_layer.push(TxMerkleNode::from_engine(eng)); - - if index / 2 == i / 2 { - let sibling = if index.is_multiple_of(2) { right } else { left }; - branch.push(sibling); - } - } - - index /= 2; - layer = next_layer; - } - - branch -} - -fn compute_merkle_root_from_branch( - tx: &Txid, - tx_index: usize, - branch: &[TxMerkleNode], -) -> TxMerkleNode { - let mut res = TxMerkleNode::from_raw_hash(*tx.as_raw_hash()); - let mut pos = tx_index; - - for leaf in branch { - let mut eng = TxMerkleNode::engine(); - - if pos & 1 == 0 { - eng.input(res.as_raw_hash().as_byte_array()); - eng.input(leaf.as_raw_hash().as_byte_array()); - } else { - eng.input(leaf.as_raw_hash().as_byte_array()); - eng.input(res.as_raw_hash().as_byte_array()); - } - res = TxMerkleNode::from_engine(eng); - - pos >>= 1; - } - - res -} - -#[cfg(test)] -mod test { - use super::*; - - /// Taken from rust-elements - /// - macro_rules! hex_deserialize( - ($e:expr) => ({ - use simplicityhl::elements::encode::deserialize; - - fn hex_char(c: char) -> u8 { - match c { - '0' => 0, - '1' => 1, - '2' => 2, - '3' => 3, - '4' => 4, - '5' => 5, - '6' => 6, - '7' => 7, - '8' => 8, - '9' => 9, - 'a' | 'A' => 10, - 'b' | 'B' => 11, - 'c' | 'C' => 12, - 'd' | 'D' => 13, - 'e' | 'E' => 14, - 'f' | 'F' => 15, - x => panic!("Invalid character {} in hex string", x), - } - } - - let mut ret = Vec::with_capacity($e.len() / 2); - let mut byte = 0; - for (ch, store) in $e.chars().zip([false, true].iter().cycle()) { - byte = (byte << 4) + hex_char(ch); - if *store { - ret.push(byte); - byte = 0; - } - } - deserialize(&ret).expect("deserialize object") - }); - ); - - const SIMPLE_BLOCK: &str = "\ - 00000020a66e4a4baff69735267346d12e59e8a0da848b593813554deb16a6f3\ - 6cd035e9aab0e2451724598471dd4e45f0dca40ca5f4ac62e61957e50925af08\ - 59891fcc8842805b020000000151000102000000010100000000000000000000\ - 00000000000000000000000000000000000000000000ffffffff03520101ffff\ - ffff0201230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a\ - 0d5de1b201000000000000000000016a01230f4f5d4b7c6fa845806ee4f67713\ - 459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000000000266a24aa21\ - a9ed94f15ed3a62165e4a0b99699cc28b48e19cb5bc1b1f47155db62d63f1e04\ - 7d45000000000000012000000000000000000000000000000000000000000000\ - 000000000000000000000000000000\ - "; - - fn fixture_block() -> Block { - let mut block: Block = hex_deserialize!(SIMPLE_BLOCK); - let coinbase = block.txdata[0].clone(); - block.txdata = vec![coinbase.clone(), coinbase.clone(), coinbase]; - - let tx = block.txdata[0].txid(); - let branch = build_merkle_branch(0, &block); - block.header.merkle_root = compute_merkle_root_from_branch(&tx, 0, &branch); - - block - } - - #[test] - fn test_merkle_branch_construction() { - let block = fixture_block(); - - assert_eq!(block.txdata.len(), 3); - - let tx = block.txdata[1].txid(); - let proof = merkle_branch(&tx, &block).expect("Failed to find tx in block"); - - assert!( - verify_tx(&tx, &block.header.merkle_root, &proof), - "Invalid merkle proof" - ); - } -} diff --git a/crates/wallet-abi/tests/data/test-tx-incl-block.hex b/crates/wallet-abi/tests/data/test-tx-incl-block.hex deleted file mode 100644 index e957c2b..0000000 --- a/crates/wallet-abi/tests/data/test-tx-incl-block.hex +++ /dev/null @@ -1 +0,0 @@ -000000207e3dba98460e4136659f0fccf3e59338dfe53ed5f094fb0bb94d771c48341854d875900105c87e5dd46c740cb1129c06f8f4007e868f61b25e37cffa946c718d8742805b01000000015100030200000001010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0201230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000009b64001976a914608c0ea8194a8ceb57f0196f44a6b48a54fc065988ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000000000266a24aa21a9ed8f8a98e5623643b24167266c2648ead4a50d18b0491c6f34e11398aaee0ca6e8000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000020000000001eb04b68e9a26d116046c76e8ff47332fb71dda90ff4bef5370f25226d3bc09fc0000000000feffffff0201230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b20100000002540bd71c001976a91448633e2c0ee9495dd3f9c43732c47f4702a362c888ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000000ce4000000000000020000000101f23ceddac67cfbbc997199daa651384d0746fb2a5482b8c8629ba8df4b788f75000000006b483045022100e0feb3e2f292000d67e24b821d87c9532230dac1de428d6a0068c9f416583abf02200e76f072788dd411b2327267cd91c6b1659809598cd4fae35be475efe1e4bbad01210201e15c23c021652d07c1557b607ea0379fca0462aca840d6c33c4d4927524547feffffff030b60424a423335923c15ae387d95d4f80d944722020bfa55b9f0a0e67579e3c13c081c4f215239c77456d121eb73bd9914a9a6398fe369b4eb8f88a5f78e257fcaa303301ee46349950886ae115c9556607fcda9381c2f72368f4b5286488c62aa0b081976a9148bb6c4d5814d43fefb9e330575e326632136389c88ac0bd436b0539f5497af792d7cb281f09b73d8a5abc198b3ce6239d79e68893e5e5d0923899fd35071ba8a209d85b556d5747b6c35539c3b2f8631a27c0d477a1f45a603d1d350b8cbf900f7666da66541bf6252fc4c162141ad49c670884c93c57db6ba1976a9148c7ab6e0fca387d03643d4846f708bf39d47c1e988ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000008e800000000000000000000043010001dc65ae13f76fde4a7172e0fb380b1a5cc8dc88eaa0659e638a25eac8ae30d79bf93eb7e487eeee323e4ac8e3a2fe6523bdeba6acce32b9b085f2286174c04655fd6c0a6020000000000000000178ad016b3e5d8165423e56d8b37e3eaee96009b2f970043ccf65d61b5c3c1e1ef343e0c479bdba442717dc861c9591566010240b9d4607efb9252a5fcef05edf640e0bb6b606729246ad07baa49d0d3b52042c65a03ca737744e45b2d2d6d177c36569ae9d6eb4437305b169bbc59f85cabff3bc49a2d6d08c177cce3121a509d3c47961bd22e35c932b79d4ec5ccaf913fac04034bfebdadbc4ff3127af96344b02ee6b967bb08326cbe6a4e1c924485e64a8c0fdf70b98c99f38acaa15aa0adb2b5b7335ed5502443891bcd657310347cbd928f40f38f1dec087a2b947c9cf7d304798f77bbc4a2c843796b2d49acce91de4e88a0a9c261277df28ffc3320d7f7d64790f592ddded48a1068ef88271395fa5606389ef90856ddd6bd6710a8d27e0147983b5dde2a7efae44e83ad02a3c3da04be43d5f2c05c205f1e17b48554c2177670f46dbb6600bd2e6c75dd5ea2e1072c5f22483dcf05d8124e3f9063a5ddb179a29c23a2d15d6e89f2192f03dae5938f66fcdcff000c5a96ffd2920f23881880af72153c96a56dd80c218bb48b44a18e54a8050ff32c869c1264ee574cdb4002f86e0779c724d11dc4a768dbec1bd22054886f1fdf2e7347e4c247b829159d1375f881c6ce0a5c4da8534000e7fec3a980afb1edc99b725c29de80f260dcf144c873bf589ae1812ef6cb05f2234f9c66c23e874a0d5d0dc52f2209e015bbcf74ee449a397f6b0318c915b7e58dea5904abbe35285e90ccf548ad1f3f52f60c3b19b3cd67644d633e68aef42d8ef1782f22a8edd0620f55f29070720ca7a078ac83e87b9ebd2783ecad17dd854ef1bbd319f1a6d3a1e4931f9097422f5a3c4af037b99e06c7610ee61102c6eea763af108e9a16b93b2dc0891658d5c6a197df6aae9b306b2c895d21c79cb6cb6dd85b4018b0a9fe7468336e3907eb4adcaf930cacc97e8e951d2d6b25744a4143679bad1f31b210c9a2ed54b80d8f5d7dc1f1c985681534c1926920cd683d95dca7e8ea285f9906d2e89cd8bfa76a98e38ee4b5152522d55f79610fe8d5278fe6ed5866b5da4dcf330ea84307c34f30e1a66eb1934dafebb0074fc27c2ff73d8c0bae8416cc87bf611f81119aba9e2a911beaf3ac9507e621fc1ed1cf15dfb31408cf55e2bfdd2880db2d3489a336d6f8348347648d882f9f376331e469e809115c6cc82468f363c910673e9ded172ded90a369e1cdd135676f623e11a1531ed221177812b1ef0c65e5ca92c0df8de7fe664710f3228a226e019c99607fe1395ecd5643e1c7ad8a132bf5131737cb970a7f0dabc00029755bf71b3f47bd69ba39b3ab104c74f04239f4919dca1dfce7c9c41cba9d449073e106ebabe3c313b598ee8b11702ec46e9ee53fb9422f0326371898b8fa4c21a951684c687398e0bebd6f6fd91b829e8666b9a19a4273cfda0f34b8ecb902f7adc6539fb9a0cba6f87a63a957acfb2dfa18973f4a3063668767b2be780311513c63f1814f082176f6a953f2ffaa49ec9b39fecc2eab603be7a969bb4c1dbebf8d39fa90f802d5ea52378b5025a19b64a8c2c2dd6a6133bd8d29730bd5724b5bf50c158b238d1137082937ad91a176aaf91577868db7581b457c917e612b242ce0065ad47e11dcdc1fc6158687142249bcf312497a547b6f43e795af7d4ae8cd022e44e417987e35e83de21e39dcdf86b97bd421e6e61881a432fa2284f20be80e32459443736b875d9036468ceb881589394441e2d10aa10b6c93332951e8ba56f89fac70baf415b4511873c0f3e418ca4fe8954a28f1f7b5f590d34470119f694e2712f184882d90396c8e6aa850eaa3c2ae51990543638c46c59512167a2c5ad593532dc2142ffb6560476e4159213b9ef017ec75310d2e4624a405bb26f7192a485a94890674928c9caa4a5819ca4ddcba8fa71afc1a6baf63f039452c8fe994f8b63d58c876dfddd61a476345eaed4f66bdc0fcfc38d485c6a5b0e27d0fbc50427ff591ba38d63445c01642cfbd7d4c032f2546a6fe80bc3b598362502c552049523fe360c3bcf1cc572feb04386f97d55871dd8cea0393cdd964e724082adc98126e6f2fe1d576be4bf911e9aca70e35538175f8382bbcd614bbecc97c9607ef25da2ff08a6e5b6f76cbe9ccb0e0fdc3528e3e2c3675a5c897d295bb76524ec8a73a70b97909368f44d92f9aceaef0b03f3dafa1faa89fc663a92da3c19b4952463fac0e825e78cf046e266cfb9975af72e9d50d2c2cafee88fe2cecae2b1465fc07b280d83b66062dc9e7a372f81aec8e0bb9e97877814a5a6813c67746e35cd068d45d8664528bd00d5a306a5319e1bea7f38345da92d3a10d91476a26aed6b8441f0f72fbbad5d5e0f8ae5cabc9f4f08e6be7902b5c53632db5264afee7422c87b3237a32d5213ad0eb807b61977d9d90666cbb0c70500526b0eb762c99351796db41166b0aa2f221b5607e0d629fac4e938488245c11557381a4f8addcc49913b11d42481cf8668e37bacbad4a20509e4fe4ccbcee7aea2909a2abe59052f7f28b9340cd92f69729d615b8d3b530941c0b30506498cd4e561a9c82d915266bb7115967bc76c5593c06d094bdf4294b868afc5fa52742d3bdbd5932df599f0e1187c49f0dba8679c771a514cc9da75e03506957800bf470d4a07c4bb8918d6085499bb8ceeaba23c0b465863327e9ab8b6b8cf8b3ca530ca7b02cfadf85437b750f305e8fbc8855c95bee8595a7e9e1f0993a03adbadc68665a18936cc99b6530b4518c0754990d7bfdfdac76f88cfcbcb7b3d9a71ee10cbd3a1bdbc2e50b642c1fef56511962f845bbec6eab727b1d4add335db8d80c4c07e8356ad05adad68b012489fa5bb5d9019a667778ddf7f5edd80f1d3c4abd64397a89e554c8007809336ddc2b2e7d5219c39fdf39aad33b9350f6b18fe3b98c690b9068f36d4b7669530fd216373842fbf70fe9bbe80854b31eed4bd515d6caeb065d6c609846c9bfae1b3fce3db70b5bfb448ec69512e7f25019c789301b77a75f2a0f81c65ec29f41bf96d597a00c310e8ba4b48ac82b5a735c1e83f22394eb2fc9b35d42a35533c938f26290a5860175637982f1733c99be39c44ac4a09187406306bde2fd3d28e4e7bda73719912c338804dea03987757dac4d73def665e11da126f9414f71624a3b753797eb0472bd334094515c4f9fe57fdd8d185f22b4bf82e4b5f6b800870cce19a0c8174dc11ee9f1cb9ffe0ac6f6fff1ebf7c915c7ae20172bb70390e3759912e0e0a4e83a0a2d2318f4386314a89f6438ccb331f89377ff7947fe4b24f788aef85c1656ca87ee41c959f1b09bde09f20c2a51ac481646b28e9b0fc2ff49cfe8cf28577bf5bf6f261f54f97fcd2875da4210c6dfe685450280b68e378d9a486243cc682ed4ec747c37de1fde848e4a8f70498d22e40c462c469c884cd67330e77b694e759232313f31a1624e0e1960f23ddae47b68ff553d0de0910c8abe2e8e5fb063aa744ff77465fc731c7af79a84dcaa9b3f741a46dd3c932877d49242c6d883e14392b8c4530986605812b636a73590ef437f27e40d1af37ed1cbd68fb4e9ca5b0b41e5daee0142c1bf59c9d71f6c19b25e6148dfbb9fb142107aabe3701e36611a7e0b13ea32d3c5f8a51f63c5f34415baa15f6ca77300eb323241ffe73c5acd97fcb682c21dc8911392979e9cb81be5218acf452b5b93f6681d323b7989fdd10efe6fe9e2ac88d0d76a4cf3ee45e3b5c430100014142c1fc7e8a658eff437594a25cf34d269556d8511918f27fdc7e9d6dd73f0e4790b91f225e9d131e6abb3dbfb66549a9aa57948fbd2f183fcd951b1d2305bffd6c0a602000000000000000016f5cdf9fb6c1b5e98a36befdc2c55bd4fd8793d554b2506f51c909362495e1216ee83cd270ddb0a00785600ba23bd3363f0798e3a7a117990415adec88e61be65170bd587ab4d2ee38edb22a91e5c29afa397dd5a73465c51c6263f5fbde47fa801ce84464acc32589acaafadfe44d6558774b7085612a88f3424b6dca3c6f07217d1cbd5c41bda46a6a492a0119c1de4d25b58c94250bee3fba6b8223777535673a2f4da6af27598030f88144f408120f07ca9c98d5d9edcdf6cdc9073f118fce55e6c9d0be80b5e87992ddaa9c22053b3a00d42bdedc9768de25c0b37a5c4fb4e86710b33cebed5588d88adde607f6bca14f0279ce35126d403ffa50f288c87f528c19749ed43bd846c513fcd92c173fe76d8f2e69770439d3d075cb19b1094a42ee07ae1de197e8c136e2bc688a75a74db24adb0fbb73872dc80074f61c9cce9bd33861bdd921ee3edacab1d6e7cec325c172b6b6e82ada11687e4fc931225074dd1f20a0f9342dbce1fc3fdbf5bb6cb74ab6475e574e9f5f247a2f7e4fcfcc354d4da8c8066e574642c7fccbbb9ef0aa592ecab5366fe87eb8e14cd64aee34578aa48f68f8f4c5372df2c3fc429f5a3e39ef6c034c87f9c52b2ea35e28c7bf3be737c3817efd6569466dc859e8ff8965c5249b6f045934d3d08b0ffd388aec58df8194ac2c4fec2152942d2626595e65664b1fa33b5dae8ee796a840a56d885cbf7ae6483fad05e507ada3f075ebce0d791b626c6dfe93f8492c4dd3b34aafc33d7644c5c8e38bfd8c19194f65be88fcb4538778632e489a626896372fdd2498b16e64daa7d3c5cfac688d6f9cdf3717261b0a1f25be1bdd6be6558ddb826fa04b5f668810a291aea51a6f05ff7c34dcf81c74849a8015bad5e4e416989b10ef01de304775db725fa0b665f4330dc9c540dc29aab144837362a97d6bb0165cb3272338c2d32386cd95ee3e66d876b591a25a6907237523cf908f736d2fdc8e54ea8d9c7562697161d1f72fc4d7b775052415cd0e5ae5bdf6edfab5776b6ff75ce5e1f8f2beea6ec74252b63966cca58abd638279dc5c998a1068079f3e5dcc8a69165c304c3d8c362ccfadab05ad12208a5655ab389eb727e8ed5f86b300331a13be26e2fbabf89fbfd2b98481dd5edb52ed456a0e03a84b6f89761f91ff251412f5cfa286e35fb9f48ef0e044c4742b6e860a08767ecb80548c2f3df3b371cdb40e86dbe118f64e84faf45ecb78d73364e9e31e3412ca2a3fad0a35983370ea9e6264a222edd1fd4aca30e3c169d7ca2d07609262e786ecd019c1417a06b7dfa32a54e0897afdc6492f26611555cbff47dba3b76381f239d597a8f687669333e0b47b53d5bcc4fea1919490bad3c6f0b6a58a50aca7ddeb9745ead454e0a38d9486fb52aefe0dbb92bf7fd6c215078aba3482b11274ec8cddff92c359bbc6d20bd823ad0bbf859cfaadf8e775b3d37b3078319f46c6d2a112cf60a673fee467538c70f1687d97fbe9d9f8a0856061592a4e00b6d10e979e674dd2cd0ba8b853f733877cd508062d5f723d58d215ad69c2be6be742496aef54eb87338622eb36a9bbc5a7a602d280a45e095b1e078dab54479e783a513c722066acaae44ccc15f9560da91ed053ec05c36d82f6809766876c45c4fbeb2321d50f48f7995437d0c5fc365974a571fb0352d28cb1cdbd21d69fab576a2e68d6b881776027bcdb7f01be22b1c847d91f26e680ef6ab2c128a89b59432383d9bd661b0b01432cf8a25319426d38ac2e2114825f59b4250569c798b1094920bb31130728313ff56a6eef2e6c4b275215dce3786d0f9024952b5f572566c53597e7ef4ab1f75743e605a564054d667f48906b5481d924769ef65751e349891d725a2c1bf8b102fea4c25c874d2fc2ce1bfec4b39bea76fbf7a28855725d52b595a4fc96892c3f1f961d46310ebd5221df729c02060035c559baf0fd7efa73a2213ca29642857aeb8ebf7efdf9d2f5c84746b6fc35ab355a8dca56e7dde4831e47ca1be6b62af30cfcf807c384e56ab84ff03bbe786251e6c4b932c9217bf671046217bd0511fdc06aa69050c1480281e4843eb73d80095a2fb8e68a2c0c98c9aea637b99d87ad847a3a76d59ea308c751f9cb4a4fce2989822bd6ba2f901f09df647536dc30730ea3160dd35b8c6dcc9aa815b79ed492a8a299a298ccdf784b9b0211ca877ec1723817c98529acaa4d3727162b5740b0fc9b498dfb2212a3cbf0c63dc4f7663fafad7905643a792862b651e8497b0f0da632b897ecf9ee63f2b20b54fa5eb2f2e424dcce5a075f50b856af266655be3a815fc83ed8027508b2536976982196b160e2219ffdb5c7a56dd3e6b700860c711f4439dbf72973f4f26fe3260ec43a3446fe14444b9787d877e107be610147eec4a3574745e95a1f424aff062f84c559d13b1e6b59e8dc2221515c229f07db8eb39c515a321d8bd07b1bd6c9a79dac6d951c04415553c7a2ce1eb77495c7f89c4d5b4cffd289435b69bc53585095083cc5a1b191781342266e204e1566aca8175e2ae84a8bd711d188b666dfb65a6442776d3e23c1b5192af09ec712537f2157d0ccbc1bb3b3a1969d9705671f16bdc266e615ad2e50a8cbd666f3ee7465cc430c6cd69d30c91e717b12f7094b6f0ef89134d6c1620d28d8f238c181146448b348e4ca2e93c737210350f18fb878fb91b70ecc5689e5b6101ecfc545f6a1c903115b0c6419c91a50fb2dbe2edd362f2815f0c75070974507c34130ac9b29747ff7efbe6e37ee4c62be3ecfedfa817fdf3309163aaff677775b77f0d288c9858cfe59cb0fa18afa591e7d574eaef43c82e79d71542c4177de4e5bd724b18cfd33c68530665728a9d5ef192772094acbf3d885d5146c1634e74754e3fbcb94fa349eac8280cfd7d1f46a0813b57a83bd078b1f7cb5a60a59b59380fe04e1c600c33b33d1add69a9ff1be546f0ec5c0083979fce940b23711f382ac0d011c1103f02cb6082c18e39cf7a9c3bf4c081f905ae7b87951a7880b57e934465ccd634e5a17fd8d8866abfdfebd33b2c3d2c5be58144900c04e9c18de0c80270660e62a3c185277555f89da4c41bd33cec1359f4ed21abdb586e1d97f720a92d16014d7f1822f1836f74c97cb7f7b38e073477c6ab064fde835916c1e624de81f2ad90f6260073c5e1848582860f033630bde225821b39c2572b30c36adf8fdb8317c33df05f6413447f4985d12e9012629df09dc8f43373a6d0db4b0048453a6f1ec662472c77a30d5cf4ac7084f736d0d598c251f2aefc986052fbf12a657885d7140ad36b07c63ab86388a2be12d943747f3f29ef9f2e11e1444cc873df0ed7826eef675389a0d5a0388a8504fe89c4791ea4a572bfd406d5f01418b4f888c9a7a566e32811936bf6950bbf786b86c41c28f2045d31953fcd15f179e7bc00c72870890537921f7deff82270b0e44b88720aa738f60a85567deb7c90b0c2444467621e53e1c079436d31d3d0b34dd237fc281eb9d87175237a9a433142db4bb7f8c4cb6a34e2dc73f074045d216695ce88ef68e18564c935c9cbd902e939655c258de2ab78def8746bffd972083afce3b6881b7147262e1a44e0224689fafa1a3cb823c8da6eb7df091bec0638bf728b7b10aa95f2bce512ec8d3252938d2eb77b44ace7a2f976588032cac5af670f9e5ca25cb0721bc1baec26f9c3a9f41b02fb62997d6cb0a01314845e9d0e78139ea49f2ead8736e0000 \ No newline at end of file diff --git a/crates/wallet-abi/tests/data/tx_with_issuance_token.hex b/crates/wallet-abi/tests/data/tx_with_issuance_token.hex deleted file mode 100644 index a1906ef..0000000 --- a/crates/wallet-abi/tests/data/tx_with_issuance_token.hex +++ /dev/null @@ -1 +0,0 @@ -020000000002414ff757f067bb505e5ec11c8cb4952fe5f9b31b86de7f0db658718123aa3a50000000800000000000000000000000000000000000000000000000000000000000000000000000000016f4e8decfb1463737260a050dd12ba218738efe1f35a6366f23fb992f71dc1100010000000000000001414ff757f067bb505e5ec11c8cb4952fe5f9b31b86de7f0db658718123aa3a50010000800000000000000000000000000000000000000000000000000000000000000000000000000016f4e8decfb1463737260a050dd12ba218738efe1f35a6366f23fb992f71dc110001000000000000000104016beed2fe2e094458270e632d2dd573a1bd4f975c29e3965d1521c6cf6c35055b010000000000000001002251203451c2c04047ab5f2d3eb747773e5a9756c10eb9bf31326bd16a6533142f1ace0120f047ced3c1c10237762dabd7185e983d97b3a0115961a182653cbed5347922010000000000000001002251203451c2c04047ab5f2d3eb747773e5a9756c10eb9bf31326bd16a6533142f1ace01499a818545f6bae39fc03b637f2a4e1e64e590cac1bc3a6f6d71aa4443654c140100000000000b1bd700225120f47f7cc53cdc7df3731230fb7030f0912a6e233b1d9c552c8d4c67ee4ae67a1801499a818545f6bae39fc03b637f2a4e1e64e590cac1bc3a6f6d71aa4443654c14010000000000000001000000000000 \ No newline at end of file From f27cfa1daa5735a4c1072e16c792c81f2fabcbe4 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Mon, 11 May 2026 12:34:57 +0300 Subject: [PATCH 3/5] general: update README to reflect new structure --- README.md | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 81f726a..f853fc2 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,20 @@ # Simplicity Contracts Workspace -This workspace contains reference implementations and tooling for working with [Simplicity HL](https://github.com/BlockstreamResearch/simfony) contracts on Elements/Liquid. +This workspace contains reference implementations for working with [Simplicity HL](https://github.com/BlockstreamResearch/simfony) contracts on Elements/Liquid. ## Workspace Crates - [`contracts`](crates/contracts): Contract templates and helpers (finance + state management modules). -- [`wallet-abi`](crates/wallet-abi): Schema-first wallet runtime and ABI layer used by contract flows. -- [`cli`](crates/cli): `simplicity-cli` binary crate for wallet and option-offer command flows. ## Repository Structure -- Simplicity sources: [`crates/contracts/src/**/source_simf/*.simf`](crates/contracts/src) +- Simplicity sources: [`crates/contracts/src/**/*.simf`](crates/contracts) - Contract-side Rust helpers: [`crates/contracts/src`](crates/contracts/src) -- Wallet runtime and schemas: [`crates/wallet-abi/src/runtime`](crates/wallet-abi/src/runtime), [`crates/wallet-abi/src/schema`](crates/wallet-abi/src/schema) -- CLI entrypoint and commands: [`crates/cli/src/main.rs`](crates/cli/src/main.rs), [`crates/cli/src/commands`](crates/cli/src/commands) ## Getting Started - Contract crate usage and module overview: [contracts README](crates/contracts/README.md) -- CLI usage and command examples: [CLI README](crates/cli/README.md) ## Notes This repository is reference-oriented. Copying and adapting modules into your own project is expected while Simplicity tooling/import ergonomics are still evolving. - -## Old structure - -Use [this version (116b0eb)](https://github.com/BlockstreamResearch/simplicity-contracts/commit/116b0eb17c3fd84e302d5288ea884c46ba702b77) -of the repo to interact with the pre-wallet-abi introduction (the last version of simplicityhl-core is here) \ No newline at end of file From d9644838f94daeceaee279c34d5f1438d5475d4e Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Mon, 11 May 2026 12:35:35 +0300 Subject: [PATCH 4/5] general: update ci to use Simplex --- .github/workflows/lint.yml | 15 ++++++++++++--- .github/workflows/tests.yml | 34 +++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 069702d..0046f2e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,15 +25,24 @@ jobs: toolchain: stable components: clippy, rustfmt + - name: Install Simplex + run: | + export SIMPLEX_DIR="$HOME/.simplex" + curl -L https://raw.githubusercontent.com/BlockstreamResearch/smplx/master/simplexup/install | bash + echo "$SIMPLEX_DIR/bin" >> "$GITHUB_PATH" + "$SIMPLEX_DIR/bin/simplexup" --install v0.0.4 --platform linux --arch amd64 + - name: Cache cargo uses: Swatinem/rust-cache@v2 with: cache-on-failure: true + - name: Generate contract artifacts + working-directory: crates/contracts + run: simplex build + - name: Format check run: cargo fmt --all --check - name: Clippy - run: cargo clippy --workspace --all-targets --all-features -- -D warnings - - + run: cargo clippy --workspace --all-targets --all-features -- -D warnings -D clippy::all -D clippy::pedantic -D clippy::nursery -D clippy::cargo -A clippy::multiple_crate_versions diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 80e9664..6cae5c0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - toolchain: ["1.90.0", "stable"] + toolchain: ["1.91.0", "stable"] steps: - name: Checkout @@ -28,22 +28,27 @@ jobs: with: toolchain: ${{ matrix.toolchain }} + - name: Install Simplex + run: | + export SIMPLEX_DIR="$HOME/.simplex" + curl -L https://raw.githubusercontent.com/BlockstreamResearch/smplx/master/simplexup/install | bash + echo "$SIMPLEX_DIR/bin" >> "$GITHUB_PATH" + "$SIMPLEX_DIR/bin/simplexup" --install v0.0.4 --platform linux --arch amd64 + - name: Cache cargo uses: Swatinem/rust-cache@v2 with: cache-on-failure: true + - name: Generate contract artifacts + working-directory: crates/contracts + run: simplex build + - name: Build run: cargo build --workspace --all-features --verbose - - name: Targeted wallet-abi library tests - run: cargo test -p wallet-abi --lib --verbose - - - name: Targeted CLI unit tests - run: cargo test -p cli --lib --verbose - - name: Test (excluding regtest-dependent contracts) - run: cargo test --workspace --all-features --exclude contracts --no-fail-fast --verbose + run: cargo test --workspace --all-features --lib --no-fail-fast --verbose contracts-regtest: name: Contracts regtest tests @@ -58,11 +63,22 @@ jobs: with: toolchain: stable + - name: Install Simplex + run: | + export SIMPLEX_DIR="$HOME/.simplex" + curl -L https://raw.githubusercontent.com/BlockstreamResearch/smplx/master/simplexup/install | bash + echo "$SIMPLEX_DIR/bin" >> "$GITHUB_PATH" + "$SIMPLEX_DIR/bin/simplexup" --install v0.0.4 --platform linux --arch amd64 + - name: Cache cargo uses: Swatinem/rust-cache@v2 with: cache-on-failure: true + - name: Generate contract artifacts + working-directory: crates/contracts + run: simplex build + - name: Setup regtest binaries run: | set -euo pipefail @@ -89,4 +105,4 @@ jobs: echo "ELEMENTSD_EXEC=${BIN_DIR}/elements-${ELEMENTSD_VERSION}/bin/elementsd" >> "$GITHUB_ENV" - name: Test contracts (regtest) - run: cargo test -p contracts --all-features --no-fail-fast --verbose -- --test-threads=1 + run: simplex test From f43d3c78dae0e463258dea6755948e0146a3d966 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Mon, 11 May 2026 12:46:53 +0300 Subject: [PATCH 5/5] readme: mention SimplicityHL Core Package --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index f853fc2..0b07da5 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,12 @@ This workspace contains reference implementations for working with [Simplicity H ## Notes This repository is reference-oriented. Copying and adapting modules into your own project is expected while Simplicity tooling/import ergonomics are still evolving. + +## SimplicityHL Core Package + +Reference: https://crates.io/crates/simplicityhl-core + +This package was previously used to help early adopters of Simplicity HL move faster when building with Simplicity. +It has now been yanked because most of its functionality is available and maintained in: +- https://github.com/Blockstream/lwk +- https://github.com/BlockstreamResearch/smplx \ No newline at end of file