From ec8773cc8c92053e0fa513a6f2b50feeafd1b88c Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Fri, 20 Feb 2026 16:14:08 +0200 Subject: [PATCH] Introduce wallet ABI. Major project refactoring. --- .github/workflows/publish-core.yml | 66 - .github/workflows/tests.yml | 56 +- Cargo.lock | 3922 ++++++++++++++--- Cargo.toml | 10 +- README.md | 43 +- crates/cli/.env.example | 7 +- crates/cli/Cargo.toml | 11 +- crates/cli/README.md | 64 +- crates/cli/assets/example-run.md | 155 - crates/cli/src/commands/basic.rs | 640 ++- crates/cli/src/commands/mod.rs | 9 +- crates/cli/src/commands/option_offer.rs | 513 +++ crates/cli/src/commands/options.rs | 844 ---- crates/cli/src/commands/smt_storage.rs | 217 - crates/cli/src/explorer.rs | 224 - crates/cli/src/lib.rs | 1 - crates/cli/src/main.rs | 83 +- crates/cli/src/modules/keys.rs | 21 - crates/cli/src/modules/mod.rs | 2 - crates/cli/src/modules/settings.rs | 32 - crates/cli/src/modules/store.rs | 130 +- crates/cli/src/modules/utils.rs | 44 +- crates/contracts/Cargo.toml | 18 +- crates/contracts/README.md | 17 +- crates/contracts/src/arguments_helpers.rs | 93 - crates/contracts/src/error.rs | 57 - .../src/finance/dcd/build_arguments.rs | 566 --- .../src/finance/dcd/build_witness.rs | 194 - crates/contracts/src/finance/dcd/mod.rs | 1949 -------- .../source_simf/dual_currency_deposit.simf | 592 --- crates/contracts/src/finance/mod.rs | 2 - .../finance/option_offer/build_arguments.rs | 103 +- .../src/finance/option_offer/build_witness.rs | 54 +- .../contracts/src/finance/option_offer/mod.rs | 1045 +++-- .../source_simf/option_offer.simf | 8 +- .../src/finance/options/build_arguments.rs | 442 +- .../src/finance/options/build_witness.rs | 4 +- crates/contracts/src/finance/options/mod.rs | 768 +--- .../finance/options/source_simf/options.simf | 28 +- crates/contracts/src/lib.rs | 32 +- crates/contracts/src/sdk/basic/issue_asset.rs | 98 - crates/contracts/src/sdk/basic/mod.rs | 11 - .../contracts/src/sdk/basic/reissue_asset.rs | 108 - .../src/sdk/basic/split_native_any.rs | 64 - .../contracts/src/sdk/basic/transfer_asset.rs | 84 - .../src/sdk/basic/transfer_native.rs | 58 - crates/contracts/src/sdk/finance/mod.rs | 9 - .../src/sdk/finance/option_offer/deposit.rs | 169 - .../src/sdk/finance/option_offer/exercise.rs | 234 - .../src/sdk/finance/option_offer/expiry.rs | 127 - .../src/sdk/finance/option_offer/mod.rs | 9 - .../src/sdk/finance/option_offer/withdraw.rs | 93 - .../finance/options/cancellation_option.rs | 152 - .../sdk/finance/options/creation_option.rs | 191 - .../sdk/finance/options/exercise_option.rs | 204 - .../src/sdk/finance/options/expiry_option.rs | 144 - .../src/sdk/finance/options/funding_option.rs | 229 - .../contracts/src/sdk/finance/options/mod.rs | 13 - .../sdk/finance/options/settlement_option.rs | 152 - .../test_data/option_arguments.hex | 1 - .../issuance_validation/test_data/taproot.hex | 1 - crates/contracts/src/sdk/mod.rs | 19 - .../src/sdk/storage/get_storage_address.rs | 48 - crates/contracts/src/sdk/storage/mod.rs | 5 - .../storage/transfer_from_storage_address.rs | 85 - .../contracts/src/sdk/taproot_pubkey_gen.rs | 200 - crates/contracts/src/sdk/validation.rs | 110 - .../array_tr_storage/build_witness.rs | 0 .../array_tr_storage/mod.rs | 25 +- .../source_simf/array_tr_storage.simf | 0 .../bytes32_tr_storage/build_witness.rs | 0 .../bytes32_tr_storage/mod.rs | 21 +- .../source_simf/bytes32_tr_storage.simf | 0 crates/contracts/src/state_management/mod.rs | 8 + .../simple_storage/build_arguments.rs | 2 +- .../simple_storage/build_witness.rs | 0 .../simple_storage/mod.rs | 16 +- .../source_simf/simple_storage.simf | 0 .../smt_storage/build_witness.rs | 0 .../{ => state_management}/smt_storage/mod.rs | 29 +- .../{ => state_management}/smt_storage/smt.rs | 16 +- .../smt_storage/source_simf/smt_storage.simf | 0 .../contracts/src/utils/arguments_helpers.rs | 72 + crates/contracts/src/utils/mod.rs | 4 + crates/contracts/src/utils/test_setup.rs | 191 + crates/simplicityhl-core/Cargo.toml | 32 - crates/simplicityhl-core/README.md | 20 - crates/simplicityhl-core/src/blinder.rs | 17 - crates/simplicityhl-core/src/constants.rs | 142 - crates/simplicityhl-core/src/error.rs | 58 - .../simplicityhl-core/src/fee_rate_fetcher.rs | 126 - crates/simplicityhl-core/src/lib.rs | 336 -- crates/simplicityhl-core/src/runner.rs | 40 - crates/simplicityhl-core/src/scripts.rs | 90 - crates/wallet-abi/Cargo.toml | 31 + crates/wallet-abi/src/encoding.rs | 83 + crates/wallet-abi/src/error.rs | 75 + .../src}/issuance_validation/mod.rs | 29 +- crates/wallet-abi/src/lib.rs | 27 + .../src/runtime/input_resolution.rs | 1294 ++++++ crates/wallet-abi/src/runtime/mod.rs | 624 +++ .../src/runtime/output_resolution.rs | 590 +++ crates/wallet-abi/src/runtime/utils.rs | 10 + crates/wallet-abi/src/schema/mod.rs | 4 + .../wallet-abi/src/schema/runtime_params.rs | 275 ++ crates/wallet-abi/src/schema/tx_create.rs | 121 + crates/wallet-abi/src/schema/types.rs | 9 + crates/wallet-abi/src/schema/values.rs | 176 + crates/wallet-abi/src/scripts.rs | 24 + crates/wallet-abi/src/simplicity/mod.rs | 1 + crates/wallet-abi/src/simplicity/p2pk.rs | 61 + .../src/source_simf/p2pk.simf | 0 crates/wallet-abi/src/taproot_pubkey_gen.rs | 448 ++ .../src/tx_inclusion.rs | 31 +- .../tests/data}/test-tx-incl-block.hex | 0 .../tests/data/tx_with_issuance_token.hex} | 0 116 files changed, 9376 insertions(+), 11466 deletions(-) delete mode 100644 .github/workflows/publish-core.yml delete mode 100644 crates/cli/assets/example-run.md create mode 100644 crates/cli/src/commands/option_offer.rs delete mode 100644 crates/cli/src/commands/options.rs delete mode 100644 crates/cli/src/commands/smt_storage.rs delete mode 100644 crates/cli/src/explorer.rs delete mode 100644 crates/cli/src/modules/keys.rs delete mode 100644 crates/cli/src/modules/settings.rs delete mode 100644 crates/contracts/src/arguments_helpers.rs delete mode 100644 crates/contracts/src/finance/dcd/build_arguments.rs delete mode 100644 crates/contracts/src/finance/dcd/build_witness.rs delete mode 100644 crates/contracts/src/finance/dcd/mod.rs delete mode 100644 crates/contracts/src/finance/dcd/source_simf/dual_currency_deposit.simf delete mode 100644 crates/contracts/src/sdk/basic/issue_asset.rs delete mode 100644 crates/contracts/src/sdk/basic/mod.rs delete mode 100644 crates/contracts/src/sdk/basic/reissue_asset.rs delete mode 100644 crates/contracts/src/sdk/basic/split_native_any.rs delete mode 100644 crates/contracts/src/sdk/basic/transfer_asset.rs delete mode 100644 crates/contracts/src/sdk/basic/transfer_native.rs delete mode 100644 crates/contracts/src/sdk/finance/mod.rs delete mode 100644 crates/contracts/src/sdk/finance/option_offer/deposit.rs delete mode 100644 crates/contracts/src/sdk/finance/option_offer/exercise.rs delete mode 100644 crates/contracts/src/sdk/finance/option_offer/expiry.rs delete mode 100644 crates/contracts/src/sdk/finance/option_offer/mod.rs delete mode 100644 crates/contracts/src/sdk/finance/option_offer/withdraw.rs delete mode 100644 crates/contracts/src/sdk/finance/options/cancellation_option.rs delete mode 100644 crates/contracts/src/sdk/finance/options/creation_option.rs delete mode 100644 crates/contracts/src/sdk/finance/options/exercise_option.rs delete mode 100644 crates/contracts/src/sdk/finance/options/expiry_option.rs delete mode 100644 crates/contracts/src/sdk/finance/options/funding_option.rs delete mode 100644 crates/contracts/src/sdk/finance/options/mod.rs delete mode 100644 crates/contracts/src/sdk/finance/options/settlement_option.rs delete mode 100644 crates/contracts/src/sdk/issuance_validation/test_data/option_arguments.hex delete mode 100644 crates/contracts/src/sdk/issuance_validation/test_data/taproot.hex delete mode 100644 crates/contracts/src/sdk/mod.rs delete mode 100644 crates/contracts/src/sdk/storage/get_storage_address.rs delete mode 100644 crates/contracts/src/sdk/storage/mod.rs delete mode 100644 crates/contracts/src/sdk/storage/transfer_from_storage_address.rs delete mode 100644 crates/contracts/src/sdk/taproot_pubkey_gen.rs delete mode 100644 crates/contracts/src/sdk/validation.rs rename crates/contracts/src/{ => state_management}/array_tr_storage/build_witness.rs (100%) rename crates/contracts/src/{ => state_management}/array_tr_storage/mod.rs (92%) rename crates/contracts/src/{ => state_management}/array_tr_storage/source_simf/array_tr_storage.simf (100%) rename crates/contracts/src/{ => state_management}/bytes32_tr_storage/build_witness.rs (100%) rename crates/contracts/src/{ => state_management}/bytes32_tr_storage/mod.rs (91%) rename crates/contracts/src/{ => state_management}/bytes32_tr_storage/source_simf/bytes32_tr_storage.simf (100%) create mode 100644 crates/contracts/src/state_management/mod.rs rename crates/contracts/src/{ => state_management}/simple_storage/build_arguments.rs (96%) rename crates/contracts/src/{ => state_management}/simple_storage/build_witness.rs (100%) rename crates/contracts/src/{ => state_management}/simple_storage/mod.rs (94%) rename crates/contracts/src/{ => state_management}/simple_storage/source_simf/simple_storage.simf (100%) rename crates/contracts/src/{ => state_management}/smt_storage/build_witness.rs (100%) rename crates/contracts/src/{ => state_management}/smt_storage/mod.rs (95%) rename crates/contracts/src/{ => state_management}/smt_storage/smt.rs (95%) rename crates/contracts/src/{ => state_management}/smt_storage/source_simf/smt_storage.simf (100%) create mode 100644 crates/contracts/src/utils/arguments_helpers.rs create mode 100644 crates/contracts/src/utils/mod.rs create mode 100644 crates/contracts/src/utils/test_setup.rs delete mode 100644 crates/simplicityhl-core/Cargo.toml delete mode 100644 crates/simplicityhl-core/README.md delete mode 100644 crates/simplicityhl-core/src/blinder.rs delete mode 100644 crates/simplicityhl-core/src/constants.rs delete mode 100644 crates/simplicityhl-core/src/error.rs delete mode 100644 crates/simplicityhl-core/src/fee_rate_fetcher.rs delete mode 100644 crates/simplicityhl-core/src/lib.rs delete mode 100644 crates/simplicityhl-core/src/runner.rs delete mode 100644 crates/simplicityhl-core/src/scripts.rs create mode 100644 crates/wallet-abi/Cargo.toml create mode 100644 crates/wallet-abi/src/encoding.rs create mode 100644 crates/wallet-abi/src/error.rs rename crates/{contracts/src/sdk => wallet-abi/src}/issuance_validation/mod.rs (95%) create mode 100644 crates/wallet-abi/src/lib.rs create mode 100644 crates/wallet-abi/src/runtime/input_resolution.rs create mode 100644 crates/wallet-abi/src/runtime/mod.rs create mode 100644 crates/wallet-abi/src/runtime/output_resolution.rs create mode 100644 crates/wallet-abi/src/runtime/utils.rs create mode 100644 crates/wallet-abi/src/schema/mod.rs create mode 100644 crates/wallet-abi/src/schema/runtime_params.rs create mode 100644 crates/wallet-abi/src/schema/tx_create.rs create mode 100644 crates/wallet-abi/src/schema/types.rs create mode 100644 crates/wallet-abi/src/schema/values.rs create mode 100644 crates/wallet-abi/src/scripts.rs create mode 100644 crates/wallet-abi/src/simplicity/mod.rs create mode 100644 crates/wallet-abi/src/simplicity/p2pk.rs rename crates/{simplicityhl-core => wallet-abi}/src/source_simf/p2pk.simf (100%) create mode 100644 crates/wallet-abi/src/taproot_pubkey_gen.rs rename crates/{simplicityhl-core => wallet-abi}/src/tx_inclusion.rs (81%) rename crates/{simplicityhl-core/src/assets => wallet-abi/tests/data}/test-tx-incl-block.hex (100%) rename crates/{contracts/src/sdk/issuance_validation/test_data/transaction.hex => wallet-abi/tests/data/tx_with_issuance_token.hex} (100%) diff --git a/.github/workflows/publish-core.yml b/.github/workflows/publish-core.yml deleted file mode 100644 index e5e955e..0000000 --- a/.github/workflows/publish-core.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Publish simplicityhl-core - -on: - push: - tags: - - 'simplicityhl-core-v*' - workflow_dispatch: {} - release: - types: [published] - -jobs: - lint: - name: Lint (reusable) - uses: ./.github/workflows/lint.yml - - tests: - name: Tests (reusable) - uses: ./.github/workflows/tests.yml - - publish: - name: Publish simplicityhl-core to crates.io - needs: [lint, tests] - runs-on: ubuntu-latest - environment: release - concurrency: - group: publish-core-${{ github.ref }} - cancel-in-progress: false - permissions: - id-token: write - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Rust (stable) - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: stable - - - name: Verify tag matches crate version - shell: bash - run: | - TAG="${GITHUB_REF##*/}" - VERSION_TAG="${TAG#simplicityhl-core-v}" - CRATE_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name=="simplicityhl-core").version') - echo "Tag version: $VERSION_TAG" - echo "Crate version: $CRATE_VERSION" - if [ "$VERSION_TAG" != "$CRATE_VERSION" ]; then - echo "Tag version ($VERSION_TAG) does not match crate version ($CRATE_VERSION)" - exit 1 - fi - - - name: Check package - run: cargo package -p simplicityhl-core - - - name: Authenticate with crates.io - id: auth - uses: rust-lang/crates-io-auth-action@v1 - - - name: Publish - env: - CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} - run: cargo publish -p simplicityhl-core - - diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c135c2e..80e9664 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ permissions: jobs: test: - name: Build and test (matrix) + name: Build and test (matrix, non-regtest) runs-on: ubuntu-latest strategy: fail-fast: false @@ -36,7 +36,57 @@ jobs: - name: Build run: cargo build --workspace --all-features --verbose - - name: Test - run: cargo test --workspace --all-features --no-fail-fast --verbose + - name: Targeted wallet-abi library tests + run: cargo test -p wallet-abi --lib --verbose + - name: Targeted CLI unit tests + run: cargo test -p cli --lib --verbose + - name: Test (excluding regtest-dependent contracts) + run: cargo test --workspace --all-features --exclude contracts --no-fail-fast --verbose + + contracts-regtest: + name: Contracts regtest tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust (stable) + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + + - name: Setup regtest binaries + run: | + set -euo pipefail + BIN_DIR="$RUNNER_TEMP/lwk-bin" + mkdir -p "$BIN_DIR" + cd "$BIN_DIR" + + ELECTRS_FILENAME="electrs_linux_esplora_027e38d3ebc2f85b28ae76f8f3448438ee4fc7b1_liquid.zip" + ELECTRS_SHA256="a63a314c16bc6642fc060bbc19bd1d54ebf86b42188ff2a11c705177c1eb22f7" + wget "https://github.com/RCasatta/electrsd/releases/download/electrs_releases/${ELECTRS_FILENAME}" + echo "${ELECTRS_SHA256} ${ELECTRS_FILENAME}" | sha256sum -c - + unzip -o "${ELECTRS_FILENAME}" + chmod +x "${BIN_DIR}/electrs" + + ELEMENTSD_VERSION="23.3.1" + ELEMENTSD_FILENAME="elements-${ELEMENTSD_VERSION}-x86_64-linux-gnu.tar.gz" + ELEMENTSD_SHA256="864e3a8240137c4e948ecae7c526ccb363771351ea68737a14c682025d5fedaa" + curl -Ls "https://github.com/ElementsProject/elements/releases/download/elements-${ELEMENTSD_VERSION}/${ELEMENTSD_FILENAME}" -o "${ELEMENTSD_FILENAME}" + echo "${ELEMENTSD_SHA256} ${ELEMENTSD_FILENAME}" | sha256sum -c - + tar -xzf "${ELEMENTSD_FILENAME}" + chmod +x "${BIN_DIR}/elements-${ELEMENTSD_VERSION}/bin/elementsd" + + echo "ELECTRS_LIQUID_EXEC=${BIN_DIR}/electrs" >> "$GITHUB_ENV" + echo "ELEMENTSD_EXEC=${BIN_DIR}/elements-${ELEMENTSD_VERSION}/bin/elementsd" >> "$GITHUB_ENV" + + - name: Test contracts (regtest) + run: cargo test -p contracts --all-features --no-fail-fast --verbose -- --test-threads=1 diff --git a/Cargo.lock b/Cargo.lock index 575503b..a8c61f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,85 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + +[[package]] +name = "age" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf640be7658959746f1f0f2faab798f6098a9436a8e18e148d18bc9875e13c4b" +dependencies = [ + "age-core", + "base64 0.21.7", + "bech32 0.9.1", + "chacha20poly1305", + "cookie-factory", + "hmac", + "i18n-embed", + "i18n-embed-fl", + "lazy_static", + "nom", + "pin-project", + "rand 0.8.5", + "rust-embed", + "scrypt", + "sha2", + "subtle", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "age-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99" +dependencies = [ + "base64 0.21.7", + "chacha20poly1305", + "cookie-factory", + "hkdf", + "io_tee", + "nom", + "rand 0.8.5", + "secrecy", + "sha2", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -11,6 +90,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -63,15 +151,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] -name = "arraydeque" -version = "0.5.1" +name = "arc-swap" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] [[package]] name = "arrayvec" @@ -80,15 +171,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "async-trait" -version = "0.1.89" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base58ck" @@ -100,12 +192,39 @@ dependencies = [ "bitcoin_hashes", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bech32" version = "0.11.1" @@ -132,6 +251,19 @@ dependencies = [ "virtue", ] +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "rand_core 0.6.4", + "serde", + "unicode-normalization", +] + [[package]] name = "bitcoin" version = "0.32.8" @@ -139,7 +271,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", - "bech32", + "base64 0.21.7", + "bech32 0.11.1", "bitcoin-internals", "bitcoin-io", "bitcoin-units", @@ -147,6 +280,7 @@ dependencies = [ "hex-conservative", "hex_lit", "secp256k1", + "serde", ] [[package]] @@ -154,6 +288,9 @@ name = "bitcoin-internals" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] [[package]] name = "bitcoin-io" @@ -174,6 +311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" dependencies = [ "bitcoin-internals", + "serde", ] [[package]] @@ -184,6 +322,44 @@ checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", "hex-conservative", + "serde", +] + +[[package]] +name = "bitcoincore-rpc" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedd23ae0fd321affb4bbbc36126c6f49a32818dc6b979395d24da8c9d4e80ee" +dependencies = [ + "bitcoincore-rpc-json", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoincore-rpc-json" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8909583c5fab98508e80ef73e5592a651c954993dc6b7739963257d19f0e71a" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoind" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce6620b7c942dbe28cc49c21d95e792feb9ffd95a093205e7875ccfa69c2925" +dependencies = [ + "anyhow", + "bitcoincore-rpc", + "log", + "tempfile", + "which", ] [[package]] @@ -194,12 +370,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -210,6 +383,23 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bmp-monochrome" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829a082bd3761fde7476dc2ed85ca56c11628948460ece621e4f56fef5046567" + +[[package]] +name = "bollard-stubs" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2f2e73fffe9455141e170fb9c1feb0ac521ec7e7dcd47a7cab72a658490fb8" +dependencies = [ + "chrono", + "serde", + "serde_with", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -222,11 +412,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cc" -version = "1.2.51" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -238,11 +434,66 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" -version = "4.5.54" +version = "4.5.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" dependencies = [ "clap_builder", "clap_derive", @@ -250,33 +501,33 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.5.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", ] [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "cli" @@ -284,16 +535,15 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "config", "contracts", "dotenvy", "hex", - "minreq", + "lwk_common", "simplicityhl", - "simplicityhl-core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "sled", - "thiserror", "tokio", + "tracing-subscriber", + "wallet-abi", ] [[package]] @@ -303,67 +553,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "config" -version = "0.15.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" +name = "contracts" +version = "0.1.0" dependencies = [ - "async-trait", - "convert_case", - "json5", - "pathdiff", - "ron", - "rust-ini", - "serde-untagged", - "serde_core", + "anyhow", + "bincode", + "bitcoincore-rpc", + "hex", + "lwk_common", + "lwk_test_util", + "serde", "serde_json", - "toml", - "winnow", - "yaml-rust2", + "simplicityhl", + "thiserror 2.0.18", + "tokio", + "wallet-abi", ] [[package]] -name = "const-random" -version = "0.1.18" +name = "cookie-factory" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" dependencies = [ - "const-random-macro", + "futures", ] [[package]] -name = "const-random-macro" -version = "0.1.16" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "contracts" -version = "0.1.0" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "anyhow", - "bincode", - "hex", - "ring", - "sha2", - "simplicityhl", - "simplicityhl-core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror", + "core-foundation-sys", + "libc", ] [[package]] -name = "convert_case" -version = "0.6.0" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" @@ -398,12 +628,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-common" version = "0.1.7" @@ -411,1154 +635,3416 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] [[package]] -name = "digest" -version = "0.10.7" +name = "ctr" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "block-buffer", - "crypto-common", + "cipher", ] [[package]] -name = "dlv-list" -version = "0.5.2" +name = "curve25519-dalek" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "const-random", + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", ] [[package]] -name = "dotenvy" -version = "0.15.7" +name = "curve25519-dalek-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] [[package]] -name = "either" -version = "1.15.0" +name = "darling" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] [[package]] -name = "elements" -version = "0.25.2" +name = "darling_core" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b2569d3495bfdfce36c504fd4d78752ff4a7699f8a33e6f3ee523bddf9f6ad" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ - "bech32", - "bitcoin", - "secp256k1-zkp", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", ] [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "darling_macro" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "cfg-if", + "darling_core", + "quote", + "syn 1.0.109", ] [[package]] -name = "erased-serde" -version = "0.4.9" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "serde", - "serde_core", - "typeid", + "block-buffer", + "crypto-common", + "subtle", ] [[package]] -name = "find-msvc-tools" -version = "0.1.6" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] [[package]] -name = "foldhash" -version = "0.1.5" +name = "dotenvy" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] -name = "fs2" -version = "0.4.3" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] -name = "fxhash" -version = "0.2.1" +name = "electrsd" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +checksum = "91435161fb2ad5098e7ac7a4b793bf9c34723b0208a3fcf6f33707489e771396" dependencies = [ - "byteorder", + "bitcoind", + "electrum-client", + "log", + "nix", + "which", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "electrum-client" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "7a0bd443023f9f5c4b7153053721939accc7113cbdf810a024434eed454b3db1" dependencies = [ - "typenum", - "version_check", + "bitcoin", + "byteorder", + "libc", + "log", + "rustls", + "serde", + "serde_json", + "webpki-roots 0.25.4", + "winapi", ] [[package]] -name = "getrandom" -version = "0.2.16" +name = "elements" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "81b2569d3495bfdfce36c504fd4d78752ff4a7699f8a33e6f3ee523bddf9f6ad" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", + "bech32 0.11.1", + "bitcoin", + "secp256k1-zkp", + "serde", + "serde_json", ] [[package]] -name = "ghost-cell" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8449d342b1c67f49169e92e71deb7b9b27f30062301a16dbc27a4cc8d2351b7" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" +name = "elements" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "9d562b364c5d2aced40b01b3f73fc968311787e6813957593d4ffa94cd8733e3" dependencies = [ - "foldhash", + "bech32 0.11.1", + "bitcoin", + "secp256k1-zkp", + "serde_json", ] [[package]] -name = "hashlink" -version = "0.10.0" +name = "elements-miniscript" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "571fa105690f83c7833df2109eb2e14ca0e62d633d2624ffcb166ff18a3da870" dependencies = [ - "hashbrown 0.15.5", + "bitcoin", + "elements 0.25.2", + "miniscript", + "serde", ] [[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hex-conservative" -version = "0.2.2" +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "arrayvec", + "cfg-if", ] [[package]] -name = "hex_lit" -version = "0.1.1" +name = "env_filter" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] [[package]] -name = "instant" -version = "0.1.13" +name = "env_logger" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ - "cfg-if", + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "itertools" -version = "0.13.0" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "either", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "itoa" -version = "1.0.17" +name = "fastrand" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "js-sys" -version = "0.3.83" +name = "fiat-crypto" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" -dependencies = [ - "once_cell", - "wasm-bindgen", -] +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] -name = "json5" -version = "0.4.1" +name = "find-crate" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" dependencies = [ - "pest", - "pest_derive", - "serde", + "toml", ] [[package]] -name = "libc" -version = "0.2.179" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] -name = "lock_api" -version = "0.4.14" +name = "fluent" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" dependencies = [ - "scopeguard", + "fluent-bundle", + "unic-langid", ] [[package]] -name = "log" -version = "0.4.29" +name = "fluent-bundle" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 1.1.0", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] [[package]] -name = "memchr" -version = "2.7.6" +name = "fluent-langneg" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] [[package]] -name = "miniscript" -version = "12.3.5" +name = "fluent-syntax" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" dependencies = [ - "bech32", - "bitcoin", + "thiserror 1.0.69", ] [[package]] -name = "minreq" -version = "2.14.1" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" -dependencies = [ - "rustls", - "rustls-webpki", - "serde", - "serde_json", - "webpki-roots", -] +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "once_cell" -version = "1.21.3" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "form_urlencoded" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] [[package]] -name = "ordered-multimap" -version = "0.7.3" +name = "fs2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" dependencies = [ - "dlv-list", - "hashbrown 0.14.5", + "libc", + "winapi", ] [[package]] -name = "parking_lot" -version = "0.11.2" +name = "futures" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ - "instant", - "lock_api", - "parking_lot_core", + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] -name = "parking_lot_core" -version = "0.8.6" +name = "futures-channel" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", + "futures-core", + "futures-sink", ] [[package]] -name = "pathdiff" -version = "0.2.3" +name = "futures-core" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] -name = "pest" -version = "2.8.5" +name = "futures-executor" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ - "memchr", - "ucd-trie", + "futures-core", + "futures-task", + "futures-util", ] [[package]] -name = "pest_derive" -version = "2.8.5" +name = "futures-io" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" -dependencies = [ - "pest", - "pest_generator", -] +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] -name = "pest_generator" -version = "2.8.5" +name = "futures-macro" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ - "pest", - "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] -name = "pest_meta" -version = "2.8.5" +name = "futures-sink" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" -dependencies = [ - "pest", - "sha2", -] +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "futures-task" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "futures-util" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "zerocopy", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", ] [[package]] -name = "proc-macro2" -version = "1.0.105" +name = "fxhash" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" dependencies = [ - "unicode-ident", + "byteorder", ] [[package]] -name = "quote" -version = "1.0.43" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "proc-macro2", + "typenum", + "version_check", ] [[package]] -name = "rand" -version = "0.8.5" +name = "getopts" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ - "libc", - "rand_chacha", - "rand_core", + "unicode-width", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "ppv-lite86", - "rand_core", + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "getrandom", + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", ] [[package]] -name = "redox_syscall" -version = "0.2.16" +name = "getrandom" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ - "bitflags 1.3.2", + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", ] [[package]] -name = "regex" -version = "1.12.2" +name = "ghost-cell" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] +checksum = "d8449d342b1c67f49169e92e71deb7b9b27f30062301a16dbc27a4cc8d2351b7" [[package]] -name = "regex-automata" +name = "h2" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] -name = "regex-syntax" -version = "0.8.8" +name = "hashbrown" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] [[package]] -name = "ring" -version = "0.17.14" +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "untrusted", - "windows-sys 0.52.0", + "arrayvec", ] [[package]] -name = "ron" -version = "0.12.0" +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "hkdf" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "bitflags 2.10.0", - "once_cell", - "serde", - "serde_derive", - "typeid", - "unicode-ident", + "hmac", ] [[package]] -name = "rust-ini" -version = "0.21.3" +name = "hmac" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "cfg-if", - "ordered-multimap", + "digest", ] [[package]] -name = "rustls" -version = "0.21.12" +name = "home" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "log", - "ring", - "rustls-webpki", - "sct", + "windows-sys 0.61.2", ] [[package]] -name = "rustls-webpki" -version = "0.101.7" +name = "http" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ - "ring", - "untrusted", + "bytes", + "itoa", ] [[package]] -name = "rustversion" -version = "1.0.22" +name = "http-body" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] [[package]] -name = "santiago" -version = "1.3.1" +name = "http-body-util" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de36022292bc2086eb8f55bffa460fef3475e4459b478820711f4c421feb87ec" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "regex", + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", ] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "httparse" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "sct" -version = "0.7.1" +name = "hyper" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ - "ring", - "untrusted", + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", ] [[package]] -name = "secp256k1" -version = "0.29.1" +name = "hyper-rustls" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "bitcoin_hashes", - "rand", - "secp256k1-sys", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.6", ] [[package]] -name = "secp256k1-sys" -version = "0.10.1" +name = "hyper-util" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "cc", + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", ] [[package]] -name = "secp256k1-zkp" -version = "0.11.0" +name = "i18n-config" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a44aed3002b5ae975f8624c5df3a949cfbf00479e18778b6058fcd213b76e3" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" dependencies = [ - "bitcoin-private", - "rand", - "secp256k1", - "secp256k1-zkp-sys", + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", ] [[package]] -name = "secp256k1-zkp-sys" -version = "0.10.1" +name = "i18n-embed" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57f08b2d0b143a22e07f798ae4f0ab20d5590d7c68e0d090f2088a48a21d1654" +checksum = "669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4" dependencies = [ - "cc", - "secp256k1-sys", + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "log", + "parking_lot 0.12.5", + "rust-embed", + "thiserror 1.0.69", + "unic-langid", + "walkdir", ] [[package]] -name = "serde" -version = "1.0.228" +name = "i18n-embed-fl" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2d" dependencies = [ - "serde_core", - "serde_derive", + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "proc-macro-error2", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.116", + "unic-langid", ] [[package]] -name = "serde-untagged" -version = "0.1.9" +name = "i18n-embed-impl" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn 2.0.116", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "iana-time-zone" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ - "serde_derive", + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "iana-time-zone-haiku" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "proc-macro2", - "quote", - "syn", + "cc", ] [[package]] -name = "serde_json" -version = "1.0.149" +name = "icu_collections" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", ] [[package]] -name = "serde_spanned" -version = "1.0.4" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ - "serde_core", + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", ] [[package]] -name = "sha2" -version = "0.10.9" +name = "icu_normalizer" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", ] [[package]] -name = "shlex" -version = "1.3.0" +name = "icu_normalizer_data" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] -name = "simplicity-lang" -version = "0.7.0" +name = "icu_properties" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e57bd4d84853974a212eab24ed89da54f49fbccf5e33e93bcd29f0a6591cd5" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "bitcoin", - "bitcoin_hashes", - "byteorder", - "elements", - "getrandom", - "ghost-cell", - "hex-conservative", - "miniscript", - "santiago", - "simplicity-sys", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", ] [[package]] -name = "simplicity-sys" -version = "0.6.1" -source = "git+https://github.com/BlockstreamResearch/rust-simplicity?tag=simplicity-sys-0.6.1#7f42b532fdcad8b88e65af467a238af28204bc8b" +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ - "bitcoin_hashes", - "cc", + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", ] [[package]] -name = "simplicityhl" -version = "0.4.0" +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79c6da84ac06f1162607b83508e83a58dc19049a2a3e006b44926bee3841bc2" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "base64", - "clap", - "either", - "getrandom", - "itertools", - "miniscript", - "pest", - "pest_derive", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", "serde", - "serde_json", - "simplicity-lang", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", ] [[package]] -name = "simplicityhl-core" -version = "0.4.2" +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ - "bincode", - "hex", - "minreq", - "sha2", - "simplicityhl", - "thiserror", + "cfg-if", ] [[package]] -name = "simplicityhl-core" -version = "0.4.2" +name = "intl-memoizer" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e341dd0a1c4967109d4b71bc6821d45d0c2310ea0b70efeefe154cd1a0f8932" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" dependencies = [ - "bincode", - "hex", - "sha2", - "simplicityhl", - "thiserror", + "type-map", + "unic-langid", ] [[package]] -name = "sled" -version = "0.34.7" +name = "intl_pluralrules" +version = "7.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot", + "unic-langid", +] + +[[package]] +name = "io_tee" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", ] [[package]] -name = "smallvec" -version = "1.15.1" +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf" +dependencies = [ + "base64 0.13.1", + "minreq", + "serde", + "serde_json", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lwk_common" +version = "0.14.0" +source = "git+https://github.com/Blockstream/lwk?rev=4074c527fcc1268eb26953af397eecc6c1444293#4074c527fcc1268eb26953af397eecc6c1444293" +dependencies = [ + "aes-gcm-siv", + "base64 0.21.7", + "elements 0.25.2", + "elements-miniscript", + "getrandom 0.2.17", + "qr_code", + "rand 0.8.5", + "serde", + "serde_json", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "lwk_containers" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb8dd8521dc2613c494b5ad4856d864499b694a83e3bd33c4aec2c30e084462" +dependencies = [ + "elements 0.25.2", + "rand 0.8.5", + "tempfile", + "testcontainers", +] + +[[package]] +name = "lwk_signer" +version = "0.14.0" +source = "git+https://github.com/Blockstream/lwk?rev=4074c527fcc1268eb26953af397eecc6c1444293#4074c527fcc1268eb26953af397eecc6c1444293" +dependencies = [ + "base64 0.21.7", + "bip39", + "elements-miniscript", + "lwk_common", + "thiserror 1.0.69", +] + +[[package]] +name = "lwk_simplicity" +version = "0.1.0" +source = "git+https://github.com/Blockstream/lwk?rev=4074c527fcc1268eb26953af397eecc6c1444293#4074c527fcc1268eb26953af397eecc6c1444293" +dependencies = [ + "lwk_common", + "simplicityhl", + "thiserror 2.0.18", +] + +[[package]] +name = "lwk_test_util" +version = "0.14.0" +source = "git+https://github.com/Blockstream/lwk?rev=4074c527fcc1268eb26953af397eecc6c1444293#4074c527fcc1268eb26953af397eecc6c1444293" +dependencies = [ + "bip39", + "electrsd", + "elements-miniscript", + "env_logger", + "log", + "lwk_common", + "lwk_containers", + "pulldown-cmark", + "rand 0.8.5", + "reqwest", + "serde_json", + "tempfile", +] + +[[package]] +name = "lwk_wollet" +version = "0.14.0" +source = "git+https://github.com/Blockstream/lwk?rev=4074c527fcc1268eb26953af397eecc6c1444293#4074c527fcc1268eb26953af397eecc6c1444293" +dependencies = [ + "aes-gcm-siv", + "age", + "base64 0.21.7", + "bip39", + "electrum-client", + "elements 0.25.2", + "elements 0.26.1", + "elements-miniscript", + "futures", + "fxhash", + "idna", + "js-sys", + "log", + "lwk_common", + "once_cell", + "rand 0.8.5", + "regex-lite", + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "url", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniscript" +version = "12.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" +dependencies = [ + "bech32 0.11.1", + "bitcoin", +] + +[[package]] +name = "minreq" +version = "2.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.12", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.116", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags 2.11.0", + "getopts", + "memchr", + "unicase", +] + +[[package]] +name = "qr_code" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d2564aae5faaf3acb512b35b8bcb9a298d9d8c72d181c598691d800ee78a00" +dependencies = [ + "bmp-monochrome", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[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", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.6", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.116", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "santiago" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de36022292bc2086eb8f55bffa460fef3475e4459b478820711f4c421feb87ec" +dependencies = [ + "regex", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-zkp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a44aed3002b5ae975f8624c5df3a949cfbf00479e18778b6058fcd213b76e3" +dependencies = [ + "bitcoin-private", + "rand 0.8.5", + "secp256k1", + "secp256k1-zkp-sys", + "serde", +] + +[[package]] +name = "secp256k1-zkp-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f08b2d0b143a22e07f798ae4f0ab20d5590d7c68e0d090f2088a48a21d1654" +dependencies = [ + "cc", + "secp256k1-sys", +] + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.2.2", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simplicity-lang" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e57bd4d84853974a212eab24ed89da54f49fbccf5e33e93bcd29f0a6591cd5" +dependencies = [ + "bitcoin", + "bitcoin_hashes", + "byteorder", + "elements 0.25.2", + "getrandom 0.2.17", + "ghost-cell", + "hex-conservative", + "miniscript", + "santiago", + "simplicity-sys", +] + +[[package]] +name = "simplicity-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bcb4e5bfc15080d67e0ce2c17d1c31bfb7521d65c86ea26ed0de72d5119d119" +dependencies = [ + "bitcoin_hashes", + "cc", +] + +[[package]] +name = "simplicityhl" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa7477fc9bfef4cc53ae969db00539f0e67af38156822ac79662513d04f6fee" +dependencies = [ + "base64 0.21.7", + "clap", + "either", + "getrandom 0.2.17", + "itertools", + "miniscript", + "pest", + "pest_derive", + "serde", + "serde_json", + "simplicity-lang", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "testcontainers" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e2b1567ca8a2b819ea7b28c92be35d9f76fb9edb214321dcc86eb96023d1f87" +dependencies = [ + "bollard-stubs", + "futures", + "hex", + "hmac", + "log", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.1", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "serde", + "tinystr", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wallet-abi" +version = "0.1.0" +dependencies = [ + "bincode", + "hex", + "lwk_common", + "lwk_signer", + "lwk_simplicity", + "lwk_wollet", + "ring", + "serde", + "serde_json", + "sha2", + "simplicityhl", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.116", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] [[package]] -name = "strsim" -version = "0.11.1" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] [[package]] -name = "syn" -version = "2.0.114" +name = "windows-interface" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn 2.0.116", ] [[package]] -name = "thiserror" -version = "2.0.17" +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "thiserror-impl", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "thiserror-impl" -version = "2.0.17" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows-link", ] [[package]] -name = "tiny-keccak" -version = "2.0.2" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "crunchy", + "windows-link", ] [[package]] -name = "tokio" -version = "1.49.0" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "pin-project-lite", - "tokio-macros", + "windows-targets 0.52.6", ] [[package]] -name = "tokio-macros" -version = "2.6.0" +name = "windows-sys" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows-targets 0.53.5", ] [[package]] -name = "toml" -version = "0.9.10+spec-1.1.0" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "winnow", + "windows-link", ] [[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "serde_core", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] -name = "toml_parser" -version = "1.0.6+spec-1.1.0" +name = "windows-targets" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "winnow", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] -name = "typeid" -version = "1.0.3" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "typenum" -version = "1.19.0" +name = "windows_aarch64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] -name = "ucd-trie" -version = "0.1.7" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "unicode-ident" -version = "1.0.22" +name = "windows_aarch64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] -name = "unicode-segmentation" -version = "1.12.0" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "untrusted" -version = "0.9.0" +name = "windows_i686_gnu" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] -name = "unty" -version = "0.0.4" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "utf8parse" -version = "0.2.2" +name = "windows_i686_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] -name = "version_check" -version = "0.9.5" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "virtue" -version = "0.0.18" +name = "windows_i686_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "wasm-bindgen" -version = "0.2.106" +name = "windows_x86_64_gnu" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] -name = "wasm-bindgen-macro" -version = "0.2.106" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.106" +name = "windows_x86_64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] -name = "wasm-bindgen-shared" -version = "0.2.106" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" -dependencies = [ - "unicode-ident", -] +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "webpki-roots" -version = "0.25.4" +name = "windows_x86_64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] -name = "winapi" -version = "0.3.9" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "wit-bindgen-rust-macro", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.116", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] [[package]] -name = "windows-link" -version = "0.2.1" +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.116", + "wit-bindgen-core", + "wit-bindgen-rust", +] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "wit-component" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ - "windows-targets", + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", ] [[package]] -name = "windows-sys" -version = "0.61.2" +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ - "windows-link", + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "writeable" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" +name = "x25519-dalek" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" +name = "yoke" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] [[package]] -name = "windows_i686_gnu" -version = "0.52.6" +name = "yoke-derive" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", + "synstructure", +] [[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" +name = "zerocopy" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] [[package]] -name = "windows_i686_msvc" -version = "0.52.6" +name = "zerocopy-derive" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "zerofrom" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "zerofrom-derive" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", + "synstructure", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "zeroize" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] [[package]] -name = "winnow" -version = "0.7.14" +name = "zeroize_derive" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ - "memchr", + "proc-macro2", + "quote", + "syn 2.0.116", ] [[package]] -name = "yaml-rust2" -version = "0.10.4" +name = "zerotrie" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink", + "displaydoc", + "yoke", + "zerofrom", ] [[package]] -name = "zerocopy" -version = "0.8.32" +name = "zerovec" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fabae64378cb18147bb18bca364e63bdbe72a0ffe4adf0addfec8aa166b2c56" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ - "zerocopy-derive", + "serde", + "yoke", + "zerofrom", + "zerovec-derive", ] [[package]] -name = "zerocopy-derive" -version = "0.8.32" +name = "zerovec-derive" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9c2d862265a8bb4471d87e033e730f536e2a285cc7cb05dbce09a2a97075f90" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] name = "zmij" -version = "1.0.12" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 103cb79..d1e115e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,9 @@ [workspace] resolver = "3" members = [ - "crates/*" + "crates/cli", + "crates/contracts", + "crates/wallet-abi", ] [workspace.package] @@ -21,7 +23,9 @@ tracing = { version = "0.1.41" } minreq = { version = "2.14.1", features = ["https", "json-using-serde"]} simplicityhl = { version = "0.4.0" } -simplicityhl-core = { version = "0.4.2", features = ["encoding"] } [patch.crates-io] -simplicity-sys = { git = "https://github.com/BlockstreamResearch/rust-simplicity", tag = "simplicity-sys-0.6.1" } +lwk_common = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_common" } +lwk_signer = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_signer" } +lwk_simplicity = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_simplicity" } +lwk_wollet = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_wollet" } diff --git a/README.md b/README.md index 8fffee9..81f726a 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,30 @@ -# Simplicity Contracts +# Simplicity Contracts Workspace -This repository is a reference for how you can interact and work with [Simplicity HL](https://github.com/BlockstreamResearch/simfony) contracts. +This workspace contains reference implementations and tooling for working with [Simplicity HL](https://github.com/BlockstreamResearch/simfony) contracts on Elements/Liquid. -The only crate published to crates.io is `simplicity-contracts`, so read its [README](crates/contracts/README.md) to understand more about how to use it. +## Workspace Crates -## Repository Structure - -If you are new to contract development with Simplicity HL, below you will find a step-by-step workflow for how a contract can be developed, deployed, and used. - -Everything starts with the actual Simplicity HL code. See the [.simf files](crates/contracts/src), especially the [Options contract](crates/contracts/src/options/source_simf/options.simf) as the most structured example. Check the issues referenced in the contract for better understanding of development implications. - -You can use tools like [`hal-simplicity`](https://github.com/BlockstreamResearch/hal-simplicity) for ad-hoc interaction and manual testing. +- [`contracts`](crates/contracts): Contract templates and helpers (finance + state management modules). +- [`wallet-abi`](crates/wallet-abi): Schema-first wallet runtime and ABI layer used by contract flows. +- [`cli`](crates/cli): `simplicity-cli` binary crate for wallet and option-offer command flows. -If you want to build an application or service around your new contract, take a look at the relevant builders of arguments and witness. See the [Options contract](crates/contracts/src/options/mod.rs) for how to write Rust functions to build those — it is well tested. - -The next step after writing helper functions to build args and witness is to do actual testing. Again, see [mod.rs](crates/contracts/src/options/mod.rs) as the best example. - -Here you could also take a look at how relevant SDK functions in [options](crates/contracts/src/sdk/options) are structured, and what their responsibilities are. +## Repository Structure -The last step is actual transaction publishing on the chain. This part can be found in the [CLI README](crates/cli/README.md). +- Simplicity sources: [`crates/contracts/src/**/source_simf/*.simf`](crates/contracts/src) +- Contract-side Rust helpers: [`crates/contracts/src`](crates/contracts/src) +- Wallet runtime and schemas: [`crates/wallet-abi/src/runtime`](crates/wallet-abi/src/runtime), [`crates/wallet-abi/src/schema`](crates/wallet-abi/src/schema) +- CLI entrypoint and commands: [`crates/cli/src/main.rs`](crates/cli/src/main.rs), [`crates/cli/src/commands`](crates/cli/src/commands) -## How to Use +## Getting Started -You can install the [contracts crate](crates/contracts/Cargo.toml) to build services/SDK around available contracts. +- Contract crate usage and module overview: [contracts README](crates/contracts/README.md) +- CLI usage and command examples: [CLI README](crates/cli/README.md) -Though this repository provides only examples, and because the basic Simplicity compiler does not support imports, you can just copy paste the code into your project. +## Notes -## Important Concepts +This repository is reference-oriented. Copying and adapting modules into your own project is expected while Simplicity tooling/import ergonomics are still evolving. -It is recommended to study the [options.simf](crates/contracts/src/options/source_simf/options.simf) and related issues on GitHub to understand better how to write such contracts securely. +## Old structure -You can see how to use the storage for 32 bytes in the [bytes32_tr_storage.simf](crates/contracts/src/bytes32_tr_storage/source_simf/bytes32_tr_storage.simf) file. -For a more general case, see the [array_tr_storage.simf](crates/contracts/src/array_tr_storage/source_simf/array_tr_storage.simf) file. -You can find a more detailed explanation of how it works in the first file. \ No newline at end of file +Use [this version (116b0eb)](https://github.com/BlockstreamResearch/simplicity-contracts/commit/116b0eb17c3fd84e302d5288ea884c46ba702b77) +of the repo to interact with the pre-wallet-abi introduction (the last version of simplicityhl-core is here) \ No newline at end of file diff --git a/crates/cli/.env.example b/crates/cli/.env.example index b136e28..9333b00 100644 --- a/crates/cli/.env.example +++ b/crates/cli/.env.example @@ -1,2 +1,5 @@ -SEED_HEX=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef -DEX_LOG=debug +# Optional mnemonic consumed by `--mnemonic` / `MNEMONIC`. +MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + +# Optional wallet data root override. +# SIMPLICITY_CLI_WALLET_DATA_DIR=".cache/wallet" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index dbd5018..ebabc76 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -16,21 +16,20 @@ workspace = true [dependencies] anyhow = "1" -thiserror = "2" +dotenvy = "0.15.7" sled = "0.34.7" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } -dotenvy = "0.15" -clap = { version = "4.5", features = ["derive"] } -config = { version = "0.15.16", default-features = true } +clap = { version = "4", features = ["derive", "env"] } -minreq = { version = "2.14", features = ["https"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "sync"] } hex = { workspace = true } +wallet-abi = { path = "../wallet-abi" } contracts = { path = "../contracts" } simplicityhl = { workspace = true } -simplicityhl-core = { workspace = true } +lwk_common = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_common", default-features = false } diff --git a/crates/cli/README.md b/crates/cli/README.md index 16cac92..3d1f576 100644 --- a/crates/cli/README.md +++ b/crates/cli/README.md @@ -1,27 +1,71 @@ -# Simplicity HL Core -- CLI +# Simplicity Contracts CLI -This CLI showcases how to deploy and interact with Simplicity HL contracts. +This crate provides `simplicity-cli`, a helper CLI for basic wallet flows and option-offer flows. -It provides [basic](src/commands/basic.rs) and [options](src/commands/options.rs) CLIs. +## Command Topology -Use `--help` to understand what each command can do. +- `basic ...` +- `option-offer ...` -The basic commands are tools for playing around with the options contract. +`--network` is required for every command invocation. -Refer to the [contracts README](../contracts/README.md) for more information about the contracts. +Use `--help` at each level: -## How to Use +```bash +cargo run -p cli -- --help +cargo run -p cli -- --network testnet-liquid basic --help +cargo run -p cli -- --network testnet-liquid option-offer --help +``` + +Top-level command groups currently exposed: + +- `basic`: address/balance/transfer/split/issue/reissue flows +- `option-offer`: create/import/export/exercise/withdraw/expiry flows + +## Runtime Configuration + +Current behavior: + +- `--network` is required on every invocation. + - supported: `liquid`, `testnet-liquid`, `localtest-liquid` +- `--mnemonic` is optional and also supports env var `MNEMONIC`. +- if mnemonic is not provided, a built-in test mnemonic is used. +- Esplora URL is derived from network in code (no env override in current build): + - `liquid` -> `https://blockstream.info/liquid/api` + - `testnet-liquid` -> `https://blockstream.info/liquidtestnet/api` + - `localtest-liquid` -> `http://127.0.0.1:3001` +- wallet data dir defaults to `.cache/wallet` and can be overridden with `SIMPLICITY_CLI_WALLET_DATA_DIR`. + +Example: ```bash -cargo run -p cli -- basic --help -cargo run -p cli -- options --help +export MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" +cargo run -p cli -- --network localtest-liquid basic address ``` -See [example-run.md](assets/example-run.md) for actual usage of this CLI. +## Local Store Behavior + +Option-offer arguments are stored in `.cache/store`. + +- `option-offer create` rejects duplicate keys. +- `option-offer import` overwrites existing keys. +- `option-offer export` reads by key. + +Import/export examples: + +```bash +cargo run -p cli -- --network testnet-liquid option-offer import \ + --option-offer-taproot-pubkey-gen \ + --encoded-option-offer-arguments + +cargo run -p cli -- --network testnet-liquid option-offer export \ + --option-offer-taproot-pubkey-gen +``` ## License Dual-licensed under either of: + - Apache License, Version 2.0 (Apache-2.0) - MIT license (MIT) diff --git a/crates/cli/assets/example-run.md b/crates/cli/assets/example-run.md deleted file mode 100644 index ed45803..0000000 --- a/crates/cli/assets/example-run.md +++ /dev/null @@ -1,155 +0,0 @@ -## Options and basic commands Testing Status - ---- Main key - -X Only Public Key: ff81247f6aaac08a0edcd422c89ae4726e1e69fe9f059f3845e017c77999765d -P2PK Address: tex1p9sv7g8tyljjymz4t6zyjpvepw44f9wfjxskvyjlczqjdw7rrykqsl9hwe7 -Script hash: c50c741beb17a0decf6feafafc242fe77f3c4c6749e99bab09a5c8b6e2bda6eb - -```bash -cargo run -p cli -- basic issue-asset \ - --broadcast \ - --asset-name "test8" \ - --fee-utxo 11c3739232369d16b75042f2be3f471e6a6aeca407ad618b1d46e522ab2e4950:0 \ - --issue-sats 1000000000000000 \ - --fee-sats 50 \ - --account-index 0 -``` - -Asset id: 9ec1fbdafa60888ae667cc7387dcc39337be14cc86a0263b5d4df5b388a748a6, -Reissuance asset: fea85ec0ec4e69193394d70068a86ee170138c008f0c15639a980d7293d75a2f, -Asset entropy: 2f9a1be5962a775e5efd86f62d3bc001468537291a4952dab7d0dc65698108d7 -Broadcasted txid: fd7a0240fa56668f644610b6ef8b5b49551d00ec03f86bd1fdcac8af87b86e04 - - -```bash -cargo run -p cli -- basic reissue-asset \ - --broadcast \ - --reissue-asset-utxo fd7a0240fa56668f644610b6ef8b5b49551d00ec03f86bd1fdcac8af87b86e04:0 \ - --fee-utxo c2f0cd45deacea442330fea2f5c6e41725e0de66a233b65bcd576e63ab5bda65:3 \ - --asset-name "test8" \ - --reissue-sats 54321 \ - --fee-sats 100 -``` - -```bash -cargo run -p cli -- basic transfer-native \ - --broadcast \ - --utxo fbf2c06ca35ac6ae48b964c99e93a447a7c432017eee76dc77f2d802f6d30704:2 \ - --to-address tex1p9sv7g8tyljjymz4t6zyjpvepw44f9wfjxskvyjlczqjdw7rrykqsl9hwe7 \ - --send-sats 1234 \ - --fee-sats 100 -``` - -```bash - cargo run -p cli -- basic transfer-asset \ - --broadcast \ - --asset-utxo fbf2c06ca35ac6ae48b964c99e93a447a7c432017eee76dc77f2d802f6d30704:1 \ - --fee-utxo 92224e5fc99fd7ef525f23e3ca29057f0f5192e4391fd7410789da77af854c58:0 \ - --to-address tex1p9sv7g8tyljjymz4t6zyjpvepw44f9wfjxskvyjlczqjdw7rrykqsl9hwe7 \ - --send-sats 123 \ - --fee-sats 100 -``` - -```bash -cargo run -p cli -- basic split-native \ - --broadcast \ - --fee-utxo a35f7fc317cf8cf2eec88b1046de046e8c7e186259bf54c01eb0ead391f1505b:4 \ - --split-parts 2 \ - --fee-amount 100 -``` - -Broadcasted txid: 866e9012120260bb2aab86e5585191b2c75b8e4009f65074e92c1c0f63c07452 - -```bash -cargo run -p cli -- options create \ - --broadcast \ - --first-fee-utxo 866e9012120260bb2aab86e5585191b2c75b8e4009f65074e92c1c0f63c07452:0 \ - --second-fee-utxo 866e9012120260bb2aab86e5585191b2c75b8e4009f65074e92c1c0f63c07452:1 \ - --start-time 1765722750 \ - --expiry-time 1765722750 \ - --collateral-per-contract 10 \ - --settlement-per-contract 25 \ - --settlement-asset-id-hex-be 9ec1fbdafa60888ae667cc7387dcc39337be14cc86a0263b5d4df5b388a748a6 \ - --account-index 0 \ - --fee-amount 100 -``` - -options_taproot_pubkey_gen: f6454c9c47b6e2d3ba8e323e2006e35cef3a435e62d51b765edaa861dc119262:02ba1b8af1e19c95637243a16162a0d8b4b6c72326d40da348841fe12d0eaed6b0:tex1plf03p9jjduz3aumdqalmj0euwuje0luk76592xqj9ruvffaj6rfq2t8amc -Broadcasted txid: aa415c2558f2a67d5f63388d5fb9207d6507c172dccea044902dbc3a94a2ca17 - -```bash -cargo run -p cli -- options fund \ - --broadcast \ - --option-asset-utxo aa415c2558f2a67d5f63388d5fb9207d6507c172dccea044902dbc3a94a2ca17:0 \ - --grantor-asset-utxo aa415c2558f2a67d5f63388d5fb9207d6507c172dccea044902dbc3a94a2ca17:1 \ - --collateral-and-fee-utxo aa415c2558f2a67d5f63388d5fb9207d6507c172dccea044902dbc3a94a2ca17:2 \ - --option-taproot-pubkey-gen f6454c9c47b6e2d3ba8e323e2006e35cef3a435e62d51b765edaa861dc119262:02ba1b8af1e19c95637243a16162a0d8b4b6c72326d40da348841fe12d0eaed6b0:tex1plf03p9jjduz3aumdqalmj0euwuje0luk76592xqj9ruvffaj6rfq2t8amc \ - --collateral-amount 1000 \ - --account-index 0 \ - --fee-amount 250 -``` - -Broadcasted txid: 873a10b32cf7b7f3d5c48c35907c052e6ed6d0289378f407bf3a8a4a0ccbe528 - -```bash -cargo run -p cli -- options exercise \ - --broadcast \ - --collateral-utxo 873a10b32cf7b7f3d5c48c35907c052e6ed6d0289378f407bf3a8a4a0ccbe528:2 \ - --option-asset-utxo 873a10b32cf7b7f3d5c48c35907c052e6ed6d0289378f407bf3a8a4a0ccbe528:3 \ - --asset-utxo fd7a0240fa56668f644610b6ef8b5b49551d00ec03f86bd1fdcac8af87b86e04:1 \ - --fee-utxo 873a10b32cf7b7f3d5c48c35907c052e6ed6d0289378f407bf3a8a4a0ccbe528:5 \ - --option-taproot-pubkey-gen f6454c9c47b6e2d3ba8e323e2006e35cef3a435e62d51b765edaa861dc119262:02ba1b8af1e19c95637243a16162a0d8b4b6c72326d40da348841fe12d0eaed6b0:tex1plf03p9jjduz3aumdqalmj0euwuje0luk76592xqj9ruvffaj6rfq2t8amc \ - --amount-to-burn 15 \ - --fee-amount 200 \ - --account-index 0 -``` - -Broadcasted txid: b15803f5d4e0bb228b19d1f856ed0fa8d7eddb2e2da71351d06374b2296867af - -```bash -cargo run -p cli -- options settle \ - --broadcast \ - --settlement-asset-utxo b15803f5d4e0bb228b19d1f856ed0fa8d7eddb2e2da71351d06374b2296867af:2 \ - --grantor-asset-utxo 873a10b32cf7b7f3d5c48c35907c052e6ed6d0289378f407bf3a8a4a0ccbe528:4 \ - --fee-utxo b15803f5d4e0bb228b19d1f856ed0fa8d7eddb2e2da71351d06374b2296867af:5 \ - --option-taproot-pubkey-gen f6454c9c47b6e2d3ba8e323e2006e35cef3a435e62d51b765edaa861dc119262:02ba1b8af1e19c95637243a16162a0d8b4b6c72326d40da348841fe12d0eaed6b0:tex1plf03p9jjduz3aumdqalmj0euwuje0luk76592xqj9ruvffaj6rfq2t8amc \ - --grantor-token-amount-to-burn 15 \ - --fee-amount 200 \ - --account-index 0 - -``` - -Broadcasted txid: c2f0cd45deacea442330fea2f5c6e41725e0de66a233b65bcd576e63ab5bda65 - - -```bash -cargo run -p cli -- options expire \ - --broadcast \ - --collateral-utxo 39f39306c2f84b6b60b6036de24a421e9cac9d1a533b8040770ddfef2f253108:0 \ - --grantor-asset-utxo 39f39306c2f84b6b60b6036de24a421e9cac9d1a533b8040770ddfef2f253108:5 \ - --fee-utxo 39f39306c2f84b6b60b6036de24a421e9cac9d1a533b8040770ddfef2f253108:6 \ - --option-taproot-pubkey-gen f96a6282708cbf9b4f4db34c82236b29081bd17c01c760a3e510aabfba281104:02024607d3db437ed882b2dfc0c1fe7068bed34e71e3ea0cb5c7c562581b588df3:tex1pws8eu77t4jytanwfwh79s9zqcygn0tamt839mumz3440hy95462sw4j969 \ - --grantor-token-amount-to-burn 10 \ - --fee-amount 150 \ - --account-index 0 -``` - -Broadcasted txid: a35f7fc317cf8cf2eec88b1046de046e8c7e186259bf54c01eb0ead391f1505b - -```bash -cargo run -p cli -- options cancel \ - --broadcast \ - --collateral-utxo bd8109755df1708d8cf9a4903acefcee2ebe9c3653909752b389d5ecffdb3b62:2 \ - --option-asset-utxo bd8109755df1708d8cf9a4903acefcee2ebe9c3653909752b389d5ecffdb3b62:3 \ - --grantor-asset-utxo bd8109755df1708d8cf9a4903acefcee2ebe9c3653909752b389d5ecffdb3b62:4 \ - --fee-utxo bd8109755df1708d8cf9a4903acefcee2ebe9c3653909752b389d5ecffdb3b62:5 \ - --option-taproot-pubkey-gen f96a6282708cbf9b4f4db34c82236b29081bd17c01c760a3e510aabfba281104:02024607d3db437ed882b2dfc0c1fe7068bed34e71e3ea0cb5c7c562581b588df3:tex1pws8eu77t4jytanwfwh79s9zqcygn0tamt839mumz3440hy95462sw4j969 \ - --amount-to-burn 10 \ - --fee-amount 150 \ - --account-index 0 -``` - -Broadcasted txid: 39f39306c2f84b6b60b6036de24a421e9cac9d1a533b8040770ddfef2f253108 - - diff --git a/crates/cli/src/commands/basic.rs b/crates/cli/src/commands/basic.rs index d833af4..03cfd77 100644 --- a/crates/cli/src/commands/basic.rs +++ b/crates/cli/src/commands/basic.rs @@ -1,136 +1,87 @@ use crate::modules::store::Store; -use crate::modules::utils::derive_keypair; -use anyhow::anyhow; +use crate::modules::utils::execute_request; -use crate::commands::NETWORK; -use crate::explorer::{broadcast_tx, fetch_utxo}; +use anyhow::{Context, anyhow}; use clap::Subcommand; -use simplicityhl::elements::hashes::{Hash, sha256}; + +use simplicityhl::elements::encode; +use simplicityhl::elements::hashes::sha256::Midstate; +use simplicityhl::elements::hex::ToHex; use simplicityhl::elements::pset::serialize::Serialize; -use simplicityhl::elements::secp256k1_zkp::SECP256K1; -use simplicityhl::elements::{AssetId, ContractHash}; -use simplicityhl::simplicity::elements::{Address, OutPoint}; -use simplicityhl::simplicity::hex::DisplayHex; - -use simplicityhl::tracker::TrackerLogLevel; -use simplicityhl_core::{ - create_p2pk_signature, derive_public_blinder_key, finalize_p2pk_transaction, get_p2pk_address, - hash_script, +use simplicityhl::elements::{Address, AssetId, Sequence, Transaction}; + +use wallet_abi::runtime::WalletRuntimeConfig; +use wallet_abi::schema::tx_create::{TX_CREATE_ABI_VERSION, TxCreateRequest}; +use wallet_abi::taproot_pubkey_gen::get_random_seed; +use wallet_abi::{ + AmountFilter, AssetFilter, AssetVariant, BlinderVariant, FinalizerSpec, InputBlinder, + InputIssuance, InputIssuanceKind, InputSchema, LockFilter, LockVariant, OutputSchema, + RuntimeParams, UTXOSource, WalletSourceFilter, get_new_asset_entropy, }; +fn decode_transaction(tx_hex: &str) -> anyhow::Result { + let tx_bytes = hex::decode(tx_hex).context("failed to decode transaction hex")?; + encode::deserialize(&tx_bytes).context("failed to decode transaction bytes") +} + #[derive(Subcommand, Debug)] pub enum Basic { - /// Print a deterministic Liquid testnet address derived from index - Address { - /// Address index (0-based) - index: u32, - }, - /// Build tx transferring LBTC (explicit) to recipient - TransferNative { - /// Transaction id (hex) and output index (vout) of the UTXO you will spend - #[arg(long = "utxo")] - utxo_outpoint: OutPoint, - /// Recipient address (Liquid testnet bech32m) + /// Print a deterministic address + Address, + /// Print wallet balances grouped by asset id + Balance, + /// Build tx transferring an asset to recipient + Transfer { + /// Recipient Liquid address #[arg(long = "to-address")] to_address: Address, - /// Amount to send to the recipient in satoshis (LBTC) + /// Asset to send, LBTC by default + #[arg(long = "asset")] + asset: Option, + /// Amount to send to the recipient in satoshis #[arg(long = "send-sats")] amount_to_send: u64, - /// Miner fee in satoshis (LBTC) - #[arg(long = "fee-sats")] - fee_amount: u64, - /// Account that will pay for transaction fees and that owns a tokens to send - #[arg(long = "account-index", default_value_t = 0)] - account_index: u32, /// When set, broadcast the built transaction via Esplora and print txid #[arg(long = "broadcast")] broadcast: bool, }, - /// Build tx splitting one LBTC UTXO into any number of UTXOs. - SplitNative { - /// Transaction id (hex) and output index (vout) of the UTXO you will split - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Number of UTXOs to split the LBTC UTXO into + /// Build tx splitting funds into multiple outputs. + Split { + /// Asset to split, LBTC by default + #[arg(long = "asset")] + asset: Option, + /// Number of UTXOs to split the UTXO into #[arg(long = "split-parts")] split_parts: u64, - /// Miner fee in satoshis (LBTC) - #[arg(long = "fee-amount")] - fee_amount: u64, - /// Account that will pay for transaction fees and that owns a tokens to split - #[arg(long = "account-index", default_value_t = 0)] - account_index: u32, - /// When set, broadcast the built transaction via Esplora and print txid - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// Build tx transferring an asset UTXO to recipient (LBTC UTXO pays fees) - TransferAsset { - /// Transaction id (hex) and output index (vout) of the ASSET UTXO you will spend - #[arg(long = "asset-utxo")] - asset_utxo: OutPoint, - /// Transaction id (hex) and output index (vout) of the LBTC UTXO used to pay fees - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Recipient address (Liquid testnet bech32m) - #[arg(long = "to-address")] - to_address: Address, - /// Amount to send of the asset in its smallest units (it does not account for decimals) - #[arg(long = "send-sats")] - send_amount: u64, - /// Miner fee in satoshis (LBTC) - #[arg(long = "fee-sats")] - fee_amount: u64, - /// Account that will pay for transaction fees and that owns a tokens to send - #[arg(long = "account-index", default_value_t = 0)] - account_index: u32, + /// Value of single split + #[arg(long = "part-amount")] + part_amount: u64, /// When set, broadcast the built transaction via Esplora and print txid #[arg(long = "broadcast")] broadcast: bool, }, /// Build tx issuing an asset IssueAsset { - /// Transaction id (hex) and output index (vout) of the LBTC UTXO used to pay fees and issue the asset - #[arg(long = "fee-utxo")] - fee_utxo_outpoint: OutPoint, /// Asset name (this will be stored in the CLI's database only, so it will be not shown on the Esplora UI) #[arg(long = "asset-name")] asset_name: String, /// Amount to issue of the asset in its satoshi units #[arg(long = "issue-sats")] issue_amount: u64, - /// Miner fee in satoshis (LBTC) - #[arg(long = "fee-sats")] - fee_amount: u64, - /// Account that will pay for transaction fees and that owns a tokens to send - #[arg(long = "account-index", default_value_t = 0)] - account_index: u32, /// When set, broadcast the built transaction via Esplora and print txid #[arg(long = "broadcast")] broadcast: bool, }, /// Reissue an asset ReissueAsset { - /// Transaction id (hex) and output index (vout) of the REISSUANCE ASSET UTXO you will spend - #[arg(long = "reissue-asset-utxo")] - reissue_asset_outpoint: OutPoint, - /// Transaction id (hex) and output index (vout) of the LBTC UTXO used to pay fees and reissue the asset - #[arg(long = "fee-utxo")] - fee_utxo_outpoint: OutPoint, /// Asset name (this will be stored in the CLI's database only, so it will be not shown on the Esplora UI) #[arg(long = "asset-name")] asset_name: String, /// Amount to reissue of the asset in its satoshi units #[arg(long = "reissue-sats")] reissue_amount: u64, - /// Miner fee in satoshis (LBTC) - #[arg(long = "fee-sats")] - fee_amount: u64, - /// Account that will pay for transaction fees and that owns a tokens to send - #[arg(long = "account-index", default_value_t = 0)] - account_index: u32, /// When set, broadcast the built transaction via Esplora and print txid #[arg(long = "broadcast")] broadcast: bool, @@ -146,169 +97,163 @@ impl Basic { /// # Panics /// Panics if asset entropy conversion fails. #[expect(clippy::too_many_lines)] - pub async fn handle(&self) -> anyhow::Result<()> { + pub async fn handle(&self, runtime: WalletRuntimeConfig) -> anyhow::Result<()> { + let mut runtime = runtime; + match self { - Self::Address { index } => { - let keypair = derive_keypair(*index); + Self::Address => { + let receiver_address = runtime.signer_receive_address()?; + + let signer_address = runtime.signer_x_only_public_key()?; + + println!("Receiver Address: {receiver_address}"); + println!("Signer X Only Public Key: {signer_address}"); - let public_key = keypair.x_only_public_key().0; - let address = get_p2pk_address(&public_key, NETWORK)?; + Ok(()) + } + Self::Balance => { + runtime.sync_wallet().await?; + + let mut balances: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + for utxo in runtime.wollet.utxos()? { + let entry = balances.entry(utxo.unblinded.asset).or_insert(0); + *entry = entry + .checked_add(utxo.unblinded.value) + .ok_or_else(|| anyhow!("balance overflow while summing wallet UTXOs"))?; + } - let mut script_hash: [u8; 32] = hash_script(&address.script_pubkey()); - script_hash.reverse(); + if balances.is_empty() { + println!("No available assets"); + return Ok(()); + } - println!("X Only Public Key: {public_key}"); - println!("P2PK Address: {address}"); - println!("Script hash: {}", hex::encode(script_hash)); + for (asset_id, amount_sat) in balances { + println!("{asset_id}: {amount_sat}"); + } Ok(()) } - Self::TransferNative { - utxo_outpoint, + Self::Transfer { + asset, to_address, amount_to_send, - fee_amount, - account_index, broadcast, } => { - let keypair = derive_keypair(*account_index); - - let tx_out = fetch_utxo(*utxo_outpoint).await?; - - let pst = contracts::sdk::transfer_native( - (*utxo_outpoint, tx_out.clone()), - to_address, - *amount_to_send, - *fee_amount, - )?; - - let tx = pst.extract_tx()?; - let utxos = &[tx_out]; - - let x_only_public_key = keypair.x_only_public_key().0; - let signature = create_p2pk_signature(&tx, utxos, &keypair, 0, NETWORK)?; - - let tx = finalize_p2pk_transaction( - tx, - utxos, - &x_only_public_key, - &signature, - 0, - NETWORK, - TrackerLogLevel::None, - )?; - - if *broadcast { - println!("Broadcasted txid: {}", broadcast_tx(&tx).await?); - } else { - println!("{}", tx.serialize().to_lower_hex_string()); - } + let asset_to_send = asset.unwrap_or(*runtime.network.policy_asset()); + + let blinder = to_address + .blinding_pubkey + .map_or(BlinderVariant::Explicit, |blinder| { + BlinderVariant::Provided { pubkey: blinder } + }); + + let request = TxCreateRequest { + abi_version: TX_CREATE_ABI_VERSION.to_string(), + request_id: "request-basic.transfer".to_string(), + network: runtime.network, + params: RuntimeParams { + inputs: vec![InputSchema { + id: "input0".to_string(), + utxo_source: UTXOSource::Wallet { + filter: WalletSourceFilter { + asset: AssetFilter::Exact { + asset_id: asset_to_send, + }, + amount: AmountFilter::default(), + lock: LockFilter::default(), + }, + }, + blinder: InputBlinder::default(), + sequence: Sequence::default(), + issuance: None, + finalizer: FinalizerSpec::default(), + }], + outputs: vec![OutputSchema { + id: "to-recipient".to_string(), + amount_sat: *amount_to_send, + lock: LockVariant::Script { + script: to_address.script_pubkey(), + }, + asset: AssetVariant::AssetId { + asset_id: asset_to_send, + }, + blinder, + }], + fee_rate_sat_vb: Some(0.1), + locktime: None, + }, + broadcast: *broadcast, + }; + + let _ = execute_request(&mut runtime, request).await?; Ok(()) } - Self::SplitNative { + Self::Split { + asset, split_parts, - fee_utxo, - fee_amount, - account_index, + part_amount, broadcast, } => { - let keypair = derive_keypair(*account_index); - - let tx_out = fetch_utxo(*fee_utxo).await?; - - let pst = contracts::sdk::split_native_any( - (*fee_utxo, tx_out.clone()), - *split_parts, - *fee_amount, - )?; - - let tx = pst.extract_tx()?; - let utxos = &[tx_out]; - - let x_only_public_key = keypair.x_only_public_key().0; - let signature = create_p2pk_signature(&tx, utxos, &keypair, 0, NETWORK)?; - - let tx = finalize_p2pk_transaction( - tx, - utxos, - &x_only_public_key, - &signature, - 0, - NETWORK, - TrackerLogLevel::None, - )?; - - if *broadcast { - println!("Broadcasted txid: {}", broadcast_tx(&tx).await?); - } else { - println!("{}", tx.serialize().to_lower_hex_string()); + let asset_to_split = asset.unwrap_or(*runtime.network.policy_asset()); + + if *split_parts == 0 { + return Err(anyhow!("split-parts must be > 0")); } - Ok(()) - } - Self::TransferAsset { - asset_utxo: asset_utxo_outpoint, - fee_utxo: fee_utxo_outpoint, - to_address, - send_amount, - fee_amount, - account_index, - broadcast, - } => { - let keypair = derive_keypair(*account_index); - - let asset_tx_out = fetch_utxo(*asset_utxo_outpoint).await?; - let fee_tx_out = fetch_utxo(*fee_utxo_outpoint).await?; - - let pst = contracts::sdk::transfer_asset( - (*asset_utxo_outpoint, asset_tx_out.clone()), - (*fee_utxo_outpoint, fee_tx_out.clone()), - to_address, - *send_amount, - *fee_amount, - )?; - - let tx = pst.extract_tx()?; - let utxos = vec![asset_tx_out, fee_tx_out]; - let x_only_public_key = keypair.x_only_public_key().0; - - let signature_0 = create_p2pk_signature(&tx, &utxos, &keypair, 0, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_0, - 0, - NETWORK, - TrackerLogLevel::None, - )?; - - let signature_1 = create_p2pk_signature(&tx, &utxos, &keypair, 1, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_1, - 1, - NETWORK, - TrackerLogLevel::None, - )?; - - if *broadcast { - println!("Broadcasted txid: {}", broadcast_tx(&tx).await?); - } else { - println!("{}", tx.serialize().to_lower_hex_string()); + let signer_address = runtime.signer_receive_address()?; + + let mut outputs = Vec::new(); + for output_index in 0..*split_parts { + outputs.push(OutputSchema { + id: format!("out{output_index}"), + amount_sat: *part_amount, + lock: LockVariant::Script { + script: signer_address.script_pubkey(), + }, + asset: AssetVariant::AssetId { + asset_id: asset_to_split, + }, + blinder: BlinderVariant::Wallet, + }); } + let request = TxCreateRequest { + abi_version: TX_CREATE_ABI_VERSION.to_string(), + request_id: "request-basic.split_native".to_string(), + network: runtime.network, + params: RuntimeParams { + inputs: vec![InputSchema { + id: "input0".to_string(), + utxo_source: UTXOSource::Wallet { + filter: WalletSourceFilter { + asset: AssetFilter::Exact { + asset_id: asset_to_split, + }, + amount: AmountFilter::default(), + lock: LockFilter::default(), + }, + }, + blinder: InputBlinder::default(), + sequence: Sequence::default(), + issuance: None, + finalizer: FinalizerSpec::default(), + }], + outputs, + fee_rate_sat_vb: Some(0.1), + locktime: None, + }, + broadcast: *broadcast, + }; + + let _ = execute_request(&mut runtime, request).await?; + Ok(()) } Self::IssueAsset { - fee_utxo_outpoint, asset_name, issue_amount, - fee_amount, - account_index, broadcast, } => { let store = Store::load()?; @@ -316,70 +261,93 @@ impl Basic { if store.store.get(asset_name)?.is_some() { return Err(anyhow!("Asset name already exists")); } + let issuance_entropy = get_random_seed(); + + let policy_asset = *runtime.network.policy_asset(); + let signer_script = runtime.signer_receive_address()?.script_pubkey(); + + let request = TxCreateRequest { + abi_version: TX_CREATE_ABI_VERSION.to_string(), + request_id: "request-basic.issue_asset".to_string(), + network: runtime.network, + params: RuntimeParams { + inputs: vec![InputSchema { + id: "in0".to_string(), + utxo_source: UTXOSource::Wallet { + filter: WalletSourceFilter { + // TODO: really? + asset: AssetFilter::Exact { + asset_id: policy_asset, + }, + amount: AmountFilter::default(), + lock: LockFilter::default(), + }, + }, + blinder: InputBlinder::default(), + sequence: Sequence::default(), + issuance: Some(InputIssuance { + kind: InputIssuanceKind::New, + asset_amount_sat: *issue_amount, + token_amount_sat: 1, + entropy: issuance_entropy, + }), + finalizer: FinalizerSpec::default(), + }], + outputs: vec![ + OutputSchema { + id: "out0".to_string(), + amount_sat: 1, + lock: LockVariant::Script { + script: signer_script.clone(), + }, + asset: AssetVariant::NewIssuanceToken { input_index: 0 }, + blinder: BlinderVariant::Wallet, + }, + OutputSchema { + id: "out1".to_string(), + amount_sat: *issue_amount, + lock: LockVariant::Script { + script: signer_script, + }, + asset: AssetVariant::NewIssuanceAsset { input_index: 0 }, + blinder: BlinderVariant::Wallet, + }, + ], + fee_rate_sat_vb: Some(0.1), + locktime: None, + }, + broadcast: *broadcast, + }; - let keypair = derive_keypair(*account_index); - let blinding_keypair = derive_public_blinder_key(); - - let fee_tx_out = fetch_utxo(*fee_utxo_outpoint).await?; - - let pst = contracts::sdk::issue_asset( - &blinding_keypair.public_key(), - (*fee_utxo_outpoint, fee_tx_out.clone()), - *issue_amount, - *fee_amount, - )?; - - let (asset_id, reissuance_asset_id) = pst.inputs()[0].issuance_ids(); - let asset_entropy = pst.inputs()[0] - .issuance_asset_entropy - .expect("expected entropy"); - let asset_entropy = AssetId::generate_asset_entropy( - *fee_utxo_outpoint, - ContractHash::from_byte_array(asset_entropy), - ); - - let tx = pst.extract_tx()?; - let utxos = &[fee_tx_out]; + let tx_info = execute_request(&mut runtime, request).await?; - let x_only_public_key = keypair.x_only_public_key().0; - let signature = create_p2pk_signature(&tx, utxos, &keypair, 0, NETWORK)?; + let tx = decode_transaction(&tx_info.tx_hex)?; + let input = tx + .input + .first() + .ok_or_else(|| anyhow!("issued transaction is missing input[0]"))?; + let (asset_id, reissuance_asset_id) = input.issuance_ids(); - let tx = finalize_p2pk_transaction( - tx, - utxos, - &x_only_public_key, - &signature, - 0, - NETWORK, - TrackerLogLevel::None, - )?; + let asset_entropy = get_new_asset_entropy(&input.previous_output, issuance_entropy); println!( - "Asset id: {asset_id}, \ - Reissuance asset: {reissuance_asset_id}, \ - Asset entropy: {}", - hex::encode(asset_entropy) + "Asset id: {asset_id}, Reissuance asset: {reissuance_asset_id}, Asset entropy: {}", + asset_entropy.to_hex() ); - if *broadcast { - println!("Broadcasted txid: {}", broadcast_tx(&tx).await?); - } else { - println!("{}", tx.serialize().to_lower_hex_string()); - } + store + .store + .insert(asset_name, &asset_entropy.to_byte_array())?; store .store - .insert(asset_name, hex::encode(asset_entropy).as_bytes())?; + .insert(format!("re-{asset_name}"), reissuance_asset_id.serialize())?; Ok(()) } Self::ReissueAsset { - reissue_asset_outpoint, - fee_utxo_outpoint, asset_name, reissue_amount, - fee_amount, - account_index, broadcast, } => { let store = Store::load()?; @@ -387,67 +355,69 @@ impl Basic { let Some(asset_entropy) = store.store.get(asset_name)? else { return Err(anyhow!("Asset name not found")); }; - let asset_entropy = String::from_utf8(asset_entropy.to_vec())?; - let asset_entropy = hex::decode(asset_entropy)?; - - let asset_entropy_bytes: [u8; 32] = - asset_entropy.try_into().expect("expected 32 bytes"); - let asset_entropy = sha256::Midstate::from_byte_array(asset_entropy_bytes); - - let keypair = derive_keypair(*account_index); - let blinding_keypair = derive_public_blinder_key(); - - let reissue_tx_out = fetch_utxo(*reissue_asset_outpoint).await?; - let fee_tx_out = fetch_utxo(*fee_utxo_outpoint).await?; - - let reissue_utxo_secrets = - reissue_tx_out.unblind(SECP256K1, blinding_keypair.secret_key())?; - - let pst = contracts::sdk::reissue_asset( - &blinding_keypair.public_key(), - (*reissue_asset_outpoint, reissue_tx_out.clone()), - reissue_utxo_secrets, - (*fee_utxo_outpoint, fee_tx_out.clone()), - *reissue_amount, - *fee_amount, - asset_entropy, - )?; - - let (asset_id, reissuance_asset_id) = pst.inputs()[0].issuance_ids(); - - let tx = pst.extract_tx()?; - let utxos = vec![reissue_tx_out, fee_tx_out]; - let x_only_public_key = keypair.x_only_public_key().0; - - let signature_0 = create_p2pk_signature(&tx, &utxos, &keypair, 0, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_0, - 0, - NETWORK, - TrackerLogLevel::None, - )?; - - let signature_1 = create_p2pk_signature(&tx, &utxos, &keypair, 1, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_1, - 1, - NETWORK, - TrackerLogLevel::None, - )?; - - println!("Asset id: {asset_id}, Reissuance id: {reissuance_asset_id}"); - - if *broadcast { - println!("Broadcasted txid: {}", broadcast_tx(&tx).await?); - } else { - println!("{}", tx.serialize().to_lower_hex_string()); - } + let Some(reissue_token_id) = store.store.get(format!("re-{asset_name}"))? else { + return Err(anyhow!("Asset name not found")); + }; + let asset_entropy = Midstate::from_slice(&asset_entropy)?; + let reissue_token_id = AssetId::from_slice(&reissue_token_id)?; + + let signer_script = runtime.signer_receive_address()?.script_pubkey(); + + let request = TxCreateRequest { + abi_version: TX_CREATE_ABI_VERSION.to_string(), + request_id: "request-basic.reissue_asset".to_string(), + network: runtime.network, + params: RuntimeParams { + inputs: vec![InputSchema { + id: "input0".to_string(), + utxo_source: UTXOSource::Wallet { + filter: WalletSourceFilter { + asset: AssetFilter::Exact { + asset_id: reissue_token_id, + }, + amount: AmountFilter::Min { satoshi: 1 }, + lock: LockFilter::None, + }, + }, + blinder: InputBlinder::default(), + sequence: Sequence::default(), + issuance: Some(InputIssuance { + kind: InputIssuanceKind::Reissue, + asset_amount_sat: *reissue_amount, + token_amount_sat: 0, + entropy: asset_entropy.to_byte_array(), + }), + finalizer: FinalizerSpec::default(), + }], + outputs: vec![ + OutputSchema { + id: "out0".to_string(), + amount_sat: 1, + lock: LockVariant::Script { + script: signer_script.clone(), + }, + asset: AssetVariant::AssetId { + asset_id: reissue_token_id, + }, + blinder: BlinderVariant::Wallet, + }, + OutputSchema { + id: "out1".to_string(), + amount_sat: *reissue_amount, + lock: LockVariant::Script { + script: signer_script, + }, + asset: AssetVariant::ReIssuanceAsset { input_index: 0 }, + blinder: BlinderVariant::Wallet, + }, + ], + fee_rate_sat_vb: Some(0.1), + locktime: None, + }, + broadcast: *broadcast, + }; + + let _ = execute_request(&mut runtime, request).await?; Ok(()) } diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index b4d90c3..df32184 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -1,7 +1,4 @@ -use simplicityhl_core::SimplicityNetwork; - pub mod basic; -pub mod options; -pub mod smt_storage; - -const NETWORK: SimplicityNetwork = SimplicityNetwork::LiquidTestnet; +pub mod option_offer; +// pub mod options; +// pub mod smt_storage; diff --git a/crates/cli/src/commands/option_offer.rs b/crates/cli/src/commands/option_offer.rs new file mode 100644 index 0000000..eb336ff --- /dev/null +++ b/crates/cli/src/commands/option_offer.rs @@ -0,0 +1,513 @@ +#![allow(clippy::missing_errors_doc)] + +use std::convert::TryFrom; +use std::future::Future; + +use crate::modules::store::Store; +use crate::modules::utils::execute_request; + +use anyhow::{Context, anyhow}; +use clap::Subcommand; +use contracts::option_offer::{OptionOfferArguments, OptionOfferRuntime, get_option_offer_address}; +use simplicityhl::elements::{AssetId, OutPoint, Script, Txid}; +use wallet_abi::Encodable; +use wallet_abi::runtime::WalletRuntimeConfig; +use wallet_abi::schema::tx_create::TxCreateRequest; +use wallet_abi::taproot_pubkey_gen::TaprootPubkeyGen; + +/// Option-offer contract utilities. +#[derive(Subcommand, Debug)] +pub enum OptionOffer { + /// Create, store, and fund a new option-offer contract in one command. + Create { + /// Collateral asset id. + #[arg(long = "collateral-asset-id")] + collateral_asset_id: AssetId, + /// Premium asset id. + #[arg(long = "premium-asset-id")] + premium_asset_id: AssetId, + /// Settlement asset id. + #[arg(long = "settlement-asset-id")] + settlement_asset_id: AssetId, + /// Expected collateral amount user will deposit. + #[arg(long = "expected-to-deposit-collateral")] + expected_to_deposit_collateral: u64, + /// Expected premium amount user will deposit. + #[arg(long = "expected-to-deposit-premium")] + expected_to_deposit_premium: u64, + /// Expected settlement amount user will get on exercise. + #[arg(long = "expected-to-get-settlement")] + expected_to_get_settlement: u64, + /// Unix timestamp after which expiry path becomes valid. + #[arg(long = "expiry-time")] + expiry_time: u32, + /// When set, broadcast the built transaction via Esplora and print txid. + #[arg(long = "broadcast")] + broadcast: bool, + }, + /// Import option-offer arguments into local store. + Import { + /// Option-offer taproot pubkey gen handle used as local store key. + #[arg(long = "option-offer-taproot-pubkey-gen")] + option_offer_taproot_pubkey_gen: String, + /// Encoded option-offer arguments (hex). + #[arg(long = "encoded-option-offer-arguments")] + encoded_option_offer_arguments: String, + }, + /// Export option-offer arguments from local store. + Export { + /// Option-offer taproot pubkey gen handle used as local store key. + #[arg(long = "option-offer-taproot-pubkey-gen")] + option_offer_taproot_pubkey_gen: String, + }, + /// Exercise path: swap settlement asset for collateral and premium. + Exercise { + /// Option-offer taproot pubkey gen handle. + #[arg(long = "option-offer-taproot-pubkey-gen")] + option_offer_taproot_pubkey_gen: String, + /// Creation txid containing covenant outputs (collateral at vout=0, premium at vout=1). + #[arg(long = "creation-txid")] + creation_tx_id: Txid, + /// Collateral amount to receive from covenant. + #[arg(long = "collateral-amount")] + collateral_amount: u64, + /// When set, broadcast the built transaction via Esplora and print txid. + #[arg(long = "broadcast")] + broadcast: bool, + }, + /// Withdraw settlement from covenant after exercise. + Withdraw { + /// Option-offer taproot pubkey gen handle. + #[arg(long = "option-offer-taproot-pubkey-gen")] + option_offer_taproot_pubkey_gen: String, + /// Exercise txid that produced settlement output in covenant. + #[arg(long = "exercise-txid")] + exercise_tx_id: Txid, + /// When set, broadcast the built transaction via Esplora and print txid. + #[arg(long = "broadcast")] + broadcast: bool, + }, + /// Expiry path: reclaim collateral and premium after expiry time. + Expiry { + /// Option-offer taproot pubkey gen handle. + #[arg(long = "option-offer-taproot-pubkey-gen")] + option_offer_taproot_pubkey_gen: String, + /// Creation txid containing covenant outputs (collateral at vout=0, premium at vout=1). + #[arg(long = "creation-txid")] + creation_tx_id: Txid, + /// When set, broadcast the built transaction via Esplora and print txid. + #[arg(long = "broadcast")] + broadcast: bool, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct CandidateOutput { + vout: u32, + script_pubkey: Script, + asset_id: Option, + value_sat: Option, +} + +struct OptionOfferContext { + runtime: OptionOfferRuntime, +} + +impl OptionOfferContext { + const fn new(runtime: OptionOfferRuntime) -> Self { + Self { runtime } + } + + fn load(runtime: WalletRuntimeConfig, taproot_pubkey_gen: &str) -> anyhow::Result { + let store = Store::load().context("failed to open local store")?; + let args = store + .get_arguments::(taproot_pubkey_gen) + .with_context(|| { + format!("failed to load option-offer arguments for key '{taproot_pubkey_gen}'") + })?; + + let tap = TaprootPubkeyGen::build_from_str( + taproot_pubkey_gen, + &args, + runtime.network, + &get_option_offer_address, + ) + .with_context(|| format!("invalid option-offer taproot handle '{taproot_pubkey_gen}'"))?; + + let signer_pubkey = runtime.signer_x_only_public_key()?.serialize(); + if signer_pubkey != args.user_pubkey() { + return Err(anyhow!( + "signer x-only pubkey mismatch: signer={}, args.user_pubkey={}", + hex::encode(signer_pubkey), + hex::encode(args.user_pubkey()) + )); + } + + Ok(Self::new(OptionOfferRuntime::new(runtime, args, tap))) + } + + const fn runtime_mut(&mut self) -> &mut WalletRuntimeConfig { + self.runtime.runtime_mut() + } + + fn taproot_pubkey_gen(&self) -> String { + self.runtime.tap().to_string() + } + + fn build_deposit_request( + &self, + collateral_deposit_amount: u64, + broadcast: bool, + ) -> TxCreateRequest { + let mut request = self + .runtime + .build_deposit_request(collateral_deposit_amount); + request.broadcast = broadcast; + request + } + + async fn build_exercise_request( + &self, + creation_tx_id: Txid, + collateral_amount: u64, + broadcast: bool, + ) -> anyhow::Result { + let mut request = self + .runtime + .build_exercise_request(creation_tx_id, collateral_amount) + .await?; + request.broadcast = broadcast; + Ok(request) + } + + async fn build_withdraw_request( + &self, + exercise_tx_id: Txid, + broadcast: bool, + ) -> anyhow::Result { + let tx = { + let inner_esplora = self.runtime.runtime().esplora.lock().await; + inner_esplora.get_transaction(exercise_tx_id).await? + }; + + let covenant_script = self.runtime.tap().address.script_pubkey(); + let settlement_asset_id = self.runtime.args().get_settlement_asset_id(); + + let outputs = tx + .output + .iter() + .enumerate() + .filter(|(_, tx_out)| tx_out.script_pubkey.eq(&covenant_script)) + .map( + |(vout, tx_out)| -> anyhow::Result> { + if tx_out.asset.is_confidential() { + return Ok(None); + } + + let asset_id = tx_out.asset.explicit().ok_or_else(|| { + anyhow!( + "exercise transaction output at vout={vout} has non-explicit asset id" + ) + })?; + + if asset_id != settlement_asset_id { + return Ok(None); + } + + Ok(Some(CandidateOutput { + vout: u32::try_from(vout).context("exercise transaction vout overflow")?, + script_pubkey: tx_out.script_pubkey.clone(), + asset_id: Some(asset_id), + value_sat: tx_out.value.explicit(), + })) + }, + ) + .collect::>>()?; + + let outputs: Vec<_> = outputs.into_iter().flatten().collect(); + + let (settlement_vout, settlement_amount) = + select_settlement_output_for_withdraw(&outputs, &covenant_script, settlement_asset_id)?; + + let mut request = self.runtime.build_withdraw_request_for_outpoint( + OutPoint::new(exercise_tx_id, settlement_vout), + settlement_amount, + )?; + request.broadcast = broadcast; + + Ok(request) + } + + async fn build_expiry_request( + &self, + creation_tx_id: Txid, + broadcast: bool, + ) -> anyhow::Result { + let collateral_outpoint = OutPoint::new(creation_tx_id, 0); + let collateral_tx_out = self + .runtime + .runtime() + .fetch_tx_out(&collateral_outpoint) + .await?; + ensure_explicit_asset( + Option::from(&collateral_tx_out.asset.explicit()), + self.runtime.args().get_collateral_asset_id(), + "covenant collateral output", + )?; + let collateral_amount = explicit_amount( + collateral_tx_out.value.explicit(), + "covenant collateral output", + )?; + + let premium_outpoint = OutPoint::new(creation_tx_id, 1); + let premium_tx_out = self + .runtime + .runtime() + .fetch_tx_out(&premium_outpoint) + .await?; + ensure_explicit_asset( + Option::from(&premium_tx_out.asset.explicit()), + self.runtime.args().get_premium_asset_id(), + "covenant premium output", + )?; + let premium_amount = + explicit_amount(premium_tx_out.value.explicit(), "covenant premium output")?; + + let mut request = + self.runtime + .build_expiry_request(creation_tx_id, collateral_amount, premium_amount)?; + request.broadcast = broadcast; + + Ok(request) + } +} + +impl OptionOffer { + #[allow(clippy::too_many_lines)] + pub async fn handle(&self, runtime: WalletRuntimeConfig) -> anyhow::Result<()> { + match self { + Self::Create { + collateral_asset_id, + premium_asset_id, + settlement_asset_id, + expected_to_deposit_collateral, + expected_to_deposit_premium, + expected_to_get_settlement, + expiry_time, + broadcast, + } => { + let (collateral_per_contract, premium_per_collateral) = + derive_contract_terms_from_expected_amounts( + *expected_to_deposit_collateral, + *expected_to_deposit_premium, + *expected_to_get_settlement, + )?; + + let user_pubkey = runtime.signer_x_only_public_key()?.serialize(); + let args = OptionOfferArguments::new( + *collateral_asset_id, + *premium_asset_id, + *settlement_asset_id, + collateral_per_contract, + premium_per_collateral, + *expiry_time, + user_pubkey, + ); + + let tap = TaprootPubkeyGen::from(&args, runtime.network, &get_option_offer_address) + .context("failed to derive option-offer taproot handle")?; + let taproot_pubkey_gen = tap.to_string(); + let encoded = args.encode()?; + + let store = Store::load().context("failed to open local store")?; + if store.store.get(&taproot_pubkey_gen)?.is_some() { + return Err(anyhow!( + "option-offer key already exists in store: {taproot_pubkey_gen}" + )); + } + + let mut context = + OptionOfferContext::new(OptionOfferRuntime::new(runtime, args, tap)); + let request = + context.build_deposit_request(*expected_to_deposit_collateral, *broadcast); + execute_then_store_option_offer(&store, &taproot_pubkey_gen, &encoded, || async { + let _ = execute_request(context.runtime_mut(), request).await?; + Ok(()) + }) + .await?; + + println!( + "Option-offer taproot pubkey gen: {}", + context.taproot_pubkey_gen() + ); + println!("Option-offer address: {}", context.runtime.tap().address); + println!("Encoded option-offer arguments: {}", hex::encode(encoded)); + println!("Derived collateral-per-contract: {collateral_per_contract}"); + println!("Derived premium-per-collateral: {premium_per_collateral}"); + println!("Expiry-time: {expiry_time}"); + + Ok(()) + } + Self::Import { + option_offer_taproot_pubkey_gen, + encoded_option_offer_arguments, + } => Store::load()?.import_arguments::( + option_offer_taproot_pubkey_gen, + encoded_option_offer_arguments, + runtime.network, + &get_option_offer_address, + ), + Self::Export { + option_offer_taproot_pubkey_gen, + } => { + println!( + "{}", + Store::load()?.export_arguments(option_offer_taproot_pubkey_gen)? + ); + Ok(()) + } + Self::Exercise { + option_offer_taproot_pubkey_gen, + creation_tx_id, + collateral_amount, + broadcast, + } => { + let mut context = + OptionOfferContext::load(runtime, option_offer_taproot_pubkey_gen)?; + let request = context + .build_exercise_request(*creation_tx_id, *collateral_amount, *broadcast) + .await?; + let _ = execute_request(context.runtime_mut(), request).await?; + Ok(()) + } + Self::Withdraw { + option_offer_taproot_pubkey_gen, + exercise_tx_id, + broadcast, + } => { + let mut context = + OptionOfferContext::load(runtime, option_offer_taproot_pubkey_gen)?; + let request = context + .build_withdraw_request(*exercise_tx_id, *broadcast) + .await?; + let _ = execute_request(context.runtime_mut(), request).await?; + Ok(()) + } + Self::Expiry { + option_offer_taproot_pubkey_gen, + creation_tx_id, + broadcast, + } => { + let mut context = + OptionOfferContext::load(runtime, option_offer_taproot_pubkey_gen)?; + let request = context + .build_expiry_request(*creation_tx_id, *broadcast) + .await?; + let _ = execute_request(context.runtime_mut(), request).await?; + Ok(()) + } + } + } +} + +fn checked_div_exact_u64(numerator: u64, denominator: u64, label: &str) -> anyhow::Result { + if denominator == 0 { + return Err(anyhow!("{label} denominator must be > 0")); + } + if !numerator.is_multiple_of(denominator) { + return Err(anyhow!( + "{label} must divide exactly: numerator={numerator}, denominator={denominator}" + )); + } + Ok(numerator / denominator) +} + +async fn execute_then_store_option_offer( + store: &Store, + taproot_pubkey_gen: &str, + encoded: &[u8], + execute: Exec, +) -> anyhow::Result<()> +where + Exec: FnOnce() -> ExecFuture, + ExecFuture: Future>, +{ + execute().await?; + + store + .store + .insert(taproot_pubkey_gen, encoded) + .with_context(|| { + format!( + "transaction succeeded but failed to persist option-offer arguments; \ +taproot key: {taproot_pubkey_gen}; encoded args (hex): {}. \ +You can recover by re-importing this value with `option-offer import`.", + hex::encode(encoded) + ) + })?; + + Ok(()) +} + +fn derive_contract_terms_from_expected_amounts( + expected_to_deposit_collateral: u64, + expected_to_deposit_premium: u64, + expected_to_get_settlement: u64, +) -> anyhow::Result<(u64, u64)> { + if expected_to_deposit_collateral == 0 { + return Err(anyhow!("expected-to-deposit-collateral must be > 0")); + } + + let premium_per_collateral = checked_div_exact_u64( + expected_to_deposit_premium, + expected_to_deposit_collateral, + "expected-to-deposit-premium / expected-to-deposit-collateral", + )?; + let collateral_per_contract = checked_div_exact_u64( + expected_to_get_settlement, + expected_to_deposit_collateral, + "expected-to-get-settlement / expected-to-deposit-collateral", + )?; + + Ok((collateral_per_contract, premium_per_collateral)) +} + +fn ensure_explicit_asset( + actual: Option<&AssetId>, + expected: AssetId, + context_label: &str, +) -> anyhow::Result<()> { + match actual { + Some(asset_id) if *asset_id == expected => Ok(()), + Some(asset_id) => Err(anyhow!( + "{context_label} has wrong asset id: expected {expected}, got {asset_id}" + )), + None => Err(anyhow!("{context_label} must have explicit asset id")), + } +} + +fn explicit_amount(value: Option, context_label: &str) -> anyhow::Result { + value.ok_or_else(|| anyhow!("{context_label} must have explicit value")) +} + +fn select_settlement_output_for_withdraw( + outputs: &[CandidateOutput], + covenant_script: &Script, + settlement_asset_id: AssetId, +) -> anyhow::Result<(u32, u64)> { + let Some(selected) = outputs + .iter() + .rfind(|output| output.script_pubkey == *covenant_script) + else { + return Err(anyhow!("exercise transaction has no covenant outputs")); + }; + + ensure_explicit_asset( + Option::from(&selected.asset_id), + settlement_asset_id, + "selected covenant settlement output", + )?; + let amount = explicit_amount(selected.value_sat, "selected covenant settlement output")?; + + Ok((selected.vout, amount)) +} diff --git a/crates/cli/src/commands/options.rs b/crates/cli/src/commands/options.rs deleted file mode 100644 index 6c8ec86..0000000 --- a/crates/cli/src/commands/options.rs +++ /dev/null @@ -1,844 +0,0 @@ -#![allow(clippy::similar_names)] - -use std::str::FromStr; - -use anyhow::Result; - -use clap::Subcommand; - -use contracts::options::{OptionsArguments, finalize_options_transaction, get_options_program}; -use contracts::sdk::taproot_pubkey_gen::{TaprootPubkeyGen, get_random_seed}; - -use simplicityhl::elements::OutPoint; -use simplicityhl::elements::pset::serialize::Serialize; -use simplicityhl::elements::secp256k1_zkp::SECP256K1; -use simplicityhl::simplicity::elements::AssetId; -use simplicityhl::simplicity::hex::DisplayHex; -use simplicityhl::tracker::TrackerLogLevel; - -use crate::commands::NETWORK; -use crate::explorer::{broadcast_tx, fetch_utxo}; -use crate::modules::store::Store; -use crate::modules::utils::derive_keypair; -use simplicityhl_core::{ - Encodable, create_p2pk_signature, derive_public_blinder_key, finalize_p2pk_transaction, -}; - -/// Options contract utilities -#[derive(Subcommand, Debug)] -pub enum Options { - /// Creates a set of two UTXOs (option and grantor) for the options contract - Create { - /// First fee utxo used to issue the option reissuance token - #[arg(long = "first-fee-utxo")] - first_fee_utxo: OutPoint, - /// Second fee utxo used to issue the grantor reissuance token - #[arg(long = "second-fee-utxo")] - second_fee_utxo: OutPoint, - /// Start time (UNIX seconds) - #[arg(long = "start-time")] - start_time: u32, - /// Expiry time (UNIX seconds) - #[arg(long = "expiry-time")] - expiry_time: u32, - /// Collateral per contract - #[arg(long = "collateral-per-contract")] - collateral_per_contract: u64, - /// Settlement per contract (in settlement asset units) - #[arg(long = "settlement-per-contract")] - settlement_per_contract: u64, - /// Settlement asset id (hex, BE) - #[arg(long = "settlement-asset-id-hex-be")] - settlement_asset_id_hex_be: String, - /// Collateral asset id (hex, BE) - #[arg(long = "collateral-asset-id-hex-be")] - collateral_asset_id_hex_be: String, - /// Account index that will pay for transaction fees and that owns a tokens to send - #[arg(long = "account-index")] - account_index: u32, - /// Fee amount in satoshis (LBTC) - #[arg(long = "fee-amount")] - fee_amount: u64, - /// When set, broadcast the built transaction via Esplora and print txid - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// Lock collateral on the options contract to get option and grantor tokens - Fund { - /// Option reissuance token UTXO - #[arg(long = "option-asset-utxo")] - option_asset_utxo: OutPoint, - /// Grantor reissuance token UTXO - #[arg(long = "grantor-asset-utxo")] - grantor_asset_utxo: OutPoint, - /// Collateral and fee UTXO - #[arg(long = "collateral-utxo")] - collateral_utxo: OutPoint, - /// Option taproot pubkey gen that in this CLI works as unique contract identifier - #[arg(long = "option-taproot-pubkey-gen")] - option_taproot_pubkey_gen: String, - /// Collateral amount in satoshis (LBTC) - #[arg(long = "collateral-amount")] - collateral_amount: u64, - /// Account index that will pay for transaction fees and that owns a tokens to send - #[arg(long = "account-index")] - account_index: u32, - /// Transaction id (hex) and output index (vout) of the LBTC UTXO used to pay fees - #[arg(long = "fee-utxo")] - fee_utxo: Option, - /// Fee amount in satoshis (LBTC) - #[arg(long = "fee-amount")] - fee_amount: u64, - /// When set, broadcast the built transaction via Esplora and print txid - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// Exercise path: burn option tokens and settle against settlement asset - Exercise { - /// Collateral UTXO at the options address (LBTC) - #[arg(long = "collateral-utxo")] - collateral_utxo: OutPoint, - /// Option asset utxo - #[arg(long = "option-asset-utxo")] - option_asset_utxo: OutPoint, - /// Settlement asset UTXO - #[arg(long = "asset-utxo")] - asset_utxo: OutPoint, - /// Fee UTXO used to pay fees (P2PK) - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Option taproot pubkey gen that in this CLI works as unique contract identifier - #[arg(long = "option-taproot-pubkey-gen")] - option_taproot_pubkey_gen: String, - /// Amount of option tokens to burn - #[arg(long = "amount-to-burn")] - amount_to_burn: u64, - /// Fee amount in satoshis (LBTC) to pay (deducted from collateral input) - #[arg(long = "fee-amount")] - fee_amount: u64, - /// Account index that will pay for transaction fees and that owns a tokens to send - #[arg(long = "account-index")] - account_index: u32, - /// When set, broadcast the built transaction via Esplora and print txid - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// Settlement path: burn grantor tokens against settlement asset held by the covenant (contract) - Settle { - /// Settlement asset UTXO owned by the options contract - #[arg(long = "settlement-asset-utxo")] - settlement_asset_utxo: OutPoint, - /// Grantor token UTXO consumed for burning - #[arg(long = "grantor-asset-utxo")] - grantor_asset_utxo: OutPoint, - /// Fee UTXO used to pay fees (P2PK) - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Option taproot pubkey gen that in this CLI works as unique contract identifier - #[arg(long = "option-taproot-pubkey-gen")] - option_taproot_pubkey_gen: String, - /// Amount of grantor tokens to burn - #[arg(long = "grantor-token-amount-to-burn")] - grantor_token_amount_to_burn: u64, - /// Fee amount in satoshis (LBTC) to pay - #[arg(long = "fee-amount")] - fee_amount: u64, - /// Account index that will pay for transaction fees and that owns a tokens to send - #[arg(long = "account-index")] - account_index: u32, - /// When set, broadcast the built transaction via Esplora and print txid - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// Expiry path: burn grantor tokens and withdraw collateral to P2PK (contract) - Expire { - /// Collateral UTXO owned by the options contract (LBTC) - #[arg(long = "collateral-utxo")] - collateral_utxo: OutPoint, - /// Grantor token UTXO consumed for burning - #[arg(long = "grantor-asset-utxo")] - grantor_asset_utxo: OutPoint, - /// Fee UTXO used to pay fees (P2PK) - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Option taproot pubkey gen that in this CLI works as unique contract identifier - #[arg(long = "option-taproot-pubkey-gen")] - option_taproot_pubkey_gen: String, - /// Amount of grantor tokens to burn - #[arg(long = "grantor-token-amount-to-burn")] - grantor_token_amount_to_burn: u64, - /// Fee amount in satoshis (LBTC) to pay (deducted from collateral input) - #[arg(long = "fee-amount")] - fee_amount: u64, - /// Account index that will pay for transaction fees and that owns a tokens to send - #[arg(long = "account-index")] - account_index: u32, - /// When set, broadcast the built transaction via Esplora and print txid - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// Cancellation path: burn both tokens and withdraw collateral to P2PK (contract) - Cancel { - /// Collateral UTXO owned by the options contract (LBTC) - #[arg(long = "collateral-utxo")] - collateral_utxo: OutPoint, - /// Option token UTXO consumed for burning - #[arg(long = "option-asset-utxo")] - option_asset_utxo: OutPoint, - /// Grantor token UTXO consumed for burning - #[arg(long = "grantor-asset-utxo")] - grantor_asset_utxo: OutPoint, - /// Fee UTXO used to pay fees (P2PK) - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Option taproot pubkey gen that in this CLI works as unique contract identifier - #[arg(long = "option-taproot-pubkey-gen")] - option_taproot_pubkey_gen: String, - /// Amount of both tokens to burn - #[arg(long = "amount-to-burn")] - amount_to_burn: u64, - /// Fee amount in satoshis (LBTC) to pay (deducted from collateral input) - #[arg(long = "fee-amount")] - fee_amount: u64, - /// Account index that will pay for transaction fees and that owns a tokens to send - #[arg(long = "account-index")] - account_index: u32, - /// When set, broadcast the built transaction via Esplora and print txid - #[arg(long = "broadcast")] - broadcast: bool, - }, - /// This function is used to import the options arguments into the store - /// Se the user can perform operations on the contract - Import { - /// Option taproot pubkey gen that in this CLI works as unique contract identifier - #[arg(long = "option-taproot-pubkey-gen")] - option_taproot_pubkey_gen: String, - /// Encoded options arguments - #[arg(long = "encoded-options-arguments")] - encoded_options_arguments: String, - }, - Export { - /// Option taproot pubkey gen - #[arg(long = "option-taproot-pubkey-gen")] - option_taproot_pubkey_gen: String, - }, -} - -impl Options { - /// Handle options CLI subcommand execution. - /// - /// # Errors - /// Returns error if the subcommand operation fails. - #[expect(clippy::too_many_lines)] - pub async fn handle(&self) -> Result<()> { - match self { - Self::Import { - option_taproot_pubkey_gen, - encoded_options_arguments, - } => Store::load()?.import_arguments( - option_taproot_pubkey_gen, - encoded_options_arguments, - NETWORK, - &contracts::options::get_options_address, - ), - Self::Export { - option_taproot_pubkey_gen, - } => { - println!( - "{}", - Store::load()?.export_arguments(option_taproot_pubkey_gen)? - ); - Ok(()) - } - Self::Create { - first_fee_utxo, - second_fee_utxo, - start_time, - expiry_time, - collateral_per_contract, - settlement_per_contract, - settlement_asset_id_hex_be, - collateral_asset_id_hex_be, - account_index, - fee_amount, - broadcast, - } => { - let store = Store::load()?; - let keypair = derive_keypair(*account_index); - let blinder_keypair = derive_public_blinder_key(); - - let first_tx_out = fetch_utxo(*first_fee_utxo).await?; - let second_tx_out = fetch_utxo(*second_fee_utxo).await?; - - let issuance_asset_entropy = get_random_seed(); - - let settlement_asset_id = AssetId::from_str(settlement_asset_id_hex_be)?; - let collateral_asset_id = AssetId::from_str(collateral_asset_id_hex_be)?; - - let option_arguments = OptionsArguments::new( - *start_time, - *expiry_time, - *collateral_per_contract, - *settlement_per_contract, - collateral_asset_id, - settlement_asset_id, - issuance_asset_entropy, - (*first_fee_utxo, false), - (*second_fee_utxo, false), - ); - - let (pst, options_taproot_pubkey_gen) = contracts::sdk::build_option_creation( - &blinder_keypair.public_key(), - (*first_fee_utxo, first_tx_out.clone()), - (*second_fee_utxo, second_tx_out.clone()), - &option_arguments, - issuance_asset_entropy, - *fee_amount, - NETWORK, - )?; - - let tx = pst.extract_tx()?; - let utxos = vec![first_tx_out.clone(), second_tx_out.clone()]; - let x_only_public_key = keypair.x_only_public_key().0; - - let signature_0 = create_p2pk_signature(&tx, &utxos, &keypair, 0, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_0, - 0, - NETWORK, - TrackerLogLevel::None, - )?; - - let signature_1 = create_p2pk_signature(&tx, &utxos, &keypair, 1, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_1, - 1, - NETWORK, - TrackerLogLevel::None, - )?; - - println!("options_taproot_pubkey_gen: {options_taproot_pubkey_gen}"); - - store.import_arguments( - &options_taproot_pubkey_gen.to_string(), - &option_arguments.to_hex()?, - NETWORK, - &contracts::options::get_options_address, - )?; - - if *broadcast { - println!("Broadcasted txid: {}", broadcast_tx(&tx).await?); - } else { - println!("{}", tx.serialize().to_lower_hex_string()); - } - - Ok(()) - } - Self::Fund { - option_asset_utxo, - grantor_asset_utxo, - collateral_utxo, - option_taproot_pubkey_gen, - collateral_amount, - account_index, - fee_utxo, - fee_amount, - broadcast, - } => { - let store = Store::load()?; - let keypair = derive_keypair(*account_index); - let blinder_keypair = derive_public_blinder_key(); - - let option_arguments: OptionsArguments = - store.get_arguments(option_taproot_pubkey_gen)?; - - let taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - option_taproot_pubkey_gen, - &option_arguments, - NETWORK, - &contracts::options::get_options_address, - )?; - - let option_tx_out = fetch_utxo(*option_asset_utxo).await?; - let grantor_tx_out = fetch_utxo(*grantor_asset_utxo).await?; - let collateral_tx_out = fetch_utxo(*collateral_utxo).await?; - - let fee_utxo = if let Some(outpoint) = *fee_utxo { - let fee_tx_out = fetch_utxo(outpoint).await?; - Some(&(outpoint, fee_tx_out)) - } else { - None - }; - - let input_option_secrets = - option_tx_out.unblind(SECP256K1, blinder_keypair.secret_key())?; - let input_grantor_secrets = - grantor_tx_out.unblind(SECP256K1, blinder_keypair.secret_key())?; - - let (pst, option_branch) = contracts::sdk::build_option_funding( - &blinder_keypair, - ( - *option_asset_utxo, - option_tx_out.clone(), - input_option_secrets, - ), - ( - *grantor_asset_utxo, - grantor_tx_out.clone(), - input_grantor_secrets, - ), - (*collateral_utxo, collateral_tx_out.clone()), - fee_utxo, - &option_arguments, - *collateral_amount, - *fee_amount, - )?; - - let tx = pst.extract_tx()?; - - let mut utxos = vec![ - option_tx_out.clone(), - grantor_tx_out.clone(), - collateral_tx_out.clone(), - ]; - - if let Some((_, fee_tx_out)) = fee_utxo { - utxos.push(fee_tx_out.clone()); - } - - let tx = finalize_options_transaction( - tx, - &taproot_pubkey_gen.get_x_only_pubkey(), - &get_options_program(&option_arguments)?, - &utxos, - 0, - &option_branch, - NETWORK, - TrackerLogLevel::None, - )?; - - let tx = finalize_options_transaction( - tx, - &taproot_pubkey_gen.get_x_only_pubkey(), - &get_options_program(&option_arguments)?, - &utxos, - 1, - &option_branch, - NETWORK, - TrackerLogLevel::None, - )?; - - let x_only_public_key = keypair.x_only_public_key().0; - let signature = create_p2pk_signature(&tx, &utxos, &keypair, 2, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature, - 2, - NETWORK, - TrackerLogLevel::None, - )?; - - let tx = if fee_utxo.is_some() { - let signature = create_p2pk_signature(&tx, &utxos, &keypair, 3, NETWORK)?; - finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature, - 3, - NETWORK, - TrackerLogLevel::None, - )? - } else { - tx - }; - - if *broadcast { - println!("Broadcasted txid: {}", broadcast_tx(&tx).await?); - } else { - println!("{}", tx.serialize().to_lower_hex_string()); - } - Ok(()) - } - Self::Exercise { - collateral_utxo, - option_asset_utxo, - asset_utxo, - fee_utxo, - option_taproot_pubkey_gen, - amount_to_burn, - fee_amount, - account_index, - broadcast, - } => { - let keypair = derive_keypair(*account_index); - - let store = Store::load()?; - let option_arguments: OptionsArguments = - store.get_arguments(option_taproot_pubkey_gen)?; - - let taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - option_taproot_pubkey_gen, - &option_arguments, - NETWORK, - &contracts::options::get_options_address, - )?; - - let collateral_tx_out = fetch_utxo(*collateral_utxo).await?; - let option_tx_out = fetch_utxo(*option_asset_utxo).await?; - let asset_tx_out = fetch_utxo(*asset_utxo).await?; - let fee_tx_out = fetch_utxo(*fee_utxo).await?; - - let (pst, option_branch) = contracts::sdk::build_option_exercise( - (*collateral_utxo, collateral_tx_out.clone()), - (*option_asset_utxo, option_tx_out.clone()), - (*asset_utxo, asset_tx_out.clone()), - Some((*fee_utxo, fee_tx_out.clone())), - *amount_to_burn, - *fee_amount, - &option_arguments, - )?; - - let tx = pst.extract_tx()?; - let utxos = vec![ - collateral_tx_out.clone(), - option_tx_out.clone(), - asset_tx_out.clone(), - fee_tx_out.clone(), - ]; - - let tx = finalize_options_transaction( - tx, - &taproot_pubkey_gen.get_x_only_pubkey(), - &get_options_program(&option_arguments)?, - &utxos, - 0, - &option_branch, - NETWORK, - TrackerLogLevel::None, - )?; - - let x_only_public_key = keypair.x_only_public_key().0; - - let signature_1 = create_p2pk_signature(&tx, &utxos, &keypair, 1, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_1, - 1, - NETWORK, - TrackerLogLevel::None, - )?; - - let signature_2 = create_p2pk_signature(&tx, &utxos, &keypair, 2, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_2, - 2, - NETWORK, - TrackerLogLevel::None, - )?; - - let signature_3 = create_p2pk_signature(&tx, &utxos, &keypair, 3, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_3, - 3, - NETWORK, - TrackerLogLevel::None, - )?; - - if *broadcast { - println!("Broadcasted txid: {}", broadcast_tx(&tx).await?); - } else { - println!("{}", tx.serialize().to_lower_hex_string()); - } - - Ok(()) - } - Self::Settle { - settlement_asset_utxo, - grantor_asset_utxo, - fee_utxo, - option_taproot_pubkey_gen, - grantor_token_amount_to_burn, - fee_amount, - account_index, - broadcast, - } => { - let store = Store::load()?; - let option_arguments: OptionsArguments = - store.get_arguments(option_taproot_pubkey_gen)?; - - let keypair = derive_keypair(*account_index); - - let taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - option_taproot_pubkey_gen, - &option_arguments, - NETWORK, - &contracts::options::get_options_address, - )?; - - let settlement_tx_out = fetch_utxo(*settlement_asset_utxo).await?; - let grantor_tx_out = fetch_utxo(*grantor_asset_utxo).await?; - let fee_tx_out = fetch_utxo(*fee_utxo).await?; - - let (pst, option_branch) = contracts::sdk::build_option_settlement( - (*settlement_asset_utxo, settlement_tx_out.clone()), - (*grantor_asset_utxo, grantor_tx_out.clone()), - (*fee_utxo, fee_tx_out.clone()), - *grantor_token_amount_to_burn, - *fee_amount, - &option_arguments, - )?; - - let tx = pst.extract_tx()?; - let utxos = vec![ - settlement_tx_out.clone(), - grantor_tx_out.clone(), - fee_tx_out.clone(), - ]; - - let tx = finalize_options_transaction( - tx, - &taproot_pubkey_gen.get_x_only_pubkey(), - &get_options_program(&option_arguments)?, - &utxos, - 0, - &option_branch, - NETWORK, - TrackerLogLevel::None, - )?; - - let x_only_public_key = keypair.x_only_public_key().0; - - let signature_1 = create_p2pk_signature(&tx, &utxos, &keypair, 1, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_1, - 1, - NETWORK, - TrackerLogLevel::None, - )?; - - let signature_2 = create_p2pk_signature(&tx, &utxos, &keypair, 2, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_2, - 2, - NETWORK, - TrackerLogLevel::None, - )?; - - if *broadcast { - println!("Broadcasted txid: {}", broadcast_tx(&tx).await?); - } else { - println!("{}", tx.serialize().to_lower_hex_string()); - } - Ok(()) - } - Self::Expire { - collateral_utxo, - grantor_asset_utxo, - fee_utxo, - option_taproot_pubkey_gen, - grantor_token_amount_to_burn, - fee_amount, - account_index, - broadcast, - } => { - let store = Store::load()?; - let option_arguments: OptionsArguments = - store.get_arguments(option_taproot_pubkey_gen)?; - - let keypair = derive_keypair(*account_index); - - let taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - option_taproot_pubkey_gen, - &option_arguments, - NETWORK, - &contracts::options::get_options_address, - )?; - - let collateral_tx_out = fetch_utxo(*collateral_utxo).await?; - let grantor_tx_out = fetch_utxo(*grantor_asset_utxo).await?; - let fee_tx_out = fetch_utxo(*fee_utxo).await?; - - let (pst, option_branch) = contracts::sdk::build_option_expiry( - (*collateral_utxo, collateral_tx_out.clone()), - (*grantor_asset_utxo, grantor_tx_out.clone()), - (*fee_utxo, fee_tx_out.clone()), - *grantor_token_amount_to_burn, - *fee_amount, - &option_arguments, - )?; - - let tx = pst.extract_tx()?; - let utxos = vec![ - collateral_tx_out.clone(), - grantor_tx_out.clone(), - fee_tx_out.clone(), - ]; - - let tx = finalize_options_transaction( - tx, - &taproot_pubkey_gen.get_x_only_pubkey(), - &get_options_program(&option_arguments)?, - &utxos, - 0, - &option_branch, - NETWORK, - TrackerLogLevel::None, - )?; - - let x_only_public_key = keypair.x_only_public_key().0; - - let signature_1 = create_p2pk_signature(&tx, &utxos, &keypair, 1, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_1, - 1, - NETWORK, - TrackerLogLevel::None, - )?; - - let signature_2 = create_p2pk_signature(&tx, &utxos, &keypair, 2, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_2, - 2, - NETWORK, - TrackerLogLevel::None, - )?; - - if *broadcast { - println!("Broadcasted txid: {}", broadcast_tx(&tx).await?); - } else { - println!("{}", tx.serialize().to_lower_hex_string()); - } - Ok(()) - } - Self::Cancel { - collateral_utxo, - option_asset_utxo, - grantor_asset_utxo, - fee_utxo, - option_taproot_pubkey_gen, - amount_to_burn, - fee_amount, - account_index, - broadcast, - } => { - let store = Store::load()?; - let option_arguments: OptionsArguments = - store.get_arguments(option_taproot_pubkey_gen)?; - - let keypair = derive_keypair(*account_index); - - let taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - option_taproot_pubkey_gen, - &option_arguments, - NETWORK, - &contracts::options::get_options_address, - )?; - - let collateral_tx_out = fetch_utxo(*collateral_utxo).await?; - let option_tx_out = fetch_utxo(*option_asset_utxo).await?; - let grantor_tx_out = fetch_utxo(*grantor_asset_utxo).await?; - let fee_tx_out = fetch_utxo(*fee_utxo).await?; - - let (pst, option_branch) = contracts::sdk::build_option_cancellation( - (*collateral_utxo, collateral_tx_out.clone()), - (*option_asset_utxo, option_tx_out.clone()), - (*grantor_asset_utxo, grantor_tx_out.clone()), - (*fee_utxo, fee_tx_out.clone()), - &option_arguments, - *amount_to_burn, - *fee_amount, - )?; - - let tx = pst.extract_tx()?; - let utxos = vec![ - collateral_tx_out.clone(), - option_tx_out.clone(), - grantor_tx_out.clone(), - fee_tx_out.clone(), - ]; - - let tx = finalize_options_transaction( - tx, - &taproot_pubkey_gen.get_x_only_pubkey(), - &get_options_program(&option_arguments)?, - &utxos, - 0, - &option_branch, - NETWORK, - TrackerLogLevel::None, - )?; - - let x_only_public_key = keypair.x_only_public_key().0; - - let signature_1 = create_p2pk_signature(&tx, &utxos, &keypair, 1, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_1, - 1, - NETWORK, - TrackerLogLevel::None, - )?; - - let signature_2 = create_p2pk_signature(&tx, &utxos, &keypair, 2, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_2, - 2, - NETWORK, - TrackerLogLevel::None, - )?; - - let signature_3 = create_p2pk_signature(&tx, &utxos, &keypair, 3, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &x_only_public_key, - &signature_3, - 3, - NETWORK, - TrackerLogLevel::None, - )?; - - if *broadcast { - println!("Broadcasted txid: {}", broadcast_tx(&tx).await?); - } else { - println!("{}", tx.serialize().to_lower_hex_string()); - } - Ok(()) - } - } - } -} diff --git a/crates/cli/src/commands/smt_storage.rs b/crates/cli/src/commands/smt_storage.rs deleted file mode 100644 index 614dc31..0000000 --- a/crates/cli/src/commands/smt_storage.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::commands::NETWORK; -use crate::explorer::{broadcast_tx, fetch_utxo}; -use crate::modules::utils::derive_keypair; -use clap::Subcommand; -use contracts::smt_storage::{ - DEPTH, SMTWitness, SparseMerkleTree, finalize_get_storage_transaction, get_path_bits, - get_smt_storage_compiled_program, smt_storage_taproot_spend_info, -}; -use simplicityhl::elements::pset::serialize::Serialize; -use simplicityhl::simplicity::elements::OutPoint; -use simplicityhl::simplicity::elements::Script; -use simplicityhl::simplicity::elements::taproot::TaprootSpendInfo; -use simplicityhl::simplicity::hex::DisplayHex; -use simplicityhl::tracker::TrackerLogLevel; -use simplicityhl_core::{create_p2pk_signature, finalize_p2pk_transaction, hash_script}; - -fn parse_hex_32(s: &str) -> Result<[u8; 32], String> { - let bytes = hex::decode(s).map_err(|_| "Invalid hex string".to_string())?; - - if bytes.len() != 32 { - return Err(format!( - "Expected 32 bytes (64 hex characters), got {}", - bytes.len() - )); - } - - let mut array = [0u8; 32]; - array.copy_from_slice(&bytes); - Ok(array) -} - -fn parse_bit_path(s: &str) -> Result<[bool; DEPTH], String> { - if s.len() != DEPTH { - return Err(format!("Expected 7 bits, got {}", s.len())); - } - - let mut path = [false; DEPTH]; - - for (ind, char) in s.char_indices() { - if char == 'r' { - path[ind] = true; - } else if char == 'l' { - path[ind] = false; - } else { - return Err(String::from( - "Expected only 'r' and 'l' symbols, got something else.", - )); - } - } - - Ok(path) -} - -/// SMT Storage contract utilities -#[derive(Subcommand, Debug)] -pub enum SMTStorage { - /// Lock collateral on the storage contract (Mint/Fund operation) - GetStorageAddress { - /// The initial 32-byte data payload to store in the tree at the specified path - #[arg(long = "storage-bytes", value_parser = parse_hex_32)] - storage_bytes: [u8; 32], - /// The path in the Merkle Tree use for the contract logic (e.g., "rrll...") - #[arg(long = "path", value_parser = parse_bit_path)] - path: [bool; DEPTH], - - /// Account that will pay for transaction fees and that owns a tokens to send - #[arg(long = "account-index", default_value_t = 0)] - account_index: u32, - }, - /// Build tx transferring an asset UTXO to recipient (LBTC UTXO pays fees) and updating state - TransferFromStorageAddress { - /// Transaction id (hex) and output index (vout) of the ASSET UTXO you will spend - #[arg(long = "storage-utxo")] - storage_utxo: OutPoint, - /// Transaction id (hex) and output index (vout) of the LBTC UTXO used to pay fees (P2PK) - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Miner fee in satoshis (LBTC) - #[arg(long = "fee-sats")] - fee_amount: u64, - - /// The current 32-byte data payload stored in the contract (Pre-state) - #[arg(long = "storage-bytes", value_parser = parse_hex_32)] - storage_bytes: [u8; 32], - /// The new 32-byte data payload to replace the old one (Post-state) - #[arg(long = "changed-bytes", value_parser = parse_hex_32)] - changed_bytes: [u8; 32], - /// The Merkle path used to generate the witness for the state transition - #[arg(long = "path", value_parser = parse_bit_path)] - path: [bool; DEPTH], - - /// Account that will pay for transaction fees and that owns a tokens to send - #[arg(long = "account-index", default_value_t = 0)] - account_index: u32, - /// When set, broadcast the built transaction via Esplora and print txid - #[arg(long = "broadcast")] - broadcast: bool, - }, -} - -impl SMTStorage { - /// Handle basic CLI subcommand execution. - /// - /// # Errors - /// Returns error if the subcommand operation fails. - /// - /// # Panics - /// Panics if asset entropy conversion fails. - pub async fn handle(&self) -> anyhow::Result<()> { - match self { - Self::GetStorageAddress { - storage_bytes, - path, - account_index, - } => { - let keypair = derive_keypair(*account_index); - let public_key = keypair.x_only_public_key().0; - - let address = contracts::sdk::get_storage_address( - &public_key, - storage_bytes, - *path, - NETWORK, - )?; - - let mut script_hash: [u8; 32] = hash_script(&address.script_pubkey()); - script_hash.reverse(); - - println!("X Only Public Key: {public_key}"); - println!("P2PK Address: {address}"); - println!("Script hash: {}", hex::encode(script_hash)); - - Ok(()) - } - Self::TransferFromStorageAddress { - storage_utxo: storage_utxo_outpoint, - fee_utxo: fee_utxo_outpoint, - fee_amount, - storage_bytes, - changed_bytes, - path, - account_index, - broadcast, - } => { - let keypair = derive_keypair(*account_index); - let public_key = keypair.x_only_public_key().0; - - let storage_tx_out = fetch_utxo(*storage_utxo_outpoint).await?; - let fee_tx_out = fetch_utxo(*fee_utxo_outpoint).await?; - - let mut smt = SparseMerkleTree::new(); - let merkle_hashes = smt.update(storage_bytes, *path); - - let merkle_data = - std::array::from_fn(|i| (merkle_hashes[DEPTH - i - 1], path[DEPTH - i - 1])); - - let witness = SMTWitness::new( - &public_key.serialize(), - storage_bytes, - get_path_bits(path, true), - &merkle_data, - ); - smt.update(changed_bytes, *path); - - let program = get_smt_storage_compiled_program(); - let cmr = program.commit().cmr(); - - let old_spend_info: TaprootSpendInfo = - smt_storage_taproot_spend_info(public_key, storage_bytes, &merkle_data, cmr); - - let new_spend_info = - smt_storage_taproot_spend_info(public_key, changed_bytes, &merkle_data, cmr); - let new_script_pubkey = Script::new_v1_p2tr_tweaked(new_spend_info.output_key()); - - let pst = contracts::sdk::transfer_asset_with_storage( - (*storage_utxo_outpoint, storage_tx_out.clone()), - (*fee_utxo_outpoint, fee_tx_out.clone()), - *fee_amount, - &new_script_pubkey, - )?; - - let tx = pst.extract_tx()?; - let utxos = vec![storage_tx_out, fee_tx_out]; - - let tx = finalize_get_storage_transaction( - tx, - &old_spend_info, - &witness, - &program, - &utxos, - 0, - NETWORK, - TrackerLogLevel::None, - )?; - - let signature = create_p2pk_signature(&tx, &utxos, &keypair, 1, NETWORK)?; - let tx = finalize_p2pk_transaction( - tx, - &utxos, - &public_key, - &signature, - 1, - NETWORK, - TrackerLogLevel::None, - )?; - - if *broadcast { - println!("Broadcasted txid: {}", broadcast_tx(&tx).await?); - } else { - println!("{}", tx.serialize().to_lower_hex_string()); - } - - Ok(()) - } - } - } -} diff --git a/crates/cli/src/explorer.rs b/crates/cli/src/explorer.rs deleted file mode 100644 index 5f60193..0000000 --- a/crates/cli/src/explorer.rs +++ /dev/null @@ -1,224 +0,0 @@ -//! Esplora API client for interacting with Liquid testnet. - -use std::path::PathBuf; -use std::time::Duration; - -use tokio::fs; - -use simplicityhl::simplicity::elements::{OutPoint, Transaction, TxOut, encode}; - -/// Default Esplora API base URL for Liquid testnet. -pub const DEFAULT_BASE_URL: &str = "https://blockstream.info/liquidtestnet/api"; - -/// Default request timeout in seconds. -pub const DEFAULT_TIMEOUT_SECS: u64 = 10; - -/// Client for interacting with the Esplora API. -#[derive(Debug, Clone)] -pub struct EsploraClient { - base_url: String, - timeout: Duration, -} - -impl Default for EsploraClient { - fn default() -> Self { - Self::new() - } -} - -impl EsploraClient { - /// Creates a new client with default configuration. - #[must_use] - pub fn new() -> Self { - Self::with_base_url(DEFAULT_BASE_URL) - } - - /// Creates a new client with a custom base URL. - #[must_use] - pub fn with_base_url(base_url: &str) -> Self { - Self { - base_url: base_url.trim_end_matches('/').to_owned(), - timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS), - } - } - - /// Broadcasts a transaction to the network. - /// - /// # Returns - /// - /// The transaction ID (txid) as a hex string on success. - /// - /// # Errors - /// - /// Returns an error if: - /// - The HTTP request fails - /// - The server rejects the transaction - pub async fn broadcast_transaction(&self, tx: &Transaction) -> Result { - let tx_hex = encode::serialize_hex(tx); - let url = format!("{}/tx", self.base_url); - let timeout_secs = self.timeout.as_secs(); - - let response = tokio::task::spawn_blocking(move || { - minreq::post(&url) - .with_timeout(timeout_secs) - .with_body(tx_hex) - .send() - }) - .await - .map_err(|e| ExplorerError::TaskJoin(e.to_string()))??; - - let status = response.status_code; - let body = response.as_str().unwrap_or("").trim().to_owned(); - - if !(200..300).contains(&status) { - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - return Err(ExplorerError::BroadcastRejected { - status: status as u16, // HTTP status codes are always positive and fit in u16 - url: format!("{}/tx", self.base_url), - message: body, - }); - } - - Ok(body) - } - - /// Fetches a UTXO from the network with local file caching. - /// - /// The transaction hex is cached locally to avoid redundant network requests. - /// Cache is stored in `.cache/explorer/tx/{txid}.hex` relative to the current directory. - /// - /// # Errors - /// - /// Returns an error if: - /// - The network request fails - /// - The transaction cannot be decoded - /// - The output index is out of bounds - pub async fn fetch_utxo(&self, outpoint: OutPoint) -> Result { - let tx_hex = self.fetch_transaction_hex(outpoint.txid).await?; - - extract_output(&tx_hex, outpoint.vout as usize) - } - - /// Fetches raw transaction hex, using cache when available. - async fn fetch_transaction_hex( - &self, - tx_id: simplicityhl::simplicity::elements::Txid, - ) -> Result { - let cache_path = transaction_cache_path(&tx_id.to_string())?; - - if cache_path.exists() { - return Ok(fs::read_to_string(&cache_path).await?); - } - - let url = format!("{}/tx/{tx_id}/hex", self.base_url); - let timeout_secs = self.timeout.as_secs(); - - let response = tokio::task::spawn_blocking(move || { - minreq::get(&url).with_timeout(timeout_secs).send() - }) - .await - .map_err(|e| ExplorerError::TaskJoin(e.to_string()))??; - - if !(200..300).contains(&response.status_code) { - return Err(ExplorerError::HttpRequest(format!( - "Request failed with status {}", - response.status_code - ))); - } - - let tx_hex = response.as_str().unwrap_or("").to_owned(); - - // (best-effort, ignore errors) - let _ = fs::write(&cache_path, &tx_hex).await; - - Ok(tx_hex) - } -} - -/// Extracts a specific output from a serialized transaction. -fn extract_output(tx_hex: &str, index: usize) -> Result { - let tx_bytes = hex::decode(tx_hex.trim())?; - - let tx: Transaction = encode::deserialize(&tx_bytes)?; - - tx.output - .get(index) - .cloned() - .ok_or_else(|| ExplorerError::OutputIndexOutOfBounds { - index, - txid: tx.txid().to_string(), - }) -} - -/// Returns the cache file path for a transaction. -fn transaction_cache_path(txid: &str) -> Result { - let mut path = std::env::current_dir()?; - - path.extend([".cache", "explorer", "tx"]); - - std::fs::create_dir_all(&path)?; - - path.push(format!("{txid}.hex")); - - Ok(path) -} - -/// Broadcasts a transaction using the default Esplora client. -/// -/// # Errors -/// -/// See [`EsploraClient::broadcast_transaction`]. -pub async fn broadcast_tx(tx: &Transaction) -> Result { - EsploraClient::new().broadcast_transaction(tx).await -} - -/// Fetches a UTXO using the default Esplora client. -/// -/// # Errors -/// -/// See [`EsploraClient::fetch_utxo`]. -pub async fn fetch_utxo(outpoint: OutPoint) -> Result { - EsploraClient::new().fetch_utxo(outpoint).await -} - -/// Errors that occur when interacting with the Esplora API or local cache. -/// -/// These errors are returned by [`EsploraClient`](crate::EsploraClient) methods -/// for broadcasting transactions and fetching UTXOs. -#[derive(Debug, thiserror::Error)] -pub enum ExplorerError { - /// Returned when an HTTP request to the Esplora API fails. - #[error("HTTP request failed: {0}")] - HttpRequest(String), - - /// Returned when minreq encounters an error. - #[error("HTTP error: {0}")] - Minreq(#[from] minreq::Error), - - /// Returned when a tokio task join fails. - #[error("Task join error: {0}")] - TaskJoin(String), - - #[error("Broadcast failed with HTTP {status} for {url}: {message}")] - BroadcastRejected { - status: u16, - url: String, - message: String, - }, - - /// Returned when a filesystem operation fails (cache read/write, directory creation). - #[error("IO operation failed: {0}")] - Io(#[from] std::io::Error), - - /// Returned when transaction data is not valid hexadecimal. - #[error("Invalid transaction hex: {0}")] - InvalidTransactionHex(#[from] hex::FromHexError), - - /// Returned when raw transaction bytes cannot be parsed. - #[error("Failed to deserialize transaction: {0}")] - TransactionDeserialize(#[from] simplicityhl::simplicity::elements::encode::Error), - - /// Returned when the requested output index does not exist in the transaction. - #[error("Output index {index} out of bounds for transaction {txid}")] - OutputIndexOutOfBounds { index: usize, txid: String }, -} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index fe03986..33e473b 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,5 +1,4 @@ #![warn(clippy::all, clippy::pedantic)] pub mod commands; -pub mod explorer; pub mod modules; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 2df1fe2..c00fd0f 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,21 +1,16 @@ #![warn(clippy::all, clippy::pedantic)] -//! Simplicity helper CLI for Liquid testnet. -//! -//! This binary exposes multiple subcommand groups to work with Liquid testnet: -//! - `basic`: P2PK utilities such as deriving addresses and building simple transfers. -//! - `options`: Utilities for the options contract. - -mod commands; -mod explorer; -mod modules; +use cli::commands::basic::Basic; +use cli::commands::option_offer::OptionOffer; +use cli::modules::utils::{esplora_url_from_network, wallet_data_root}; use anyhow::Result; + use clap::{Parser, Subcommand}; -use crate::commands::basic::Basic; -use crate::commands::options::Options; -use crate::commands::smt_storage::SMTStorage; +use wallet_abi::runtime::WalletRuntimeConfig; + +use tracing_subscriber::{EnvFilter, fmt, prelude::*}; /// Command-line entrypoint for the Simplicity helper CLI. #[derive(Parser, Debug)] @@ -27,33 +22,71 @@ use crate::commands::smt_storage::SMTStorage; struct Cli { #[command(subcommand)] command: Commands, + /// Network on which to send a transaction + #[arg(long = "network")] + network: lwk_common::Network, + #[arg(short, long, env = "MNEMONIC")] + mnemonic: Option, } /// Top-level subcommand groups. #[derive(Subcommand, Debug)] enum Commands { - /// P2PK and simple transaction utilities + /// Simple transaction utilities Basic { #[command(subcommand)] basic: Box, }, - /// Options contract utilities - Options { + /// Option-offer contract utilities + OptionOffer { #[command(subcommand)] - options: Box, - }, - /// Storage utilities - Storage { - #[command(subcommand)] - storage: Box, + option_offer: Box, }, + // /// Options contract utilities + // Options { + // #[command(subcommand)] + // options: Box, + // }, + // /// Storage utilities + // Storage { + // #[command(subcommand)] + // storage: Box, + // }, } +const TEST_MNEMONIC: &str = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + #[tokio::main] async fn main() -> Result<()> { - match Cli::parse().command { - Commands::Basic { basic } => basic.handle().await, - Commands::Options { options } => options.handle().await, - Commands::Storage { storage } => storage.handle().await, + let _ = dotenvy::dotenv(); + + logging_init(); + + let parsed = Cli::parse(); + + let mnemonic = parsed.mnemonic.unwrap_or_else(|| TEST_MNEMONIC.to_string()); + + let runtime = WalletRuntimeConfig::from_mnemonic( + &mnemonic, + parsed.network, + &esplora_url_from_network(parsed.network), + wallet_data_root(), + )?; + + match parsed.command { + Commands::Basic { basic } => basic.handle(runtime).await, + Commands::OptionOffer { option_offer } => Box::pin(option_offer.handle(runtime)).await, + // TODO: Commands::Options { options } => options.handle().await, + // TODO: Commands::Storage { storage } => storage.handle().await, } } + +fn logging_init() { + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + + tracing_subscriber::registry() + .with(fmt::layer().with_target(true)) + .with(filter) + .init(); +} diff --git a/crates/cli/src/modules/keys.rs b/crates/cli/src/modules/keys.rs deleted file mode 100644 index 78e449a..0000000 --- a/crates/cli/src/modules/keys.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::modules::settings::Settings; -use simplicityhl::elements::secp256k1_zkp as secp256k1; - -/// Derive a secret key from index using seed from environment. -/// -/// # Panics -/// Panics if `SEED_HEX` is not configured or invalid. -#[must_use] -pub fn derive_secret_key_from_index(index: u32) -> secp256k1::SecretKey { - let settings = Settings::load().expect("SEED_HEX should be configured"); - let seed_vec = hex::decode(settings.seed_hex).expect("SEED_HEX must be hex"); - assert_eq!(seed_vec.len(), 32, "SEED_HEX must be 32 bytes hex"); - - let mut seed_bytes = [0u8; 32]; - seed_bytes.copy_from_slice(&seed_vec); - - for (i, b) in index.to_be_bytes().iter().enumerate() { - seed_bytes[24 + i] ^= *b; - } - secp256k1::SecretKey::from_slice(&seed_bytes).unwrap() -} diff --git a/crates/cli/src/modules/mod.rs b/crates/cli/src/modules/mod.rs index e568d6d..ca7140d 100644 --- a/crates/cli/src/modules/mod.rs +++ b/crates/cli/src/modules/mod.rs @@ -1,4 +1,2 @@ -pub mod keys; -pub mod settings; pub mod store; pub mod utils; diff --git a/crates/cli/src/modules/settings.rs b/crates/cli/src/modules/settings.rs deleted file mode 100644 index c709024..0000000 --- a/crates/cli/src/modules/settings.rs +++ /dev/null @@ -1,32 +0,0 @@ -use anyhow::{Result, anyhow}; -use config::{Case, Config}; - -#[derive(Clone, Debug)] -pub struct Settings { - pub seed_hex: String, -} - -impl Settings { - /// Load settings from environment. - /// - /// # Errors - /// Returns error if .env loading fails or `SEED_HEX` is not set. - pub fn load() -> Result { - let _ = dotenvy::dotenv(); - let _ = dotenvy::from_path("crates/cli"); - - let cfg = Config::builder() - .add_source( - config::Environment::default() - .separator("__") - .convert_case(Case::ScreamingSnake), - ) - .build()?; - - let seed_hex = cfg - .get_string("SEED_HEX") - .map_err(|_| anyhow!("SEED_HEX not set in environment or .env"))?; - - Ok(Self { seed_hex }) - } -} diff --git a/crates/cli/src/modules/store.rs b/crates/cli/src/modules/store.rs index 6ae9282..5ed74cc 100644 --- a/crates/cli/src/modules/store.rs +++ b/crates/cli/src/modules/store.rs @@ -1,6 +1,7 @@ -use contracts::sdk::taproot_pubkey_gen::TaprootPubkeyGen; +#![allow(dead_code)] -use simplicityhl_core::{Encodable, ProgramError, SimplicityNetwork}; +use wallet_abi::taproot_pubkey_gen::TaprootPubkeyGen; +use wallet_abi::{Encodable, ProgramError}; use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; use simplicityhl::simplicity::elements::Address; @@ -29,11 +30,11 @@ impl Store { &self, taproot_pubkey_gen: &str, encoded_data: &str, - network: SimplicityNetwork, - get_address: &impl Fn(&XOnlyPublicKey, &A, SimplicityNetwork) -> Result, + network: lwk_common::Network, + get_address: &impl Fn(&XOnlyPublicKey, &A, lwk_common::Network) -> Result, ) -> anyhow::Result<()> where - A: Encodable + simplicityhl_core::encoding::Decode<()>, + A: Encodable + wallet_abi::encoding::Decode<()>, { let decoded_data = hex::decode(encoded_data)?; @@ -64,7 +65,7 @@ impl Store { /// Returns error if arguments are not found or decoding fails. pub fn get_arguments(&self, arg_name: &str) -> anyhow::Result where - A: Encodable + simplicityhl_core::encoding::Decode<()>, + A: Encodable + wallet_abi::encoding::Decode<()>, { if let Some(value) = self.store.get(arg_name)? { return Encodable::decode(&value).map_err(anyhow::Error::msg); @@ -73,120 +74,3 @@ impl Store { anyhow::bail!("Arguments not found"); } } - -#[cfg(test)] -mod tests { - use contracts::options::OptionsArguments; - use contracts::options::get_options_address; - use contracts::sdk::taproot_pubkey_gen::get_random_seed; - use simplicityhl::elements::AssetId; - use simplicityhl::elements::OutPoint; - use simplicityhl::elements::Txid; - use simplicityhl::elements::hashes::Hash; - use simplicityhl_core::{ - Encodable, LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_TEST_ASSET_ID_STR, - SimplicityNetwork, - }; - - use super::*; - - const NETWORK: SimplicityNetwork = SimplicityNetwork::LiquidTestnet; - - fn load_mock() -> Store { - Store { - store: sled::Config::new() - .temporary(true) - .open() - .expect("expected store"), - } - } - - fn get_mocked_data() -> anyhow::Result<(OptionsArguments, TaprootPubkeyGen)> { - let settlement_asset_id = - AssetId::from_slice(&hex::decode(LIQUID_TESTNET_TEST_ASSET_ID_STR)?)?; - - let args = OptionsArguments::new( - 10, - 50, - 100, - 1000, - *LIQUID_TESTNET_BITCOIN_ASSET, - settlement_asset_id, - get_random_seed(), - (OutPoint::new(Txid::from_slice(&[1; 32])?, 0), false), - (OutPoint::new(Txid::from_slice(&[2; 32])?, 0), false), - ); - - let options_taproot_pubkey_gen = - TaprootPubkeyGen::from(&args, NETWORK, &get_options_address)?; - - Ok((args, options_taproot_pubkey_gen)) - } - - #[test] - fn test_sled_serialize_deserialize() -> anyhow::Result<()> { - let store = load_mock(); - - let (args, options_taproot_pubkey_gen) = get_mocked_data()?; - - store.import_arguments( - &options_taproot_pubkey_gen.to_string(), - &args.to_hex()?, - NETWORK, - &get_options_address, - )?; - - let retrieved = - store.get_arguments::(&options_taproot_pubkey_gen.to_string())?; - - assert_eq!(args, retrieved); - - Ok(()) - } - - #[test] - fn test_sled_import_export_roundtrip() -> anyhow::Result<()> { - let store = load_mock(); - - let (args, options_taproot_pubkey_gen) = get_mocked_data()?; - - store.import_arguments( - &options_taproot_pubkey_gen.to_string(), - &args.to_hex()?, - NETWORK, - &get_options_address, - )?; - - let exported_hex = store.export_arguments(&options_taproot_pubkey_gen.to_string())?; - - assert_eq!(exported_hex, args.to_hex()?); - - Ok(()) - } - - #[test] - fn test_sled_export_get_consistency() -> anyhow::Result<()> { - let store = load_mock(); - - let (args, options_taproot_pubkey_gen) = get_mocked_data()?; - - store.import_arguments( - &options_taproot_pubkey_gen.to_string(), - &args.to_hex()?, - NETWORK, - &get_options_address, - )?; - - let exported_hex = store.export_arguments(&options_taproot_pubkey_gen.to_string())?; - let exported_bytes = hex::decode(&exported_hex)?; - let decoded_from_export: OptionsArguments = Encodable::decode(&exported_bytes)?; - - let retrieved = - store.get_arguments::(&options_taproot_pubkey_gen.to_string())?; - - assert_eq!(decoded_from_export, retrieved); - assert_eq!(retrieved, args); - - Ok(()) - } -} diff --git a/crates/cli/src/modules/utils.rs b/crates/cli/src/modules/utils.rs index d18168a..c47f8f1 100644 --- a/crates/cli/src/modules/utils.rs +++ b/crates/cli/src/modules/utils.rs @@ -1,8 +1,42 @@ -use crate::modules::keys::derive_secret_key_from_index; -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::schnorr::Keypair; +#![allow(clippy::missing_errors_doc)] + +use anyhow::anyhow; + +use wallet_abi::runtime::WalletRuntimeConfig; +use wallet_abi::schema::tx_create::{TransactionInfo, TxCreateRequest}; + +pub async fn execute_request( + runtime: &mut WalletRuntimeConfig, + request: TxCreateRequest, +) -> anyhow::Result { + let response = runtime.process_request(&request).await?; + let tx_info = response + .transaction + .ok_or_else(|| anyhow!("Expected transaction info in runtime response"))?; + + if request.broadcast { + println!("Broadcasted txid: {}", tx_info.txid); + } else { + println!("{}", tx_info.tx_hex); + } + + Ok(tx_info) +} + +pub fn wallet_data_root() -> std::path::PathBuf { + std::env::var_os("SIMPLICITY_CLI_WALLET_DATA_DIR").map_or_else( + || std::path::PathBuf::from(".cache/wallet"), + std::path::PathBuf::from, + ) +} #[must_use] -pub fn derive_keypair(index: u32) -> Keypair { - Keypair::from_secret_key(secp256k1::SECP256K1, &derive_secret_key_from_index(index)) +pub fn esplora_url_from_network(network: lwk_common::Network) -> String { + match network { + lwk_common::Network::Liquid => "https://blockstream.info/liquid/api".to_string(), + lwk_common::Network::TestnetLiquid => { + "https://blockstream.info/liquidtestnet/api".to_string() + } + lwk_common::Network::LocaltestLiquid => "http://127.0.0.1:3001".to_string(), + } } diff --git a/crates/contracts/Cargo.toml b/crates/contracts/Cargo.toml index cabbc19..d559a9c 100644 --- a/crates/contracts/Cargo.toml +++ b/crates/contracts/Cargo.toml @@ -10,10 +10,8 @@ keywords = ["simplicity", "liquid", "bitcoin", "elements", "contracts"] categories = ["cryptography::cryptocurrencies"] [features] -default = ["sdk-basic", "finance-options", "finance-dcd", "finance-option-offer", "simple-storage", "bytes32-tr-storage", "array-tr-storage", "smt-storage"] -sdk-basic = [] +default = ["finance-options", "finance-option-offer", "simple-storage", "bytes32-tr-storage", "array-tr-storage", "smt-storage"] finance-options = [] -finance-dcd = [] finance-option-offer = [] simple-storage = [] bytes32-tr-storage = [] @@ -28,14 +26,16 @@ bincode = "2.0.1" thiserror = "2" -ring = { workspace = true } -sha2 = { workspace = true } - hex = { workspace = true } simplicityhl = { workspace = true } - -simplicityhl-core = { workspace = true } +wallet-abi = { path = "../wallet-abi" } +serde_json = "1" +serde = { version = "1", features = ["derive"] } [dev-dependencies] -anyhow = "1" \ No newline at end of file +anyhow = "1" +bitcoincore-rpc = "0.19.0" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "sync"] } +lwk_common = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_common" } +lwk_test_util = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_test_util" } diff --git a/crates/contracts/README.md b/crates/contracts/README.md index fb4f545..5f263c9 100644 --- a/crates/contracts/README.md +++ b/crates/contracts/README.md @@ -1,12 +1,21 @@ -# Simplicity HL Core -- Contracts +# 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). -Here you will find Simplicity HL implementations of [Options](https://blockstream.com/assets/downloads/pdf/options-whitepaper.pdf) (see [options](src/options)), Dual Currency Deposit Contract (see [dcd](src/dual_currency_deposit)), and more. +Current contract modules in this crate: -The module that stands out is [`sdk`](src/sdk). This module does not contain contract source code; instead, it provides builder functions to help build Elements transactions to issue assets or transfer native currency (see [basic](src/sdk/basic)), as well as builder functions for the Options contract. +- Finance: + - [Options](src/finance/options) + - [Option Offer](src/finance/option_offer) +- State management: + - [Simple Storage](src/state_management/simple_storage) + - [Bytes32 Taproot Storage](src/state_management/bytes32_tr_storage) + - [Array Taproot Storage](src/state_management/array_tr_storage) + - [Sparse Merkle Tree Storage](src/state_management/smt_storage) -This SDK can be used as a reference for how to build libraries targeting a particular Simplicity contract, integrateable with mobile/desktop, etc. +Wallet-facing transaction construction is schema-first and lives in the wallet ABI crate. +Contract-side modules in this crate focus on program compilation, argument/witness helpers, execution, and +transaction finalization primitives. ## License diff --git a/crates/contracts/src/arguments_helpers.rs b/crates/contracts/src/arguments_helpers.rs deleted file mode 100644 index f074d8d..0000000 --- a/crates/contracts/src/arguments_helpers.rs +++ /dev/null @@ -1,93 +0,0 @@ -use simplicityhl::Arguments; -use simplicityhl::str::WitnessName; -use simplicityhl::value::{UIntValue, ValueInner}; - -use crate::error::FromArgumentsError; - -/// Extract a U256 value as `[u8; 32]` from Arguments by witness name. -/// -/// # Errors -/// -/// Returns error if the witness is missing or has wrong type. -pub fn extract_u256_bytes(args: &Arguments, name: &str) -> Result<[u8; 32], FromArgumentsError> { - let witness_name = WitnessName::from_str_unchecked(name); - let value = args - .get(&witness_name) - .ok_or_else(|| FromArgumentsError::MissingWitness { - name: name.to_string(), - })?; - - match value.inner() { - ValueInner::UInt(UIntValue::U256(u256)) => Ok(u256.to_byte_array()), - _ => Err(FromArgumentsError::WrongValueType { - name: name.to_string(), - expected: "U256".to_string(), - }), - } -} - -/// Extract a U64 value from Arguments by witness name. -/// -/// # Errors -/// -/// Returns error if the witness is missing or has wrong type. -pub fn extract_u64(args: &Arguments, name: &str) -> Result { - let witness_name = WitnessName::from_str_unchecked(name); - let value = args - .get(&witness_name) - .ok_or_else(|| FromArgumentsError::MissingWitness { - name: name.to_string(), - })?; - - match value.inner() { - ValueInner::UInt(UIntValue::U64(v)) => Ok(*v), - _ => Err(FromArgumentsError::WrongValueType { - name: name.to_string(), - expected: "U64".to_string(), - }), - } -} - -/// Extract a U32 value from Arguments by witness name. -/// -/// # Errors -/// -/// Returns error if the witness is missing or has wrong type. -pub fn extract_u32(args: &Arguments, name: &str) -> Result { - let witness_name = WitnessName::from_str_unchecked(name); - let value = args - .get(&witness_name) - .ok_or_else(|| FromArgumentsError::MissingWitness { - name: name.to_string(), - })?; - - match value.inner() { - ValueInner::UInt(UIntValue::U32(v)) => Ok(*v), - _ => Err(FromArgumentsError::WrongValueType { - name: name.to_string(), - expected: "U32".to_string(), - }), - } -} - -/// Extract a boolean value from Arguments by witness name. -/// -/// # Errors -/// -/// Returns error if the witness is missing or has wrong type. -pub fn extract_bool(args: &Arguments, name: &str) -> Result { - let witness_name = WitnessName::from_str_unchecked(name); - let value = args - .get(&witness_name) - .ok_or_else(|| FromArgumentsError::MissingWitness { - name: name.to_string(), - })?; - - match value.inner() { - ValueInner::Boolean(b) => Ok(*b), - _ => Err(FromArgumentsError::WrongValueType { - name: name.to_string(), - expected: "bool".to_string(), - }), - } -} diff --git a/crates/contracts/src/error.rs b/crates/contracts/src/error.rs index c6dea73..e7c75ac 100644 --- a/crates/contracts/src/error.rs +++ b/crates/contracts/src/error.rs @@ -22,36 +22,6 @@ pub enum ValidationError { }, } -/// Errors from taproot pubkey generation and verification. -#[derive(Debug, thiserror::Error)] -pub enum TaprootPubkeyGenError { - #[error("Invalid pubkey recovered: expected {expected}, got {actual}")] - InvalidPubkey { expected: String, actual: String }, - - #[error("Invalid address recovered: expected {expected}, got {actual}")] - InvalidAddress { expected: String, actual: String }, - - #[error( - "Invalid taproot pubkey gen string: expected 3 parts separated by ':', got {parts_count}" - )] - InvalidFormat { parts_count: usize }, - - #[error("Failed to decode seed hex: {0}")] - SeedHexDecode(#[from] hex::FromHexError), - - #[error("Failed to parse public key: {0}")] - PublicKeyParse(#[from] simplicityhl::simplicity::bitcoin::key::ParsePublicKeyError), - - #[error("Failed to parse address: {0}")] - AddressParse(#[from] simplicityhl::elements::address::AddressError), - - #[error("Failed to create X-only public key from bytes: {0}")] - XOnlyPublicKey(#[from] simplicityhl::simplicity::bitcoin::secp256k1::Error), - - #[error("Failed to generate address: {0}")] - AddressGeneration(#[from] simplicityhl_core::ProgramError), -} - /// Errors from transaction building operations. #[derive(Debug, thiserror::Error)] pub enum TransactionBuildError { @@ -120,12 +90,6 @@ pub enum TransactionBuildError { #[error(transparent)] Validation(#[from] ValidationError), - - #[error(transparent)] - TaprootPubkeyGen(#[from] TaprootPubkeyGenError), - - #[error(transparent)] - IssuanceValidation(#[from] crate::sdk::IssuanceVerificationError), } /// Errors from extracting arguments from Arguments struct. @@ -140,24 +104,3 @@ pub enum FromArgumentsError { #[error("Invalid asset ID bytes for {name}")] InvalidAssetId { name: String }, } - -/// Errors from DCD ratio calculations. -#[cfg(feature = "finance-dcd")] -#[derive(Debug, thiserror::Error)] -pub enum DCDRatioError { - #[error("Arithmetic overflow: {operation}")] - Overflow { operation: String }, - - #[error("Value exceeds u64: {value_name}")] - U64Overflow { value_name: String }, - - #[error("{dividend} must be divisible by {divisor}, remainder {remainder}")] - NotDivisible { - dividend: String, - divisor: String, - remainder: u64, - }, - - #[error("{value_name} must be non-zero")] - ZeroValue { value_name: String }, -} diff --git a/crates/contracts/src/finance/dcd/build_arguments.rs b/crates/contracts/src/finance/dcd/build_arguments.rs deleted file mode 100644 index 60f388e..0000000 --- a/crates/contracts/src/finance/dcd/build_arguments.rs +++ /dev/null @@ -1,566 +0,0 @@ -use std::collections::HashMap; -use std::str::FromStr; - -use hex::FromHex; - -use simplicityhl::num::U256; -use simplicityhl::{ - Arguments, simplicity::bitcoin::XOnlyPublicKey, str::WitnessName, value::UIntValue, -}; - -use crate::error::DCDRatioError; - -#[derive(Debug, Clone, bincode::Encode, bincode::Decode, PartialEq, Eq)] -pub struct DCDArguments { - // Time parameters - taker_funding_start_time: u32, - taker_funding_end_time: u32, - contract_expiry_time: u32, - early_termination_end_time: u32, - settlement_height: u32, - - // Pricing parameters - strike_price: u64, - incentive_basis_points: u64, - - // Asset IDs (hex LE strings) - collateral_asset_id_hex_le: String, - settlement_asset_id_hex_le: String, - filler_token_asset_id_hex_le: String, - grantor_collateral_token_asset_id_hex_le: String, - grantor_settlement_token_asset_id_hex_le: String, - - // Fee parameters - fee_basis_points: u64, - fee_script_hash_hex_le: String, - - // Ratio/denominator parameters - ratio_args: DCDRatioArguments, - - // Oracle - oracle_public_key: String, -} - -#[derive(Debug, Clone, bincode::Encode, bincode::Decode, PartialEq, Eq, Default)] -pub struct DCDRatioArguments { - principal_collateral_amount: u64, - interest_collateral_amount: u64, - total_collateral_amount: u64, - principal_asset_amount: u64, - interest_asset_amount: u64, - total_asset_amount: u64, - filler_token_amount: u64, - grantor_collateral_token_amount: u64, - grantor_settlement_token_amount: u64, - filler_per_settlement_collateral: u64, - filler_per_settlement_asset: u64, - filler_per_principal_collateral: u64, - grantor_settlement_per_deposited_asset: u64, - grantor_collateral_per_deposited_collateral: u64, - grantor_per_settlement_collateral: u64, - grantor_per_settlement_asset: u64, -} - -impl DCDRatioArguments { - /// Returns the principal collateral amount. - #[must_use] - pub const fn principal_collateral_amount(&self) -> u64 { - self.principal_collateral_amount - } - - /// Returns the interest collateral amount. - #[must_use] - pub const fn interest_collateral_amount(&self) -> u64 { - self.interest_collateral_amount - } - - /// Returns the total collateral amount. - #[must_use] - pub const fn total_collateral_amount(&self) -> u64 { - self.total_collateral_amount - } - - /// Returns the principal asset amount. - #[must_use] - pub const fn principal_asset_amount(&self) -> u64 { - self.principal_asset_amount - } - - /// Returns the interest asset amount. - #[must_use] - pub const fn interest_asset_amount(&self) -> u64 { - self.interest_asset_amount - } - - /// Returns the total asset amount. - #[must_use] - pub const fn total_asset_amount(&self) -> u64 { - self.total_asset_amount - } - - /// Returns the filler token amount. - #[must_use] - pub const fn filler_token_amount(&self) -> u64 { - self.filler_token_amount - } - - /// Returns the grantor collateral token amount. - #[must_use] - pub const fn grantor_collateral_token_amount(&self) -> u64 { - self.grantor_collateral_token_amount - } - - /// Returns the grantor settlement token amount. - #[must_use] - pub const fn grantor_settlement_token_amount(&self) -> u64 { - self.grantor_settlement_token_amount - } - - /// Returns the filler per settlement collateral ratio. - #[must_use] - pub const fn filler_per_settlement_collateral(&self) -> u64 { - self.filler_per_settlement_collateral - } - - /// Returns the filler per settlement asset ratio. - #[must_use] - pub const fn filler_per_settlement_asset(&self) -> u64 { - self.filler_per_settlement_asset - } - - /// Returns the filler per principal collateral ratio. - #[must_use] - pub const fn filler_per_principal_collateral(&self) -> u64 { - self.filler_per_principal_collateral - } - - /// Returns the grantor settlement per deposited asset ratio. - #[must_use] - pub const fn grantor_settlement_per_deposited_asset(&self) -> u64 { - self.grantor_settlement_per_deposited_asset - } - - /// Returns the grantor collateral per deposited collateral ratio. - #[must_use] - pub const fn grantor_collateral_per_deposited_collateral(&self) -> u64 { - self.grantor_collateral_per_deposited_collateral - } - - /// Returns the grantor per settlement collateral ratio. - #[must_use] - pub const fn grantor_per_settlement_collateral(&self) -> u64 { - self.grantor_per_settlement_collateral - } - - /// Returns the grantor per settlement asset ratio. - #[must_use] - pub const fn grantor_per_settlement_asset(&self) -> u64 { - self.grantor_per_settlement_asset - } -} - -pub const MAX_BASIS_POINTS: u64 = 10000; - -impl DCDRatioArguments { - /// Build ratio arguments from contract parameters. - /// - /// # Errors - /// Returns error if arithmetic overflow occurs or divisibility requirements aren't met. - #[expect(clippy::too_many_lines)] - pub fn build_from( - principal_collateral_amount: u64, - incentive_basis_points: u64, - strike_price: u64, - filler_per_principal_collateral: u64, - ) -> Result { - // interest_collateral_amount = (principal_collateral_amount * incentive_basis_points) / MAX_BASIS_POINTS - let interest_collateral_amount: u128 = u128::from(principal_collateral_amount) - .checked_mul(u128::from(incentive_basis_points)) - .ok_or_else(|| DCDRatioError::Overflow { - operation: "principal_collateral_amount * incentive_basis_points".to_string(), - })?; - - let remainder = interest_collateral_amount % u128::from(MAX_BASIS_POINTS); - if remainder != 0 { - return Err(DCDRatioError::NotDivisible { - dividend: "principal_collateral_amount * incentive_basis_points".to_string(), - divisor: "MAX_BASIS_POINTS".to_string(), - #[allow(clippy::cast_possible_truncation)] - remainder: remainder as u64, - }); - } - let interest_collateral_amount: u64 = (interest_collateral_amount - / u128::from(MAX_BASIS_POINTS)) - .try_into() - .map_err(|_| DCDRatioError::U64Overflow { - value_name: "interest_collateral_amount".to_string(), - })?; - - let total_collateral_amount = principal_collateral_amount - .checked_add(interest_collateral_amount) - .ok_or_else(|| DCDRatioError::Overflow { - operation: "principal_collateral_amount + interest_collateral_amount".to_string(), - })?; - - // principal_asset_amount = principal_collateral_amount * strike_price - let principal_asset_amount = principal_collateral_amount - .checked_mul(strike_price) - .ok_or_else(|| DCDRatioError::Overflow { - operation: "principal_collateral_amount * strike_price".to_string(), - })?; - - // interest_asset_amount = (principal_asset_amount * incentive_basis_points) / MAX_BASIS_POINTS - let interest_asset_amount: u128 = u128::from(principal_asset_amount) - .checked_mul(u128::from(incentive_basis_points)) - .ok_or_else(|| DCDRatioError::Overflow { - operation: "principal_asset_amount * incentive_basis_points".to_string(), - })?; - - let remainder = interest_asset_amount % u128::from(MAX_BASIS_POINTS); - if remainder != 0 { - return Err(DCDRatioError::NotDivisible { - dividend: "principal_asset_amount * incentive_basis_points".to_string(), - divisor: "MAX_BASIS_POINTS".to_string(), - #[allow(clippy::cast_possible_truncation)] - remainder: remainder as u64, - }); - } - let interest_asset_amount: u64 = (interest_asset_amount / u128::from(MAX_BASIS_POINTS)) - .try_into() - .map_err(|_| DCDRatioError::U64Overflow { - value_name: "interest_asset_amount".to_string(), - })?; - - let total_asset_amount = principal_asset_amount - .checked_add(interest_asset_amount) - .ok_or_else(|| DCDRatioError::Overflow { - operation: "principal_asset_amount + interest_asset_amount".to_string(), - })?; - - // filler_token_amount = principal_collateral_amount / filler_per_principal_collateral - if filler_per_principal_collateral == 0 { - return Err(DCDRatioError::ZeroValue { - value_name: "filler_per_principal_collateral".to_string(), - }); - } - let remainder = principal_collateral_amount % filler_per_principal_collateral; - if remainder != 0 { - return Err(DCDRatioError::NotDivisible { - dividend: "principal_collateral_amount".to_string(), - divisor: "filler_per_principal_collateral".to_string(), - remainder, - }); - } - let filler_token_amount = principal_collateral_amount / filler_per_principal_collateral; - if filler_token_amount == 0 { - return Err(DCDRatioError::ZeroValue { - value_name: "filler_token_amount".to_string(), - }); - } - - let grantor_collateral_token_amount = filler_token_amount; - let grantor_settlement_token_amount = filler_token_amount; - - // filler_per_settlement_* divisions by filler_token_amount - let remainder = total_collateral_amount % filler_token_amount; - if remainder != 0 { - return Err(DCDRatioError::NotDivisible { - dividend: "total_collateral_amount".to_string(), - divisor: "filler_token_amount".to_string(), - remainder, - }); - } - let filler_per_settlement_collateral = total_collateral_amount / filler_token_amount; - - let remainder = total_asset_amount % filler_token_amount; - if remainder != 0 { - return Err(DCDRatioError::NotDivisible { - dividend: "total_asset_amount".to_string(), - divisor: "filler_token_amount".to_string(), - remainder, - }); - } - let filler_per_settlement_asset = total_asset_amount / filler_token_amount; - - // grantor_* per deposited/settlement divisions by grantor_*_token_amount (same as filler_token_amount) - let remainder = total_asset_amount % grantor_settlement_token_amount; - if remainder != 0 { - return Err(DCDRatioError::NotDivisible { - dividend: "total_asset_amount".to_string(), - divisor: "grantor_settlement_token_amount".to_string(), - remainder, - }); - } - let grantor_settlement_per_deposited_asset = - total_asset_amount / grantor_settlement_token_amount; - - let remainder = interest_collateral_amount % grantor_collateral_token_amount; - if remainder != 0 { - return Err(DCDRatioError::NotDivisible { - dividend: "interest_collateral_amount".to_string(), - divisor: "grantor_collateral_token_amount".to_string(), - remainder, - }); - } - let grantor_collateral_per_deposited_collateral = - interest_collateral_amount / grantor_collateral_token_amount; - - let remainder = total_collateral_amount % grantor_settlement_token_amount; - if remainder != 0 { - return Err(DCDRatioError::NotDivisible { - dividend: "total_collateral_amount".to_string(), - divisor: "grantor_settlement_token_amount".to_string(), - remainder, - }); - } - let grantor_per_settlement_collateral = - total_collateral_amount / grantor_settlement_token_amount; - - let remainder = total_asset_amount % grantor_settlement_token_amount; - if remainder != 0 { - return Err(DCDRatioError::NotDivisible { - dividend: "total_asset_amount".to_string(), - divisor: "grantor_settlement_token_amount".to_string(), - remainder, - }); - } - let grantor_per_settlement_asset = total_asset_amount / grantor_settlement_token_amount; - - Ok(Self { - principal_collateral_amount, - interest_collateral_amount, - total_collateral_amount, - principal_asset_amount, - interest_asset_amount, - total_asset_amount, - filler_token_amount, - grantor_collateral_token_amount, - grantor_settlement_token_amount, - filler_per_settlement_collateral, - filler_per_settlement_asset, - filler_per_principal_collateral, - grantor_settlement_per_deposited_asset, - grantor_collateral_per_deposited_collateral, - grantor_per_settlement_collateral, - grantor_per_settlement_asset, - }) - } -} - -impl Default for DCDArguments { - fn default() -> Self { - Self { - taker_funding_start_time: 0, - taker_funding_end_time: 0, - contract_expiry_time: 0, - early_termination_end_time: 0, - settlement_height: 0, - strike_price: 0, - incentive_basis_points: 0, - fee_basis_points: 0, - collateral_asset_id_hex_le: "00".repeat(32), - settlement_asset_id_hex_le: "00".repeat(32), - filler_token_asset_id_hex_le: "00".repeat(32), - grantor_collateral_token_asset_id_hex_le: "00".repeat(32), - grantor_settlement_token_asset_id_hex_le: "00".repeat(32), - fee_script_hash_hex_le: "00".repeat(32), - ratio_args: DCDRatioArguments::default(), - oracle_public_key: String::new(), - } - } -} - -impl DCDArguments { - /// Create new DCD arguments. - #[allow(clippy::too_many_arguments)] - #[must_use] - pub const fn new( - taker_funding_start_time: u32, - taker_funding_end_time: u32, - contract_expiry_time: u32, - early_termination_end_time: u32, - settlement_height: u32, - strike_price: u64, - incentive_basis_points: u64, - fee_basis_points: u64, - collateral_asset_id_hex_le: String, - settlement_asset_id_hex_le: String, - filler_token_asset_id_hex_le: String, - grantor_collateral_token_asset_id_hex_le: String, - grantor_settlement_token_asset_id_hex_le: String, - ratio_args: DCDRatioArguments, - oracle_public_key: String, - fee_script_hash_hex_le: String, - ) -> Self { - Self { - taker_funding_start_time, - taker_funding_end_time, - contract_expiry_time, - early_termination_end_time, - settlement_height, - strike_price, - incentive_basis_points, - collateral_asset_id_hex_le, - settlement_asset_id_hex_le, - filler_token_asset_id_hex_le, - grantor_collateral_token_asset_id_hex_le, - grantor_settlement_token_asset_id_hex_le, - fee_basis_points, - fee_script_hash_hex_le, - ratio_args, - oracle_public_key, - } - } - - /// Returns the taker funding start time. - #[must_use] - pub const fn taker_funding_start_time(&self) -> u32 { - self.taker_funding_start_time - } - - /// Returns the early termination end time. - #[must_use] - pub const fn early_termination_end_time(&self) -> u32 { - self.early_termination_end_time - } - - /// Convert to Simplicity program arguments. - /// - /// # Panics - /// Panics if asset IDs or public key cannot be parsed. - #[must_use] - #[expect(clippy::too_many_lines)] - pub fn build_arguments(&self) -> Arguments { - let collateral_asset = u256_from_le_hex(&self.collateral_asset_id_hex_le); - let settlement_asset = u256_from_le_hex(&self.settlement_asset_id_hex_le); - let filler_token_asset = u256_from_le_hex(&self.filler_token_asset_id_hex_le); - let grantor_collateral_token_asset = - u256_from_le_hex(&self.grantor_collateral_token_asset_id_hex_le); - let grantor_settlement_token_asset = - u256_from_le_hex(&self.grantor_settlement_token_asset_id_hex_le); - let fee_script_hash = u256_from_le_hex(&self.fee_script_hash_hex_le); - - let oracle_bytes = XOnlyPublicKey::from_str(self.oracle_public_key.as_str()) - .unwrap() - .serialize(); - - Arguments::from(HashMap::from([ - // Times - ( - WitnessName::from_str_unchecked("TAKER_FUNDING_START_TIME"), - simplicityhl::Value::from(UIntValue::U32(self.taker_funding_start_time)), - ), - ( - WitnessName::from_str_unchecked("TAKER_FUNDING_END_TIME"), - simplicityhl::Value::from(UIntValue::U32(self.taker_funding_end_time)), - ), - ( - WitnessName::from_str_unchecked("CONTRACT_EXPIRY_TIME"), - simplicityhl::Value::from(UIntValue::U32(self.contract_expiry_time)), - ), - ( - WitnessName::from_str_unchecked("EARLY_TERMINATION_END_TIME"), - simplicityhl::Value::from(UIntValue::U32(self.early_termination_end_time)), - ), - ( - WitnessName::from_str_unchecked("SETTLEMENT_HEIGHT"), - simplicityhl::Value::from(UIntValue::U32(self.settlement_height)), - ), - // Pricing - ( - WitnessName::from_str_unchecked("STRIKE_PRICE"), - simplicityhl::Value::from(UIntValue::U64(self.strike_price)), - ), - ( - WitnessName::from_str_unchecked("INCENTIVE_BASIS_POINTS"), - simplicityhl::Value::from(UIntValue::U64(self.incentive_basis_points)), - ), - ( - WitnessName::from_str_unchecked("FEE_BASIS_POINTS"), - simplicityhl::Value::from(UIntValue::U64(self.fee_basis_points)), - ), - // Assets - ( - WitnessName::from_str_unchecked("COLLATERAL_ASSET_ID"), - simplicityhl::Value::from(UIntValue::U256(collateral_asset)), - ), - ( - WitnessName::from_str_unchecked("SETTLEMENT_ASSET_ID"), - simplicityhl::Value::from(UIntValue::U256(settlement_asset)), - ), - ( - WitnessName::from_str_unchecked("FILLER_TOKEN_ASSET"), - simplicityhl::Value::from(UIntValue::U256(filler_token_asset)), - ), - ( - WitnessName::from_str_unchecked("GRANTOR_COLLATERAL_TOKEN_ASSET"), - simplicityhl::Value::from(UIntValue::U256(grantor_collateral_token_asset)), - ), - ( - WitnessName::from_str_unchecked("GRANTOR_SETTLEMENT_TOKEN_ASSET"), - simplicityhl::Value::from(UIntValue::U256(grantor_settlement_token_asset)), - ), - // Ratios - ( - WitnessName::from_str_unchecked("FILLER_PER_SETTLEMENT_COLLATERAL"), - simplicityhl::Value::from(UIntValue::U64( - self.ratio_args.filler_per_settlement_collateral(), - )), - ), - ( - WitnessName::from_str_unchecked("FILLER_PER_SETTLEMENT_ASSET"), - simplicityhl::Value::from(UIntValue::U64( - self.ratio_args.filler_per_settlement_asset(), - )), - ), - ( - WitnessName::from_str_unchecked("FILLER_PER_PRINCIPAL_COLLATERAL"), - simplicityhl::Value::from(UIntValue::U64( - self.ratio_args.filler_per_principal_collateral(), - )), - ), - ( - WitnessName::from_str_unchecked("GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET"), - simplicityhl::Value::from(UIntValue::U64( - self.ratio_args.grantor_settlement_per_deposited_asset(), - )), - ), - ( - WitnessName::from_str_unchecked("GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL"), - simplicityhl::Value::from(UIntValue::U64( - self.ratio_args - .grantor_collateral_per_deposited_collateral(), - )), - ), - ( - WitnessName::from_str_unchecked("GRANTOR_PER_SETTLEMENT_COLLATERAL"), - simplicityhl::Value::from(UIntValue::U64( - self.ratio_args.grantor_per_settlement_collateral(), - )), - ), - ( - WitnessName::from_str_unchecked("GRANTOR_PER_SETTLEMENT_ASSET"), - simplicityhl::Value::from(UIntValue::U64( - self.ratio_args.grantor_per_settlement_asset(), - )), - ), - ( - WitnessName::from_str_unchecked("FEE_SCRIPT_HASH"), - simplicityhl::Value::from(UIntValue::U256(fee_script_hash)), - ), - // Oracle - ( - WitnessName::from_str_unchecked("ORACLE_PK"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(oracle_bytes))), - ), - ])) - } -} - -impl simplicityhl_core::Encodable for DCDArguments {} - -fn u256_from_le_hex(hex_le: &str) -> U256 { - let mut bytes = <[u8; 32]>::from_hex(hex_le).expect("expected 32 bytes hex"); - bytes.reverse(); - U256::from_byte_array(bytes) -} diff --git a/crates/contracts/src/finance/dcd/build_witness.rs b/crates/contracts/src/finance/dcd/build_witness.rs deleted file mode 100644 index 62fb988..0000000 --- a/crates/contracts/src/finance/dcd/build_witness.rs +++ /dev/null @@ -1,194 +0,0 @@ -use std::collections::HashMap; - -use simplicityhl::parse::ParseFromStr; -use simplicityhl::simplicity::bitcoin; -use simplicityhl::{ResolvedType, WitnessValues, str::WitnessName, types::TypeConstructible}; - -#[derive(Debug, Default, Copy, Clone)] -pub enum MergeBranch { - // Left(Left(())) => merge 2 tokens - #[default] - Two, - // Left(Right(())) => merge 3 tokens - Three, - // Right(()) => merge 4 tokens - Four, -} - -impl MergeBranch { - const fn to_str(self) -> &'static str { - match self { - Self::Two => "Left(Left(()))", - Self::Three => "Left(Right(()))", - Self::Four => "Right(())", - } - } -} - -#[derive(Debug, Default, Clone, Copy)] -pub enum TokenBranch { - // Left(()) in SIMF - #[default] - Maker, - // Right(()) in SIMF - Taker, -} - -impl TokenBranch { - const fn to_str(self) -> &'static str { - match self { - Self::Maker => "Left(())", - Self::Taker => "Right(())", - } - } -} - -#[derive(Debug)] -pub enum DcdBranch<'a> { - // Left(Left(Left((u64, u64, u64, u64)))) - MakerFunding { - principal_collateral_amount: u64, - principal_asset_amount: u64, - interest_collateral_amount: u64, - interest_asset_amount: u64, - }, - // Left(Left(Right((u64, u64, bool)))) - TakerFunding { - collateral_amount_to_deposit: u64, - filler_token_amount_to_get: u64, - is_change_needed: bool, - }, - // Left(Right((u64, Signature, u64, u64, bool))) - Settlement { - price_at_current_block_height: u64, - oracle_sig: &'a bitcoin::secp256k1::schnorr::Signature, - amount_to_burn: u64, - amount_to_get: u64, - is_change_needed: bool, - }, - // Right(Left((bool, u64, u64))) - TakerEarlyTermination { - is_change_needed: bool, - filler_token_amount_to_return: u64, - collateral_amount_to_get: u64, - }, - // Right(Right((bool, u64, u64))) - MakerTermination { - is_change_needed: bool, - grantor_token_amount_to_burn: u64, - amount_to_get: u64, - }, - // Right(Right(())) with MERGE_BRANCH controlling merge routine - Merge, -} - -/// Build witness values for DCD program execution. -/// -/// # Panics -/// Panics if type parsing fails (should never happen with valid constants). -#[must_use] -pub fn build_dcd_witness( - token_branch: TokenBranch, - branch: &DcdBranch, - merge_branch: MergeBranch, -) -> WitnessValues { - // Types - let maker_funding = ResolvedType::parse_from_str("(u64, u64, u64, u64)").unwrap(); - let taker_funding = ResolvedType::parse_from_str("(u64, u64, bool)").unwrap(); - let settlement = ResolvedType::parse_from_str("(u64, Signature, u64, u64, bool)").unwrap(); - let taker_termination = ResolvedType::parse_from_str("(bool, u64, u64)").unwrap(); - let maker_termination = ResolvedType::parse_from_str("(bool, u64, u64)").unwrap(); - - let funding_either = ResolvedType::either(maker_funding, taker_funding); - let left_type = ResolvedType::either(funding_either, settlement); - let termination_or_maker = ResolvedType::either(taker_termination, maker_termination); - let right_type = ResolvedType::either( - termination_or_maker, - ResolvedType::parse_from_str("()").unwrap(), - ); - let path_type = ResolvedType::either(left_type, right_type); - - // Merge branch type: Either, ()> - let merge_choice = ResolvedType::either( - ResolvedType::parse_from_str("()").unwrap(), - ResolvedType::parse_from_str("()").unwrap(), - ); - let merge_type = - ResolvedType::either(merge_choice, ResolvedType::parse_from_str("()").unwrap()); - - // Values - let branch_str = match branch { - DcdBranch::MakerFunding { - principal_collateral_amount, - principal_asset_amount, - interest_collateral_amount, - interest_asset_amount, - } => { - format!( - "Left(Left(Left(({principal_collateral_amount}, {principal_asset_amount}, {interest_collateral_amount}, {interest_asset_amount}))))" - ) - } - DcdBranch::TakerFunding { - collateral_amount_to_deposit, - filler_token_amount_to_get, - is_change_needed, - } => { - format!( - "Left(Left(Right(({collateral_amount_to_deposit}, {filler_token_amount_to_get}, {is_change_needed}))))" - ) - } - DcdBranch::Settlement { - price_at_current_block_height, - oracle_sig, - amount_to_burn, - amount_to_get, - is_change_needed, - } => { - let sig_hex = hex::encode(oracle_sig.serialize()); - format!( - "Left(Right(({price_at_current_block_height}, 0x{sig_hex}, {amount_to_burn}, {amount_to_get}, {is_change_needed})))" - ) - } - DcdBranch::TakerEarlyTermination { - is_change_needed, - filler_token_amount_to_return, - collateral_amount_to_get, - } => { - format!( - "Right(Left(Left(({is_change_needed}, {filler_token_amount_to_return}, {collateral_amount_to_get}))))" - ) - } - DcdBranch::MakerTermination { - is_change_needed, - grantor_token_amount_to_burn, - amount_to_get, - } => { - format!( - "Right(Left(Right(({is_change_needed}, {grantor_token_amount_to_burn}, {amount_to_get}))))" - ) - } - DcdBranch::Merge => "Right(Right(()))".to_string(), - }; - - simplicityhl::WitnessValues::from(HashMap::from([ - ( - WitnessName::from_str_unchecked("TOKEN_BRANCH"), - simplicityhl::Value::parse_from_str( - token_branch.to_str(), - &ResolvedType::either( - ResolvedType::parse_from_str("()").unwrap(), - ResolvedType::parse_from_str("()").unwrap(), - ), - ) - .unwrap(), - ), - ( - WitnessName::from_str_unchecked("PATH"), - simplicityhl::Value::parse_from_str(&branch_str, &path_type).unwrap(), - ), - ( - WitnessName::from_str_unchecked("MERGE_BRANCH"), - simplicityhl::Value::parse_from_str(merge_branch.to_str(), &merge_type).unwrap(), - ), - ])) -} diff --git a/crates/contracts/src/finance/dcd/mod.rs b/crates/contracts/src/finance/dcd/mod.rs deleted file mode 100644 index 06d571a..0000000 --- a/crates/contracts/src/finance/dcd/mod.rs +++ /dev/null @@ -1,1949 +0,0 @@ -//! Dual Currency Deposit (DCD) – price-attested Simplicity covenant for Liquid testnet. -//! -//! This module exposes helpers to compile, execute, and finalize the DCD program: -//! - `get_dcd_template_program`, `get_dcd_program`, `get_compiled_dcd_program` -//! - `get_dcd_address` to derive the covenant P2TR address bound to a Taproot pubkey -//! - `execute_dcd_program` to run a specific branch with witness values -//! - `finalize_dcd_transaction_on_liquid_testnet` to attach the Simplicity witness to a tx input -//! -//! DCD flows supported by the Simplicity program and CLI: -//! - Maker funding: deposit settlement asset and collateral, issue grantor tokens -//! - Taker funding: deposit collateral within the funding window and receive filler tokens -//! - Settlement at `settlement_height`: oracle Schnorr-signature selects LBTC vs ALT branch -//! - Early/post-expiry termination: taker returns filler; maker burns grantor tokens -//! - Token merge utilities: merge 2/3/4 token UTXOs -//! -//! See `crates/cli/README.md` for canonical CLI usage and parameters. -//! All transactions are explicit and target Liquid testnet. - -use std::sync::Arc; - -use simplicityhl::simplicity::RedeemNode; -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; -use simplicityhl::simplicity::elements::{Address, Transaction, TxInWitness, TxOut}; -use simplicityhl::simplicity::hashes::{Hash, sha256}; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; -use simplicityhl::tracker::TrackerLogLevel; -use simplicityhl::{CompiledProgram, TemplateProgram}; -use simplicityhl_core::{ - ProgramError, SimplicityNetwork, control_block, create_p2tr_address, load_program, run_program, -}; - -mod build_arguments; -mod build_witness; - -pub use build_arguments::{DCDArguments, DCDRatioArguments}; -pub use build_witness::{DcdBranch, MergeBranch, TokenBranch, build_dcd_witness}; - -pub const PRICE_ATTESTED_SDK_SOURCE: &str = include_str!("source_simf/dual_currency_deposit.simf"); - -/// Get the DCD template program for instantiation with arguments. -/// -/// # Panics -/// Panics if the embedded source fails to compile (should never happen). -#[must_use] -pub fn get_dcd_template_program() -> TemplateProgram { - TemplateProgram::new(PRICE_ATTESTED_SDK_SOURCE) - .expect("INTERNAL: expected DCD Price Attested Program to compile successfully.") -} - -/// Derive P2TR address for a DCD contract with given arguments. -/// -/// # Errors -/// Returns error if program compilation fails. -pub fn get_dcd_address( - x_only_public_key: &XOnlyPublicKey, - arguments: &DCDArguments, - network: SimplicityNetwork, -) -> Result { - Ok(create_p2tr_address( - get_dcd_program(arguments)?.commit().cmr(), - x_only_public_key, - network.address_params(), - )) -} - -/// Compile DCD program with the given arguments. -/// -/// # Errors -/// Returns error if compilation fails. -pub fn get_dcd_program(arguments: &DCDArguments) -> Result { - load_program(PRICE_ATTESTED_SDK_SOURCE, arguments.build_arguments()) -} - -/// Get compiled DCD program, panicking on failure. -/// -/// # Panics -/// Panics if program instantiation fails. -#[must_use] -pub fn get_compiled_dcd_program(arguments: &DCDArguments) -> CompiledProgram { - let program = get_dcd_template_program(); - - program - .instantiate(arguments.build_arguments(), true) - .unwrap() -} - -/// Execute DCD program with witness values for the specified branches. -/// -/// # Errors -/// Returns error if program execution fails. -pub fn execute_dcd_program( - compiled_program: &CompiledProgram, - env: &ElementsEnv>, - token_branch: TokenBranch, - branch: &DcdBranch, - merge_branch: MergeBranch, - runner_log_level: TrackerLogLevel, -) -> Result>, ProgramError> { - let witness_values = build_dcd_witness(token_branch, branch, merge_branch); - Ok(run_program(compiled_program, witness_values, env, runner_log_level)?.0) -} - -/// Finalize DCD transaction by attaching Simplicity witness on Liquid testnet. -/// -/// # Errors -/// Returns error if program execution or environment verification fails. -/// -/// # Panics -/// Panics if UTXO index is out of bounds or script pubkey mismatch. -#[allow(clippy::too_many_arguments)] -pub fn finalize_dcd_transaction_on_liquid_testnet( - mut tx: Transaction, - dcd_program: &CompiledProgram, - dcd_public_key: &XOnlyPublicKey, - utxos: &[TxOut], - input_index: u32, - token_branch: TokenBranch, - branch: &DcdBranch, - merge_branch: MergeBranch, - log_level: TrackerLogLevel, -) -> Result { - let network = SimplicityNetwork::LiquidTestnet; - let cmr = dcd_program.commit().cmr(); - - assert!( - utxos.len() > input_index as usize, - "UTXOs must be greater than input index" - ); - - let target_utxo = &utxos[input_index as usize]; - let script_pubkey = - create_p2tr_address(cmr, dcd_public_key, network.address_params()).script_pubkey(); - - assert_eq!( - target_utxo.script_pubkey, script_pubkey, - "Expected for the UTXO to be spent by DCD to have the same script." - ); - - let env: ElementsEnv> = ElementsEnv::new( - Arc::new(tx.clone()), - utxos - .iter() - .map(|utxo| ElementsUtxo { - script_pubkey: utxo.script_pubkey.clone(), - asset: utxo.asset, - value: utxo.value, - }) - .collect(), - input_index, - cmr, - control_block(cmr, *dcd_public_key), - None, - network.genesis_block_hash(), - ); - - let pruned = execute_dcd_program( - dcd_program, - &env, - token_branch, - branch, - merge_branch, - log_level, - )?; - - let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); - let cmr = pruned.cmr(); - - tx.input[input_index as usize].witness = TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, - script_witness: vec![ - simplicity_witness_bytes, - simplicity_program_bytes, - cmr.as_ref().to_vec(), - control_block(cmr, *dcd_public_key).serialize(), - ], - pegin_witness: vec![], - }; - - Ok(tx) -} - -#[must_use] -pub fn oracle_msg(expiry_height: u32, price_at_current_block_height: u64) -> [u8; 32] { - let mut b = [0u8; 12]; - b[..4].copy_from_slice(&expiry_height.to_be_bytes()); - b[4..].copy_from_slice(&price_at_current_block_height.to_be_bytes()); - sha256::Hash::hash(&b).to_byte_array() -} - -#[cfg(test)] -#[expect(clippy::too_many_lines)] -mod dcd_merge_tests { - use super::*; - - use std::str::FromStr; - use std::sync::Arc; - use std::time::{SystemTime, UNIX_EPOCH}; - - use crate::finance::dcd::build_arguments::DCDRatioArguments; - use crate::finance::dcd::build_witness::build_dcd_witness; - use anyhow::Result; - use simplicityhl::elements::secp256k1_zkp::Secp256k1; - use simplicityhl::elements::taproot::ControlBlock; - use simplicityhl::simplicity::bitcoin::key::Keypair; - use simplicityhl::simplicity::bitcoin::secp256k1; - use simplicityhl::simplicity::elements::confidential::{Asset, Value}; - use simplicityhl::simplicity::elements::pset::{Input, Output, PartiallySignedTransaction}; - use simplicityhl::simplicity::elements::{self, AssetId, OutPoint, Script, Txid}; - use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; - use simplicityhl_core::{ - LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_TEST_ASSET_ID_STR, SimplicityNetwork, - get_new_asset_entropy, get_p2pk_address, hash_script, - }; - - const NETWORK: SimplicityNetwork = SimplicityNetwork::LiquidTestnet; - - #[test] - fn test_dcd_maker_funding_path() -> Result<()> { - let oracle_kp = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[3u8; 32])?, - ); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); - let second_asset_id = AssetId::from_entropy(second_asset_entropy); - let third_asset_id = AssetId::from_entropy(third_asset_entropy); - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let dcd_arguments = DCDArguments::new( - 10, - 0, - 0, - 0, - 0, - strike_price, - incentive_basis_points, - 0, - LIQUID_TESTNET_BITCOIN_ASSET.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args.clone(), - oracle_kp.x_only_public_key().0.to_string(), - "00".repeat(32), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - - let mut first_reissuance_tx = Input::from_prevout(outpoint); - first_reissuance_tx.issuance_value_amount = Some(ratio_args.filler_token_amount()); - first_reissuance_tx.issuance_inflation_keys = None; - first_reissuance_tx.issuance_asset_entropy = Some(first_asset_entropy.to_byte_array()); - - let mut second_reissuance_tx = Input::from_prevout(outpoint); - second_reissuance_tx.issuance_value_amount = Some(ratio_args.filler_token_amount()); - second_reissuance_tx.issuance_inflation_keys = None; - second_reissuance_tx.issuance_asset_entropy = Some(second_asset_entropy.to_byte_array()); - - let mut third_reissuance_tx = Input::from_prevout(outpoint); - third_reissuance_tx.issuance_value_amount = Some(ratio_args.filler_token_amount()); - third_reissuance_tx.issuance_inflation_keys = None; - third_reissuance_tx.issuance_asset_entropy = Some(third_asset_entropy.to_byte_array()); - - pst.add_input(first_reissuance_tx); - pst.add_input(second_reissuance_tx); - pst.add_input(third_reissuance_tx); - pst.add_input(Input::from_prevout(outpoint)); - pst.add_input(Input::from_prevout(outpoint)); - - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - 1, - AssetId::default(), - None, - )); - - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - 1, - AssetId::default(), - None, - )); - - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - 1, - AssetId::default(), - None, - )); - - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - ratio_args.interest_collateral_amount(), - *LIQUID_TESTNET_BITCOIN_ASSET, - None, - )); - - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - ratio_args.total_asset_amount(), - AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?, - None, - )); - - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - ratio_args.filler_token_amount(), - first_asset_id, - None, - )); - - pst.add_output(Output::new_explicit( - Script::new(), - ratio_args.filler_token_amount(), - second_asset_id, - None, - )); - - pst.add_output(Output::new_explicit( - Script::new(), - ratio_args.filler_token_amount(), - third_asset_id, - None, - )); - - // Output 8: collateral change - pst.add_output(Output::new_explicit( - Script::new(), - 1, - *LIQUID_TESTNET_BITCOIN_ASSET, - None, - )); - - // Output 9: settlement change - pst.add_output(Output::new_explicit( - Script::new(), - 1, - AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?, - None, - )); - - // Output 10: fee output (collateral) - pst.add_output(Output::new_explicit( - Script::new(), - 1, - *LIQUID_TESTNET_BITCOIN_ASSET, - None, - )); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1000), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1000), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1000), - }, - // Input 3: settlement asset - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit( - AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR).unwrap(), - ), - value: Value::Explicit(ratio_args.total_asset_amount()), - }, - // Input 4: collateral asset - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(ratio_args.interest_collateral_amount()), - }, - ], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_dcd_witness( - TokenBranch::default(), - &DcdBranch::MakerFunding { - principal_collateral_amount: ratio_args.principal_collateral_amount(), - principal_asset_amount: ratio_args.principal_asset_amount(), - interest_collateral_amount: ratio_args.interest_collateral_amount(), - interest_asset_amount: ratio_args.interest_asset_amount(), - }, - MergeBranch::default(), - ); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Debug).is_ok(), - "expected success funding path -- dcd price attested" - ); - - Ok(()) - } - - #[test] - fn test_dcd_taker_funding_path_with_change_and_locktime() -> Result<()> { - let now: u32 = u32::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())?; - - let oracle_kp = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[3u8; 32])?, - ); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); // FILLER - let second_asset_id = AssetId::from_entropy(second_asset_entropy); // GRANTOR_COLLATERAL - let third_asset_id = AssetId::from_entropy(third_asset_entropy); // GRANTOR_SETTLEMENT - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let dcd_arguments = DCDArguments::new( - now + 10, - now + 20, - now + 30, - now, - 0, - strike_price, - incentive_basis_points, - 0, - AssetId::LIQUID_BTC.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args.clone(), - oracle_kp.x_only_public_key().0.to_string(), - "00".repeat(32), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - // Locktime window: start <= t < end and t < expiry - pst.global.tx_data.fallback_locktime = Some(elements::LockTime::from_time( - dcd_arguments.taker_funding_start_time() + 5, - )?); - - // Input[0]: FILLER token input that will provide change - pst.add_input(Input::from_prevout(outpoint)); - pst.inputs_mut()[0].sequence = Some(elements::Sequence::ENABLE_LOCKTIME_NO_RBF); - - // Outputs: - // 0: FILLER change (available - to_get) - let available_filler = ratio_args.filler_token_amount() * 2; // 200 - let filler_to_get = ratio_args.filler_token_amount(); // 100 - let filler_change = available_filler - filler_to_get; // 100 - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - filler_change, - first_asset_id, - None, - )); - - // 1: Collateral to covenant (deposit) - let collateral_deposit = ratio_args.filler_per_principal_collateral() * filler_to_get; // 1000 - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - collateral_deposit, - AssetId::LIQUID_BTC, - None, - )); - - // 2: FILLER to user - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - filler_to_get, - first_asset_id, - None, - )); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(first_asset_id), - value: Value::Explicit(available_filler), - }], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_dcd_witness( - TokenBranch::default(), - &DcdBranch::TakerFunding { - collateral_amount_to_deposit: collateral_deposit, - filler_token_amount_to_get: filler_to_get, - is_change_needed: true, - }, - MergeBranch::default(), - ); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Debug).is_ok(), - "expected success taker funding path -- dcd price attested" - ); - - Ok(()) - } - - #[test] - fn test_dcd_taker_early_termination_path_with_change_and_locktime() -> Result<()> { - let now: u32 = u32::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())?; - - let oracle_kp = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[3u8; 32])?, - ); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); // FILLER - let second_asset_id = AssetId::from_entropy(second_asset_entropy); // GRANTOR_COLLATERAL - let third_asset_id = AssetId::from_entropy(third_asset_entropy); // GRANTOR_SETTLEMENT - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let dcd_arguments = DCDArguments::new( - now, - now, - now + 30, - now + 20, - 0, - strike_price, - incentive_basis_points, - 0, - AssetId::LIQUID_BTC.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args.clone(), - oracle_kp.x_only_public_key().0.to_string(), - "00".repeat(32), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - // Locktime t <= early_term_end - pst.global.tx_data.fallback_locktime = Some(elements::LockTime::from_time( - dcd_arguments.early_termination_end_time() - 5, - )?); - - // Inputs: 0 -> collateral, 1 -> filler token - pst.add_input(Input::from_prevout(outpoint)); - pst.inputs_mut()[0].sequence = Some(elements::Sequence::ENABLE_LOCKTIME_NO_RBF); - pst.add_input(Input::from_prevout(outpoint)); - - let collateral_get = ratio_args.principal_collateral_amount(); // 1000 - let filler_return = ratio_args.filler_token_amount(); // 100 - let available_collateral = collateral_get + 500; // to force change - let collateral_change = available_collateral - collateral_get; // 500 - - // 0: collateral change - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - collateral_change, - AssetId::LIQUID_BTC, - None, - )); - // 1: return filler to covenant (same script) - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - filler_return, - first_asset_id, - None, - )); - // 2: return collateral to user - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - collateral_get, - AssetId::LIQUID_BTC, - None, - )); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(available_collateral), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(first_asset_id), - value: Value::Explicit(filler_return), - }, - ], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_dcd_witness( - TokenBranch::default(), - &DcdBranch::TakerEarlyTermination { - is_change_needed: true, - filler_token_amount_to_return: filler_return, - collateral_amount_to_get: collateral_get, - }, - MergeBranch::default(), - ); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Debug).is_ok(), - "expected success taker early termination path -- dcd price attested" - ); - - Ok(()) - } - - #[test] - fn test_dcd_maker_collateral_termination_path_with_change_and_locktime() -> Result<()> { - let now: u32 = u32::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())?; - - let oracle_kp = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[3u8; 32])?, - ); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); // FILLER - let second_asset_id = AssetId::from_entropy(second_asset_entropy); // GRANTOR_COLLATERAL - let third_asset_id = AssetId::from_entropy(third_asset_entropy); // GRANTOR_SETTLEMENT - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let dcd_arguments = DCDArguments::new( - now, - now, - now + 30, - now + 20, - 0, - strike_price, - incentive_basis_points, - 0, - AssetId::LIQUID_BTC.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args.clone(), - oracle_kp.x_only_public_key().0.to_string(), - "00".repeat(32), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - pst.global.tx_data.fallback_locktime = Some(elements::LockTime::from_time( - dcd_arguments.early_termination_end_time() - 1, - )?); - - // Inputs: 0 -> collateral, 1 -> grantor collateral token - pst.add_input(Input::from_prevout(outpoint)); - pst.inputs_mut()[0].sequence = Some(elements::Sequence::ENABLE_LOCKTIME_NO_RBF); - pst.add_input(Input::from_prevout(outpoint)); - - let grantor_burn = ratio_args.filler_token_amount() / 2; // 50 - let collateral_get = ratio_args.interest_collateral_amount() / 2; // 1000 - let collateral_change = ratio_args.interest_collateral_amount() - collateral_get; - - // 0: collateral change - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - collateral_change, - AssetId::LIQUID_BTC, - None, - )); - // 1: burn grantor collateral token (OP_RETURN) - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - grantor_burn, - second_asset_id, - None, - )); - // 2: return collateral to user - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - collateral_get, - AssetId::LIQUID_BTC, - None, - )); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(ratio_args.interest_collateral_amount()), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(second_asset_id), - value: Value::Explicit(grantor_burn), - }, - ], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_dcd_witness( - TokenBranch::Maker, - &DcdBranch::MakerTermination { - is_change_needed: true, - grantor_token_amount_to_burn: grantor_burn, - amount_to_get: collateral_get, - }, - MergeBranch::default(), - ); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::None).is_ok(), - "expected success maker collateral termination path -- dcd price attested" - ); - - Ok(()) - } - - #[test] - fn test_dcd_maker_settlement_termination_path_with_change_and_locktime() -> Result<()> { - let now: u32 = u32::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())?; - - let oracle_kp = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[3u8; 32])?, - ); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); // FILLER - let second_asset_id = AssetId::from_entropy(second_asset_entropy); // GRANTOR_COLLATERAL - let third_asset_id = AssetId::from_entropy(third_asset_entropy); // GRANTOR_SETTLEMENT - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let dcd_arguments = DCDArguments::new( - now, - now, - now + 30, - now + 20, - 0, - strike_price, - incentive_basis_points, - 0, - AssetId::LIQUID_BTC.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args.clone(), - oracle_kp.x_only_public_key().0.to_string(), - "00".repeat(32), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - pst.global.tx_data.fallback_locktime = Some(elements::LockTime::from_time( - dcd_arguments.early_termination_end_time() - 1, - )?); - - // Inputs: 0 -> settlement asset, 1 -> grantor settlement token - pst.add_input(Input::from_prevout(outpoint)); - pst.inputs_mut()[0].sequence = Some(elements::Sequence::ENABLE_LOCKTIME_NO_RBF); - pst.add_input(Input::from_prevout(outpoint)); - - let grantor_burn = ratio_args.filler_token_amount() / 2; // 100 - let settlement_get = ratio_args.total_asset_amount() / 2; // 11000 - let settlement_change = ratio_args.total_asset_amount() - settlement_get; - - // 0: settlement change - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - settlement_change, - AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?, - None, - )); - // 1: burn grantor settlement token (OP_RETURN) - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - grantor_burn, - third_asset_id, - None, - )); - // 2: return settlement to user - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - settlement_get, - AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?, - None, - )); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?), - value: Value::Explicit(ratio_args.total_asset_amount()), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(third_asset_id), - value: Value::Explicit(grantor_burn), - }, - ], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_dcd_witness( - TokenBranch::Taker, - &DcdBranch::MakerTermination { - is_change_needed: true, - grantor_token_amount_to_burn: grantor_burn, - amount_to_get: settlement_get, - }, - MergeBranch::default(), - ); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Debug).is_ok(), - "expected success maker settlement termination path -- dcd price attested" - ); - - Ok(()) - } - - #[test] - fn test_dcd_maker_settlement_path_price_le_strike() -> Result<()> { - let now: u32 = u32::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())?; - - let secp = Secp256k1::new(); - let oracle_sk = secp256k1::SecretKey::from_slice(&[3u8; 32])?; - let oracle_kp = Keypair::from_secret_key(&secp, &oracle_sk); - let oracle_schnorr_kp = Keypair::from_secret_key(&secp, &oracle_sk); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); // FILLER - let second_asset_id = AssetId::from_entropy(second_asset_entropy); // GRANTOR_COLLATERAL - let third_asset_id = AssetId::from_entropy(third_asset_entropy); // GRANTOR_SETTLEMENT - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let settlement_height = 100u32; - let dcd_arguments = DCDArguments::new( - now, - now, - now, - now, - settlement_height, - strike_price, - incentive_basis_points, - 0, - AssetId::LIQUID_BTC.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args.clone(), - oracle_kp.x_only_public_key().0.to_string(), - "00".repeat(32), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - // Height lock for settlement - pst.global.tx_data.fallback_locktime = - Some(elements::LockTime::from_height(settlement_height)?); - pst.add_input(Input::from_prevout(outpoint)); - pst.inputs_mut()[0].sequence = Some(elements::Sequence::ENABLE_LOCKTIME_NO_RBF); - - // Maker gets ALT branch (price <= strike): input is SETTLEMENT asset - let amount_to_get = ratio_args.total_asset_amount() / 2; // 11000 - let grantor_burn = ratio_args.filler_token_amount() / 2; // 100 - let available_settlement = ratio_args.total_asset_amount(); - - // 0: settlement change - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - available_settlement - amount_to_get, - AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?, - None, - )); - // 1: burn grantor settlement token (OP_RETURN) - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - grantor_burn, - third_asset_id, - None, - )); - // 2: burn grantor collateral token (OP_RETURN) - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - grantor_burn, - second_asset_id, - None, - )); - // 3: settlement to user - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - amount_to_get, - AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?, - None, - )); - - // Oracle signature for price <= strike - let price = strike_price - 2; // <= strike - let msg = oracle_msg(settlement_height, price); - let sig = secp.sign_schnorr( - &secp256k1::Message::from_digest_slice(&msg)?, - &oracle_schnorr_kp, - ); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?), - value: Value::Explicit(available_settlement), - }], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_dcd_witness( - TokenBranch::Maker, - &DcdBranch::Settlement { - price_at_current_block_height: price, - oracle_sig: &sig, - amount_to_burn: grantor_burn, - amount_to_get, - is_change_needed: true, - }, - MergeBranch::default(), - ); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Debug).is_ok(), - "expected success maker settlement (price <= strike) -- dcd price attested" - ); - - Ok(()) - } - - #[test] - fn test_dcd_maker_settlement_path_price_gt_strike() -> Result<()> { - let now: u32 = u32::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())?; - - let secp = Secp256k1::new(); - let oracle_sk = secp256k1::SecretKey::from_slice(&[3u8; 32])?; - let oracle_kp = Keypair::from_secret_key(&secp, &oracle_sk); - let oracle_schnorr_kp = Keypair::from_secret_key(&secp, &oracle_sk); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); // FILLER - let second_asset_id = AssetId::from_entropy(second_asset_entropy); // GRANTOR_COLLATERAL - let third_asset_id = AssetId::from_entropy(third_asset_entropy); // GRANTOR_SETTLEMENT - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let settlement_height = 100u32; - let dcd_arguments = DCDArguments::new( - now, - now, - now, - now, - settlement_height, - strike_price, - incentive_basis_points, - 0, - AssetId::LIQUID_BTC.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args.clone(), - oracle_kp.x_only_public_key().0.to_string(), - "00".repeat(32), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - // Height lock for settlement - pst.global.tx_data.fallback_locktime = - Some(elements::LockTime::from_height(settlement_height)?); - pst.add_input(Input::from_prevout(outpoint)); - pst.inputs_mut()[0].sequence = Some(elements::Sequence::ENABLE_LOCKTIME_NO_RBF); - - // Maker gets LBTC branch (price > strike): input is COLLATERAL - let amount_to_get = ratio_args.total_collateral_amount() / 2; // 1100 - let grantor_burn = ratio_args.filler_token_amount() / 2; // 100 - let available_collateral = ratio_args.total_collateral_amount(); - - // 0: collateral change - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - available_collateral - amount_to_get, - AssetId::LIQUID_BTC, - None, - )); - // 1: burn grantor collateral token (OP_RETURN) - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - grantor_burn, - second_asset_id, - None, - )); - // 2: burn grantor settlement token (OP_RETURN) - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - grantor_burn, - third_asset_id, - None, - )); - // 3: collateral to user - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - amount_to_get, - AssetId::LIQUID_BTC, - None, - )); - - // Oracle signature for price > strike - let price = strike_price + 2; // > strike - let msg = oracle_msg(settlement_height, price); - let sig = secp.sign_schnorr( - &secp256k1::Message::from_digest_slice(&msg)?, - &oracle_schnorr_kp, - ); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(available_collateral), - }], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_dcd_witness( - TokenBranch::Maker, - &DcdBranch::Settlement { - price_at_current_block_height: price, - oracle_sig: &sig, - amount_to_burn: grantor_burn, - amount_to_get, - is_change_needed: true, - }, - MergeBranch::default(), - ); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Debug).is_ok(), - "expected success maker settlement (price > strike) -- dcd price attested" - ); - - Ok(()) - } - - #[test] - fn test_dcd_taker_settlement_path_price_le_strike() -> Result<()> { - let now: u32 = u32::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())?; - - let secp = Secp256k1::new(); - let oracle_sk = secp256k1::SecretKey::from_slice(&[3u8; 32])?; - let oracle_kp = Keypair::from_secret_key(&secp, &oracle_sk); - let oracle_schnorr_kp = Keypair::from_secret_key(&secp, &oracle_sk); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); // FILLER - let second_asset_id = AssetId::from_entropy(second_asset_entropy); // GRANTOR_COLLATERAL - let third_asset_id = AssetId::from_entropy(third_asset_entropy); // GRANTOR_SETTLEMENT - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let settlement_height = 100u32; - let dcd_arguments = DCDArguments::new( - now, - now, - now, - now, - settlement_height, - strike_price, - incentive_basis_points, - 0, - AssetId::LIQUID_BTC.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args.clone(), - oracle_kp.x_only_public_key().0.to_string(), - "00".repeat(32), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - pst.global.tx_data.fallback_locktime = - Some(elements::LockTime::from_height(settlement_height)?); - pst.add_input(Input::from_prevout(outpoint)); - pst.inputs_mut()[0].sequence = Some(elements::Sequence::ENABLE_LOCKTIME_NO_RBF); - - // Taker receives LBTC (price <= strike): input is COLLATERAL - let amount_to_get = ratio_args.total_collateral_amount() / 2; // 1100 - let filler_burn = ratio_args.filler_token_amount() / 2; // 100 - let available_collateral = ratio_args.total_collateral_amount(); - - // 0: collateral change - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - available_collateral - amount_to_get, - AssetId::LIQUID_BTC, - None, - )); - // 1: burn filler token (OP_RETURN) - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - filler_burn, - first_asset_id, - None, - )); - // 2: collateral to user - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - amount_to_get, - AssetId::LIQUID_BTC, - None, - )); - - // Oracle sig - let price = strike_price - 2; // <= strike - let msg = oracle_msg(settlement_height, price); - let sig = secp.sign_schnorr( - &secp256k1::Message::from_digest_slice(&msg)?, - &oracle_schnorr_kp, - ); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(available_collateral), - }], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_dcd_witness( - TokenBranch::Taker, - &DcdBranch::Settlement { - price_at_current_block_height: price, - oracle_sig: &sig, - amount_to_burn: filler_burn, - amount_to_get, - is_change_needed: true, - }, - MergeBranch::default(), - ); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Debug).is_ok(), - "expected success taker settlement (price <= strike) -- dcd price attested" - ); - - Ok(()) - } - - #[test] - fn test_dcd_taker_settlement_path_with_fee_basis_points() -> Result<()> { - let now: u32 = u32::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())?; - - let secp = Secp256k1::new(); - let oracle_sk = secp256k1::SecretKey::from_slice(&[3u8; 32])?; - let oracle_kp = Keypair::from_secret_key(&secp, &oracle_sk); - let oracle_schnorr_kp = Keypair::from_secret_key(&secp, &oracle_sk); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); // FILLER - let second_asset_id = AssetId::from_entropy(second_asset_entropy); // GRANTOR_COLLATERAL - let third_asset_id = AssetId::from_entropy(third_asset_entropy); // GRANTOR_SETTLEMENT - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let fee_basis_points = 100; - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let fee_recipient = get_p2pk_address(&oracle_kp.x_only_public_key().0, NETWORK)?; - - let mut fee_script_hash: [u8; 32] = hash_script(&fee_recipient.script_pubkey()); - fee_script_hash.reverse(); - - let settlement_height = 100u32; - let dcd_arguments = DCDArguments::new( - now, - now, - now, - now, - settlement_height, - strike_price, - incentive_basis_points, - fee_basis_points, - AssetId::LIQUID_BTC.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args.clone(), - oracle_kp.x_only_public_key().0.to_string(), - hex::encode(fee_script_hash), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let amount_to_get = ratio_args.total_collateral_amount(); - let filler_burn = ratio_args.filler_token_amount(); - let fee_amount = amount_to_get * fee_basis_points / build_arguments::MAX_BASIS_POINTS; - let user_amount = amount_to_get - fee_amount; - - let mut pst = PartiallySignedTransaction::new_v2(); - pst.global.tx_data.fallback_locktime = - Some(elements::LockTime::from_height(settlement_height)?); - pst.add_input(Input::from_prevout(outpoint)); - pst.inputs_mut()[0].sequence = Some(elements::Sequence::ENABLE_LOCKTIME_NO_RBF); - - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - filler_burn, - first_asset_id, - None, - )); - - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - user_amount, - AssetId::LIQUID_BTC, - None, - )); - - pst.add_output(Output::new_explicit( - fee_recipient.script_pubkey(), - fee_amount, - AssetId::LIQUID_BTC, - None, - )); - - let price = strike_price - 1; // price <= strike ensures taker receives collateral - let msg = oracle_msg(settlement_height, price); - let sig = secp.sign_schnorr( - &secp256k1::Message::from_digest_slice(&msg)?, - &oracle_schnorr_kp, - ); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(amount_to_get), - }], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_dcd_witness( - TokenBranch::Taker, - &DcdBranch::Settlement { - price_at_current_block_height: price, - oracle_sig: &sig, - amount_to_burn: filler_burn, - amount_to_get, - is_change_needed: false, - }, - MergeBranch::default(), - ); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Debug).is_ok(), - "expected success taker settlement with fees -- dcd price attested" - ); - - Ok(()) - } - - #[test] - fn test_dcd_taker_settlement_path_price_gt_strike() -> Result<()> { - let now: u32 = u32::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())?; - - let secp = Secp256k1::new(); - let oracle_sk = secp256k1::SecretKey::from_slice(&[3u8; 32])?; - let oracle_kp = Keypair::from_secret_key(&secp, &oracle_sk); - let oracle_schnorr_kp = Keypair::from_secret_key(&secp, &oracle_sk); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); // FILLER - let second_asset_id = AssetId::from_entropy(second_asset_entropy); // GRANTOR_COLLATERAL - let third_asset_id = AssetId::from_entropy(third_asset_entropy); // GRANTOR_SETTLEMENT - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let settlement_height = 100u32; - let dcd_arguments = DCDArguments::new( - now, - now, - now, - now, - settlement_height, - strike_price, - incentive_basis_points, - 0, - AssetId::LIQUID_BTC.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args.clone(), - oracle_kp.x_only_public_key().0.to_string(), - "00".repeat(32), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - pst.global.tx_data.fallback_locktime = - Some(elements::LockTime::from_height(settlement_height)?); - pst.add_input(Input::from_prevout(outpoint)); - pst.inputs_mut()[0].sequence = Some(elements::Sequence::ENABLE_LOCKTIME_NO_RBF); - - // Taker receives SETTLEMENT (price > strike): input is SETTLEMENT asset - let amount_to_get = ratio_args.total_asset_amount() / 2; // 11000 - let filler_burn = ratio_args.filler_token_amount() / 2; // 100 - let available_settlement = ratio_args.total_asset_amount(); - - // 0: settlement change - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - available_settlement - amount_to_get, - AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?, - None, - )); - // 1: burn filler token (OP_RETURN) - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - filler_burn, - first_asset_id, - None, - )); - // 2: settlement to user - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - amount_to_get, - AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?, - None, - )); - - // Oracle sig - let price = strike_price + 2; // > strike - let msg = oracle_msg(settlement_height, price); - let sig = secp.sign_schnorr( - &secp256k1::Message::from_digest_slice(&msg)?, - &oracle_schnorr_kp, - ); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?), - value: Value::Explicit(available_settlement), - }], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_dcd_witness( - TokenBranch::Taker, - &DcdBranch::Settlement { - price_at_current_block_height: price, - oracle_sig: &sig, - amount_to_burn: filler_burn, - amount_to_get, - is_change_needed: true, - }, - MergeBranch::default(), - ); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Debug).is_ok(), - "expected success taker settlement (price > strike) -- dcd price attested" - ); - - Ok(()) - } - - #[test] - fn test_dcd_merge_2_tokens() -> Result<()> { - let now: u32 = u32::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())?; - - let oracle_kp = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[3u8; 32])?, - ); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); - let second_asset_id = AssetId::from_entropy(second_asset_entropy); - let third_asset_id = AssetId::from_entropy(third_asset_entropy); - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let dcd_arguments = DCDArguments::new( - now, - now, - now, - now, - 0, - strike_price, - incentive_basis_points, - 0, - AssetId::LIQUID_BTC.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args, - oracle_kp.x_only_public_key().0.to_string(), - "00".repeat(32), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - // 2 tokens to merge + 1 fee => 3 inputs - pst.add_input(Input::from_prevout(outpoint)); - pst.add_input(Input::from_prevout(outpoint)); - pst.add_input(Input::from_prevout(outpoint)); - - // 3 outputs required - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - 1, - AssetId::LIQUID_BTC, - None, - )); - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - 1, - AssetId::LIQUID_BTC, - None, - )); - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - 1, - AssetId::LIQUID_BTC, - None, - )); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1), - }, - ], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = - build_dcd_witness(TokenBranch::default(), &DcdBranch::Merge, MergeBranch::Two); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Debug).is_ok(), - "expected success merge 2 tokens -- dcd price attested" - ); - - Ok(()) - } - - #[test] - fn test_dcd_merge_3_tokens() -> Result<()> { - let now: u32 = u32::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())?; - - let oracle_kp = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[3u8; 32])?, - ); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); - let second_asset_id = AssetId::from_entropy(second_asset_entropy); - let third_asset_id = AssetId::from_entropy(third_asset_entropy); - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let dcd_arguments = DCDArguments::new( - now, - now, - now, - now, - 0, - strike_price, - incentive_basis_points, - 0, - AssetId::LIQUID_BTC.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args, - oracle_kp.x_only_public_key().0.to_string(), - "00".repeat(32), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - // 3 tokens to merge + 1 fee => 4 inputs - pst.add_input(Input::from_prevout(outpoint)); - pst.add_input(Input::from_prevout(outpoint)); - pst.add_input(Input::from_prevout(outpoint)); - pst.add_input(Input::from_prevout(outpoint)); - - // 3 outputs required - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - 1, - AssetId::LIQUID_BTC, - None, - )); - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - 1, - AssetId::LIQUID_BTC, - None, - )); - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - 1, - AssetId::LIQUID_BTC, - None, - )); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1), - }, - ], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_dcd_witness( - TokenBranch::default(), - &DcdBranch::Merge, - MergeBranch::Three, - ); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Debug).is_ok(), - "expected success merge 3 tokens -- dcd price attested" - ); - - Ok(()) - } - - #[test] - fn test_dcd_merge_4_tokens() -> Result<()> { - let now: u32 = u32::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())?; - - let oracle_kp = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[3u8; 32])?, - ); - - let outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 33); - - let first_asset_entropy = get_new_asset_entropy(&outpoint, [1; 32]); - let second_asset_entropy = get_new_asset_entropy(&outpoint, [2; 32]); - let third_asset_entropy = get_new_asset_entropy(&outpoint, [3; 32]); - - let first_asset_id = AssetId::from_entropy(first_asset_entropy); - let second_asset_id = AssetId::from_entropy(second_asset_entropy); - let third_asset_id = AssetId::from_entropy(third_asset_entropy); - - let strike_price = 10; - let incentive_basis_points = 1000; // 10% - let ratio_args = - DCDRatioArguments::build_from(1000, incentive_basis_points, strike_price, 10)?; - - let dcd_arguments = DCDArguments::new( - now, - now, - now, - now, - 0, - strike_price, - incentive_basis_points, - 0, - AssetId::LIQUID_BTC.to_string(), - LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - first_asset_id.to_string(), - second_asset_id.to_string(), - third_asset_id.to_string(), - ratio_args, - oracle_kp.x_only_public_key().0.to_string(), - "00".repeat(32), - ); - - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - let dcd_address = get_dcd_address(&keypair.x_only_public_key().0, &dcd_arguments, NETWORK)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - // 4 tokens to merge + 1 fee => 5 inputs - pst.add_input(Input::from_prevout(outpoint)); - pst.add_input(Input::from_prevout(outpoint)); - pst.add_input(Input::from_prevout(outpoint)); - pst.add_input(Input::from_prevout(outpoint)); - pst.add_input(Input::from_prevout(outpoint)); - - // 3 outputs required - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - 1, - AssetId::LIQUID_BTC, - None, - )); - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - 1, - AssetId::LIQUID_BTC, - None, - )); - pst.add_output(Output::new_explicit( - dcd_address.script_pubkey(), - 1, - AssetId::LIQUID_BTC, - None, - )); - - let program = get_compiled_dcd_program(&dcd_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1), - }, - ElementsUtxo { - script_pubkey: dcd_address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(1), - }, - ], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = - build_dcd_witness(TokenBranch::default(), &DcdBranch::Merge, MergeBranch::Four); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Debug).is_ok(), - "expected success merge 4 tokens -- dcd price attested" - ); - - Ok(()) - } -} diff --git a/crates/contracts/src/finance/dcd/source_simf/dual_currency_deposit.simf b/crates/contracts/src/finance/dcd/source_simf/dual_currency_deposit.simf deleted file mode 100644 index e1a460a..0000000 --- a/crates/contracts/src/finance/dcd/source_simf/dual_currency_deposit.simf +++ /dev/null @@ -1,592 +0,0 @@ -/* - * DCD: Dual Currency Deposit – price-attested settlement and funding windows - * - * Flows implemented: - * - Maker funding: deposit settlement asset and collateral, issue grantor tokens - * - Taker funding: deposit collateral in window and receive filler tokens - * - Settlement: at SETTLEMENT_HEIGHT, oracle Schnorr signature over (height, price) - * selects LBTC vs ALT branch based on price <= STRIKE_PRICE - * - Early/post-expiry termination: taker returns filler; maker burns grantor tokens - * - Merge: consolidate 2/3/4 token UTXOs - * - * All amounts and asset/script invariants are enforced on-chain; time guards use - * fallback locktime and height checks. - * - * Batching discussion: https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 - */ - -// Verify Schnorr signature against SHA256 of (u32 || u64) -fn checksig_priceblock(pk: Pubkey, current_block_height: u32, price_at_current_block_height: u64, sig: Signature) { - let hasher: Ctx8 = jet::sha_256_ctx_8_init(); - let hasher: Ctx8 = jet::sha_256_ctx_8_add_4(hasher, current_block_height); - let hasher: Ctx8 = jet::sha_256_ctx_8_add_8(hasher, price_at_current_block_height); - let msg: u256 = jet::sha_256_ctx_8_finalize(hasher); - jet::bip_0340_verify((pk, msg), sig); -} - -// Signed <= using XOR with 0x8000.. bias: a<=b (signed) iff (a^bias) <= (b^bias) (unsigned) -fn signed_le_u64(a_bits: u64, b_bits: u64) -> bool { - let bias: u64 = 0x8000000000000000; - jet::le_64(jet::xor_64(a_bits, bias), jet::xor_64(b_bits, bias)) -} - -fn signed_lt_u64(a: u64, b: u64) -> bool { - let bias: u64 = 0x8000000000000000; - jet::lt_64(jet::xor_64(a, bias), jet::xor_64(b, bias)) -} - -/// Assert: a == b * expected_q, via divmod -fn divmod_eq(a: u64, b: u64, expected_q: u64) { - let (q, r): (u64, u64) = jet::div_mod_64(a, b); - assert!(jet::eq_64(q, expected_q)); - assert!(jet::eq_64(r, 0)); -} - -/// Assert: base_amount * basis_point_percentage == provided_amount * MAX_BASIS_POINTS -fn constraint_percentage(base_amount: u64, basis_point_percentage: u64, provided_amount: u64) { - let MAX_BASIS_POINTS: u64 = 10000; - - let arg1: u256 = <(u128, u128)>::into((0, jet::multiply_64(base_amount, basis_point_percentage))); - let arg2: u256 = <(u128, u128)>::into((0, jet::multiply_64(provided_amount, MAX_BASIS_POINTS))); - - assert!(jet::eq_256(arg1, arg2)); -} - -fn get_output_script_hash(index: u32) -> u256 { - unwrap(jet::output_script_hash(index)) -} - -fn get_input_script_hash(index: u32) -> u256 { - unwrap(jet::input_script_hash(index)) -} - -fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } -fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } - -fn ensure_one_bit_or(bit1: bool, bit2: bool) { - assert!( - jet::eq_1( - ::into(jet::or_1(::into(bit1), ::into(bit2))), - 1 - ) - ); -} - -fn increment_by(index: u32, amount: u32) -> u32 { - let (carry, result): (bool, u32) = jet::add_32(index, amount); - ensure_zero_bit(carry); - result -} - -fn ensure_input_and_output_script_hash_eq(index: u32) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); -} - -fn ensure_output_is_op_return(index: u32) { - match jet::output_null_datum(index, 0) { - Some(entry: Option>>) => (), - None => panic!(), - } -} - -fn ensure_input_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::input_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::output_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { - let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); - assert!(jet::eq_256(asset, expected_bits)); - assert!(jet::eq_64(amount, expected_amount)); -} - -fn ensure_input_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected)); -} - -fn ensure_output_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); -} - -fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { - let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); - assert!(jet::eq_32(jet::current_index(), index)); - - match is_change_needed { - true => { - ensure_input_and_output_script_hash_eq(index); - - let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); - ensure_zero_bit(carry); - ensure_output_asset_with_amount_eq(index, asset_id, collateral_change); - }, - false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), - } -} - -fn merge_2_tokens() { - // 2 tokens to merge + 1 input as fee - assert!(jet::eq_32(jet::num_inputs(), 3)); - // 3 outputs: 1 merged token + 1 change + 1 fee - assert!(jet::eq_32(jet::num_outputs(), 3)); - assert!(jet::le_32(jet::current_index(), 1)); - - ensure_input_and_output_script_hash_eq(0); - let script_hash: u256 = get_input_script_hash(0); - assert!(jet::eq_256(script_hash, get_input_script_hash(1))); -} - -fn merge_3_tokens() { - // 3 tokens to merge + 1 input as fee - assert!(jet::eq_32(jet::num_inputs(), 4)); - // 3 outputs: 1 merged token + 1 change + 1 fee - assert!(jet::eq_32(jet::num_outputs(), 3)); - assert!(jet::le_32(jet::current_index(), 2)); - - ensure_input_and_output_script_hash_eq(0); - let script_hash: u256 = get_input_script_hash(0); - assert!(jet::eq_256(script_hash, get_input_script_hash(1))); - assert!(jet::eq_256(script_hash, get_input_script_hash(2))); -} - -fn merge_4_tokens() { - // 4 tokens to merge + 1 input as fee - assert!(jet::eq_32(jet::num_inputs(), 5)); - // 3 outputs: 1 merged token + 1 change + 1 fee - assert!(jet::eq_32(jet::num_outputs(), 3)); - assert!(jet::le_32(jet::current_index(), 3)); - - ensure_input_and_output_script_hash_eq(0); - let script_hash: u256 = get_input_script_hash(0); - assert!(jet::eq_256(script_hash, get_input_script_hash(1))); - assert!(jet::eq_256(script_hash, get_input_script_hash(2))); - assert!(jet::eq_256(script_hash, get_input_script_hash(3))); -} - -/* -* Maker funding path -* Params: -* 1. FILLER_PER_SETTLEMENT_COLLATERAL -* 2. FILLER_PER_SETTLEMENT_ASSET -* 3. FILLER_PER_PRINCIPAL_COLLATERAL -* 4. GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET -* 5. GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL -* 6. GRANTOR_PER_SETTLEMENT_COLLATERAL -* 7. GRANTOR_PER_SETTLEMENT_ASSET -*/ -fn maker_funding_path(principal_collateral_amount: u64, principal_asset_amount: u64, interest_collateral_amount: u64, interest_asset_amount: u64) { - assert!(jet::eq_32(jet::num_inputs(), 5)); - assert!(jet::eq_32(jet::num_outputs(), 11)); - - let current_time: u32 = ::into(jet::lock_time()); - assert!(jet::lt_32(current_time, param::TAKER_FUNDING_START_TIME)); - - ensure_input_and_output_script_hash_eq(0); - ensure_input_and_output_script_hash_eq(1); - ensure_input_and_output_script_hash_eq(2); - - assert!(jet::le_32(jet::current_index(), 2)); - - let script_hash: u256 = get_output_script_hash(0); - ensure_output_script_hash_eq(1, script_hash); - ensure_output_script_hash_eq(2, script_hash); - ensure_output_script_hash_eq(3, script_hash); - ensure_output_script_hash_eq(4, script_hash); - ensure_output_script_hash_eq(5, script_hash); - - let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(3); - let (settlement_asset_bits, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(4); - let filler_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); - let grantor_collateral_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); - let grantor_settlement_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(2)))); - assert!(jet::eq_64(filler_token_amount, grantor_collateral_token_amount)); - assert!(jet::eq_64(filler_token_amount, grantor_settlement_token_amount)); - - divmod_eq(principal_asset_amount, param::STRIKE_PRICE, principal_collateral_amount); - - assert!(jet::eq_64(collateral_amount, interest_collateral_amount)); - constraint_percentage(principal_collateral_amount, param::INCENTIVE_BASIS_POINTS, collateral_amount); - - let MAX_BASIS_POINTS: u64 = 10000; - let (carry, asset_incentive_percentage): (bool, u64) = jet::add_64(param::INCENTIVE_BASIS_POINTS, MAX_BASIS_POINTS); - ensure_zero_bit(carry); - - constraint_percentage(principal_asset_amount, asset_incentive_percentage, settlement_amount); - - let (carry, calculated_total_asset_amount): (bool, u64) = jet::add_64(principal_asset_amount, interest_asset_amount); - ensure_zero_bit(carry); - assert!(jet::eq_64(calculated_total_asset_amount, settlement_amount)); - - let (carry, calculated_total_collateral_amount): (bool, u64) = jet::add_64(principal_collateral_amount, interest_collateral_amount); - ensure_zero_bit(carry); - - // Filler token constraints - divmod_eq(calculated_total_collateral_amount, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_token_amount); - divmod_eq(calculated_total_asset_amount, param::FILLER_PER_SETTLEMENT_ASSET, filler_token_amount); - divmod_eq(principal_collateral_amount, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount); - - // Grantor token constraints - divmod_eq(calculated_total_asset_amount, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_token_amount); - divmod_eq(interest_collateral_amount, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_token_amount); - - divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_collateral_token_amount); - // divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_settlement_token_amount); // duplicated because of lines 203-204 - - divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_collateral_token_amount); - // divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_settlement_token_amount); // duplicated because of lines 203-204 - - assert!(jet::eq_256(param::COLLATERAL_ASSET_ID, collateral_asset_bits)); - assert!(jet::eq_256(param::SETTLEMENT_ASSET_ID, settlement_asset_bits)); - - ensure_output_asset_with_amount_eq(5, param::FILLER_TOKEN_ASSET, filler_token_amount); - ensure_output_asset_with_amount_eq(6, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_token_amount); - ensure_output_asset_with_amount_eq(7, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_token_amount); - - ensure_input_asset_eq(3, param::SETTLEMENT_ASSET_ID); - ensure_input_asset_eq(4, param::COLLATERAL_ASSET_ID); - - ensure_output_asset_eq(8, param::COLLATERAL_ASSET_ID); - ensure_output_asset_eq(9, param::SETTLEMENT_ASSET_ID); - ensure_output_asset_eq(10, param::COLLATERAL_ASSET_ID); -} - -fn taker_funding_path(collateral_amount_to_deposit: u64, filler_token_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - assert!(jet::le_32(param::TAKER_FUNDING_START_TIME, current_time)); - assert!(jet::lt_32(current_time, param::TAKER_FUNDING_END_TIME)); - assert!(jet::lt_32(current_time, param::CONTRACT_EXPIRY_TIME)); - - let filler_token_input_index: u32 = 0; - let collateral_input_index: u32 = 1; - - let (collateral_to_covenant_output_index, filler_to_user_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(filler_token_input_index); - - // Check and ensure filler token change - ensure_correct_change_at_index(0, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_deposit, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_get); - - // Ensure collateral asset and script hash are correct - ensure_output_asset_with_amount_eq(collateral_to_covenant_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_deposit); - ensure_output_script_hash_eq(collateral_to_covenant_output_index, expected_current_script_hash); - - ensure_output_asset_with_amount_eq(filler_to_user_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get); -} - -fn taker_early_termination_path(filler_token_amount_to_return: u64, collateral_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); - - let collateral_input_index: u32 = 0; - let filler_token_input_index: u32 = 1; - - let (return_filler_output_index, return_collateral_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_get, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_return); - - // Ensure filler token transferred to covenant - ensure_output_asset_with_amount_eq(return_filler_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_return); - ensure_output_script_hash_eq(return_filler_output_index, expected_current_script_hash); - - // Ensure collateral transferred to user - ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); -} - -fn maker_collateral_termination_path(grantor_collateral_amount_to_burn: u64, collateral_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); - - let collateral_input_index: u32 = 0; - let grantor_collateral_token_input_index: u32 = 1; - - let (burn_grantor_collateral_output_index, return_collateral_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_get, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_amount_to_burn); - - // Burn grantor collateral token - ensure_output_is_op_return(burn_grantor_collateral_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_amount_to_burn); - - // Ensure collateral transferred to user - ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); -} - -fn maker_settlement_termination_path(grantor_settlement_amount_to_burn: u64, settlement_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); - - let settlement_asset_input_index: u32 = 0; - let grantor_settlement_token_input_index: u32 = 1; - - let (burn_grantor_settlement_output_index, return_settlement_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset amount is correct - divmod_eq(settlement_amount_to_get, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_amount_to_burn); - - // Burn grantor settlement token - ensure_output_is_op_return(burn_grantor_settlement_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_amount_to_burn); - - // Ensure settlement asset transferred to user - ensure_output_asset_with_amount_eq(return_settlement_output_index, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get); -} - -fn ensure_correct_return_at(user_output_index: u32, asset_id: u256, amount_to_get: u64, fee_basis_points: u64) { - match jet::eq_64(fee_basis_points, 0) { - true => ensure_output_asset_with_amount_eq(user_output_index, asset_id, amount_to_get), - false => { - let fee_output_index: u32 = increment_by(user_output_index, 1); - - let (user_asset_bits, user_amount): (u256, u64) = get_output_explicit_asset_amount(user_output_index); - assert!(jet::eq_256(user_asset_bits, asset_id)); - - let (fee_asset_bits, fee_amount): (u256, u64) = get_output_explicit_asset_amount(fee_output_index); - assert!(jet::eq_256(fee_asset_bits, asset_id)); - - let (carry, calculated_total_amount): (bool, u64) = jet::add_64(user_amount, fee_amount); - ensure_zero_bit(carry); - - constraint_percentage(calculated_total_amount, fee_basis_points, fee_amount); - - ensure_output_script_hash_eq(fee_output_index, param::FEE_SCRIPT_HASH); - }, - }; -} - -fn maker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, grantor_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { - jet::check_lock_height(param::SETTLEMENT_HEIGHT); - checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); - - match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { - true => { - // Maker gets ALT - let settlement_asset_input_index: u32 = 0; - - let (burn_grantor_settlement_output_index, burn_grantor_collateral_output_index, settlement_output_index): (u32, u32, u32) = match is_change_needed { - true => (1, 2, 3), - false => (0, 1, 2), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset amount is correct - divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_amount_to_burn); - - // Burn grantor settlement and collateral tokens - ensure_output_is_op_return(burn_grantor_settlement_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); - ensure_output_is_op_return(burn_grantor_collateral_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); - - // Ensure settlement asset transferred to user - ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - false => { - // Maker gets the LBTC - let collateral_input_index: u32 = 0; - - let (burn_grantor_collateral_output_index, burn_grantor_settlement_output_index, collateral_output_index): (u32, u32, u32) = match is_change_needed { - true => (1, 2, 3), - false => (0, 1, 2), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_amount_to_burn); - - // Burn grantor collateral and settlement tokens - ensure_output_is_op_return(burn_grantor_collateral_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); - ensure_output_is_op_return(burn_grantor_settlement_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); - - // Ensure collateral transferred to user - ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - } -} - -fn taker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, filler_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { - jet::check_lock_height(param::SETTLEMENT_HEIGHT); - checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); - - match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { - true => { - // Taker receives LBTC principal+interest - let collateral_input_index: u32 = 0; - - let (burn_filler_output_index, collateral_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_amount_to_burn); - - // Burn filler token - ensure_output_is_op_return(burn_filler_output_index); - ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); - - // Ensure collateral transferred to user - ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - false => { - // Taker receives ALT - let settlement_asset_input_index: u32 = 0; - - let (burn_filler_output_index, settlement_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset amount is correct - divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_ASSET, filler_amount_to_burn); - - // Burn filler token - ensure_output_is_op_return(burn_filler_output_index); - ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); - - // Ensure filler token transferred to user - ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - } -} - -fn main() { - let token_branch: Either<(), ()> = witness::TOKEN_BRANCH; - let merge_branch: Either, ()> = witness::MERGE_BRANCH; - - match witness::PATH { - Left(funding_or_settlement: Either, (u64, Signature, u64, u64, bool)>) => match funding_or_settlement { - // Funding branches - Left(funding_params: Either<(u64, u64, u64, u64), (u64, u64, bool)>) => match funding_params { - // Maker funding: (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) - Left(params: (u64, u64, u64, u64)) => { - let (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount): (u64, u64, u64, u64) = params; - maker_funding_path(principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) - }, - // Taker funding: (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) - Right(params: (u64, u64, bool)) => { - let (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed): (u64, u64, bool) = params; - taker_funding_path(collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) - }, - }, - // Settlement branches (oracle price attested) - Right(params: (u64, Signature, u64, u64, bool)) => { - let (price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed): (u64, Signature, u64, u64, bool) = params; - - match token_branch { - // Maker settlement: burn grantor token - Left(u: ()) => maker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), - // Taker settlement: burn filler token - Right(u: ()) => taker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), - } - }, - }, - // Termination flows (early termination or post-expiry) or Merge flows - Right(termination_or_maker_or_merge: Either, ()>) => match termination_or_maker_or_merge { - Left(termination_or_maker: Either<(bool, u64, u64), (bool, u64, u64)>) => match termination_or_maker { - // Taker early termination: (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get) - Left(params: (bool, u64, u64)) => { - let (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get): (bool, u64, u64) = params; - taker_early_termination_path(filler_token_amount_to_return, collateral_amount_to_get, is_change_needed) - }, - // Maker termination (burn grantor token): choose collateral vs settlement token via token_branch - Right(params: (bool, u64, u64)) => { - let (is_change_needed, grantor_token_amount_to_burn, amount_to_get): (bool, u64, u64) = params; - - match token_branch { - // Burn grantor collateral token -> receive collateral - Left(u: ()) => maker_collateral_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), - // Burn grantor settlement token -> receive settlement asset - Right(u: ()) => maker_settlement_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), - } - }, - }, - Right(u: ()) => { - // Merge tokens based on MERGE_BRANCH discriminator - match merge_branch { - Left(left_or_right: Either<(), ()>) => match left_or_right { - Left(u: ()) => merge_2_tokens(), - Right(u: ()) => merge_3_tokens(), - }, - Right(u: ()) => merge_4_tokens(), - } - }, - }, - } - -} diff --git a/crates/contracts/src/finance/mod.rs b/crates/contracts/src/finance/mod.rs index daaee7d..77fa54e 100644 --- a/crates/contracts/src/finance/mod.rs +++ b/crates/contracts/src/finance/mod.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "finance-dcd")] -pub mod dcd; #[cfg(feature = "finance-option-offer")] pub mod option_offer; #[cfg(feature = "finance-options")] diff --git a/crates/contracts/src/finance/option_offer/build_arguments.rs b/crates/contracts/src/finance/option_offer/build_arguments.rs index 4817d5c..ecf5357 100644 --- a/crates/contracts/src/finance/option_offer/build_arguments.rs +++ b/crates/contracts/src/finance/option_offer/build_arguments.rs @@ -1,11 +1,14 @@ -use std::collections::HashMap; +#![allow(clippy::missing_errors_doc)] +use crate::error::FromArgumentsError; +use crate::utils::arguments_helpers::{extract_u32, extract_u64, extract_u256_bytes}; +use serde_json::Value; use simplicityhl::elements::AssetId; use simplicityhl::num::U256; use simplicityhl::{Arguments, str::WitnessName, value::UIntValue}; - -use crate::arguments_helpers::{extract_u32, extract_u64, extract_u256_bytes}; -use crate::error::FromArgumentsError; +use std::collections::HashMap; +use wallet_abi::WalletAbiError; +use wallet_abi::schema::values::SimfArguments; #[derive(Debug, Clone, bincode::Encode, bincode::Decode, PartialEq, Eq, Default)] pub struct OptionOfferArguments { @@ -90,6 +93,18 @@ impl OptionOfferArguments { ])) } + #[must_use] + pub fn build_simf_arguments(&self) -> SimfArguments { + SimfArguments { + resolved: self.build_arguments(), + runtime_arguments: HashMap::default(), + } + } + + pub fn to_json(&self) -> Result { + serde_json::to_value(self.build_arguments()).map_err(WalletAbiError::from) + } + /// Returns the collateral per contract amount. #[must_use] pub const fn collateral_per_contract(&self) -> u64 { @@ -150,13 +165,22 @@ impl OptionOfferArguments { /// /// Returns error if any required witness is missing, has wrong type, or has invalid value. pub fn from_arguments(args: &Arguments) -> Result { - let collateral_asset_id = extract_u256_bytes(args, "COLLATERAL_ASSET_ID")?; - let premium_asset_id = extract_u256_bytes(args, "PREMIUM_ASSET_ID")?; - let settlement_asset_id = extract_u256_bytes(args, "SETTLEMENT_ASSET_ID")?; - let collateral_per_contract = extract_u64(args, "COLLATERAL_PER_CONTRACT")?; - let premium_per_collateral = extract_u64(args, "PREMIUM_PER_COLLATERAL")?; - let expiry_time = extract_u32(args, "EXPIRY_TIME")?; - let user_pubkey = extract_u256_bytes(args, "USER_PUBKEY")?; + let collateral_asset_id_name = WitnessName::from_str_unchecked("COLLATERAL_ASSET_ID"); + let premium_asset_id_name = WitnessName::from_str_unchecked("PREMIUM_ASSET_ID"); + let settlement_asset_id_name = WitnessName::from_str_unchecked("SETTLEMENT_ASSET_ID"); + let collateral_per_contract_name = + WitnessName::from_str_unchecked("COLLATERAL_PER_CONTRACT"); + let premium_per_collateral_name = WitnessName::from_str_unchecked("PREMIUM_PER_COLLATERAL"); + let expiry_time_name = WitnessName::from_str_unchecked("EXPIRY_TIME"); + let user_pubkey_name = WitnessName::from_str_unchecked("USER_PUBKEY"); + + let collateral_asset_id = extract_u256_bytes(args, &collateral_asset_id_name)?; + let premium_asset_id = extract_u256_bytes(args, &premium_asset_id_name)?; + let settlement_asset_id = extract_u256_bytes(args, &settlement_asset_id_name)?; + let collateral_per_contract = extract_u64(args, &collateral_per_contract_name)?; + let premium_per_collateral = extract_u64(args, &premium_per_collateral_name)?; + let expiry_time = extract_u32(args, &expiry_time_name)?; + let user_pubkey = extract_u256_bytes(args, &user_pubkey_name)?; Ok(Self { collateral_asset_id, @@ -170,12 +194,24 @@ impl OptionOfferArguments { } } -impl simplicityhl_core::Encodable for OptionOfferArguments {} +impl wallet_abi::Encodable for OptionOfferArguments {} #[cfg(test)] mod tests { use super::*; - use simplicityhl_core::Encodable; + use wallet_abi::Encodable; + + fn make_full_args() -> anyhow::Result { + Ok(OptionOfferArguments::new( + AssetId::from_slice(&[1u8; 32])?, + AssetId::from_slice(&[2u8; 32])?, + AssetId::from_slice(&[3u8; 32])?, + 1000, + 100, + 1_700_000_000, + [4u8; 32], + )) + } #[test] fn test_serialize_deserialize_default() -> anyhow::Result<()> { @@ -185,26 +221,35 @@ mod tests { let deserialized = OptionOfferArguments::decode(&serialized)?; assert_eq!(args, deserialized); + assert_eq!(deserialized.build_arguments().iter().count(), 7); Ok(()) } #[test] fn test_serialize_deserialize_full() -> anyhow::Result<()> { - let args = OptionOfferArguments::new( - AssetId::from_slice(&[1u8; 32])?, - AssetId::from_slice(&[2u8; 32])?, - AssetId::from_slice(&[3u8; 32])?, - 1000, - 100, - 1_700_000_000, - [4u8; 32], - ); + let args = make_full_args()?; let serialized = args.encode()?; let deserialized = OptionOfferArguments::decode(&serialized)?; assert_eq!(args, deserialized); + assert_eq!(deserialized.collateral_per_contract(), 1000); + assert_eq!(deserialized.premium_per_collateral(), 100); + assert_eq!(deserialized.expiry_time(), 1_700_000_000); + assert_eq!(deserialized.user_pubkey(), [4u8; 32]); + assert_eq!( + deserialized.get_collateral_asset_id(), + AssetId::from_slice(&[1u8; 32])? + ); + assert_eq!( + deserialized.get_premium_asset_id(), + AssetId::from_slice(&[2u8; 32])? + ); + assert_eq!( + deserialized.get_settlement_asset_id(), + AssetId::from_slice(&[3u8; 32])? + ); Ok(()) } @@ -223,20 +268,16 @@ mod tests { #[test] fn test_arguments_roundtrip_full() -> anyhow::Result<()> { - let original = OptionOfferArguments::new( - AssetId::from_slice(&[1u8; 32])?, - AssetId::from_slice(&[2u8; 32])?, - AssetId::from_slice(&[3u8; 32])?, - 1000, - 100, - 1_700_000_000, - [4u8; 32], - ); + let original = make_full_args()?; let arguments = original.build_arguments(); let recovered = OptionOfferArguments::from_arguments(&arguments)?; assert_eq!(original, recovered); + assert_eq!(arguments.iter().count(), 7); + let simf_arguments = original.build_simf_arguments(); + assert!(simf_arguments.runtime_arguments.is_empty()); + assert_eq!(simf_arguments.resolved.iter().count(), 7); Ok(()) } diff --git a/crates/contracts/src/finance/option_offer/build_witness.rs b/crates/contracts/src/finance/option_offer/build_witness.rs index 04a8969..74e12cd 100644 --- a/crates/contracts/src/finance/option_offer/build_witness.rs +++ b/crates/contracts/src/finance/option_offer/build_witness.rs @@ -1,11 +1,12 @@ use std::collections::HashMap; +use simplicityhl::elements::bitcoin::XOnlyPublicKey; use simplicityhl::{ - ResolvedType, Value, WitnessValues, elements::secp256k1_zkp::schnorr::Signature, - parse::ParseFromStr, str::WitnessName, types::TypeConstructible, value::ValueConstructible, + ResolvedType, Value, WitnessValues, parse::ParseFromStr, str::WitnessName, + types::TypeConstructible, }; +use wallet_abi::schema::values::{RuntimeSimfWitness, SimfWitness}; -/// Represents the different execution paths for the option offer contract. #[derive(Debug, Clone)] pub enum OptionOfferBranch { /// Exercise path: counterparty swaps settlement asset for collateral + premium @@ -16,46 +17,45 @@ pub enum OptionOfferBranch { is_change_needed: bool, }, /// Withdraw path: user withdraws settlement asset - Withdraw { schnorr_signature: Signature }, + Withdraw, /// Expiry path: user reclaims collateral + premium after expiry - Expiry { schnorr_signature: Signature }, + Expiry, } -/// Build witness values for option offer program execution. +#[must_use] +/// Build runtime SIMF witness payload for option-offer branches. /// /// # Panics /// -/// Panics if type parsing fails (should never happen with valid constants). -#[must_use] -pub fn build_option_offer_witness(branch: &OptionOfferBranch) -> WitnessValues { +/// Panics if internal witness type/value parsing fails for static type descriptors. +pub fn build_option_offer_witness( + branch: &OptionOfferBranch, + to_sign_x_only: XOnlyPublicKey, +) -> SimfWitness { let exercise_type = ResolvedType::parse_from_str("(u64, bool)").unwrap(); - let signature_type = ResolvedType::parse_from_str("Signature").unwrap(); + let signature_type = ResolvedType::parse_from_str("()").unwrap(); let withdraw_or_expiry_type = ResolvedType::either(signature_type.clone(), signature_type); let path_type = ResolvedType::either(exercise_type, withdraw_or_expiry_type); - let branch_str = match branch { + let path = match branch { OptionOfferBranch::Exercise { collateral_amount, is_change_needed, } => { format!("Left(({collateral_amount}, {is_change_needed}))") } - OptionOfferBranch::Withdraw { schnorr_signature } => { - format!( - "Right(Left({}))", - Value::byte_array(schnorr_signature.serialize()) - ) - } - OptionOfferBranch::Expiry { schnorr_signature } => { - format!( - "Right(Right({}))", - Value::byte_array(schnorr_signature.serialize()) - ) - } + OptionOfferBranch::Withdraw => "Right(Left(()))".to_string(), + OptionOfferBranch::Expiry => "Right(Right(()))".to_string(), }; - simplicityhl::WitnessValues::from(HashMap::from([( - WitnessName::from_str_unchecked("PATH"), - simplicityhl::Value::parse_from_str(&branch_str, &path_type).unwrap(), - )])) + SimfWitness { + resolved: WitnessValues::from(HashMap::from([( + WitnessName::from_str_unchecked("PATH"), + Value::parse_from_str(&path, &path_type).unwrap(), + )])), + runtime_arguments: vec![RuntimeSimfWitness::SigHashAll { + name: "USER_SIGHASH_ALL".to_string(), + public_key: to_sign_x_only, + }], + } } diff --git a/crates/contracts/src/finance/option_offer/mod.rs b/crates/contracts/src/finance/option_offer/mod.rs index 913c9ee..d372cf3 100644 --- a/crates/contracts/src/finance/option_offer/mod.rs +++ b/crates/contracts/src/finance/option_offer/mod.rs @@ -1,26 +1,26 @@ -use crate::finance::option_offer::build_witness::{OptionOfferBranch, build_option_offer_witness}; +pub mod build_arguments; +pub mod build_witness; -use std::sync::Arc; +pub use build_arguments::OptionOfferArguments; -use simplicityhl_core::{ - ProgramError, SimplicityNetwork, control_block, create_p2tr_address, get_and_verify_env, - load_program, run_program, +use crate::option_offer::build_witness::{OptionOfferBranch, build_option_offer_witness}; + +use wallet_abi::runtime::WalletRuntimeConfig; +use wallet_abi::schema::tx_create::{TX_CREATE_ABI_VERSION, TxCreateRequest}; +use wallet_abi::schema::values::{serialize_arguments, serialize_witness}; +use wallet_abi::taproot_pubkey_gen::TaprootPubkeyGen; +use wallet_abi::{ + AssetVariant, BlinderVariant, FinalizerSpec, InputBlinder, InputSchema, LockVariant, Network, + OutputSchema, ProgramError, RuntimeParams, UTXOSource, WalletAbiError, create_p2tr_address, + load_program, }; -use simplicityhl::elements::{Address, Transaction, TxInWitness, TxOut}; +use simplicityhl::elements::{Address, LockTime, OutPoint, Sequence, Txid}; -use simplicityhl::simplicity::RedeemNode; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::{bitcoin::XOnlyPublicKey, jet::elements::ElementsEnv}; +use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; -use simplicityhl::tracker::TrackerLogLevel; use simplicityhl::{CompiledProgram, TemplateProgram}; -pub mod build_arguments; -pub mod build_witness; - -pub use build_arguments::OptionOfferArguments; - pub const OPTION_OFFER_SOURCE: &str = include_str!("source_simf/option_offer.simf"); /// Get the option offer template program for instantiation. @@ -42,7 +42,7 @@ pub fn get_option_offer_template_program() -> TemplateProgram { pub fn get_option_offer_address( x_only_public_key: &XOnlyPublicKey, arguments: &OptionOfferArguments, - network: SimplicityNetwork, + network: Network, ) -> Result { Ok(create_p2tr_address( get_option_offer_program(arguments)?.commit().cmr(), @@ -76,568 +76,565 @@ pub fn get_compiled_option_offer_program(arguments: &OptionOfferArguments) -> Co .unwrap() } -/// Execute option offer program for a given branch. -/// -/// # Errors -/// -/// Returns error if program execution fails. -pub fn execute_option_offer_program( - compiled_program: &CompiledProgram, - env: &ElementsEnv>, - branch: &OptionOfferBranch, - runner_log_level: TrackerLogLevel, -) -> Result>, ProgramError> { - let witness_values = build_option_offer_witness(branch); - - Ok(run_program(compiled_program, witness_values, env, runner_log_level)?.0) +pub struct OptionOfferRuntime { + runtime: WalletRuntimeConfig, + args: OptionOfferArguments, + tap: TaprootPubkeyGen, } -/// Finalize option offer transaction with Simplicity witness. -/// -/// # Errors -/// -/// Returns error if program execution fails or script pubkey doesn't match. -#[allow(clippy::too_many_arguments)] -pub fn finalize_option_offer_transaction( - mut tx: Transaction, - contract_public_key: &XOnlyPublicKey, - contract_program: &CompiledProgram, - utxos: &[TxOut], - input_index: usize, - branch: &OptionOfferBranch, - network: SimplicityNetwork, - log_level: TrackerLogLevel, -) -> Result { - let env = get_and_verify_env( - &tx, - contract_program, - contract_public_key, - utxos, - network, - input_index, - )?; - - let pruned = execute_option_offer_program(contract_program, &env, branch, log_level)?; - - let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); - let cmr = pruned.cmr(); - - tx.input[input_index].witness = TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, - script_witness: vec![ - simplicity_witness_bytes, - simplicity_program_bytes, - cmr.as_ref().to_vec(), - control_block(cmr, *contract_public_key).serialize(), - ], - pegin_witness: vec![], - }; - - Ok(tx) -} - -#[cfg(test)] -mod option_offer_tests { - use super::*; - - use anyhow::Result; - use simplicityhl::elements::confidential::{Asset, Value}; - use simplicityhl::simplicity::bitcoin::key::Keypair; - use simplicityhl::simplicity::bitcoin::secp256k1; - use simplicityhl::simplicity::bitcoin::secp256k1::Secp256k1; - use simplicityhl::simplicity::elements::{self, OutPoint, Txid}; - use simplicityhl::simplicity::hashes::Hash; - - use simplicityhl::elements::taproot::ControlBlock; - use simplicityhl::elements::{AssetId, Script}; - use simplicityhl::simplicity::jet::elements::ElementsUtxo; - use simplicityhl_core::{ - LIQUID_TESTNET_BITCOIN_ASSET, SimplicityNetwork, get_and_verify_env, get_p2pk_address, - }; - - use crate::sdk::{ - build_option_offer_deposit, build_option_offer_exercise, build_option_offer_expiry, - build_option_offer_withdraw, - }; - - const NETWORK: SimplicityNetwork = SimplicityNetwork::LiquidTestnet; - - fn get_test_arguments( - x_only_public_key: &XOnlyPublicKey, - expiry_time: u32, - ) -> OptionOfferArguments { - OptionOfferArguments::new( - AssetId::from_slice(&[1u8; 32]).unwrap(), - AssetId::from_slice(&[2u8; 32]).unwrap(), - *LIQUID_TESTNET_BITCOIN_ASSET, - 100, - 10, - expiry_time, - x_only_public_key.serialize(), - ) +impl OptionOfferRuntime { + /// Create runtime helper from resolved runtime config, arguments, and taproot handle. + #[must_use] + pub const fn new( + runtime: WalletRuntimeConfig, + args: OptionOfferArguments, + tap: TaprootPubkeyGen, + ) -> Self { + Self { runtime, args, tap } } - #[test] - fn test_sdk_deposit() -> Result<()> { - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - - let args = get_test_arguments(&keypair.x_only_public_key().0, 1_700_000_000); - - let collateral_deposit_amount = 1000u64; - let premium_deposit_amount = collateral_deposit_amount * args.premium_per_collateral(); - let fee_amount = 500u64; - - let (pst, _) = build_option_offer_deposit( - ( - OutPoint::new(Txid::from_slice(&[1; 32])?, 0), - TxOut { - asset: Asset::Explicit(args.get_collateral_asset_id()), - value: Value::Explicit(collateral_deposit_amount + 500), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::new(Txid::from_slice(&[2; 32])?, 0), - TxOut { - asset: Asset::Explicit(args.get_premium_asset_id()), - value: Value::Explicit(premium_deposit_amount + 500), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::new(Txid::from_slice(&[3; 32])?, 0), - TxOut { - asset: Asset::Explicit(NETWORK.policy_asset()), - value: Value::Explicit(fee_amount + 100), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - collateral_deposit_amount, - fee_amount, - &args, - NETWORK, - )?; - - let tx = pst.extract_tx()?; - - assert_eq!( - tx.output[0].asset, - Asset::Explicit(args.get_collateral_asset_id()) - ); - assert_eq!( - tx.output[0].value, - Value::Explicit(collateral_deposit_amount) - ); - - assert_eq!( - tx.output[1].asset, - Asset::Explicit(args.get_premium_asset_id()) - ); - assert_eq!(tx.output[1].value, Value::Explicit(premium_deposit_amount)); + /// Return immutable access to underlying wallet runtime. + #[must_use] + pub const fn runtime(&self) -> &WalletRuntimeConfig { + &self.runtime + } - Ok(()) + /// Return mutable access to underlying wallet runtime. + pub const fn runtime_mut(&mut self) -> &mut WalletRuntimeConfig { + &mut self.runtime } - #[test] - fn test_sdk_exercise_with_change() -> Result<()> { - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); + /// Return option-offer arguments used by this runtime. + #[must_use] + pub const fn args(&self) -> &OptionOfferArguments { + &self.args + } - let args = get_test_arguments(&keypair.x_only_public_key().0, 1_700_000_000); + /// Return taproot handle used by this runtime. + #[must_use] + pub const fn tap(&self) -> &TaprootPubkeyGen { + &self.tap + } - let program = get_compiled_option_offer_program(&args); + /// Compute premium amount from collateral amount. + /// + /// # Panics + /// + /// Panics if multiplication overflows `u64`. + pub const fn premium_amount(&self, collateral_amount: u64) -> u64 { + collateral_amount + .checked_mul(self.args.premium_per_collateral()) + .expect("premium amount overflow") + } - let covenant_address = - get_option_offer_address(&keypair.x_only_public_key().0, &args, NETWORK)?; + /// Compute settlement amount from collateral amount. + /// + /// # Panics + /// + /// Panics if multiplication overflows `u64`. + pub const fn settlement_amount(&self, collateral_amount: u64) -> u64 { + collateral_amount + .checked_mul(self.args.collateral_per_contract()) + .expect("settlement amount overflow") + } - let change_recipient = get_p2pk_address(&keypair.x_only_public_key().0, NETWORK)?; + fn get_base_finalizer_spec( + &self, + witness: &OptionOfferBranch, + ) -> Result { + Ok(FinalizerSpec::Simf { + source_simf: OPTION_OFFER_SOURCE.to_string(), + internal_key: Box::new(self.tap.clone()), + arguments: serialize_arguments(&self.args.build_simf_arguments())?, + witness: serialize_witness(&build_option_offer_witness( + witness, + self.runtime.signer_x_only_public_key()?, + ))?, + }) + } - let input_collateral_amount = 1000u64; - let input_premium_amount = input_collateral_amount * args.premium_per_collateral(); - let collateral_to_receive = 400u64; - let settlement_required = collateral_to_receive * args.collateral_per_contract(); - let fee_amount = 500u64; + /// Build the initial deposit transaction request. + /// + /// This constructor is currently infallible and always returns `Ok`. + /// + /// # Panics + /// + /// Panics if premium amount multiplication overflows `u64`. + pub fn build_deposit_request(&self, collateral_deposit_amount: u64) -> TxCreateRequest { + let premium_deposit_amount = self.premium_amount(collateral_deposit_amount); + + TxCreateRequest { + abi_version: TX_CREATE_ABI_VERSION.to_string(), + request_id: "request-option_offer.deposit".to_string(), + network: self.runtime.network, + params: RuntimeParams { + inputs: vec![InputSchema::new("input0"), InputSchema::new("input1")], + outputs: vec![ + OutputSchema::from_script( + "out0", + self.args.get_collateral_asset_id(), + collateral_deposit_amount, + self.tap.address.script_pubkey(), + ), + OutputSchema::from_script( + "out1", + self.args.get_premium_asset_id(), + premium_deposit_amount, + self.tap.address.script_pubkey(), + ), + ], + fee_rate_sat_vb: Some(0.1), + locktime: None, + }, + broadcast: true, + } + } - let (pst, branch) = build_option_offer_exercise( - ( - OutPoint::new(Txid::from_slice(&[1; 32])?, 0), - TxOut { - asset: Asset::Explicit(args.get_collateral_asset_id()), - value: Value::Explicit(input_collateral_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: covenant_address.script_pubkey(), - witness: elements::TxOutWitness::default(), + /// Build the exercise transaction request. + /// + /// # Errors + /// + /// Returns an error if covenant inputs are not explicit or if requested amounts exceed + /// available covenant balances. + /// + /// # Panics + /// + /// Panics if premium/settlement amount multiplication overflows `u64`. + #[expect(clippy::too_many_lines)] + pub async fn build_exercise_request( + &self, + creation_tx_id: Txid, + collateral_amount: u64, + ) -> Result { + let premium_amount = self.premium_amount(collateral_amount); + let settlement_amount = self.settlement_amount(collateral_amount); + + let collateral_outpoint = OutPoint::new(creation_tx_id, 0); + let collateral_tx_out = self.runtime.fetch_tx_out(&collateral_outpoint).await?; + let available_collateral = collateral_tx_out.value.explicit().ok_or_else(|| { + WalletAbiError::InvalidRequest( + "covenant collateral output must be explicit".to_string(), + ) + })?; + + let premium_outpoint = OutPoint::new(creation_tx_id, 1); + let premium_tx_out = self.runtime.fetch_tx_out(&premium_outpoint).await?; + let available_premium = premium_tx_out.value.explicit().ok_or_else(|| { + WalletAbiError::InvalidRequest("covenant premium output must be explicit".to_string()) + })?; + + let collateral_change = available_collateral + .checked_sub(collateral_amount) + .ok_or_else(|| { + WalletAbiError::InvalidRequest( + "requested collateral exceeds covenant collateral balance".to_string(), + ) + })?; + let premium_change = available_premium + .checked_sub(premium_amount) + .ok_or_else(|| { + WalletAbiError::InvalidRequest( + "requested premium exceeds covenant premium balance".to_string(), + ) + })?; + + let finalizer = self.get_base_finalizer_spec(&OptionOfferBranch::Exercise { + collateral_amount, + is_change_needed: collateral_change != 0, + })?; + + let receiver = self.runtime.signer_receive_address()?; + + let mut outputs: Vec = Vec::new(); + if collateral_change != 0 { + outputs.push(OutputSchema { + id: "covenant-collateral-change".to_string(), + amount_sat: collateral_change, + lock: LockVariant::Script { + script: self.tap.address.script_pubkey(), }, - ), - ( - OutPoint::new(Txid::from_slice(&[2; 32])?, 0), - TxOut { - asset: Asset::Explicit(args.get_premium_asset_id()), - value: Value::Explicit(input_premium_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: covenant_address.script_pubkey(), - witness: elements::TxOutWitness::default(), + asset: AssetVariant::AssetId { + asset_id: self.args.get_collateral_asset_id(), }, - ), - ( - OutPoint::new(Txid::from_slice(&[3; 32])?, 0), - TxOut { - asset: Asset::Explicit(args.get_settlement_asset_id()), - value: Value::Explicit(settlement_required + 1000), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), + blinder: BlinderVariant::Explicit, + }); + outputs.push(OutputSchema { + id: "covenant-premium-change".to_string(), + amount_sat: premium_change, + lock: LockVariant::Script { + script: self.tap.address.script_pubkey(), }, - ), - ( - OutPoint::new(Txid::from_slice(&[4; 32])?, 0), - TxOut { - asset: Asset::Explicit(NETWORK.policy_asset()), - value: Value::Explicit(fee_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), + asset: AssetVariant::AssetId { + asset_id: self.args.get_premium_asset_id(), }, - ), - collateral_to_receive, - fee_amount, - &args, - change_recipient.script_pubkey(), - )?; - - let tx = pst.extract_tx()?; - - let env = ElementsEnv::new( - Arc::new(tx), - vec![ - ElementsUtxo { - script_pubkey: covenant_address.script_pubkey(), - asset: Asset::Explicit(args.get_collateral_asset_id()), - value: Value::Explicit(input_collateral_amount), + blinder: BlinderVariant::Explicit, + }); + } + + outputs.extend(vec![ + OutputSchema { + id: "covenant-settlement-change".to_string(), + amount_sat: settlement_amount, + lock: LockVariant::Script { + script: self.tap.address.script_pubkey(), }, - ElementsUtxo { - script_pubkey: covenant_address.script_pubkey(), - asset: Asset::Explicit(args.get_premium_asset_id()), - value: Value::Explicit(input_premium_amount), + asset: AssetVariant::AssetId { + asset_id: self.args.get_settlement_asset_id(), }, - ], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_option_offer_witness(&branch); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Trace).is_ok(), - "expected success exercise path with change" - ); - - Ok(()) - } - - #[test] - fn test_sdk_exercise_without_change() -> Result<()> { - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - - let args = get_test_arguments(&keypair.x_only_public_key().0, 1_700_000_000); - let program = get_compiled_option_offer_program(&args); - - let change_recipient = get_p2pk_address(&keypair.x_only_public_key().0, NETWORK)?; - - let covenant_address = - get_option_offer_address(&keypair.x_only_public_key().0, &args, NETWORK)?; - - let input_collateral_amount = 1000u64; - let input_premium_amount = input_collateral_amount * args.premium_per_collateral(); - let collateral_to_receive = 1000u64; - let settlement_required = collateral_to_receive * args.collateral_per_contract(); - let fee_amount = 500u64; - - let (pst, branch) = build_option_offer_exercise( - ( - OutPoint::new(Txid::from_slice(&[1; 32])?, 0), - TxOut { - asset: Asset::Explicit(args.get_collateral_asset_id()), - value: Value::Explicit(input_collateral_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: covenant_address.script_pubkey(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::new(Txid::from_slice(&[2; 32])?, 0), - TxOut { - asset: Asset::Explicit(args.get_premium_asset_id()), - value: Value::Explicit(input_premium_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: covenant_address.script_pubkey(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::new(Txid::from_slice(&[3; 32])?, 0), - TxOut { - asset: Asset::Explicit(args.get_settlement_asset_id()), - value: Value::Explicit(settlement_required), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "user-collateral-requested".to_string(), + amount_sat: collateral_amount, + lock: LockVariant::Script { + script: receiver.script_pubkey(), }, - ), - ( - OutPoint::new(Txid::from_slice(&[4; 32])?, 0), - TxOut { - asset: Asset::Explicit(NETWORK.policy_asset()), - value: Value::Explicit(fee_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), + asset: AssetVariant::AssetId { + asset_id: self.args.get_collateral_asset_id(), }, - ), - collateral_to_receive, - fee_amount, - &args, - change_recipient.script_pubkey(), - )?; - - let tx = pst.extract_tx()?; - - let env = ElementsEnv::new( - Arc::new(tx), - vec![ - ElementsUtxo { - script_pubkey: covenant_address.script_pubkey(), - asset: Asset::Explicit(args.get_collateral_asset_id()), - value: Value::Explicit(input_collateral_amount), + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "user-premium-requested".to_string(), + amount_sat: premium_amount, + lock: LockVariant::Script { + script: receiver.script_pubkey(), }, - ElementsUtxo { - script_pubkey: covenant_address.script_pubkey(), - asset: Asset::Explicit(args.get_premium_asset_id()), - value: Value::Explicit(input_premium_amount), + asset: AssetVariant::AssetId { + asset_id: self.args.get_premium_asset_id(), }, - ], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_option_offer_witness(&branch); + blinder: BlinderVariant::Explicit, + }, + ]); + + Ok(TxCreateRequest { + abi_version: TX_CREATE_ABI_VERSION.to_string(), + request_id: "request-option_offer.exercise".to_string(), + network: self.runtime.network, + params: RuntimeParams { + inputs: vec![ + InputSchema { + id: "input0".to_string(), + utxo_source: UTXOSource::Provided { + outpoint: OutPoint::new(creation_tx_id, 0), + }, + blinder: InputBlinder::Explicit, + sequence: Sequence::default(), + issuance: None, + finalizer: finalizer.clone(), + }, + InputSchema { + id: "input1".to_string(), + utxo_source: UTXOSource::Provided { + outpoint: OutPoint::new(creation_tx_id, 1), + }, + blinder: InputBlinder::Explicit, + sequence: Sequence::default(), + issuance: None, + finalizer, + }, + InputSchema::new("input2"), + ], + outputs, + fee_rate_sat_vb: Some(0.1), + locktime: None, + }, + broadcast: true, + }) + } - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Trace).is_ok(), - "expected success exercise path without change" - ); + /// Build the withdraw transaction request for an explicit covenant settlement outpoint. + /// + /// Settlement output index is not fixed. Resolve the concrete settlement outpoint from the + /// actual exercise transaction outputs (exercise with change can shift `vout` positions). + /// + /// # Errors + /// + /// Returns an error if runtime-derived metadata cannot be serialized. + pub fn build_withdraw_request_for_outpoint( + &self, + settlement_outpoint: OutPoint, + settlement_amount: u64, + ) -> Result { + let finalizer = self.get_base_finalizer_spec(&OptionOfferBranch::Withdraw)?; + + let receiver = self.runtime.signer_receive_address()?; + + Ok(TxCreateRequest { + abi_version: TX_CREATE_ABI_VERSION.to_string(), + request_id: "request-option_offer.withdraw".to_string(), + network: self.runtime.network, + params: RuntimeParams { + inputs: vec![InputSchema { + id: "input0".to_string(), + utxo_source: UTXOSource::Provided { + outpoint: settlement_outpoint, + }, + blinder: InputBlinder::Explicit, + sequence: Sequence::default(), + issuance: None, + finalizer, + }], + outputs: vec![OutputSchema::from_script( + "out0", + self.args.get_settlement_asset_id(), + settlement_amount, + receiver.script_pubkey(), + )], + fee_rate_sat_vb: Some(0.1), + locktime: None, + }, + broadcast: true, + }) + } - Ok(()) + /// Build the expiry transaction request. + /// + /// # Errors + /// + /// Returns an error if locktime conversion or runtime metadata serialization fails. + pub fn build_expiry_request( + &self, + creation_tx_id: Txid, + collateral_amount: u64, + premium_amount: u64, + ) -> Result { + let finalizer = self.get_base_finalizer_spec(&OptionOfferBranch::Expiry)?; + + let receiver = self.runtime.signer_receive_address()?; + + Ok(TxCreateRequest { + abi_version: TX_CREATE_ABI_VERSION.to_string(), + request_id: "request-option_offer.expiry".to_string(), + network: self.runtime.network, + params: RuntimeParams { + inputs: vec![ + InputSchema { + id: "input0".to_string(), + utxo_source: UTXOSource::Provided { + outpoint: OutPoint::new(creation_tx_id, 0), + }, + blinder: InputBlinder::Explicit, + sequence: Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: finalizer.clone(), + }, + InputSchema { + id: "input1".to_string(), + utxo_source: UTXOSource::Provided { + outpoint: OutPoint::new(creation_tx_id, 1), + }, + blinder: InputBlinder::Explicit, + sequence: Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer, + }, + ], + outputs: vec![ + OutputSchema::from_script( + "out0", + self.args.get_collateral_asset_id(), + collateral_amount, + receiver.script_pubkey(), + ), + OutputSchema::from_script( + "out1", + self.args.get_premium_asset_id(), + premium_amount, + receiver.script_pubkey(), + ), + ], + fee_rate_sat_vb: Some(0.1), + locktime: Some(LockTime::from_time(self.args.expiry_time())?), + }, + broadcast: true, + }) } +} - #[test] - fn test_sdk_withdraw() -> Result<()> { - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); +#[cfg(test)] +mod test { + use super::*; - let args = get_test_arguments(&keypair.x_only_public_key().0, 1_700_000_000); - let program = get_compiled_option_offer_program(&args); + use crate::utils::test_setup::{ + RuntimeFundingAsset, ensure_node_running, fund_runtime, get_esplora_url, mine_blocks, + wallet_data_root, + }; - let change_recipient = get_p2pk_address(&keypair.x_only_public_key().0, NETWORK)?; + use anyhow::anyhow; + use simplicityhl::elements::{OutPoint, Txid}; - let covenant_address = - get_option_offer_address(&keypair.x_only_public_key().0, &args, NETWORK)?; + use wallet_abi::runtime::WalletRuntimeConfig; + use wallet_abi::schema::tx_create::TxCreateRequest; + use wallet_abi::taproot_pubkey_gen::TaprootPubkeyGen; - let settlement_amount = 50000u64; - let fee_amount = 500u64; + const COLLATERAL_PER_CONTRACT: u64 = 100; + const PREMIUM_PER_COLLATERAL: u64 = 10; + const EXPIRY_TIME: u32 = 1_700_000_000; - let pst = build_option_offer_withdraw( - ( - OutPoint::new(Txid::from_slice(&[1; 32])?, 0), - TxOut { - asset: Asset::Explicit(args.get_settlement_asset_id()), - value: Value::Explicit(settlement_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: covenant_address.script_pubkey(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::new(Txid::from_slice(&[2; 32])?, 0), - TxOut { - asset: Asset::Explicit(NETWORK.policy_asset()), - value: Value::Explicit(fee_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - fee_amount, - &args, - change_recipient.script_pubkey(), + fn setup() -> anyhow::Result { + ensure_node_running()?; + + let runtime_config = WalletRuntimeConfig::build_random( + Network::LocaltestLiquid, + &get_esplora_url()?, + wallet_data_root(), )?; - let tx = pst.extract_tx()?; + let collateral_funding = fund_runtime(&runtime_config, RuntimeFundingAsset::Lbtc)?; + let premium_funding = fund_runtime(&runtime_config, RuntimeFundingAsset::NewAsset)?; + let settlement_funding = fund_runtime(&runtime_config, RuntimeFundingAsset::NewAsset)?; + + let args = OptionOfferArguments::new( + collateral_funding.funded_asset_id, + premium_funding.funded_asset_id, + settlement_funding.funded_asset_id, + COLLATERAL_PER_CONTRACT, + PREMIUM_PER_COLLATERAL, + EXPIRY_TIME, + runtime_config.signer_x_only_public_key()?.serialize(), + ); + let tap = + TaprootPubkeyGen::from(&args, Network::LocaltestLiquid, &get_option_offer_address)?; + + Ok(OptionOfferRuntime { + runtime: runtime_config, + args, + tap, + }) + } - let utxos = vec![ - TxOut { - asset: Asset::Explicit(args.get_settlement_asset_id()), - value: Value::Explicit(settlement_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: covenant_address.script_pubkey(), - witness: elements::TxOutWitness::default(), - }, - TxOut { - asset: Asset::Explicit(NETWORK.policy_asset()), - value: Value::Explicit(fee_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ]; - - let env = get_and_verify_env( - &tx, - &program, - &keypair.x_only_public_key().0, - &utxos, - NETWORK, - 0, - )?; + async fn assert_broadcast_happy_path( + runtime: &mut OptionOfferRuntime, + request: &TxCreateRequest, + ) -> anyhow::Result { + let response = runtime.runtime.process_request(request).await?; + + let Some(tx_info) = response.transaction else { + panic!("Expected a response broadcast info"); + }; - let sighash_all = env.c_tx_env().sighash_all(); - let signature = - keypair.sign_schnorr(secp256k1::Message::from_digest(sighash_all.to_byte_array())); + Ok(tx_info.txid) + } - let branch = OptionOfferBranch::Withdraw { - schnorr_signature: signature, + async fn find_settlement_outpoint( + runtime: &OptionOfferRuntime, + exercise_tx_id: Txid, + ) -> anyhow::Result<(OutPoint, u64)> { + let tx = { + let inner_esplora = runtime.runtime.esplora.lock().await; + inner_esplora.get_transaction(exercise_tx_id).await? }; - let witness_values = build_option_offer_witness(&branch); + let covenant_script = runtime.tap.address.script_pubkey(); + let settlement_asset_id = runtime.args.get_settlement_asset_id(); + + let Some((vout, value_sat)) = tx.output.iter().enumerate().find_map(|(vout, tx_out)| { + if tx_out.script_pubkey != covenant_script { + return None; + } + + let asset = tx_out.asset.explicit()?; + if asset != settlement_asset_id { + return None; + } + + let value_sat = tx_out.value.explicit()?; + Some((vout, value_sat)) + }) else { + return Err(anyhow!( + "exercise tx {exercise_tx_id} does not contain explicit settlement output for covenant script" + )); + }; - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Trace).is_ok(), - "expected success withdraw path" - ); + Ok(( + OutPoint::new( + exercise_tx_id, + u32::try_from(vout).map_err(|_| anyhow!("exercise vout index overflow"))?, + ), + value_sat, + )) + } + #[tokio::test] + async fn test_option_offer_deposit() -> anyhow::Result<()> { + let mut fixture = setup()?; + let request = fixture.build_deposit_request(1_000u64); + let _ = assert_broadcast_happy_path(&mut fixture, &request).await?; Ok(()) } - #[test] - fn test_sdk_expiry() -> Result<()> { - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - - let args = get_test_arguments(&keypair.x_only_public_key().0, 1_700_000_000); - let program = get_compiled_option_offer_program(&args); + #[tokio::test] + async fn test_option_offer_exercise() -> anyhow::Result<()> { + let mut fixture = setup()?; + let collateral_amount = 1_000u64; + let request = fixture.build_deposit_request(collateral_amount); + let creation_tx_id = assert_broadcast_happy_path(&mut fixture, &request).await?; + mine_blocks(1)?; + + let request = fixture + .build_exercise_request(creation_tx_id, collateral_amount) + .await?; + let _ = assert_broadcast_happy_path(&mut fixture, &request).await?; + Ok(()) + } - let change_recipient = get_p2pk_address(&keypair.x_only_public_key().0, NETWORK)?; + #[tokio::test] + async fn test_option_offer_exercise_with_change() -> anyhow::Result<()> { + let mut fixture = setup()?; + let collateral_amount = 1_000u64; + let request = fixture.build_deposit_request(collateral_amount); + let creation_tx_id = assert_broadcast_happy_path(&mut fixture, &request).await?; + mine_blocks(1)?; + + let request = fixture + .build_exercise_request(creation_tx_id, collateral_amount - 500) + .await?; + let _ = assert_broadcast_happy_path(&mut fixture, &request).await?; + Ok(()) + } - let covenant_address = - get_option_offer_address(&keypair.x_only_public_key().0, &args, NETWORK)?; + #[tokio::test] + async fn test_option_offer_withdraw() -> anyhow::Result<()> { + let mut fixture = setup()?; + let collateral_amount = 1_000u64; + let exercise_collateral_amount = collateral_amount - 500; + let expected_settlement_amount = fixture.settlement_amount(exercise_collateral_amount); - let collateral_amount = 1000u64; - let premium_amount = collateral_amount * args.premium_per_collateral(); - let fee_amount = 500u64; + let deposit_request = fixture.build_deposit_request(collateral_amount); + let creation_tx_id = assert_broadcast_happy_path(&mut fixture, &deposit_request).await?; + mine_blocks(1)?; - let pst = build_option_offer_expiry( - ( - OutPoint::new(Txid::from_slice(&[1; 32])?, 0), - TxOut { - asset: Asset::Explicit(args.get_collateral_asset_id()), - value: Value::Explicit(collateral_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: covenant_address.script_pubkey(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::new(Txid::from_slice(&[2; 32])?, 0), - TxOut { - asset: Asset::Explicit(args.get_premium_asset_id()), - value: Value::Explicit(premium_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: covenant_address.script_pubkey(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::new(Txid::from_slice(&[3; 32])?, 0), - TxOut { - asset: Asset::Explicit(NETWORK.policy_asset()), - value: Value::Explicit(fee_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - fee_amount, - &args, - change_recipient.script_pubkey(), - )?; + let exercise_request = fixture + .build_exercise_request(creation_tx_id, exercise_collateral_amount) + .await?; + let exercise_tx_id = assert_broadcast_happy_path(&mut fixture, &exercise_request).await?; + mine_blocks(1)?; - let tx = pst.extract_tx()?; + let (settlement_outpoint, settlement_amount) = + find_settlement_outpoint(&fixture, exercise_tx_id).await?; + assert_eq!(settlement_amount, expected_settlement_amount); - let utxos = vec![ - TxOut { - asset: Asset::Explicit(args.get_collateral_asset_id()), - value: Value::Explicit(collateral_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: covenant_address.script_pubkey(), - witness: elements::TxOutWitness::default(), - }, - TxOut { - asset: Asset::Explicit(args.get_premium_asset_id()), - value: Value::Explicit(premium_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: covenant_address.script_pubkey(), - witness: elements::TxOutWitness::default(), - }, - ]; - - let env = get_and_verify_env( - &tx, - &program, - &keypair.x_only_public_key().0, - &utxos, - NETWORK, - 0, - )?; + let request = + fixture.build_withdraw_request_for_outpoint(settlement_outpoint, settlement_amount)?; + let _ = assert_broadcast_happy_path(&mut fixture, &request).await?; - let sighash_all = env.c_tx_env().sighash_all(); - let signature = - keypair.sign_schnorr(secp256k1::Message::from_digest(sighash_all.to_byte_array())); + Ok(()) + } - let branch = OptionOfferBranch::Expiry { - schnorr_signature: signature, - }; + #[tokio::test] + async fn test_option_offer_expiry() -> anyhow::Result<()> { + let mut fixture = setup()?; + let collateral_amount = 1_000u64; + let premium_amount = fixture.premium_amount(collateral_amount); - let witness_values = build_option_offer_witness(&branch); + let deposit_request = fixture.build_deposit_request(collateral_amount); + let creation_tx_id = assert_broadcast_happy_path(&mut fixture, &deposit_request).await?; + mine_blocks(1)?; - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::Trace).is_ok(), - "expected success expiry path" - ); + let request = + fixture.build_expiry_request(creation_tx_id, collateral_amount, premium_amount)?; + let _ = assert_broadcast_happy_path(&mut fixture, &request).await?; Ok(()) } diff --git a/crates/contracts/src/finance/option_offer/source_simf/option_offer.simf b/crates/contracts/src/finance/option_offer/source_simf/option_offer.simf index 5cb2108..4f9d9bd 100644 --- a/crates/contracts/src/finance/option_offer/source_simf/option_offer.simf +++ b/crates/contracts/src/finance/option_offer/source_simf/option_offer.simf @@ -200,14 +200,16 @@ fn expiry_path(sig: Signature) { } 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(sig: Signature) => withdraw_path(sig), - Right(sig: Signature) => expiry_path(sig), + 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/finance/options/build_arguments.rs b/crates/contracts/src/finance/options/build_arguments.rs index 123e513..d04d7e4 100644 --- a/crates/contracts/src/finance/options/build_arguments.rs +++ b/crates/contracts/src/finance/options/build_arguments.rs @@ -1,79 +1,37 @@ use std::collections::HashMap; -use simplicityhl::elements::hashes::Hash; -use simplicityhl::elements::{AssetId, ContractHash, OutPoint, Txid}; -use simplicityhl::num::U256; -use simplicityhl::{Arguments, str::WitnessName, value::UIntValue}; - -use crate::arguments_helpers::{extract_bool, extract_u32, extract_u64, extract_u256_bytes}; use crate::error::FromArgumentsError; +use crate::utils::arguments_helpers::{extract_u32, extract_u64, extract_u256_bytes}; -#[derive(Debug, Clone, bincode::Encode, bincode::Decode, PartialEq, Eq, Default)] -pub struct OutPointWithConfidential { - txid: [u8; 32], - vout: u32, - confidential: bool, -} - -impl OutPointWithConfidential { - #[must_use] - pub fn new(outpoint: OutPoint, confidential: bool) -> Self { - Self { - txid: outpoint.txid.to_byte_array(), - vout: outpoint.vout, - confidential, - } - } - - /// Convert back to `OutPoint`. - /// - /// # Panics - /// - /// Panics if the stored txid bytes are invalid (should never happen). - #[must_use] - pub fn outpoint(&self) -> OutPoint { - OutPoint::new(Txid::from_slice(&self.txid).unwrap(), self.vout) - } - - /// Returns the confidential flag. - #[must_use] - pub const fn confidential(&self) -> bool { - self.confidential - } - - /// Returns as tuple (`OutPoint`, bool). - #[must_use] - pub fn as_tuple(&self) -> (OutPoint, bool) { - (self.outpoint(), self.confidential) - } -} - -impl From<(OutPoint, bool)> for OutPointWithConfidential { - fn from((outpoint, confidential): (OutPoint, bool)) -> Self { - Self::new(outpoint, confidential) - } -} +use simplicityhl::elements::AssetId; +use simplicityhl::num::U256; +use simplicityhl::{Arguments, str::WitnessName, value::UIntValue}; #[derive(Debug, Clone, bincode::Encode, bincode::Decode, PartialEq, Eq, Default)] pub struct OptionsArguments { + /// Unix timestamp (seconds) when exercise/settlement becomes valid. start_time: u32, + /// Unix timestamp (seconds) when expiry path becomes valid. expiry_time: u32, + /// Collateral units locked per option contract. collateral_per_contract: u64, + /// Settlement units paid per option contract. settlement_per_contract: u64, + /// Collateral asset ID committed in covenant parameters. collateral_asset_id: [u8; 32], + /// Settlement asset ID committed in covenant parameters. settlement_asset_id: [u8; 32], - issuance_asset_entropy: [u8; 32], - option_creation_outpoint: OutPointWithConfidential, - grantor_creation_outpoint: OutPointWithConfidential, + /// Option token asset ID committed in covenant parameters. + option_token_asset: [u8; 32], + /// Option reissuance token ID committed in covenant parameters. + option_reissuance_token_asset: [u8; 32], + /// Grantor token asset ID committed in covenant parameters. + grantor_token_asset: [u8; 32], + /// Grantor reissuance token ID committed in covenant parameters. + grantor_reissuance_token_asset: [u8; 32], } impl OptionsArguments { - /// Create new options arguments. - /// - /// # Arguments - /// - /// * `option_creation_outpoint` - Tuple of (`OutPoint`, `confidential_flag`) for option token - /// * `grantor_creation_outpoint` - Tuple of (`OutPoint`, `confidential_flag`) for grantor token #[allow(clippy::too_many_arguments)] #[must_use] pub fn new( @@ -83,9 +41,10 @@ impl OptionsArguments { settlement_per_contract: u64, collateral_asset_id: AssetId, settlement_asset_id: AssetId, - issuance_asset_entropy: [u8; 32], - option_creation_outpoint: (OutPoint, bool), - grantor_creation_outpoint: (OutPoint, bool), + option_token_asset: AssetId, + option_reissuance_token_asset: AssetId, + grantor_token_asset: AssetId, + grantor_reissuance_token_asset: AssetId, ) -> Self { Self { start_time, @@ -94,125 +53,81 @@ impl OptionsArguments { settlement_per_contract, collateral_asset_id: collateral_asset_id.into_inner().0, settlement_asset_id: settlement_asset_id.into_inner().0, - issuance_asset_entropy, - option_creation_outpoint: option_creation_outpoint.into(), - grantor_creation_outpoint: grantor_creation_outpoint.into(), + option_token_asset: option_token_asset.into_inner().0, + option_reissuance_token_asset: option_reissuance_token_asset.into_inner().0, + grantor_token_asset: grantor_token_asset.into_inner().0, + grantor_reissuance_token_asset: grantor_reissuance_token_asset.into_inner().0, } } - /// Returns the start time. #[must_use] pub const fn start_time(&self) -> u32 { self.start_time } - /// Returns the expiry time. #[must_use] pub const fn expiry_time(&self) -> u32 { self.expiry_time } - /// Returns the collateral per contract amount. #[must_use] pub const fn collateral_per_contract(&self) -> u64 { self.collateral_per_contract } - /// Returns the settlement per contract amount. #[must_use] pub const fn settlement_per_contract(&self) -> u64 { self.settlement_per_contract } - /// Returns the issuance asset entropy. - #[must_use] - pub const fn issuance_asset_entropy(&self) -> [u8; 32] { - self.issuance_asset_entropy - } - - /// Returns the option creation outpoint with confidential flag. - #[must_use] - pub fn option_creation_outpoint(&self) -> (OutPoint, bool) { - self.option_creation_outpoint.as_tuple() - } - - /// Returns the grantor creation outpoint with confidential flag. - #[must_use] - pub fn grantor_creation_outpoint(&self) -> (OutPoint, bool) { - self.grantor_creation_outpoint.as_tuple() - } - - /// Computes and returns the option token entropy. - #[must_use] - pub fn option_token_entropy(&self) -> [u8; 32] { - let contract_hash = ContractHash::from_byte_array(self.issuance_asset_entropy); - AssetId::generate_asset_entropy(self.option_creation_outpoint.outpoint(), contract_hash).0 - } - - /// Computes and returns the grantor token entropy. - #[must_use] - pub fn grantor_token_entropy(&self) -> [u8; 32] { - let contract_hash = ContractHash::from_byte_array(self.issuance_asset_entropy); - AssetId::generate_asset_entropy(self.grantor_creation_outpoint.outpoint(), contract_hash).0 - } - - /// Computes and returns the option token asset ID. #[must_use] + /// # Panics + /// + /// Panics if internal bytes are not a valid `AssetId`. pub fn option_token(&self) -> AssetId { - let entropy = simplicityhl::elements::hashes::sha256::Midstate::from_byte_array( - self.option_token_entropy(), - ); - AssetId::from_entropy(entropy) + AssetId::from_slice(&self.option_token_asset) + .expect("option_token_asset must be a valid 32-byte asset id") } - /// Computes and returns the option reissuance token asset ID. #[must_use] + /// # Panics + /// + /// Panics if internal bytes are not a valid `AssetId`. pub fn option_reissuance_token(&self) -> AssetId { - let entropy = simplicityhl::elements::hashes::sha256::Midstate::from_byte_array( - self.option_token_entropy(), - ); - AssetId::reissuance_token_from_entropy( - entropy, - self.option_creation_outpoint.confidential(), - ) + AssetId::from_slice(&self.option_reissuance_token_asset) + .expect("option_reissuance_token_asset must be a valid 32-byte asset id") } - /// Computes and returns the grantor token asset ID. #[must_use] + /// # Panics + /// + /// Panics if internal bytes are not a valid `AssetId`. pub fn grantor_token(&self) -> AssetId { - let entropy = simplicityhl::elements::hashes::sha256::Midstate::from_byte_array( - self.grantor_token_entropy(), - ); - AssetId::from_entropy(entropy) + AssetId::from_slice(&self.grantor_token_asset) + .expect("grantor_token_asset must be a valid 32-byte asset id") } - /// Computes and returns the grantor reissuance token asset ID. #[must_use] + /// # Panics + /// + /// Panics if internal bytes are not a valid `AssetId`. pub fn grantor_reissuance_token(&self) -> AssetId { - let entropy = simplicityhl::elements::hashes::sha256::Midstate::from_byte_array( - self.grantor_token_entropy(), - ); - AssetId::reissuance_token_from_entropy( - entropy, - self.grantor_creation_outpoint.confidential(), - ) + AssetId::from_slice(&self.grantor_reissuance_token_asset) + .expect("grantor_reissuance_token_asset must be a valid 32-byte asset id") } - /// Returns the option token ID and reissuance asset ID as a tuple. #[must_use] - pub fn get_option_token_ids(&self) -> (AssetId, AssetId) { + pub fn option_token_ids(&self) -> (AssetId, AssetId) { (self.option_token(), self.option_reissuance_token()) } - /// Returns the grantor token ID and reissuance asset ID as a tuple. #[must_use] - pub fn get_grantor_token_ids(&self) -> (AssetId, AssetId) { + pub fn grantor_token_ids(&self) -> (AssetId, AssetId) { (self.grantor_token(), self.grantor_reissuance_token()) } - /// Build Simplicity arguments for contract instantiation. #[must_use] - pub fn build_option_arguments(&self) -> Arguments { + pub fn build_arguments(&self) -> Arguments { Arguments::from(HashMap::from([ ( WitnessName::from_str_unchecked("START_TIME"), @@ -242,89 +157,66 @@ impl OptionsArguments { self.settlement_asset_id, ))), ), - ( - WitnessName::from_str_unchecked("ISSUANCE_ASSET_ENTROPY"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.issuance_asset_entropy, - ))), - ), - ( - WitnessName::from_str_unchecked("OPTION_OUTPOINT_TXID"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.option_creation_outpoint.txid, - ))), - ), - ( - WitnessName::from_str_unchecked("OPTION_OUTPOINT_VOUT"), - simplicityhl::Value::from(UIntValue::U32(self.option_creation_outpoint.vout)), - ), - ( - WitnessName::from_str_unchecked("OPTION_CONFIDENTIAL"), - simplicityhl::Value::from(self.option_creation_outpoint.confidential), - ), - ( - WitnessName::from_str_unchecked("GRANTOR_OUTPOINT_TXID"), - simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.grantor_creation_outpoint.txid, - ))), - ), - ( - WitnessName::from_str_unchecked("GRANTOR_OUTPOINT_VOUT"), - simplicityhl::Value::from(UIntValue::U32(self.grantor_creation_outpoint.vout)), - ), - ( - WitnessName::from_str_unchecked("GRANTOR_CONFIDENTIAL"), - simplicityhl::Value::from(self.grantor_creation_outpoint.confidential), - ), - // Also include computed values for convenience ( WitnessName::from_str_unchecked("OPTION_TOKEN_ASSET"), simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.option_token().into_inner().0, + self.option_token_asset, ))), ), ( WitnessName::from_str_unchecked("OPTION_REISSUANCE_TOKEN_ASSET"), simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.option_reissuance_token().into_inner().0, + self.option_reissuance_token_asset, ))), ), ( WitnessName::from_str_unchecked("GRANTOR_TOKEN_ASSET"), simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.grantor_token().into_inner().0, + self.grantor_token_asset, ))), ), ( WitnessName::from_str_unchecked("GRANTOR_REISSUANCE_TOKEN_ASSET"), simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array( - self.grantor_reissuance_token().into_inner().0, + self.grantor_reissuance_token_asset, ))), ), ])) } - /// Build struct from Simplicity Arguments. + /// Build struct from Simplicity arguments. /// /// # Errors /// - /// Returns error if any required witness is missing, has wrong type, or has invalid value. + /// Returns error if any required parameter is missing or has wrong type. pub fn from_arguments(args: &Arguments) -> Result { - let start_time = extract_u32(args, "START_TIME")?; - let expiry_time = extract_u32(args, "EXPIRY_TIME")?; - let collateral_per_contract = extract_u64(args, "COLLATERAL_PER_CONTRACT")?; - let settlement_per_contract = extract_u64(args, "SETTLEMENT_PER_CONTRACT")?; - let collateral_asset_id = extract_u256_bytes(args, "COLLATERAL_ASSET_ID")?; - let settlement_asset_id = extract_u256_bytes(args, "SETTLEMENT_ASSET_ID")?; - let issuance_asset_entropy = extract_u256_bytes(args, "ISSUANCE_ASSET_ENTROPY")?; - - let option_outpoint_txid = extract_u256_bytes(args, "OPTION_OUTPOINT_TXID")?; - let option_outpoint_vout = extract_u32(args, "OPTION_OUTPOINT_VOUT")?; - let option_confidential = extract_bool(args, "OPTION_CONFIDENTIAL")?; - - let grantor_outpoint_txid = extract_u256_bytes(args, "GRANTOR_OUTPOINT_TXID")?; - let grantor_outpoint_vout = extract_u32(args, "GRANTOR_OUTPOINT_VOUT")?; - let grantor_confidential = extract_bool(args, "GRANTOR_CONFIDENTIAL")?; + let start_time_name = WitnessName::from_str_unchecked("START_TIME"); + let expiry_time_name = WitnessName::from_str_unchecked("EXPIRY_TIME"); + let collateral_per_contract_name = + WitnessName::from_str_unchecked("COLLATERAL_PER_CONTRACT"); + let settlement_per_contract_name = + WitnessName::from_str_unchecked("SETTLEMENT_PER_CONTRACT"); + let collateral_asset_id_name = WitnessName::from_str_unchecked("COLLATERAL_ASSET_ID"); + let settlement_asset_id_name = WitnessName::from_str_unchecked("SETTLEMENT_ASSET_ID"); + let option_token_asset_name = WitnessName::from_str_unchecked("OPTION_TOKEN_ASSET"); + let option_reissuance_token_asset_name = + WitnessName::from_str_unchecked("OPTION_REISSUANCE_TOKEN_ASSET"); + let grantor_token_asset_name = WitnessName::from_str_unchecked("GRANTOR_TOKEN_ASSET"); + let grantor_reissuance_token_asset_name = + WitnessName::from_str_unchecked("GRANTOR_REISSUANCE_TOKEN_ASSET"); + + let start_time = extract_u32(args, &start_time_name)?; + let expiry_time = extract_u32(args, &expiry_time_name)?; + let collateral_per_contract = extract_u64(args, &collateral_per_contract_name)?; + let settlement_per_contract = extract_u64(args, &settlement_per_contract_name)?; + let collateral_asset_id = extract_u256_bytes(args, &collateral_asset_id_name)?; + let settlement_asset_id = extract_u256_bytes(args, &settlement_asset_id_name)?; + let option_token_asset = extract_u256_bytes(args, &option_token_asset_name)?; + let option_reissuance_token_asset = + extract_u256_bytes(args, &option_reissuance_token_asset_name)?; + let grantor_token_asset = extract_u256_bytes(args, &grantor_token_asset_name)?; + let grantor_reissuance_token_asset = + extract_u256_bytes(args, &grantor_reissuance_token_asset_name)?; Ok(Self { start_time, @@ -333,51 +225,86 @@ impl OptionsArguments { settlement_per_contract, collateral_asset_id, settlement_asset_id, - issuance_asset_entropy, - option_creation_outpoint: OutPointWithConfidential { - txid: option_outpoint_txid, - vout: option_outpoint_vout, - confidential: option_confidential, - }, - grantor_creation_outpoint: OutPointWithConfidential { - txid: grantor_outpoint_txid, - vout: grantor_outpoint_vout, - confidential: grantor_confidential, - }, + option_token_asset, + option_reissuance_token_asset, + grantor_token_asset, + grantor_reissuance_token_asset, }) } - /// Returns the settlement asset ID. - /// + #[must_use] /// # Panics /// - /// Panics if the settlement asset ID bytes are invalid. This should never - /// happen since the field is a fixed 32-byte array. - #[must_use] - pub fn get_settlement_asset_id(&self) -> AssetId { - AssetId::from_slice(&self.settlement_asset_id).unwrap() + /// Panics if internal bytes are not a valid `AssetId`. + pub fn settlement_asset_id(&self) -> AssetId { + AssetId::from_slice(&self.settlement_asset_id) + .expect("settlement_asset_id must be a valid 32-byte asset id") } - /// Returns the collateral asset ID. - /// + #[must_use] /// # Panics /// - /// Panics if the collateral asset ID bytes are invalid. This should never - /// happen since the field is a fixed 32-byte array. - #[must_use] - pub fn get_collateral_asset_id(&self) -> AssetId { - AssetId::from_slice(&self.collateral_asset_id).unwrap() + /// Panics if internal bytes are not a valid `AssetId`. + pub fn collateral_asset_id(&self) -> AssetId { + AssetId::from_slice(&self.collateral_asset_id) + .expect("collateral_asset_id must be a valid 32-byte asset id") } } -impl simplicityhl_core::Encodable for OptionsArguments {} +impl wallet_abi::Encodable for OptionsArguments {} #[cfg(test)] mod tests { use super::*; - use crate::sdk::taproot_pubkey_gen::get_random_seed; - use simplicityhl::elements::hashes::Hash; - use simplicityhl_core::{Encodable, LIQUID_TESTNET_BITCOIN_ASSET}; + use simplicityhl::value::{UIntValue, ValueInner}; + use wallet_abi::Encodable; + + const NETWORK: ::wallet_abi::Network = ::wallet_abi::Network::TestnetLiquid; + + fn make_full_args() -> anyhow::Result { + Ok(OptionsArguments::new( + 10, + 50, + 100, + 1000, + *NETWORK.policy_asset(), + AssetId::from_slice(&[2; 32])?, + AssetId::from_slice(&[4; 32])?, + AssetId::from_slice(&[5; 32])?, + AssetId::from_slice(&[6; 32])?, + AssetId::from_slice(&[7; 32])?, + )) + } + + fn assert_is_u32(arguments: &Arguments, name: &str) { + let value = arguments + .get(&WitnessName::from_str_unchecked(name)) + .unwrap_or_else(|| panic!("{name} is missing")); + assert!( + matches!(value.inner(), ValueInner::UInt(UIntValue::U32(_))), + "{name} should be U32" + ); + } + + fn assert_is_u64(arguments: &Arguments, name: &str) { + let value = arguments + .get(&WitnessName::from_str_unchecked(name)) + .unwrap_or_else(|| panic!("{name} is missing")); + assert!( + matches!(value.inner(), ValueInner::UInt(UIntValue::U64(_))), + "{name} should be U64" + ); + } + + fn assert_is_u256(arguments: &Arguments, name: &str) { + let value = arguments + .get(&WitnessName::from_str_unchecked(name)) + .unwrap_or_else(|| panic!("{name} is missing")); + assert!( + matches!(value.inner(), ValueInner::UInt(UIntValue::U256(_))), + "{name} should be U256" + ); + } #[test] fn test_serialize_deserialize_default() -> anyhow::Result<()> { @@ -385,61 +312,70 @@ mod tests { let serialized = args.encode()?; let deserialized = OptionsArguments::decode(&serialized)?; assert_eq!(args, deserialized); + assert_eq!(deserialized.build_arguments().iter().count(), 10); Ok(()) } #[test] fn test_serialize_deserialize_full() -> anyhow::Result<()> { - let args = OptionsArguments::new( - 10, - 50, - 100, - 1000, - *LIQUID_TESTNET_BITCOIN_ASSET, - *LIQUID_TESTNET_BITCOIN_ASSET, - get_random_seed(), - (OutPoint::new(Txid::from_slice(&[1; 32])?, 0), false), - (OutPoint::new(Txid::from_slice(&[2; 32])?, 0), false), - ); - + let args = make_full_args()?; let serialized = args.encode()?; let deserialized = OptionsArguments::decode(&serialized)?; - assert_eq!(args, deserialized); - + assert_eq!(deserialized.start_time(), 10); + assert_eq!(deserialized.expiry_time(), 50); + assert_eq!(deserialized.collateral_per_contract(), 100); + assert_eq!(deserialized.settlement_per_contract(), 1000); + assert_eq!(deserialized.collateral_asset_id(), *NETWORK.policy_asset()); + assert_eq!( + deserialized.settlement_asset_id(), + AssetId::from_slice(&[2; 32])? + ); + assert_eq!(deserialized.option_token(), AssetId::from_slice(&[4; 32])?); + assert_eq!( + deserialized.option_reissuance_token(), + AssetId::from_slice(&[5; 32])? + ); + assert_eq!(deserialized.grantor_token(), AssetId::from_slice(&[6; 32])?); + assert_eq!( + deserialized.grantor_reissuance_token(), + AssetId::from_slice(&[7; 32])? + ); Ok(()) } #[test] fn test_arguments_roundtrip_default() -> anyhow::Result<()> { let original = OptionsArguments::default(); - let arguments = original.build_option_arguments(); - - let recovered = OptionsArguments::from_arguments(&arguments)?; - + let recovered = OptionsArguments::from_arguments(&original.build_arguments())?; assert_eq!(original, recovered); - Ok(()) } #[test] fn test_arguments_roundtrip_full() -> anyhow::Result<()> { - let original = OptionsArguments::new( - 10, - 50, - 100, - 1000, - *LIQUID_TESTNET_BITCOIN_ASSET, - *LIQUID_TESTNET_BITCOIN_ASSET, - get_random_seed(), - (OutPoint::new(Txid::from_slice(&[1; 32])?, 0), false), - (OutPoint::new(Txid::from_slice(&[2; 32])?, 0), true), - ); - let arguments = original.build_option_arguments(); - - let recovered = OptionsArguments::from_arguments(&arguments)?; - + let original = make_full_args()?; + let recovered = OptionsArguments::from_arguments(&original.build_arguments())?; assert_eq!(original, recovered); + Ok(()) + } + + #[test] + fn test_build_arguments_keys_and_value_types() -> anyhow::Result<()> { + let arguments = make_full_args()?.build_arguments(); + + assert_eq!(arguments.iter().count(), 10); + + assert_is_u32(&arguments, "START_TIME"); + assert_is_u32(&arguments, "EXPIRY_TIME"); + assert_is_u64(&arguments, "COLLATERAL_PER_CONTRACT"); + assert_is_u64(&arguments, "SETTLEMENT_PER_CONTRACT"); + assert_is_u256(&arguments, "COLLATERAL_ASSET_ID"); + assert_is_u256(&arguments, "SETTLEMENT_ASSET_ID"); + assert_is_u256(&arguments, "OPTION_TOKEN_ASSET"); + assert_is_u256(&arguments, "OPTION_REISSUANCE_TOKEN_ASSET"); + assert_is_u256(&arguments, "GRANTOR_TOKEN_ASSET"); + assert_is_u256(&arguments, "GRANTOR_REISSUANCE_TOKEN_ASSET"); Ok(()) } diff --git a/crates/contracts/src/finance/options/build_witness.rs b/crates/contracts/src/finance/options/build_witness.rs index b8448ac..7bc6a90 100644 --- a/crates/contracts/src/finance/options/build_witness.rs +++ b/crates/contracts/src/finance/options/build_witness.rs @@ -54,7 +54,9 @@ pub enum OptionBranch { /// Build witness values for options program execution. /// /// # Panics -/// Panics if type parsing fails (should never happen with valid constants). +/// +/// Panics if internal static type descriptors or generated witness values fail +/// to parse. #[must_use] #[allow(clippy::too_many_lines)] pub fn build_option_witness(branch: &OptionBranch) -> WitnessValues { diff --git a/crates/contracts/src/finance/options/mod.rs b/crates/contracts/src/finance/options/mod.rs index a4776e7..2fdb1c2 100644 --- a/crates/contracts/src/finance/options/mod.rs +++ b/crates/contracts/src/finance/options/mod.rs @@ -1,21 +1,11 @@ #![allow(clippy::similar_names)] -use crate::finance::options::build_witness::{OptionBranch, build_option_witness}; +use wallet_abi::{Network, ProgramError, create_p2tr_address, load_program}; -use std::sync::Arc; +use simplicityhl::elements::Address; -use simplicityhl_core::{ - ProgramError, SimplicityNetwork, control_block, create_p2tr_address, get_and_verify_env, - load_program, run_program, -}; +use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; -use simplicityhl::elements::{Address, Transaction, TxInWitness, TxOut}; - -use simplicityhl::simplicity::RedeemNode; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::{bitcoin::XOnlyPublicKey, jet::elements::ElementsEnv}; - -use simplicityhl::tracker::TrackerLogLevel; use simplicityhl::{CompiledProgram, TemplateProgram}; pub mod build_arguments; @@ -23,26 +13,28 @@ pub mod build_witness; pub use build_arguments::OptionsArguments; -pub const OPTION_SOURCE: &str = include_str!("source_simf/options.simf"); +pub const OPTIONS_SOURCE: &str = include_str!("source_simf/options.simf"); /// Get the options template program for instantiation. /// /// # Panics -/// - if the embedded source fails to compile (should never happen). +/// +/// Panics if the embedded source fails to compile. #[must_use] pub fn get_options_template_program() -> TemplateProgram { - TemplateProgram::new(OPTION_SOURCE) + TemplateProgram::new(OPTIONS_SOURCE) .expect("INTERNAL: expected Options Program to compile successfully.") } /// Derive P2TR address for an options contract. /// /// # Errors +/// /// Returns error if program compilation fails. pub fn get_options_address( x_only_public_key: &XOnlyPublicKey, arguments: &OptionsArguments, - network: SimplicityNetwork, + network: Network, ) -> Result { Ok(create_p2tr_address( get_options_program(arguments)?.commit().cmr(), @@ -54,752 +46,22 @@ pub fn get_options_address( /// Compile options program with the given arguments. /// /// # Errors +/// /// Returns error if compilation fails. pub fn get_options_program(arguments: &OptionsArguments) -> Result { - load_program(OPTION_SOURCE, arguments.build_option_arguments()) + load_program(OPTIONS_SOURCE, arguments.build_arguments()) } /// Get compiled options program, panicking on failure. /// /// # Panics -/// - if program instantiation fails. +/// +/// Panics if program instantiation fails. #[must_use] pub fn get_compiled_options_program(arguments: &OptionsArguments) -> CompiledProgram { let program = get_options_template_program(); program - .instantiate(arguments.build_option_arguments(), true) - .unwrap() -} - -/// Execute options program for funding path. -/// -/// # Errors -/// Returns error if program execution fails. -pub fn execute_options_program( - compiled_program: &CompiledProgram, - env: &ElementsEnv>, - option_branch: &OptionBranch, - runner_log_level: TrackerLogLevel, -) -> Result>, ProgramError> { - let witness_values = build_option_witness(option_branch); - - Ok(run_program(compiled_program, witness_values, env, runner_log_level)?.0) -} - -/// Finalize options funding path transaction with Simplicity witness. -/// -/// # Errors -/// -/// Returns error if program execution fails or script pubkey doesn't match. -#[allow(clippy::too_many_arguments)] -pub fn finalize_options_transaction( - mut tx: Transaction, - options_public_key: &XOnlyPublicKey, - options_program: &CompiledProgram, - utxos: &[TxOut], - input_index: usize, - option_branch: &OptionBranch, - network: SimplicityNetwork, - log_level: TrackerLogLevel, -) -> Result { - let env = get_and_verify_env( - &tx, - options_program, - options_public_key, - utxos, - network, - input_index, - )?; - - let pruned = execute_options_program(options_program, &env, option_branch, log_level)?; - - let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); - let cmr = pruned.cmr(); - - tx.input[input_index].witness = TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, - script_witness: vec![ - simplicity_witness_bytes, - simplicity_program_bytes, - cmr.as_ref().to_vec(), - control_block(cmr, *options_public_key).serialize(), - ], - pegin_witness: vec![], - }; - - Ok(tx) -} - -#[cfg(test)] -mod options_tests { - use super::*; - - use crate::sdk::taproot_pubkey_gen::{TaprootPubkeyGen, get_random_seed}; - use crate::sdk::{ - build_option_cancellation, build_option_creation, build_option_exercise, - build_option_expiry, build_option_funding, build_option_settlement, - }; - - use anyhow::Result; - use std::str::FromStr; - - use simplicityhl::elements::Script; - use simplicityhl::elements::confidential::{Asset, Value}; - use simplicityhl::simplicity::bitcoin::key::Keypair; - use simplicityhl::simplicity::bitcoin::secp256k1; - use simplicityhl::simplicity::bitcoin::secp256k1::Secp256k1; - use simplicityhl::simplicity::elements::{self, AssetId, OutPoint, Txid}; - use simplicityhl::simplicity::hashes::Hash; - - use simplicityhl::elements::pset::PartiallySignedTransaction; - use simplicityhl::elements::secp256k1_zkp::SECP256K1; - use simplicityhl::elements::taproot::ControlBlock; - use simplicityhl::simplicity::jet::elements::ElementsUtxo; - use simplicityhl_core::{ - LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_TEST_ASSET_ID_STR, SimplicityNetwork, - }; - - const NETWORK: SimplicityNetwork = SimplicityNetwork::LiquidTestnet; - - fn get_creation_pst( - keypair: &Keypair, - start_time: u32, - expiry_time: u32, - collateral_per_contract: u64, - settlement_per_contract: u64, - ) -> Result<( - (PartiallySignedTransaction, TaprootPubkeyGen), - OptionsArguments, - )> { - let option_outpoint = OutPoint::new(Txid::from_slice(&[1; 32])?, 0); - let grantor_outpoint = OutPoint::new(Txid::from_slice(&[2; 32])?, 0); - - let issuance_asset_entropy = get_random_seed(); - - let option_arguments = OptionsArguments::new( - start_time, - expiry_time, - collateral_per_contract, - settlement_per_contract, - *LIQUID_TESTNET_BITCOIN_ASSET, - AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?, - issuance_asset_entropy, - (option_outpoint, false), - (grantor_outpoint, false), - ); - - Ok(( - build_option_creation( - &keypair.public_key(), - ( - option_outpoint, - TxOut { - asset: Asset::Explicit(NETWORK.policy_asset()), - value: Value::Explicit(500), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - grantor_outpoint, - TxOut { - asset: Asset::Explicit(NETWORK.policy_asset()), - value: Value::Explicit(1000), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - &option_arguments, - issuance_asset_entropy, - 100, - NETWORK, - )?, - option_arguments, - )) - } - - struct FundingTestContext { - pub tx: Transaction, // Basic funding trasaction - pub program: CompiledProgram, // Compiled Simplicity program - - #[allow(dead_code)] - pub arguments: OptionsArguments, // Contract arguments - - pub branch: OptionBranch, // Execution branch (for witness) - pub pubkey_gen: TaprootPubkeyGen, // For generating addresses in Env - pub collateral_amount: u64, // For Env - pub input_option_tx_out: TxOut, // Original input UTXO for option token - pub input_grantor_tx_out: TxOut, // Original input UTXO for grantor token - } - - impl FundingTestContext { - pub fn create_env(&self, tx: Arc) -> Result>> { - // Use original input UTXOs for input verification - Ok(ElementsEnv::new( - tx, - vec![ - ElementsUtxo { - script_pubkey: self.pubkey_gen.address.script_pubkey(), - asset: self.input_option_tx_out.asset, - value: self.input_option_tx_out.value, - }, - ElementsUtxo { - script_pubkey: self.pubkey_gen.address.script_pubkey(), - asset: self.input_grantor_tx_out.asset, - value: self.input_grantor_tx_out.value, - }, - ElementsUtxo { - script_pubkey: self.pubkey_gen.address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(self.collateral_amount + 1000), - }, - ], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - )) - } - } - - fn setup_funding_scenario() -> Result { - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - - let collateral_per_contract = 20; - let settlement_per_contract = 25; - let collateral_amount = 1000; - - // 1. Creation PST - let ((pst, pubkey_gen), arguments) = get_creation_pst( - &keypair, - 0, - 0, - collateral_per_contract, - settlement_per_contract, - )?; - let pst_tx = pst.extract_tx()?; - - // 2. Unblinding outputs - these are INPUT secrets for the funding tx - let input_option_tx_out = pst_tx.output[0].clone(); - let input_option_secrets = pst_tx.output[0].unblind(SECP256K1, keypair.secret_key())?; - - let input_grantor_tx_out = pst_tx.output[1].clone(); - let input_grantor_secrets = pst_tx.output[1].unblind(SECP256K1, keypair.secret_key())?; - - // 3. Build Funding - returns PST and OptionBranch with all ABF/VBF extracted - let (pst, branch) = build_option_funding( - &keypair, - ( - OutPoint::default(), - input_option_tx_out.clone(), - input_option_secrets, - ), - ( - OutPoint::default(), - input_grantor_tx_out.clone(), - input_grantor_secrets, - ), - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(collateral_amount + 1000), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - None, - &arguments, - collateral_amount, - 50, - )?; - - let final_tx = pst.extract_tx()?; - - Ok(FundingTestContext { - tx: final_tx, - program: get_compiled_options_program(&arguments), - arguments, - branch, - pubkey_gen, - collateral_amount, - input_option_tx_out, - input_grantor_tx_out, - }) - } - - #[test] - fn test_options_creation() -> Result<()> { - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - - let _ = get_creation_pst(&keypair, 0, 0, 20, 25)?; - - Ok(()) - } - - #[test] - fn test_options_funding_path() -> Result<()> { - let ctx = setup_funding_scenario()?; - - let env = ctx.create_env(Arc::new(ctx.tx.clone()))?; - let witness_values = build_option_witness(&ctx.branch); - - assert!( - run_program(&ctx.program, witness_values, &env, TrackerLogLevel::Trace).is_ok(), - "expected success funding path" - ); - - Ok(()) - } - - #[test] - fn test_explicit_hack_options_funding_path() -> Result<()> { - let ctx = setup_funding_scenario()?; - - // Let's reproduce issue: https://github.com/BlockstreamResearch/simplicity-contracts/issues/21#issue-3686301161 - let mut hacked_tx = ctx.tx.clone(); - let stolen_asset_output = hacked_tx.output[0].clone(); - - hacked_tx.output[0].asset = Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET); // Test LBTC instead of reissuance token - hacked_tx.output[0].value = Value::Explicit(0); // Some dust - - // Add stolen asset to other output (for example next tx) - hacked_tx.output.push(stolen_asset_output); - - let env = ctx.create_env(Arc::new(hacked_tx))?; - let witness_values = build_option_witness(&ctx.branch); - - assert!( - run_program(&ctx.program, witness_values, &env, TrackerLogLevel::Trace).is_err(), - "SECURITY HOLE: The contract accepted an EXPLICIT hacked transaction!" - ); - - Ok(()) - } - - #[test] - fn test_blind_hack_options_funding_path() -> Result<()> { - use simplicityhl::elements::secp256k1_zkp::{ - Generator, PedersenCommitment, PublicKey, SecretKey, Tag, Tweak, rand::thread_rng, - }; - - let ctx = setup_funding_scenario()?; - let mut hacked_tx = ctx.tx.clone(); - - let stolen_asset_output = hacked_tx.output[0].clone(); - - let mut rng = thread_rng(); - let asset_blinding_factor = Tweak::new(&mut rng); - let value_blinding_factor = Tweak::new(&mut rng); - - let lbtc_bytes = LIQUID_TESTNET_BITCOIN_ASSET.into_inner().to_byte_array(); - let lbtc_tag = Tag::from(lbtc_bytes); - - let blinded_asset_generator = - Generator::new_blinded(SECP256K1, lbtc_tag, asset_blinding_factor); - - let blinded_value_commitment = - PedersenCommitment::new(SECP256K1, 0, value_blinding_factor, blinded_asset_generator); - - let ephemeral_secret_key = SecretKey::new(&mut rng); - let ephemeral_pub_key = PublicKey::from_secret_key(SECP256K1, &ephemeral_secret_key); - - // Substitution - hacked_tx.output[0].asset = Asset::Confidential(blinded_asset_generator); - hacked_tx.output[0].value = Value::Confidential(blinded_value_commitment); - hacked_tx.output[0].nonce = elements::confidential::Nonce::Confidential(ephemeral_pub_key); - - hacked_tx.output.push(stolen_asset_output); - - let env = ctx.create_env(Arc::new(hacked_tx))?; - let witness_values = build_option_witness(&ctx.branch); - - assert!( - run_program(&ctx.program, witness_values, &env, TrackerLogLevel::Trace).is_err(), - "SECURITY HOLE: The contract accepted a BLINDED hacked transaction!" - ); - - Ok(()) - } - - #[test] - fn test_options_cancellation_path() -> Result<()> { - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - - let collateral_per_contract = 20; - let settlement_per_contract = 25; - - let ((_, option_pubkey_gen), option_arguments) = get_creation_pst( - &keypair, - 0, - 0, - collateral_per_contract, - settlement_per_contract, - )?; - - let collateral_amount = 1000; - let option_token_amount = collateral_amount / collateral_per_contract; - let amount_to_burn = option_token_amount / 2; - - let (option_asset_id, _) = option_arguments.get_option_token_ids(); - let (grantor_asset_id, _) = option_arguments.get_grantor_token_ids(); - - let (pst, option_branch) = build_option_cancellation( - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(collateral_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: option_pubkey_gen.address.script_pubkey(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(option_asset_id), - value: Value::Explicit(option_token_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(grantor_asset_id), - value: Value::Explicit(option_token_amount), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(NETWORK.policy_asset()), - value: Value::Explicit(100), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - &option_arguments, - amount_to_burn, - 50, - )?; - - let program = get_compiled_options_program(&option_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ElementsUtxo { - script_pubkey: option_pubkey_gen.address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(collateral_amount), - }], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_option_witness(&option_branch); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::None).is_ok(), - "expected success cancellation path" - ); - - Ok(()) - } - - #[test] - fn test_options_exercise_path() -> Result<()> { - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - - let collateral_per_contract = 20; - let settlement_per_contract = 10; - - let ((_, option_pubkey_gen), option_arguments) = get_creation_pst( - &keypair, - 500_000_000, - 500_000_000, - collateral_per_contract, - settlement_per_contract, - )?; - - let collateral_amount_total = 1000; - let option_token_amount_total = collateral_amount_total / collateral_per_contract; // 50 - let option_amount_to_burn = option_token_amount_total - 5; // 45 - let asset_amount_to_pay = option_amount_to_burn * settlement_per_contract; // 450 - - let (option_asset_id, _) = option_arguments.get_option_token_ids(); - let settlement_asset_id = AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?; - - let (pst, option_branch) = build_option_exercise( - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(collateral_amount_total), - nonce: elements::confidential::Nonce::Null, - script_pubkey: option_pubkey_gen.address.script_pubkey(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(option_asset_id), - value: Value::Explicit(option_token_amount_total), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(settlement_asset_id), - value: Value::Explicit(asset_amount_to_pay + 100), // extra for change - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - Some(( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(NETWORK.policy_asset()), - value: Value::Explicit(100), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - )), - option_amount_to_burn, - 50, - &option_arguments, - )?; - - let program = get_compiled_options_program(&option_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ElementsUtxo { - script_pubkey: option_pubkey_gen.address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(collateral_amount_total), - }], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_option_witness(&option_branch); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::None).is_ok(), - "expected success exercise path" - ); - - Ok(()) - } - - #[test] - fn test_options_settlement_path() -> Result<()> { - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - - let collateral_per_contract = 20; - let settlement_per_contract = 40; - - let ((_, option_pubkey_gen), option_arguments) = get_creation_pst( - &keypair, - 500_000_000, - 500_000_000, - collateral_per_contract, - settlement_per_contract, - )?; - - let grantor_token_amount_to_burn = 10; - let available_target_asset = 1000; // available in input utxo - - let (grantor_asset_id, _) = option_arguments.get_grantor_token_ids(); - let settlement_asset_id = AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?; - - let (pst, option_branch) = build_option_settlement( - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(settlement_asset_id), - value: Value::Explicit(available_target_asset), - nonce: elements::confidential::Nonce::Null, - script_pubkey: option_pubkey_gen.address.script_pubkey(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(grantor_asset_id), - value: Value::Explicit(grantor_token_amount_to_burn + 5), // extra for change - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(NETWORK.policy_asset()), - value: Value::Explicit(100), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - grantor_token_amount_to_burn, - 50, - &option_arguments, - )?; - - let program = get_compiled_options_program(&option_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ElementsUtxo { - script_pubkey: option_pubkey_gen.address.script_pubkey(), - asset: Asset::Explicit(settlement_asset_id), - value: Value::Explicit(available_target_asset), - }], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_option_witness(&option_branch); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::None).is_ok(), - "expected success settlement path" - ); - - Ok(()) - } - - #[test] - fn test_options_expiry_path() -> Result<()> { - let keypair = Keypair::from_secret_key( - &Secp256k1::new(), - &secp256k1::SecretKey::from_slice(&[1u8; 32])?, - ); - - let collateral_per_contract = 20; - let settlement_per_contract = 25; - - let ((_, option_pubkey_gen), option_arguments) = get_creation_pst( - &keypair, - 500_000_000, - 500_000_000, - collateral_per_contract, - settlement_per_contract, - )?; - - let collateral_amount_total = 1000; - let option_token_amount_total = collateral_amount_total / collateral_per_contract; // 50 - - // At expiry, burn grantor tokens to withdraw collateral - let grantor_token_amount_to_burn = option_token_amount_total / 2; // 25 - - let (grantor_asset_id, _) = option_arguments.get_grantor_token_ids(); - - let (pst, option_branch) = build_option_expiry( - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(collateral_amount_total), - nonce: elements::confidential::Nonce::Null, - script_pubkey: option_pubkey_gen.address.script_pubkey(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(grantor_asset_id), - value: Value::Explicit(grantor_token_amount_to_burn + 5), // extra for change - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - ( - OutPoint::default(), - TxOut { - asset: Asset::Explicit(NETWORK.policy_asset()), - value: Value::Explicit(100), - nonce: elements::confidential::Nonce::Null, - script_pubkey: Script::new(), - witness: elements::TxOutWitness::default(), - }, - ), - grantor_token_amount_to_burn, - 50, - &option_arguments, - )?; - - let program = get_compiled_options_program(&option_arguments); - - let env = ElementsEnv::new( - Arc::new(pst.extract_tx()?), - vec![ElementsUtxo { - script_pubkey: option_pubkey_gen.address.script_pubkey(), - asset: Asset::Explicit(*LIQUID_TESTNET_BITCOIN_ASSET), - value: Value::Explicit(collateral_amount_total), - }], - 0, - simplicityhl::simplicity::Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&[0xc0; 33])?, - None, - elements::BlockHash::all_zeros(), - ); - - let witness_values = build_option_witness(&option_branch); - - assert!( - run_program(&program, witness_values, &env, TrackerLogLevel::None).is_ok(), - "expected success expiry path" - ); - - Ok(()) - } + .instantiate(arguments.build_arguments(), true) + .expect("INTERNAL: expected Options Program instantiation to succeed") } diff --git a/crates/contracts/src/finance/options/source_simf/options.simf b/crates/contracts/src/finance/options/source_simf/options.simf index e7da014..8398dcf 100644 --- a/crates/contracts/src/finance/options/source_simf/options.simf +++ b/crates/contracts/src/finance/options/source_simf/options.simf @@ -1,18 +1,38 @@ /* * Options - * - * Important: Currently only the LBTC collateral is supported. + * + * Collateral and settlement assets are selected via contract parameters. * * Based on the https://blockstream.com/assets/downloads/pdf/options-whitepaper.pdf * - * This contract implements cash-settled European-style options using covenant-locked collateral. + * This contract implements cash-settled options using covenant-locked collateral. + * + * Exercise/settlement timing nuance: + * + * Standard “European-style” (in finance) means the option is exercisable only at expiry. + * + * This implementation enforces only a *no-early-exercise * rule: `exercise_path` and + * `settlement_path` require `check_lock_time(START_TIME)`, so they are valid at/after + * `START_TIME` (and are not, by themselves, restricted to only `EXPIRY_TIME`). + * + * What it most closely matches is a Bermudan-style (or “American-with-a-delay”) option: + * exercisable after a certain date/time (`START_TIME`), potentially up to some endpoint— + * though this code (as shown) does not enforce an upper bound like `EXPIRY_TIME` for exercise. + * + * `expiry_path` is gated by `check_lock_time(EXPIRY_TIME)` for post-expiry reclaim. + * + * Possible contract change to fit a particular style: + * - American-style: remove the `START_TIME` locktime requirement (or set `START_TIME` to the + * funding time / 0, depending on your conventions). + * - European-style: require exercise only at expiry (add a check tying exercise to `EXPIRY_TIME`, + * not `START_TIME`). * * Room for optimization: * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/2 (Use input asset to determine option covenent type) * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/3 (Simplify match token_branch in funding_path.) * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 (why batching is hard to implement) * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/5 (Reduce Contract Parameters) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/21 (explains why funding is limited) + * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/21 (Explains why funding is limited) */ /// Assert: a == b * expected_q, via divmod diff --git a/crates/contracts/src/lib.rs b/crates/contracts/src/lib.rs index 75535c6..b37a7e0 100644 --- a/crates/contracts/src/lib.rs +++ b/crates/contracts/src/lib.rs @@ -1,28 +1,24 @@ #![warn(clippy::all, clippy::pedantic)] extern crate core; -pub mod arguments_helpers; pub mod error; -pub mod sdk; - -#[cfg(feature = "array-tr-storage")] -pub mod array_tr_storage; -#[cfg(feature = "bytes32-tr-storage")] -pub mod bytes32_tr_storage; -#[cfg(any( - feature = "finance-dcd", - feature = "finance-option-offer", - feature = "finance-options" -))] +#[cfg(any(feature = "finance-option-offer", feature = "finance-options"))] pub mod finance; -#[cfg(feature = "simple-storage")] -pub mod simple_storage; -#[cfg(feature = "smt-storage")] -pub mod smt_storage; -#[cfg(feature = "finance-dcd")] -pub use finance::dcd; +pub mod state_management; + +mod utils; + #[cfg(feature = "finance-option-offer")] pub use finance::option_offer; #[cfg(feature = "finance-options")] pub use finance::options; + +#[cfg(feature = "array-tr-storage")] +pub use state_management::array_tr_storage; +#[cfg(feature = "bytes32-tr-storage")] +pub use state_management::bytes32_tr_storage; +#[cfg(feature = "simple-storage")] +pub use state_management::simple_storage; +#[cfg(feature = "smt-storage")] +pub use state_management::smt_storage; diff --git a/crates/contracts/src/sdk/basic/issue_asset.rs b/crates/contracts/src/sdk/basic/issue_asset.rs deleted file mode 100644 index d6d5931..0000000 --- a/crates/contracts/src/sdk/basic/issue_asset.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::error::TransactionBuildError; -use crate::sdk::taproot_pubkey_gen::get_random_seed; -use crate::sdk::validation::TxOutExt; - -use std::collections::HashMap; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::confidential::{AssetBlindingFactor, ValueBlindingFactor}; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::secp256k1_zkp::PublicKey; -use simplicityhl::elements::secp256k1_zkp::rand::thread_rng; -use simplicityhl::elements::{OutPoint, TxOut, TxOutSecrets}; -use simplicityhl_core::PLACEHOLDER_ISSUANCE_VALUE; - -/// Issue a new asset with given amount. -/// -/// # Errors -/// -/// Returns an error if: -/// - The fee UTXO asset or amount (fee included) validation fails (ALL amounts and assets are expected to be explicit) -/// - Transaction blinding fails -/// - Transaction extraction or amount proof verification fails -pub fn issue_asset( - blinding_key: &PublicKey, - fee_utxo: (OutPoint, TxOut), - issue_amount: u64, - fee_amount: u64, -) -> Result { - let (fee_out_point, fee_tx_out) = fee_utxo; - - let (fee_asset_id, total_lbtc_left) = ( - fee_tx_out.explicit_asset()?, - fee_tx_out.remaining_after_required(fee_amount)?, - ); - - let change_recipient_script = fee_tx_out.script_pubkey.clone(); - - let mut pst = PartiallySignedTransaction::new_v2(); - - let mut issuance_tx = Input::from_prevout(fee_out_point); - issuance_tx.witness_utxo = Some(fee_tx_out.clone()); - issuance_tx.issuance_value_amount = Some(issue_amount); - issuance_tx.issuance_inflation_keys = Some(1); - issuance_tx.issuance_asset_entropy = Some(get_random_seed()); - issuance_tx.blinded_issuance = Some(0x00); - - let (asset_id, reissuance_asset_id) = issuance_tx.issuance_ids(); - - pst.add_input(issuance_tx); - - let mut inp_txout_sec: HashMap = HashMap::new(); - inp_txout_sec.insert( - 0, - TxOutSecrets { - asset_bf: AssetBlindingFactor::zero(), - value_bf: ValueBlindingFactor::zero(), - value: PLACEHOLDER_ISSUANCE_VALUE, - asset: fee_asset_id, - }, - ); - - // Output with reissuance token - let mut output = Output::new_explicit( - change_recipient_script.clone(), - 1, - reissuance_asset_id, - Some((*blinding_key).into()), - ); - output.blinder_index = Some(0); - pst.add_output(output); - - // Output with issuance token - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - issue_amount, - asset_id, - None, - )); - - // LBTC Change - if total_lbtc_left != 0 { - pst.add_output(Output::new_explicit( - change_recipient_script, - total_lbtc_left, - fee_asset_id, - None, - )); - } - - pst.add_output(Output::from_txout(TxOut::new_fee(fee_amount, fee_asset_id))); - - pst.blind_last(&mut thread_rng(), secp256k1::SECP256K1, &inp_txout_sec)?; - - pst.extract_tx()? - .verify_tx_amt_proofs(secp256k1::SECP256K1, &[fee_tx_out])?; - - Ok(pst) -} diff --git a/crates/contracts/src/sdk/basic/mod.rs b/crates/contracts/src/sdk/basic/mod.rs deleted file mode 100644 index a5a6183..0000000 --- a/crates/contracts/src/sdk/basic/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod issue_asset; -mod reissue_asset; -mod split_native_any; -mod transfer_asset; -mod transfer_native; - -pub use issue_asset::*; -pub use reissue_asset::*; -pub use split_native_any::*; -pub use transfer_asset::*; -pub use transfer_native::*; diff --git a/crates/contracts/src/sdk/basic/reissue_asset.rs b/crates/contracts/src/sdk/basic/reissue_asset.rs deleted file mode 100644 index 22a86a6..0000000 --- a/crates/contracts/src/sdk/basic/reissue_asset.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::error::TransactionBuildError; -use crate::sdk::validation::TxOutExt; - -use std::collections::HashMap; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::hashes::sha256::Midstate; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::secp256k1_zkp::PublicKey; -use simplicityhl::elements::secp256k1_zkp::rand::thread_rng; -use simplicityhl::elements::{AssetId, OutPoint, TxOut, TxOutSecrets}; - -/// Reissue an existing asset by spending its reissuance token. -/// -/// # Errors -/// -/// Returns an error if: -/// - The fee UTXO asset or amount (fee included) validation fails (ALL amounts and assets are expected to be explicit) -/// - The reissuance UTXO unblinding fails -/// - Transaction blinding fails -/// - Transaction extraction or amount proof verification fails -pub fn reissue_asset( - blinding_key: &PublicKey, - reissue_utxo: (OutPoint, TxOut), - reissue_utxo_secrets: TxOutSecrets, - fee_utxo: (OutPoint, TxOut), - reissue_amount: u64, - fee_amount: u64, - asset_entropy: Midstate, -) -> Result { - let (reissue_out_point, reissue_tx_out) = reissue_utxo; - let (fee_out_point, fee_tx_out) = fee_utxo; - - let (fee_asset_id, total_lbtc_left) = ( - fee_tx_out.explicit_asset()?, - fee_tx_out.remaining_after_required(fee_amount)?, - ); - - let change_recipient_script = fee_tx_out.script_pubkey.clone(); - - let asset_bf = reissue_utxo_secrets.asset_bf; - - let asset_id = AssetId::from_entropy(asset_entropy); - let reissuance_asset_id = AssetId::reissuance_token_from_entropy(asset_entropy, false); - - let mut inp_txout_sec: HashMap = HashMap::new(); - - let mut pst = PartiallySignedTransaction::new_v2(); - - { - let mut reissuance_tx = Input::from_prevout(reissue_out_point); - reissuance_tx.witness_utxo = Some(reissue_tx_out.clone()); - reissuance_tx.issuance_value_amount = Some(reissue_amount); - reissuance_tx.issuance_inflation_keys = None; - reissuance_tx.issuance_asset_entropy = Some(asset_entropy.to_byte_array()); - - reissuance_tx.blinded_issuance = Some(0x00); - reissuance_tx.issuance_blinding_nonce = Some(asset_bf.into_inner()); - - pst.add_input(reissuance_tx); - - inp_txout_sec.insert(0, reissue_utxo_secrets); - } - - { - let mut fee_input = Input::from_prevout(fee_out_point); - fee_input.witness_utxo = Some(fee_tx_out.clone()); - - pst.add_input(fee_input); - } - - { - let mut output = Output::new_explicit( - change_recipient_script.clone(), - 1, - reissuance_asset_id, - Some((*blinding_key).into()), - ); - output.blinder_index = Some(0); - - pst.add_output(output); - } - - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - reissue_amount, - asset_id, - None, - )); - - if total_lbtc_left != 0 { - pst.add_output(Output::new_explicit( - change_recipient_script, - total_lbtc_left, - fee_asset_id, - None, - )); - } - - pst.add_output(Output::from_txout(TxOut::new_fee(fee_amount, fee_asset_id))); - - pst.blind_last(&mut thread_rng(), secp256k1::SECP256K1, &inp_txout_sec)?; - - pst.extract_tx()? - .verify_tx_amt_proofs(secp256k1::SECP256K1, &[reissue_tx_out, fee_tx_out])?; - - Ok(pst) -} diff --git a/crates/contracts/src/sdk/basic/split_native_any.rs b/crates/contracts/src/sdk/basic/split_native_any.rs deleted file mode 100644 index 3fc5382..0000000 --- a/crates/contracts/src/sdk/basic/split_native_any.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::error::TransactionBuildError; -use crate::sdk::validation::TxOutExt; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::{OutPoint, TxOut}; - -/// Split a native UTXO into any number of outputs + change. -/// -/// # Errors -/// -/// Returns an error if: -/// - The UTXO asset or amount (fee included) validation fails (ALL amounts and assets are expected to be explicit) -/// - Transaction extraction or amount proof verification fails -pub fn split_native_any( - utxo: (OutPoint, TxOut), - parts_to_split: u64, - fee_amount: u64, -) -> Result { - if parts_to_split == 0 { - return Err(TransactionBuildError::InvalidSplitParts); - } - - let (out_point, tx_out) = utxo; - - let (asset_id, total_lbtc_left) = ( - tx_out.explicit_asset()?, - tx_out.remaining_after_required(fee_amount)?, - ); - - let recipient_script = tx_out.script_pubkey.clone(); - - let mut pst = PartiallySignedTransaction::new_v2(); - - let mut input = Input::from_prevout(out_point); - input.witness_utxo = Some(tx_out.clone()); - pst.add_input(input); - - let split_amount = total_lbtc_left / parts_to_split; - let change_amount = total_lbtc_left - split_amount * (parts_to_split - 1); - - for _ in 0..(parts_to_split - 1) { - pst.add_output(Output::new_explicit( - recipient_script.clone(), - split_amount, - asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - recipient_script, - change_amount, - asset_id, - None, - )); - - pst.add_output(Output::from_txout(TxOut::new_fee(fee_amount, asset_id))); - - pst.extract_tx()? - .verify_tx_amt_proofs(secp256k1::SECP256K1, &[tx_out])?; - - Ok(pst) -} diff --git a/crates/contracts/src/sdk/basic/transfer_asset.rs b/crates/contracts/src/sdk/basic/transfer_asset.rs deleted file mode 100644 index 03da666..0000000 --- a/crates/contracts/src/sdk/basic/transfer_asset.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::error::TransactionBuildError; -use crate::sdk::validation::TxOutExt; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::{Address, OutPoint, TxOut}; - -/// Transfer an asset to another address. -/// -/// # Errors -/// -/// Returns an error if: -/// - The asset or fee UTXO asset or amount (fee included) validation fails (ALL amounts and assets are expected to be explicit) -/// - Transaction extraction or amount proof verification fails -pub fn transfer_asset( - asset_utxo: (OutPoint, TxOut), - fee_utxo: (OutPoint, TxOut), - to_address: &Address, - send_amount: u64, - fee_amount: u64, -) -> Result { - let (asset_out_point, asset_tx_out) = asset_utxo; - let (fee_out_point, fee_tx_out) = fee_utxo; - - let (asset_id, total_input_asset) = asset_tx_out.explicit()?; - let (fee_asset_id, total_lbtc_left) = ( - fee_tx_out.explicit_asset()?, - fee_tx_out.remaining_after_required(fee_amount)?, - ); - - if send_amount > total_input_asset { - return Err(TransactionBuildError::SendAmountExceedsUtxo { - send_amount, - available: total_input_asset, - }); - } - - let change_recipient_script = fee_tx_out.script_pubkey.clone(); - - let mut pst = PartiallySignedTransaction::new_v2(); - - let mut asset_input = Input::from_prevout(asset_out_point); - asset_input.witness_utxo = Some(asset_tx_out.clone()); - pst.add_input(asset_input); - - let mut fee_input = Input::from_prevout(fee_out_point); - fee_input.witness_utxo = Some(fee_tx_out.clone()); - pst.add_input(fee_input); - - pst.add_output(Output::new_explicit( - to_address.script_pubkey(), - send_amount, - asset_id, - None, - )); - - let is_asset_change_needed = total_input_asset != send_amount; - let is_fee_change_needed = total_lbtc_left != 0; - - if is_asset_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - total_input_asset - send_amount, - asset_id, - None, - )); - } - - if is_fee_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script, - total_lbtc_left, - fee_asset_id, - None, - )); - } - - pst.add_output(Output::from_txout(TxOut::new_fee(fee_amount, fee_asset_id))); - - pst.extract_tx()? - .verify_tx_amt_proofs(secp256k1::SECP256K1, &[asset_tx_out, fee_tx_out])?; - - Ok(pst) -} diff --git a/crates/contracts/src/sdk/basic/transfer_native.rs b/crates/contracts/src/sdk/basic/transfer_native.rs deleted file mode 100644 index 37054af..0000000 --- a/crates/contracts/src/sdk/basic/transfer_native.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::error::TransactionBuildError; -use crate::sdk::validation::TxOutExt; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::{Address, OutPoint, TxOut}; - -/// Transfer native asset (LBTC) to another address. -/// -/// # Errors -/// -/// Returns an error if: -/// - The UTXO asset or amount (fee included) validation fails (ALL amounts and assets are expected to be explicit) -/// - Transaction extraction or amount proof verification fails -pub fn transfer_native( - utxo: (OutPoint, TxOut), - to_address: &Address, - amount_to_send: u64, - fee_amount: u64, -) -> Result { - let (out_point, tx_out) = utxo; - - let (asset_id, total_lbtc_left) = ( - tx_out.explicit_asset()?, - tx_out.remaining_after_required(amount_to_send + fee_amount)?, - ); - - let change_recipient_script = tx_out.script_pubkey.clone(); - - let mut pst = PartiallySignedTransaction::new_v2(); - - let mut input = Input::from_prevout(out_point); - input.witness_utxo = Some(tx_out.clone()); - pst.add_input(input); - - pst.add_output(Output::new_explicit( - to_address.script_pubkey(), - amount_to_send, - asset_id, - None, - )); - - if total_lbtc_left != 0 { - pst.add_output(Output::new_explicit( - change_recipient_script, - total_lbtc_left, - asset_id, - None, - )); - } - - pst.add_output(Output::from_txout(TxOut::new_fee(fee_amount, asset_id))); - - pst.extract_tx()? - .verify_tx_amt_proofs(secp256k1::SECP256K1, &[tx_out])?; - - Ok(pst) -} diff --git a/crates/contracts/src/sdk/finance/mod.rs b/crates/contracts/src/sdk/finance/mod.rs deleted file mode 100644 index ef85222..0000000 --- a/crates/contracts/src/sdk/finance/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[cfg(feature = "finance-option-offer")] -mod option_offer; -#[cfg(feature = "finance-options")] -mod options; - -#[cfg(feature = "finance-option-offer")] -pub use option_offer::*; -#[cfg(feature = "finance-options")] -pub use options::*; diff --git a/crates/contracts/src/sdk/finance/option_offer/deposit.rs b/crates/contracts/src/sdk/finance/option_offer/deposit.rs deleted file mode 100644 index 9e6b1dc..0000000 --- a/crates/contracts/src/sdk/finance/option_offer/deposit.rs +++ /dev/null @@ -1,169 +0,0 @@ -use crate::error::TransactionBuildError; -use crate::finance::option_offer::{OptionOfferArguments, get_option_offer_address}; -use crate::sdk::taproot_pubkey_gen::TaprootPubkeyGen; -use crate::sdk::validation::TxOutExt; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::{OutPoint, Script, TxOut}; -use simplicityhl_core::SimplicityNetwork; - -/// Build PSET for user to deposit collateral and premium into the option offer covenant. -/// -/// # Layout -/// -/// - Input[0]: Collateral from user -/// - Input[1]: Premium from user -/// - Input[2]: Fee -/// - Output[0]: Collateral → covenant -/// - Output[1]: Premium → covenant -/// - Output[2]: Collateral change → user (if any) -/// - Output[3]: Premium change → user (if any) -/// - Output[4]: Fee change (if any) -/// - Output[5]: Fee -/// -/// # Errors -/// -/// Returns an error if: -/// - The UTXO asset or amount validation fails (amounts and assets must be explicit) -/// - Collateral asset doesn't match expected -/// - Premium asset doesn't match expected -/// - Premium amount doesn't satisfy ratio constraint -/// - Insufficient collateral or premium for deposit -/// - Transaction extraction or amount proof verification fails -#[allow(clippy::too_many_arguments, clippy::too_many_lines)] -pub fn build_option_offer_deposit( - collateral_utxo: (OutPoint, TxOut), - premium_utxo: (OutPoint, TxOut), - fee_utxo: (OutPoint, TxOut), - collateral_deposit_amount: u64, - fee_amount: u64, - arguments: &OptionOfferArguments, - network: SimplicityNetwork, -) -> Result<(PartiallySignedTransaction, TaprootPubkeyGen), TransactionBuildError> { - let (collateral_outpoint, collateral_tx_out) = collateral_utxo; - let (premium_outpoint, premium_tx_out) = premium_utxo; - let (fee_outpoint, fee_tx_out) = fee_utxo; - - let (fee_asset_id, fee_change) = ( - fee_tx_out.explicit_asset()?, - fee_tx_out.remaining_after_required(fee_amount)?, - ); - let (collateral_asset_id, total_collateral) = collateral_tx_out.explicit()?; - let (premium_asset_id, total_premium) = premium_tx_out.explicit()?; - - let expected_collateral = arguments.get_collateral_asset_id(); - if collateral_asset_id != expected_collateral { - return Err(TransactionBuildError::WrongSettlementAsset { - expected: expected_collateral.to_string(), - actual: collateral_asset_id.to_string(), - }); - } - - let expected_premium = arguments.get_premium_asset_id(); - if premium_asset_id != expected_premium { - return Err(TransactionBuildError::WrongSettlementAsset { - expected: expected_premium.to_string(), - actual: premium_asset_id.to_string(), - }); - } - - let premium_deposit_amount = collateral_deposit_amount - .checked_mul(arguments.premium_per_collateral()) - .ok_or(TransactionBuildError::InsufficientSettlementAsset { - required: u64::MAX, - available: total_premium, - })?; - - if collateral_deposit_amount > total_collateral { - return Err(TransactionBuildError::InsufficientCollateral { - required: collateral_deposit_amount, - available: total_collateral, - }); - } - - if premium_deposit_amount > total_premium { - return Err(TransactionBuildError::InsufficientSettlementAsset { - required: premium_deposit_amount, - available: total_premium, - }); - } - - let change_recipient_script = collateral_tx_out.script_pubkey.clone(); - - let option_offer_taproot_pubkey_gen = - TaprootPubkeyGen::from(arguments, network, &get_option_offer_address)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - - let mut collateral_input = Input::from_prevout(collateral_outpoint); - collateral_input.witness_utxo = Some(collateral_tx_out.clone()); - pst.add_input(collateral_input); - - let mut premium_input = Input::from_prevout(premium_outpoint); - premium_input.witness_utxo = Some(premium_tx_out.clone()); - pst.add_input(premium_input); - - let mut fee_input = Input::from_prevout(fee_outpoint); - fee_input.witness_utxo = Some(fee_tx_out.clone()); - pst.add_input(fee_input); - - let is_collateral_change_needed = total_collateral != collateral_deposit_amount; - let is_premium_change_needed = total_premium != premium_deposit_amount; - let is_fee_change_needed = fee_change != 0; - - pst.add_output(Output::new_explicit( - option_offer_taproot_pubkey_gen.address.script_pubkey(), - collateral_deposit_amount, - collateral_asset_id, - None, - )); - - pst.add_output(Output::new_explicit( - option_offer_taproot_pubkey_gen.address.script_pubkey(), - premium_deposit_amount, - premium_asset_id, - None, - )); - - if is_collateral_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - total_collateral - collateral_deposit_amount, - collateral_asset_id, - None, - )); - } - - if is_premium_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - total_premium - premium_deposit_amount, - premium_asset_id, - None, - )); - } - - if is_fee_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script, - fee_change, - fee_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - Script::new(), - fee_amount, - fee_asset_id, - None, - )); - - pst.extract_tx()?.verify_tx_amt_proofs( - secp256k1::SECP256K1, - &[collateral_tx_out, premium_tx_out, fee_tx_out], - )?; - - Ok((pst, option_offer_taproot_pubkey_gen)) -} diff --git a/crates/contracts/src/sdk/finance/option_offer/exercise.rs b/crates/contracts/src/sdk/finance/option_offer/exercise.rs deleted file mode 100644 index d10fb56..0000000 --- a/crates/contracts/src/sdk/finance/option_offer/exercise.rs +++ /dev/null @@ -1,234 +0,0 @@ -use crate::finance::option_offer::OptionOfferArguments; -use crate::finance::option_offer::build_witness::OptionOfferBranch; - -use crate::error::TransactionBuildError; - -use crate::sdk::validation::TxOutExt; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::{OutPoint, Script, TxOut}; - -/// Build PSET for counterparty to swap settlement asset for collateral + premium. -/// -/// # Layout -/// -/// With change (partial swap): -/// - Input[0]: Collateral from covenant -/// - Input[1]: Premium from covenant -/// - Input[2]: Settlement from counterparty -/// - Input[3]: Fee -/// - Output[0]: Collateral change → covenant -/// - Output[1]: Premium change → covenant -/// - Output[2]: Settlement asset → covenant -/// - Output[3]: Collateral → counterparty -/// - Output[4]: Premium → counterparty -/// - Output[5+]: Settlement change, fee change, fee -/// -/// Without change (full swap): -/// - Input[0]: Collateral from covenant -/// - Input[1]: Premium from covenant -/// - Input[2]: Settlement from counterparty -/// - Input[3]: Fee -/// - Output[0]: Settlement asset → covenant -/// - Output[1]: Collateral → counterparty -/// - Output[2]: Premium → counterparty -/// - Output[3+]: Settlement change, fee change, fee -/// -/// # Errors -/// -/// Returns an error if: -/// - The UTXO asset or amount validation fails (amounts and assets must be explicit) -/// - Insufficient collateral or premium in covenant -/// - Insufficient settlement asset from counterparty -/// - Settlement amount doesn't satisfy constraint: `settlement` = `collateral_per_contract` * `collateral` -/// - Premium amount doesn't satisfy constraint: `premium` = `premium_per_collateral` * `collateral` -/// - Transaction extraction or amount proof verification fails -#[allow(clippy::too_many_lines, clippy::too_many_arguments)] -pub fn build_option_offer_exercise( - collateral_covenant_utxo: (OutPoint, TxOut), - premium_covenant_utxo: (OutPoint, TxOut), - settlement_utxo: (OutPoint, TxOut), - fee_utxo: (OutPoint, TxOut), - collateral_amount_to_receive: u64, - fee_amount: u64, - arguments: &OptionOfferArguments, - counterparty_recipient_script: Script, -) -> Result<(PartiallySignedTransaction, OptionOfferBranch), TransactionBuildError> { - let (collateral_outpoint, collateral_tx_out) = collateral_covenant_utxo; - let (premium_outpoint, premium_tx_out) = premium_covenant_utxo; - let (settlement_outpoint, settlement_tx_out) = settlement_utxo; - let (fee_outpoint, fee_tx_out) = fee_utxo; - - let (fee_asset_id, fee_change) = ( - fee_tx_out.explicit_asset()?, - fee_tx_out.remaining_after_required(fee_amount)?, - ); - let (collateral_asset_id, total_collateral) = collateral_tx_out.explicit()?; - let (premium_asset_id, total_premium) = premium_tx_out.explicit()?; - let (settlement_asset_id, total_settlement) = settlement_tx_out.explicit()?; - - let expected_collateral = arguments.get_collateral_asset_id(); - if collateral_asset_id != expected_collateral { - return Err(TransactionBuildError::WrongSettlementAsset { - expected: expected_collateral.to_string(), - actual: collateral_asset_id.to_string(), - }); - } - - let expected_premium = arguments.get_premium_asset_id(); - if premium_asset_id != expected_premium { - return Err(TransactionBuildError::WrongSettlementAsset { - expected: expected_premium.to_string(), - actual: premium_asset_id.to_string(), - }); - } - - let expected_settlement = arguments.get_settlement_asset_id(); - if settlement_asset_id != expected_settlement { - return Err(TransactionBuildError::WrongSettlementAsset { - expected: expected_settlement.to_string(), - actual: settlement_asset_id.to_string(), - }); - } - - let settlement_amount_required = collateral_amount_to_receive - .checked_mul(arguments.collateral_per_contract()) - .ok_or(TransactionBuildError::InsufficientSettlementAsset { - required: u64::MAX, - available: total_settlement, - })?; - - let premium_amount_to_receive = collateral_amount_to_receive - .checked_mul(arguments.premium_per_collateral()) - .ok_or(TransactionBuildError::InsufficientSettlementAsset { - required: u64::MAX, - available: total_premium, - })?; - - if collateral_amount_to_receive > total_collateral { - return Err(TransactionBuildError::InsufficientCollateral { - required: collateral_amount_to_receive, - available: total_collateral, - }); - } - - if premium_amount_to_receive > total_premium { - return Err(TransactionBuildError::InsufficientSettlementAsset { - required: premium_amount_to_receive, - available: total_premium, - }); - } - - if settlement_amount_required > total_settlement { - return Err(TransactionBuildError::InsufficientSettlementAsset { - required: settlement_amount_required, - available: total_settlement, - }); - } - - let contract_script = collateral_tx_out.script_pubkey.clone(); - let change_recipient_script = fee_tx_out.script_pubkey.clone(); - - let mut pst = PartiallySignedTransaction::new_v2(); - - let mut collateral_input = Input::from_prevout(collateral_outpoint); - collateral_input.witness_utxo = Some(collateral_tx_out.clone()); - pst.add_input(collateral_input); - - let mut premium_input = Input::from_prevout(premium_outpoint); - premium_input.witness_utxo = Some(premium_tx_out.clone()); - pst.add_input(premium_input); - - let mut settlement_input = Input::from_prevout(settlement_outpoint); - settlement_input.witness_utxo = Some(settlement_tx_out.clone()); - pst.add_input(settlement_input); - - let mut fee_input = Input::from_prevout(fee_outpoint); - fee_input.witness_utxo = Some(fee_tx_out.clone()); - pst.add_input(fee_input); - - let is_change_needed = total_collateral != collateral_amount_to_receive; - let is_settlement_change_needed = total_settlement != settlement_amount_required; - let is_fee_change_needed = fee_change != 0; - - if is_change_needed { - pst.add_output(Output::new_explicit( - contract_script.clone(), - total_collateral - collateral_amount_to_receive, - collateral_asset_id, - None, - )); - - pst.add_output(Output::new_explicit( - contract_script.clone(), - total_premium - premium_amount_to_receive, - premium_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - contract_script, - settlement_amount_required, - settlement_asset_id, - None, - )); - - pst.add_output(Output::new_explicit( - counterparty_recipient_script.clone(), - collateral_amount_to_receive, - collateral_asset_id, - None, - )); - - pst.add_output(Output::new_explicit( - counterparty_recipient_script, - premium_amount_to_receive, - premium_asset_id, - None, - )); - - if is_settlement_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - total_settlement - settlement_amount_required, - settlement_asset_id, - None, - )); - } - - if is_fee_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script, - fee_change, - fee_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - Script::new(), - fee_amount, - fee_asset_id, - None, - )); - - pst.extract_tx()?.verify_tx_amt_proofs( - secp256k1::SECP256K1, - &[ - collateral_tx_out, - premium_tx_out, - settlement_tx_out, - fee_tx_out, - ], - )?; - - Ok(( - pst, - OptionOfferBranch::Exercise { - collateral_amount: collateral_amount_to_receive, - is_change_needed, - }, - )) -} diff --git a/crates/contracts/src/sdk/finance/option_offer/expiry.rs b/crates/contracts/src/sdk/finance/option_offer/expiry.rs deleted file mode 100644 index a8e630c..0000000 --- a/crates/contracts/src/sdk/finance/option_offer/expiry.rs +++ /dev/null @@ -1,127 +0,0 @@ -use crate::finance::option_offer::OptionOfferArguments; - -use crate::error::TransactionBuildError; - -use crate::sdk::validation::TxOutExt; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::{LockTime, OutPoint, Script, Sequence, TxOut}; - -/// Build PSET for user to reclaim collateral and premium after expiry. -/// -/// Only works after `EXPIRY_TIME` has passed. -/// Withdraws the full collateral and premium amounts - no partial withdrawal. -/// -/// # Layout -/// -/// - Input[0]: Collateral from covenant -/// - Input[1]: Premium from covenant -/// - Input[2]: Fee -/// - Output[0]: Collateral → user (full amount) -/// - Output[1]: Premium → user (full amount) -/// - Output[2]: Fee change (if any) -/// - Output[3]: Fee -/// -/// # Errors -/// -/// Returns an error if: -/// - The UTXO asset or amount validation fails (amounts and assets must be explicit) -/// - Collateral asset doesn't match expected -/// - Premium asset doesn't match expected -/// - Transaction extraction or amount proof verification fails -pub fn build_option_offer_expiry( - collateral_covenant_utxo: (OutPoint, TxOut), - premium_covenant_utxo: (OutPoint, TxOut), - fee_utxo: (OutPoint, TxOut), - fee_amount: u64, - arguments: &OptionOfferArguments, - user_recipient_script: Script, -) -> Result { - let (collateral_outpoint, collateral_tx_out) = collateral_covenant_utxo; - let (premium_outpoint, premium_tx_out) = premium_covenant_utxo; - let (fee_outpoint, fee_tx_out) = fee_utxo; - - let (fee_asset_id, fee_change) = ( - fee_tx_out.explicit_asset()?, - fee_tx_out.remaining_after_required(fee_amount)?, - ); - let (collateral_asset_id, collateral_amount) = collateral_tx_out.explicit()?; - let (premium_asset_id, premium_amount) = premium_tx_out.explicit()?; - - let expected_collateral = arguments.get_collateral_asset_id(); - if collateral_asset_id != expected_collateral { - return Err(TransactionBuildError::WrongSettlementAsset { - expected: expected_collateral.to_string(), - actual: collateral_asset_id.to_string(), - }); - } - - let expected_premium = arguments.get_premium_asset_id(); - if premium_asset_id != expected_premium { - return Err(TransactionBuildError::WrongSettlementAsset { - expected: expected_premium.to_string(), - actual: premium_asset_id.to_string(), - }); - } - - let change_recipient_script = fee_tx_out.script_pubkey.clone(); - - let mut pst = PartiallySignedTransaction::new_v2(); - - pst.global.tx_data.fallback_locktime = Some(LockTime::from_time(arguments.expiry_time())?); - - let mut collateral_input = Input::from_prevout(collateral_outpoint); - collateral_input.witness_utxo = Some(collateral_tx_out.clone()); - collateral_input.sequence = Some(Sequence::ENABLE_LOCKTIME_NO_RBF); - pst.add_input(collateral_input); - - let mut premium_input = Input::from_prevout(premium_outpoint); - premium_input.witness_utxo = Some(premium_tx_out.clone()); - premium_input.sequence = Some(Sequence::ENABLE_LOCKTIME_NO_RBF); - pst.add_input(premium_input); - - let mut fee_input = Input::from_prevout(fee_outpoint); - fee_input.witness_utxo = Some(fee_tx_out.clone()); - fee_input.sequence = Some(Sequence::ENABLE_LOCKTIME_NO_RBF); - pst.add_input(fee_input); - - let is_fee_change_needed = fee_change != 0; - - pst.add_output(Output::new_explicit( - user_recipient_script.clone(), - collateral_amount, - collateral_asset_id, - None, - )); - - pst.add_output(Output::new_explicit( - user_recipient_script, - premium_amount, - premium_asset_id, - None, - )); - - if is_fee_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script, - fee_change, - fee_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - Script::new(), - fee_amount, - fee_asset_id, - None, - )); - - pst.extract_tx()?.verify_tx_amt_proofs( - secp256k1::SECP256K1, - &[collateral_tx_out, premium_tx_out, fee_tx_out], - )?; - - Ok(pst) -} diff --git a/crates/contracts/src/sdk/finance/option_offer/mod.rs b/crates/contracts/src/sdk/finance/option_offer/mod.rs deleted file mode 100644 index 13aa033..0000000 --- a/crates/contracts/src/sdk/finance/option_offer/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod deposit; -mod exercise; -mod expiry; -mod withdraw; - -pub use deposit::*; -pub use exercise::*; -pub use expiry::*; -pub use withdraw::*; diff --git a/crates/contracts/src/sdk/finance/option_offer/withdraw.rs b/crates/contracts/src/sdk/finance/option_offer/withdraw.rs deleted file mode 100644 index c322f00..0000000 --- a/crates/contracts/src/sdk/finance/option_offer/withdraw.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::finance::option_offer::OptionOfferArguments; - -use crate::error::TransactionBuildError; -use crate::sdk::validation::TxOutExt; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::{OutPoint, Script, TxOut}; - -/// Build PSET for user to withdraw settlement asset from the covenant. -/// -/// Withdraws the full settlement asset amount - no partial withdrawal. -/// -/// # Layout -/// -/// - Input[0]: Settlement asset from covenant -/// - Input[1]: Fee -/// - Output[0]: Settlement asset → user (full amount) -/// - Output[1]: Fee change (if any) -/// - Output[2]: Fee -/// -/// # Errors -/// -/// Returns an error if: -/// - The UTXO asset or amount validation fails (amounts and assets must be explicit) -/// - Settlement asset doesn't match expected -/// - Transaction extraction or amount proof verification fails -pub fn build_option_offer_withdraw( - settlement_covenant_utxo: (OutPoint, TxOut), - fee_utxo: (OutPoint, TxOut), - fee_amount: u64, - arguments: &OptionOfferArguments, - user_recipient_script: Script, -) -> Result { - let (settlement_outpoint, settlement_tx_out) = settlement_covenant_utxo; - let (fee_outpoint, fee_tx_out) = fee_utxo; - - let (fee_asset_id, fee_change) = ( - fee_tx_out.explicit_asset()?, - fee_tx_out.remaining_after_required(fee_amount)?, - ); - let (settlement_asset_id, settlement_amount) = settlement_tx_out.explicit()?; - - let expected_settlement = arguments.get_settlement_asset_id(); - if settlement_asset_id != expected_settlement { - return Err(TransactionBuildError::WrongSettlementAsset { - expected: expected_settlement.to_string(), - actual: settlement_asset_id.to_string(), - }); - } - - let change_recipient_script = fee_tx_out.script_pubkey.clone(); - - let mut pst = PartiallySignedTransaction::new_v2(); - - let mut settlement_input = Input::from_prevout(settlement_outpoint); - settlement_input.witness_utxo = Some(settlement_tx_out.clone()); - pst.add_input(settlement_input); - - let mut fee_input = Input::from_prevout(fee_outpoint); - fee_input.witness_utxo = Some(fee_tx_out.clone()); - pst.add_input(fee_input); - - let is_fee_change_needed = fee_change != 0; - - pst.add_output(Output::new_explicit( - user_recipient_script, - settlement_amount, - settlement_asset_id, - None, - )); - - if is_fee_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script, - fee_change, - fee_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - Script::new(), - fee_amount, - fee_asset_id, - None, - )); - - pst.extract_tx()? - .verify_tx_amt_proofs(secp256k1::SECP256K1, &[settlement_tx_out, fee_tx_out])?; - - Ok(pst) -} diff --git a/crates/contracts/src/sdk/finance/options/cancellation_option.rs b/crates/contracts/src/sdk/finance/options/cancellation_option.rs deleted file mode 100644 index 2a22e9c..0000000 --- a/crates/contracts/src/sdk/finance/options/cancellation_option.rs +++ /dev/null @@ -1,152 +0,0 @@ -use crate::finance::options::OptionsArguments; -use crate::finance::options::build_witness::OptionBranch; - -use crate::error::TransactionBuildError; -use crate::sdk::validation::TxOutExt; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::{OutPoint, Script, Sequence, TxOut}; - -/// Cancel an option contract by burning option and grantor tokens. -/// -/// # Errors -/// -/// Returns an error if: -/// - The UTXO asset or amount (fee included) validation fails (ALL amounts and assets are expected to be explicit) -/// - Insufficient collateral in comparison to amount of option and grantor tokens to burn -/// - Transaction extraction or amount proof verification fails -#[allow(clippy::too_many_lines)] -pub fn build_option_cancellation( - collateral_utxo: (OutPoint, TxOut), - option_asset_utxo: (OutPoint, TxOut), - grantor_asset_utxo: (OutPoint, TxOut), - fee_utxo: (OutPoint, TxOut), - option_arguments: &OptionsArguments, - amount_to_burn: u64, - fee_amount: u64, -) -> Result<(PartiallySignedTransaction, OptionBranch), TransactionBuildError> { - let (collateral_out_point, collateral_tx_out) = collateral_utxo; - let (option_out_point, option_tx_out) = option_asset_utxo; - let (grantor_out_point, grantor_tx_out) = grantor_asset_utxo; - let (fee_out_point, fee_tx_out) = fee_utxo; - - let (fee_asset_id, total_lbtc_left) = ( - fee_tx_out.explicit_asset()?, - fee_tx_out.remaining_after_required(fee_amount)?, - ); - let (collateral_asset_id, total_collateral) = collateral_tx_out.explicit()?; - - let collateral_amount_to_withdraw = amount_to_burn * option_arguments.collateral_per_contract(); - - if collateral_amount_to_withdraw > total_collateral { - return Err(TransactionBuildError::InsufficientCollateral { - required: collateral_amount_to_withdraw, - available: total_collateral, - }); - } - - let (option_token_id, total_option_token_amount) = option_tx_out.explicit()?; - let (grantor_token_id, total_grantor_token_amount) = grantor_tx_out.explicit()?; - - let change_recipient_script = fee_tx_out.script_pubkey.clone(); - - let mut pst = PartiallySignedTransaction::new_v2(); - - let mut collateral_input = Input::from_prevout(collateral_out_point); - collateral_input.witness_utxo = Some(collateral_tx_out.clone()); - collateral_input.sequence = Some(Sequence::ZERO); - pst.add_input(collateral_input); - - let mut option_input = Input::from_prevout(option_out_point); - option_input.witness_utxo = Some(option_tx_out.clone()); - option_input.sequence = Some(Sequence::ZERO); - pst.add_input(option_input); - - let mut grantor_input = Input::from_prevout(grantor_out_point); - grantor_input.witness_utxo = Some(grantor_tx_out.clone()); - grantor_input.sequence = Some(Sequence::ZERO); - pst.add_input(grantor_input); - - let mut fee_input = Input::from_prevout(fee_out_point); - fee_input.witness_utxo = Some(fee_tx_out.clone()); - fee_input.sequence = Some(Sequence::ZERO); - pst.add_input(fee_input); - - let is_collateral_change_needed = total_collateral != collateral_amount_to_withdraw; - let is_option_change_needed = total_option_token_amount != amount_to_burn; - let is_grantor_change_needed = total_grantor_token_amount != amount_to_burn; - let is_lbtc_change_needed = total_lbtc_left != 0; - - if is_collateral_change_needed { - pst.add_output(Output::new_explicit( - collateral_tx_out.script_pubkey.clone(), - total_collateral - collateral_amount_to_withdraw, - collateral_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - amount_to_burn, - option_token_id, - None, - )); - - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - amount_to_burn, - grantor_token_id, - None, - )); - - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - collateral_amount_to_withdraw, - collateral_asset_id, - None, - )); - - if is_option_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - total_option_token_amount - amount_to_burn, - option_token_id, - None, - )); - } - - if is_grantor_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - total_grantor_token_amount - amount_to_burn, - grantor_token_id, - None, - )); - } - - if is_lbtc_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script, - total_lbtc_left, - fee_asset_id, - None, - )); - } - - pst.add_output(Output::from_txout(TxOut::new_fee(fee_amount, fee_asset_id))); - - pst.extract_tx()?.verify_tx_amt_proofs( - secp256k1::SECP256K1, - &[collateral_tx_out, option_tx_out, grantor_tx_out, fee_tx_out], - )?; - - let option_branch = OptionBranch::Cancellation { - is_change_needed: is_collateral_change_needed, - amount_to_burn, - collateral_amount_to_withdraw, - }; - - Ok((pst, option_branch)) -} diff --git a/crates/contracts/src/sdk/finance/options/creation_option.rs b/crates/contracts/src/sdk/finance/options/creation_option.rs deleted file mode 100644 index 422ee08..0000000 --- a/crates/contracts/src/sdk/finance/options/creation_option.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::finance::options::OptionsArguments; -use crate::finance::options::get_options_address; - -use crate::error::TransactionBuildError; - -use crate::sdk::taproot_pubkey_gen::TaprootPubkeyGen; -use crate::sdk::validation::TxOutExt; -use crate::sdk::{IssuanceInputConstraints, IssuanceTxConstraints, verify_issuance}; - -use std::collections::HashMap; - -use simplicityhl::elements::secp256k1_zkp::PublicKey; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::confidential::{AssetBlindingFactor, ValueBlindingFactor}; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::secp256k1_zkp::rand::thread_rng; -use simplicityhl::elements::{OutPoint, Script, Sequence, TxOut, TxOutSecrets}; -use simplicityhl_core::PLACEHOLDER_ISSUANCE_VALUE; -use simplicityhl_core::SimplicityNetwork; - -/// Create a new option contract with option and grantor token issuance. -/// -/// # Errors -/// -/// Returns an error if: -/// - The UTXO values are not explicit -/// - The taproot pubkey generation fails -/// - Transaction blinding fails -/// - Transaction extraction or amount proof verification fails -#[allow(clippy::too_many_lines)] -pub fn build_option_creation( - blinding_key: &PublicKey, - first_fee_utxo: (OutPoint, TxOut), - second_fee_utxo: (OutPoint, TxOut), - option_arguments: &OptionsArguments, - issuance_asset_entropy: [u8; 32], - fee_amount: u64, - network: SimplicityNetwork, -) -> Result<(PartiallySignedTransaction, TaprootPubkeyGen), TransactionBuildError> { - let (first_out_point, first_tx_out) = first_fee_utxo; - let (second_out_point, second_tx_out) = second_fee_utxo; - - let (first_asset_id, first_value) = first_tx_out.explicit()?; - let (second_asset_id, second_value) = second_tx_out.explicit()?; - - if first_asset_id != second_asset_id { - return Err(TransactionBuildError::FeeUtxoAssetMismatch { - first_script_hash: first_tx_out.script_pubkey.script_hash().to_string(), - first_asset: first_asset_id.to_string(), - second_script_hash: second_tx_out.script_pubkey.script_hash().to_string(), - second_asset: second_asset_id.to_string(), - }); - } - - let total_input_fee = first_value + second_value; - - first_tx_out.remaining_after_required(fee_amount.saturating_sub(second_value))?; - - let change_recipient_script = first_tx_out.script_pubkey.clone(); - - let mut first_issuance_tx = Input::from_prevout(first_out_point); - first_issuance_tx.witness_utxo = Some(first_tx_out.clone()); - first_issuance_tx.issuance_value_amount = None; - first_issuance_tx.issuance_inflation_keys = Some(1); - first_issuance_tx.issuance_asset_entropy = Some(issuance_asset_entropy); - first_issuance_tx.blinded_issuance = Some(0x00); - first_issuance_tx.sequence = Some(Sequence::ZERO); - - let mut second_issuance_tx = Input::from_prevout(second_out_point); - second_issuance_tx.witness_utxo = Some(second_tx_out.clone()); - second_issuance_tx.issuance_value_amount = None; - second_issuance_tx.issuance_inflation_keys = Some(1); - second_issuance_tx.issuance_asset_entropy = Some(issuance_asset_entropy); - second_issuance_tx.blinded_issuance = Some(0x00); - second_issuance_tx.sequence = Some(Sequence::ZERO); - - let (first_issuance_token, first_reissuance_asset) = first_issuance_tx.issuance_ids(); - let (second_issuance_token, second_reissuance_asset) = second_issuance_tx.issuance_ids(); - - let expected_option = option_arguments.get_option_token_ids(); - if (first_issuance_token, first_reissuance_asset) != expected_option { - return Err(TransactionBuildError::OptionTokenMismatch { - expected_token: expected_option.0.to_string(), - expected_reissuance: expected_option.1.to_string(), - actual_token: first_issuance_token.to_string(), - actual_reissuance: first_reissuance_asset.to_string(), - }); - } - - let expected_grantor = option_arguments.get_grantor_token_ids(); - if (second_issuance_token, second_reissuance_asset) != expected_grantor { - return Err(TransactionBuildError::GrantorTokenMismatch { - expected_token: expected_grantor.0.to_string(), - expected_reissuance: expected_grantor.1.to_string(), - actual_token: second_issuance_token.to_string(), - actual_reissuance: second_reissuance_asset.to_string(), - }); - } - - let options_taproot_pubkey_gen = - TaprootPubkeyGen::from(option_arguments, network, &get_options_address)?; - - let mut pst = PartiallySignedTransaction::new_v2(); - - pst.add_input(first_issuance_tx); - pst.add_input(second_issuance_tx); - - let mut output = Output::new_explicit( - options_taproot_pubkey_gen.address.script_pubkey(), - 1, - first_reissuance_asset, - Some((*blinding_key).into()), - ); - output.blinder_index = Some(0); - pst.add_output(output); - - let mut output = Output::new_explicit( - options_taproot_pubkey_gen.address.script_pubkey(), - 1, - second_reissuance_asset, - Some((*blinding_key).into()), - ); - output.blinder_index = Some(1); - pst.add_output(output); - - pst.add_output(Output::new_explicit( - change_recipient_script, - total_input_fee - fee_amount, - second_asset_id, - None, - )); - - pst.add_output(Output::new_explicit( - Script::new(), - fee_amount, - second_asset_id, - None, - )); - - let first_input_secrets = TxOutSecrets { - asset_bf: AssetBlindingFactor::zero(), - value_bf: ValueBlindingFactor::zero(), - value: PLACEHOLDER_ISSUANCE_VALUE, - asset: second_asset_id, - }; - let second_input_secrets = TxOutSecrets { - asset_bf: AssetBlindingFactor::zero(), - value_bf: ValueBlindingFactor::zero(), - value: PLACEHOLDER_ISSUANCE_VALUE, - asset: second_asset_id, - }; - - let mut inp_txout_sec = HashMap::new(); - inp_txout_sec.insert(0, first_input_secrets); - inp_txout_sec.insert(1, second_input_secrets); - - verify_issuance( - &pst.extract_tx()?, - &IssuanceTxConstraints { - inputs: vec![ - IssuanceInputConstraints { - input_idx: 0, - issuance_destination: None, - reissuance_destination: Some(( - options_taproot_pubkey_gen.address.script_pubkey(), - 1, - None, - )), - }, - IssuanceInputConstraints { - input_idx: 1, - issuance_destination: None, - reissuance_destination: Some(( - options_taproot_pubkey_gen.address.script_pubkey(), - 1, - None, - )), - }, - ], - allow_unconstrained_issuances: false, - }, - )?; - - pst.blind_last(&mut thread_rng(), secp256k1::SECP256K1, &inp_txout_sec)?; - - pst.extract_tx()? - .verify_tx_amt_proofs(secp256k1::SECP256K1, &[first_tx_out, second_tx_out])?; - - Ok((pst, options_taproot_pubkey_gen)) -} diff --git a/crates/contracts/src/sdk/finance/options/exercise_option.rs b/crates/contracts/src/sdk/finance/options/exercise_option.rs deleted file mode 100644 index deb3b79..0000000 --- a/crates/contracts/src/sdk/finance/options/exercise_option.rs +++ /dev/null @@ -1,204 +0,0 @@ -use crate::finance::options::OptionsArguments; -use crate::finance::options::build_witness::OptionBranch; - -use crate::error::TransactionBuildError; - -use crate::sdk::validation::TxOutExt; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::{LockTime, OutPoint, Script, Sequence, TxOut}; - -/// Exercise an option by burning option tokens to withdraw collateral. -/// -/// # Arguments -/// * `fee_utxo` - Optional separate fee UTXO. When `None`, the fee is deducted from the -/// settlement asset UTXO (only valid when settlement asset is LBTC). -/// -/// # Errors -/// -/// Returns an error if: -/// - The UTXO asset or amount (fee included) validation fails (ALL amounts and assets are expected to be explicit) -/// - Insufficient collateral or settlement asset -/// - Settlement asset id mismatch -/// - Transaction extraction or amount proof verification fails -#[allow(clippy::too_many_lines)] -pub fn build_option_exercise( - collateral_utxo: (OutPoint, TxOut), - option_asset_utxo: (OutPoint, TxOut), - asset_utxo: (OutPoint, TxOut), - fee_utxo: Option<(OutPoint, TxOut)>, - amount_to_burn: u64, - fee_amount: u64, - option_arguments: &OptionsArguments, -) -> Result<(PartiallySignedTransaction, OptionBranch), TransactionBuildError> { - let (collateral_out_point, collateral_tx_out) = collateral_utxo; - let (option_out_point, option_tx_out) = option_asset_utxo; - let (asset_out_point, asset_tx_out) = asset_utxo; - - let (collateral_asset_id, total_collateral) = collateral_tx_out.explicit()?; - let (option_token_id, total_option_token_amount) = option_tx_out.explicit()?; - let (settlement_asset_id, total_asset_amount) = asset_tx_out.explicit()?; - - let collateral_amount_to_get = amount_to_burn * option_arguments.collateral_per_contract(); - let asset_amount_to_pay = amount_to_burn * option_arguments.settlement_per_contract(); - - if collateral_amount_to_get > total_collateral { - return Err(TransactionBuildError::InsufficientCollateral { - required: collateral_amount_to_get, - available: total_collateral, - }); - } - - let contract_script = collateral_tx_out.script_pubkey.clone(); - - let mut pst = PartiallySignedTransaction::new_v2(); - pst.global.tx_data.fallback_locktime = - Some(LockTime::from_time(option_arguments.start_time())?); - - let mut collateral_input = Input::from_prevout(collateral_out_point); - collateral_input.witness_utxo = Some(collateral_tx_out.clone()); - collateral_input.sequence = Some(Sequence::ZERO); - pst.add_input(collateral_input); - - let mut option_input = Input::from_prevout(option_out_point); - option_input.witness_utxo = Some(option_tx_out.clone()); - option_input.sequence = Some(Sequence::ZERO); - pst.add_input(option_input); - - let mut asset_input = Input::from_prevout(asset_out_point); - asset_input.witness_utxo = Some(asset_tx_out.clone()); - asset_input.sequence = Some(Sequence::ZERO); - pst.add_input(asset_input); - - let has_separate_fee_utxo = fee_utxo.is_some(); - let (change_recipient_script, fee_asset_id, total_lbtc_left, utxos_for_verification) = - if let Some((fee_out_point, fee_tx_out)) = fee_utxo { - let (fee_asset_id, total_lbtc_left) = ( - fee_tx_out.explicit_asset()?, - fee_tx_out.remaining_after_required(fee_amount)?, - ); - - if asset_amount_to_pay > total_asset_amount { - return Err(TransactionBuildError::InsufficientSettlementAsset { - required: asset_amount_to_pay, - available: total_asset_amount, - }); - } - - let change_recipient_script = fee_tx_out.script_pubkey.clone(); - - let mut fee_input = Input::from_prevout(fee_out_point); - fee_input.witness_utxo = Some(fee_tx_out.clone()); - fee_input.sequence = Some(Sequence::ZERO); - pst.add_input(fee_input); - - ( - change_recipient_script, - fee_asset_id, - total_lbtc_left, - vec![collateral_tx_out, option_tx_out, asset_tx_out, fee_tx_out], - ) - } else { - let total_required = asset_amount_to_pay + fee_amount; - if total_required > total_asset_amount { - return Err(TransactionBuildError::InsufficientSettlementAsset { - required: total_required, - available: total_asset_amount, - }); - } - - let change_recipient_script = asset_tx_out.script_pubkey.clone(); - let total_lbtc_left = total_asset_amount - asset_amount_to_pay - fee_amount; - - ( - change_recipient_script, - settlement_asset_id, - total_lbtc_left, - vec![collateral_tx_out, option_tx_out, asset_tx_out], - ) - }; - - let is_collateral_change_needed = total_collateral != collateral_amount_to_get; - let is_option_token_change_needed = total_option_token_amount != amount_to_burn; - let is_lbtc_change_needed = total_lbtc_left != 0; - - let is_asset_change_needed = has_separate_fee_utxo && total_asset_amount != asset_amount_to_pay; - - if is_collateral_change_needed { - pst.add_output(Output::new_explicit( - contract_script.clone(), - total_collateral - collateral_amount_to_get, - collateral_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - amount_to_burn, - option_token_id, - None, - )); - - pst.add_output(Output::new_explicit( - contract_script, - asset_amount_to_pay, - settlement_asset_id, - None, - )); - - if is_option_token_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - total_option_token_amount - amount_to_burn, - option_token_id, - None, - )); - } - - if is_asset_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - total_asset_amount - asset_amount_to_pay, - settlement_asset_id, - None, - )); - } - - if is_lbtc_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - total_lbtc_left, - fee_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - change_recipient_script, - collateral_amount_to_get, - collateral_asset_id, - None, - )); - - pst.add_output(Output::new_explicit( - Script::new(), - fee_amount, - fee_asset_id, - None, - )); - - pst.extract_tx()? - .verify_tx_amt_proofs(secp256k1::SECP256K1, &utxos_for_verification)?; - - Ok(( - pst, - OptionBranch::Exercise { - is_change_needed: is_collateral_change_needed, - amount_to_burn, - collateral_amount_to_get, - asset_amount: asset_amount_to_pay, - }, - )) -} diff --git a/crates/contracts/src/sdk/finance/options/expiry_option.rs b/crates/contracts/src/sdk/finance/options/expiry_option.rs deleted file mode 100644 index cca54c9..0000000 --- a/crates/contracts/src/sdk/finance/options/expiry_option.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::finance::options::OptionsArguments; -use crate::finance::options::build_witness::OptionBranch; - -use crate::error::TransactionBuildError; - -use crate::sdk::validation::TxOutExt; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::{LockTime, OutPoint, Script, Sequence, TxOut}; - -/// Withdraw collateral at option expiry by burning grantor tokens. -/// -/// # Errors -/// -/// Returns an error if: -/// - The UTXO asset or amount (fee included) validation fails (ALL amounts and assets are expected to be explicit) -/// - Insufficient collateral in comparison to amount of grantor tokens to burn -/// - Transaction extraction or amount proof verification fails -pub fn build_option_expiry( - collateral_utxo: (OutPoint, TxOut), - grantor_asset_utxo: (OutPoint, TxOut), - fee_utxo: (OutPoint, TxOut), - grantor_token_amount_to_burn: u64, - fee_amount: u64, - option_arguments: &OptionsArguments, -) -> Result<(PartiallySignedTransaction, OptionBranch), TransactionBuildError> { - let (collateral_out_point, collateral_tx_out) = collateral_utxo; - let (grantor_out_point, grantor_tx_out) = grantor_asset_utxo; - let (fee_out_point, fee_tx_out) = fee_utxo; - - let (fee_asset_id, total_lbtc_left) = ( - fee_tx_out.explicit_asset()?, - fee_tx_out.remaining_after_required(fee_amount)?, - ); - let (collateral_asset_id, total_collateral) = collateral_tx_out.explicit()?; - let (grantor_token_id, total_grantor_token_amount) = grantor_tx_out.explicit()?; - - let (expected_grantor_token_id, _) = option_arguments.get_grantor_token_ids(); - - if grantor_token_id != expected_grantor_token_id { - return Err(TransactionBuildError::WrongGrantorToken { - expected: expected_grantor_token_id.to_string(), - actual: grantor_token_id.to_string(), - }); - } - - let collateral_amount = - grantor_token_amount_to_burn.saturating_mul(option_arguments.collateral_per_contract()); - - if collateral_amount > total_collateral { - return Err(TransactionBuildError::InsufficientCollateral { - required: collateral_amount, - available: total_collateral, - }); - } - - let change_recipient_script = fee_tx_out.script_pubkey.clone(); - let contract_script = collateral_tx_out.script_pubkey.clone(); - - let mut pst = PartiallySignedTransaction::new_v2(); - pst.global.tx_data.fallback_locktime = - Some(LockTime::from_time(option_arguments.expiry_time())?); - - let mut collateral_input = Input::from_prevout(collateral_out_point); - collateral_input.witness_utxo = Some(collateral_tx_out.clone()); - collateral_input.sequence = Some(Sequence::ZERO); - pst.add_input(collateral_input); - - let mut grantor_input = Input::from_prevout(grantor_out_point); - grantor_input.witness_utxo = Some(grantor_tx_out.clone()); - grantor_input.sequence = Some(Sequence::ZERO); - pst.add_input(grantor_input); - - let mut fee_input = Input::from_prevout(fee_out_point); - fee_input.witness_utxo = Some(fee_tx_out.clone()); - fee_input.sequence = Some(Sequence::ZERO); - pst.add_input(fee_input); - - let is_collateral_change_needed = total_collateral != collateral_amount; - let is_grantor_change_needed = total_grantor_token_amount != grantor_token_amount_to_burn; - let is_lbtc_change_needed = total_lbtc_left != 0; - - if is_collateral_change_needed { - pst.add_output(Output::new_explicit( - contract_script, - total_collateral - collateral_amount, - collateral_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - grantor_token_amount_to_burn, - grantor_token_id, - None, - )); - - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - collateral_amount, - collateral_asset_id, - None, - )); - - if is_grantor_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - total_grantor_token_amount - grantor_token_amount_to_burn, - grantor_token_id, - None, - )); - } - - if is_lbtc_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script, - total_lbtc_left, - fee_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - Script::new(), - fee_amount, - fee_asset_id, - None, - )); - - pst.extract_tx()?.verify_tx_amt_proofs( - secp256k1::SECP256K1, - &[collateral_tx_out, grantor_tx_out, fee_tx_out], - )?; - - let option_branch = OptionBranch::Expiry { - is_change_needed: is_collateral_change_needed, - grantor_token_amount_to_burn, - collateral_amount_to_withdraw: collateral_amount, - }; - - Ok((pst, option_branch)) -} diff --git a/crates/contracts/src/sdk/finance/options/funding_option.rs b/crates/contracts/src/sdk/finance/options/funding_option.rs deleted file mode 100644 index 31693b9..0000000 --- a/crates/contracts/src/sdk/finance/options/funding_option.rs +++ /dev/null @@ -1,229 +0,0 @@ -use crate::finance::options::OptionsArguments; -use crate::finance::options::build_witness::{OptionBranch, blinding_factors_from_secrets}; - -use crate::error::TransactionBuildError; - -use crate::sdk::validation::TxOutExt; - -use std::collections::HashMap; - -use simplicityhl::elements::bitcoin::secp256k1::Keypair; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::secp256k1_zkp::SECP256K1; -use simplicityhl::elements::secp256k1_zkp::rand::thread_rng; -use simplicityhl::elements::{OutPoint, Script, Sequence, TxOut, TxOutSecrets}; - -/// Fund an option contract by depositing collateral and reissuing tokens. -/// -/// Returns a tuple of (PST, `OptionBranch::Funding`) with all blinding factors extracted. -/// -/// # Errors -/// -/// Returns an error if: -/// - The UTXO asset or amount validation fails (ALL amounts and assets are expected to be explicit) -/// - Transaction blinding fails -/// - Transaction extraction or amount proof verification fails -#[allow( - clippy::too_many_arguments, - clippy::too_many_lines, - clippy::similar_names -)] -pub fn build_option_funding( - blinding_keypair: &Keypair, - option_asset_utxo: (OutPoint, TxOut, TxOutSecrets), - grantor_asset_utxo: (OutPoint, TxOut, TxOutSecrets), - collateral_utxo: (OutPoint, TxOut), - fee_utxo: Option<&(OutPoint, TxOut)>, - option_arguments: &OptionsArguments, - collateral_amount: u64, - fee_amount: u64, -) -> Result<(PartiallySignedTransaction, OptionBranch), TransactionBuildError> { - let blinding_key = blinding_keypair.public_key(); - - let (option_out_point, option_tx_out, input_option_secrets) = option_asset_utxo; - let (grantor_out_point, grantor_tx_out, input_grantor_secrets) = grantor_asset_utxo; - - let (collateral_out_point, collateral_tx_out) = collateral_utxo; - - let (collateral_asset_id, total_collateral) = collateral_tx_out.explicit()?; - - let (option_asset_id, option_token_id) = option_arguments.get_option_token_ids(); - let (grantor_asset_id, grantor_token_id) = option_arguments.get_grantor_token_ids(); - - let option_token_amount = collateral_amount / option_arguments.collateral_per_contract(); - - let change_recipient_script = collateral_tx_out.script_pubkey.clone(); - let contract_script = option_tx_out.script_pubkey.clone(); - - let mut pst = PartiallySignedTransaction::new_v2(); - - let mut first_reissuance_tx = Input::from_prevout(option_out_point); - first_reissuance_tx.witness_utxo = Some(option_tx_out.clone()); - first_reissuance_tx.issuance_value_amount = Some(option_token_amount); - first_reissuance_tx.issuance_inflation_keys = None; - first_reissuance_tx.issuance_asset_entropy = Some(option_arguments.option_token_entropy()); - first_reissuance_tx.blinded_issuance = Some(0x00); - first_reissuance_tx.issuance_blinding_nonce = Some(input_option_secrets.asset_bf.into_inner()); - first_reissuance_tx.sequence = Some(Sequence::ZERO); - - let mut second_reissuance_tx = Input::from_prevout(grantor_out_point); - second_reissuance_tx.witness_utxo = Some(grantor_tx_out.clone()); - second_reissuance_tx.issuance_value_amount = Some(option_token_amount); - second_reissuance_tx.issuance_inflation_keys = None; - second_reissuance_tx.issuance_asset_entropy = Some(option_arguments.grantor_token_entropy()); - second_reissuance_tx.blinded_issuance = Some(0x00); - second_reissuance_tx.issuance_blinding_nonce = - Some(input_grantor_secrets.asset_bf.into_inner()); - second_reissuance_tx.sequence = Some(Sequence::ZERO); - - let mut collateral_tx = Input::from_prevout(collateral_out_point); - collateral_tx.witness_utxo = Some(collateral_tx_out.clone()); - collateral_tx.sequence = Some(Sequence::ZERO); - - pst.add_input(first_reissuance_tx); - pst.add_input(second_reissuance_tx); - pst.add_input(collateral_tx); - - if let Some((fee_out_point, fee_tx_out)) = fee_utxo { - let mut fee_tx = Input::from_prevout(*fee_out_point); - fee_tx.witness_utxo = Some(fee_tx_out.clone()); - fee_tx.sequence = Some(Sequence::ZERO); - pst.add_input(fee_tx); - } - - let mut output = Output::new_explicit( - contract_script.clone(), - 1, - option_token_id, - Some(blinding_key.into()), - ); - output.blinder_index = Some(0); - pst.add_output(output); - - let mut output = Output::new_explicit( - contract_script.clone(), - 1, - grantor_token_id, - Some(blinding_key.into()), - ); - output.blinder_index = Some(1); - pst.add_output(output); - - pst.add_output(Output::new_explicit( - contract_script, - collateral_amount, - collateral_asset_id, - None, - )); - - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - option_token_amount, - option_asset_id, - None, - )); - - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - option_token_amount, - grantor_asset_id, - None, - )); - - let utxos = if let Some((_, fee_tx_out)) = fee_utxo { - let total_fee = fee_tx_out.remaining_after_required(fee_amount)?; - - let is_collateral_change_needed = total_collateral != collateral_amount; - let is_fee_change_needed = total_fee != 0; - - if is_collateral_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - total_collateral - collateral_amount, - collateral_asset_id, - None, - )); - } - - if is_fee_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script, - total_fee, - fee_tx_out.explicit_asset()?, - None, - )); - } - - pst.add_output(Output::new_explicit( - Script::new(), - fee_amount, - fee_tx_out.explicit_asset()?, - None, - )); - - vec![ - option_tx_out, - grantor_tx_out, - collateral_tx_out, - fee_tx_out.clone(), - ] - } else { - let is_collateral_change_needed = total_collateral != (collateral_amount + fee_amount); - - if is_collateral_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script, - total_collateral - collateral_amount - fee_amount, - collateral_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - Script::new(), - fee_amount, - collateral_asset_id, - None, - )); - - vec![option_tx_out, grantor_tx_out, collateral_tx_out] - }; - - let mut inp_tx_out_sec = HashMap::new(); - inp_tx_out_sec.insert(0, input_option_secrets); - inp_tx_out_sec.insert(1, input_grantor_secrets); - - pst.blind_last(&mut thread_rng(), SECP256K1, &inp_tx_out_sec)?; - - let tx = pst.extract_tx()?; - - tx.verify_tx_amt_proofs(SECP256K1, &utxos)?; - - let output_option_secrets = tx.output[0].unblind(SECP256K1, blinding_keypair.secret_key())?; - let output_grantor_secrets = tx.output[1].unblind(SECP256K1, blinding_keypair.secret_key())?; - - let (input_option_abf, input_option_vbf) = blinding_factors_from_secrets(&input_option_secrets); - let (input_grantor_abf, input_grantor_vbf) = - blinding_factors_from_secrets(&input_grantor_secrets); - - let (output_option_abf, output_option_vbf) = - blinding_factors_from_secrets(&output_option_secrets); - let (output_grantor_abf, output_grantor_vbf) = - blinding_factors_from_secrets(&output_grantor_secrets); - - let expected_asset_amount = option_token_amount * option_arguments.settlement_per_contract(); - - let option_branch = OptionBranch::Funding { - expected_asset_amount, - input_option_abf, - input_option_vbf, - input_grantor_abf, - input_grantor_vbf, - output_option_abf, - output_option_vbf, - output_grantor_abf, - output_grantor_vbf, - }; - - Ok((pst, option_branch)) -} diff --git a/crates/contracts/src/sdk/finance/options/mod.rs b/crates/contracts/src/sdk/finance/options/mod.rs deleted file mode 100644 index 8ccd6eb..0000000 --- a/crates/contracts/src/sdk/finance/options/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod cancellation_option; -mod creation_option; -mod exercise_option; -mod expiry_option; -mod funding_option; -mod settlement_option; - -pub use cancellation_option::*; -pub use creation_option::*; -pub use exercise_option::*; -pub use expiry_option::*; -pub use funding_option::*; -pub use settlement_option::*; diff --git a/crates/contracts/src/sdk/finance/options/settlement_option.rs b/crates/contracts/src/sdk/finance/options/settlement_option.rs deleted file mode 100644 index 8a8e65c..0000000 --- a/crates/contracts/src/sdk/finance/options/settlement_option.rs +++ /dev/null @@ -1,152 +0,0 @@ -use crate::finance::options::OptionsArguments; -use crate::finance::options::build_witness::OptionBranch; - -use crate::error::TransactionBuildError; - -use crate::sdk::validation::TxOutExt; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::elements::{LockTime, OutPoint, Script, Sequence, TxOut}; - -/// Settle an option by burning grantor tokens to withdraw settlement asset. -/// -/// # Errors -/// -/// Returns an error if: -/// - The UTXO asset or amount (fee included) validation fails (ALL amounts and assets are expected to be explicit) -/// - Insufficient settlement asset in comparison to amount of grantor tokens to burn -/// - Transaction extraction or amount proof verification fails -#[allow(clippy::too_many_lines)] -pub fn build_option_settlement( - settlement_asset_utxo: (OutPoint, TxOut), - grantor_asset_utxo: (OutPoint, TxOut), - fee_utxo: (OutPoint, TxOut), - grantor_token_amount_to_burn: u64, - fee_amount: u64, - option_arguments: &OptionsArguments, -) -> Result<(PartiallySignedTransaction, OptionBranch), TransactionBuildError> { - let (settlement_out_point, settlement_tx_out) = settlement_asset_utxo; - let (grantor_out_point, grantor_tx_out) = grantor_asset_utxo; - let (fee_out_point, fee_tx_out) = fee_utxo; - - let (fee_asset_id, total_lbtc_left) = ( - fee_tx_out.explicit_asset()?, - fee_tx_out.remaining_after_required(fee_amount)?, - ); - let (settlement_asset_id, available_settlement_asset) = settlement_tx_out.explicit()?; - let (grantor_token_id, total_grantor_token_amount) = grantor_tx_out.explicit()?; - - let (expected_grantor_token_id, _) = option_arguments.get_grantor_token_ids(); - let expected_settlement_asset_id = option_arguments.get_settlement_asset_id(); - - if grantor_token_id != expected_grantor_token_id { - return Err(TransactionBuildError::WrongGrantorToken { - expected: expected_grantor_token_id.to_string(), - actual: grantor_token_id.to_string(), - }); - } - if settlement_asset_id != expected_settlement_asset_id { - return Err(TransactionBuildError::WrongSettlementAsset { - expected: expected_settlement_asset_id.to_string(), - actual: settlement_asset_id.to_string(), - }); - } - - let asset_amount = - grantor_token_amount_to_burn.saturating_mul(option_arguments.settlement_per_contract()); - - if asset_amount > available_settlement_asset { - return Err(TransactionBuildError::InsufficientSettlementAsset { - required: asset_amount, - available: available_settlement_asset, - }); - } - - let change_recipient_script = fee_tx_out.script_pubkey.clone(); - let contract_script = settlement_tx_out.script_pubkey.clone(); - - let mut pst = PartiallySignedTransaction::new_v2(); - pst.global.tx_data.fallback_locktime = - Some(LockTime::from_time(option_arguments.start_time())?); - - let mut settlement_input = Input::from_prevout(settlement_out_point); - settlement_input.witness_utxo = Some(settlement_tx_out.clone()); - settlement_input.sequence = Some(Sequence::ZERO); - pst.add_input(settlement_input); - - let mut grantor_input = Input::from_prevout(grantor_out_point); - grantor_input.witness_utxo = Some(grantor_tx_out.clone()); - grantor_input.sequence = Some(Sequence::ZERO); - pst.add_input(grantor_input); - - let mut fee_input = Input::from_prevout(fee_out_point); - fee_input.witness_utxo = Some(fee_tx_out.clone()); - fee_input.sequence = Some(Sequence::ZERO); - pst.add_input(fee_input); - - let is_settlement_change_needed = available_settlement_asset != asset_amount; - let is_grantor_change_needed = total_grantor_token_amount != grantor_token_amount_to_burn; - let is_lbtc_change_needed = total_lbtc_left != 0; - - if is_settlement_change_needed { - pst.add_output(Output::new_explicit( - contract_script, - available_settlement_asset - asset_amount, - settlement_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - Script::new_op_return(b"burn"), - grantor_token_amount_to_burn, - grantor_token_id, - None, - )); - - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - asset_amount, - settlement_asset_id, - None, - )); - - if is_grantor_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - total_grantor_token_amount - grantor_token_amount_to_burn, - grantor_token_id, - None, - )); - } - - if is_lbtc_change_needed { - pst.add_output(Output::new_explicit( - change_recipient_script, - total_lbtc_left, - fee_asset_id, - None, - )); - } - - pst.add_output(Output::new_explicit( - Script::new(), - fee_amount, - fee_asset_id, - None, - )); - - pst.extract_tx()?.verify_tx_amt_proofs( - secp256k1::SECP256K1, - &[settlement_tx_out, grantor_tx_out, fee_tx_out], - )?; - - let option_branch = OptionBranch::Settlement { - is_change_needed: is_settlement_change_needed, - grantor_token_amount_to_burn, - asset_amount, - }; - - Ok((pst, option_branch)) -} diff --git a/crates/contracts/src/sdk/issuance_validation/test_data/option_arguments.hex b/crates/contracts/src/sdk/issuance_validation/test_data/option_arguments.hex deleted file mode 100644 index 04aa22a..0000000 --- a/crates/contracts/src/sdk/issuance_validation/test_data/option_arguments.hex +++ /dev/null @@ -1 +0,0 @@ -fc478a8469fccb8d8469fb1027649451dab675641203b4b2f8117c53ba1587adbb0720da6e7066f7416417a7fcb6499a818545f6bae39fc03b637f2a4e1e64e590cac1bc3a6f6d71aa4443654c1416f4e8decfb1463737260a050dd12ba218738efe1f35a6366f23fb992f71dc11414ff757f067bb505e5ec11c8cb4952fe5f9b31b86de7f0db658718123aa3a500000414ff757f067bb505e5ec11c8cb4952fe5f9b31b86de7f0db658718123aa3a500100 \ No newline at end of file diff --git a/crates/contracts/src/sdk/issuance_validation/test_data/taproot.hex b/crates/contracts/src/sdk/issuance_validation/test_data/taproot.hex deleted file mode 100644 index e2c9a87..0000000 --- a/crates/contracts/src/sdk/issuance_validation/test_data/taproot.hex +++ /dev/null @@ -1 +0,0 @@ -e5185c8c07ae1df03b34258e3792dafbe40e9790dc98f3ba175205d36c182ad6:028ce2c40723228499f5e1847864d2dbd21401f7eb5389b76406eafaefcd306fc4:tex1px3gu9szqg7447tf7karhw0j6jatvzr4ehucny673dfjnx9p0rt8qg0sfgm \ No newline at end of file diff --git a/crates/contracts/src/sdk/mod.rs b/crates/contracts/src/sdk/mod.rs deleted file mode 100644 index fa44faf..0000000 --- a/crates/contracts/src/sdk/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[cfg(feature = "sdk-basic")] -mod basic; -#[cfg(any(feature = "finance-option-offer", feature = "finance-options"))] -mod finance; -#[cfg(feature = "smt-storage")] -mod storage; - -pub mod issuance_validation; -pub mod taproot_pubkey_gen; -pub mod validation; - -#[cfg(feature = "sdk-basic")] -pub use basic::*; -#[cfg(any(feature = "finance-option-offer", feature = "finance-options"))] -pub use finance::*; - -pub use issuance_validation::*; -#[cfg(feature = "smt-storage")] -pub use storage::*; diff --git a/crates/contracts/src/sdk/storage/get_storage_address.rs b/crates/contracts/src/sdk/storage/get_storage_address.rs deleted file mode 100644 index 005e2d6..0000000 --- a/crates/contracts/src/sdk/storage/get_storage_address.rs +++ /dev/null @@ -1,48 +0,0 @@ -use simplicityhl::elements::schnorr::XOnlyPublicKey; -use simplicityhl::simplicity::elements::Address; -use simplicityhl::simplicity::elements::Script; -use simplicityhl::simplicity::elements::taproot::TaprootSpendInfo; -use simplicityhl_core::SimplicityNetwork; - -use crate::error::TransactionBuildError; -use crate::smt_storage::{ - DEPTH, SparseMerkleTree, get_smt_storage_compiled_program, smt_storage_taproot_spend_info, -}; - -/// Derives the Taproot address for the SMT storage contract based on its initial state. -/// -/// This function calculates the script pubkey by committing to the Simplicity program -/// configured with the provided `storage_bytes` (root hash) and `path`. It then -/// encodes this script into a network-specific address. -/// -/// # Errors -/// -/// Returns an error if: -/// - The function signature requires a `Result` for consistency with the builder API, -/// though the current implementation primarily panics on failure rather than returning `Err`. -/// -/// # Panics -/// -/// Panics if: -/// - The generated script is invalid for address creation (e.g., invalid witness program). -pub fn get_storage_address( - storage_key: &XOnlyPublicKey, - storage_bytes: &[u8; 32], - path: [bool; DEPTH], - network: SimplicityNetwork, -) -> Result { - let mut smt = SparseMerkleTree::new(); - let merkle_hashes = smt.update(storage_bytes, path); - - let merkle_data = std::array::from_fn(|i| (merkle_hashes[DEPTH - i - 1], path[DEPTH - i - 1])); - - let program = get_smt_storage_compiled_program(); - let cmr = program.commit().cmr(); - - let mint_spend_info: TaprootSpendInfo = - smt_storage_taproot_spend_info(*storage_key, storage_bytes, &merkle_data, cmr); - - let mint_script_pubkey = Script::new_v1_p2tr_tweaked(mint_spend_info.output_key()); - - Ok(Address::from_script(&mint_script_pubkey, None, network.address_params()).unwrap()) -} diff --git a/crates/contracts/src/sdk/storage/mod.rs b/crates/contracts/src/sdk/storage/mod.rs deleted file mode 100644 index 41df9ef..0000000 --- a/crates/contracts/src/sdk/storage/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod get_storage_address; -mod transfer_from_storage_address; - -pub use get_storage_address::*; -pub use transfer_from_storage_address::*; diff --git a/crates/contracts/src/sdk/storage/transfer_from_storage_address.rs b/crates/contracts/src/sdk/storage/transfer_from_storage_address.rs deleted file mode 100644 index 68255f5..0000000 --- a/crates/contracts/src/sdk/storage/transfer_from_storage_address.rs +++ /dev/null @@ -1,85 +0,0 @@ -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicityhl::simplicity::elements::OutPoint; -use simplicityhl::simplicity::elements::Script; -use simplicityhl::simplicity::elements::TxOut; - -use crate::error::TransactionBuildError; -use crate::sdk::validation::TxOutExt as _; - -/// Derives the Taproot address for the SMT storage contract. -/// -/// This function constructs the Simplicity program committed to the provided `storage_bytes` -/// (initial state) and `path`. It then calculates the Taproot script pubkey by tweaking -/// the `storage_key` with the program's commitment (CMR) and converts it into a -/// human-readable address for the specified network. -/// -/// Use this address to fund (mint) the contract by sending assets to it. -/// -/// # Arguments -/// -/// * `storage_key` - The internal X-only public key used for Taproot tweaking (usually an unspendable key). -/// * `storage_bytes` - The 32-byte data payload (SMT root hash) representing the initial state of the contract. -/// * `path` - The binary path in the Sparse Merkle Tree used to generate the witness data. -/// * `network` - The network parameters (e.g., Liquid Testnet, Mainnet) used to format the address. -/// -/// -/// # Errors -/// -/// This function returns a `Result` to maintain consistency with the builder API, -/// though the current implementation is unlikely to return an `Err` variant unless -/// address generation logic changes. -/// -/// # Panics -/// -/// Panics if the generated script is not a valid witness program (this should theoretically -/// never happen with a valid `new_v1_p2tr_tweaked` script). -pub fn transfer_asset_with_storage( - storage_utxo: (OutPoint, TxOut), - fee_utxo: (OutPoint, TxOut), - fee_amount: u64, - new_script_pubkey: &Script, -) -> Result { - let (storage_out_point, storage_tx_out) = storage_utxo; - let (fee_out_point, fee_tx_out) = fee_utxo; - - let (storage_asset_id, total_input_storage_amount) = storage_tx_out.explicit()?; - let (fee_asset_id, change_amount) = ( - fee_tx_out.explicit_asset()?, - fee_tx_out.remaining_after_required(fee_amount)?, - ); - - let mut pst = PartiallySignedTransaction::new_v2(); - let change_recipient_script = fee_tx_out.script_pubkey.clone(); - - let mut storage_input = Input::from_prevout(storage_out_point); - storage_input.witness_utxo = Some(storage_tx_out.clone()); - pst.add_input(storage_input); - - let mut fee_input = Input::from_prevout(fee_out_point); - fee_input.witness_utxo = Some(fee_tx_out.clone()); - pst.add_input(fee_input); - - pst.add_output(Output::new_explicit( - new_script_pubkey.clone(), - total_input_storage_amount, - storage_asset_id, - None, - )); - - if change_amount > 0 { - pst.add_output(Output::new_explicit( - change_recipient_script, - change_amount, - fee_asset_id, - None, - )); - } - - pst.add_output(Output::from_txout(TxOut::new_fee(fee_amount, fee_asset_id))); - - pst.extract_tx()? - .verify_tx_amt_proofs(secp256k1::SECP256K1, &[storage_tx_out, fee_tx_out])?; - - Ok(pst) -} diff --git a/crates/contracts/src/sdk/taproot_pubkey_gen.rs b/crates/contracts/src/sdk/taproot_pubkey_gen.rs deleted file mode 100644 index 4184fcb..0000000 --- a/crates/contracts/src/sdk/taproot_pubkey_gen.rs +++ /dev/null @@ -1,200 +0,0 @@ -//! Ephemeral Taproot pubkey and address generator for argument-bound programs. -//! -//! Produces a deterministic X-only public key and corresponding address without -//! holding a private key, based on a random seed. The resulting trio -//! `::` can be printed and -//! later verified with the same arguments to prevent mismatches. - -use sha2::{Digest, Sha256}; -use std::fmt::Display; -use std::str::FromStr; - -use simplicityhl::elements::{Address, schnorr::XOnlyPublicKey}; - -use crate::error::TaprootPubkeyGenError; -use simplicityhl::simplicity::ToXOnlyPubkey; -use simplicityhl::simplicity::bitcoin::PublicKey; -use simplicityhl::simplicity::bitcoin::key::Parity; -use simplicityhl_core::SimplicityNetwork; - -/// Container for the seed, public key and derived address. -#[derive(Debug, Clone)] -pub struct TaprootPubkeyGen { - pub seed: Vec, - pub pubkey: PublicKey, - pub address: Address, -} - -impl TaprootPubkeyGen { - /// Build from current process randomness and compute the address given `arguments`. - /// - /// # Errors - /// Returns error if address generation fails. - pub fn from( - arguments: &A, - network: SimplicityNetwork, - get_address: &impl Fn( - &XOnlyPublicKey, - &A, - SimplicityNetwork, - ) -> Result, - ) -> Result { - let (not_existent_public_key, seed) = generate_public_key_without_private(); - - let address = get_address( - ¬_existent_public_key.to_x_only_pubkey(), - arguments, - network, - )?; - - Ok(Self { - seed, - pubkey: not_existent_public_key, - address, - }) - } - - /// Parse from string and verify that pubkey and address match the provided arguments. - /// - /// # Errors - /// Returns error if parsing fails or verification doesn't match. - pub fn build_from_str( - s: &str, - arguments: &A, - network: SimplicityNetwork, - get_address: &impl Fn( - &XOnlyPublicKey, - &A, - SimplicityNetwork, - ) -> Result, - ) -> Result { - let taproot_pubkey_gen = Self::parse_from_str(s)?; - - taproot_pubkey_gen.verify(arguments, network, get_address)?; - - Ok(taproot_pubkey_gen) - } - - /// Verify that the stored pubkey and address are consistent with `arguments`. - /// - /// # Errors - /// Returns error if pubkey or address doesn't match the expected values. - pub fn verify( - &self, - arguments: &A, - network: SimplicityNetwork, - get_address: &impl Fn( - &XOnlyPublicKey, - &A, - SimplicityNetwork, - ) -> Result, - ) -> Result<(), TaprootPubkeyGenError> { - let rand_seed = self.seed.as_slice(); - - let mut hasher = Sha256::new(); - sha2::digest::Update::update(&mut hasher, rand_seed); - sha2::digest::Update::update(&mut hasher, rand_seed); - sha2::digest::Update::update(&mut hasher, rand_seed); - let potential_pubkey: [u8; 32] = hasher.finalize().into(); - - let expected_pubkey: PublicKey = XOnlyPublicKey::from_slice(&potential_pubkey)? - .public_key(Parity::Even) - .into(); - - if expected_pubkey != self.pubkey { - return Err(TaprootPubkeyGenError::InvalidPubkey { - expected: expected_pubkey.to_string(), - actual: self.pubkey.to_string(), - }); - } - - let expected_address = get_address(&self.pubkey.to_x_only_pubkey(), arguments, network)?; - if self.address != expected_address { - return Err(TaprootPubkeyGenError::InvalidAddress { - expected: expected_address.to_string(), - actual: self.address.to_string(), - }); - } - - Ok(()) - } - - /// Get the X-only public key. - #[must_use] - pub fn get_x_only_pubkey(&self) -> XOnlyPublicKey { - self.pubkey.to_x_only_pubkey() - } - - /// Parse `::
` representation. - fn parse_from_str(s: &str) -> Result { - let parts = s.split(':').collect::>(); - - if parts.len() != 3 { - return Err(TaprootPubkeyGenError::InvalidFormat { - parts_count: parts.len(), - }); - } - - Ok(Self { - seed: hex::decode(parts[0])?, - pubkey: PublicKey::from_str(parts[1])?, - address: Address::from_str(parts[2])?, - }) - } -} - -impl Display for TaprootPubkeyGen { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}:{}:{}", - hex::encode(&self.seed), - self.pubkey, - self.address - ) - } -} - -/// Try to deterministically map a random seed into a valid X-only pubkey. -fn try_generate_public_key_without_private() -> Result<(PublicKey, Vec), TaprootPubkeyGenError> -{ - let rand_seed: [u8; 32] = get_random_seed(); - - let mut hasher = Sha256::new(); - sha2::digest::Update::update(&mut hasher, &rand_seed); - sha2::digest::Update::update(&mut hasher, &rand_seed); - sha2::digest::Update::update(&mut hasher, &rand_seed); - let potential_pubkey: [u8; 32] = hasher.finalize().into(); - - Ok(( - XOnlyPublicKey::from_slice(&potential_pubkey)? - .public_key(Parity::Even) - .into(), - rand_seed.to_vec(), - )) -} - -/// Generate a valid ephemeral public key and its seed; repeats until valid. -#[must_use] -pub fn generate_public_key_without_private() -> (PublicKey, Vec) { - let not_existent_public_key; - loop { - if let Ok(public_key) = try_generate_public_key_without_private() { - not_existent_public_key = public_key; - break; - } - } - - not_existent_public_key -} - -/// System-random 32-byte seed. -/// -/// # Panics -/// Panics if the system random number generator fails. -#[must_use] -pub fn get_random_seed() -> [u8; 32] { - ring::rand::generate(&ring::rand::SystemRandom::new()) - .unwrap() - .expose() -} diff --git a/crates/contracts/src/sdk/validation.rs b/crates/contracts/src/sdk/validation.rs deleted file mode 100644 index b8ae759..0000000 --- a/crates/contracts/src/sdk/validation.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Extensions and validation helpers for Elements transaction outputs. - -use simplicityhl::elements::{AssetId, TxOut}; - -use crate::error::ValidationError; - -/// Extension trait for [`TxOut`] providing convenient access to explicit values. -pub trait TxOutExt { - /// Returns the explicit (non-confidential) asset and value. - /// - /// # Errors - /// - /// Returns an error if the asset or value is confidential (blinded). - fn explicit(&self) -> Result<(AssetId, u64), ValidationError>; - - /// Returns the explicit (non-confidential) value. - /// - /// # Errors - /// - /// Returns an error if the value is confidential (blinded). - fn explicit_value(&self) -> Result; - - /// Returns the explicit (non-confidential) asset ID. - /// - /// # Errors - /// - /// Returns an error if the asset is confidential (blinded). - fn explicit_asset(&self) -> Result; - - /// Validates that this UTXO can cover the required amount. - /// - /// Returns the change amount (available - required) on success. - /// - /// # Errors - /// - /// Returns an error if: - /// - The value is confidential - /// - The available amount is less than required - fn remaining_after_required(&self, required: u64) -> Result; - - /// Validates that this UTXO can cover the required fee with the expected asset. - /// - /// Returns the change amount (available - required) on success. - /// - /// # Errors - /// - /// Returns an error if: - /// - The value or asset is confidential - /// - The asset doesn't match the expected fee asset - /// - The available amount is less than required - fn remaining_fee_after_required( - &self, - required: u64, - expected_asset: AssetId, - ) -> Result; -} - -impl TxOutExt for TxOut { - fn explicit(&self) -> Result<(AssetId, u64), ValidationError> { - Ok((self.explicit_asset()?, self.explicit_value()?)) - } - - fn explicit_value(&self) -> Result { - self.value - .explicit() - .ok_or_else(|| ValidationError::ConfidentialValue { - script_hash: self.script_pubkey.script_hash().to_string(), - }) - } - - fn explicit_asset(&self) -> Result { - self.asset - .explicit() - .ok_or_else(|| ValidationError::ConfidentialAsset { - script_hash: self.script_pubkey.script_hash().to_string(), - }) - } - - fn remaining_after_required(&self, required: u64) -> Result { - let available = self.explicit_value()?; - - if available < required { - return Err(ValidationError::InsufficientFunds { - script_hash: self.script_pubkey.script_hash().to_string(), - available, - required, - }); - } - - Ok(available - required) - } - - fn remaining_fee_after_required( - &self, - required: u64, - expected_asset: AssetId, - ) -> Result { - let asset = self.explicit_asset()?; - - if asset != expected_asset { - return Err(ValidationError::FeeAssetMismatch { - script_hash: self.script_pubkey.script_hash().to_string(), - expected: expected_asset.to_string(), - actual: asset.to_string(), - }); - } - - self.remaining_after_required(required) - } -} diff --git a/crates/contracts/src/array_tr_storage/build_witness.rs b/crates/contracts/src/state_management/array_tr_storage/build_witness.rs similarity index 100% rename from crates/contracts/src/array_tr_storage/build_witness.rs rename to crates/contracts/src/state_management/array_tr_storage/build_witness.rs diff --git a/crates/contracts/src/array_tr_storage/mod.rs b/crates/contracts/src/state_management/array_tr_storage/mod.rs similarity index 92% rename from crates/contracts/src/array_tr_storage/mod.rs rename to crates/contracts/src/state_management/array_tr_storage/mod.rs index a74e419..6b402b5 100644 --- a/crates/contracts/src/array_tr_storage/mod.rs +++ b/crates/contracts/src/state_management/array_tr_storage/mod.rs @@ -1,16 +1,15 @@ use std::sync::Arc; use simplicityhl::simplicity::bitcoin::secp256k1; -use simplicityhl::simplicity::elements::hashes::HashEngine as _; use simplicityhl::simplicity::elements::taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo}; use simplicityhl::simplicity::elements::{Script, Transaction}; -use simplicityhl::simplicity::hashes::{Hash, sha256}; +use simplicityhl::simplicity::hashes::sha256; use simplicityhl::simplicity::jet::Elements; use simplicityhl::simplicity::jet::elements::ElementsEnv; -use simplicityhl::simplicity::{Cmr, RedeemNode, leaf_version}; +use simplicityhl::simplicity::{Cmr, RedeemNode}; use simplicityhl::tracker::TrackerLogLevel; use simplicityhl::{Arguments, CompiledProgram, TemplateProgram}; -use simplicityhl_core::{ProgramError, run_program}; +use wallet_abi::{ProgramError, run_program, simplicity_leaf_version, tap_data_hash}; mod build_witness; @@ -57,20 +56,19 @@ pub fn execute_array_tr_storage_program( } fn array_tr_storage_script_ver(cmr: Cmr) -> (Script, LeafVersion) { - (Script::from(cmr.as_ref().to_vec()), leaf_version()) + ( + Script::from(cmr.as_ref().to_vec()), + simplicity_leaf_version(), + ) } #[must_use] pub fn compute_tapdata_tagged_hash_of_the_state(state: &State) -> sha256::Hash { - let tag = sha256::Hash::hash(b"TapData"); - let mut eng = sha256::Hash::engine(); - eng.input(tag.as_byte_array()); - eng.input(tag.as_byte_array()); - - for item in state.limbs { - eng.input(&item); + let mut state_bytes = Vec::with_capacity(state.limbs.len() * 32); + for item in &state.limbs { + state_bytes.extend_from_slice(item); } - sha256::Hash::from_engine(eng) + tap_data_hash(&state_bytes) } /// Given a Simplicity CMR and an internal key, computes the [`TaprootSpendInfo`] @@ -119,6 +117,7 @@ mod array_tr_storage_tests { use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; use simplicityhl::elements::{AssetId, BlockHash, OutPoint, Script, Txid}; use simplicityhl::simplicity::elements::taproot::ControlBlock; + use simplicityhl::simplicity::hashes::Hash as _; use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; #[rustfmt::skip] // mangles byte vectors diff --git a/crates/contracts/src/array_tr_storage/source_simf/array_tr_storage.simf b/crates/contracts/src/state_management/array_tr_storage/source_simf/array_tr_storage.simf similarity index 100% rename from crates/contracts/src/array_tr_storage/source_simf/array_tr_storage.simf rename to crates/contracts/src/state_management/array_tr_storage/source_simf/array_tr_storage.simf diff --git a/crates/contracts/src/bytes32_tr_storage/build_witness.rs b/crates/contracts/src/state_management/bytes32_tr_storage/build_witness.rs similarity index 100% rename from crates/contracts/src/bytes32_tr_storage/build_witness.rs rename to crates/contracts/src/state_management/bytes32_tr_storage/build_witness.rs diff --git a/crates/contracts/src/bytes32_tr_storage/mod.rs b/crates/contracts/src/state_management/bytes32_tr_storage/mod.rs similarity index 91% rename from crates/contracts/src/bytes32_tr_storage/mod.rs rename to crates/contracts/src/state_management/bytes32_tr_storage/mod.rs index e374a33..4dc6310 100644 --- a/crates/contracts/src/bytes32_tr_storage/mod.rs +++ b/crates/contracts/src/state_management/bytes32_tr_storage/mod.rs @@ -1,16 +1,14 @@ use std::sync::Arc; use simplicityhl::simplicity::bitcoin::secp256k1; -use simplicityhl::simplicity::elements::hashes::HashEngine as _; use simplicityhl::simplicity::elements::taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo}; use simplicityhl::simplicity::elements::{Script, Transaction}; -use simplicityhl::simplicity::hashes::{Hash, sha256}; use simplicityhl::simplicity::jet::Elements; use simplicityhl::simplicity::jet::elements::ElementsEnv; -use simplicityhl::simplicity::{Cmr, RedeemNode, leaf_version}; +use simplicityhl::simplicity::{Cmr, RedeemNode}; use simplicityhl::tracker::TrackerLogLevel; use simplicityhl::{CompiledProgram, TemplateProgram}; -use simplicityhl_core::{ProgramError, run_program}; +use wallet_abi::{ProgramError, run_program, simplicity_leaf_version, tap_data_hash}; mod build_witness; @@ -75,7 +73,10 @@ pub fn unspendable_internal_key() -> secp256k1::XOnlyPublicKey { } fn script_ver(cmr: Cmr) -> (Script, LeafVersion) { - (Script::from(cmr.as_ref().to_vec()), leaf_version()) + ( + Script::from(cmr.as_ref().to_vec()), + simplicity_leaf_version(), + ) } /// Given a Simplicity CMR and an internal key, computes the [`TaprootSpendInfo`] @@ -94,14 +95,7 @@ pub fn taproot_spend_info( cmr: Cmr, ) -> TaprootSpendInfo { let (script, version) = script_ver(cmr); - - // Compute TapData-tagged hash of the state - let tag = sha256::Hash::hash(b"TapData"); - let mut eng = sha256::Hash::engine(); - eng.input(tag.as_byte_array()); - eng.input(tag.as_byte_array()); - eng.input(&state); - let state_hash = sha256::Hash::from_engine(eng); + let state_hash = tap_data_hash(&state); // Build taproot tree with hidden leaf let builder = TaprootBuilder::new() @@ -125,6 +119,7 @@ mod bytes32_tr_tests { use simplicityhl::elements::pset::{Input, Output, PartiallySignedTransaction}; use simplicityhl::elements::{self, AssetId, OutPoint, Script, Txid}; use simplicityhl::simplicity::elements::taproot::ControlBlock; + use simplicityhl::simplicity::hashes::Hash as _; use simplicityhl::simplicity::jet::elements::ElementsEnv; #[test] diff --git a/crates/contracts/src/bytes32_tr_storage/source_simf/bytes32_tr_storage.simf b/crates/contracts/src/state_management/bytes32_tr_storage/source_simf/bytes32_tr_storage.simf similarity index 100% rename from crates/contracts/src/bytes32_tr_storage/source_simf/bytes32_tr_storage.simf rename to crates/contracts/src/state_management/bytes32_tr_storage/source_simf/bytes32_tr_storage.simf diff --git a/crates/contracts/src/state_management/mod.rs b/crates/contracts/src/state_management/mod.rs new file mode 100644 index 0000000..b0cc24b --- /dev/null +++ b/crates/contracts/src/state_management/mod.rs @@ -0,0 +1,8 @@ +#[cfg(feature = "array-tr-storage")] +pub mod array_tr_storage; +#[cfg(feature = "bytes32-tr-storage")] +pub mod bytes32_tr_storage; +#[cfg(feature = "simple-storage")] +pub mod simple_storage; +#[cfg(feature = "smt-storage")] +pub mod smt_storage; diff --git a/crates/contracts/src/simple_storage/build_arguments.rs b/crates/contracts/src/state_management/simple_storage/build_arguments.rs similarity index 96% rename from crates/contracts/src/simple_storage/build_arguments.rs rename to crates/contracts/src/state_management/simple_storage/build_arguments.rs index 203dc88..9ba23d6 100644 --- a/crates/contracts/src/simple_storage/build_arguments.rs +++ b/crates/contracts/src/state_management/simple_storage/build_arguments.rs @@ -55,4 +55,4 @@ pub fn build_storage_arguments(args: &StorageArguments) -> Arguments { ])) } -impl simplicityhl_core::Encodable for StorageArguments {} +impl wallet_abi::Encodable for StorageArguments {} diff --git a/crates/contracts/src/simple_storage/build_witness.rs b/crates/contracts/src/state_management/simple_storage/build_witness.rs similarity index 100% rename from crates/contracts/src/simple_storage/build_witness.rs rename to crates/contracts/src/state_management/simple_storage/build_witness.rs diff --git a/crates/contracts/src/simple_storage/mod.rs b/crates/contracts/src/state_management/simple_storage/mod.rs similarity index 94% rename from crates/contracts/src/simple_storage/mod.rs rename to crates/contracts/src/state_management/simple_storage/mod.rs index 71e89a7..519e44b 100644 --- a/crates/contracts/src/simple_storage/mod.rs +++ b/crates/contracts/src/state_management/simple_storage/mod.rs @@ -1,5 +1,5 @@ -use simplicityhl_core::{ProgramError, create_p2tr_address, load_program, run_program}; use std::sync::Arc; +use wallet_abi::{ProgramError, create_p2tr_address, load_program, run_program}; use simplicityhl::simplicity::RedeemNode; use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; @@ -97,9 +97,9 @@ mod simple_storage_tests { use simplicityhl::simplicity::elements::taproot::ControlBlock; use simplicityhl::simplicity::jet::elements::ElementsEnv; - use simplicityhl_core::SimplicityNetwork; + use wallet_abi::Network; - const NETWORK: SimplicityNetwork = SimplicityNetwork::LiquidTestnet; + const NETWORK: Network = Network::TestnetLiquid; #[test] fn test_simple_storage_mint_path() -> Result<()> { @@ -133,7 +133,7 @@ mod simple_storage_tests { pst.add_output(Output::new_explicit( storage_address.script_pubkey(), new_value, - NETWORK.policy_asset(), + *NETWORK.policy_asset(), None, )); @@ -151,7 +151,7 @@ mod simple_storage_tests { vec![ simplicityhl::simplicity::jet::elements::ElementsUtxo { script_pubkey: storage_address.script_pubkey(), - asset: Asset::Explicit(NETWORK.policy_asset()), + asset: Asset::Explicit(*NETWORK.policy_asset()), value: Value::Explicit(old_value), }, simplicityhl::simplicity::jet::elements::ElementsUtxo { @@ -207,7 +207,7 @@ mod simple_storage_tests { pst.add_output(Output::new_explicit( storage_address.script_pubkey(), new_value, - NETWORK.policy_asset(), + *NETWORK.policy_asset(), None, )); @@ -215,7 +215,7 @@ mod simple_storage_tests { pst.add_output(Output::new_explicit( Script::new_op_return(b"burn"), old_value - new_value, - NETWORK.policy_asset(), + *NETWORK.policy_asset(), None, )); @@ -225,7 +225,7 @@ mod simple_storage_tests { Arc::new(pst.extract_tx()?), vec![simplicityhl::simplicity::jet::elements::ElementsUtxo { script_pubkey: storage_address.script_pubkey(), - asset: Asset::Explicit(NETWORK.policy_asset()), + asset: Asset::Explicit(*NETWORK.policy_asset()), value: Value::Explicit(old_value), }], 0, diff --git a/crates/contracts/src/simple_storage/source_simf/simple_storage.simf b/crates/contracts/src/state_management/simple_storage/source_simf/simple_storage.simf similarity index 100% rename from crates/contracts/src/simple_storage/source_simf/simple_storage.simf rename to crates/contracts/src/state_management/simple_storage/source_simf/simple_storage.simf diff --git a/crates/contracts/src/smt_storage/build_witness.rs b/crates/contracts/src/state_management/smt_storage/build_witness.rs similarity index 100% rename from crates/contracts/src/smt_storage/build_witness.rs rename to crates/contracts/src/state_management/smt_storage/build_witness.rs diff --git a/crates/contracts/src/smt_storage/mod.rs b/crates/contracts/src/state_management/smt_storage/mod.rs similarity index 95% rename from crates/contracts/src/smt_storage/mod.rs rename to crates/contracts/src/state_management/smt_storage/mod.rs index 9aa14aa..ecd48c9 100644 --- a/crates/contracts/src/smt_storage/mod.rs +++ b/crates/contracts/src/state_management/smt_storage/mod.rs @@ -10,10 +10,11 @@ use simplicityhl::simplicity::elements::{Script, Transaction}; use simplicityhl::simplicity::hashes::{Hash, sha256}; use simplicityhl::simplicity::jet::Elements; use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; -use simplicityhl::simplicity::{Cmr, RedeemNode, leaf_version}; +use simplicityhl::simplicity::{Cmr, RedeemNode}; use simplicityhl::tracker::TrackerLogLevel; use simplicityhl::{Arguments, CompiledProgram, TemplateProgram}; -use simplicityhl_core::{ProgramError, SimplicityNetwork, run_program}; +use wallet_abi::{Network, ProgramError, run_program}; +use wallet_abi::{simplicity_leaf_version, tap_data_hash}; mod build_witness; mod smt; @@ -71,7 +72,10 @@ pub fn execute_smt_storage_program( #[must_use] pub fn smt_storage_script_ver(cmr: Cmr) -> (Script, LeafVersion) { - (Script::from(cmr.as_ref().to_vec()), leaf_version()) + ( + Script::from(cmr.as_ref().to_vec()), + simplicity_leaf_version(), + ) } /// Computes the control block for the given CMR and spend info. @@ -117,16 +121,11 @@ pub fn compute_tapdata_tagged_hash_of_the_state( leaf: &u256, path: &[(u256, bool); DEPTH], ) -> sha256::Hash { - let tag = sha256::Hash::hash(b"TapData"); - let mut eng = sha256::Hash::engine(); - eng.input(tag.as_byte_array()); - eng.input(tag.as_byte_array()); - eng.input(leaf); - let raw_path: [bool; DEPTH] = std::array::from_fn(|i| path[i].1); - eng.input(&[get_path_bits(&raw_path, false)]); - - let mut current_hash = sha256::Hash::from_engine(eng); + let mut tapdata_input = Vec::with_capacity(leaf.len() + 1); + tapdata_input.extend_from_slice(leaf); + tapdata_input.push(get_path_bits(&raw_path, false)); + let mut current_hash = tap_data_hash(&tapdata_input); for (hash, is_right_direction) in path { let mut eng = sha256::Hash::engine(); @@ -187,10 +186,10 @@ pub fn get_and_verify_env( program: &CompiledProgram, spend_info: &TaprootSpendInfo, utxos: &[TxOut], - network: SimplicityNetwork, + network: Network, input_index: usize, ) -> Result>, ProgramError> { - let genesis_hash = network.genesis_block_hash(); + let genesis_hash = network.genesis_hash(); let cmr = program.commit().cmr(); if utxos.len() <= input_index { @@ -243,7 +242,7 @@ pub fn finalize_get_storage_transaction( storage_program: &CompiledProgram, utxos: &[TxOut], input_index: usize, - network: SimplicityNetwork, + network: Network, log_level: TrackerLogLevel, ) -> Result { let env = get_and_verify_env( diff --git a/crates/contracts/src/smt_storage/smt.rs b/crates/contracts/src/state_management/smt_storage/smt.rs similarity index 95% rename from crates/contracts/src/smt_storage/smt.rs rename to crates/contracts/src/state_management/smt_storage/smt.rs index b1660a9..e2ce89c 100644 --- a/crates/contracts/src/smt_storage/smt.rs +++ b/crates/contracts/src/state_management/smt_storage/smt.rs @@ -1,7 +1,8 @@ use simplicityhl::simplicity::elements::hashes::HashEngine as _; use simplicityhl::simplicity::hashes::{Hash, sha256}; +use wallet_abi::tap_data_hash; -use crate::smt_storage::get_path_bits; +use crate::state_management::smt_storage::get_path_bits; use super::build_witness::{DEPTH, u256}; @@ -134,15 +135,12 @@ impl SparseMerkleTree { hashes: &mut [u256], ) { if ind >= DEPTH { - let tag = sha256::Hash::hash(b"TapData"); - let mut eng = sha256::Hash::engine(); - eng.input(tag.as_byte_array()); - eng.input(tag.as_byte_array()); - eng.input(leaf); - eng.input(&[get_path_bits(path, true)]); - + let mut tapdata_input = Vec::with_capacity(leaf.len() + 1); + tapdata_input.extend_from_slice(leaf); + tapdata_input.push(get_path_bits(path, true)); + let leaf_hash = tap_data_hash(&tapdata_input); **root = TreeNode::Leaf { - leaf_hash: *sha256::Hash::from_engine(eng).as_byte_array(), + leaf_hash: *leaf_hash.as_byte_array(), }; return; } diff --git a/crates/contracts/src/smt_storage/source_simf/smt_storage.simf b/crates/contracts/src/state_management/smt_storage/source_simf/smt_storage.simf similarity index 100% rename from crates/contracts/src/smt_storage/source_simf/smt_storage.simf rename to crates/contracts/src/state_management/smt_storage/source_simf/smt_storage.simf diff --git a/crates/contracts/src/utils/arguments_helpers.rs b/crates/contracts/src/utils/arguments_helpers.rs new file mode 100644 index 0000000..fbda166 --- /dev/null +++ b/crates/contracts/src/utils/arguments_helpers.rs @@ -0,0 +1,72 @@ +use crate::error::FromArgumentsError; + +use simplicityhl::Arguments; +use simplicityhl::str::WitnessName; +use simplicityhl::value::{UIntValue, ValueInner}; + +fn extract_uint<'a>( + args: &'a Arguments, + name: &WitnessName, +) -> Result<&'a UIntValue, FromArgumentsError> { + let value = args + .get(name) + .ok_or_else(|| FromArgumentsError::MissingWitness { + name: name.as_inner().to_owned(), + })?; + + match value.inner() { + ValueInner::UInt(uint_value) => Ok(uint_value), + _ => Err(FromArgumentsError::WrongValueType { + name: name.as_inner().to_owned(), + expected: "UInt".to_owned(), + }), + } +} + +/// Extract a U256 value as `[u8; 32]` from `Arguments` for a witness name. +/// +/// # Errors +/// +/// Returns error if the witness is missing or has wrong type. +pub fn extract_u256_bytes( + args: &Arguments, + name: &WitnessName, +) -> Result<[u8; 32], FromArgumentsError> { + match extract_uint(args, name)? { + UIntValue::U256(u256) => Ok(u256.to_byte_array()), + _ => Err(FromArgumentsError::WrongValueType { + name: name.as_inner().to_owned(), + expected: "U256".to_owned(), + }), + } +} + +/// Extract a U64 value from `Arguments` for a witness name. +/// +/// # Errors +/// +/// Returns error if the witness is missing or has wrong type. +pub fn extract_u64(args: &Arguments, name: &WitnessName) -> Result { + match extract_uint(args, name)? { + UIntValue::U64(v) => Ok(*v), + _ => Err(FromArgumentsError::WrongValueType { + name: name.as_inner().to_owned(), + expected: "U64".to_owned(), + }), + } +} + +/// Extract a U32 value from `Arguments` for a witness name. +/// +/// # Errors +/// +/// Returns error if the witness is missing or has wrong type. +pub fn extract_u32(args: &Arguments, name: &WitnessName) -> Result { + match extract_uint(args, name)? { + UIntValue::U32(v) => Ok(*v), + _ => Err(FromArgumentsError::WrongValueType { + name: name.as_inner().to_owned(), + expected: "U32".to_owned(), + }), + } +} diff --git a/crates/contracts/src/utils/mod.rs b/crates/contracts/src/utils/mod.rs new file mode 100644 index 0000000..a6b69c8 --- /dev/null +++ b/crates/contracts/src/utils/mod.rs @@ -0,0 +1,4 @@ +pub mod arguments_helpers; + +#[cfg(test)] +pub mod test_setup; diff --git a/crates/contracts/src/utils/test_setup.rs b/crates/contracts/src/utils/test_setup.rs new file mode 100644 index 0000000..dda1d8c --- /dev/null +++ b/crates/contracts/src/utils/test_setup.rs @@ -0,0 +1,191 @@ +#![allow(clippy::missing_errors_doc)] + +use std::str::FromStr; +use std::sync::{Arc, Mutex, OnceLock, RwLock}; + +use anyhow::{Context, anyhow}; +use bitcoincore_rpc::{Auth, Client, RpcApi}; +use lwk_test_util::{TestEnv, TestEnvBuilder}; +use serde_json::Value; +use simplicityhl::elements::AssetId; +use wallet_abi::Network; +use wallet_abi::runtime::WalletRuntimeConfig; + +const DEFAULT_FUND_AMOUNT_SAT: u64 = 1_000_000; + +static TEST_ENV: OnceLock>> = OnceLock::new(); +static TEST_ENV_INIT_LOCK: Mutex<()> = Mutex::new(()); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RuntimeFundingAsset { + Lbtc, + NewAsset, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RuntimeFundingResult { + pub funded_asset_id: AssetId, + pub issued_token_id: Option, + pub funded_amount_sat: u64, +} + +pub fn ensure_node_running() -> anyhow::Result<()> { + if TEST_ENV.get().is_some() { + return Ok(()); + } + + let _init_guard = TEST_ENV_INIT_LOCK + .lock() + .map_err(|_| anyhow!("test env init lock poisoned"))?; + if TEST_ENV.get().is_some() { + return Ok(()); + } + + for var in ["ELEMENTSD_EXEC", "ELECTRS_LIQUID_EXEC"] { + if std::env::var(var) + .map(|value| value.trim().is_empty()) + .unwrap_or(true) + { + return Err(anyhow!("{var} must be set")); + } + } + + let env = std::panic::catch_unwind(|| TestEnvBuilder::from_env().with_esplora().build()) + .map_err(|panic| { + panic.downcast_ref::().map_or_else( + || anyhow!("failed to start regtest test environment"), + |message| anyhow!("failed to start regtest test environment: {message}"), + ) + })?; + + let _ = TEST_ENV.set(Arc::new(RwLock::new(env))); + + Ok(()) +} + +/// Return the wallet data root path for integration tests. +/// +/// Uses `SIMPLICITY_CLI_WALLET_DATA_DIR` if set; otherwise falls back to a +/// deterministic workspace-relative path rooted at `CARGO_MANIFEST_DIR`. +pub fn wallet_data_root() -> std::path::PathBuf { + std::env::var_os("SIMPLICITY_CLI_WALLET_DATA_DIR").map_or_else( + || std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.cache/wallet"), + std::path::PathBuf::from, + ) +} + +fn test_env() -> anyhow::Result>> { + ensure_node_running()?; + TEST_ENV + .get() + .cloned() + .ok_or_else(|| anyhow!("test env failed to initialize")) +} + +#[allow(clippy::cast_precision_loss)] +fn sat_to_btc(satoshi: u64) -> f64 { + satoshi as f64 / 100_000_000.0 +} + +fn issue_asset_with_token(env: &TestEnv, amount_sat: u64) -> anyhow::Result<(AssetId, AssetId)> { + let rpc_url = env.elements_rpc_url(); + let (rpc_user, rpc_password) = env.elements_rpc_credentials(); + let client = Client::new(&rpc_url, Auth::UserPass(rpc_user, rpc_password)) + .context("failed to connect to elements RPC")?; + + let issued: Value = client + .call("issueasset", &[sat_to_btc(amount_sat).into(), 0.into()]) + .context("issueasset RPC failed")?; + + let asset_id = issued + .get("asset") + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("issueasset response missing 'asset'"))?; + let token_id = issued + .get("token") + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("issueasset response missing 'token'"))?; + + let asset_id = AssetId::from_str(asset_id).context("invalid issued asset id")?; + let token_id = AssetId::from_str(token_id).context("invalid issued token id")?; + + Ok((asset_id, token_id)) +} + +pub fn fund_runtime( + runtime: &WalletRuntimeConfig, + asset: RuntimeFundingAsset, +) -> anyhow::Result { + fund_runtime_with_amount(runtime, asset, DEFAULT_FUND_AMOUNT_SAT) +} + +pub fn fund_runtime_with_amount( + runtime: &WalletRuntimeConfig, + asset: RuntimeFundingAsset, + amount_sat: u64, +) -> anyhow::Result { + if runtime.network != Network::LocaltestLiquid { + return Err(anyhow!( + "fund_runtime supports only Network::LocaltestLiquid" + )); + } + + if amount_sat == 0 { + return Err(anyhow!("amount_sat must be > 0")); + } + + let test_env = test_env()?; + let env = test_env + .read() + .map_err(|_| anyhow!("test env lock poisoned"))?; + + let signer_address = runtime.signer_receive_address()?; + + let (asset_id, token_id) = match asset { + RuntimeFundingAsset::Lbtc => { + let lbtc = *runtime.network.policy_asset(); + env.elementsd_sendtoaddress(&signer_address, amount_sat, Some(lbtc)); + env.elementsd_generate(1); + drop(env); + (lbtc, None) + } + RuntimeFundingAsset::NewAsset => { + let (issued_asset, token_id) = issue_asset_with_token(&env, amount_sat)?; + env.elementsd_generate(1); + env.elementsd_sendtoaddress(&signer_address, amount_sat, Some(issued_asset)); + env.elementsd_generate(1); + drop(env); + (issued_asset, Some(token_id)) + } + }; + + Ok(RuntimeFundingResult { + funded_asset_id: asset_id, + issued_token_id: token_id, + funded_amount_sat: amount_sat, + }) +} + +pub fn get_esplora_url() -> anyhow::Result { + let test_env = test_env()?; + let env = test_env + .read() + .map_err(|_| anyhow!("test env lock poisoned"))?; + + Ok(env.esplora_url()) +} + +pub fn mine_blocks(blocks: usize) -> anyhow::Result<()> { + let test_env = test_env()?; + { + let env = test_env + .read() + .map_err(|_| anyhow!("test env lock poisoned"))?; + + for _ in 0..blocks { + env.elementsd_generate(1); + } + } + + Ok(()) +} diff --git a/crates/simplicityhl-core/Cargo.toml b/crates/simplicityhl-core/Cargo.toml deleted file mode 100644 index 67e065a..0000000 --- a/crates/simplicityhl-core/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "simplicityhl-core" -version = "0.4.2" -edition = "2024" -rust-version = "1.90" -description = "High-level helpers for compiling and executing Simplicity programs on Liquid" -license = "MIT OR Apache-2.0" -repository = "https://github.com/BlockstreamResearch/simplicity-contracts" -homepage = "https://github.com/BlockstreamResearch/simplicity-contracts/tree/dev/crates/simplicityhl-core" -readme = "README.md" -documentation = "https://docs.rs/simplicityhl-core" -keywords = ["simplicity", "liquid", "bitcoin", "elements", "taproot"] -categories = ["cryptography::cryptocurrencies"] - -[lints] -workspace = true - -[features] -encoding = ["dep:bincode"] - -[dependencies] -thiserror = "2" - -bincode = { version = "2.0.1", optional = true } - -sha2 = { workspace = true } - -hex = { workspace = true } - -simplicityhl = { workspace = true } - -minreq = { workspace = true } diff --git a/crates/simplicityhl-core/README.md b/crates/simplicityhl-core/README.md deleted file mode 100644 index 58ec430..0000000 --- a/crates/simplicityhl-core/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Simplicity HL Core - -This crate provides useful utilities for working with Simplicity on Elements. - -- `blinder.rs` — derives deterministic blinder keypair from a "public secret" -- `constants.rs` — Liquid network constants (policy asset IDs, genesis hashes) -- `explorer.rs` — explorer API utilities (behind `explorer` feature) -- `runner.rs` — program execution helpers with logging -- `scripts.rs` — P2TR address creation, Taproot control block, and asset entropy utilities -- `lib.rs` — P2PK program helpers and transaction finalization - -Consider this more like a test helper tool rather than a production-ready version. - -## License - -Dual-licensed under either of: -- Apache License, Version 2.0 (Apache-2.0) -- MIT license (MIT) - -at your option. diff --git a/crates/simplicityhl-core/src/blinder.rs b/crates/simplicityhl-core/src/blinder.rs deleted file mode 100644 index 8754bdc..0000000 --- a/crates/simplicityhl-core/src/blinder.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::PUBLIC_SECRET_BLINDER_KEY; - -use simplicityhl::elements::bitcoin::secp256k1; -use simplicityhl::elements::secp256k1_zkp::SecretKey; - -/// Derives a deterministic blinder keypair from the hardcoded public secret. -/// -/// # Panics -/// -/// Panics if the secret key bytes are invalid (should never happen with valid constant). -#[must_use] -pub fn derive_public_blinder_key() -> secp256k1::Keypair { - secp256k1::Keypair::from_secret_key( - secp256k1::SECP256K1, - &SecretKey::from_slice(&PUBLIC_SECRET_BLINDER_KEY).unwrap(), - ) -} diff --git a/crates/simplicityhl-core/src/constants.rs b/crates/simplicityhl-core/src/constants.rs deleted file mode 100644 index 541d6f7..0000000 --- a/crates/simplicityhl-core/src/constants.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! Common Liquid network constants and helpers. -//! -//! Exposes policy asset identifiers and the Liquid testnet genesis hash. -//! -//! These are used throughout the CLI and examples to ensure consistent -//! parameters when constructing Elements transactions. - -use simplicityhl::simplicity::elements; -use simplicityhl::simplicity::hashes::{Hash, sha256}; -use std::str::FromStr; - -pub const PUBLIC_SECRET_BLINDER_KEY: [u8; 32] = [1; 32]; - -/// `PLACEHOLDER_ISSUANCE_VALUE` constant is used for issuance and reissuance tokens during the blinding process. -/// -/// During blinding, the PSET collects surjection proof inputs from all outputs with matching asset IDs. -/// For issuance tokens, only the `asset_id` from `TxOutSectet` is copied into surjection proofs while the `value` is set to `0`, -/// as the exact value inserted for the issuance token is irrelevant to the proof computation. -/// This is because issuance and reissuance tokens surjection proofs only care about the `asset_id` or `token_id`, not the token value. -/// -/// See: `` -pub const PLACEHOLDER_ISSUANCE_VALUE: u64 = 0; - -/// Policy asset id (hex, BE) for Liquid mainnet. -pub const LIQUID_POLICY_ASSET_STR: &str = - "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"; - -/// Policy asset id (hex, BE) for Liquid testnet. -pub const LIQUID_TESTNET_POLICY_ASSET_STR: &str = - "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; - -/// Policy asset id (hex, BE) for Elements regtest. -pub const LIQUID_DEFAULT_REGTEST_ASSET_STR: &str = - "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"; - -/// Example test asset id (hex, BE) on Liquid testnet. -pub static LIQUID_TESTNET_TEST_ASSET_ID_STR: &str = - "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; - -/// LBTC asset id for Liquid testnet. -pub static LIQUID_TESTNET_BITCOIN_ASSET: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - elements::AssetId::from_inner(sha256::Midstate([ - 0x49, 0x9a, 0x81, 0x85, 0x45, 0xf6, 0xba, 0xe3, 0x9f, 0xc0, 0x3b, 0x63, 0x7f, 0x2a, - 0x4e, 0x1e, 0x64, 0xe5, 0x90, 0xca, 0xc1, 0xbc, 0x3a, 0x6f, 0x6d, 0x71, 0xaa, 0x44, - 0x43, 0x65, 0x4c, 0x14, - ])) - }); - -/// Genesis block hash for Liquid mainnet. -pub static LIQUID_MAINNET_GENESIS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - elements::BlockHash::from_byte_array([ - 0x03, 0x60, 0x20, 0x8a, 0x88, 0x96, 0x92, 0x37, 0x2c, 0x8d, 0x68, 0xb0, 0x84, 0xa6, - 0x2e, 0xfd, 0xf6, 0x0e, 0xa1, 0xa3, 0x59, 0xa0, 0x4c, 0x94, 0xb2, 0x0d, 0x22, 0x36, - 0x58, 0x27, 0x66, 0x14, - ]) - }); - -/// Genesis block hash for Liquid testnet. -pub static LIQUID_TESTNET_GENESIS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - elements::BlockHash::from_byte_array([ - 0xc1, 0xb1, 0x6a, 0xe2, 0x4f, 0x24, 0x23, 0xae, 0xa2, 0xea, 0x34, 0x55, 0x22, 0x92, - 0x79, 0x3b, 0x5b, 0x5e, 0x82, 0x99, 0x9a, 0x1e, 0xed, 0x81, 0xd5, 0x6a, 0xee, 0x52, - 0x8e, 0xda, 0x71, 0xa7, - ]) - }); - -/// Genesis block hash for Liquid regtest. -pub static LIQUID_REGTEST_GENESIS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - elements::BlockHash::from_byte_array([ - 0x21, 0xca, 0xb1, 0xe5, 0xda, 0x47, 0x18, 0xea, 0x14, 0x0d, 0x97, 0x16, 0x93, 0x17, - 0x02, 0x42, 0x2f, 0x0e, 0x6a, 0xd9, 0x15, 0xc8, 0xd9, 0xb5, 0x83, 0xca, 0xc2, 0x70, - 0x6b, 0x2a, 0x90, 0x00, - ]) - }); - -/// The network of the elements blockchain. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum SimplicityNetwork { - // Liquid mainnet policy asset - Liquid, - // Liquid testnet policy asset - LiquidTestnet, - /// Liquid regtest with a custom policy asset. - ElementsRegtest { - /// The policy asset to use for this regtest network. - /// You can use the default one using [`SimplicityNetwork::default_regtest()`]. - policy_asset: elements::AssetId, - }, -} - -impl SimplicityNetwork { - /// Return the default policy asset for regtest network. - /// - /// # Panics - /// - /// Doesn't panic as constants are defined correctly. - #[must_use] - pub fn default_regtest() -> Self { - let policy_asset = elements::AssetId::from_str(LIQUID_DEFAULT_REGTEST_ASSET_STR).unwrap(); - Self::ElementsRegtest { policy_asset } - } - - /// Return the policy asset for specific network. - /// - /// # Panics - /// - /// Doesn't panic as constants are defined correctly. - #[must_use] - pub fn policy_asset(&self) -> elements::AssetId { - match self { - Self::Liquid => elements::AssetId::from_str(LIQUID_POLICY_ASSET_STR).unwrap(), - Self::LiquidTestnet => { - elements::AssetId::from_str(LIQUID_TESTNET_POLICY_ASSET_STR).unwrap() - } - Self::ElementsRegtest { policy_asset } => *policy_asset, - } - } - - /// Return the genesis block hash for this network. - #[must_use] - pub fn genesis_block_hash(&self) -> elements::BlockHash { - match self { - Self::Liquid => *LIQUID_MAINNET_GENESIS, - Self::LiquidTestnet => *LIQUID_TESTNET_GENESIS, - Self::ElementsRegtest { .. } => *LIQUID_REGTEST_GENESIS, - } - } - - /// Return the address parameters for this network to generate addresses compatible for this network. - #[must_use] - pub const fn address_params(&self) -> &'static elements::AddressParams { - match self { - Self::Liquid => &elements::AddressParams::LIQUID, - Self::LiquidTestnet => &elements::AddressParams::LIQUID_TESTNET, - Self::ElementsRegtest { .. } => &elements::AddressParams::ELEMENTS, - } - } -} diff --git a/crates/simplicityhl-core/src/error.rs b/crates/simplicityhl-core/src/error.rs deleted file mode 100644 index a8b4179..0000000 --- a/crates/simplicityhl-core/src/error.rs +++ /dev/null @@ -1,58 +0,0 @@ -/// Errors that occur during binary or hex encoding/decoding operations. -/// -/// These errors are returned by the [`Encodable`](crate::Encodable) trait methods -/// when serializing or deserializing data. -#[cfg(feature = "encoding")] -#[derive(Debug, thiserror::Error)] -pub enum EncodingError { - #[error("Failed to encode to binary: {0}")] - BinaryEncode(#[from] bincode::error::EncodeError), - - #[error("Failed to decode from binary: {0}")] - BinaryDecode(#[from] bincode::error::DecodeError), - - /// Returned when a hex string cannot be parsed. - #[error("Failed to decode hex string: {0}")] - HexDecode(#[from] hex::FromHexError), -} - -/// Errors that occur during Simplicity program compilation, execution, or environment setup. -/// -/// These errors cover the full lifecycle of working with Simplicity programs: -/// loading source, satisfying witnesses, running on the Bit Machine, and -/// validating transaction environments. -#[derive(Debug, thiserror::Error)] -pub enum ProgramError { - #[error("Failed to compile Simplicity program: {0}")] - Compilation(String), - - /// Returned when witness values cannot satisfy the program's requirements. - #[error("Failed to satisfy witness: {0}")] - WitnessSatisfaction(String), - - /// Returned when the program cannot be pruned against the transaction environment. - #[error("Failed to prune program: {0}")] - Pruning(#[from] simplicityhl::simplicity::bit_machine::ExecutionError), - - #[error("Failed to construct a Bit Machine with enough space: {0}")] - BitMachineCreation(#[from] simplicityhl::simplicity::bit_machine::LimitError), - - #[error("Failed to execute program on the Bit Machine: {0}")] - Execution(simplicityhl::simplicity::bit_machine::ExecutionError), - - #[error("UTXO index {input_index} out of bounds (have {utxo_count} UTXOs)")] - UtxoIndexOutOfBounds { - input_index: usize, - utxo_count: usize, - }, - - /// Returned when the UTXO's script does not match the expected program address. - #[error("Script pubkey mismatch: expected hash {expected_hash}, got {actual_hash}")] - ScriptPubkeyMismatch { - expected_hash: String, - actual_hash: String, - }, - - #[error("Input index exceeds u32 maximum: {0}")] - InputIndexOverflow(#[from] std::num::TryFromIntError), -} diff --git a/crates/simplicityhl-core/src/fee_rate_fetcher.rs b/crates/simplicityhl-core/src/fee_rate_fetcher.rs deleted file mode 100644 index 663c84a..0000000 --- a/crates/simplicityhl-core/src/fee_rate_fetcher.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::collections::HashMap; - -/// Fee estimates response from Esplora. -/// Key: confirmation target (in blocks as string), Value: fee rate (sat/vB). -pub type FeeEstimates = HashMap; - -/// Default Target blocks value for using `DEFAULT_FEE_RATE` later -pub const DEFAULT_TARGET_BLOCKS: u32 = 0; - -/// Default fallback fee rate in sats/kvb (0.10 sat/vB). -/// Higher than LWK default to meet Liquid minimum relay fee requirements. -pub const DEFAULT_FEE_RATE: f32 = 100.0; - -/// Error type for Esplora sync operations. -#[derive(thiserror::Error, Debug)] -pub enum FeeFetcherError { - #[error("HTTP request failed: {0}")] - Request(String), - - #[error("Failed to deserialize response: {0}")] - Deserialize(String), - - #[error("Invalid txid format: {0}")] - InvalidTxid(String), -} - -pub trait SyncFeeFetcher { - /// Fetch fee estimates for various confirmation targets. - /// - /// # Errors - /// - /// Returns error if the HTTP request fails or response body cannot be parsed. - fn fetch_fee_estimates() -> Result; - - /// Get fee rate for a specific confirmation target. - /// - /// Fetches fee estimates from Esplora and returns the rate for the given target. - /// If the exact target is not available, falls back to higher targets. - /// - /// # Arguments - /// - /// * `target_blocks` - Desired confirmation target in blocks (1-25, 144, 504, 1008) - /// - /// # Returns - /// - /// Fee rate in sats/kvb (satoshis per 1000 virtual bytes). - /// Multiply Esplora's sat/vB value by 1000. - /// - /// # Errors - /// - /// Returns an error if the `fetch_fee_estimates()` fails or no suitable fee rate is found. - #[allow(clippy::cast_possible_truncation)] - fn get_fee_rate(target_blocks: u32) -> Result { - if target_blocks == 0 { - return Ok(DEFAULT_FEE_RATE); - } - - let estimates = Self::fetch_fee_estimates()?; - - let target_str = target_blocks.to_string(); - if let Some(&rate) = estimates.get(&target_str) { - return Ok((rate * 1000.0) as f32); // Convert sat/vB to sats/kvb - } - - // Fall back to higher targets (lower fee rates) - // Available targets: 1-25, 144, 504, 1008 - let fallback_targets = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, 144, 504, 1008, - ]; - - for &target in fallback_targets.iter().filter(|&&t| t >= target_blocks) { - let key = target.to_string(); - if let Some(&rate) = estimates.get(&key) { - return Ok((rate * 1000.0) as f32); - } - } - - // If no higher target found, try any available rate (use lowest target = highest rate) - for &target in &fallback_targets { - let key = target.to_string(); - if let Some(&rate) = estimates.get(&key) { - return Ok((rate * 1000.0) as f32); - } - } - - Err(FeeFetcherError::Request( - "No fee estimates available".to_string(), - )) - } -} - -pub struct EsploraFeeFetcher; - -impl SyncFeeFetcher for EsploraFeeFetcher { - /// Fetch fee estimates for various confirmation targets. - /// - /// Uses the `GET /fee-estimates` endpoint. - /// Note: Liquid testnet typically returns empty results, so callers should - /// use a fallback rate (see `config.fee.fallback_rate`). - /// - /// Returns a map where key is confirmation target (blocks) and value is fee rate (sat/vB). - /// - /// Example response: `{ "1": 87.882, "2": 87.882, ..., "144": 1.027, "1008": 1.027 }` - fn fetch_fee_estimates() -> Result { - const ESPLORA_URL: &str = "https://blockstream.info/liquidtestnet/api"; - - let url = format!("{ESPLORA_URL}/fee-estimates"); - let response = minreq::get(&url) - .send() - .map_err(|e| FeeFetcherError::Request(e.to_string()))?; - - if response.status_code != 200 { - return Err(FeeFetcherError::Request(format!( - "HTTP {}: {}", - response.status_code, response.reason_phrase - ))); - } - - let estimates: FeeEstimates = response - .json() - .map_err(|e| FeeFetcherError::Deserialize(e.to_string()))?; - - Ok(estimates) - } -} diff --git a/crates/simplicityhl-core/src/lib.rs b/crates/simplicityhl-core/src/lib.rs deleted file mode 100644 index aeee21d..0000000 --- a/crates/simplicityhl-core/src/lib.rs +++ /dev/null @@ -1,336 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] - -//! High-level helpers for building and executing Simplicity programs on Liquid. - -mod blinder; -mod constants; -mod error; -mod fee_rate_fetcher; -mod runner; -mod scripts; -mod tx_inclusion; - -#[cfg(feature = "encoding")] -pub mod encoding { - pub use bincode::{Decode, Encode}; - - use crate::error::EncodingError; - - /// Trait for binary encoding/decoding with hex string support. - pub trait Encodable { - /// Encode to binary bytes. - /// - /// # Errors - /// Returns error if encoding fails. - fn encode(&self) -> Result, EncodingError> - where - Self: Encode, - { - Ok(bincode::encode_to_vec(self, bincode::config::standard())?) - } - - /// Decode from binary bytes. - /// - /// # Errors - /// Returns error if decoding fails. - fn decode(buf: &[u8]) -> Result - where - Self: Sized + Decode<()>, - { - Ok(bincode::decode_from_slice(buf, bincode::config::standard())?.0) - } - - /// Encode to hex string. - /// - /// # Errors - /// Returns error if encoding fails. - fn to_hex(&self) -> Result - where - Self: Encode, - { - Ok(hex::encode(Encodable::encode(self)?)) - } - - /// Decode from hex string. - /// - /// # Errors - /// Returns error if hex decoding or binary decoding fails. - fn from_hex(hex: &str) -> Result - where - Self: bincode::Decode<()>, - { - Encodable::decode(&hex::decode(hex)?) - } - } -} - -pub use blinder::*; -pub use constants::*; -pub use error::ProgramError; - -#[cfg(feature = "encoding")] -pub use error::EncodingError; - -pub use runner::*; -pub use scripts::*; -pub use tx_inclusion::*; - -pub use fee_rate_fetcher::*; - -#[cfg(feature = "encoding")] -pub use encoding::Encodable; - -use simplicityhl::elements::secp256k1_zkp::schnorr::Signature; - -use std::collections::HashMap; -use std::sync::Arc; - -use simplicityhl::num::U256; -use simplicityhl::simplicity::RedeemNode; -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; -use simplicityhl::simplicity::elements::{Address, Transaction, TxInWitness, TxOut}; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; -use simplicityhl::str::WitnessName; -use simplicityhl::tracker::TrackerLogLevel; -use simplicityhl::value::ValueConstructible; -use simplicityhl::{CompiledProgram, Value, WitnessValues, elements}; - -/// Embedded Simplicity source for a basic P2PK program used to sign a single input. -pub const P2PK_SOURCE: &str = include_str!("source_simf/p2pk.simf"); - -/// Construct a P2TR address for the embedded P2PK program and the provided public key. -/// -/// # Errors -/// Returns error if the P2PK program fails to compile. -pub fn get_p2pk_address( - x_only_public_key: &XOnlyPublicKey, - network: SimplicityNetwork, -) -> Result { - Ok(create_p2tr_address( - get_p2pk_program(x_only_public_key)?.commit().cmr(), - x_only_public_key, - network.address_params(), - )) -} - -/// Compile the embedded P2PK program with the given X-only public key as argument. -/// -/// # Errors -/// Returns error if program compilation fails. -pub fn get_p2pk_program( - account_public_key: &XOnlyPublicKey, -) -> Result { - let arguments = simplicityhl::Arguments::from(HashMap::from([( - WitnessName::from_str_unchecked("PUBLIC_KEY"), - Value::u256(U256::from_byte_array(account_public_key.serialize())), - )])); - - load_program(P2PK_SOURCE, arguments) -} - -/// Execute the compiled P2PK program against the provided env, producing a pruned redeem node. -/// -/// The `schnorr_signature` should be created by signing the `sighash_all` from the environment: -/// ```ignore -/// let sighash_all = secp256k1::Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); -/// let schnorr_signature = keypair.sign_schnorr(sighash_all); -/// ``` -/// -/// # Errors -/// Returns error if program execution fails. -pub fn execute_p2pk_program( - compiled_program: &CompiledProgram, - schnorr_signature: &Signature, - env: &ElementsEnv>, - runner_log_level: TrackerLogLevel, -) -> Result>, ProgramError> { - let witness_values = WitnessValues::from(HashMap::from([( - WitnessName::from_str_unchecked("SIGNATURE"), - Value::byte_array(schnorr_signature.serialize()), - )])); - - Ok(run_program(compiled_program, witness_values, env, runner_log_level)?.0) -} - -/// Create a Schnorr signature for the P2PK program by signing the `sighash_all` of the transaction. -/// -/// This is a convenience function that builds the environment and signs the transaction hash. -/// -/// # Errors -/// Returns error if program compilation or environment verification fails. -pub fn create_p2pk_signature( - tx: &Transaction, - utxos: &[TxOut], - keypair: &elements::schnorr::Keypair, - input_index: usize, - network: SimplicityNetwork, -) -> Result { - use simplicityhl::simplicity::hashes::Hash as _; - - let x_only_public_key = keypair.x_only_public_key().0; - let p2pk_program = get_p2pk_program(&x_only_public_key)?; - - let env = get_and_verify_env( - tx, - &p2pk_program, - &x_only_public_key, - utxos, - network, - input_index, - )?; - - let sighash_all = - elements::secp256k1_zkp::Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); - Ok(keypair.sign_schnorr(sighash_all)) -} - -/// Finalize the given transaction by attaching a Simplicity witness for the specified P2PK input. -/// -/// The `schnorr_signature` should be created by signing the `sighash_all` from the environment. -/// Use [`create_p2pk_signature`] to create the signature if you have access to the secret key: -/// ```ignore -/// let signature = create_p2pk_signature(&tx, &utxos, &keypair, input_index, params, genesis_hash)?; -/// let tx = finalize_p2pk_transaction(tx, &utxos, &public_key, &signature, input_index, params, genesis_hash, TrackerLogLevel::None)?; -/// ``` -/// -/// Preconditions: -/// - `utxos[input_index]` must match the P2PK address derived from `x_only_public_key` and program CMR. -/// -/// # Errors -/// Returns error if program compilation, execution, or environment verification fails. -#[allow(clippy::too_many_arguments)] -pub fn finalize_p2pk_transaction( - mut tx: Transaction, - utxos: &[TxOut], - x_only_public_key: &XOnlyPublicKey, - schnorr_signature: &Signature, - input_index: usize, - network: SimplicityNetwork, - log_level: TrackerLogLevel, -) -> Result { - let p2pk_program = get_p2pk_program(x_only_public_key)?; - - let env = get_and_verify_env( - &tx, - &p2pk_program, - x_only_public_key, - utxos, - network, - input_index, - )?; - - let pruned = execute_p2pk_program(&p2pk_program, schnorr_signature, &env, log_level)?; - - let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); - let cmr = pruned.cmr(); - - tx.input[input_index].witness = TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, - script_witness: vec![ - simplicity_witness_bytes, - simplicity_program_bytes, - cmr.as_ref().to_vec(), - control_block(cmr, *x_only_public_key).serialize(), - ], - pegin_witness: vec![], - }; - - Ok(tx) -} - -/// Finalize transaction with a Simplicity witness for the specified input. -/// -/// # Errors -/// Returns error if environment verification or program execution fails. -#[allow(clippy::too_many_arguments)] -pub fn finalize_transaction( - mut tx: Transaction, - program: &CompiledProgram, - program_public_key: &XOnlyPublicKey, - utxos: &[TxOut], - input_index: usize, - witness_values: WitnessValues, - network: SimplicityNetwork, - log_level: TrackerLogLevel, -) -> Result { - let env = get_and_verify_env( - &tx, - program, - program_public_key, - utxos, - network, - input_index, - )?; - - let pruned = run_program(program, witness_values, &env, log_level)?.0; - - let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); - let cmr = pruned.cmr(); - - tx.input[input_index].witness = TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, - script_witness: vec![ - simplicity_witness_bytes, - simplicity_program_bytes, - cmr.as_ref().to_vec(), - control_block(cmr, *program_public_key).serialize(), - ], - pegin_witness: vec![], - }; - - Ok(tx) -} - -/// Build and verify an Elements environment for program execution. -/// -/// # Errors -/// Returns error if UTXO index is invalid or script pubkey doesn't match. -pub fn get_and_verify_env( - tx: &Transaction, - program: &CompiledProgram, - program_public_key: &XOnlyPublicKey, - utxos: &[TxOut], - network: SimplicityNetwork, - input_index: usize, -) -> Result>, ProgramError> { - let params = network.address_params(); - let genesis_hash = network.genesis_block_hash(); - let cmr = program.commit().cmr(); - - if utxos.len() <= input_index { - return Err(ProgramError::UtxoIndexOutOfBounds { - input_index, - utxo_count: utxos.len(), - }); - } - - let target_utxo = &utxos[input_index]; - let script_pubkey = create_p2tr_address(cmr, program_public_key, params).script_pubkey(); - - if target_utxo.script_pubkey != script_pubkey { - return Err(ProgramError::ScriptPubkeyMismatch { - expected_hash: script_pubkey.script_hash().to_string(), - actual_hash: target_utxo.script_pubkey.script_hash().to_string(), - }); - } - - Ok(ElementsEnv::new( - Arc::new(tx.clone()), - utxos - .iter() - .map(|utxo| ElementsUtxo { - script_pubkey: utxo.script_pubkey.clone(), - asset: utxo.asset, - value: utxo.value, - }) - .collect(), - u32::try_from(input_index)?, - cmr, - control_block(cmr, *program_public_key), - None, - genesis_hash, - )) -} diff --git a/crates/simplicityhl-core/src/runner.rs b/crates/simplicityhl-core/src/runner.rs deleted file mode 100644 index 5695e45..0000000 --- a/crates/simplicityhl-core/src/runner.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Program execution helpers and logging levels for Simplicity programs. -//! -//! Provides `run_program` which satisfies and executes a compiled program -//! against an `ElementsEnv`, with optional debug and jet-trace logging. - -use std::sync::Arc; - -use simplicityhl::simplicity::elements::Transaction; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::ElementsEnv; -use simplicityhl::simplicity::{BitMachine, RedeemNode, Value}; -use simplicityhl::tracker::{DefaultTracker, TrackerLogLevel}; -use simplicityhl::{CompiledProgram, WitnessValues}; - -use crate::error::ProgramError; - -/// Satisfy and execute a compiled program in the provided environment. -/// Returns the pruned program and the resulting value. -/// -/// # Errors -/// Returns error if witness satisfaction or program execution fails. -pub fn run_program( - program: &CompiledProgram, - witness_values: WitnessValues, - env: &ElementsEnv>, - log_level: TrackerLogLevel, -) -> Result<(Arc>, Value), ProgramError> { - let satisfied = program - .satisfy(witness_values) - .map_err(ProgramError::WitnessSatisfaction)?; - - let mut tracker = DefaultTracker::new(satisfied.debug_symbols()).with_log_level(log_level); - - let pruned = satisfied.redeem().prune_with_tracker(env, &mut tracker)?; - let mut mac = BitMachine::for_program(&pruned)?; - - let result = mac.exec(&pruned, env).map_err(ProgramError::Execution)?; - - Ok((pruned, result)) -} diff --git a/crates/simplicityhl-core/src/scripts.rs b/crates/simplicityhl-core/src/scripts.rs deleted file mode 100644 index e98ae39..0000000 --- a/crates/simplicityhl-core/src/scripts.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! Script and Taproot utilities plus minor helpers around Elements types. - -use sha2::{Digest, Sha256}; - -use simplicityhl::elements::{ - Address, AddressParams, AssetId, ContractHash, OutPoint, Script, script, taproot, -}; - -use simplicityhl::simplicity::bitcoin::{XOnlyPublicKey, secp256k1}; -use simplicityhl::simplicity::hashes::{Hash, sha256}; -use simplicityhl::{Arguments, CompiledProgram}; - -use crate::error::ProgramError; - -/// Load program source and compile it to a Simplicity program. -/// -/// # Errors -/// Returns error if the program fails to compile. -pub fn load_program(source: &str, arguments: Arguments) -> Result { - let compiled = - CompiledProgram::new(source, arguments, true).map_err(ProgramError::Compilation)?; - Ok(compiled) -} - -/// Generate a non-confidential P2TR address for the given program CMR and key. -#[must_use] -pub fn create_p2tr_address( - cmr: simplicityhl::simplicity::Cmr, - x_only_public_key: &XOnlyPublicKey, - params: &'static AddressParams, -) -> Address { - let spend_info = taproot_spending_info(cmr, *x_only_public_key); - - Address::p2tr( - secp256k1::SECP256K1, - spend_info.internal_key(), - spend_info.merkle_root(), - None, - params, - ) -} - -fn script_version(cmr: simplicityhl::simplicity::Cmr) -> (Script, taproot::LeafVersion) { - let script = script::Script::from(cmr.as_ref().to_vec()); - (script, simplicityhl::simplicity::leaf_version()) -} - -fn taproot_spending_info( - cmr: simplicityhl::simplicity::Cmr, - internal_key: XOnlyPublicKey, -) -> taproot::TaprootSpendInfo { - let builder = taproot::TaprootBuilder::new(); - let (script, version) = script_version(cmr); - let builder = builder - .add_leaf_with_ver(0, script, version) - .expect("tap tree should be valid"); - builder - .finalize(secp256k1::SECP256K1, internal_key) - .expect("tap tree should be valid") -} - -/// Compute the Taproot control block for script-path spending. -/// -/// # Panics -/// Panics if the taproot tree is invalid (should never happen with valid CMR). -#[must_use] -pub fn control_block( - cmr: simplicityhl::simplicity::Cmr, - internal_key: XOnlyPublicKey, -) -> taproot::ControlBlock { - let info = taproot_spending_info(cmr, internal_key); - let script_ver = script_version(cmr); - info.control_block(&script_ver) - .expect("control block should exist") -} - -/// SHA256 hash of an address's scriptPubKey bytes. -#[must_use] -pub fn hash_script(script: &Script) -> [u8; 32] { - let mut hasher = Sha256::new(); - sha2::digest::Update::update(&mut hasher, script.as_bytes()); - hasher.finalize().into() -} - -/// Compute issuance entropy for a new asset given an outpoint and contract hash entropy. -#[must_use] -pub fn get_new_asset_entropy(outpoint: &OutPoint, entropy: [u8; 32]) -> sha256::Midstate { - let contract_hash = ContractHash::from_byte_array(entropy); - AssetId::generate_asset_entropy(*outpoint, contract_hash) -} diff --git a/crates/wallet-abi/Cargo.toml b/crates/wallet-abi/Cargo.toml new file mode 100644 index 0000000..cec1ae2 --- /dev/null +++ b/crates/wallet-abi/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "wallet-abi" +version = "0.1.0" +edition = "2024" +description = "Schema-driven wallet create runtime for Simplicity contracts" +license = "MIT OR Apache-2.0" +publish = false + +[lints] +workspace = true + +[dependencies] +thiserror = "2" + +bincode = "2.0.1" + +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +hex = { workspace = true } +sha2 = { workspace = true } +ring = { workspace = true } + +simplicityhl = { workspace = true } + +lwk_wollet = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_wollet", default-features = false, features = ["electrum", "esplora"] } +lwk_signer = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_signer", default-features = false } +lwk_common = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_common", default-features = false } +lwk_simplicity = { git = "https://github.com/Blockstream/lwk", rev = "4074c527fcc1268eb26953af397eecc6c1444293", package = "lwk_simplicity" } + +tokio = { version = "1", features = ["sync"] } diff --git a/crates/wallet-abi/src/encoding.rs b/crates/wallet-abi/src/encoding.rs new file mode 100644 index 0000000..f512f32 --- /dev/null +++ b/crates/wallet-abi/src/encoding.rs @@ -0,0 +1,83 @@ +pub use bincode::{Decode, Encode}; + +use crate::error::EncodingError; + +/// Trait for binary encoding/decoding with hex string support. +pub trait Encodable { + fn encode(&self) -> Result, EncodingError> + where + Self: Encode, + { + Ok(bincode::encode_to_vec(self, bincode::config::standard())?) + } + + /// Decode from binary and fail if trailing bytes remain. + fn decode(buf: &[u8]) -> Result + where + Self: Sized + Decode<()>, + { + let (decoded, consumed) = bincode::decode_from_slice(buf, bincode::config::standard())?; + if consumed != buf.len() { + return Err(EncodingError::TrailingBytes { + consumed, + total: buf.len(), + }); + } + + Ok(decoded) + } + + fn to_hex(&self) -> Result + where + Self: Encode, + { + Ok(hex::encode(Encodable::encode(self)?)) + } + + fn from_hex(hex: &str) -> Result + where + Self: bincode::Decode<()>, + { + Encodable::decode(&hex::decode(hex)?) + } +} + +#[cfg(test)] +mod tests { + use super::Encodable; + use crate::error::EncodingError; + + #[derive(Debug, PartialEq, bincode::Encode, bincode::Decode)] + struct TestPayload { + value: u32, + } + + impl Encodable for TestPayload {} + + #[test] + fn decode_rejects_trailing_bytes() { + let payload = TestPayload { value: 7 }; + let mut encoded = payload.encode().expect("encodes"); + encoded.push(0); + + let err = ::decode(&encoded).expect_err("must reject trailing"); + assert!(matches!( + err, + EncodingError::TrailingBytes { consumed, total } if consumed + 1 == total + )); + } + + #[test] + fn from_hex_rejects_trailing_bytes() { + let payload = TestPayload { value: 7 }; + let mut encoded_hex = payload.to_hex().expect("encodes"); + encoded_hex.push_str("00"); + + let err = + ::from_hex(&encoded_hex).expect_err("must reject trailing"); + assert!(matches!( + err, + EncodingError::TrailingBytes { consumed, total } if consumed + 1 == total + )); + } +} diff --git a/crates/wallet-abi/src/error.rs b/crates/wallet-abi/src/error.rs new file mode 100644 index 0000000..1e551f1 --- /dev/null +++ b/crates/wallet-abi/src/error.rs @@ -0,0 +1,75 @@ +#[derive(Debug, thiserror::Error)] +pub enum WalletAbiError { + #[error("Invalid request: {0}")] + InvalidRequest(String), + + #[error("Invalid response: {0}")] + InvalidResponse(String), + + #[error("Invalid finalization steps: {0}")] + InvalidFinalizationSteps(String), + + #[error("Invalid signer configuration: {0}")] + InvalidSignerConfig(String), + + #[error("Funding failed: {0}")] + Funding(String), + + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("JSON error: {0}")] + Serde(#[from] serde_json::Error), + + #[error("Hex decode error: {0}")] + Hex(#[from] hex::FromHexError), + + #[error("PSET error: {0}")] + Pset(#[from] simplicityhl::elements::pset::Error), + + #[error("PSET blinding error: {0}")] + PsetBlind(#[from] simplicityhl::elements::pset::PsetBlindError), + + #[error("Transaction decode error: {0}")] + TxDecode(#[from] simplicityhl::elements::encode::Error), + + #[error("Transaction amount proof verification failed: {0}")] + AmountProofVerification(#[from] simplicityhl::elements::VerificationError), + + #[error("Program error: {0}")] + Program(#[from] lwk_simplicity::error::ProgramError), + + #[error("esplora mutex poisoned: {0}")] + EsploraPoisoned(String), + + #[error("LWK Signer error: {0}")] + LWKSigner(#[from] lwk_signer::NewError), + + #[error("LWK sign error: {0}")] + LWKSign(#[from] lwk_signer::SignError), + + #[error("LWK wollet error: {0}")] + LWKWollet(#[from] lwk_wollet::Error), + + #[error("TXOut unblinding error: {0}")] + Unblind(#[from] lwk_wollet::elements::UnblindError), + + #[error("Locktime error: {0}")] + Locktime(#[from] lwk_wollet::elements::locktime::Error), +} + +/// Errors that occur during binary or hex encoding/decoding operations. +#[derive(Debug, thiserror::Error)] +pub enum EncodingError { + #[error("Failed to encode to binary: {0}")] + BinaryEncode(#[from] bincode::error::EncodeError), + + #[error("Failed to decode from binary: {0}")] + BinaryDecode(#[from] bincode::error::DecodeError), + + #[error("Unexpected trailing bytes after decode: consumed {consumed} of {total}")] + TrailingBytes { consumed: usize, total: usize }, + + #[error("Failed to decode hex string: {0}")] + HexDecode(#[from] hex::FromHexError), +} diff --git a/crates/contracts/src/sdk/issuance_validation/mod.rs b/crates/wallet-abi/src/issuance_validation/mod.rs similarity index 95% rename from crates/contracts/src/sdk/issuance_validation/mod.rs rename to crates/wallet-abi/src/issuance_validation/mod.rs index 3aff61d..79ba136 100644 --- a/crates/contracts/src/sdk/issuance_validation/mod.rs +++ b/crates/wallet-abi/src/issuance_validation/mod.rs @@ -1,8 +1,8 @@ -use std::collections::HashSet; - +use lwk_wollet::elements::Script; use simplicityhl::elements::confidential::Value as ConfidentialValue; use simplicityhl::elements::secp256k1_zkp::{SECP256K1, SecretKey, ZERO_TWEAK}; -use simplicityhl::elements::{AssetId, Script, Transaction}; +use simplicityhl::elements::{AssetId, Transaction}; +use std::collections::HashSet; /// Constraints for verifying an issuance transaction. #[derive(Clone, Debug, Default)] @@ -347,9 +347,8 @@ fn verify_asset_destination( #[cfg(test)] mod tests { use super::*; + use lwk_wollet::elements::hex::FromHex; - use crate::options::{OptionsArguments, get_options_address}; - use crate::sdk::taproot_pubkey_gen::TaprootPubkeyGen; use simplicityhl::elements::confidential::{Asset as ConfidentialAsset, Nonce, Value}; use simplicityhl::elements::hashes::Hash; use simplicityhl::elements::pset::serialize::Deserialize; @@ -357,7 +356,6 @@ mod tests { use simplicityhl::elements::{ AssetIssuance, LockTime, OutPoint, Sequence, Transaction, TxIn, TxOut, TxOutWitness, Txid, }; - use simplicityhl_core::{Encodable, SimplicityNetwork}; fn outpoint(vout: u32) -> OutPoint { OutPoint { @@ -617,21 +615,12 @@ mod tests { #[test] fn test_verify_issuance_valid() -> Result<(), String> { - let option_arguments_str = include_str!("./test_data/option_arguments.hex"); - let option_arguments = - OptionsArguments::decode(&hex::decode(option_arguments_str).expect("Invalid hex")) - .unwrap(); - - let taproot_str = include_str!("./test_data/taproot.hex"); - let taproot_gen = TaprootPubkeyGen::build_from_str( - taproot_str, - &option_arguments, - SimplicityNetwork::LiquidTestnet, - &get_options_address, + let script_pubkey = Script::from_hex( + "51203451c2c04047ab5f2d3eb747773e5a9756c10eb9bf31326bd16a6533142f1ace", ) .unwrap(); - let tx_hex = include_str!("./test_data/transaction.hex"); + let tx_hex = include_str!("../../tests/data/tx_with_issuance_token.hex"); let tx_bytes = hex::decode(tx_hex.trim()).unwrap(); let tx: Transaction = Deserialize::deserialize(&tx_bytes[..]).unwrap(); @@ -640,12 +629,12 @@ mod tests { IssuanceInputConstraints { input_idx: 0, issuance_destination: None, - reissuance_destination: Some((taproot_gen.address.script_pubkey(), 1, None)), + reissuance_destination: Some((script_pubkey.clone(), 1, None)), }, IssuanceInputConstraints { input_idx: 1, issuance_destination: None, - reissuance_destination: Some((taproot_gen.address.script_pubkey(), 1, None)), + reissuance_destination: Some((script_pubkey, 1, None)), }, ], ..Default::default() diff --git a/crates/wallet-abi/src/lib.rs b/crates/wallet-abi/src/lib.rs new file mode 100644 index 0000000..ab5a101 --- /dev/null +++ b/crates/wallet-abi/src/lib.rs @@ -0,0 +1,27 @@ +#![warn(clippy::all, clippy::pedantic)] +#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] + +pub mod encoding; +pub mod error; +pub mod issuance_validation; +pub mod runtime; +pub mod schema; +pub mod scripts; +pub mod simplicity; +pub mod taproot_pubkey_gen; +pub mod tx_inclusion; + +pub use encoding::Encodable; +pub use error::WalletAbiError; +pub use lwk_common::Network; +pub use lwk_simplicity::error::ProgramError; +pub use lwk_simplicity::runner::run_program; +pub use lwk_simplicity::signer::{finalize_transaction, get_and_verify_env, get_sighash_all}; +pub use schema::runtime_params::*; +pub use scripts::{ + control_block, create_p2tr_address, get_new_asset_entropy, hash_script, load_program, + simplicity_leaf_version, tap_data_hash, +}; +pub use simplicity::p2pk::{P2PK_SOURCE, execute_p2pk_program, get_p2pk_address, get_p2pk_program}; + +pub use tx_inclusion::*; diff --git a/crates/wallet-abi/src/runtime/input_resolution.rs b/crates/wallet-abi/src/runtime/input_resolution.rs new file mode 100644 index 0000000..17d0363 --- /dev/null +++ b/crates/wallet-abi/src/runtime/input_resolution.rs @@ -0,0 +1,1294 @@ +//! Input resolution for transaction construction. +//! +//! This module balances the input/output equation in four phases: +//! 1. Build demand from all requested outputs and inject implicit fee demand on policy asset. +//! 2. Resolve declared inputs in order. +//! 3. Materialize deferred issuance-linked output demand when referenced inputs are known. +//! 4. Add auxiliary wallet inputs until every positive asset deficit is closed. +//! +//! # Algorithm +//! +//! Auxiliary funding for each asset deficit uses a deterministic stack: +//! 1. Bounded Branch-and-Bound (`BnB`) for exact subset match. +//! 2. Deterministic single-input fallback (largest UTXO above target). +//! 3. Deterministic largest-first accumulation fallback. +//! +//! This mirrors formal coin-selection framing (subset-sum / knapsack) while keeping runtime +//! bounded by an explicit node cap. +//! +//! # Determinism +//! +//! Candidate order and tie-breaks are stable: +//! - primary sort: amount descending +//! - tie-break 1: `txid` lexicographic ascending +//! - tie-break 2: `vout` ascending +//! +//! For multiple exact `BnB` matches with equal input count, the lexicographically smaller +//! outpoint list is selected. +//! +//! # Complexity +//! +//! Let: +//! - `O` = number of outputs +//! - `I` = number of declared inputs +//! - `U` = wallet UTXO count in snapshot +//! - `A` = number of distinct demanded assets +//! - `K` = number of auxiliary inputs added +//! - `N` = max candidate UTXOs for one deficit asset +//! +//! Worst-case time is: +//! - declared-input selection: `O(I * U * A)` +//! - auxiliary selection per deficit asset: bounded Branch-and-Bound search +//! plus deterministic fallbacks, `O(MAX_BNB_NODES + N)` +//! - overall: `O(I * U * A + K * (MAX_BNB_NODES + N) + O + I)` +//! +//! Space is `O(U + A + O + N)` for used-outpoint tracking, equation state and +//! per-asset candidate working sets. +//! +//! # Failure modes +//! +//! - Duplicate `"fee"` output ids fail fast. +//! - Fee output with non-policy asset fails fast. +//! - Arithmetic overflow fails with `InvalidRequest`. +//! - Unclosable deficits fail with `Funding`. + +use crate::runtime::{WalletRuntimeConfig, get_finalizer_spec_key, get_secrets_spec_key}; +use crate::{ + AmountFilter, AssetFilter, AssetVariant, FinalizerSpec, InputBlinder, InputIssuance, + InputIssuanceKind, InputSchema, LockFilter, RuntimeParams, UTXOSource, WalletAbiError, + WalletSourceFilter, +}; + +use lwk_common::Bip::Bip84; +use lwk_common::Signer; +use lwk_wollet::bitcoin::bip32::{ChildNumber, DerivationPath}; +use lwk_wollet::elements::confidential::{Asset, AssetBlindingFactor, Value, ValueBlindingFactor}; +use lwk_wollet::elements::hashes::Hash; +use lwk_wollet::elements::pset::{Input, PartiallySignedTransaction}; +use lwk_wollet::elements::{AssetId, ContractHash, OutPoint, TxOut, TxOutSecrets, secp256k1_zkp}; +use lwk_wollet::{Chain, EC, WalletTxOut}; +use std::collections::{BTreeMap, HashMap, HashSet}; + +type CandidateScore = (u64, u64, u64, String, u32); + +type Midstate = lwk_wollet::elements::hashes::sha256::Midstate; +/// Upper bound on DFS nodes visited by `BnB` before deterministic fallback is used. +const MAX_BNB_NODES: usize = 100_000; + +/// Auxiliary `BnB` candidate projection used for deterministic subset search. +#[derive(Clone, Debug, Eq, PartialEq)] +struct BnbCandidate { + amount_sat: u64, + txid_lex: String, + vout: u32, +} + +/// Selected auxiliary strategy used for one deficit asset. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum BnbSelectionStatus { + Exact, + FallbackSingleLargestAboveTarget, + FallbackLargestFirstAccumulation, +} + +#[derive(Clone, Copy)] +struct WalletDerivationIndex { + ext_int: Chain, + wildcard_index: u32, +} + +#[derive(Clone, Copy)] +enum DeferredDemandKind { + NewIssuanceAsset, + NewIssuanceToken, + ReIssuanceAsset, +} + +#[derive(Default)] +struct ResolutionState { + used_outpoints: HashSet, + demand_by_asset: BTreeMap, + supply_by_asset: BTreeMap, + deferred_demands: HashMap>, +} + +struct ResolvedInputMaterial { + outpoint: OutPoint, + tx_out: TxOut, + secrets: TxOutSecrets, + wallet_derivation: Option, +} + +/// Add `amount_sat` to one asset bucket with overflow protection. +pub(super) fn add_balance( + map: &mut BTreeMap, + asset_id: AssetId, + amount_sat: u64, +) -> Result<(), WalletAbiError> { + let entry = map.entry(asset_id).or_insert(0); + *entry = entry.checked_add(amount_sat).ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "asset amount overflow while aggregating balances for {asset_id}" + )) + })?; + Ok(()) +} + +/// Compute positive deficits `(demand - supply)` per asset. +/// +/// ```rust,ignore +/// use std::collections::BTreeMap; +/// use lwk_wollet::elements::AssetId; +/// +/// let lbtc = AssetId::from_slice(&[1u8; 32]).unwrap(); +/// let usdt = AssetId::from_slice(&[2u8; 32]).unwrap(); +/// let demand = BTreeMap::from([(lbtc, 9_u64), (usdt, 5)]); +/// let supply = BTreeMap::from([(lbtc, 4_u64), (usdt, 5)]); +/// let d = current_deficits(&demand, &supply); +/// assert_eq!(d, BTreeMap::from([(lbtc, 5)])); +/// ``` +fn current_deficits( + demand_by_asset: &BTreeMap, + supply_by_asset: &BTreeMap, +) -> BTreeMap { + // Deficits are kept only for assets where demand is still strictly above supply. + let mut deficits = BTreeMap::new(); + for (asset_id, demand_sat) in demand_by_asset { + let supplied = supply_by_asset.get(asset_id).copied().unwrap_or(0); + if *demand_sat > supplied { + deficits.insert(*asset_id, demand_sat - supplied); + } + } + deficits +} + +/// Reserve an outpoint and fail if it was already used. +fn reserve_outpoint( + used_outpoints: &mut HashSet, + input_id: &str, + outpoint: OutPoint, +) -> Result<(), WalletAbiError> { + if used_outpoints.insert(outpoint) { + return Ok(()); + } + + Err(WalletAbiError::InvalidRequest(format!( + "duplicate input outpoint resolved for '{}': {}:{}", + input_id, outpoint.txid, outpoint.vout + ))) +} + +/// Validate that an output reference points to an existing declared input index. +fn validate_output_input_index( + output_id: &str, + input_index: u32, + input_count: usize, +) -> Result<(), WalletAbiError> { + let idx = usize::try_from(input_index).map_err(|_| { + WalletAbiError::InvalidRequest(format!( + "output '{output_id}' input_index overflow: {input_index}" + )) + })?; + + if idx >= input_count { + return Err(WalletAbiError::InvalidRequest(format!( + "output '{output_id}' references missing input_index {input_index}" + ))); + } + + Ok(()) +} + +/// Compute issuance entropy from input outpoint and issuance kind. +pub(super) fn derive_issuance_entropy(outpoint: OutPoint, issuance: &InputIssuance) -> Midstate { + match issuance.kind { + InputIssuanceKind::New => AssetId::generate_asset_entropy( + outpoint, + ContractHash::from_byte_array(issuance.entropy), + ), + InputIssuanceKind::Reissue => Midstate::from_byte_array(issuance.entropy), + } +} + +/// Resolve issuance token id for the current runtime issuance model. +/// +/// This mirrors `elements::pset::Input::issuance_ids()` token derivation semantics where +/// the token confidentiality flag tracks `issuance_value_comm.is_some()`. +/// +/// Runtime currently sets unblinded issuance amounts (`issuance_value_amount`) and does not +/// populate `issuance_value_comm`, so the confidentiality flag is intentionally fixed to `false`. +pub(super) fn issuance_token_from_entropy_for_unblinded_issuance( + issuance_entropy: Midstate, +) -> AssetId { + let issuance_value_commitment_present = false; + AssetId::reissuance_token_from_entropy(issuance_entropy, issuance_value_commitment_present) +} + +/// Resolve a deferred issuance-linked output demand into a concrete asset id. +fn demand_asset_from_deferred( + kind: DeferredDemandKind, + issuance: &InputIssuance, + material: &ResolvedInputMaterial, + input_id: &str, +) -> Result { + match (kind, &issuance.kind) { + (DeferredDemandKind::NewIssuanceAsset, InputIssuanceKind::New) + | (DeferredDemandKind::ReIssuanceAsset, InputIssuanceKind::Reissue) => Ok( + AssetId::from_entropy(derive_issuance_entropy(material.outpoint, issuance)), + ), + (DeferredDemandKind::NewIssuanceToken, InputIssuanceKind::New) => { + Ok(issuance_token_from_entropy_for_unblinded_issuance( + derive_issuance_entropy(material.outpoint, issuance), + )) + } + (DeferredDemandKind::NewIssuanceAsset, InputIssuanceKind::Reissue) => { + Err(WalletAbiError::InvalidRequest(format!( + "output asset variant new_issuance_asset references reissue input '{input_id}'" + ))) + } + (DeferredDemandKind::NewIssuanceToken, InputIssuanceKind::Reissue) => { + Err(WalletAbiError::InvalidRequest(format!( + "output asset variant new_issuance_token references reissue input '{input_id}'" + ))) + } + (DeferredDemandKind::ReIssuanceAsset, InputIssuanceKind::New) => { + Err(WalletAbiError::InvalidRequest(format!( + "output asset variant re_issuance_asset references new issuance input '{input_id}'" + ))) + } + } +} + +/// Populate issuance-related PSET input fields from request metadata. +fn apply_issuance_to_pset_input( + pset_input: &mut Input, + issuance: &InputIssuance, + secrets: &TxOutSecrets, +) -> Result<(), WalletAbiError> { + pset_input.issuance_value_amount = if issuance.asset_amount_sat == 0 { + None + } else { + Some(issuance.asset_amount_sat) + }; + pset_input.issuance_asset_entropy = Some(issuance.entropy); + pset_input.issuance_inflation_keys = if issuance.token_amount_sat == 0 { + None + } else { + Some(issuance.token_amount_sat) + }; + + if issuance.kind == InputIssuanceKind::Reissue { + // Runtime currently emits unblinded issuance amounts; for reissuance we still need a + // non-zero nonce and derive it from the input asset blinding factor. + let mut nonce = secrets.asset_bf.into_inner(); + if nonce == secp256k1_zkp::ZERO_TWEAK { + let mut one = [0u8; 32]; + one[0] = 1; + nonce = secp256k1_zkp::Tweak::from_slice(&one).map_err(|error| { + WalletAbiError::InvalidRequest(format!( + "failed to construct non-zero reissuance blinding nonce: {error}" + )) + })?; + } + pset_input.issuance_blinding_nonce = Some(nonce); + } + + pset_input.blinded_issuance = Some(0x00); + + Ok(()) +} + +/// Check whether a wallet UTXO candidate satisfies source filters and is unused. +fn matches_wallet_filter( + candidate: &WalletTxOut, + filter: &WalletSourceFilter, + used_outpoints: &HashSet, +) -> bool { + if used_outpoints.contains(&candidate.outpoint) { + return false; + } + + let asset_ok = match filter.asset { + AssetFilter::None => true, + AssetFilter::Exact { asset_id } => candidate.unblinded.asset == asset_id, + }; + if !asset_ok { + return false; + } + + let amount_ok = match filter.amount { + AmountFilter::None => true, + AmountFilter::Exact { satoshi } => candidate.unblinded.value == satoshi, + AmountFilter::Min { satoshi } => candidate.unblinded.value >= satoshi, + }; + if !amount_ok { + return false; + } + + match &filter.lock { + LockFilter::None => true, + LockFilter::Script { script } => candidate.script_pubkey == *script, + } +} + +/// Score one candidate by simulating its supply contribution. +/// +/// Lower score tuple is better. +fn score_candidate( + candidate: &WalletTxOut, + demand_by_asset: &BTreeMap, + supply_by_asset: &BTreeMap, +) -> Result { + // Simulate adding this candidate to the current supply map, then compute a + // deterministic lexicographic score that favors candidates which reduce deficits fastest. + let mut simulated_supply = supply_by_asset.clone(); + let current_supply = simulated_supply + .get(&candidate.unblinded.asset) + .copied() + .unwrap_or(0); + let updated_supply = current_supply + .checked_add(candidate.unblinded.value) + .ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "asset amount overflow while scoring candidate {}:{}", + candidate.outpoint.txid, candidate.outpoint.vout + )) + })?; + simulated_supply.insert(candidate.unblinded.asset, updated_supply); + + let mut total_remaining_deficit = 0u64; + for (asset_id, demand_sat) in demand_by_asset { + let supplied = simulated_supply.get(asset_id).copied().unwrap_or(0); + let remaining = demand_sat.saturating_sub(supplied); + total_remaining_deficit = + total_remaining_deficit + .checked_add(remaining) + .ok_or_else(|| { + WalletAbiError::InvalidRequest( + "deficit overflow while scoring wallet candidates".to_string(), + ) + })?; + } + + let candidate_demand = demand_by_asset + .get(&candidate.unblinded.asset) + .copied() + .unwrap_or(0); + let candidate_before_supply = supply_by_asset + .get(&candidate.unblinded.asset) + .copied() + .unwrap_or(0); + let candidate_after_supply = simulated_supply + .get(&candidate.unblinded.asset) + .copied() + .unwrap_or(0); + + let remaining_candidate_deficit = candidate_demand.saturating_sub(candidate_after_supply); + let needed_before = candidate_demand.saturating_sub(candidate_before_supply); + let overshoot_or_undershoot = candidate.unblinded.value.abs_diff(needed_before); + + Ok(( + total_remaining_deficit, + remaining_candidate_deficit, + overshoot_or_undershoot, + candidate.outpoint.txid.to_string(), + candidate.outpoint.vout, + )) +} + +/// Sort candidates using the canonical deterministic order for `BnB` and fallbacks. +/// +/// Order: +/// 1. amount descending +/// 2. txid lexicographic ascending +/// 3. vout ascending +fn sort_bnb_candidates(candidates: &mut [BnbCandidate]) { + candidates.sort_by(|a, b| { + b.amount_sat + .cmp(&a.amount_sat) + .then_with(|| a.txid_lex.cmp(&b.txid_lex)) + .then_with(|| a.vout.cmp(&b.vout)) + }); +} + +/// Build a comparable outpoint-key for one candidate subset. +/// +/// The key is sorted so subset order itself does not affect comparisons. +fn subset_lexicographic_key(indices: &[usize], candidates: &[BnbCandidate]) -> Vec<(String, u32)> { + let mut key = indices + .iter() + .map(|index| { + let candidate = &candidates[*index]; + (candidate.txid_lex.clone(), candidate.vout) + }) + .collect::>(); + key.sort(); + key +} + +/// Compare two exact-match subsets by deterministic tie-break rules. +/// +/// Preference: +/// 1. fewer selected inputs +/// 2. lexicographically smaller outpoint key +fn is_better_exact_subset( + proposed: &[usize], + current_best: Option<&[usize]>, + candidates: &[BnbCandidate], +) -> bool { + let Some(current_best) = current_best else { + return true; + }; + + if proposed.len() < current_best.len() { + return true; + } + if proposed.len() > current_best.len() { + return false; + } + + subset_lexicographic_key(proposed, candidates) + < subset_lexicographic_key(current_best, candidates) +} + +fn build_bnb_suffix_sums(candidates: &[BnbCandidate]) -> Result, WalletAbiError> { + let mut suffix_sum_sat = vec![0u64; candidates.len() + 1]; + for index in (0..candidates.len()).rev() { + suffix_sum_sat[index] = suffix_sum_sat[index + 1] + .checked_add(candidates[index].amount_sat) + .ok_or_else(|| { + WalletAbiError::InvalidRequest( + "asset amount overflow while computing BnB suffix sums".to_string(), + ) + })?; + } + + Ok(suffix_sum_sat) +} + +struct BnbSearch<'a> { + target_sat: u64, + candidates: &'a [BnbCandidate], + suffix_sum_sat: &'a [u64], + max_nodes: usize, + nodes_visited: usize, + node_limit_hit: bool, + current: Vec, + best: Option>, +} + +impl<'a> BnbSearch<'a> { + const fn new( + target_sat: u64, + candidates: &'a [BnbCandidate], + suffix_sum_sat: &'a [u64], + max_nodes: usize, + ) -> Self { + Self { + target_sat, + candidates, + suffix_sum_sat, + max_nodes, + nodes_visited: 0, + node_limit_hit: false, + current: Vec::new(), + best: None, + } + } + + const fn mark_node_visit(&mut self) -> bool { + self.nodes_visited = self.nodes_visited.saturating_add(1); + self.nodes_visited > self.max_nodes + } + + fn record_exact_if_better(&mut self) { + if is_better_exact_subset(&self.current, self.best.as_deref(), self.candidates) { + self.best = Some(self.current.clone()); + } + } + + fn can_reach_target(&self, index: usize, sum_sat: u64) -> Result { + let max_possible = sum_sat + .checked_add(self.suffix_sum_sat[index]) + .ok_or_else(|| { + WalletAbiError::InvalidRequest( + "asset amount overflow while evaluating BnB pruning bounds".to_string(), + ) + })?; + + Ok(max_possible >= self.target_sat) + } + + fn search(&mut self, index: usize, sum_sat: u64) -> Result<(), WalletAbiError> { + if self.node_limit_hit { + return Ok(()); + } + if self.mark_node_visit() { + self.node_limit_hit = true; + return Ok(()); + } + + if sum_sat == self.target_sat { + self.record_exact_if_better(); + return Ok(()); + } + if index >= self.candidates.len() || sum_sat > self.target_sat { + return Ok(()); + } + if !self.can_reach_target(index, sum_sat)? { + return Ok(()); + } + + let included_sum = sum_sat + .checked_add(self.candidates[index].amount_sat) + .ok_or_else(|| { + WalletAbiError::InvalidRequest( + "asset amount overflow while evaluating BnB include branch".to_string(), + ) + })?; + if included_sum <= self.target_sat { + self.current.push(index); + self.search(index + 1, included_sum)?; + self.current.pop(); + } + + self.search(index + 1, sum_sat) + } +} + +/// Bounded depth-first Branch-and-Bound search for an exact subset sum. +/// +/// Returns: +/// - `Some(indices)` on exact match +/// - `None` if no exact match or node bound is reached +/// +/// Pruning: +/// - stop include branch when `sum > target` +/// - stop branch when `sum + remaining < target` +fn bnb_exact_subset_indices( + candidates: &[BnbCandidate], + target_sat: u64, + max_nodes: usize, +) -> Result>, WalletAbiError> { + if target_sat == 0 { + return Ok(Some(Vec::new())); + } + if candidates.is_empty() { + return Ok(None); + } + + let suffix_sum_sat = build_bnb_suffix_sums(candidates)?; + let mut search = BnbSearch::new(target_sat, candidates, &suffix_sum_sat, max_nodes); + search.search(0, 0)?; + + if search.node_limit_hit { + return Ok(None); + } + + Ok(search.best) +} + +/// Deterministic fallback A: select one largest UTXO whose amount is `>= target`. +/// +/// Candidates are expected to be pre-sorted with `sort_bnb_candidates`. +fn select_single_largest_above_target( + candidates: &[BnbCandidate], + target_sat: u64, +) -> Option> { + candidates + .iter() + .position(|candidate| candidate.amount_sat >= target_sat) + .map(|index| vec![index]) +} + +/// Deterministic fallback B: accumulate largest-first until the target is reached. +/// +/// Candidates are expected to be pre-sorted with `sort_bnb_candidates`. +fn select_largest_first_accumulation( + candidates: &[BnbCandidate], + target_sat: u64, +) -> Result>, WalletAbiError> { + let mut selected_indices = Vec::new(); + let mut sum_sat = 0u64; + + for (index, candidate) in candidates.iter().enumerate() { + selected_indices.push(index); + sum_sat = sum_sat.checked_add(candidate.amount_sat).ok_or_else(|| { + WalletAbiError::InvalidRequest( + "asset amount overflow while running fallback accumulation".to_string(), + ) + })?; + if sum_sat >= target_sat { + return Ok(Some(selected_indices)); + } + } + + Ok(None) +} + +/// Sum selected candidate amounts with overflow checks. +fn sum_selected_amount( + candidates: &[BnbCandidate], + selected_indices: &[usize], +) -> Result { + selected_indices.iter().try_fold(0u64, |sum, index| { + sum.checked_add(candidates[*index].amount_sat) + .ok_or_else(|| { + WalletAbiError::InvalidRequest( + "asset amount overflow while summing selected auxiliary inputs".to_string(), + ) + }) + }) +} + +impl WalletRuntimeConfig { + /// Build demand from output specs and store issuance-linked entries as deferred. + /// + /// Rules: + /// - Non-fee outputs contribute demand directly (or deferred for issuance-derived assets). + /// - Exactly one implicit policy-asset demand entry is added for `fee_target_sat`. + /// - `"fee"` output, if present, is validated only for uniqueness and policy-asset type. + /// - Caller-provided fee output amount is ignored for demand accounting. + fn resolve_output_demands( + params: &RuntimeParams, + fee_target_sat: u64, + policy_asset: AssetId, + state: &mut ResolutionState, + ) -> Result<(), WalletAbiError> { + // Convert output-level asset requirements into equation demand. + // Issuance-derived outputs are deferred until their referenced input is resolved. + let mut fee_output_seen = false; + for output in ¶ms.outputs { + if output.id == "fee" { + if fee_output_seen { + return Err(WalletAbiError::InvalidRequest( + "duplicate output id 'fee' in params.outputs".to_string(), + )); + } + fee_output_seen = true; + + match output.asset { + AssetVariant::AssetId { asset_id } if asset_id == policy_asset => {} + AssetVariant::AssetId { asset_id } => { + return Err(WalletAbiError::InvalidRequest(format!( + "fee output must use policy asset {policy_asset}, found {asset_id}" + ))); + } + _ => { + return Err(WalletAbiError::InvalidRequest( + "fee output must use explicit asset_id policy asset variant" + .to_string(), + )); + } + } + + continue; + } + + match &output.asset { + AssetVariant::AssetId { asset_id } => { + add_balance(&mut state.demand_by_asset, *asset_id, output.amount_sat)?; + } + AssetVariant::NewIssuanceAsset { input_index } => { + validate_output_input_index(&output.id, *input_index, params.inputs.len())?; + state + .deferred_demands + .entry(*input_index) + .or_default() + .push((DeferredDemandKind::NewIssuanceAsset, output.amount_sat)); + } + AssetVariant::NewIssuanceToken { input_index } => { + validate_output_input_index(&output.id, *input_index, params.inputs.len())?; + state + .deferred_demands + .entry(*input_index) + .or_default() + .push((DeferredDemandKind::NewIssuanceToken, output.amount_sat)); + } + AssetVariant::ReIssuanceAsset { input_index } => { + validate_output_input_index(&output.id, *input_index, params.inputs.len())?; + state + .deferred_demands + .entry(*input_index) + .or_default() + .push((DeferredDemandKind::ReIssuanceAsset, output.amount_sat)); + } + } + } + + // Fee demand is always modeled from runtime target, independent of params fee amount. + add_balance(&mut state.demand_by_asset, policy_asset, fee_target_sat)?; + + Ok(()) + } + + /// Resolve input material from a provided outpoint and optional blinder hints. + async fn resolve_provided_input_material( + &self, + input: &InputSchema, + outpoint: OutPoint, + state: &mut ResolutionState, + ) -> Result { + reserve_outpoint(&mut state.used_outpoints, &input.id, outpoint)?; + + let tx_out = self.fetch_tx_out(&outpoint).await?; + + let secrets = match &input.blinder { + InputBlinder::Wallet => { + let (_, unblinded) = self.unblind_with_wallet(tx_out.clone())?; + + unblinded + } + InputBlinder::Provided { secret_key } => { + tx_out.unblind(&EC, *secret_key).map_err(|error| { + WalletAbiError::InvalidRequest(format!( + "unable to unblind input '{}' with provided blinder: {error}", + input.id + )) + })? + } + InputBlinder::Explicit => { + let (Asset::Explicit(asset), Value::Explicit(value)) = (tx_out.asset, tx_out.value) + else { + return Err(WalletAbiError::InvalidRequest(format!( + "marked input '{}' as explicit when the confidential was provided", + input.id + ))); + }; + + TxOutSecrets { + asset, + asset_bf: AssetBlindingFactor::zero(), + value, + value_bf: ValueBlindingFactor::zero(), + } + } + }; + + Ok(ResolvedInputMaterial { + outpoint, + tx_out, + secrets, + wallet_derivation: None, + }) + } + + /// Resolve input material from wallet snapshot using deficit-aware selection. + async fn resolve_wallet_input_material( + &self, + input: &InputSchema, + filter: &WalletSourceFilter, + wallet_snapshot: &[WalletTxOut], + state: &mut ResolutionState, + ) -> Result { + let selected = Self::filter_tx_out( + wallet_snapshot, + filter, + &state.used_outpoints, + &state.demand_by_asset, + &state.supply_by_asset, + )? + .ok_or_else(|| { + WalletAbiError::Funding(format!( + "no wallet UTXO matched contract input '{}' filter", + input.id + )) + })?; + + reserve_outpoint(&mut state.used_outpoints, &input.id, selected.outpoint)?; + + let tx_out = self.fetch_tx_out(&selected.outpoint).await?; + + Ok(ResolvedInputMaterial { + outpoint: selected.outpoint, + tx_out, + secrets: selected.unblinded, + wallet_derivation: Some(WalletDerivationIndex { + ext_int: selected.ext_int, + wildcard_index: selected.wildcard_index, + }), + }) + } + + /// Resolve one declared input from either provided or wallet source. + async fn resolve_declared_input_material( + &self, + input: &InputSchema, + wallet_snapshot: &[WalletTxOut], + state: &mut ResolutionState, + ) -> Result { + match &input.utxo_source { + UTXOSource::Wallet { filter } => { + self.resolve_wallet_input_material(input, filter, wallet_snapshot, state) + .await + } + UTXOSource::Provided { outpoint } => { + self.resolve_provided_input_material(input, *outpoint, state) + .await + } + } + } + + fn signer_origin_for_wallet_utxo( + &self, + index: WalletDerivationIndex, + ) -> Result<(lwk_wollet::elements::bitcoin::PublicKey, DerivationPath), WalletAbiError> { + let ext_int = match index.ext_int { + Chain::External => ChildNumber::from_normal_idx(0), + Chain::Internal => ChildNumber::from_normal_idx(1), + } + .map_err(|error| { + WalletAbiError::InvalidSignerConfig(format!( + "invalid change index for descriptor derivation: {error}" + )) + })?; + let wildcard = ChildNumber::from_normal_idx(index.wildcard_index).map_err(|error| { + WalletAbiError::InvalidRequest(format!( + "invalid wallet wildcard index {}: {error}", + index.wildcard_index + )) + })?; + + let derivation_path = self + .get_derivation_path(Bip84) + .child(ext_int) + .child(wildcard); + let pubkey = self.signer.derive_xpub(&derivation_path)?.public_key.into(); + + Ok((pubkey, derivation_path)) + } + + /// Append a resolved input to the PSET and attach sequence, prevout and witness UTXO. + fn add_resolved_input_to_pset( + &self, + pst: &mut PartiallySignedTransaction, + input: &InputSchema, + material: &ResolvedInputMaterial, + ) -> Result<(), WalletAbiError> { + let mut pset_input = Input::from_prevout(material.outpoint); + pset_input.sequence = Some(input.sequence); + pset_input.witness_utxo = Some(material.tx_out.clone()); + pset_input.amount = Some(material.secrets.value); + pset_input.asset = Some(material.secrets.asset); + + if let Some(issuance) = input.issuance.as_ref() { + apply_issuance_to_pset_input(&mut pset_input, issuance, &material.secrets)?; + } + + pset_input + .proprietary + .insert(get_finalizer_spec_key(), input.finalizer.try_encode()?); + pset_input.proprietary.insert( + get_secrets_spec_key(), + serde_json::to_vec(&material.secrets)?, + ); + if let Some(index) = material.wallet_derivation { + let (pubkey, derivation_path) = self.signer_origin_for_wallet_utxo(index)?; + pset_input + .bip32_derivation + .insert(pubkey, (self.signer.fingerprint(), derivation_path)); + } + pst.add_input(pset_input); + + Ok(()) + } + + /// Apply the resolved input contribution to equation supply (base + issuance minting). + fn apply_input_supply( + input: &InputSchema, + material: &ResolvedInputMaterial, + state: &mut ResolutionState, + ) -> Result<(), WalletAbiError> { + add_balance( + &mut state.supply_by_asset, + material.secrets.asset, + material.secrets.value, + )?; + + if let Some(issuance) = input.issuance.as_ref() { + let issuance_entropy = derive_issuance_entropy(material.outpoint, issuance); + let issuance_asset = AssetId::from_entropy(issuance_entropy); + add_balance( + &mut state.supply_by_asset, + issuance_asset, + issuance.asset_amount_sat, + )?; + + if issuance.token_amount_sat > 0 { + let token_asset = + issuance_token_from_entropy_for_unblinded_issuance(issuance_entropy); + add_balance( + &mut state.supply_by_asset, + token_asset, + issuance.token_amount_sat, + )?; + } + } + + Ok(()) + } + + /// Convert deferred issuance-linked demand into concrete asset demand for one input index. + fn activate_deferred_demands_for_input( + input_index: usize, + input: &InputSchema, + material: &ResolvedInputMaterial, + state: &mut ResolutionState, + ) -> Result<(), WalletAbiError> { + // Deferred demands become concrete once the referenced input is known, + // because issuance-derived asset ids depend on that input outpoint/entropy. + let input_index_u32 = u32::try_from(input_index).map_err(|_| { + WalletAbiError::InvalidRequest(format!( + "input index overflow while activating deferred demands: {input_index}" + )) + })?; + let Some(entries) = state.deferred_demands.remove(&input_index_u32) else { + return Ok(()); + }; + + let issuance = input.issuance.as_ref().ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "output asset references input {} but input '{}' has no issuance metadata", + input_index, input.id + )) + })?; + + for (kind, amount_sat) in entries { + let demand_asset = demand_asset_from_deferred(kind, issuance, material, &input.id)?; + add_balance(&mut state.demand_by_asset, demand_asset, amount_sat)?; + } + + Ok(()) + } + + /// Resolve all declared inputs in order and mutate both PSET and equation state. + async fn resolve_declared_inputs( + &self, + pst: &mut PartiallySignedTransaction, + params: &RuntimeParams, + wallet_snapshot: &[WalletTxOut], + state: &mut ResolutionState, + ) -> Result<(), WalletAbiError> { + // Main declared-input pass: + // resolve source -> append PSET input -> increase supply -> unlock deferred demands. + for (input_index, input) in params.inputs.iter().enumerate() { + let material = self + .resolve_declared_input_material(input, wallet_snapshot, state) + .await?; + + self.add_resolved_input_to_pset(pst, input, &material)?; + Self::apply_input_supply(input, &material, state)?; + Self::activate_deferred_demands_for_input(input_index, input, &material, state)?; + } + + Ok(()) + } + + /// Pick the currently largest positive deficit asset (tie-break by asset id ordering). + fn pick_largest_deficit_asset(state: &ResolutionState) -> Option<(AssetId, u64)> { + current_deficits(&state.demand_by_asset, &state.supply_by_asset) + .iter() + .fold( + None, + |best: Option<(AssetId, u64)>, (asset, missing)| match best { + None => Some((*asset, *missing)), + Some((best_asset, best_missing)) => { + if *missing > best_missing + || (*missing == best_missing && *asset < best_asset) + { + Some((*asset, *missing)) + } else { + Some((best_asset, best_missing)) + } + } + }, + ) + } + + /// Select deterministic auxiliary wallet inputs for one deficit asset. + /// + /// Strategy order: + /// 1. exact `BnB` + /// 2. single largest-above-target + /// 3. largest-first accumulation + fn select_auxiliary_inputs_for_asset( + wallet_snapshot: &[WalletTxOut], + used_outpoints: &HashSet, + target_asset: AssetId, + target_missing: u64, + ) -> Result<(Vec, BnbSelectionStatus), WalletAbiError> { + let mut wallet_candidates: Vec = wallet_snapshot + .iter() + .filter(|candidate| { + !used_outpoints.contains(&candidate.outpoint) + && candidate.unblinded.asset == target_asset + }) + .cloned() + .collect(); + if wallet_candidates.is_empty() { + return Err(WalletAbiError::Funding( + "unable to cover remaining deficits with wallet utxos".to_string(), + )); + } + + let mut bnb_candidates = wallet_candidates + .iter() + .map(|candidate| BnbCandidate { + amount_sat: candidate.unblinded.value, + txid_lex: candidate.outpoint.txid.to_string(), + vout: candidate.outpoint.vout, + }) + .collect::>(); + sort_bnb_candidates(&mut bnb_candidates); + wallet_candidates.sort_by(|a, b| { + b.unblinded + .value + .cmp(&a.unblinded.value) + .then_with(|| { + a.outpoint + .txid + .to_string() + .cmp(&b.outpoint.txid.to_string()) + }) + .then_with(|| a.outpoint.vout.cmp(&b.outpoint.vout)) + }); + + let (selected_indices, status) = if let Some(exact) = + bnb_exact_subset_indices(&bnb_candidates, target_missing, MAX_BNB_NODES)? + { + (exact, BnbSelectionStatus::Exact) + } else if let Some(single) = + select_single_largest_above_target(&bnb_candidates, target_missing) + { + (single, BnbSelectionStatus::FallbackSingleLargestAboveTarget) + } else if let Some(accumulated) = + select_largest_first_accumulation(&bnb_candidates, target_missing)? + { + ( + accumulated, + BnbSelectionStatus::FallbackLargestFirstAccumulation, + ) + } else { + return Err(WalletAbiError::Funding( + "unable to cover remaining deficits with wallet utxos".to_string(), + )); + }; + + let selected_total = sum_selected_amount(&bnb_candidates, &selected_indices)?; + if selected_total < target_missing { + return Err(WalletAbiError::Funding( + "unable to cover remaining deficits with wallet utxos".to_string(), + )); + } + + let selected = selected_indices + .iter() + .map(|index| wallet_candidates[*index].clone()) + .collect::>(); + + Ok((selected, status)) + } + + async fn add_auxiliary_wallet_input( + &self, + pst: &mut PartiallySignedTransaction, + state: &mut ResolutionState, + selected: &WalletTxOut, + ) -> Result<(), WalletAbiError> { + if !state.used_outpoints.insert(selected.outpoint) { + return Err(WalletAbiError::InvalidRequest(format!( + "duplicate auxiliary outpoint resolved: {}:{}", + selected.outpoint.txid, selected.outpoint.vout + ))); + } + + // TODO: use wollet cache here instead of fetching. + let tx_out = self.fetch_tx_out(&selected.outpoint).await?; + let mut pset_input = Input::from_prevout(selected.outpoint); + pset_input.witness_utxo = Some(tx_out); + pset_input.amount = Some(selected.unblinded.value); + pset_input.asset = Some(selected.unblinded.asset); + pset_input.proprietary.insert( + get_finalizer_spec_key(), + FinalizerSpec::Wallet.try_encode()?, + ); + pset_input.proprietary.insert( + get_secrets_spec_key(), + serde_json::to_vec(&selected.unblinded)?, + ); + let (pubkey, derivation_path) = + self.signer_origin_for_wallet_utxo(WalletDerivationIndex { + ext_int: selected.ext_int, + wildcard_index: selected.wildcard_index, + })?; + pset_input + .bip32_derivation + .insert(pubkey, (self.signer.fingerprint(), derivation_path)); + pst.add_input(pset_input); + + add_balance( + &mut state.supply_by_asset, + selected.unblinded.asset, + selected.unblinded.value, + )?; + + Ok(()) + } + + /// Add one or more auxiliary wallet inputs targeting one missing asset amount. + /// + /// The selected inputs are appended in deterministic order and each contribution updates + /// `supply_by_asset` immediately. + async fn add_auxiliary_input_for_asset( + &self, + pst: &mut PartiallySignedTransaction, + wallet_snapshot: &[WalletTxOut], + state: &mut ResolutionState, + target_asset: AssetId, + target_missing: u64, + ) -> Result<(), WalletAbiError> { + let (selected_inputs, _status) = Self::select_auxiliary_inputs_for_asset( + wallet_snapshot, + &state.used_outpoints, + target_asset, + target_missing, + )?; + + for selected in &selected_inputs { + self.add_auxiliary_wallet_input(pst, state, selected) + .await?; + } + + Ok(()) + } + + /// Repeatedly add auxiliary wallet inputs until there is no remaining positive deficit. + /// + /// Assets are processed by current largest deficit (asset-id tie-break). + async fn add_auxiliary_inputs_until_balanced( + &self, + pst: &mut PartiallySignedTransaction, + wallet_snapshot: &[WalletTxOut], + state: &mut ResolutionState, + ) -> Result<(), WalletAbiError> { + // Keep injecting auxiliary inputs until the equation has no remaining positive deficits. + while let Some((target_asset, target_missing)) = Self::pick_largest_deficit_asset(state) { + self.add_auxiliary_input_for_asset( + pst, + wallet_snapshot, + state, + target_asset, + target_missing, + ) + .await?; + } + + Ok(()) + } + + /// Resolve all inputs required to satisfy output demand, including issuance-derived demand. + /// + /// The algorithm first consumes declared inputs, then greedily appends auxiliary wallet + /// inputs until the equation has no positive deficits. + /// + /// Fee nuance: + /// - Fee demand is injected implicitly as policy-asset demand equal to `fee_target_sat`. + /// - Fee output amount in request params is ignored for funding demand purposes. + /// - Fee output id validation (`"fee"`) is still enforced for duplicates and asset type. + /// + /// Change nuance: + /// - This resolver does not create or place change outputs. + /// - It guarantees only `supply >= demand` per asset after resolution. + /// - Any surplus created by UTXO granularity/overshoot is left for the output stage + /// to materialize as explicit change. + /// + /// # Complexity + /// + /// Let `I` be declared inputs, `U` wallet UTXOs, `A` demanded assets, and `K` auxiliary + /// inputs added. Declared-input selection is `O(I * U * A)`. Auxiliary per-asset funding + /// is bounded by `MAX_BNB_NODES` search plus deterministic fallbacks. + pub(super) async fn resolve_inputs( + &self, + pst: PartiallySignedTransaction, + params: &RuntimeParams, + fee_target_sat: u64, + ) -> Result { + // Phase 1: initialize demand/supply state and load wallet snapshot once. + // We keep all equation state in a dedicated struct so each phase mutates a single object. + let mut pst = pst; + let wallet_snapshot = self.wollet.utxos()?; + let mut state = ResolutionState::default(); + + // Phase 2: build output demand from AssetVariant. + // AssetId contributes directly, while issuance-linked variants are deferred until their + // referenced input is resolved and its issuance entropy is known. + Self::resolve_output_demands( + params, + fee_target_sat, + *self.network.policy_asset(), + &mut state, + )?; + + // Phase 3: resolve declared inputs in order. + // Each input updates the PSET, contributes supply, and may unlock deferred output demand. + self.resolve_declared_inputs(&mut pst, params, &wallet_snapshot, &mut state) + .await?; + + // Safety check: all deferred output demands must have been activated by now. + if !state.deferred_demands.is_empty() { + return Err(WalletAbiError::InvalidRequest( + "unresolved deferred output demands remain after input resolution".to_string(), + )); + } + + // Phase 4: if the declared inputs do not close the equation, add auxiliary wallet inputs + // greedily by largest remaining deficit asset until fully balanced. + self.add_auxiliary_inputs_until_balanced(&mut pst, &wallet_snapshot, &mut state) + .await?; + + Ok(pst) + } + + /// Return the best wallet UTXO candidate under a deterministic, deficit-aware score. + /// + /// Candidates must pass `WalletSourceFilter`, then are ranked lexicographically by: + /// 1. total remaining deficit after simulated addition + /// 2. remaining deficit on candidate asset + /// 3. candidate overshoot/undershoot for that asset + /// 4. `txid`, then `vout` + /// + /// # Complexity + /// + /// With `U` wallet UTXOs and `A` demanded assets, selection is `O(U * A)` time and `O(A)` + /// temporary space per scored candidate simulation. + fn filter_tx_out( + snapshot: &[WalletTxOut], + filter: &WalletSourceFilter, + used_outpoints: &HashSet, + demand_by_asset: &BTreeMap, + supply_by_asset: &BTreeMap, + ) -> Result, WalletAbiError> { + // Candidate ranking is lexicographic and fully deterministic: + // 1) total remaining deficit after adding candidate + // 2) remaining deficit on candidate's asset + // 3) candidate overshoot/undershoot for that asset + // 4) txid + vout tie-break + let mut best: Option<(WalletTxOut, CandidateScore)> = None; + + for candidate in snapshot + .iter() + .filter(|x| matches_wallet_filter(x, filter, used_outpoints)) + { + let score = score_candidate(candidate, demand_by_asset, supply_by_asset)?; + + match &best { + Some((_, best_score)) if score >= *best_score => {} + _ => { + best = Some((candidate.clone(), score)); + } + } + } + + Ok(best.map(|(candidate, _)| candidate)) + } +} diff --git a/crates/wallet-abi/src/runtime/mod.rs b/crates/wallet-abi/src/runtime/mod.rs new file mode 100644 index 0000000..0813224 --- /dev/null +++ b/crates/wallet-abi/src/runtime/mod.rs @@ -0,0 +1,624 @@ +//! Runtime transaction builder/finalizer. +//! +//! High-level flow: +//! 1. Build a fee-targeted PSET (`resolve_inputs` + `balance_out`). +//! 2. Estimate required fee from a finalized+blinded estimation transaction. +//! 3. Iterate fee target to fixed-point convergence (bounded). +//! 4. Build final PSET with converged fee, blind, finalize, and verify proofs. +//! +//! Fee convergence: +//! - initial target: `1 sat` +//! - max iterations: `MAX_FEE_ITERS` +//! - cycle handling: if oscillation is detected, escalate once to max cycle value +//! - failure mode: deterministic `Funding` error when convergence is not reached +//! +//! Formal references: +//! - Bitcoin Core coin selection context: +//! +//! - Murch, *An Evaluation of Coin Selection Strategies*: +//! +//! +pub mod utils; + +mod input_resolution; +mod output_resolution; + +use crate::error::WalletAbiError; +use crate::schema::tx_create::{TransactionInfo, TxCreateRequest, TxCreateResponse}; +use crate::{FinalizerSpec, InputSchema, LockFilter, RuntimeParams, UTXOSource}; +use std::collections::HashMap; + +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; + +use crate::runtime::utils::to_lwk_wollet_network; +use crate::schema::values::{resolve_arguments, resolve_witness}; +use lwk_common::{Bip, Network, Signer}; +use lwk_signer::SwSigner; +use lwk_signer::bip39::rand::thread_rng; +use lwk_simplicity::runner::run_program; +use lwk_simplicity::scripts::{control_block, load_program}; +use lwk_simplicity::signer::get_and_verify_env; +use lwk_wollet::asyncr::EsploraClient; +use lwk_wollet::bitcoin::bip32::{DerivationPath, Xpriv}; +use lwk_wollet::elements::hex::ToHex; +use lwk_wollet::elements::pset::PartiallySignedTransaction; +use lwk_wollet::elements::pset::raw::ProprietaryKey; +use lwk_wollet::elements::{Address, BlockHash, OutPoint, Script, TxOut, TxOutSecrets}; +use lwk_wollet::elements_miniscript::ToPublicKey; +use lwk_wollet::elements_miniscript::psbt::PsbtExt; +use lwk_wollet::hashes::Hash; +use lwk_wollet::secp256k1::{Keypair, XOnlyPublicKey}; +use lwk_wollet::{EC, Wollet, WolletDescriptor}; +use simplicityhl::elements::{Transaction, encode}; +use simplicityhl::tracker::TrackerLogLevel; +use tokio::sync::Mutex; + +/// Maximum number of fee fixed-point iterations before failing. +const MAX_FEE_ITERS: usize = 8; + +pub(crate) fn get_finalizer_spec_key() -> ProprietaryKey { + ProprietaryKey::from_pset_pair(1, b"finalizer-spec".to_vec()) +} + +pub(crate) fn get_secrets_spec_key() -> ProprietaryKey { + ProprietaryKey::from_pset_pair(1, b"secrets-spec".to_vec()) +} + +pub const DEFAULT_FEE_RATE_SAT_VB: f32 = 0.1; + +#[derive(Debug)] +pub struct WalletRuntimeConfig { + pub signer: SwSigner, + pub network: Network, + pub esplora: Arc>, + pub wollet: Wollet, +} + +impl WalletRuntimeConfig { + pub fn build_random( + network: Network, + esplora_url: &str, + wallet_data_dir: impl AsRef, + ) -> Result { + let (signer, _) = SwSigner::random(network.is_mainnet())?; + + Self::from_signer(signer, network, esplora_url, wallet_data_dir) + } + + pub fn from_mnemonic( + mnemonic: &str, + network: Network, + esplora_url: &str, + wallet_data_dir: impl AsRef, + ) -> Result { + let signer = SwSigner::new(mnemonic, network.is_mainnet())?; + + Self::from_signer(signer, network, esplora_url, wallet_data_dir) + } + + fn x_private(&self, bip: Bip) -> Result { + let x_private = self.signer.derive_xprv(&self.get_derivation_path(bip))?; + + Ok(x_private) + } + + pub(crate) fn get_derivation_path(&self, bip: Bip) -> DerivationPath { + let coin_type = if self.network.is_mainnet() { 1776 } else { 1 }; + let path = match bip { + Bip::Bip84 => format!("84h/{coin_type}h/0h"), + Bip::Bip49 => format!("49h/{coin_type}h/0h"), + Bip::Bip87 => format!("87h/{coin_type}h/0h"), + }; + + DerivationPath::from_str(&format!("m/{path}")).expect("static") + } + + pub fn from_signer( + signer: SwSigner, + network: Network, + esplora_url: &str, + wallet_data_dir: impl AsRef, + ) -> Result { + let descriptor = WolletDescriptor::from_str( + &signer + .wpkh_slip77_descriptor() + .map_err(WalletAbiError::InvalidSignerConfig)?, + )?; + + let lwk_wollet_network = to_lwk_wollet_network(network); + + let esplora = Arc::new(Mutex::new(EsploraClient::new( + lwk_wollet_network, + esplora_url, + ))); + let wollet = Wollet::with_fs_persist(lwk_wollet_network, descriptor, wallet_data_dir)?; + + Ok(Self { + signer, + network, + esplora, + wollet, + }) + } + + pub fn get_descriptor(&self) -> Result { + Ok(WolletDescriptor::from_str( + &self + .signer + .wpkh_slip77_descriptor() + .map_err(WalletAbiError::InvalidSignerConfig)?, + )?) + } + + pub fn signer_x_only_public_key(&self) -> Result { + Ok(self.signer_keypair()?.x_only_public_key().0) + } + + pub fn signer_receive_address(&self) -> Result { + let descriptor = self.get_descriptor()?; + + Ok(descriptor.address(0, self.network.address_params())?) + } + + pub(crate) fn signer_keypair(&self) -> Result { + Ok(self.x_private(Bip::Bip87)?.to_keypair(&EC)) + } + + pub async fn sync_wallet(&mut self) -> Result<(), WalletAbiError> { + self.sync_descriptor(self.get_descriptor()?).await + } + + /// TODO: (this is broken for now) Request a full scan using a script descriptor. + pub async fn sync_script_wollet( + &mut self, + script_pubkey: &Script, + ) -> Result<(), WalletAbiError> { + let spk_descriptor = format!(":{}", script_pubkey.to_hex()); + + self.sync_descriptor(WolletDescriptor::from_str(&spk_descriptor)?) + .await + } + + /// TODO: (this is broken for now) Request a full scan while validating an arbitrary descriptor shape. + pub async fn sync_descriptor( + &mut self, + wollet_descriptor: WolletDescriptor, + ) -> Result<(), WalletAbiError> { + // TODO: fix. + // The descriptor is validated by constructing a temporary wollet, while the full scan is + // currently executed against the runtime's persisted primary wallet. + let _wollet = + Wollet::without_persist(to_lwk_wollet_network(self.network), wollet_descriptor)?; + + if let Some(update) = { + let mut inner_esplora = self.esplora.lock().await; + inner_esplora.full_scan(&self.wollet).await? + } { + self.wollet.apply_update(update)?; + } + + Ok(()) + } + + pub async fn fetch_tx_out(&self, outpoint: &OutPoint) -> Result { + let tx = { + let inner_esplora = self.esplora.lock().await; + inner_esplora.get_transaction(outpoint.txid).await? + }; + let tx_out = tx.output.get(outpoint.vout as usize).ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "prevout transaction {} missing vout {}", + outpoint.txid, outpoint.vout + )) + })?; + + Ok(tx_out.clone()) + } + + pub async fn process_request( + &mut self, + request: &TxCreateRequest, + ) -> Result { + request.validate_for_runtime(self.network)?; + + let fee_rate_sat_vb = validate_fee_rate_sat_vb( + request + .params + .fee_rate_sat_vb + .unwrap_or(DEFAULT_FEE_RATE_SAT_VB), + )?; + + self.sync_wallet().await?; + self.pre_sync_inputs(&request.params.inputs).await?; + + let finalized_tx = self.finalize(&request.params, fee_rate_sat_vb).await?; + + let txid = finalized_tx.txid(); + + if request.broadcast { + let published_txid = { + let inner_esplora = self.esplora.lock().await; + inner_esplora.broadcast(&finalized_tx).await? + }; + if txid != published_txid { + return Err(WalletAbiError::InvalidResponse(format!( + "broadcast txid mismatch: locally built txid={txid}, esplora returned txid={published_txid}" + ))); + } + } + + let response = TxCreateResponse::ok( + request, + TransactionInfo { + tx_hex: encode::serialize_hex(&finalized_tx), + txid, + }, + None, + ); + + Ok(response) + } + + /// Build, blind and finalize a transaction with bounded fee fixed-point convergence. + /// + /// The output stage models fee as explicit policy-asset demand, so this method iterates + /// `fee_target_sat` until the estimated fee matches the target. + /// + /// Failure conditions: + /// - convergence not reached within `MAX_FEE_ITERS` + /// - any intermediate funding deficit raised by resolvers + async fn finalize( + &self, + params: &RuntimeParams, + fee_rate: f32, + ) -> Result { + // Bounded fixed-point fee convergence: + // fee_target -> build tx -> estimate fee -> repeat until stable or cap reached. + let mut fee_target_sat = 1u64; + let mut seen_targets = Vec::new(); + let mut escalated_cycle_once = false; + let mut converged_fee_target = None; + + for _ in 0..MAX_FEE_ITERS { + let estimated_fee_sat = self + .estimate_fee_target(params, fee_target_sat, fee_rate) + .await?; + + if estimated_fee_sat == fee_target_sat { + converged_fee_target = Some(estimated_fee_sat); + break; + } + + if let Some(cycle_start) = seen_targets + .iter() + .position(|previous| *previous == estimated_fee_sat) + { + let cycle_max = seen_targets[cycle_start..] + .iter() + .copied() + .chain(std::iter::once(estimated_fee_sat)) + .max() + .unwrap_or(estimated_fee_sat); + if !escalated_cycle_once { + escalated_cycle_once = true; + seen_targets.push(fee_target_sat); + fee_target_sat = cycle_max; + continue; + } + } + + seen_targets.push(fee_target_sat); + fee_target_sat = estimated_fee_sat; + } + + let converged_fee_target = converged_fee_target.ok_or_else(|| { + WalletAbiError::Funding(format!( + "fee convergence failed after {MAX_FEE_ITERS} iterations; last target={} sat, visited=[{}]", + fee_target_sat, + seen_targets + .iter() + .map(u64::to_string) + .collect::>() + .join(",") + )) + })?; + + let mut pst = self.build_transaction(params, converged_fee_target).await?; + let inp_txout_secrets = Self::input_blinding_secrets(&pst)?; + pst.blind_last(&mut thread_rng(), &EC, &inp_txout_secrets)?; + let pst = self.finalize_all_inputs(pst)?; + + let utxos: Vec = pst + .inputs() + .iter() + .filter_map(|x| x.witness_utxo.clone()) + .collect(); + + let tx = pst.extract_tx()?; + + tx.verify_tx_amt_proofs(&EC, &utxos)?; + + Ok(tx) + } + + /// Collect input secrets used by blinding and surjection-proof domain construction. + fn input_blinding_secrets( + pst: &PartiallySignedTransaction, + ) -> Result, WalletAbiError> { + let mut inp_txout_secrets: HashMap = HashMap::new(); + for (input_index, input) in pst.inputs().iter().enumerate() { + let encoded_secrets = + input + .proprietary + .get(&get_secrets_spec_key()) + .ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "missing input blinding secrets metadata for input index {input_index}" + )) + })?; + let secrets: TxOutSecrets = serde_json::from_slice(encoded_secrets)?; + inp_txout_secrets.insert(input_index, secrets); + } + + Ok(inp_txout_secrets) + } + + fn input_finalizer_spec( + pst: &PartiallySignedTransaction, + input_index: usize, + ) -> Result { + let finalizer_payload = pst + .inputs() + .get(input_index) + .ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "missing input index {input_index} while finalizing transaction" + )) + })? + .proprietary + .get(&get_finalizer_spec_key()) + .ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "missing finalizer metadata for input index {input_index}" + )) + })?; + + FinalizerSpec::decode(finalizer_payload) + } + + /// Estimate required fee for a candidate fee target using a finalized+blinded estimation tx. + /// + /// This is used inside the bounded fixed-point loop in `finalize`. + async fn estimate_fee_target( + &self, + params: &RuntimeParams, + fee_target_sat: u64, + fee_rate: f32, + ) -> Result { + let fee_estimation_build = self.build_transaction(params, fee_target_sat).await?; + let mut pst = self.finalize_all_inputs(fee_estimation_build)?; + let inp_txout_secrets = Self::input_blinding_secrets(&pst)?; + pst.blind_last(&mut thread_rng(), &EC, &inp_txout_secrets)?; + + Ok(calculate_fee(pst.extract_tx()?.discount_weight(), fee_rate)) + } + + fn unblind_with_wallet(&self, tx_out: TxOut) -> Result<(TxOut, TxOutSecrets), WalletAbiError> { + let blinding_private_key = self + .signer + .slip77_master_blinding_key()? + .blinding_private_key(&tx_out.script_pubkey); + + let secrets = tx_out.unblind(&EC, blinding_private_key)?; + + Ok((tx_out, secrets)) + } + + /// Build a fee-targeted PSET by running fee-aware input and output resolvers. + async fn build_transaction( + &self, + params: &RuntimeParams, + fee_target_sat: u64, + ) -> Result { + let mut pst = PartiallySignedTransaction::new_v2(); + pst.global.tx_data.fallback_locktime = params.locktime; + + // Input resolution is fee-aware and receives the current fee target. + pst = self.resolve_inputs(pst, params, fee_target_sat).await?; + + pst = self.balance_out(pst, params, fee_target_sat)?; + + Ok(pst) + } + + pub fn finalize_all_inputs( + &self, + mut pst: PartiallySignedTransaction, + ) -> Result { + let utxos: Vec = pst + .inputs() + .iter() + .filter_map(|x| x.witness_utxo.clone()) + .collect(); + + self.signer.sign(&mut pst)?; + + for input_index in 0..pst.inputs().len() { + let finalizer = Self::input_finalizer_spec(&pst, input_index)?; + + match finalizer { + FinalizerSpec::Wallet => { + pst.finalize_inp_mut(&EC, input_index, BlockHash::all_zeros()) + .map_err(|error| { + WalletAbiError::InvalidFinalizationSteps(format!( + "wallet finalization failed for input index {input_index}: {error}" + )) + })?; + } + FinalizerSpec::Simf { + source_simf, + internal_key, + arguments, + witness, + } => { + let arguments = resolve_arguments(&arguments, &pst)?; + + let program = load_program(&source_simf, arguments)?; + + let env = get_and_verify_env( + &pst.extract_tx()?, + &program, + &internal_key.pubkey.to_x_only_pubkey(), + &utxos, + self.network, + input_index, + )?; + + let witness = resolve_witness(&witness, self, &env)?; + + let pruned = run_program(&program, witness, &env, TrackerLogLevel::None)?.0; + + let (simplicity_program_bytes, simplicity_witness_bytes) = + pruned.to_vec_with_witness(); + let cmr = pruned.cmr(); + + pst.inputs_mut()[input_index].final_script_witness = Some(vec![ + simplicity_witness_bytes, + simplicity_program_bytes, + cmr.as_ref().to_vec(), + control_block(cmr, internal_key.pubkey.to_x_only_pubkey()).serialize(), + ]); + } + } + } + + Ok(pst) + } + + /// Pre-sync script-locked wallet filters before input resolution. + /// + /// Current behavior note: + /// this only triggers sync calls; input resolution still reads UTXOs from + /// `self.wollet.utxos()` and does not switch the resolver snapshot source. + async fn pre_sync_inputs(&mut self, inputs: &[InputSchema]) -> Result<(), WalletAbiError> { + for i in inputs { + match &i.utxo_source { + UTXOSource::Wallet { filter } => match &filter.lock { + LockFilter::None => {} + LockFilter::Script { script } => { + self.sync_script_wollet(script).await?; + } + }, + UTXOSource::Provided { .. } => {} + } + } + + Ok(()) + } +} + +fn validate_fee_rate_sat_vb(fee_rate_sat_vb: f32) -> Result { + if !fee_rate_sat_vb.is_finite() || fee_rate_sat_vb < 0.0 { + return Err(WalletAbiError::InvalidRequest(format!( + "fee_rate_sat_vb must be a finite non-negative value in sat/vB, got {fee_rate_sat_vb}" + ))); + } + + Ok(fee_rate_sat_vb) +} + +/// Calculate fee from weight and fee rate (sat/vB). +/// +/// Formula: `fee = ceil(vsize * fee_rate_sat_vb)` +/// where `vsize = ceil(weight / 4)` +/// +/// # Arguments +/// +/// * `weight` - Transaction weight in weight units (WU) +/// * `fee_rate` - Fee rate in satoshis per virtual byte (sat/vB) +/// +/// # Returns +/// +/// The calculated fee in satoshis. +#[must_use] +#[allow( + clippy::cast_precision_loss, + clippy::cast_possible_truncation, + clippy::cast_sign_loss +)] +pub fn calculate_fee(weight: usize, fee_rate: f32) -> u64 { + let vsize = weight.div_ceil(4); + (vsize as f32 * fee_rate).ceil() as u64 +} + +#[cfg(test)] +mod tests { + use super::*; + use lwk_wollet::elements::hashes::Hash; + use lwk_wollet::elements::pset::Input; + use lwk_wollet::elements::{OutPoint, Txid}; + + fn test_outpoint(tag: u8, vout: u32) -> OutPoint { + OutPoint::new( + Txid::from_slice(&[tag; 32]).expect("txid from fixed bytes"), + vout, + ) + } + + #[test] + fn calculate_fee_rounds_vsize_and_fee_up() { + assert_eq!(calculate_fee(4, 1.0), 1); + assert_eq!(calculate_fee(5, 1.0), 2); + assert_eq!(calculate_fee(8, 1.5), 3); + } + + #[test] + fn calculate_fee_allows_zero_fee_rate() { + assert_eq!(calculate_fee(123, 0.0), 0); + } + + #[test] + fn validate_fee_rate_rejects_negative_and_non_finite_values() { + for invalid in [-0.01, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { + let err = validate_fee_rate_sat_vb(invalid).expect_err("invalid fee rate must fail"); + match err { + WalletAbiError::InvalidRequest(message) => { + assert!(message.contains("fee_rate_sat_vb")); + } + other => panic!("unexpected error variant: {other}"), + } + } + } + + #[test] + fn input_blinding_secrets_missing_metadata_returns_error() { + let mut pst = PartiallySignedTransaction::new_v2(); + pst.add_input(Input::from_prevout(test_outpoint(1, 0))); + + let err = + WalletRuntimeConfig::input_blinding_secrets(&pst).expect_err("missing secrets key"); + match err { + WalletAbiError::InvalidRequest(message) => { + assert!(message.contains("missing input blinding secrets metadata")); + } + other => panic!("unexpected error variant: {other}"), + } + } + + #[test] + fn input_finalizer_spec_missing_metadata_returns_error() { + let mut pst = PartiallySignedTransaction::new_v2(); + pst.add_input(Input::from_prevout(test_outpoint(2, 0))); + + let err = WalletRuntimeConfig::input_finalizer_spec(&pst, 0) + .expect_err("missing finalizer key should fail"); + match err { + WalletAbiError::InvalidRequest(message) => { + assert!(message.contains("missing finalizer metadata")); + } + other => panic!("unexpected error variant: {other}"), + } + } +} diff --git a/crates/wallet-abi/src/runtime/output_resolution.rs b/crates/wallet-abi/src/runtime/output_resolution.rs new file mode 100644 index 0000000..a956718 --- /dev/null +++ b/crates/wallet-abi/src/runtime/output_resolution.rs @@ -0,0 +1,590 @@ +//! Output resolution for transaction construction. +//! +//! This module balances the final output set through a deterministic, equation-first flow: +//! 1. Materialize requested outputs exactly as declared in `RuntimeParams`. +//! 2. Normalize or append the fee output to match the target policy-asset fee. +//! 3. Aggregate per-asset supply from resolved inputs plus issuance/reissuance minting. +//! 4. Aggregate per-asset demand from all outputs currently present in the PSET. +//! 5. Compute deficits and residuals per asset. +//! 6. Fail if any deficit remains; otherwise append one change output per residual asset. +//! 7. Assign output blinder indices and assert exact per-asset conservation. +//! +//! # Assumptions +//! +//! Input resolution is expected to be fee-aware (implicit fee demand injected on policy asset). +//! This module still enforces exact conservation independently and hard-fails on any deficit. +//! +//! # Complexity +//! +//! Let: +//! - `I` = number of inputs in the PSET +//! - `O` = number of outputs after fee/change materialization +//! - `A` = number of distinct assets across supply and demand +//! +//! Time complexity is `O(I + O + A)` and auxiliary space complexity is `O(A)`. + +use crate::runtime::input_resolution::{ + add_balance, derive_issuance_entropy, issuance_token_from_entropy_for_unblinded_issuance, +}; +use crate::runtime::{WalletRuntimeConfig, get_finalizer_spec_key}; +use crate::{ + AssetVariant, BlinderVariant, FinalizerSpec, InputIssuance, InputIssuanceKind, LockVariant, + RuntimeParams, WalletAbiError, +}; + +use std::collections::{BTreeMap, BTreeSet}; + +use lwk_wollet::bitcoin::PublicKey; +use lwk_wollet::elements::pset::{Output, PartiallySignedTransaction}; +use lwk_wollet::elements::{Address, AssetId, OutPoint, Script}; + +/// Aggregated amounts keyed by asset id. +type AssetBalances = BTreeMap; + +/// Result of one supply-vs-demand comparison pass. +#[derive(Debug, Default)] +struct BalanceDelta { + /// Positive entries where `demand > supply`. + deficit_by_asset: AssetBalances, + /// Positive entries where `supply > demand`. + residual_by_asset: AssetBalances, +} + +/// Validate one output-linked input index and return it as `usize`. +fn validate_output_input_index( + output_id: &str, + input_index: u32, + input_count: usize, +) -> Result { + let idx = usize::try_from(input_index).map_err(|_| { + WalletAbiError::InvalidRequest(format!( + "output '{output_id}' input_index overflow: {input_index}" + )) + })?; + + if idx >= input_count { + return Err(WalletAbiError::InvalidRequest(format!( + "output '{output_id}' references missing input_index {input_index}" + ))); + } + + Ok(idx) +} + +/// Return indices of inputs finalized by the wallet signer. +/// +/// These indices are later used as valid `blinder_index` anchors for blinded outputs. +fn wallet_input_indices(pst: &PartiallySignedTransaction) -> Result, WalletAbiError> { + let mut indices = Vec::new(); + + for (index, input) in pst.inputs().iter().enumerate() { + let finalizer = FinalizerSpec::decode( + input + .proprietary + .get(&get_finalizer_spec_key()) + .ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "missing finalizer metadata for input index {index}" + )) + })?, + )?; + + if matches!(finalizer, FinalizerSpec::Wallet) { + let index_u32 = u32::try_from(index).map_err(|_| { + WalletAbiError::InvalidRequest(format!( + "wallet input index overflow while balancing outputs: {index}" + )) + })?; + indices.push(index_u32); + } + } + + Ok(indices) +} + +/// Aggregate issuance/reissuance minting supply from declared inputs. +/// +/// For each declared input with issuance metadata: +/// - Add `asset_amount_sat` to the derived issuance asset id. +/// - Add `token_amount_sat` to the derived reissuance token id (if non-zero). +fn aggregate_issuance_supply( + pst: &PartiallySignedTransaction, + params: &RuntimeParams, +) -> Result { + let mut balances = AssetBalances::new(); + + for (input_index, input) in params.inputs.iter().enumerate() { + let Some(issuance) = input.issuance.as_ref() else { + continue; + }; + + let pset_input = pst.inputs().get(input_index).ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "input '{}' at index {input_index} missing from PSET while aggregating issuance supply", + input.id + )) + })?; + + let outpoint = OutPoint::new(pset_input.previous_txid, pset_input.previous_output_index); + let entropy = derive_issuance_entropy(outpoint, issuance); + let issuance_asset = AssetId::from_entropy(entropy); + add_balance(&mut balances, issuance_asset, issuance.asset_amount_sat)?; + + if issuance.token_amount_sat > 0 { + let token_asset = issuance_token_from_entropy_for_unblinded_issuance(entropy); + add_balance(&mut balances, token_asset, issuance.token_amount_sat)?; + } + } + + Ok(balances) +} + +/// Aggregate total per-asset input supply. +/// +/// Supply is the sum of: +/// - Base amounts from all PSET inputs. +/// - Minted issuance/reissuance amounts derived from declared input metadata. +/// +/// Overflow is rejected via checked arithmetic. +fn aggregate_input_supply( + pst: &PartiallySignedTransaction, + params: &RuntimeParams, +) -> Result { + let mut balances = AssetBalances::new(); + + for (input_index, input) in pst.inputs().iter().enumerate() { + let asset = input.asset.ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "input index {input_index} missing explicit asset while aggregating supply" + )) + })?; + let amount_sat = input.amount.ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "input index {input_index} missing explicit amount while aggregating supply" + )) + })?; + add_balance(&mut balances, asset, amount_sat)?; + } + + let issuance_supply = aggregate_issuance_supply(pst, params)?; + for (asset_id, amount_sat) in issuance_supply { + add_balance(&mut balances, asset_id, amount_sat)?; + } + + Ok(balances) +} + +/// Aggregate total per-asset output demand from current PSET outputs. +/// +/// Fee output (policy asset, empty script) is treated as ordinary demand and is not +/// special-cased in this aggregation. +fn aggregate_output_demand( + pst: &PartiallySignedTransaction, +) -> Result { + let mut balances = AssetBalances::new(); + + for (output_index, output) in pst.outputs().iter().enumerate() { + let asset = output.asset.ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "output index {output_index} missing explicit asset while aggregating demand" + )) + })?; + let amount_sat = output.amount.ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "output index {output_index} missing explicit amount while aggregating demand" + )) + })?; + add_balance(&mut balances, asset, amount_sat)?; + } + + Ok(balances) +} + +/// Compute per-asset deficits and residuals from supply/demand maps. +/// +/// Definitions: +/// - deficit: `max(demand - supply, 0)` +/// - residual: `max(supply - demand, 0)` +/// +/// Returns `Funding` error if any deficit is present, including deterministic asset-ordered +/// deficit details and the applied fee target context. +fn compute_balance_delta( + supply_by_asset: &AssetBalances, + demand_by_asset: &AssetBalances, + fee_target_sat: u64, +) -> Result { + let mut delta = BalanceDelta::default(); + let mut all_assets = BTreeSet::new(); + + all_assets.extend(supply_by_asset.keys().copied()); + all_assets.extend(demand_by_asset.keys().copied()); + + for asset_id in all_assets { + let supply_sat = supply_by_asset.get(&asset_id).copied().unwrap_or(0); + let demand_sat = demand_by_asset.get(&asset_id).copied().unwrap_or(0); + + if demand_sat > supply_sat { + delta + .deficit_by_asset + .insert(asset_id, demand_sat - supply_sat); + continue; + } + + if supply_sat > demand_sat { + delta + .residual_by_asset + .insert(asset_id, supply_sat - demand_sat); + } + } + + if !delta.deficit_by_asset.is_empty() { + let details = delta + .deficit_by_asset + .iter() + .map(|(asset_id, missing_sat)| format!("{asset_id}:{missing_sat}")) + .collect::>() + .join(", "); + return Err(WalletAbiError::Funding(format!( + "asset deficits after applying fee target {fee_target_sat}: {details}" + ))); + } + + Ok(delta) +} + +/// Resolve the issuance context required for one issuance-derived output asset. +/// +/// The returned tuple is `(issuance_metadata, prevout)`. +fn resolve_issuance_asset_context( + output_id: &str, + input_index: u32, + pst: &PartiallySignedTransaction, + params: &RuntimeParams, +) -> Result<(InputIssuance, OutPoint), WalletAbiError> { + let idx = validate_output_input_index(output_id, input_index, params.inputs.len())?; + let input = params.inputs.get(idx).ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "output '{output_id}' references missing input_index {input_index}" + )) + })?; + let issuance = input.issuance.as_ref().ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "output '{output_id}' references input {} but input '{}' has no issuance metadata", + input_index, input.id + )) + })?; + + let pset_input = pst.inputs().get(idx).ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "resolved PSET input index {input_index} missing while materializing output '{output_id}'" + )) + })?; + let outpoint = OutPoint { + txid: pset_input.previous_txid, + vout: pset_input.previous_output_index, + }; + Ok((issuance.clone(), outpoint)) +} + +/// Resolve one output `AssetVariant` into a concrete `AssetId`. +/// +/// Issuance-linked variants validate issuance-kind compatibility against the referenced input. +fn resolve_output_asset( + output_id: &str, + variant: &AssetVariant, + pst: &PartiallySignedTransaction, + params: &RuntimeParams, +) -> Result { + match variant { + AssetVariant::AssetId { asset_id } => Ok(*asset_id), + AssetVariant::NewIssuanceAsset { input_index } => { + let (issuance, outpoint) = + resolve_issuance_asset_context(output_id, *input_index, pst, params)?; + if issuance.kind != InputIssuanceKind::New { + return Err(WalletAbiError::InvalidRequest(format!( + "output '{output_id}' new_issuance_asset references non-new issuance input index {input_index}" + ))); + } + Ok(AssetId::from_entropy(derive_issuance_entropy( + outpoint, &issuance, + ))) + } + AssetVariant::NewIssuanceToken { input_index } => { + let (issuance, outpoint) = + resolve_issuance_asset_context(output_id, *input_index, pst, params)?; + if issuance.kind != InputIssuanceKind::New { + return Err(WalletAbiError::InvalidRequest(format!( + "output '{output_id}' new_issuance_token references non-new issuance input index {input_index}" + ))); + } + Ok(issuance_token_from_entropy_for_unblinded_issuance( + derive_issuance_entropy(outpoint, &issuance), + )) + } + AssetVariant::ReIssuanceAsset { input_index } => { + let (issuance, outpoint) = + resolve_issuance_asset_context(output_id, *input_index, pst, params)?; + if issuance.kind != InputIssuanceKind::Reissue { + return Err(WalletAbiError::InvalidRequest(format!( + "output '{output_id}' re_issuance_asset references non-reissue input index {input_index}" + ))); + } + Ok(AssetId::from_entropy(derive_issuance_entropy( + outpoint, &issuance, + ))) + } + } +} + +/// Resolve output locking script from request lock variant. +/// +/// - `Script` uses caller-provided script directly. +/// - `Finalizer::Wallet` uses signer receive script. +/// - `Finalizer::Simf` uses internal taproot script from finalizer metadata. +fn resolve_output_lock_script(lock: &LockVariant, signer_address: &Address) -> Script { + match lock { + LockVariant::Script { script } => script.clone(), + LockVariant::Finalizer { finalizer } => match finalizer.as_ref() { + FinalizerSpec::Wallet => signer_address.script_pubkey(), + FinalizerSpec::Simf { internal_key, .. } => internal_key.address.script_pubkey(), + }, + } +} + +/// Mutate or append fee output so it matches the requested fee target. +/// +/// If `fee_output_index` is `Some(i)`, validates and overwrites that output as explicit fee. +/// Otherwise appends a new explicit fee output. +fn apply_fee_target( + pst: &mut PartiallySignedTransaction, + fee_output_index: Option, + fee_target_sat: u64, + policy_asset: AssetId, +) -> Result<(), WalletAbiError> { + if let Some(index) = fee_output_index { + let fee_output = pst.outputs_mut().get_mut(index).ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "fee output index {index} missing while applying fee target" + )) + })?; + + let fee_asset = fee_output.asset.ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "fee output at index {index} is missing explicit asset metadata" + )) + })?; + if fee_asset != policy_asset { + return Err(WalletAbiError::InvalidRequest(format!( + "fee output must use policy asset {policy_asset}, found {fee_asset}" + ))); + } + + fee_output.script_pubkey = Script::new(); + fee_output.amount = Some(fee_target_sat); + fee_output.asset = Some(policy_asset); + fee_output.blinding_key = None; + fee_output.blinder_index = None; + + return Ok(()); + } + + pst.add_output(Output::new_explicit( + Script::new(), + fee_target_sat, + policy_asset, + None, + )); + + Ok(()) +} + +/// Materialize all user-requested outputs into the PSET in declared order. +/// +/// Returns the index of the output whose id is `"fee"` if present, and fails on duplicates. +fn materialize_requested_outputs( + pst: &mut PartiallySignedTransaction, + params: &RuntimeParams, + signer_address: &Address, +) -> Result, WalletAbiError> { + let mut fee_output_index = None; + + for output in ¶ms.outputs { + let asset_id = resolve_output_asset(&output.id, &output.asset, pst, params)?; + let script = resolve_output_lock_script(&output.lock, signer_address); + + let blinding_key: Option = match output.blinder { + BlinderVariant::Wallet => Some( + signer_address + .blinding_pubkey + .ok_or_else(|| { + WalletAbiError::InvalidSignerConfig( + "signer receive address missing blinding pubkey for wallet output blinder" + .to_string(), + ) + })? + .into(), + ), + BlinderVariant::Provided { pubkey } => Some(pubkey.into()), + BlinderVariant::Explicit => None, + }; + + pst.add_output(Output::new_explicit( + script, + output.amount_sat, + asset_id, + blinding_key, + )); + + if output.id == "fee" { + let inserted_index = pst.outputs().len() - 1; + if fee_output_index.replace(inserted_index).is_some() { + return Err(WalletAbiError::InvalidRequest( + "duplicate output id 'fee' in params.outputs".to_string(), + )); + } + } + } + + Ok(fee_output_index) +} + +/// Append one blinded change output per positive residual asset. +/// +/// Change outputs are deterministic because `residual_by_asset` is a `BTreeMap` and therefore +/// iterated in ascending `AssetId` order. +fn append_global_change_outputs( + pst: &mut PartiallySignedTransaction, + signer_address: &Address, + residual_by_asset: &AssetBalances, +) -> Result<(), WalletAbiError> { + let change_blinding_key = signer_address.blinding_pubkey.ok_or_else(|| { + WalletAbiError::InvalidSignerConfig( + "signer receive address missing blinding pubkey for change output".to_string(), + ) + })?; + + for (asset_id, residual_sat) in residual_by_asset { + if *residual_sat == 0 { + continue; + } + + pst.add_output(Output::new_explicit( + signer_address.script_pubkey(), + *residual_sat, + *asset_id, + Some(change_blinding_key.into()), + )); + } + + Ok(()) +} + +/// Apply output `blinder_index` values using the first wallet-finalized input as source. +/// +/// Unblinded outputs get `None`; blinded outputs require at least one wallet-finalized input. +fn apply_output_blinder_indices( + pst: &mut PartiallySignedTransaction, + wallet_input_indices: &[u32], +) -> Result<(), WalletAbiError> { + let wallet_blinder_index = wallet_input_indices.first().copied(); + for (output_index, output) in pst.outputs_mut().iter_mut().enumerate() { + if output.blinding_key.is_none() { + output.blinder_index = None; + continue; + } + + output.blinder_index = Some(wallet_blinder_index.ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "blinded output at index {output_index} requires at least one wallet-finalized input" + )) + })?); + } + + Ok(()) +} + +/// Final safety check asserting exact per-asset conservation after change materialization. +/// +/// This enforces `supply[a] == demand[a]` for every asset `a`. +/// +/// ```rust,ignore +/// assert_exact_asset_conservation(&pst, ¶ms)?; +/// ``` +fn assert_exact_asset_conservation( + pst: &PartiallySignedTransaction, + params: &RuntimeParams, +) -> Result<(), WalletAbiError> { + let supply_by_asset = aggregate_input_supply(pst, params)?; + let demand_by_asset = aggregate_output_demand(pst)?; + let mut all_assets = BTreeSet::new(); + let mut mismatches = Vec::new(); + + all_assets.extend(supply_by_asset.keys().copied()); + all_assets.extend(demand_by_asset.keys().copied()); + + for asset_id in all_assets { + let supply_sat = supply_by_asset.get(&asset_id).copied().unwrap_or(0); + let demand_sat = demand_by_asset.get(&asset_id).copied().unwrap_or(0); + if supply_sat != demand_sat { + mismatches.push(format!( + "{asset_id}:supply={supply_sat},demand={demand_sat}" + )); + } + } + + if mismatches.is_empty() { + return Ok(()); + } + + Err(WalletAbiError::InvalidRequest(format!( + "asset conservation violated after balancing: {}", + mismatches.join("; ") + ))) +} + +impl WalletRuntimeConfig { + /// Materialize and balance final outputs for an already input-resolved PSET. + /// + /// Pipeline: + /// 1. Materialize outputs from params in order. + /// 2. Apply fee target. + /// 3. Build supply/demand equations. + /// 4. Fail on deficits; otherwise append residual change. + /// 5. Assign blinder indices and assert exact conservation. + /// + /// Safety: + /// - This stage intentionally keeps hard deficit failure even when input selection is + /// fee-aware; it is the final conservation gate before signing. + /// + /// # Complexity + /// + /// With `I` inputs, `O` outputs and `A` distinct assets, runtime is `O(I + O + A)` and + /// additional space is `O(A)`. + pub(super) fn balance_out( + &self, + mut pst: PartiallySignedTransaction, + params: &RuntimeParams, + fee_target_sat: u64, + ) -> Result { + let signer_address = self.signer_receive_address()?; + let wallet_input_indices = wallet_input_indices(&pst)?; + let fee_output_index = materialize_requested_outputs(&mut pst, params, &signer_address)?; + + apply_fee_target( + &mut pst, + fee_output_index, + fee_target_sat, + *self.network.policy_asset(), + )?; + + let supply_by_asset = aggregate_input_supply(&pst, params)?; + let demand_by_asset = aggregate_output_demand(&pst)?; + let delta = compute_balance_delta(&supply_by_asset, &demand_by_asset, fee_target_sat)?; + + append_global_change_outputs(&mut pst, &signer_address, &delta.residual_by_asset)?; + apply_output_blinder_indices(&mut pst, &wallet_input_indices)?; + assert_exact_asset_conservation(&pst, params)?; + + Ok(pst) + } +} diff --git a/crates/wallet-abi/src/runtime/utils.rs b/crates/wallet-abi/src/runtime/utils.rs new file mode 100644 index 0000000..d0e5eef --- /dev/null +++ b/crates/wallet-abi/src/runtime/utils.rs @@ -0,0 +1,10 @@ +use lwk_common::Network; + +#[must_use] +pub fn to_lwk_wollet_network(network: Network) -> lwk_wollet::ElementsNetwork { + match network { + Network::Liquid => lwk_wollet::ElementsNetwork::Liquid, + Network::TestnetLiquid => lwk_wollet::ElementsNetwork::LiquidTestnet, + Network::LocaltestLiquid => lwk_wollet::ElementsNetwork::default_regtest(), + } +} diff --git a/crates/wallet-abi/src/schema/mod.rs b/crates/wallet-abi/src/schema/mod.rs new file mode 100644 index 0000000..dfa1255 --- /dev/null +++ b/crates/wallet-abi/src/schema/mod.rs @@ -0,0 +1,4 @@ +pub mod runtime_params; +pub mod tx_create; +pub mod types; +pub mod values; diff --git a/crates/wallet-abi/src/schema/runtime_params.rs b/crates/wallet-abi/src/schema/runtime_params.rs new file mode 100644 index 0000000..8907208 --- /dev/null +++ b/crates/wallet-abi/src/schema/runtime_params.rs @@ -0,0 +1,275 @@ +//! Runtime transaction parameter schema used by `wallet-create-0.1`. +//! +//! Serialization note: +//! enum variants are serialized in `snake_case` across this schema. + +use crate::WalletAbiError; +use crate::taproot_pubkey_gen::TaprootPubkeyGen; + +use lwk_wollet::elements::LockTime; +use serde::{Deserialize, Serialize}; + +use simplicityhl::elements::secp256k1_zkp::{PublicKey, SecretKey}; +use simplicityhl::elements::{Address, AssetId, OutPoint, Script, Sequence}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RuntimeParams { + #[serde(default)] + pub inputs: Vec, + #[serde(default)] + pub outputs: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fee_rate_sat_vb: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub locktime: Option, +} + +impl RuntimeParams { + pub fn from_request_params(value: &serde_json::Value) -> Result { + serde_json::from_value(value.clone()) + .map_err(|e| WalletAbiError::InvalidRequest(format!("invalid request params: {e}"))) + } + + pub fn to_request_params_value(&self) -> Result { + serde_json::to_value(self).map_err(WalletAbiError::from) + } +} + +impl InputSchema { + #[must_use] + pub const fn with_issuance(mut self, issuance: InputIssuance) -> Self { + self.issuance = Some(issuance); + self + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum AssetFilter { + #[default] + None, + Exact { + asset_id: AssetId, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum AmountFilter { + #[default] + None, + Exact { + satoshi: u64, + }, + Min { + satoshi: u64, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum LockFilter { + #[default] + None, + Script { + script: Script, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct WalletSourceFilter { + pub asset: AssetFilter, + pub amount: AmountFilter, + pub lock: LockFilter, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum UTXOSource { + Wallet { filter: WalletSourceFilter }, + Provided { outpoint: OutPoint }, +} + +impl Default for UTXOSource { + fn default() -> Self { + Self::Wallet { + filter: WalletSourceFilter::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum InputIssuanceKind { + New, + Reissue, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InputIssuance { + pub kind: InputIssuanceKind, + pub asset_amount_sat: u64, + pub token_amount_sat: u64, + pub entropy: [u8; 32], +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum FinalizerSpec { + #[default] + Wallet, + Simf { + source_simf: String, + internal_key: Box, + arguments: Vec, + witness: Vec, + }, +} + +impl FinalizerSpec { + pub fn try_encode(&self) -> Result, WalletAbiError> { + serde_json::to_vec(self).map_err(Into::into) + } + + #[must_use] + pub fn encode(&self) -> Vec { + self.try_encode() + .expect("finalizer spec serialization should not fail") + } + + pub fn decode(bytes: &[u8]) -> Result { + serde_json::from_slice(bytes).map_err(Into::into) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum InputBlinder { + #[default] + Wallet, + Provided { + secret_key: SecretKey, + }, + Explicit, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct InputSchema { + pub id: String, + pub utxo_source: UTXOSource, + pub blinder: InputBlinder, + pub sequence: Sequence, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub issuance: Option, + pub finalizer: FinalizerSpec, +} + +impl InputSchema { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + ..Default::default() + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum LockVariant { + Script { script: Script }, + Finalizer { finalizer: Box }, +} + +impl Default for LockVariant { + fn default() -> Self { + Self::Finalizer { + finalizer: Box::new(FinalizerSpec::default()), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AssetVariant { + AssetId { asset_id: AssetId }, + NewIssuanceAsset { input_index: u32 }, + NewIssuanceToken { input_index: u32 }, + ReIssuanceAsset { input_index: u32 }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum BlinderVariant { + #[default] + Wallet, + Provided { + pubkey: PublicKey, + }, + Explicit, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct OutputSchema { + pub id: String, + pub amount_sat: u64, + pub lock: LockVariant, + pub asset: AssetVariant, + pub blinder: BlinderVariant, +} + +impl OutputSchema { + #[must_use] + pub fn from_address( + id: impl Into, + asset_id: AssetId, + amount_sat: u64, + address: &Address, + ) -> Self { + let blinder = address + .blinding_pubkey + .map_or_else(BlinderVariant::default, |pubkey| BlinderVariant::Provided { + pubkey, + }); + + Self { + id: id.into(), + amount_sat, + lock: LockVariant::Script { + script: address.script_pubkey(), + }, + asset: AssetVariant::AssetId { asset_id }, + blinder, + } + } + + #[must_use] + pub fn from_script( + id: impl Into, + asset_id: AssetId, + amount_sat: u64, + script: Script, + ) -> Self { + Self { + id: id.into(), + amount_sat, + lock: LockVariant::Script { script }, + asset: AssetVariant::AssetId { asset_id }, + blinder: BlinderVariant::Explicit, + } + } + + #[must_use] + pub fn fee_placeholder(policy_asset: AssetId) -> Self { + Self { + id: "fee".to_string(), + amount_sat: 0, + lock: LockVariant::Script { + script: Script::new(), + }, + asset: AssetVariant::AssetId { + asset_id: policy_asset, + }, + blinder: BlinderVariant::Explicit, + } + } +} diff --git a/crates/wallet-abi/src/schema/tx_create.rs b/crates/wallet-abi/src/schema/tx_create.rs new file mode 100644 index 0000000..040223e --- /dev/null +++ b/crates/wallet-abi/src/schema/tx_create.rs @@ -0,0 +1,121 @@ +use serde::{Deserialize, Serialize}; + +use lwk_wollet::elements::Txid; + +use crate::schema::types::ErrorInfo; +use crate::{Network, RuntimeParams, WalletAbiError}; + +pub const TX_CREATE_ABI_VERSION: &str = "wallet-create-0.1"; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TransactionInfo { + pub tx_hex: String, + pub txid: Txid, +} + +pub type TxCreateArtifacts = serde_json::Map; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum Status { + Ok, + Error, +} + +/// Transaction-create request envelope for the `wallet-create-0.1` ABI. +/// +/// `abi_version` and `network` are contract-level fields and should be validated +/// by runtime entrypoints before any wallet/network side effects. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TxCreateRequest { + pub abi_version: String, + pub request_id: String, + pub network: Network, + pub params: RuntimeParams, + pub broadcast: bool, +} + +impl TxCreateRequest { + /// Validate request-level contract fields against the active runtime context. + /// + /// # Errors + /// + /// Returns [`WalletAbiError::InvalidRequest`] when `abi_version` or `network` + /// does not match runtime expectations. + pub fn validate_for_runtime(&self, runtime_network: Network) -> Result<(), WalletAbiError> { + if self.abi_version != TX_CREATE_ABI_VERSION { + return Err(WalletAbiError::InvalidRequest(format!( + "request abi_version mismatch: expected '{TX_CREATE_ABI_VERSION}', got '{}'", + self.abi_version + ))); + } + + if self.network != runtime_network { + return Err(WalletAbiError::InvalidRequest(format!( + "request network mismatch: expected {:?}, got {:?}", + runtime_network, self.network + ))); + } + + Ok(()) + } +} + +/// Transaction-create response envelope for the `wallet-create-0.1` ABI. +/// +/// Runtime currently returns `Result`. +/// This type is still useful for adapters that always emit ABI envelopes. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TxCreateResponse { + pub abi_version: String, + pub request_id: String, + pub network: Network, + pub status: Status, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub transaction: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub artifacts: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl TxCreateResponse { + /// Build a successful ABI response envelope. + #[must_use] + pub fn ok( + request: &TxCreateRequest, + transaction: TransactionInfo, + artifacts: Option, + ) -> Self { + Self { + abi_version: TX_CREATE_ABI_VERSION.to_string(), + request_id: request.request_id.clone(), + network: request.network, + status: Status::Ok, + transaction: Some(transaction), + artifacts, + error: None, + } + } + + /// Build an error ABI response envelope. + /// + /// Intended for transport/adapters that must always return ABI responses + /// instead of bubbling runtime errors. + #[must_use] + pub fn error(request: &TxCreateRequest, code: &str, message: &str) -> Self { + Self { + abi_version: TX_CREATE_ABI_VERSION.to_string(), + request_id: request.request_id.clone(), + network: request.network, + status: Status::Error, + transaction: None, + artifacts: None, + error: Some(ErrorInfo { + code: code.to_string(), + message: message.to_string(), + details: None, + }), + } + } +} diff --git a/crates/wallet-abi/src/schema/types.rs b/crates/wallet-abi/src/schema/types.rs new file mode 100644 index 0000000..14b499c --- /dev/null +++ b/crates/wallet-abi/src/schema/types.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ErrorInfo { + pub code: String, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub details: Option, +} diff --git a/crates/wallet-abi/src/schema/values.rs b/crates/wallet-abi/src/schema/values.rs new file mode 100644 index 0000000..07bb6fd --- /dev/null +++ b/crates/wallet-abi/src/schema/values.rs @@ -0,0 +1,176 @@ +use crate::WalletAbiError; +use crate::runtime::WalletRuntimeConfig; + +use std::collections::HashMap; +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use lwk_wollet::elements::Transaction; +use lwk_wollet::elements::pset::PartiallySignedTransaction; +use lwk_wollet::hashes::Hash; +use lwk_wollet::secp256k1::{Message, XOnlyPublicKey}; + +use simplicityhl::num::U256; +use simplicityhl::parse::ParseFromStr; +use simplicityhl::simplicity::jet::elements::ElementsEnv; +use simplicityhl::str::WitnessName; +use simplicityhl::value::{UIntValue, ValueConstructible}; +use simplicityhl::{Arguments, Value, WitnessValues}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeSimfValue { + NewIssuanceAsset { input_index: u32 }, + NewIssuanceToken { input_index: u32 }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SimfArguments { + pub resolved: simplicityhl::Arguments, + pub runtime_arguments: HashMap, +} + +impl SimfArguments { + #[must_use] + pub fn new(static_arguments: simplicityhl::Arguments) -> Self { + Self { + resolved: static_arguments, + runtime_arguments: HashMap::new(), + } + } + + pub fn append_runtime_simf_value(&mut self, name: &str, runtime_simf_value: RuntimeSimfValue) { + self.runtime_arguments + .insert(name.to_string(), runtime_simf_value); + } +} + +/// Convert compiled Simplicity arguments into bytes. +pub fn serialize_arguments(arguments: &SimfArguments) -> Result, WalletAbiError> { + Ok(serde_json::to_vec(arguments)?) +} + +fn parse_witness_name(name: &str, source: &str) -> Result { + WitnessName::parse_from_str(name).map_err(|error| { + WalletAbiError::InvalidRequest(format!( + "invalid Simplicity witness name '{name}' in {source}: {error}" + )) + }) +} + +/// Deserialize and resolve compiled Simplicity arguments from bytes. +pub fn resolve_arguments( + bytes: &[u8], + pst: &PartiallySignedTransaction, +) -> Result { + let simf_arguments: SimfArguments = serde_json::from_slice(bytes)?; + + let mut final_arguments: HashMap = HashMap::::new(); + + for (name, value) in simf_arguments.runtime_arguments { + match value { + RuntimeSimfValue::NewIssuanceAsset { input_index } => { + let input = pst + .inputs() + .get(input_index as usize) + .ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "runtime Simplicity argument '{name}' references missing input_index {input_index} (pset inputs: {})", + pst.inputs().len() + )) + })?; + let (asset, _) = input.issuance_ids(); + let witness_name = parse_witness_name(&name, "runtime argument map")?; + + final_arguments.insert( + witness_name, + Value::from(UIntValue::U256(U256::from_byte_array(asset.into_inner().0))), + ); + } + RuntimeSimfValue::NewIssuanceToken { input_index } => { + let input = pst + .inputs() + .get(input_index as usize) + .ok_or_else(|| { + WalletAbiError::InvalidRequest(format!( + "runtime Simplicity argument '{name}' references missing input_index {input_index} (pset inputs: {})", + pst.inputs().len() + )) + })?; + let (_, token) = input.issuance_ids(); + let witness_name = parse_witness_name(&name, "runtime argument map")?; + + final_arguments.insert( + witness_name, + Value::from(UIntValue::U256(U256::from_byte_array(token.into_inner().0))), + ); + } + } + } + + for static_arg in simf_arguments.resolved.iter() { + final_arguments.insert(static_arg.0.clone(), static_arg.1.clone()); + } + + Ok(Arguments::from(final_arguments)) +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeSimfWitness { + SigHashAll { + name: String, + public_key: XOnlyPublicKey, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SimfWitness { + pub resolved: WitnessValues, + pub runtime_arguments: Vec, +} + +/// Convert compiled Simplicity witness into bytes. +pub fn serialize_witness(witness: &SimfWitness) -> Result, WalletAbiError> { + Ok(serde_json::to_vec(witness)?) +} + +/// Deserialize and resolve compiled Simplicity witness values from bytes. +pub fn resolve_witness( + bytes: &[u8], + runtime: &WalletRuntimeConfig, + env: &ElementsEnv>, +) -> Result { + let simf_arguments: SimfWitness = serde_json::from_slice(bytes)?; + + let mut final_witness: HashMap = HashMap::::new(); + + let keypair = runtime.signer_keypair()?; + let sighash_all = Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); + + for value in simf_arguments.runtime_arguments { + match value { + RuntimeSimfWitness::SigHashAll { name, public_key } => { + let signer_public_key = keypair.x_only_public_key().0; + if signer_public_key != public_key { + return Err(WalletAbiError::InvalidRequest(format!( + "sighash_all witness '{name}' public key mismatch: expected {public_key}, runtime signer is {signer_public_key}" + ))); + } + let witness_name = parse_witness_name(&name, "runtime witness map")?; + + final_witness.insert( + witness_name, + Value::byte_array(keypair.sign_schnorr(sighash_all).serialize()), + ); + } + } + } + + for static_arg in simf_arguments.resolved.iter() { + final_witness.insert(static_arg.0.clone(), static_arg.1.clone()); + } + + Ok(WitnessValues::from(final_witness)) +} diff --git a/crates/wallet-abi/src/scripts.rs b/crates/wallet-abi/src/scripts.rs new file mode 100644 index 0000000..593f7af --- /dev/null +++ b/crates/wallet-abi/src/scripts.rs @@ -0,0 +1,24 @@ +//! Script and Taproot helpers shared by contract crates. + +use sha2::{Digest, Sha256}; + +pub use lwk_simplicity::scripts::{ + control_block, create_p2tr_address, load_program, simplicity_leaf_version, tap_data_hash, +}; +use simplicityhl::elements::{AssetId, ContractHash, OutPoint, Script}; +use simplicityhl::simplicity::hashes::{Hash, sha256}; + +/// SHA256 of a scriptPubKey byte payload. +#[must_use] +pub fn hash_script(script: &Script) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(script.as_bytes()); + hasher.finalize().into() +} + +/// Compute issuance entropy for a new issuance from outpoint + contract hash entropy. +#[must_use] +pub fn get_new_asset_entropy(outpoint: &OutPoint, entropy: [u8; 32]) -> sha256::Midstate { + let contract_hash = ContractHash::from_byte_array(entropy); + AssetId::generate_asset_entropy(*outpoint, contract_hash) +} diff --git a/crates/wallet-abi/src/simplicity/mod.rs b/crates/wallet-abi/src/simplicity/mod.rs new file mode 100644 index 0000000..5083e09 --- /dev/null +++ b/crates/wallet-abi/src/simplicity/mod.rs @@ -0,0 +1 @@ +pub mod p2pk; diff --git a/crates/wallet-abi/src/simplicity/p2pk.rs b/crates/wallet-abi/src/simplicity/p2pk.rs new file mode 100644 index 0000000..854a45d --- /dev/null +++ b/crates/wallet-abi/src/simplicity/p2pk.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use lwk_common::Network; +use lwk_simplicity::runner::run_program; +use lwk_simplicity::scripts::{create_p2tr_address, load_program}; + +use simplicityhl::elements::secp256k1_zkp::schnorr::Signature; +use simplicityhl::num::U256; +use simplicityhl::simplicity::RedeemNode; +use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; +use simplicityhl::simplicity::jet::Elements; +use simplicityhl::simplicity::jet::elements::ElementsEnv; +use simplicityhl::str::WitnessName; +use simplicityhl::tracker::TrackerLogLevel; +use simplicityhl::value::ValueConstructible; +use simplicityhl::{CompiledProgram, Value, WitnessValues, elements}; + +use crate::ProgramError; + +/// Embedded Simplicity source for a basic P2PK program. +pub const P2PK_SOURCE: &str = include_str!("../source_simf/p2pk.simf"); + +/// Construct a P2TR address for the embedded P2PK program and the provided public key. +pub fn get_p2pk_address( + x_only_public_key: &XOnlyPublicKey, + network: Network, +) -> Result { + Ok(create_p2tr_address( + get_p2pk_program(x_only_public_key)?.commit().cmr(), + x_only_public_key, + network.address_params(), + )) +} + +/// Compile the embedded P2PK program with the given X-only public key as argument. +pub fn get_p2pk_program( + account_public_key: &XOnlyPublicKey, +) -> Result { + let arguments = simplicityhl::Arguments::from(HashMap::from([( + WitnessName::from_str_unchecked("PUBLIC_KEY"), + Value::u256(U256::from_byte_array(account_public_key.serialize())), + )])); + + load_program(P2PK_SOURCE, arguments) +} + +/// Execute the compiled P2PK program against the provided env, producing a pruned redeem node. +pub fn execute_p2pk_program( + compiled_program: &CompiledProgram, + schnorr_signature: &Signature, + env: &ElementsEnv>, + runner_log_level: TrackerLogLevel, +) -> Result>, ProgramError> { + let witness_values = WitnessValues::from(HashMap::from([( + WitnessName::from_str_unchecked("SIGNATURE"), + Value::byte_array(schnorr_signature.serialize()), + )])); + + Ok(run_program(compiled_program, witness_values, env, runner_log_level)?.0) +} diff --git a/crates/simplicityhl-core/src/source_simf/p2pk.simf b/crates/wallet-abi/src/source_simf/p2pk.simf similarity index 100% rename from crates/simplicityhl-core/src/source_simf/p2pk.simf rename to crates/wallet-abi/src/source_simf/p2pk.simf diff --git a/crates/wallet-abi/src/taproot_pubkey_gen.rs b/crates/wallet-abi/src/taproot_pubkey_gen.rs new file mode 100644 index 0000000..698b578 --- /dev/null +++ b/crates/wallet-abi/src/taproot_pubkey_gen.rs @@ -0,0 +1,448 @@ +//! Ephemeral Taproot pubkey and address generator for argument-bound programs. +//! +//! Produces a deterministic X-only public key and corresponding address without +//! holding a private key, based on a random seed. The resulting trio +//! `::` can be printed and +//! later verified with the same arguments to prevent mismatches. +//! +//! Identity field formats: +//! - `seed_hex`: 32-byte random seed (legacy/current default) +//! - `ext-`: externally supplied 32-byte x-only key handle + +use sha2::{Digest, Sha256}; +use std::fmt::Display; +use std::str::FromStr; + +use lwk_common::Network; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use simplicityhl::elements::{Address, schnorr::XOnlyPublicKey}; + +use crate::ProgramError; +use simplicityhl::simplicity::ToXOnlyPubkey; +use simplicityhl::simplicity::bitcoin::PublicKey; +use simplicityhl::simplicity::bitcoin::key::Parity; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +enum TaprootIdentity { + Seed(Vec), + ExternalXOnly(XOnlyPublicKey), +} + +/// Errors from taproot pubkey generation and verification. +#[derive(Debug, thiserror::Error)] +pub enum TaprootPubkeyGenError { + #[error("Invalid pubkey recovered: expected {expected}, got {actual}")] + InvalidPubkey { expected: String, actual: String }, + + #[error("Invalid address recovered: expected {expected}, got {actual}")] + InvalidAddress { expected: String, actual: String }, + + #[error( + "Invalid taproot pubkey gen string: expected 3 parts separated by ':', got {parts_count}" + )] + InvalidFormat { parts_count: usize }, + + #[error("Failed to decode seed hex: {0}")] + SeedHexDecode(#[from] hex::FromHexError), + + #[error("Invalid seed length: expected 32 bytes, got {actual}")] + InvalidSeedLength { actual: usize }, + + #[error("Failed to parse public key: {0}")] + PublicKeyParse(#[from] simplicityhl::simplicity::bitcoin::key::ParsePublicKeyError), + + #[error("Failed to parse address: {0}")] + AddressParse(#[from] simplicityhl::elements::address::AddressError), + + #[error("Failed to create X-only public key from bytes: {0}")] + XOnlyPublicKey(#[from] simplicityhl::simplicity::bitcoin::secp256k1::Error), + + #[error("Invalid external x-only key: {0}")] + InvalidExternalKey(String), + + #[error("Failed to generate address: {0}")] + AddressGeneration(#[from] ProgramError), +} + +/// Container for the seed, public key and derived address. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TaprootPubkeyGen { + identity: TaprootIdentity, + pub pubkey: PublicKey, + pub address: Address, +} + +impl TaprootPubkeyGen { + /// Build from current process randomness and compute the address given `arguments`. + /// + /// Kept as `from` for compatibility with existing callers. + /// + /// # Errors + /// Returns error if address generation fails. + pub fn from( + arguments: &A, + network: Network, + get_address: &impl Fn(&XOnlyPublicKey, &A, Network) -> Result, + ) -> Result { + let (not_existent_public_key, seed) = generate_public_key_without_private(); + + let address = get_address( + ¬_existent_public_key.to_x_only_pubkey(), + arguments, + network, + )?; + + Ok(Self { + identity: TaprootIdentity::Seed(seed), + pubkey: not_existent_public_key, + address, + }) + } + + /// Parse from string and verify that pubkey and address match the provided arguments. + /// + /// # Errors + /// Returns error if parsing fails or verification doesn't match. + pub fn build_from_str( + s: &str, + arguments: &A, + network: Network, + get_address: &impl Fn(&XOnlyPublicKey, &A, Network) -> Result, + ) -> Result { + let taproot_pubkey_gen = Self::parse_from_str(s)?; + + taproot_pubkey_gen.verify(arguments, network, get_address)?; + + Ok(taproot_pubkey_gen) + } + + /// Verify that the stored pubkey and address are consistent with `arguments`. + /// + /// # Errors + /// Returns error if pubkey or address doesn't match the expected values. + pub fn verify( + &self, + arguments: &A, + network: Network, + get_address: &impl Fn(&XOnlyPublicKey, &A, Network) -> Result, + ) -> Result<(), TaprootPubkeyGenError> { + match &self.identity { + TaprootIdentity::Seed(seed) => { + let rand_seed = seed.as_slice(); + + let mut hasher = Sha256::new(); + sha2::digest::Update::update(&mut hasher, rand_seed); + sha2::digest::Update::update(&mut hasher, rand_seed); + sha2::digest::Update::update(&mut hasher, rand_seed); + let potential_pubkey: [u8; 32] = hasher.finalize().into(); + + let expected_pubkey: PublicKey = XOnlyPublicKey::from_slice(&potential_pubkey)? + .public_key(Parity::Even) + .into(); + + if expected_pubkey != self.pubkey { + return Err(TaprootPubkeyGenError::InvalidPubkey { + expected: expected_pubkey.to_string(), + actual: self.pubkey.to_string(), + }); + } + } + TaprootIdentity::ExternalXOnly(xonly) => { + if &self.pubkey.to_x_only_pubkey() != xonly { + let expected_pubkey: PublicKey = xonly.public_key(Parity::Even).into(); + return Err(TaprootPubkeyGenError::InvalidPubkey { + expected: expected_pubkey.to_string(), + actual: self.pubkey.to_string(), + }); + } + } + } + + let expected_address = get_address(&self.pubkey.to_x_only_pubkey(), arguments, network)?; + if self.address != expected_address { + return Err(TaprootPubkeyGenError::InvalidAddress { + expected: expected_address.to_string(), + actual: self.address.to_string(), + }); + } + + Ok(()) + } + + /// Get the X-only public key. + #[must_use] + pub fn get_x_only_pubkey(&self) -> XOnlyPublicKey { + self.pubkey.to_x_only_pubkey() + } + + pub fn to_json(&self) -> serde_json::Result { + serde_json::to_value(self) + } + + /// Parse `::
` representation. + fn parse_from_str(s: &str) -> Result { + let parts = s.split(':').collect::>(); + + if parts.len() != 3 { + return Err(TaprootPubkeyGenError::InvalidFormat { + parts_count: parts.len(), + }); + } + + let identity = if let Some(xonly_hex) = parts[0].strip_prefix("ext-") { + let xonly_bytes = hex::decode(xonly_hex) + .map_err(|e| TaprootPubkeyGenError::InvalidExternalKey(e.to_string()))?; + if xonly_bytes.len() != 32 { + return Err(TaprootPubkeyGenError::InvalidExternalKey(format!( + "expected 32-byte x-only pubkey, got {} bytes", + xonly_bytes.len() + ))); + } + TaprootIdentity::ExternalXOnly( + XOnlyPublicKey::from_slice(&xonly_bytes) + .map_err(|e| TaprootPubkeyGenError::InvalidExternalKey(e.to_string()))?, + ) + } else { + let seed = hex::decode(parts[0])?; + if seed.len() != 32 { + return Err(TaprootPubkeyGenError::InvalidSeedLength { actual: seed.len() }); + } + TaprootIdentity::Seed(seed) + }; + + Ok(Self { + identity, + pubkey: PublicKey::from_str(parts[1])?, + address: Address::from_str(parts[2])?, + }) + } +} + +impl Display for TaprootPubkeyGen { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let id = match &self.identity { + TaprootIdentity::Seed(seed) => hex::encode(seed), + TaprootIdentity::ExternalXOnly(xonly) => { + format!("ext-{}", hex::encode(xonly.serialize())) + } + }; + write!(f, "{}:{}:{}", id, self.pubkey, self.address) + } +} + +/// Try to deterministically map a random seed into a valid X-only pubkey. +fn try_generate_public_key_without_private() -> Result<(PublicKey, Vec), TaprootPubkeyGenError> +{ + let rand_seed: [u8; 32] = get_random_seed(); + + let mut hasher = Sha256::new(); + sha2::digest::Update::update(&mut hasher, &rand_seed); + sha2::digest::Update::update(&mut hasher, &rand_seed); + sha2::digest::Update::update(&mut hasher, &rand_seed); + let potential_pubkey: [u8; 32] = hasher.finalize().into(); + + Ok(( + XOnlyPublicKey::from_slice(&potential_pubkey)? + .public_key(Parity::Even) + .into(), + rand_seed.to_vec(), + )) +} + +/// Generate a valid ephemeral public key and its seed; repeats until valid. +#[must_use] +pub fn generate_public_key_without_private() -> (PublicKey, Vec) { + let not_existent_public_key; + loop { + if let Ok(public_key) = try_generate_public_key_without_private() { + not_existent_public_key = public_key; + break; + } + } + + not_existent_public_key +} + +/// System-random 32-byte seed. +/// +/// # Panics +/// Panics if the system random number generator fails. +#[must_use] +pub fn get_random_seed() -> [u8; 32] { + ring::rand::generate(&ring::rand::SystemRandom::new()) + .unwrap() + .expose() +} + +#[cfg(test)] +mod tests { + use super::*; + use simplicityhl::elements::schnorr::Keypair; + use simplicityhl::elements::secp256k1_zkp::{SECP256K1, SecretKey}; + + struct TestArgs; + + fn address_for_key( + xonly: &XOnlyPublicKey, + _args: &TestArgs, + network: Network, + ) -> Result { + crate::get_p2pk_address(xonly, network) + } + + fn deterministic_test_keypair(byte: u8) -> (XOnlyPublicKey, PublicKey) { + let secret = SecretKey::from_slice(&[byte; 32]).expect("secret"); + let keypair = Keypair::from_secret_key(SECP256K1, &secret); + let xonly = keypair.x_only_public_key().0; + let pubkey: PublicKey = xonly.public_key(Parity::Even).into(); + (xonly, pubkey) + } + + #[test] + fn build_from_str_supports_legacy_seed_handle() { + let args = TestArgs; + let generated = TaprootPubkeyGen::from(&args, Network::TestnetLiquid, &address_for_key) + .expect("generated handle"); + let encoded = generated.to_string(); + let decoded = TaprootPubkeyGen::build_from_str( + &encoded, + &args, + Network::TestnetLiquid, + &address_for_key, + ) + .expect("decoded handle"); + assert_eq!(decoded.pubkey, generated.pubkey); + assert_eq!(decoded.address, generated.address); + } + + #[test] + fn build_from_str_supports_external_xonly_handle() { + let args = TestArgs; + let (xonly, pubkey) = deterministic_test_keypair(0x12); + let address = address_for_key(&xonly, &args, Network::TestnetLiquid).expect("address"); + + let encoded = format!( + "ext-{}:{}:{}", + hex::encode(xonly.serialize()), + pubkey, + address + ); + let decoded = TaprootPubkeyGen::build_from_str( + &encoded, + &args, + Network::TestnetLiquid, + &address_for_key, + ) + .expect("decoded handle"); + + assert_eq!(decoded.pubkey, pubkey); + assert_eq!(decoded.address, address); + } + + #[test] + fn build_from_str_rejects_non_32_byte_seed_handle() { + let args = TestArgs; + let generated = TaprootPubkeyGen::from(&args, Network::TestnetLiquid, &address_for_key) + .expect("generated handle"); + let encoded = generated.to_string(); + let parts = encoded.split(':').collect::>(); + + let invalid_seed_handle = format!("{}:{}:{}", hex::encode([0x11; 31]), parts[1], parts[2]); + let err = TaprootPubkeyGen::build_from_str( + &invalid_seed_handle, + &args, + Network::TestnetLiquid, + &address_for_key, + ) + .expect_err("must reject non-32-byte seed"); + + assert!(matches!( + err, + TaprootPubkeyGenError::InvalidSeedLength { actual } if actual == 31 + )); + } + + #[test] + fn build_from_str_rejects_external_identity_with_wrong_length() { + let args = TestArgs; + let (xonly, pubkey) = deterministic_test_keypair(0x22); + let address = address_for_key(&xonly, &args, Network::TestnetLiquid).expect("address"); + + let encoded = format!("ext-{}:{}:{}", hex::encode([0xAB; 31]), pubkey, address); + let err = TaprootPubkeyGen::build_from_str( + &encoded, + &args, + Network::TestnetLiquid, + &address_for_key, + ) + .expect_err("must reject external x-only keys with wrong size"); + + assert!(matches!(err, TaprootPubkeyGenError::InvalidExternalKey(_))); + } + + #[test] + fn build_from_str_rejects_external_identity_with_invalid_hex() { + let args = TestArgs; + let (xonly, pubkey) = deterministic_test_keypair(0x23); + let address = address_for_key(&xonly, &args, Network::TestnetLiquid).expect("address"); + + let encoded = format!("ext-not_hex:{pubkey}:{address}"); + let err = TaprootPubkeyGen::build_from_str( + &encoded, + &args, + Network::TestnetLiquid, + &address_for_key, + ) + .expect_err("must reject non-hex external x-only prefix"); + + assert!(matches!(err, TaprootPubkeyGenError::InvalidExternalKey(_))); + } + + #[test] + fn build_from_str_rejects_mismatched_pubkey_for_seed_identity() { + let args = TestArgs; + let generated = TaprootPubkeyGen::from(&args, Network::TestnetLiquid, &address_for_key) + .expect("generated handle"); + let encoded = generated.to_string(); + let parts = encoded.split(':').collect::>(); + let wrong_pubkey = deterministic_test_keypair(0x44).1; + + let tampered = format!("{}:{}:{}", parts[0], wrong_pubkey, parts[2]); + let err = TaprootPubkeyGen::build_from_str( + &tampered, + &args, + Network::TestnetLiquid, + &address_for_key, + ) + .expect_err("must reject mismatched seed-derived public key"); + + assert!(matches!(err, TaprootPubkeyGenError::InvalidPubkey { .. })); + } + + #[test] + fn build_from_str_rejects_mismatched_address() { + let args = TestArgs; + let generated = TaprootPubkeyGen::from(&args, Network::TestnetLiquid, &address_for_key) + .expect("generated handle"); + let encoded = generated.to_string(); + let parts = encoded.split(':').collect::>(); + let wrong_network_address = address_for_key( + &generated.get_x_only_pubkey(), + &args, + Network::LocaltestLiquid, + ) + .expect("address"); + + let tampered = format!("{}:{}:{}", parts[0], parts[1], wrong_network_address); + let err = TaprootPubkeyGen::build_from_str( + &tampered, + &args, + Network::TestnetLiquid, + &address_for_key, + ) + .expect_err("must reject mismatched address"); + + assert!(matches!(err, TaprootPubkeyGenError::InvalidAddress { .. })); + } +} diff --git a/crates/simplicityhl-core/src/tx_inclusion.rs b/crates/wallet-abi/src/tx_inclusion.rs similarity index 81% rename from crates/simplicityhl-core/src/tx_inclusion.rs rename to crates/wallet-abi/src/tx_inclusion.rs index 5243500..ea3951d 100644 --- a/crates/simplicityhl-core/src/tx_inclusion.rs +++ b/crates/wallet-abi/src/tx_inclusion.rs @@ -115,7 +115,6 @@ fn compute_merkle_root_from_branch( #[cfg(test)] mod test { - use super::*; /// Taken from rust-elements @@ -159,14 +158,34 @@ mod test { }); ); - // Unfortunately, `hex_deserialize` macro aforehead returns error trying deserialize - // blocks from elements-cli regtest, so this block, taken from `elements::Block::block`, is - // the only test case I have found so far. - const BLOCK_STR: &str = include_str!("./assets/test-tx-incl-block.hex"); + const SIMPLE_BLOCK: &str = "\ + 00000020a66e4a4baff69735267346d12e59e8a0da848b593813554deb16a6f3\ + 6cd035e9aab0e2451724598471dd4e45f0dca40ca5f4ac62e61957e50925af08\ + 59891fcc8842805b020000000151000102000000010100000000000000000000\ + 00000000000000000000000000000000000000000000ffffffff03520101ffff\ + ffff0201230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a\ + 0d5de1b201000000000000000000016a01230f4f5d4b7c6fa845806ee4f67713\ + 459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000000000266a24aa21\ + a9ed94f15ed3a62165e4a0b99699cc28b48e19cb5bc1b1f47155db62d63f1e04\ + 7d45000000000000012000000000000000000000000000000000000000000000\ + 000000000000000000000000000000\ + "; + + fn fixture_block() -> Block { + let mut block: Block = hex_deserialize!(SIMPLE_BLOCK); + let coinbase = block.txdata[0].clone(); + block.txdata = vec![coinbase.clone(), coinbase.clone(), coinbase]; + + let tx = block.txdata[0].txid(); + let branch = build_merkle_branch(0, &block); + block.header.merkle_root = compute_merkle_root_from_branch(&tx, 0, &branch); + + block + } #[test] fn test_merkle_branch_construction() { - let block: Block = hex_deserialize!(BLOCK_STR); + let block = fixture_block(); assert_eq!(block.txdata.len(), 3); diff --git a/crates/simplicityhl-core/src/assets/test-tx-incl-block.hex b/crates/wallet-abi/tests/data/test-tx-incl-block.hex similarity index 100% rename from crates/simplicityhl-core/src/assets/test-tx-incl-block.hex rename to crates/wallet-abi/tests/data/test-tx-incl-block.hex diff --git a/crates/contracts/src/sdk/issuance_validation/test_data/transaction.hex b/crates/wallet-abi/tests/data/tx_with_issuance_token.hex similarity index 100% rename from crates/contracts/src/sdk/issuance_validation/test_data/transaction.hex rename to crates/wallet-abi/tests/data/tx_with_issuance_token.hex