From 4fdbf5b4f98da93fed4dd93c9a8ff91c287d04e7 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Thu, 9 Jul 2026 13:24:26 +0300 Subject: [PATCH] general: move to stable simplex 0.0.8, dedupe tests, track artifacts --- .github/workflows/lint.yml | 7 +- .github/workflows/tests.yml | 4 +- CONTRIBUTING.md | 79 +++ Cargo.lock | 187 ++++-- Cargo.toml | 11 +- README.md | 11 +- crates/contracts/.gitignore | 1 - crates/contracts/Cargo.toml | 3 - crates/contracts/README.md | 27 +- crates/contracts/src/artifacts/mod.rs | 7 + .../contracts/src/artifacts/option_offer.rs | 64 ++ crates/contracts/src/artifacts/options.rs | 64 ++ .../src/artifacts/simf/option_offer.simf | 104 ++++ .../contracts/src/artifacts/simf/options.simf | 265 +++++++++ crates/contracts/src/lib.rs | 8 +- crates/contracts/src/programs/mod.rs | 2 + crates/contracts/src/programs/option_offer.rs | 3 + crates/contracts/src/programs/options.rs | 3 + .../contracts/src/simplicityhl_core/error.rs | 39 -- crates/contracts/src/simplicityhl_core/mod.rs | 3 + .../contracts/src/simplicityhl_core/runner.rs | 12 +- .../src/simplicityhl_core/scripts.rs | 88 ++- .../state_management/array_tr_storage/mod.rs | 96 +-- .../bytes32_tr_storage/mod.rs | 81 +-- crates/contracts/src/state_management/mod.rs | 1 + .../state_management/simple_storage/mod.rs | 15 +- crates/contracts/tests/common/filters.rs | 44 +- crates/contracts/tests/common/mod.rs | 34 ++ .../tests/program_builder/option_offer.rs | 218 ++++++- .../tests/program_builder/options.rs | 140 ++++- .../tests/regtest/option_offer/deposit.rs | 47 +- .../tests/regtest/option_offer/exercise.rs | 374 +++--------- .../tests/regtest/option_offer/expiry.rs | 169 ++---- .../tests/regtest/option_offer/withdraw.rs | 561 ++++-------------- .../contracts/tests/regtest/options/cancel.rs | 181 ++---- .../tests/regtest/options/exercise.rs | 345 ++++------- .../contracts/tests/regtest/options/expiry.rs | 273 +++------ .../contracts/tests/regtest/options/fund.rs | 68 +-- .../tests/regtest/options/settlement.rs | 455 +++----------- 39 files changed, 1901 insertions(+), 2193 deletions(-) delete mode 100644 crates/contracts/.gitignore create mode 100644 crates/contracts/src/artifacts/mod.rs create mode 100644 crates/contracts/src/artifacts/option_offer.rs create mode 100644 crates/contracts/src/artifacts/options.rs create mode 100644 crates/contracts/src/artifacts/simf/option_offer.simf create mode 100644 crates/contracts/src/artifacts/simf/options.simf diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bb4fa51..88a96b5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,7 +30,7 @@ jobs: 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.6 --platform linux --arch amd64 + "$SIMPLEX_DIR/bin/simplexup" --install v0.0.8 --platform linux --arch amd64 - name: Cache cargo uses: Swatinem/rust-cache@v2 @@ -41,6 +41,11 @@ jobs: working-directory: crates/contracts run: simplex build + - name: Check committed artifacts are up to date + run: | + git add -N crates/contracts/src/artifacts + git diff --exit-code -- crates/contracts/src/artifacts + - name: Format check run: cargo fmt --all --check diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ad43fc3..7ec653b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,7 +33,7 @@ jobs: 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.6 --platform linux --arch amd64 + "$SIMPLEX_DIR/bin/simplexup" --install v0.0.8 --platform linux --arch amd64 - name: Cache cargo uses: Swatinem/rust-cache@v2 @@ -68,7 +68,7 @@ jobs: 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.6 --platform linux --arch amd64 + "$SIMPLEX_DIR/bin/simplexup" --install v0.0.8 --platform linux --arch amd64 - name: Cache cargo uses: Swatinem/rust-cache@v2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4860b3..4adb0d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,82 @@ +# Contributing + +## Development Setup + +### Prerequisites + +- Rust 1.91.0 or later (CI tests 1.91.0 and stable). +- The [Simplex](https://github.com/BlockstreamResearch/smplx) CLI, pinned to + the same version as CI (see `.github/workflows/tests.yml`): + + ```sh + curl -L https://raw.githubusercontent.com/BlockstreamResearch/smplx/master/simplexup/install | bash + simplexup --install v0.0.8 + ``` + +### Building + +```sh +cargo build --workspace --all-features +``` + +Contract artifacts (`crates/contracts/src/artifacts`) are generated from the +SimplicityHL sources in `crates/contracts/simf` and are checked in, so a plain +`cargo build` works out of the box. Whenever you change a `.simf` source (or +bump the Simplex toolchain), regenerate and commit them: + +```sh +cd crates/contracts +simplex build +``` + +CI regenerates the artifacts and fails if the committed ones are stale. + +### Testing + +Unit tests need no external services: + +```sh +cargo test --workspace --all-features --lib +``` + +The regtest suite (`crates/contracts/tests`) runs against local `elementsd` +and `electrs` instances and is driven by Simplex: + +```sh +export ELEMENTSD_EXEC=/path/to/elementsd +export ELECTRS_LIQUID_EXEC=/path/to/electrs +simplex test +``` + +See the "Setup regtest binaries" step in `.github/workflows/tests.yml` for +the exact binaries CI downloads. + +### Linting + +CI enforces formatting and a strict clippy profile: + +```sh +cargo fmt --all --check +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 +``` + +## Repository Layout + +- `crates/contracts/simf/`: SimplicityHL sources for the finance contracts; + `simplex build` compiles them into the generated (and committed) + `crates/contracts/src/artifacts`. +- `crates/contracts/src/programs/`: Rust wrappers around the generated + finance contract artifacts (options, option offer). +- `crates/contracts/src/state_management/`: standalone storage contract + examples; each embeds its own source from a `source_simf/` directory and is + gated behind a feature flag. +- `crates/contracts/src/simplicityhl_core/`: shared compile/execute/taproot + helpers. +- `crates/contracts/tests/`: regtest scenarios (`regtest/`), transaction + builders (`program_builder/`), and shared test utilities (`common/`). + # PR Structure All changes must be submitted in the form of pull requests. Direct pushes diff --git a/Cargo.lock b/Cargo.lock index 457f4d9..cca184b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,29 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + [[package]] name = "base58ck" version = "0.1.0" @@ -325,6 +348,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -385,6 +410,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -397,8 +431,6 @@ version = "0.1.0" dependencies = [ "anyhow", "lwk_common", - "serde", - "serde_json", "smplx-std", "thiserror", ] @@ -468,6 +500,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -567,6 +605,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "generic-array" version = "0.14.7" @@ -765,6 +809,16 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.2", + "libc", +] + [[package]] name = "js-sys" version = "0.3.95" @@ -782,7 +836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf" dependencies = [ "base64 0.13.1", - "minreq", + "minreq 2.14.1", "serde", "serde_json", ] @@ -866,6 +920,16 @@ name = "minreq" version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "minreq" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659579df697b372ef9e36f02fcbb41f6d6f157dcec7db9c9618fa0f23cf0fc20" dependencies = [ "rustls", "rustls-webpki", @@ -927,6 +991,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "polyval" version = "0.6.2" @@ -1031,18 +1101,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.14", - "regex-syntax 0.8.10", -] - [[package]] name = "regex-automata" version = "0.3.9" @@ -1119,23 +1177,37 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.12" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ + "aws-lc-rs", "log", - "ring", + "once_cell", + "rustls-pki-types", "rustls-webpki", - "sct", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", + "rustls-pki-types", "untrusted", ] @@ -1154,25 +1226,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "santiago" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de36022292bc2086eb8f55bffa460fef3475e4459b478820711f4c421feb87ec" -dependencies = [ - "regex", -] - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "secp256k1" version = "0.29.1" @@ -1294,9 +1347,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simplicity-lang" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e57bd4d84853974a212eab24ed89da54f49fbccf5e33e93bcd29f0a6591cd5" +checksum = "13ed081e3046d66c146d7201bcbf3b655ca3436cb83f6efc26d7895bd2b79d06" dependencies = [ "bitcoin", "bitcoin_hashes", @@ -1306,15 +1359,14 @@ dependencies = [ "ghost-cell", "hex-conservative", "miniscript", - "santiago", "simplicity-sys", ] [[package]] name = "simplicity-sys" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3401ee7331f183a5458c0f5a4b3d5d00bde0fd12e2e03728c537df34efae289" +checksum = "96d1ec5477c7650b8ef511aa56dccb28f2e8cdb6e87f260ecffdaf0ebfef2d3b" dependencies = [ "bitcoin_hashes", "cc", @@ -1322,9 +1374,9 @@ dependencies = [ [[package]] name = "simplicityhl" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25de8990174fe3e1a843df138cacc4265d05839ebd2550c18b9196f567d55e81" +checksum = "361316795ec753230c421d964ab60940d20d5252c2d39272452528832c9e0ec5" dependencies = [ "base64 0.21.7", "chumsky", @@ -1340,8 +1392,9 @@ dependencies = [ [[package]] name = "smplx-build" -version = "0.0.6" -source = "git+https://github.com/BlockstreamResearch/smplx?branch=dev#1c0ca5fc0de828c6bc9c35a6a26a8b86d48ffaf4" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a9a38dc0d803acdeed6217cdf9c1b3ded9220796315a458f47572269abaf2f" dependencies = [ "glob", "globwalk", @@ -1358,8 +1411,9 @@ dependencies = [ [[package]] name = "smplx-macros" -version = "0.0.6" -source = "git+https://github.com/BlockstreamResearch/smplx?branch=dev#1c0ca5fc0de828c6bc9c35a6a26a8b86d48ffaf4" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e1091132a1f5157111920504e403fc8a4eb8a7814a376e4bd4c55a7fb6edff" dependencies = [ "smplx-build", "smplx-test", @@ -1368,8 +1422,9 @@ dependencies = [ [[package]] name = "smplx-regtest" -version = "0.0.6" -source = "git+https://github.com/BlockstreamResearch/smplx?branch=dev#1c0ca5fc0de828c6bc9c35a6a26a8b86d48ffaf4" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ccf7e3eade934e890bcee3558af658de46177dbd6a5dd0302bb1a64b7701bbe" dependencies = [ "electrsd", "hex", @@ -1383,16 +1438,17 @@ dependencies = [ [[package]] name = "smplx-sdk" -version = "0.0.6" -source = "git+https://github.com/BlockstreamResearch/smplx?branch=dev#1c0ca5fc0de828c6bc9c35a6a26a8b86d48ffaf4" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d06536b493296672867c1a1ea8d67538c233af72624c18694ecb50691fcb74" dependencies = [ "bip39", "bitcoin_hashes", + "bitcoincore-rpc", "dyn-clone", - "electrsd", "elements-miniscript", "hex", - "minreq", + "minreq 3.0.0", "serde", "serde_json", "sha2", @@ -1402,8 +1458,9 @@ dependencies = [ [[package]] name = "smplx-std" -version = "0.0.6" -source = "git+https://github.com/BlockstreamResearch/smplx?branch=dev#1c0ca5fc0de828c6bc9c35a6a26a8b86d48ffaf4" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "834fd437c9394610ece857755d5967124a0b97c6837bf1170ae8deed38ba4d32" dependencies = [ "either", "serde", @@ -1415,8 +1472,9 @@ dependencies = [ [[package]] name = "smplx-test" -version = "0.0.6" -source = "git+https://github.com/BlockstreamResearch/smplx?branch=dev#1c0ca5fc0de828c6bc9c35a6a26a8b86d48ffaf4" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82bf119d89ca2b2ac992377912efe7dd1915e0aa6a193ca5383fb8a3da86594c" dependencies = [ "electrsd", "proc-macro2", @@ -1729,9 +1787,12 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.4" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "which" diff --git a/Cargo.toml b/Cargo.toml index 17e773d..8f928b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,13 +12,6 @@ edition = "2024" multiple_crate_versions = "allow" [workspace.dependencies] -serde = "1" -serde_json = "1" +smplx-std = "0.0.8" -tracing = { version = "0.1.41" } - -minreq = { version = "2.14.1", features = ["https", "json-using-serde"]} - -smplx-std = { git = "https://github.com/BlockstreamResearch/smplx", branch = "dev" } - -lwk_common = "0.17.0" \ No newline at end of file +lwk_common = "0.17.0" diff --git a/README.md b/README.md index 0b07da5..01d06f5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Simplicity Contracts Workspace -This workspace contains reference implementations for working with [Simplicity HL](https://github.com/BlockstreamResearch/simfony) contracts on Elements/Liquid. +This workspace contains reference implementations for working with [SimplicityHL](https://github.com/BlockstreamResearch/simfony) contracts on Elements/Liquid, built on the [Simplex](https://github.com/BlockstreamResearch/smplx) toolchain. ## Workspace Crates @@ -8,11 +8,14 @@ This workspace contains reference implementations for working with [Simplicity H ## Repository Structure -- Simplicity sources: [`crates/contracts/src/**/*.simf`](crates/contracts) +- Finance contract sources: [`crates/contracts/simf`](crates/contracts/simf) — compiled by `simplex build` into the generated [`crates/contracts/src/artifacts`](crates/contracts/src/artifacts) +- State management contract sources: `crates/contracts/src/state_management/*/source_simf` - Contract-side Rust helpers: [`crates/contracts/src`](crates/contracts/src) +- Regtest scenarios and test utilities: [`crates/contracts/tests`](crates/contracts/tests) ## Getting Started +- Development setup (Simplex install, building, testing): [CONTRIBUTING.md](CONTRIBUTING.md) - Contract crate usage and module overview: [contracts README](crates/contracts/README.md) ## Notes @@ -23,7 +26,7 @@ This repository is reference-oriented. Copying and adapting modules into your ow 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. +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 +- https://github.com/BlockstreamResearch/smplx diff --git a/crates/contracts/.gitignore b/crates/contracts/.gitignore deleted file mode 100644 index cbd40b0..0000000 --- a/crates/contracts/.gitignore +++ /dev/null @@ -1 +0,0 @@ -src/artifacts \ No newline at end of file diff --git a/crates/contracts/Cargo.toml b/crates/contracts/Cargo.toml index 0ed1ff3..2ba1a77 100644 --- a/crates/contracts/Cargo.toml +++ b/crates/contracts/Cargo.toml @@ -21,9 +21,6 @@ workspace = true [dependencies] thiserror = "2" -serde = { workspace = true } -serde_json = { workspace = true } - smplx-std = { workspace = true } lwk_common = { workspace = true } diff --git a/crates/contracts/README.md b/crates/contracts/README.md index 39f8543..e7dd535 100644 --- a/crates/contracts/README.md +++ b/crates/contracts/README.md @@ -1,14 +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). +and [SimplicityHL](https://github.com/BlockstreamResearch/simfony). + +The generated [`src/artifacts`](src/artifacts) module is compiled from the +SimplicityHL sources in [`simf/`](simf) and checked in; rerun `simplex build` +in this directory after changing a `.simf` source (see +[CONTRIBUTING.md](../../CONTRIBUTING.md) for setup). Current contract modules in this crate: -- 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) +- Finance (Rust wrappers in [`src/programs`](src/programs), sources in [`simf/`](simf)): + - [Options](src/programs/options.rs) + - [Options Offer](src/programs/option_offer.rs) + +- State management (each behind a feature flag, enabled by default; sources + embedded from each module's `source_simf/` directory): + - [Simple Storage](src/state_management/simple_storage) — `simple-storage` + - [Bytes32 Taproot Storage](src/state_management/bytes32_tr_storage) — `bytes32-tr-storage` + - [Array Taproot Storage](src/state_management/array_tr_storage) — `array-tr-storage` > [!NOTE] > Sparse Merkle Tree Storage was removed from the crate. The implementation @@ -19,9 +29,10 @@ Current contract modules in this crate: > For more details, see > [PR #72](https://github.com/BlockstreamResearch/simplicity-contracts/pull/72). -- Finance: - - [Options](src/programs/options.rs) - - [Options Offer](src/programs/option_offer.rs) +## Testing + +Unit tests run with `cargo test --lib`. The scenarios in [`tests/`](tests) +require a local regtest environment and are driven by `simplex test`. ## License diff --git a/crates/contracts/src/artifacts/mod.rs b/crates/contracts/src/artifacts/mod.rs new file mode 100644 index 0000000..218ab71 --- /dev/null +++ b/crates/contracts/src/artifacts/mod.rs @@ -0,0 +1,7 @@ +// This file is @generated by Simplex. Do not edit manually. + +#![allow(clippy::all)] +#[rustfmt::skip] +pub mod options; +#[rustfmt::skip] +pub mod option_offer; diff --git a/crates/contracts/src/artifacts/option_offer.rs b/crates/contracts/src/artifacts/option_offer.rs new file mode 100644 index 0000000..b92d0d8 --- /dev/null +++ b/crates/contracts/src/artifacts/option_offer.rs @@ -0,0 +1,64 @@ +// This file is @generated by Simplex. Do not edit manually. + +use simplex::include_simf; +use simplex::program::{ArgumentsTrait, Program}; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +pub struct OptionOfferProgram { + program: Program, +} +impl OptionOfferProgram { + pub const SOURCE: &'static str = derived_option_offer::OPTION_OFFER_CONTRACT_SOURCE; + #[must_use] + pub fn new(arguments: impl ArgumentsTrait + 'static) -> Self { + Self { + program: Program::new(Self::SOURCE, Box::new(arguments)), + } + } + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); + self + } + #[must_use] + pub fn with_storage_capacity(mut self, capacity: usize) -> Self { + self.program = self.program.with_storage_capacity(capacity); + self + } + #[must_use] + pub fn set_storage_at(&mut self, index: usize, new_value: [u8; 32]) { + self.program.set_storage_at(index, new_value); + } + #[must_use] + pub fn get_storage_len(&self) -> usize { + self.program.get_storage_len() + } + #[must_use] + pub fn get_storage(&self) -> &[[u8; 32]] { + self.program.get_storage() + } + #[must_use] + pub fn get_storage_at(&self, index: usize) -> [u8; 32] { + self.program.get_storage_at(index) + } + #[must_use] + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.program.get_script_pubkey(network) + } + #[must_use] + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + self.program.get_script_hash(network) + } +} +impl AsRef for OptionOfferProgram { + fn as_ref(&self) -> &Program { + &self.program + } +} +impl AsMut for OptionOfferProgram { + fn as_mut(&mut self) -> &mut Program { + &mut self.program + } +} +include_simf!("src/artifacts/simf/option_offer.simf"); diff --git a/crates/contracts/src/artifacts/options.rs b/crates/contracts/src/artifacts/options.rs new file mode 100644 index 0000000..094037c --- /dev/null +++ b/crates/contracts/src/artifacts/options.rs @@ -0,0 +1,64 @@ +// This file is @generated by Simplex. Do not edit manually. + +use simplex::include_simf; +use simplex::program::{ArgumentsTrait, Program}; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +pub struct OptionsProgram { + program: Program, +} +impl OptionsProgram { + pub const SOURCE: &'static str = derived_options::OPTIONS_CONTRACT_SOURCE; + #[must_use] + pub fn new(arguments: impl ArgumentsTrait + 'static) -> Self { + Self { + program: Program::new(Self::SOURCE, Box::new(arguments)), + } + } + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); + self + } + #[must_use] + pub fn with_storage_capacity(mut self, capacity: usize) -> Self { + self.program = self.program.with_storage_capacity(capacity); + self + } + #[must_use] + pub fn set_storage_at(&mut self, index: usize, new_value: [u8; 32]) { + self.program.set_storage_at(index, new_value); + } + #[must_use] + pub fn get_storage_len(&self) -> usize { + self.program.get_storage_len() + } + #[must_use] + pub fn get_storage(&self) -> &[[u8; 32]] { + self.program.get_storage() + } + #[must_use] + pub fn get_storage_at(&self, index: usize) -> [u8; 32] { + self.program.get_storage_at(index) + } + #[must_use] + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.program.get_script_pubkey(network) + } + #[must_use] + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + self.program.get_script_hash(network) + } +} +impl AsRef for OptionsProgram { + fn as_ref(&self) -> &Program { + &self.program + } +} +impl AsMut for OptionsProgram { + fn as_mut(&mut self) -> &mut Program { + &mut self.program + } +} +include_simf!("src/artifacts/simf/options.simf"); diff --git a/crates/contracts/src/artifacts/simf/option_offer.simf b/crates/contracts/src/artifacts/simf/option_offer.simf new file mode 100644 index 0000000..073bf83 --- /dev/null +++ b/crates/contracts/src/artifacts/simf/option_offer.simf @@ -0,0 +1,104 @@ +mod unit_0 { + fn divmod_eq(a: u64, b: u64, expected_q: u64) { + let (q, r): (u64, u64) = jet::div_mod_64(a, b); + assert!(jet::eq_64(q, expected_q)); + assert!(jet::eq_64(r, 0)); + } + fn get_input_script_hash(index: u32) -> u256 { + unwrap(jet::input_script_hash(index))} + fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount)} + fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount)} + fn ensure_zero_bit(bit: bool) { + assert!(jet::eq_1(::into(bit), 0)); + } + fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { + let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); + assert!(jet::eq_256(asset, expected_bits)); + assert!(jet::eq_64(amount, expected_amount)); + } + fn ensure_output_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); + } + fn ensure_input_and_output_script_hash_eq(index: u32) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); + } + fn check_user_signature(sig: Signature) { + let msg: u256 = jet::sig_all_hash(); + jet::bip_0340_verify((param::USER_PUBKEY, msg), sig); + } + fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { + let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); + match is_change_needed{ + false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), + true => { + ensure_input_and_output_script_hash_eq(index); + let (carry, asset_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); + ensure_zero_bit(carry); + ensure_output_asset_with_amount_eq(index, asset_id, asset_change); + } + , + }} + fn exercise_path(collateral_amount: u64, is_change_needed: bool) { + assert!(jet::le_32(jet::current_index(), 1)); + let expected_covenant_script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); + let premium_amount_u128: u128 = jet::multiply_64(collateral_amount, param::PREMIUM_PER_COLLATERAL); + let (left_part, premium_amount): (u64, u64) = dbg!(::into(premium_amount_u128)); + assert!(jet::eq_64(left_part, 0)); + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_covenant_script_hash, is_change_needed); + ensure_correct_change_at_index(1, param::PREMIUM_ASSET_ID, premium_amount, expected_covenant_script_hash, is_change_needed); + let (settlement_output_index, collateral_output_index, premium_output_index): (u32, u32, u32) = match is_change_needed{ + false => (0, 1, 2), + true => (2, 3, 4), + }; + ensure_output_script_hash_eq(settlement_output_index, expected_covenant_script_hash); + let (output_asset, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(settlement_output_index); + assert!(jet::eq_256(output_asset, param::SETTLEMENT_ASSET_ID)); + divmod_eq(settlement_amount, param::COLLATERAL_PER_CONTRACT, collateral_amount); + ensure_output_asset_with_amount_eq(collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(premium_output_index, param::PREMIUM_ASSET_ID, premium_amount); + } + fn withdraw_path(sig: Signature) { + assert!(jet::eq_32(jet::current_index(), 0)); + let (input_asset, input_amount): (u256, u64) = get_input_explicit_asset_amount(0); + assert!(jet::eq_256(input_asset, param::SETTLEMENT_ASSET_ID)); + check_user_signature(sig); + ensure_output_asset_with_amount_eq(0, param::SETTLEMENT_ASSET_ID, input_amount); + } + fn expiry_path(sig: Signature) { + jet::check_lock_time(param::EXPIRY_TIME); + assert!(jet::le_32(jet::current_index(), 1)); + let expected_covenant_script_hash: u256 = get_input_script_hash(0); + assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); + let (collateral_asset, collateral_amount): (u256, u64) = get_input_explicit_asset_amount(0); + assert!(jet::eq_256(collateral_asset, param::COLLATERAL_ASSET_ID)); + let (premium_asset, premium_amount): (u256, u64) = get_input_explicit_asset_amount(1); + assert!(jet::eq_256(premium_asset, param::PREMIUM_ASSET_ID)); + check_user_signature(sig); + ensure_output_asset_with_amount_eq(0, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(1, param::PREMIUM_ASSET_ID, premium_amount); + } + fn main() { + let signature: Signature = witness::USER_SIGHASH_ALL; + match witness::PATH{ + Left(params: (u64, bool)) => { + let (collateral_amount, is_change_needed): (u64, bool) = params; + exercise_path(collateral_amount, is_change_needed)} + , + Right(withdraw_or_expiry: Either<(), ()>) => match withdraw_or_expiry{ + Left(params: ()) => withdraw_path(signature), + Right(params: ()) => expiry_path(signature), + }, + }} +} diff --git a/crates/contracts/src/artifacts/simf/options.simf b/crates/contracts/src/artifacts/simf/options.simf new file mode 100644 index 0000000..9f70a31 --- /dev/null +++ b/crates/contracts/src/artifacts/simf/options.simf @@ -0,0 +1,265 @@ +mod unit_0 { + fn divmod_eq(a: u64, b: u64, expected_q: u64) { + let (q, r): (u64, u64) = jet::div_mod_64(a, b); + assert!(jet::eq_64(q, expected_q)); + assert!(jet::eq_64(r, 0)); + } + fn get_output_script_hash(index: u32) -> u256 { + unwrap(jet::output_script_hash(index))} + fn get_input_script_hash(index: u32) -> u256 { + unwrap(jet::input_script_hash(index))} + fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount)} + fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount)} + fn ensure_one_bit(bit: bool) { + assert!(jet::eq_1(::into(bit), 1)); + } + fn ensure_zero_bit(bit: bool) { + assert!(jet::eq_1(::into(bit), 0)); + } + fn increment_by(index: u32, amount: u32) -> u32 { + let (carry, result): (bool, u32) = jet::add_32(index, amount); + ensure_zero_bit(carry); + result} + fn ensure_input_and_output_script_hash_eq(index: u32) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); + } + fn ensure_output_is_op_return(index: u32) { + match jet::output_null_datum(index, 0){ + None => panic!(), + Some(entry: Option>>) => (), + }} + fn ensure_input_asset_eq(index: u32, expected_bits: u256) { + let asset: Asset1 = unwrap(jet::input_asset(index)); + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + assert!(jet::eq_256(asset_bits, expected_bits)); + } + fn ensure_output_asset_eq(index: u32, expected_bits: u256) { + let asset: Asset1 = unwrap(jet::output_asset(index)); + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + assert!(jet::eq_256(asset_bits, expected_bits)); + } + fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { + let (asset, amount): (u256, u64) = dbg!(get_output_explicit_asset_amount(index)); + assert!(jet::eq_256(asset, expected_bits)); + assert!(jet::eq_64(amount, expected_amount)); + } + fn ensure_input_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected)); + } + fn ensure_output_script_hash_eq(index: u32, expected: u256) { + assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); + } + fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { + let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); + assert!(jet::eq_32(jet::current_index(), index)); + match is_change_needed{ + false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), + true => { + ensure_input_and_output_script_hash_eq(index); + let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); + ensure_zero_bit(carry); + ensure_output_asset_with_amount_eq(index, asset_id, collateral_change); + } + , + }} + fn check_y(expected_y: Fe, actual_y: Fe) { + match jet::eq_256(expected_y, actual_y){ + false => { + assert!(jet::eq_256(expected_y, jet::fe_negate(actual_y))); + } + , + true => { + } + , + }; + } + fn ensure_input_and_output_reissuance_token_eq(index: u32) { + let (input_asset, input_amount): (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (output_asset, output_amount): (Asset1, Amount1) = unwrap(jet::output_amount(index)); + match (input_asset){ + Left(in_conf: Point) => { + let (input_asset_parity, input_asset_x): (u1, u256) = in_conf; + let (output_asset_parity, output_asset_x): (u1, u256) = unwrap_left::(output_asset); + assert!(jet::eq_1(input_asset_parity, output_asset_parity)); + assert!(jet::eq_256(input_asset_x, output_asset_x)); + } + , + Right(in_expl: u256) => { + let out_expl: u256 = unwrap_right::(output_asset); + assert!(jet::eq_256(in_expl, out_expl)); + } + , + }; + match (input_amount){ + Left(in_conf: Point) => { + let (input_amount_parity, input_amount_x): (u1, u256) = in_conf; + let (output_amount_parity, output_amount_x): (u1, u256) = unwrap_left::(output_amount); + assert!(jet::eq_1(input_amount_parity, output_amount_parity)); + assert!(jet::eq_256(input_amount_x, output_amount_x)); + } + , + Right(in_expl: u64) => { + let out_expl: u64 = unwrap_right::(output_amount); + assert!(jet::eq_64(in_expl, out_expl)); + } + , + }; + } + fn verify_token_commitment(actual_asset: Asset1, actual_amount: Amount1, expected_token_id: u256, abf: u256, vbf: u256) { + match actual_asset{ + Left(conf_token: Point) => { + let amount_scalar: u256 = 1; + let (actual_ax, actual_ay): Ge = unwrap(jet::decompress(conf_token)); + let gej_point: Gej = (jet::hash_to_curve(expected_token_id), 1); + let asset_blind_point: Gej = jet::generate(abf); + let asset_generator: Gej = jet::gej_add(gej_point, asset_blind_point); + let (ax, ay): Ge = unwrap(jet::gej_normalize(asset_generator)); + assert!(jet::eq_256(actual_ax, ax)); + check_y(actual_ay, ay); + let conf_val: Point = unwrap_left::(actual_amount); + let (actual_vx, actual_vy): Ge = unwrap(jet::decompress(conf_val)); + let amount_part: Gej = jet::scale(amount_scalar, asset_generator); + let vbf_part: Gej = jet::generate(vbf); + let value_generator: Gej = jet::gej_add(amount_part, vbf_part); + let (vx, vy): Ge = unwrap(jet::gej_normalize(value_generator)); + assert!(jet::eq_256(actual_vx, vx)); + check_y(actual_vy, vy); + } + , + Right(reissuance_token: u256) => { + let expected_amount: u64 = 1; + let actual_amount: u64 = unwrap_right::(actual_amount); + assert!(jet::eq_64(expected_amount, actual_amount)); + assert!(jet::eq_256(reissuance_token, expected_token_id)); + } + , + }; + } + fn verify_output_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) { + let (asset, amount): (Asset1, Amount1) = unwrap(jet::output_amount(index)); + verify_token_commitment(asset, amount, expected_token_id, abf, vbf); + } + fn verify_input_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) { + let (asset, amount): (Asset1, Amount1) = unwrap(jet::input_amount(index)); + verify_token_commitment(asset, amount, expected_token_id, abf, vbf); + } + fn funding_path(expected_asset_amount: u64, input_option_abf: u256, input_option_vbf: u256, input_grantor_abf: u256, input_grantor_vbf: u256, output_option_abf: u256, output_option_vbf: u256, output_grantor_abf: u256, output_grantor_vbf: u256) { + ensure_input_and_output_script_hash_eq(0); + ensure_input_and_output_script_hash_eq(1); + verify_input_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, input_option_abf, input_option_vbf); + verify_input_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, input_grantor_abf, input_grantor_vbf); + verify_output_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, output_option_abf, output_option_vbf); + verify_output_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, output_grantor_abf, output_grantor_vbf); + assert!(dbg!(jet::eq_256(get_output_script_hash(0), get_output_script_hash(1)))); + assert!(jet::le_32(jet::current_index(), 1)); + ensure_output_script_hash_eq(2, get_output_script_hash(0)); + let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(2); + let option_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); + let grantor_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); + assert!(jet::eq_64(option_token_amount, grantor_token_amount)); + divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, option_token_amount); + divmod_eq(expected_asset_amount, param::SETTLEMENT_PER_CONTRACT, option_token_amount); + ensure_output_asset_with_amount_eq(2, param::COLLATERAL_ASSET_ID, collateral_amount); + ensure_output_asset_with_amount_eq(3, param::OPTION_TOKEN_ASSET, option_token_amount); + ensure_output_asset_with_amount_eq(4, param::GRANTOR_TOKEN_ASSET, grantor_token_amount); + } + fn cancellation_path(amount_to_burn: u64, collateral_amount_to_withdraw: u64, is_change_needed: bool) { + let collateral_input_index: u32 = 0; + let option_input_index: u32 = 1; + let grantor_input_index: u32 = 2; + let (burn_option_output_index, burn_grantor_output_index): (u32, u32) = match is_change_needed{ + false => (0, 1), + true => (1, 2), + }; + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_withdraw, expected_current_script_hash, is_change_needed); + ensure_output_is_op_return(burn_option_output_index); + ensure_output_is_op_return(burn_grantor_output_index); + ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, amount_to_burn); + ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, amount_to_burn); + divmod_eq(collateral_amount_to_withdraw, param::COLLATERAL_PER_CONTRACT, amount_to_burn); + } + fn exercise_path(option_amount_to_burn: u64, collateral_amount_to_get: u64, asset_amount_to_pay: u64, is_change_needed: bool) { + jet::check_lock_time(param::START_TIME); + let collateral_input_index: u32 = 0; + let (burn_option_output_index, asset_to_covenant_output_index): (u32, u32) = match is_change_needed{ + false => (0, 1), + true => (1, 2), + }; + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); + divmod_eq(collateral_amount_to_get, param::COLLATERAL_PER_CONTRACT, option_amount_to_burn); + divmod_eq(asset_amount_to_pay, param::SETTLEMENT_PER_CONTRACT, option_amount_to_burn); + ensure_output_is_op_return(burn_option_output_index); + ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, option_amount_to_burn); + ensure_output_asset_with_amount_eq(asset_to_covenant_output_index, param::SETTLEMENT_ASSET_ID, asset_amount_to_pay); + ensure_output_script_hash_eq(asset_to_covenant_output_index, expected_current_script_hash); + } + fn settlement_path(grantor_token_amount_to_burn: u64, asset_amount: u64, is_change_needed: bool) { + jet::check_lock_time(param::START_TIME); + let target_asset_input_index: u32 = 0; + let burn_grantor_output_index: u32 = match is_change_needed{ + false => 0, + true => 1, + }; + let expected_current_script_hash: u256 = get_input_script_hash(target_asset_input_index); + ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, asset_amount, expected_current_script_hash, is_change_needed); + divmod_eq(asset_amount, param::SETTLEMENT_PER_CONTRACT, grantor_token_amount_to_burn); + ensure_output_is_op_return(burn_grantor_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn); + } + fn expiry_path(grantor_token_amount_to_burn: u64, collateral_amount: u64, is_change_needed: bool) { + jet::check_lock_time(param::EXPIRY_TIME); + let collateral_input_index: u32 = 0; + let burn_grantor_output_index: u32 = match is_change_needed{ + false => 0, + true => 1, + }; + let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); + ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_current_script_hash, is_change_needed); + divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, grantor_token_amount_to_burn); + ensure_output_is_op_return(burn_grantor_output_index); + ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn); + } + fn main() { + match witness::PATH{ + Left(left_or_right: Either<(u64, u256, u256, u256, u256, u256, u256, u256, u256), Either<(bool, u64, u64, u64), (bool, u64, u64)>>) => match left_or_right{ + Left(params: (u64, u256, u256, u256, u256, u256, u256, u256, u256)) => { + let (expected_asset_amount, input_option_abf, input_option_vbf, input_grantor_abf, input_grantor_vbf, output_option_abf, output_option_vbf, output_grantor_abf, output_grantor_vbf): (u64, u256, u256, u256, u256, u256, u256, u256, u256) = params; + funding_path(expected_asset_amount, input_option_abf, input_option_vbf, input_grantor_abf, input_grantor_vbf, output_option_abf, output_option_vbf, output_grantor_abf, output_grantor_vbf); + } + , + Right(exercise_or_settlement: Either<(bool, u64, u64, u64), (bool, u64, u64)>) => match exercise_or_settlement{ + Left(params: (bool, u64, u64, u64)) => { + let (is_change_needed, amount_to_burn, collateral_amount, asset_amount): (bool, u64, u64, u64) = dbg!(params); + exercise_path(amount_to_burn, collateral_amount, asset_amount, is_change_needed)} + , + Right(params: (bool, u64, u64)) => { + let (is_change_needed, amount_to_burn, asset_amount): (bool, u64, u64) = dbg!(params); + settlement_path(amount_to_burn, asset_amount, is_change_needed)} + , + }, + }, + Right(left_or_right: Either<(bool, u64, u64), (bool, u64, u64)>) => match left_or_right{ + Left(params: (bool, u64, u64)) => { + let (is_change_needed, grantor_token_amount_to_burn, collateral_amount): (bool, u64, u64) = params; + expiry_path(grantor_token_amount_to_burn, collateral_amount, is_change_needed)} + , + Right(params: (bool, u64, u64)) => { + let (is_change_needed, amount_to_burn, collateral_amount): (bool, u64, u64) = params; + cancellation_path(amount_to_burn, collateral_amount, is_change_needed)} + , + }, + }} +} diff --git a/crates/contracts/src/lib.rs b/crates/contracts/src/lib.rs index 0d11e30..8e25536 100644 --- a/crates/contracts/src/lib.rs +++ b/crates/contracts/src/lib.rs @@ -1,5 +1,11 @@ +//! Reference Simplicity contract templates and helpers for Elements/Liquid. +//! +//! - [`programs`]: finance contracts (options, option offer) built on the +//! Simplex program API, with sources generated into [`artifacts`]. +//! - [`state_management`]: standalone storage contract examples, each behind a +//! feature flag. +//! - [`simplicityhl_core`]: shared compile/execute/taproot helpers. #![warn(clippy::all, clippy::pedantic)] -extern crate core; #[rustfmt::skip] #[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] pub mod artifacts; diff --git a/crates/contracts/src/programs/mod.rs b/crates/contracts/src/programs/mod.rs index 5417bf7..517f4a3 100644 --- a/crates/contracts/src/programs/mod.rs +++ b/crates/contracts/src/programs/mod.rs @@ -1,3 +1,5 @@ +//! Finance contract wrappers around the Simplex-generated program artifacts. + 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 index c9e88bc..e1d0595 100644 --- a/crates/contracts/src/programs/option_offer.rs +++ b/crates/contracts/src/programs/option_offer.rs @@ -1,3 +1,6 @@ +//! Collateral-for-settlement offer contract with three spending branches: +//! `Exercise`, `Withdraw`, and `Expiry`. + use crate::artifacts::option_offer::OptionOfferProgram; use crate::artifacts::option_offer::derived_option_offer::{ OptionOfferArguments, OptionOfferWitness, diff --git a/crates/contracts/src/programs/options.rs b/crates/contracts/src/programs/options.rs index d9f075d..c8bac73 100644 --- a/crates/contracts/src/programs/options.rs +++ b/crates/contracts/src/programs/options.rs @@ -1,3 +1,6 @@ +//! Covered-call options contract with five spending branches: `Fund`, +//! `Exercise`, `Settlement`, `Expiry`, and `Cancel`. + use simplex::either::{Left, Right}; use simplex::program::Program; use simplex::provider::SimplicityNetwork; diff --git a/crates/contracts/src/simplicityhl_core/error.rs b/crates/contracts/src/simplicityhl_core/error.rs index 7822336..23b7831 100644 --- a/crates/contracts/src/simplicityhl_core/error.rs +++ b/crates/contracts/src/simplicityhl_core/error.rs @@ -1,42 +1,3 @@ -#[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: diff --git a/crates/contracts/src/simplicityhl_core/mod.rs b/crates/contracts/src/simplicityhl_core/mod.rs index 4c632a7..6ad408e 100644 --- a/crates/contracts/src/simplicityhl_core/mod.rs +++ b/crates/contracts/src/simplicityhl_core/mod.rs @@ -1,3 +1,6 @@ +//! Shared helpers to compile, execute, and build taproot data for +//! `SimplicityHL` programs. + 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 index 40d1a81..ee5cbf6 100644 --- a/crates/contracts/src/simplicityhl_core/runner.rs +++ b/crates/contracts/src/simplicityhl_core/runner.rs @@ -1,9 +1,10 @@ use std::sync::Arc; +use simplex::simplicityhl::ast::ElementsJetHinter; +use simplex::simplicityhl::simplicity::RedeemNode; 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::simplicity::{BitMachine, Value}; use simplex::simplicityhl::tracker::{DefaultTracker, TrackerLogLevel}; use simplex::simplicityhl::{CompiledProgram, WitnessValues}; @@ -13,18 +14,19 @@ use super::error::ProgramError; /// Returns the pruned program and the resulting value. /// /// # Errors -/// Returns error if witness satisfaction or program execution fails. +/// Returns an error if witness satisfaction, pruning, or execution fails. pub fn run_program( program: &CompiledProgram, witness_values: WitnessValues, env: &ElementsEnv>, log_level: TrackerLogLevel, -) -> Result<(Arc>, Value), ProgramError> { +) -> 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 mut tracker = DefaultTracker::build(satisfied.debug_symbols(), Box::new(ElementsJetHinter)) + .with_log_level(log_level); let pruned = satisfied .redeem() diff --git a/crates/contracts/src/simplicityhl_core/scripts.rs b/crates/contracts/src/simplicityhl_core/scripts.rs index 221f9a7..6abae32 100644 --- a/crates/contracts/src/simplicityhl_core/scripts.rs +++ b/crates/contracts/src/simplicityhl_core/scripts.rs @@ -1,26 +1,26 @@ +use simplex::simplicityhl::ast::ElementsJetHinter; use simplex::simplicityhl::elements::{Address, AddressParams, Script, taproot}; +use simplex::simplicityhl::simplicity::Cmr; 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. +/// Compile `SimplicityHL` source into a Simplicity program with debug symbols. /// /// # Errors -/// Returns error if the program fails to compile. +/// Returns an 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) + CompiledProgram::new(source, arguments, true, Box::new(ElementsJetHinter)) + .map_err(ProgramError::Compilation) } /// 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, + cmr: Cmr, x_only_public_key: &XOnlyPublicKey, params: &'static AddressParams, ) -> Address { @@ -41,6 +41,20 @@ pub fn simplicity_leaf_version() -> taproot::LeafVersion { simplex::simplicityhl::simplicity::leaf_version() } +/// The unspendable internal key specified in BIP-0341. +/// +/// # Panics +/// Never: the hard-coded bytes are a valid x-only public key. +#[rustfmt::skip] // mangles byte vectors +#[must_use] +pub fn unspendable_internal_key() -> XOnlyPublicKey { + XOnlyPublicKey::from_slice(&[ + 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e, + 0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0, + ]) + .expect("key is valid") +} + /// Create a SHA256 context, initialized with a "`TapData`" tag and data /// /// Based on the C implementation of the `tapdata_init` jet: @@ -58,34 +72,64 @@ pub fn tap_data_hash(data: &[u8]) -> sha256::Hash { /// Compute the Taproot control block for script-path spending. /// /// # Panics -/// -/// Panics if the taproot tree is invalid (should never happen with valid CMR). +/// Panics if the CMR leaf is not part of the tap tree (never happens for a +/// tree built from the same CMR). #[must_use] -pub fn control_block( - cmr: simplex::simplicityhl::simplicity::Cmr, - internal_key: XOnlyPublicKey, -) -> taproot::ControlBlock { +pub fn control_block(cmr: Cmr, internal_key: XOnlyPublicKey) -> taproot::ControlBlock { let info = taproot_spending_info(cmr, internal_key); - let script_ver = script_version(cmr); + let script_ver = script_ver(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()) +/// Return the (script, leaf version) pair for the CMR of a Simplicity program. +#[must_use] +pub fn script_ver(cmr: Cmr) -> (Script, taproot::LeafVersion) { + ( + Script::from(cmr.as_ref().to_vec()), + simplicity_leaf_version(), + ) +} + +/// Compute the [`taproot::TaprootSpendInfo`] for a tap tree with the program +/// CMR as its single leaf. +/// +/// # Panics +/// Panics if the tap tree cannot be built (never happens for a valid CMR). +#[must_use] +pub fn taproot_spending_info(cmr: Cmr, internal_key: XOnlyPublicKey) -> taproot::TaprootSpendInfo { + let (script, version) = script_ver(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") } -fn taproot_spending_info( - cmr: simplex::simplicityhl::simplicity::Cmr, +/// Compute the [`taproot::TaprootSpendInfo`] for a tap tree with two leaves: +/// the program CMR and a hidden leaf committing to contract state. +/// +/// Both leaves sit at depth 1, directly under the root. The state hash is +/// inserted with `add_hidden`, which commits to the hash as-is instead of +/// hashing a script. +/// +/// # Panics +/// Panics if the tap tree cannot be built (never happens for a valid CMR). +#[must_use] +pub fn state_taproot_spend_info( internal_key: XOnlyPublicKey, + state_hash: sha256::Hash, + cmr: Cmr, ) -> taproot::TaprootSpendInfo { - let (script, version) = script_version(cmr); + let (script, version) = script_ver(cmr); let builder = taproot::TaprootBuilder::new() - .add_leaf_with_ver(0, script, version) + .add_leaf_with_ver(1, script, version) + .expect("tap tree should be valid") + .add_hidden(1, state_hash) .expect("tap tree should be valid"); + builder .finalize(secp256k1::SECP256K1, internal_key) .expect("tap tree should be valid") 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 81f13f7..e5fd565 100644 --- a/crates/contracts/src/state_management/array_tr_storage/mod.rs +++ b/crates/contracts/src/state_management/array_tr_storage/mod.rs @@ -1,16 +1,17 @@ +//! Stores an array of `u256` limbs hashed into a hidden taproot leaf; +//! spending proves the old array and the changed index. + use crate::error::ProgramError; use crate::runner::run_program; -use crate::scripts::{simplicity_leaf_version, tap_data_hash}; +use crate::scripts::{state_taproot_spend_info, tap_data_hash}; use std::sync::Arc; +use simplex::simplicityhl::ast::ElementsJetHinter; 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::elements::Transaction; +use simplex::simplicityhl::simplicity::elements::taproot::TaprootSpendInfo; 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; @@ -25,24 +26,22 @@ pub const ARRAY_TR_STORAGE_SOURCE: &str = include_str!("source_simf/array_tr_sto /// Get the storage template program for instantiation. /// /// # Panics -/// /// Panics if the embedded source fails to compile (should never happen). #[must_use] pub fn get_array_tr_storage_template_program() -> TemplateProgram { - TemplateProgram::new(ARRAY_TR_STORAGE_SOURCE) - .expect("INTERNAL: expected to compile successfully.") + TemplateProgram::new(ARRAY_TR_STORAGE_SOURCE, Box::new(ElementsJetHinter)) + .expect("embedded source should compile") } /// Get compiled storage program, panicking on failure. /// /// # Panics -/// /// Panics if program instantiation fails. #[must_use] pub fn get_array_tr_storage_compiled_program() -> CompiledProgram { - let program = get_array_tr_storage_template_program(); - - program.instantiate(Arguments::default(), true).unwrap() + get_array_tr_storage_template_program() + .instantiate(Arguments::default(), true) + .unwrap() } /// Execute storage program with new state. @@ -55,18 +54,12 @@ pub fn execute_array_tr_storage_program( compiled_program: &CompiledProgram, env: &ElementsEnv>, runner_log_level: TrackerLogLevel, -) -> Result>, ProgramError> { +) -> Result, ProgramError> { let witness_values = build_array_tr_storage_witness(state, changed_index); Ok(run_program(compiled_program, witness_values, env, runner_log_level)?.0) } -fn array_tr_storage_script_ver(cmr: Cmr) -> (Script, LeafVersion) { - ( - Script::from(cmr.as_ref().to_vec()), - simplicity_leaf_version(), - ) -} - +/// Compute the `TapData`-tagged hash of the concatenated state limbs. #[must_use] pub fn compute_tapdata_tagged_hash_of_the_state(state: &State) -> sha256::Hash { let mut state_bytes = Vec::with_capacity(state.limbs.len() * 32); @@ -76,40 +69,22 @@ pub fn compute_tapdata_tagged_hash_of_the_state(state: &State) -> sha256::Hash { tap_data_hash(&state_bytes) } -/// Given a Simplicity CMR and an internal key, computes the [`TaprootSpendInfo`] -/// for a Taptree with this CMR as its single leaf. +/// Compute the [`TaprootSpendInfo`] for a tap tree committing to the program +/// CMR and the `TapData`-tagged hash of the state array. /// /// # Panics -/// -/// This function **panics** if building the taproot tree fails (the calls to -/// `TaprootBuilder::add_leaf_with_ver` or `.add_hidden` return `Err`) or if -/// finalizing the builder fails. Those panics come from the `.expect(...)` -/// calls on the builder methods. +/// Panics if the tap tree cannot be built (never happens for a valid CMR). #[must_use] pub fn array_tr_storage_taproot_spend_info( internal_key: secp256k1::XOnlyPublicKey, state: &State, cmr: Cmr, ) -> TaprootSpendInfo { - let (script, version) = array_tr_storage_script_ver(cmr); - let state_hash = compute_tapdata_tagged_hash_of_the_state(state); - - // Build taproot tree with hidden leaf. - // Here, 'depth refers to the level at which the script and hash are transferred. - // At depth 0, this will take the place of the root, meaning it will be impossible to place both - // the `script` and the `state_hash`. At depth 2 or higher, additional nods are required, - // which complicates the structure. Therefore, a value of 1 was chosen, where the `script` and - // the `state_hash` values are leaves of the root. - // `add_hidden`` in this context allows you to insert the hash as is, unlike add_leaf_with_ver, which hashes under the hood `script`. - let builder = TaprootBuilder::new() - .add_leaf_with_ver(1, script, version) - .expect("tap tree should be valid") - .add_hidden(1, state_hash) - .expect("tap tree should be valid"); - - builder - .finalize(secp256k1::SECP256K1, internal_key) - .expect("tap tree should be valid") + state_taproot_spend_info( + internal_key, + compute_tapdata_tagged_hash_of_the_state(state), + cmr, + ) } #[cfg(test)] @@ -118,6 +93,8 @@ mod array_tr_storage_tests { use anyhow::Result; use std::sync::Arc; + use crate::scripts::{script_ver, unspendable_internal_key}; + use simplex::simplicityhl::elements::confidential::{Asset, Value}; use simplex::simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; use simplex::simplicityhl::elements::{AssetId, BlockHash, OutPoint, Script, Txid}; @@ -125,15 +102,6 @@ mod array_tr_storage_tests { 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 { - secp256k1::XOnlyPublicKey::from_slice(&[ - 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e, - 0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0, - ]) - .expect("key should be valid") - } - #[test] fn test_array_tr_storage_mint_path() -> Result<()> { let old_state = State::new(); @@ -147,18 +115,12 @@ mod array_tr_storage_tests { let program = get_array_tr_storage_compiled_program(); let cmr = program.commit().cmr(); - let old_spend_info = array_tr_storage_taproot_spend_info( - array_tr_storage_unspendable_internal_key(), - &old_state, - cmr, - ); + let old_spend_info = + array_tr_storage_taproot_spend_info(unspendable_internal_key(), &old_state, cmr); let old_script_pubkey = Script::new_v1_p2tr_tweaked(old_spend_info.output_key()); - let new_spend_info = array_tr_storage_taproot_spend_info( - array_tr_storage_unspendable_internal_key(), - &new_state, - cmr, - ); + let new_spend_info = + array_tr_storage_taproot_spend_info(unspendable_internal_key(), &new_state, cmr); let new_script_pubkey = Script::new_v1_p2tr_tweaked(new_spend_info.output_key()); let mut pst = PartiallySignedTransaction::new_v2(); @@ -172,7 +134,7 @@ mod array_tr_storage_tests { )); let control_block = old_spend_info - .control_block(&array_tr_storage_script_ver(cmr)) + .control_block(&script_ver(cmr)) .expect("must get control block"); let env = ElementsEnv::new( 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 f8a1c16..75263af 100644 --- a/crates/contracts/src/state_management/bytes32_tr_storage/mod.rs +++ b/crates/contracts/src/state_management/bytes32_tr_storage/mod.rs @@ -1,15 +1,15 @@ +//! Stores 32 bytes of state as a hidden taproot leaf next to the program +//! leaf; spending reveals the old state and commits to the new one. use std::sync::Arc; use crate::error::ProgramError; use crate::runner::run_program; -use crate::scripts::{simplicity_leaf_version, tap_data_hash}; +use crate::scripts::{state_taproot_spend_info, tap_data_hash}; +use simplex::simplicityhl::ast::ElementsJetHinter; 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::elements::Transaction; +use simplex::simplicityhl::simplicity::elements::taproot::TaprootSpendInfo; use simplex::simplicityhl::simplicity::jet::elements::ElementsEnv; use simplex::simplicityhl::simplicity::{Cmr, RedeemNode}; use simplex::simplicityhl::tracker::TrackerLogLevel; @@ -24,12 +24,11 @@ pub const BYTES32_TR_STORAGE_SOURCE: &str = include_str!("source_simf/bytes32_tr /// Get the storage template program for instantiation. /// /// # Panics -/// /// Panics if the embedded source fails to compile (should never happen). #[must_use] pub fn get_bytes32_tr_template_program() -> TemplateProgram { - TemplateProgram::new(BYTES32_TR_STORAGE_SOURCE) - .expect("INTERNAL: expected to compile successfully.") + TemplateProgram::new(BYTES32_TR_STORAGE_SOURCE, Box::new(ElementsJetHinter)) + .expect("embedded source should compile") } /// Get compiled storage program, panicking on failure. @@ -38,9 +37,7 @@ pub fn get_bytes32_tr_template_program() -> TemplateProgram { /// Panics if program instantiation fails. #[must_use] pub fn get_bytes32_tr_compiled_program() -> CompiledProgram { - let program = get_bytes32_tr_template_program(); - - program + get_bytes32_tr_template_program() .instantiate(simplex::simplicityhl::Arguments::default(), true) .unwrap() } @@ -54,64 +51,23 @@ pub fn execute_bytes32_tr_program( compiled_program: &CompiledProgram, env: &ElementsEnv>, log_level: TrackerLogLevel, -) -> Result>, ProgramError> { +) -> Result, ProgramError> { let witness_values = build_bytes32_tr_witness(state); Ok(run_program(compiled_program, witness_values, env, log_level)?.0) } -/// The unspendable internal key specified in BIP-0341. +/// Compute the [`TaprootSpendInfo`] for a tap tree committing to the program +/// CMR and the `TapData`-tagged hash of the 32-byte state. /// /// # Panics -/// -/// This function **panics** if the hard-coded 32-byte slice is not a valid -/// x-only public key. The panic originates from -/// `secp256k1::XOnlyPublicKey::from_slice(...).expect(...)`. -/// The unspendable internal key specified in BIP-0341. -#[rustfmt::skip] // mangles byte vectors -#[must_use] -pub fn unspendable_internal_key() -> secp256k1::XOnlyPublicKey { - secp256k1::XOnlyPublicKey::from_slice(&[ - 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e, - 0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0, - ]) - .expect("key should be valid") -} - -fn script_ver(cmr: Cmr) -> (Script, LeafVersion) { - ( - Script::from(cmr.as_ref().to_vec()), - simplicity_leaf_version(), - ) -} - -/// Given a Simplicity CMR and an internal key, computes the [`TaprootSpendInfo`] -/// for a Taptree with this CMR as its single leaf. -/// -/// # Panics -/// -/// This function **panics** if building the taproot tree fails (the calls to -/// `TaprootBuilder::add_leaf_with_ver` or `.add_hidden` return `Err`) or if -/// finalizing the builder fails. Those panics come from the `.expect(...)` -/// calls on the builder methods. +/// Panics if the tap tree cannot be built (never happens for a valid CMR). #[must_use] pub fn taproot_spend_info( internal_key: secp256k1::XOnlyPublicKey, state: [u8; 32], cmr: Cmr, ) -> TaprootSpendInfo { - let (script, version) = script_ver(cmr); - let state_hash = tap_data_hash(&state); - - // Build taproot tree with hidden leaf - let builder = TaprootBuilder::new() - .add_leaf_with_ver(1, script, version) - .expect("tap tree should be valid") - .add_hidden(1, state_hash) - .expect("tap tree should be valid"); - - builder - .finalize(secp256k1::SECP256K1, internal_key) - .expect("tap tree should be valid") + state_taproot_spend_info(internal_key, tap_data_hash(&state), cmr) } #[cfg(test)] @@ -120,6 +76,8 @@ mod bytes32_tr_tests { use anyhow::Result; use std::sync::Arc; + use crate::scripts::{script_ver, unspendable_internal_key}; + use simplex::simplicityhl::elements::confidential::{Asset, Value}; use simplex::simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; use simplex::simplicityhl::elements::{self, AssetId, OutPoint, Script, Txid}; @@ -131,8 +89,7 @@ mod bytes32_tr_tests { fn test_bytes32_tr_mint_path() -> Result<()> { let old_state: [u8; 32] = [0u8; 32]; - // Calculate new_state - // NOTE: Our example can be done with the line new_state[31] = 1 + // Increment the last qword of the state by one. let mut new_state = old_state; let mut val = u64::from_be_bytes(new_state[24..].try_into().unwrap()); val += 1; @@ -147,7 +104,6 @@ mod bytes32_tr_tests { let new_spend_info = taproot_spend_info(unspendable_internal_key(), new_state, cmr); let new_script_pubkey = Script::new_v1_p2tr_tweaked(new_spend_info.output_key()); - // Build transaction let mut pst = PartiallySignedTransaction::new_v2(); let outpoint0 = OutPoint::new(Txid::from_slice(&[0; 32])?, 0); pst.add_input(Input::from_prevout(outpoint0)); @@ -162,7 +118,6 @@ mod bytes32_tr_tests { .control_block(&script_ver(cmr)) .expect("Must retrieve control block for the script path"); - // Set up environment let env = ElementsEnv::new( Arc::new(pst.extract_tx()?), vec![ @@ -174,7 +129,7 @@ mod bytes32_tr_tests { ], 0, cmr, - ControlBlock::from_slice(&control_block.serialize())?, // Real control block + ControlBlock::from_slice(&control_block.serialize())?, None, elements::BlockHash::all_zeros(), ); diff --git a/crates/contracts/src/state_management/mod.rs b/crates/contracts/src/state_management/mod.rs index 6f06c49..a449212 100644 --- a/crates/contracts/src/state_management/mod.rs +++ b/crates/contracts/src/state_management/mod.rs @@ -1,3 +1,4 @@ +//! Standalone storage contract examples, each behind a feature flag. #[cfg(feature = "array-tr-storage")] pub mod array_tr_storage; #[cfg(feature = "bytes32-tr-storage")] diff --git a/crates/contracts/src/state_management/simple_storage/mod.rs b/crates/contracts/src/state_management/simple_storage/mod.rs index b29bdb5..53e193e 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 @@ +//! Stores a `u64` as an explicit output amount; updates are authorized by a +//! fixed key and must mint (extra output) or burn (`OP_RETURN`) the +//! difference. use std::sync::Arc; use crate::error::ProgramError; use crate::runner::run_program; use crate::scripts::{create_p2tr_address, load_program}; +use simplex::simplicityhl::ast::ElementsJetHinter; 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}; @@ -29,8 +32,8 @@ pub const SIMPLE_STORAGE_SOURCE: &str = include_str!("source_simf/simple_storage /// Panics if the embedded source fails to compile (should never happen). #[must_use] pub fn get_storage_template_program() -> TemplateProgram { - TemplateProgram::new(SIMPLE_STORAGE_SOURCE) - .expect("INTERNAL: expected to compile successfully.") + TemplateProgram::new(SIMPLE_STORAGE_SOURCE, Box::new(ElementsJetHinter)) + .expect("embedded source should compile") } /// Derive P2TR address for a storage contract. @@ -59,9 +62,7 @@ fn get_storage_program(args: &StorageArguments) -> Result CompiledProgram { - let program = get_storage_template_program(); - - program + get_storage_template_program() .instantiate(build_storage_arguments(args), true) .unwrap() } @@ -76,7 +77,7 @@ pub fn execute_storage_program( compiled_program: &CompiledProgram, env: &ElementsEnv>, log_level: TrackerLogLevel, -) -> Result>, ProgramError> { +) -> Result, ProgramError> { let sighash_all = secp256k1::Message::from_digest(*env.c_tx_env().sighash_all().as_byte_array()); diff --git a/crates/contracts/tests/common/filters.rs b/crates/contracts/tests/common/filters.rs index a8d2ced..557cb01 100644 --- a/crates/contracts/tests/common/filters.rs +++ b/crates/contracts/tests/common/filters.rs @@ -53,20 +53,6 @@ pub fn filter_signer_utxos_by_asset_id(signer: &Signer, asset_id: AssetId) -> Ve .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], @@ -135,3 +121,33 @@ const fn matches_amount_filter(utxo_amount: u64, amount: u64, amount_filter: Amo AmountFilter::EqualTo => utxo_amount == amount, } } + +/// Fetch the covenant UTXOs at `script_pubkey` and return the one matching +/// asset and amount, failing with `missing_utxo_message` if absent. +pub fn require_covenant_utxo( + context: &simplex::TestContext, + script_pubkey: &Script, + asset_id: AssetId, + amount: u64, + missing_utxo_message: &str, +) -> anyhow::Result { + let utxos = context + .get_default_provider() + .fetch_scripthash_utxos(script_pubkey)?; + require_utxo_by_asset_and_amount(&utxos, asset_id, amount, missing_utxo_message) +} + +/// Assert that a covenant UTXO with the given asset and amount exists at +/// `script_pubkey`. +pub fn assert_covenant_utxo( + context: &simplex::TestContext, + script_pubkey: &Script, + asset_id: AssetId, + amount: u64, +) -> anyhow::Result<()> { + let utxos = context + .get_default_provider() + .fetch_scripthash_utxos(script_pubkey)?; + assert_has_utxo_by_asset_and_amount(&utxos, asset_id, amount); + Ok(()) +} diff --git a/crates/contracts/tests/common/mod.rs b/crates/contracts/tests/common/mod.rs index a6e2a9e..2d40e62 100644 --- a/crates/contracts/tests/common/mod.rs +++ b/crates/contracts/tests/common/mod.rs @@ -1,3 +1,37 @@ +#![allow(clippy::missing_errors_doc)] + pub mod filters; pub mod issuance; pub mod signer; + +use simplex::simplicityhl::elements::{LockTime, Sequence}; +use simplex::transaction::{PartialInput, UTXO}; + +/// Offset the chain tip timestamp by a signed delta, checking for overflow. +pub 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")) + } +} + +/// Convert a unix timestamp into an absolute [`LockTime`]. +pub fn locktime_from(time: u32) -> anyhow::Result { + LockTime::from_time(time).map_err(|error| anyhow::anyhow!(error)) +} + +/// Wrap a UTXO into an input that enables the transaction-level locktime. +#[must_use] +pub fn locked_input(utxo: UTXO, locktime: LockTime) -> PartialInput { + PartialInput::new(utxo) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime) +} diff --git a/crates/contracts/tests/program_builder/option_offer.rs b/crates/contracts/tests/program_builder/option_offer.rs index 7b2b786..ff553fe 100644 --- a/crates/contracts/tests/program_builder/option_offer.rs +++ b/crates/contracts/tests/program_builder/option_offer.rs @@ -1,17 +1,43 @@ #![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] -use crate::common::filters::{AmountFilter, filter_signer_utxos_by_asset_and_amount}; +use crate::common::filters::{ + AmountFilter, filter_signer_utxos_by_asset_and_amount, require_covenant_utxo, +}; use crate::common::issuance::issue_asset; +use crate::common::offset_timestamp; use crate::common::signer::{ ensure_exact_signer_utxo, finalize_and_broadcast, get_lbtc_utxo, split_first_signer_utxo, }; +use std::collections::HashMap; + +use simplex::program::WitnessTrait; +use simplex::signer::SignerTrait; use simplex::simplicityhl::elements::Txid; -use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature}; +use simplex::simplicityhl::elements::pset::PartiallySignedTransaction; +use simplex::simplicityhl::str::WitnessName; +use simplex::simplicityhl::value::ValueConstructible; +use simplex::simplicityhl::{Value, WitnessValues}; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, UTXO, +}; -use contracts::programs::option_offer::{OptionOffer, OptionOfferParameters}; +use contracts::programs::option_offer::{OptionOffer, OptionOfferBranch, OptionOfferParameters}; use contracts::programs::program::SimplexProgram; +/// Standard offer sizing shared by the regtest scenarios. +pub const DEPOSIT_LBTC_AMOUNT: u64 = 1_000; +pub const EXPECTED_PREMIUM_AMOUNT: u64 = 10_000; +pub const EXPECTED_SETTLEMENT_AMOUNT: u64 = 10_000; + +/// Standard partial exercise of the offer: 600 of 1000 collateral for 6000 +/// settlement, with 6000 premium released and change locked back. +pub const PARTIAL_COLLATERAL_AMOUNT: u64 = 600; +pub const REMAINING_COLLATERAL_AMOUNT: u64 = DEPOSIT_LBTC_AMOUNT - PARTIAL_COLLATERAL_AMOUNT; +pub const EXERCISED_PREMIUM_AMOUNT: u64 = 6_000; +pub const REMAINING_PREMIUM_AMOUNT: u64 = EXPECTED_PREMIUM_AMOUNT - EXERCISED_PREMIUM_AMOUNT; +pub const EXERCISED_SETTLEMENT_AMOUNT: u64 = 6_000; + pub fn prepare_option_offer( context: &simplex::TestContext, deposit_lbtc_amount: u64, @@ -39,23 +65,10 @@ pub fn build_option_offer_program( ) -> 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 expiry_time = offset_timestamp(tip_timestamp, delta_timestamp)?; 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( @@ -64,18 +77,18 @@ pub fn build_option_offer_program( expected_premium_amount, ); - let option_offer_params: OptionOfferParameters = OptionOfferParameters { - collateral_asset_id: policy_asset, + Ok(OptionOffer::new(OptionOfferParameters { + collateral_asset_id: network.policy_asset(), premium_asset_id, settlement_asset_id, - collateral_per_contract: collateral_per_contract.unwrap(), - premium_per_collateral: premium_per_collateral.unwrap(), + collateral_per_contract: collateral_per_contract + .ok_or_else(|| anyhow::anyhow!("failed to derive collateral_per_contract"))?, + premium_per_collateral: premium_per_collateral + .ok_or_else(|| anyhow::anyhow!("failed to derive premium_per_collateral"))?, expiry_time, user_pubkey: signer.get_schnorr_public_key(), network: *network, - }; - - Ok(OptionOffer::new(option_offer_params)) + })) } pub fn create_option_offer_with_premium( @@ -111,9 +124,32 @@ pub fn create_option_offer_with_premium( option_offer.parameters.premium_asset_id, )); - let txid = finalize_and_broadcast(context, &ft)?; + finalize_and_broadcast(context, &ft) +} + +/// Prepare an offer with the standard sizing ([`DEPOSIT_LBTC_AMOUNT`], +/// [`EXPECTED_PREMIUM_AMOUNT`], [`EXPECTED_SETTLEMENT_AMOUNT`]) and lock the +/// collateral and premium in the covenant. +pub fn setup_offer_with_premium( + context: &simplex::TestContext, + delta_timestamp: i32, +) -> anyhow::Result { + let option_offer = prepare_option_offer( + context, + DEPOSIT_LBTC_AMOUNT, + EXPECTED_PREMIUM_AMOUNT, + EXPECTED_SETTLEMENT_AMOUNT, + delta_timestamp, + )?; + + create_option_offer_with_premium( + context, + &option_offer, + DEPOSIT_LBTC_AMOUNT, + EXPECTED_PREMIUM_AMOUNT, + )?; - Ok(txid) + Ok(option_offer) } pub fn deposit_to_option_offer( @@ -125,8 +161,7 @@ pub fn deposit_to_option_offer( ) -> 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 policy_asset = context.get_network().policy_asset(); let option_offer = prepare_option_offer( context, @@ -164,3 +199,130 @@ pub fn deposit_to_option_offer( Ok((option_offer, txid)) } + +/// Build a covenant input spending `utxo` on the given offer branch. +#[must_use] +pub fn offer_program_input(option_offer: &OptionOffer, branch: OptionOfferBranch) -> ProgramInput { + ProgramInput::new( + Box::new(option_offer.get_program().clone()), + Box::new(OptionOffer::get_witness(branch)), + ) +} + +/// Fetch the covenant UTXOs holding the offered collateral and premium. +pub fn require_offer_utxos( + context: &simplex::TestContext, + option_offer: &OptionOffer, + collateral_amount: u64, + premium_amount: u64, +) -> anyhow::Result<[UTXO; 2]> { + let script_pubkey = option_offer.get_script_pubkey(); + let collateral = require_covenant_utxo( + context, + &script_pubkey, + option_offer.parameters.collateral_asset_id, + collateral_amount, + "missing collateral covenant utxo", + )?; + let premium = require_covenant_utxo( + context, + &script_pubkey, + option_offer.parameters.premium_asset_id, + premium_amount, + "missing premium covenant utxo", + )?; + Ok([collateral, premium]) +} + +/// Partially exercise an offer created via [`setup_offer_with_premium`] +/// using the standard partial amounts, leaving settlement, remaining +/// collateral, and remaining premium locked in the covenant. +pub fn exercise_offer_partially( + context: &simplex::TestContext, + option_offer: &OptionOffer, +) -> anyhow::Result { + let receiver_script_pubkey = context.get_default_signer().get_address().script_pubkey(); + let settlement_input = ensure_exact_signer_utxo( + context, + option_offer.parameters.settlement_asset_id, + EXERCISED_SETTLEMENT_AMOUNT, + )?; + + let exercise_branch = OptionOfferBranch::Exercise { + collateral_amount: PARTIAL_COLLATERAL_AMOUNT, + is_change_needed: true, + }; + let offer_utxos = require_offer_utxos( + context, + option_offer, + DEPOSIT_LBTC_AMOUNT, + EXPECTED_PREMIUM_AMOUNT, + )?; + + let mut ft = FinalTransaction::new(); + for utxo in offer_utxos { + ft.add_program_input( + PartialInput::new(utxo), + offer_program_input(option_offer, 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, + )); + + finalize_and_broadcast(context, &ft) +} + +/// Build the witness for an offer branch with the user's `SIGHASH_ALL` +/// signature filled in. +pub fn witness_with_user_sighash( + context: &simplex::TestContext, + option_offer: &OptionOffer, + branch: OptionOfferBranch, + pst: &PartiallySignedTransaction, +) -> anyhow::Result { + let signature = context.get_default_signer().sign_program( + pst, + option_offer.get_program(), + 0, + context.get_network(), + )?; + + let mut signed_witness = HashMap::new(); + for (name, value) in OptionOffer::get_witness(branch).build_witness().iter() { + signed_witness.insert(name.clone(), value.clone()); + } + signed_witness.insert( + WitnessName::from_str_unchecked("USER_SIGHASH_ALL"), + Value::byte_array(signature.serialize()), + ); + + Ok(WitnessValues::from(signed_witness)) +} diff --git a/crates/contracts/tests/program_builder/options.rs b/crates/contracts/tests/program_builder/options.rs index a2660ec..07bd8c8 100644 --- a/crates/contracts/tests/program_builder/options.rs +++ b/crates/contracts/tests/program_builder/options.rs @@ -6,9 +6,14 @@ use std::collections::HashMap; -use crate::common::filters::{AmountFilter, filter_signer_utxos_by_asset_and_amount}; +use crate::common::filters::{ + AmountFilter, filter_signer_utxos_by_asset_and_amount, require_covenant_utxo, +}; use crate::common::issuance::issue_asset; -use crate::common::signer::{ensure_exact_signer_utxo, split_first_signer_utxo}; +use crate::common::signer::{ + ensure_exact_signer_utxo, finalize_and_broadcast, get_lbtc_utxo, split_first_signer_utxo, +}; +use crate::common::{locked_input, locktime_from, offset_timestamp}; use anyhow::Context; use contracts::programs::options::{ @@ -27,10 +32,18 @@ use simplex::simplicityhl::elements::{ }; use simplex::simplicityhl::simplicity::hashes::Hash; use simplex::transaction::partial_input::IssuanceInput; -use simplex::transaction::{PartialInput, PartialOutput, UTXO}; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, UTXO, +}; + const OPTION_ISSUANCE_CONTRACT_HASH: [u8; 32] = [1; 32]; const GRANTOR_ISSUANCE_CONTRACT_HASH: [u8; 32] = [2; 32]; +/// Standard options contract sizing shared by the regtest scenarios. +pub const TOTAL_COLLATERAL_AMOUNT: u64 = 1_000; +pub const EXPECTED_SETTLEMENT_AMOUNT: u64 = 500; +pub const CONTRACT_COUNT: u64 = 10; + pub struct PreparedOptionsState { pub options: Options, pub option_issuance_entropy: [u8; 32], @@ -540,6 +553,112 @@ pub fn fund_options( }) } +/// Prepare, create, and fund an options contract with the standard sizing +/// ([`TOTAL_COLLATERAL_AMOUNT`], [`EXPECTED_SETTLEMENT_AMOUNT`], +/// [`CONTRACT_COUNT`]). +pub fn setup_funded_options( + context: &simplex::TestContext, + start_delta_timestamp: i32, + expiry_delta_timestamp: i32, +) -> anyhow::Result { + let prepared = prepare_options( + context, + TOTAL_COLLATERAL_AMOUNT, + EXPECTED_SETTLEMENT_AMOUNT, + CONTRACT_COUNT, + start_delta_timestamp, + expiry_delta_timestamp, + )?; + let created = create_options(context, prepared)?; + fund_options(context, created, TOTAL_COLLATERAL_AMOUNT, CONTRACT_COUNT) +} + +/// Build a covenant input spending `utxo` on the given options branch. +#[must_use] +pub fn options_program_input(options: &Options, branch: OptionsBranch) -> ProgramInput { + ProgramInput::new( + Box::new(options.get_program().clone()), + Box::new(Options::get_witness(branch)), + ) +} + +/// Fetch the covenant UTXO holding the full locked collateral. +pub fn require_locked_collateral( + context: &simplex::TestContext, + funded: &FundedOptionsState, +) -> anyhow::Result { + require_covenant_utxo( + context, + &funded.options.get_script_pubkey(), + funded.options.parameters.collateral_asset_id, + TOTAL_COLLATERAL_AMOUNT, + "missing locked collateral covenant utxo", + ) +} + +/// Exercise the full collateral of a contract funded via +/// [`setup_funded_options`]. +/// +/// Burns all option tokens, locks the expected settlement amount in the +/// covenant, and pays the collateral to the default signer. +pub fn exercise_options_fully( + context: &simplex::TestContext, + funded: &FundedOptionsState, +) -> anyhow::Result { + let parameters = &funded.options.parameters; + let option_token_input = + ensure_exact_signer_utxo(context, parameters.option_token_asset, CONTRACT_COUNT)?; + let settlement_input = ensure_exact_signer_utxo( + context, + parameters.settlement_asset_id, + EXPECTED_SETTLEMENT_AMOUNT, + )?; + let locktime = locktime_from(parameters.start_time)?; + let locked_collateral = require_locked_collateral(context, funded)?; + + let mut ft = FinalTransaction::new(); + ft.add_program_input( + locked_input(locked_collateral, locktime), + options_program_input( + &funded.options, + OptionsBranch::Exercise { + is_change_needed: false, + amount_to_burn: CONTRACT_COUNT, + collateral_amount: TOTAL_COLLATERAL_AMOUNT, + settlement_amount: EXPECTED_SETTLEMENT_AMOUNT, + }, + ), + RequiredSignature::None, + ); + for input in [ + option_token_input, + settlement_input, + get_lbtc_utxo(context)?, + ] { + ft.add_input( + locked_input(input, locktime), + RequiredSignature::NativeEcdsa, + ); + } + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + CONTRACT_COUNT, + parameters.option_token_asset, + )); + ft.add_output(PartialOutput::new( + funded.options.get_script_pubkey(), + EXPECTED_SETTLEMENT_AMOUNT, + parameters.settlement_asset_id, + )); + ft.add_output(PartialOutput::new( + signer_script(context), + TOTAL_COLLATERAL_AMOUNT, + parameters.collateral_asset_id, + )); + + finalize_and_broadcast(context, &ft) +} + fn issuance_ids( issuance_outpoint: OutPoint, contract_hash_bytes: [u8; 32], @@ -556,21 +675,6 @@ fn issuance_ids( ) } -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() } diff --git a/crates/contracts/tests/regtest/option_offer/deposit.rs b/crates/contracts/tests/regtest/option_offer/deposit.rs index e28e194..63958e7 100644 --- a/crates/contracts/tests/regtest/option_offer/deposit.rs +++ b/crates/contracts/tests/regtest/option_offer/deposit.rs @@ -1,6 +1,6 @@ -use crate::common::filters::assert_has_utxo_by_asset_and_amount; +use crate::common::filters::assert_covenant_utxo; use crate::program_builder::option_offer::{ - create_option_offer_with_premium, deposit_to_option_offer, prepare_option_offer, + DEPOSIT_LBTC_AMOUNT, EXPECTED_PREMIUM_AMOUNT, deposit_to_option_offer, setup_offer_with_premium, }; use contracts::programs::program::SimplexProgram; @@ -9,15 +9,13 @@ use contracts::programs::program::SimplexProgram; 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)?; + deposit_to_option_offer(&context, DEPOSIT_LBTC_AMOUNT, 10_000, 10_000, 1_000)?; let transaction = provider.fetch_transaction(&deposit_txid)?; assert_eq!( transaction.output[0].value.explicit(), - Some(deposit_lbtc_amount) + Some(DEPOSIT_LBTC_AMOUNT) ); assert_eq!( transaction.output[0].asset.explicit(), @@ -35,38 +33,21 @@ fn deposit_to_create_option_offer(context: simplex::TestContext) -> anyhow::Resu 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 = setup_offer_with_premium(&context, 1_000)?; - let option_offer = prepare_option_offer( + let script_pubkey = option_offer.get_script_pubkey(); + assert_covenant_utxo( &context, - deposit_lbtc_amount, - expected_premium_amount, - expected_settlement_amount, - 1_000, + &script_pubkey, + option_offer.parameters.collateral_asset_id, + DEPOSIT_LBTC_AMOUNT, )?; - - create_option_offer_with_premium( + assert_covenant_utxo( &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, + &script_pubkey, option_offer.parameters.premium_asset_id, - expected_premium_amount, - ); + EXPECTED_PREMIUM_AMOUNT, + )?; Ok(()) } diff --git a/crates/contracts/tests/regtest/option_offer/exercise.rs b/crates/contracts/tests/regtest/option_offer/exercise.rs index 91feab4..ab1efe3 100644 --- a/crates/contracts/tests/regtest/option_offer/exercise.rs +++ b/crates/contracts/tests/regtest/option_offer/exercise.rs @@ -1,124 +1,90 @@ -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::filters::{assert_covenant_utxo, assert_has_utxo_by_asset_amount_and_script}; 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, + DEPOSIT_LBTC_AMOUNT, EXERCISED_PREMIUM_AMOUNT, EXERCISED_SETTLEMENT_AMOUNT, + EXPECTED_PREMIUM_AMOUNT, EXPECTED_SETTLEMENT_AMOUNT, PARTIAL_COLLATERAL_AMOUNT, + REMAINING_COLLATERAL_AMOUNT, REMAINING_PREMIUM_AMOUNT, exercise_offer_partially, + offer_program_input, require_offer_utxos, setup_offer_with_premium, }; use simplex::program::{ProgramError, ProgramTrait, WitnessTrait}; -use simplex::transaction::{ - FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, -}; +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, 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 option_offer = setup_offer_with_premium(&context, 1_000)?; 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, + 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, + 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 offer_utxos = require_offer_utxos( + &context, + &option_offer, + DEPOSIT_LBTC_AMOUNT, + EXPECTED_PREMIUM_AMOUNT, )?; let mut ft = FinalTransaction::new(); - - for utxo in [collateral_program_utxo, premium_program_utxo] { + for utxo in offer_utxos { ft.add_program_input( PartialInput::new(utxo), - ProgramInput::new( - Box::new(option_offer.get_program().clone()), - Box::new(OptionOffer::get_witness(exercise_branch)), - ), + offer_program_input(&option_offer, 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, + EXPECTED_SETTLEMENT_AMOUNT, option_offer.parameters.settlement_asset_id, )); ft.add_output(PartialOutput::new( receiver_script_pubkey.clone(), - deposit_lbtc_amount, + DEPOSIT_LBTC_AMOUNT, option_offer.parameters.collateral_asset_id, )); ft.add_output(PartialOutput::new( - receiver_script_pubkey, - expected_premium_amount, + receiver_script_pubkey.clone(), + 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, + assert_covenant_utxo( + &context, + &option_offer.get_script_pubkey(), option_offer.parameters.settlement_asset_id, - expected_settlement_amount, - ); + 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, + 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, + EXPECTED_PREMIUM_AMOUNT, &receiver_script_pubkey, ); @@ -127,133 +93,43 @@ fn exercise_option_offer(context: simplex::TestContext) -> anyhow::Result<()> { #[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 = setup_offer_with_premium(&context, 0)?; + let exercise_txid = exercise_offer_partially(&context, &option_offer)?; - let option_offer = prepare_option_offer( + let script_pubkey = option_offer.get_script_pubkey(); + assert_covenant_utxo( &context, - deposit_lbtc_amount, - expected_premium_amount, - expected_settlement_amount, - 0, + &script_pubkey, + option_offer.parameters.collateral_asset_id, + REMAINING_COLLATERAL_AMOUNT, )?; - - create_option_offer_with_premium( + assert_covenant_utxo( &context, - &option_offer, - deposit_lbtc_amount, - expected_premium_amount, + &script_pubkey, + option_offer.parameters.premium_asset_id, + REMAINING_PREMIUM_AMOUNT, )?; - - let receiver_script_pubkey = signer.get_address().script_pubkey(); - let settlement_input = ensure_exact_signer_utxo( + assert_covenant_utxo( &context, + &script_pubkey, 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", + EXERCISED_SETTLEMENT_AMOUNT, )?; - 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(); + let signer_utxos = signer.get_utxos_txid(exercise_txid)?; assert_has_utxo_by_asset_amount_and_script( &signer_utxos, option_offer.parameters.collateral_asset_id, - partial_collateral_amount, + 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, + EXERCISED_PREMIUM_AMOUNT, &receiver_script_pubkey, ); @@ -264,83 +140,53 @@ fn exercise_option_offer_with_change(context: simplex::TestContext) -> anyhow::R 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 option_offer = setup_offer_with_premium(&context, 1_000)?; 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, + 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, + 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 offer_utxos = require_offer_utxos( + &context, + &option_offer, + DEPOSIT_LBTC_AMOUNT, + EXPECTED_PREMIUM_AMOUNT, )?; let mut ft = FinalTransaction::new(); - - for utxo in [collateral_program_utxo, premium_program_utxo] { + for utxo in offer_utxos { ft.add_program_input( PartialInput::new(utxo), - ProgramInput::new( - Box::new(option_offer.get_program().clone()), - Box::new(OptionOffer::get_witness(exercise_branch)), - ), + offer_program_input(&option_offer, 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, + EXPECTED_SETTLEMENT_AMOUNT - 1, option_offer.parameters.settlement_asset_id, )); ft.add_output(PartialOutput::new( receiver_script_pubkey.clone(), - deposit_lbtc_amount, + DEPOSIT_LBTC_AMOUNT, option_offer.parameters.collateral_asset_id, )); ft.add_output(PartialOutput::new( receiver_script_pubkey.clone(), - expected_premium_amount, + EXPECTED_PREMIUM_AMOUNT, option_offer.parameters.premium_asset_id, )); ft.add_output(PartialOutput::new( @@ -357,18 +203,18 @@ fn exercise_option_offer_rejects_wrong_settlement_amount( .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, + assert_covenant_utxo( + &context, + &option_offer.get_script_pubkey(), option_offer.parameters.collateral_asset_id, - deposit_lbtc_amount, - ); - assert_has_utxo_by_asset_and_amount( - &covenant_utxos_after_rejection, + DEPOSIT_LBTC_AMOUNT, + )?; + assert_covenant_utxo( + &context, + &option_offer.get_script_pubkey(), option_offer.parameters.premium_asset_id, - expected_premium_amount, - ); + EXPECTED_PREMIUM_AMOUNT, + )?; Ok(()) } @@ -377,98 +223,64 @@ fn exercise_option_offer_rejects_wrong_settlement_amount( 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 option_offer = setup_offer_with_premium(&context, 1_000)?; 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, + EXERCISED_SETTLEMENT_AMOUNT, )?; - let program_utxos = provider.fetch_scripthash_utxos(&option_offer.get_script_pubkey())?; + // Partial exercise, but the change flag is not set. let exercise_branch = OptionOfferBranch::Exercise { - collateral_amount: partial_collateral_amount, + 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 offer_utxos = require_offer_utxos( + &context, + &option_offer, + DEPOSIT_LBTC_AMOUNT, + EXPECTED_PREMIUM_AMOUNT, )?; let mut ft = FinalTransaction::new(); - - for utxo in [collateral_program_utxo, premium_program_utxo] { + for utxo in offer_utxos { ft.add_program_input( PartialInput::new(utxo), - ProgramInput::new( - Box::new(option_offer.get_program().clone()), - Box::new(OptionOffer::get_witness(exercise_branch)), - ), + offer_program_input(&option_offer, 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, + REMAINING_COLLATERAL_AMOUNT, option_offer.parameters.collateral_asset_id, )); ft.add_output(PartialOutput::new( option_offer.get_script_pubkey(), - remaining_premium_amount, + REMAINING_PREMIUM_AMOUNT, option_offer.parameters.premium_asset_id, )); ft.add_output(PartialOutput::new( option_offer.get_script_pubkey(), - exercised_settlement_amount, + EXERCISED_SETTLEMENT_AMOUNT, option_offer.parameters.settlement_asset_id, )); ft.add_output(PartialOutput::new( receiver_script_pubkey.clone(), - partial_collateral_amount, + PARTIAL_COLLATERAL_AMOUNT, option_offer.parameters.collateral_asset_id, )); ft.add_output(PartialOutput::new( receiver_script_pubkey, - exercised_premium_amount, + EXERCISED_PREMIUM_AMOUNT, option_offer.parameters.premium_asset_id, )); @@ -480,18 +292,18 @@ fn exercise_option_offer_rejects_partial_exercise_without_change_flag( .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, + assert_covenant_utxo( + &context, + &option_offer.get_script_pubkey(), option_offer.parameters.collateral_asset_id, - deposit_lbtc_amount, - ); - assert_has_utxo_by_asset_and_amount( - &covenant_utxos_after_rejection, + DEPOSIT_LBTC_AMOUNT, + )?; + assert_covenant_utxo( + &context, + &option_offer.get_script_pubkey(), option_offer.parameters.premium_asset_id, - expected_premium_amount, - ); + EXPECTED_PREMIUM_AMOUNT, + )?; Ok(()) } diff --git a/crates/contracts/tests/regtest/option_offer/expiry.rs b/crates/contracts/tests/regtest/option_offer/expiry.rs index 5f0425b..e17bd36 100644 --- a/crates/contracts/tests/regtest/option_offer/expiry.rs +++ b/crates/contracts/tests/regtest/option_offer/expiry.rs @@ -1,96 +1,54 @@ -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::filters::{assert_covenant_utxo, assert_has_utxo_by_asset_amount_and_script}; use crate::common::signer::{finalize_and_broadcast, get_lbtc_utxo}; +use crate::common::{locked_input, locktime_from}; use crate::program_builder::option_offer::{ - create_option_offer_with_premium, prepare_option_offer, + DEPOSIT_LBTC_AMOUNT, EXPECTED_PREMIUM_AMOUNT, offer_program_input, require_offer_utxos, + setup_offer_with_premium, witness_with_user_sighash, }; -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 simplex::program::{ProgramError, ProgramTrait}; +use simplex::simplicityhl::elements::Sequence; +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature}; -use contracts::programs::option_offer::{OptionOffer, OptionOfferBranch}; +use contracts::programs::option_offer::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 = setup_offer_with_premium(&context, -50)?; - let option_offer = prepare_option_offer( - &context, - deposit_lbtc_amount, - expected_premium_amount, - expected_settlement_amount, - -50, - )?; - - create_option_offer_with_premium( + let locktime = locktime_from(option_offer.parameters.expiry_time)?; + let offer_utxos = require_offer_utxos( &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", + DEPOSIT_LBTC_AMOUNT, + EXPECTED_PREMIUM_AMOUNT, )?; let mut ft = FinalTransaction::new(); - for utxo in [collateral_program_utxo, premium_program_utxo] { + for utxo in offer_utxos { 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)), - ), + locked_input(utxo, locktime), + offer_program_input(&option_offer, 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), + locked_input(get_lbtc_utxo(&context)?, locktime), RequiredSignature::NativeEcdsa, ); let receiver_script_pubkey = signer.get_address().script_pubkey(); ft.add_output(PartialOutput::new( receiver_script_pubkey.clone(), - deposit_lbtc_amount, + DEPOSIT_LBTC_AMOUNT, option_offer.parameters.collateral_asset_id, )); ft.add_output(PartialOutput::new( receiver_script_pubkey.clone(), - expected_premium_amount, + EXPECTED_PREMIUM_AMOUNT, option_offer.parameters.premium_asset_id, )); @@ -100,13 +58,13 @@ fn expiry_option_offer(context: simplex::TestContext) -> anyhow::Result<()> { assert_has_utxo_by_asset_amount_and_script( &signer_utxos, option_offer.parameters.collateral_asset_id, - deposit_lbtc_amount, + 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, + EXPECTED_PREMIUM_AMOUNT, &receiver_script_pubkey, ); @@ -117,50 +75,22 @@ fn expiry_option_offer(context: simplex::TestContext) -> anyhow::Result<()> { 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, - )?; + let option_offer = setup_offer_with_premium(&context, -50)?; - create_option_offer_with_premium( + let offer_utxos = require_offer_utxos( &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", + DEPOSIT_LBTC_AMOUNT, + EXPECTED_PREMIUM_AMOUNT, )?; let mut ft = FinalTransaction::new(); - for utxo in [collateral_program_utxo, premium_program_utxo] { + for utxo in offer_utxos { 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)), - ), + offer_program_input(&option_offer, OptionOfferBranch::Expiry), RequiredSignature::Witness("USER_SIGHASH_ALL".to_string()), ); } @@ -172,50 +102,37 @@ fn expiry_option_offer_rejects_missing_locktime( let receiver_script_pubkey = signer.get_address().script_pubkey(); ft.add_output(PartialOutput::new( receiver_script_pubkey.clone(), - deposit_lbtc_amount, + DEPOSIT_LBTC_AMOUNT, option_offer.parameters.collateral_asset_id, )); ft.add_output(PartialOutput::new( receiver_script_pubkey, - expected_premium_amount, + 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 witness = + witness_with_user_sighash(&context, &option_offer, OptionOfferBranch::Expiry, &pst)?; let program_error = option_offer .get_program() - .finalize( - &pst, - &WitnessValues::from(signed_witness), - 0, - context.get_network(), - ) + .finalize(&pst, &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, + let script_pubkey = option_offer.get_script_pubkey(); + assert_covenant_utxo( + &context, + &script_pubkey, option_offer.parameters.collateral_asset_id, - deposit_lbtc_amount, - ); - assert_has_utxo_by_asset_and_amount( - &program_utxos_after_rejection, + DEPOSIT_LBTC_AMOUNT, + )?; + assert_covenant_utxo( + &context, + &script_pubkey, option_offer.parameters.premium_asset_id, - expected_premium_amount, - ); + EXPECTED_PREMIUM_AMOUNT, + )?; Ok(()) } diff --git a/crates/contracts/tests/regtest/option_offer/withdraw.rs b/crates/contracts/tests/regtest/option_offer/withdraw.rs index 081f1ed..aeaf16a 100644 --- a/crates/contracts/tests/regtest/option_offer/withdraw.rs +++ b/crates/contracts/tests/regtest/option_offer/withdraw.rs @@ -1,173 +1,117 @@ 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, + assert_covenant_utxo, assert_has_utxo_by_asset_amount_and_script, require_covenant_utxo, }; -use crate::common::signer::{ensure_exact_signer_utxo, finalize_and_broadcast, get_lbtc_utxo}; +use crate::common::signer::{finalize_and_broadcast, get_lbtc_utxo}; use crate::program_builder::option_offer::{ - create_option_offer_with_premium, prepare_option_offer, + DEPOSIT_LBTC_AMOUNT, EXERCISED_SETTLEMENT_AMOUNT, EXPECTED_PREMIUM_AMOUNT, + EXPECTED_SETTLEMENT_AMOUNT, REMAINING_COLLATERAL_AMOUNT, REMAINING_PREMIUM_AMOUNT, + create_option_offer_with_premium, exercise_offer_partially, offer_program_input, + prepare_option_offer, setup_offer_with_premium, witness_with_user_sighash, }; -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 simplex::program::{ProgramError, ProgramTrait}; +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, 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, +/// Build a withdraw transaction spending the settlement covenant UTXO left by +/// [`exercise_offer_partially`] into the given outputs. +fn build_withdraw_ft( + context: &simplex::TestContext, + option_offer: &OptionOffer, + outputs: Vec, +) -> anyhow::Result { + let settlement_program_utxo = require_covenant_utxo( + context, + &option_offer.get_script_pubkey(), + option_offer.parameters.settlement_asset_id, + EXERCISED_SETTLEMENT_AMOUNT, + "missing settlement covenant utxo", )?; - create_option_offer_with_premium( - &context, - &option_offer, - deposit_lbtc_amount, - expected_premium_amount, - )?; + let mut ft = FinalTransaction::new(); + ft.add_program_input( + PartialInput::new(settlement_program_utxo), + offer_program_input(option_offer, OptionOfferBranch::Withdraw), + RequiredSignature::Witness("USER_SIGHASH_ALL".to_string()), + ); + ft.add_input( + PartialInput::new(get_lbtc_utxo(context)?), + RequiredSignature::NativeEcdsa, + ); + for output in outputs { + ft.add_output(output); + } - 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, - )?; + Ok(ft) +} - 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, +fn assert_covenant_untouched_after_rejection( + context: &simplex::TestContext, + option_offer: &OptionOffer, +) -> anyhow::Result<()> { + let script_pubkey = option_offer.get_script_pubkey(); + assert_covenant_utxo( + context, + &script_pubkey, option_offer.parameters.collateral_asset_id, - deposit_lbtc_amount, - "missing collateral covenant utxo", + REMAINING_COLLATERAL_AMOUNT, )?; - let premium_program_utxo = require_utxo_by_asset_and_amount( - &program_utxos, + assert_covenant_utxo( + context, + &script_pubkey, option_offer.parameters.premium_asset_id, - expected_premium_amount, - "missing premium covenant utxo", + REMAINING_PREMIUM_AMOUNT, )?; - - 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, + assert_covenant_utxo( + context, + &script_pubkey, 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, - )); + EXERCISED_SETTLEMENT_AMOUNT, + )?; + Ok(()) +} - let _ = finalize_and_broadcast(&context, &exercise_ft)?; +#[simplex::test] +fn withdraw_option_offer(context: simplex::TestContext) -> anyhow::Result<()> { + let signer = context.get_default_signer(); - 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 option_offer = setup_offer_with_premium(&context, 1_000)?; + let _ = exercise_offer_partially(&context, &option_offer)?; - 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 receiver_script_pubkey = signer.get_address().script_pubkey(); + let withdraw_ft = build_withdraw_ft( + &context, + &option_offer, + vec![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, + let script_pubkey = option_offer.get_script_pubkey(); + assert_covenant_utxo( + &context, + &script_pubkey, option_offer.parameters.collateral_asset_id, - remaining_collateral_amount, - ); - assert_has_utxo_by_asset_and_amount( - &program_utxos_after_withdraw, + REMAINING_COLLATERAL_AMOUNT, + )?; + assert_covenant_utxo( + &context, + &script_pubkey, option_offer.parameters.premium_asset_id, - remaining_premium_amount, - ); + 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, + EXERCISED_SETTLEMENT_AMOUNT, &receiver_script_pubkey, ); @@ -178,23 +122,14 @@ fn withdraw_option_offer(context: simplex::TestContext) -> anyhow::Result<()> { 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; - + // Lock the offer to a pubkey that does not belong to the default signer. let prepared_option_offer = prepare_option_offer( &context, - deposit_lbtc_amount, - expected_premium_amount, - expected_settlement_amount, + DEPOSIT_LBTC_AMOUNT, + EXPECTED_PREMIUM_AMOUNT, + EXPECTED_SETTLEMENT_AMOUNT, 1_000, )?; let mismatched_user_signer = context.create_signer( @@ -207,148 +142,31 @@ fn withdraw_option_offer_rejects_invalid_signature( create_option_offer_with_premium( &context, &option_offer, - deposit_lbtc_amount, - expected_premium_amount, + DEPOSIT_LBTC_AMOUNT, + EXPECTED_PREMIUM_AMOUNT, )?; + let _ = exercise_offer_partially(&context, &option_offer)?; - let receiver_script_pubkey = signer.get_address().script_pubkey(); - let settlement_input = ensure_exact_signer_utxo( + let withdraw_ft = build_withdraw_ft( &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", + &option_offer, + vec![PartialOutput::new( + signer.get_address().script_pubkey(), + EXERCISED_SETTLEMENT_AMOUNT, + option_offer.parameters.settlement_asset_id, + )], )?; - 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 witness = + witness_with_user_sighash(&context, &option_offer, OptionOfferBranch::Withdraw, &pst)?; let program_error = option_offer .get_program() - .finalize( - &pst, - &WitnessValues::from(signed_witness), - 0, - context.get_network(), - ) + .finalize(&pst, &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, - ); + assert_covenant_untouched_after_rejection(&context, &option_offer)?; Ok(()) } @@ -357,176 +175,41 @@ fn withdraw_option_offer_rejects_invalid_signature( 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 option_offer = setup_offer_with_premium(&context, 1_000)?; + let _ = exercise_offer_partially(&context, &option_offer)?; + // Split the settlement across two outputs; the covenant requires the full + // amount at output index 0. let receiver_script_pubkey = signer.get_address().script_pubkey(); - let settlement_input = ensure_exact_signer_utxo( + let withdraw_ft = build_withdraw_ft( &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)), + &option_offer, + vec![ + PartialOutput::new( + receiver_script_pubkey.clone(), + EXERCISED_SETTLEMENT_AMOUNT - 1, + option_offer.parameters.settlement_asset_id, ), - 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", + PartialOutput::new( + receiver_script_pubkey, + 1, + option_offer.parameters.settlement_asset_id, + ), + ], )?; - 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 witness = + witness_with_user_sighash(&context, &option_offer, OptionOfferBranch::Withdraw, &pst)?; let program_error = option_offer .get_program() - .finalize( - &pst, - &WitnessValues::from(signed_witness), - 0, - context.get_network(), - ) + .finalize(&pst, &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, - ); + assert_covenant_untouched_after_rejection(&context, &option_offer)?; Ok(()) } diff --git a/crates/contracts/tests/regtest/options/cancel.rs b/crates/contracts/tests/regtest/options/cancel.rs index 3f4ece4..4669a4e 100644 --- a/crates/contracts/tests/regtest/options/cancel.rs +++ b/crates/contracts/tests/regtest/options/cancel.rs @@ -1,95 +1,64 @@ -use crate::common::filters::{ - assert_has_utxo_by_asset_and_amount, require_utxo_by_asset_and_amount, -}; +use crate::common::filters::{assert_covenant_utxo, assert_has_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 crate::program_builder::options::{ + CONTRACT_COUNT, TOTAL_COLLATERAL_AMOUNT, options_program_input, require_locked_collateral, + setup_funded_options, +}; -use contracts::programs::options::{Options, OptionsBranch}; +use contracts::programs::options::OptionsBranch; use contracts::programs::program::SimplexProgram; use simplex::simplicityhl::elements::Script; -use simplex::transaction::{ - FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, -}; +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, 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 funded = setup_funded_options(&context, -100, 1_000)?; + let parameters = &funded.options.parameters; - 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 option_token_input = + ensure_exact_signer_utxo(&context, parameters.option_token_asset, CONTRACT_COUNT)?; + let grantor_token_input = + ensure_exact_signer_utxo(&context, parameters.grantor_token_asset, CONTRACT_COUNT)?; + let locked_collateral = require_locked_collateral(&context, &funded)?; 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 { + options_program_input( + &funded.options, + OptionsBranch::Cancel { is_change_needed: false, - amount_to_burn: contract_count, - collateral_amount: total_collateral_amount, - })), + 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, - ); + for input in [ + option_token_input, + grantor_token_input, + get_lbtc_utxo(&context)?, + ] { + ft.add_input(PartialInput::new(input), RequiredSignature::NativeEcdsa); + } ft.add_output(PartialOutput::new( Script::new_op_return(b"burn"), - contract_count, - funded.options.parameters.option_token_asset, + CONTRACT_COUNT, + parameters.option_token_asset, )); ft.add_output(PartialOutput::new( Script::new_op_return(b"burn"), - contract_count, - funded.options.parameters.grantor_token_asset, + CONTRACT_COUNT, + parameters.grantor_token_asset, )); ft.add_output(PartialOutput::new( signer.get_address().script_pubkey(), - total_collateral_amount, - funded.options.parameters.collateral_asset_id, + TOTAL_COLLATERAL_AMOUNT, + parameters.collateral_asset_id, )); let cancel_txid = finalize_and_broadcast(&context, &ft)?; @@ -105,18 +74,18 @@ fn cancel_options(context: simplex::TestContext) -> anyhow::Result<()> { ); assert_eq!( transaction.output[2].asset.explicit(), - Some(funded.options.parameters.collateral_asset_id) + Some(parameters.collateral_asset_id) ); assert_eq!( transaction.output[2].value.explicit(), - Some(total_collateral_amount) + 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, + parameters.collateral_asset_id, + TOTAL_COLLATERAL_AMOUNT, ); Ok(()) @@ -126,87 +95,64 @@ fn cancel_options(context: simplex::TestContext) -> anyhow::Result<()> { 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 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 funded = setup_funded_options(&context, -100, 1_000)?; + let parameters = &funded.options.parameters; let option_token_input = ensure_exact_signer_utxo( &context, - funded.options.parameters.option_token_asset, + parameters.option_token_asset, cancelled_contract_count, )?; let grantor_token_input = ensure_exact_signer_utxo( &context, - funded.options.parameters.grantor_token_asset, + 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 locked_collateral = require_locked_collateral(&context, &funded)?; 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 { + options_program_input( + &funded.options, + 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, - ); + for input in [ + option_token_input, + grantor_token_input, + get_lbtc_utxo(&context)?, + ] { + ft.add_input(PartialInput::new(input), RequiredSignature::NativeEcdsa); + } ft.add_output(PartialOutput::new( funded.options.get_script_pubkey(), remaining_collateral_amount, - funded.options.parameters.collateral_asset_id, + parameters.collateral_asset_id, )); ft.add_output(PartialOutput::new( Script::new_op_return(b"burn"), cancelled_contract_count, - funded.options.parameters.option_token_asset, + parameters.option_token_asset, )); ft.add_output(PartialOutput::new( Script::new_op_return(b"burn"), cancelled_contract_count, - funded.options.parameters.grantor_token_asset, + 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, + parameters.collateral_asset_id, )); let cancel_txid = finalize_and_broadcast(&context, &ft)?; @@ -233,13 +179,12 @@ fn cancel_options_with_change(context: simplex::TestContext) -> anyhow::Result<( 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, + assert_covenant_utxo( + &context, + &funded.options.get_script_pubkey(), + 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 index 63183cc..ef25dba 100644 --- a/crates/contracts/tests/regtest/options/exercise.rs +++ b/crates/contracts/tests/regtest/options/exercise.rs @@ -1,118 +1,36 @@ -use crate::common::filters::{ - assert_has_utxo_by_asset_and_amount, require_utxo_by_asset_and_amount, -}; +use crate::common::filters::assert_covenant_utxo; +use crate::common::filters::assert_has_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 crate::common::{locked_input, locktime_from}; +use crate::program_builder::options::{ + CONTRACT_COUNT, EXPECTED_SETTLEMENT_AMOUNT, TOTAL_COLLATERAL_AMOUNT, exercise_options_fully, + options_program_input, require_locked_collateral, setup_funded_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, -}; +use simplex::simplicityhl::elements::{Script, Sequence}; +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, 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 funded = setup_funded_options(&context, -100, 1_000)?; + let exercise_txid = exercise_options_fully(&context, &funded)?; + let parameters = &funded.options.parameters; 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) + Some(parameters.option_token_asset) ); - assert_eq!(transaction.output[0].value.explicit(), Some(contract_count)); + assert_eq!(transaction.output[0].value.explicit(), Some(CONTRACT_COUNT)); assert_eq!( transaction.output[0].script_pubkey, Script::new_op_return(b"burn") @@ -120,11 +38,11 @@ fn exercise_options(context: simplex::TestContext) -> anyhow::Result<()> { assert_eq!( transaction.output[1].asset.explicit(), - Some(funded.options.parameters.settlement_asset_id) + Some(parameters.settlement_asset_id) ); assert_eq!( transaction.output[1].value.explicit(), - Some(expected_settlement_amount) + Some(EXPECTED_SETTLEMENT_AMOUNT) ); assert_eq!( transaction.output[1].script_pubkey, @@ -133,27 +51,26 @@ fn exercise_options(context: simplex::TestContext) -> anyhow::Result<()> { assert_eq!( transaction.output[2].asset.explicit(), - Some(funded.options.parameters.collateral_asset_id) + Some(parameters.collateral_asset_id) ); assert_eq!( transaction.output[2].value.explicit(), - Some(total_collateral_amount) + 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, - ); + assert_covenant_utxo( + &context, + &funded.options.get_script_pubkey(), + 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, + parameters.collateral_asset_id, + TOTAL_COLLATERAL_AMOUNT, ); Ok(()) @@ -164,100 +81,71 @@ 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 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 funded = setup_funded_options(&context, -100, 1_000)?; + let parameters = &funded.options.parameters; let receiver_script_pubkey = signer.get_address().script_pubkey(); let option_token_input = ensure_exact_signer_utxo( &context, - funded.options.parameters.option_token_asset, + parameters.option_token_asset, exercised_contract_count, )?; let settlement_input = ensure_exact_signer_utxo( &context, - funded.options.parameters.settlement_asset_id, + 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 locktime = locktime_from(parameters.start_time)?; + let locked_collateral = require_locked_collateral(&context, &funded)?; 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 { + locked_input(locked_collateral, locktime), + options_program_input( + &funded.options, + 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, - ); + for input in [ + option_token_input, + settlement_input, + get_lbtc_utxo(&context)?, + ] { + ft.add_input( + locked_input(input, locktime), + RequiredSignature::NativeEcdsa, + ); + } ft.add_output(PartialOutput::new( funded.options.get_script_pubkey(), remaining_collateral_amount, - funded.options.parameters.collateral_asset_id, + parameters.collateral_asset_id, )); ft.add_output(PartialOutput::new( Script::new_op_return(b"burn"), exercised_contract_count, - funded.options.parameters.option_token_asset, + parameters.option_token_asset, )); ft.add_output(PartialOutput::new( funded.options.get_script_pubkey(), exercised_settlement_amount, - funded.options.parameters.settlement_asset_id, + parameters.settlement_asset_id, )); ft.add_output(PartialOutput::new( receiver_script_pubkey.clone(), exercised_collateral_amount, - funded.options.parameters.collateral_asset_id, + parameters.collateral_asset_id, )); let exercise_txid = finalize_and_broadcast(&context, &ft)?; @@ -289,110 +177,76 @@ fn exercise_options_with_change(context: simplex::TestContext) -> anyhow::Result ); 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, + assert_covenant_utxo( + &context, + &funded.options.get_script_pubkey(), + parameters.collateral_asset_id, remaining_collateral_amount, - ); - assert_has_utxo_by_asset_and_amount( - &covenant_utxos_after_exercise, - funded.options.parameters.settlement_asset_id, + )?; + assert_covenant_utxo( + &context, + &funded.options.get_script_pubkey(), + 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 funded = setup_funded_options(&context, -100, 1_000)?; + let parameters = &funded.options.parameters; - 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 option_token_input = + ensure_exact_signer_utxo(&context, 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", + parameters.settlement_asset_id, + EXPECTED_SETTLEMENT_AMOUNT, )?; + let locked_collateral = require_locked_collateral(&context, &funded)?; + let exercise_branch = OptionsBranch::Exercise { + is_change_needed: false, + amount_to_burn: CONTRACT_COUNT, + collateral_amount: TOTAL_COLLATERAL_AMOUNT, + settlement_amount: EXPECTED_SETTLEMENT_AMOUNT, + }; 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, - })), - ), + options_program_input(&funded.options, exercise_branch), 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, - ); + for input in [ + option_token_input, + settlement_input, + get_lbtc_utxo(&context)?, + ] { + ft.add_input( + PartialInput::new(input).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, + CONTRACT_COUNT, + parameters.option_token_asset, )); ft.add_output(PartialOutput::new( funded.options.get_script_pubkey(), - expected_settlement_amount, - funded.options.parameters.settlement_asset_id, + EXPECTED_SETTLEMENT_AMOUNT, + 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, + TOTAL_COLLATERAL_AMOUNT, + 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 witness = Options::get_witness(exercise_branch).build_witness(); let program_error = funded .options .get_program() @@ -400,13 +254,12 @@ fn exercise_options_rejects_missing_locktime(context: simplex::TestContext) -> a .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, - ); + assert_covenant_utxo( + &context, + &funded.options.get_script_pubkey(), + 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 index eae3038..e3c1c60 100644 --- a/crates/contracts/tests/regtest/options/expiry.rs +++ b/crates/contracts/tests/regtest/options/expiry.rs @@ -1,90 +1,59 @@ -use crate::common::filters::{ - assert_has_utxo_by_asset_and_amount, require_utxo_by_asset_and_amount, -}; +use crate::common::filters::{assert_covenant_utxo, assert_has_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 crate::common::{locked_input, locktime_from}; +use crate::program_builder::options::{ + CONTRACT_COUNT, TOTAL_COLLATERAL_AMOUNT, options_program_input, require_locked_collateral, + setup_funded_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, -}; +use simplex::simplicityhl::elements::{Script, Sequence}; +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, 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 funded = setup_funded_options(&context, -100, -50)?; + let parameters = &funded.options.parameters; - 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 grantor_token_input = + ensure_exact_signer_utxo(&context, parameters.grantor_token_asset, CONTRACT_COUNT)?; + let locktime = locktime_from(parameters.expiry_time)?; + let locked_collateral = require_locked_collateral(&context, &funded)?; 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 { + locked_input(locked_collateral, locktime), + options_program_input( + &funded.options, + OptionsBranch::Expiry { is_change_needed: false, - amount_to_burn: contract_count, - collateral_amount: total_collateral_amount, - })), + 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, - ); + for input in [grantor_token_input, get_lbtc_utxo(&context)?] { + ft.add_input( + locked_input(input, locktime), + RequiredSignature::NativeEcdsa, + ); + } ft.add_output(PartialOutput::new( Script::new_op_return(b"burn"), - contract_count, - funded.options.parameters.grantor_token_asset, + CONTRACT_COUNT, + parameters.grantor_token_asset, )); ft.add_output(PartialOutput::new( signer.get_address().script_pubkey(), - total_collateral_amount, - funded.options.parameters.collateral_asset_id, + TOTAL_COLLATERAL_AMOUNT, + parameters.collateral_asset_id, )); let expiry_txid = finalize_and_broadcast(&context, &ft)?; @@ -96,16 +65,16 @@ fn expire_options(context: simplex::TestContext) -> anyhow::Result<()> { ); assert_eq!( transaction.output[0].asset.explicit(), - Some(funded.options.parameters.grantor_token_asset) + Some(parameters.grantor_token_asset) ); - assert_eq!(transaction.output[0].value.explicit(), Some(contract_count)); + assert_eq!(transaction.output[0].value.explicit(), Some(CONTRACT_COUNT)); assert_eq!( transaction.output[1].asset.explicit(), - Some(funded.options.parameters.collateral_asset_id) + Some(parameters.collateral_asset_id) ); assert_eq!( transaction.output[1].value.explicit(), - Some(total_collateral_amount) + Some(TOTAL_COLLATERAL_AMOUNT) ); assert_eq!( transaction.output[1].script_pubkey, @@ -115,8 +84,8 @@ fn expire_options(context: simplex::TestContext) -> anyhow::Result<()> { 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, + parameters.collateral_asset_id, + TOTAL_COLLATERAL_AMOUNT, ); Ok(()) @@ -126,81 +95,54 @@ fn expire_options(context: simplex::TestContext) -> anyhow::Result<()> { 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 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 funded = setup_funded_options(&context, -100, -50)?; + let parameters = &funded.options.parameters; let grantor_token_input = ensure_exact_signer_utxo( &context, - funded.options.parameters.grantor_token_asset, + 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 locktime = locktime_from(parameters.expiry_time)?; + let locked_collateral = require_locked_collateral(&context, &funded)?; 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 { + locked_input(locked_collateral, locktime), + options_program_input( + &funded.options, + 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, - ); + for input in [grantor_token_input, get_lbtc_utxo(&context)?] { + ft.add_input( + locked_input(input, locktime), + RequiredSignature::NativeEcdsa, + ); + } ft.add_output(PartialOutput::new( funded.options.get_script_pubkey(), remaining_collateral_amount, - funded.options.parameters.collateral_asset_id, + parameters.collateral_asset_id, )); ft.add_output(PartialOutput::new( Script::new_op_return(b"burn"), expired_contract_count, - funded.options.parameters.grantor_token_asset, + 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, + parameters.collateral_asset_id, )); let expiry_txid = finalize_and_broadcast(&context, &ft)?; @@ -223,89 +165,55 @@ fn expire_options_with_change(context: simplex::TestContext) -> anyhow::Result<( 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, + assert_covenant_utxo( + &context, + &funded.options.get_script_pubkey(), + 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 funded = setup_funded_options(&context, -100, -50)?; + let parameters = &funded.options.parameters; - 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 grantor_token_input = + ensure_exact_signer_utxo(&context, parameters.grantor_token_asset, CONTRACT_COUNT)?; + let locked_collateral = require_locked_collateral(&context, &funded)?; + let expiry_branch = OptionsBranch::Expiry { + is_change_needed: false, + amount_to_burn: CONTRACT_COUNT, + collateral_amount: TOTAL_COLLATERAL_AMOUNT, + }; 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, - })), - ), + options_program_input(&funded.options, expiry_branch), 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, - ); + for input in [grantor_token_input, get_lbtc_utxo(&context)?] { + ft.add_input( + PartialInput::new(input).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, + CONTRACT_COUNT, + 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, + TOTAL_COLLATERAL_AMOUNT, + 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 witness = Options::get_witness(expiry_branch).build_witness(); let program_error = funded .options .get_program() @@ -313,13 +221,12 @@ fn expire_options_rejects_missing_locktime(context: simplex::TestContext) -> any .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, - ); + assert_covenant_utxo( + &context, + &funded.options.get_script_pubkey(), + 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 index 0d57777..e0dd008 100644 --- a/crates/contracts/tests/regtest/options/fund.rs +++ b/crates/contracts/tests/regtest/options/fund.rs @@ -1,7 +1,7 @@ -use crate::common::filters::{ - assert_has_utxo_by_asset_amount_and_script, assert_has_utxo_by_asset_and_amount, +use crate::common::filters::{assert_covenant_utxo, assert_has_utxo_by_asset_amount_and_script}; +use crate::program_builder::options::{ + CONTRACT_COUNT, TOTAL_COLLATERAL_AMOUNT, setup_funded_options, }; -use crate::program_builder::options::{create_options, fund_options, prepare_options}; use contracts::programs::program::SimplexProgram; @@ -10,26 +10,14 @@ 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 funded = setup_funded_options(&context, -100, 1_000)?; + let parameters = &funded.options.parameters; 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 + parameters.option_reissuance_token_asset ); assert_eq!(funded.option_reissuance_token.amount(), 1); assert_eq!( @@ -40,7 +28,7 @@ fn fund_options_contract(context: simplex::TestContext) -> anyhow::Result<()> { assert_eq!(funded.grantor_reissuance_token.outpoint.vout, 1); assert_eq!( funded.grantor_reissuance_token.asset(), - funded.options.parameters.grantor_reissuance_token_asset + parameters.grantor_reissuance_token_asset ); assert_eq!(funded.grantor_reissuance_token.amount(), 1); assert_eq!( @@ -50,11 +38,11 @@ fn fund_options_contract(context: simplex::TestContext) -> anyhow::Result<()> { assert_eq!( funding_tx.output[2].asset.explicit(), - Some(funded.options.parameters.collateral_asset_id) + Some(parameters.collateral_asset_id) ); assert_eq!( funding_tx.output[2].value.explicit(), - Some(total_collateral_amount) + Some(TOTAL_COLLATERAL_AMOUNT) ); assert_eq!( funding_tx.output[2].script_pubkey, @@ -63,9 +51,9 @@ fn fund_options_contract(context: simplex::TestContext) -> anyhow::Result<()> { assert_eq!( funding_tx.output[3].asset.explicit(), - Some(funded.options.parameters.option_token_asset) + Some(parameters.option_token_asset) ); - assert_eq!(funding_tx.output[3].value.explicit(), Some(contract_count)); + assert_eq!(funding_tx.output[3].value.explicit(), Some(CONTRACT_COUNT)); assert_eq!( funding_tx.output[3].script_pubkey, signer.get_address().script_pubkey() @@ -73,43 +61,33 @@ fn fund_options_contract(context: simplex::TestContext) -> anyhow::Result<()> { assert_eq!( funding_tx.output[4].asset.explicit(), - Some(funded.options.parameters.grantor_token_asset) + Some(parameters.grantor_token_asset) ); - assert_eq!(funding_tx.output[4].value.explicit(), Some(contract_count)); + 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, - ); + assert_covenant_utxo( + &context, + &funded.options.get_script_pubkey(), + 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, + 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, + parameters.grantor_token_asset, + CONTRACT_COUNT, &receiver_script_pubkey, ); diff --git a/crates/contracts/tests/regtest/options/settlement.rs b/crates/contracts/tests/regtest/options/settlement.rs index f337fb9..f424956 100644 --- a/crates/contracts/tests/regtest/options/settlement.rs +++ b/crates/contracts/tests/regtest/options/settlement.rs @@ -1,161 +1,70 @@ use crate::common::filters::{ - assert_has_utxo_by_asset_and_amount, require_utxo_by_asset_and_amount, + assert_covenant_utxo, assert_has_utxo_by_asset_and_amount, require_covenant_utxo, }; 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 crate::common::{locked_input, locktime_from}; +use crate::program_builder::options::{ + CONTRACT_COUNT, EXPECTED_SETTLEMENT_AMOUNT, exercise_options_fully, options_program_input, + setup_funded_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, -}; +use simplex::simplicityhl::elements::{Script, Sequence}; +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, 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 funded = setup_funded_options(&context, -100, 1_000)?; + let _ = exercise_options_fully(&context, &funded)?; - 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( + let parameters = &funded.options.parameters; + let locktime = locktime_from(parameters.start_time)?; + let locked_settlement = require_covenant_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, + &funded.options.get_script_pubkey(), + 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 grantor_token_input = + ensure_exact_signer_utxo(&context, 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 { + locked_input(locked_settlement, locktime), + options_program_input( + &funded.options, + OptionsBranch::Settlement { is_change_needed: false, - amount_to_burn: contract_count, - settlement_amount: expected_settlement_amount, - })), + 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), + locked_input(grantor_token_input, locktime), RequiredSignature::NativeEcdsa, ); settlement_ft.add_input( - PartialInput::new(get_lbtc_utxo(&context)?) - .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) - .with_locktime(locktime), + locked_input(get_lbtc_utxo(&context)?, locktime), RequiredSignature::NativeEcdsa, ); settlement_ft.add_output(PartialOutput::new( Script::new_op_return(b"burn"), - contract_count, - funded.options.parameters.grantor_token_asset, + CONTRACT_COUNT, + parameters.grantor_token_asset, )); settlement_ft.add_output(PartialOutput::new( signer.get_address().script_pubkey(), - expected_settlement_amount, - funded.options.parameters.settlement_asset_id, + EXPECTED_SETTLEMENT_AMOUNT, + parameters.settlement_asset_id, )); let settlement_txid = finalize_and_broadcast(&context, &settlement_ft)?; @@ -167,16 +76,16 @@ fn settle_options(context: simplex::TestContext) -> anyhow::Result<()> { ); assert_eq!( transaction.output[0].asset.explicit(), - Some(funded.options.parameters.grantor_token_asset) + Some(parameters.grantor_token_asset) ); - assert_eq!(transaction.output[0].value.explicit(), Some(contract_count)); + assert_eq!(transaction.output[0].value.explicit(), Some(CONTRACT_COUNT)); assert_eq!( transaction.output[1].asset.explicit(), - Some(funded.options.parameters.settlement_asset_id) + Some(parameters.settlement_asset_id) ); assert_eq!( transaction.output[1].value.explicit(), - Some(expected_settlement_amount) + Some(EXPECTED_SETTLEMENT_AMOUNT) ); assert_eq!( transaction.output[1].script_pubkey, @@ -186,8 +95,8 @@ fn settle_options(context: simplex::TestContext) -> anyhow::Result<()> { 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, + parameters.settlement_asset_id, + EXPECTED_SETTLEMENT_AMOUNT, ); Ok(()) @@ -198,152 +107,63 @@ 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 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 funded = setup_funded_options(&context, -100, 1_000)?; + let _ = exercise_options_fully(&context, &funded)?; - let option_token_input = ensure_exact_signer_utxo( - &context, - funded.options.parameters.option_token_asset, - contract_count, - )?; - let settlement_input = ensure_exact_signer_utxo( + let parameters = &funded.options.parameters; + let locktime = locktime_from(parameters.start_time)?; + let locked_settlement = require_covenant_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, + &funded.options.get_script_pubkey(), + 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, + 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 { + locked_input(locked_settlement, locktime), + options_program_input( + &funded.options, + 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), + locked_input(grantor_token_input, locktime), RequiredSignature::NativeEcdsa, ); settlement_ft.add_input( - PartialInput::new(get_lbtc_utxo(&context)?) - .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) - .with_locktime(locktime), + locked_input(get_lbtc_utxo(&context)?, locktime), RequiredSignature::NativeEcdsa, ); settlement_ft.add_output(PartialOutput::new( funded.options.get_script_pubkey(), remaining_settlement_amount, - funded.options.parameters.settlement_asset_id, + 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, + parameters.grantor_token_asset, )); settlement_ft.add_output(PartialOutput::new( signer.get_address().script_pubkey(), settled_settlement_amount, - funded.options.parameters.settlement_asset_id, + parameters.settlement_asset_id, )); let settlement_txid = finalize_and_broadcast(&context, &settlement_ft)?; @@ -370,134 +190,41 @@ fn settle_options_with_change(context: simplex::TestContext) -> anyhow::Result<( 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, + assert_covenant_utxo( + &context, + &funded.options.get_script_pubkey(), + 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 funded = setup_funded_options(&context, -100, 1_000)?; + let _ = exercise_options_fully(&context, &funded)?; - let prepared = prepare_options( + let parameters = &funded.options.parameters; + let locked_settlement = require_covenant_utxo( &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, + &funded.options.get_script_pubkey(), + 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 grantor_token_input = + ensure_exact_signer_utxo(&context, parameters.grantor_token_asset, CONTRACT_COUNT)?; + let settlement_branch = OptionsBranch::Settlement { + is_change_needed: false, + amount_to_burn: CONTRACT_COUNT, + settlement_amount: EXPECTED_SETTLEMENT_AMOUNT, + }; 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, - })), - ), + options_program_input(&funded.options, settlement_branch), RequiredSignature::None, ); settlement_ft.add_input( @@ -510,22 +237,17 @@ fn settle_options_rejects_missing_locktime(context: simplex::TestContext) -> any ); settlement_ft.add_output(PartialOutput::new( Script::new_op_return(b"burn"), - contract_count, - funded.options.parameters.grantor_token_asset, + CONTRACT_COUNT, + 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, + EXPECTED_SETTLEMENT_AMOUNT, + 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 witness = Options::get_witness(settlement_branch).build_witness(); let program_error = funded .options .get_program() @@ -533,13 +255,12 @@ fn settle_options_rejects_missing_locktime(context: simplex::TestContext) -> any .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, - ); + assert_covenant_utxo( + &context, + &funded.options.get_script_pubkey(), + parameters.settlement_asset_id, + EXPECTED_SETTLEMENT_AMOUNT, + )?; Ok(()) }