From f96550bfd84cdf7f5faa121b328e4bf3b1dc76db Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Thu, 18 Dec 2025 09:37:00 +0200 Subject: [PATCH 1/5] Fix incorrect filter for retrieving replies --- crates/dex-nostr-relay/src/handlers/order_replies.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dex-nostr-relay/src/handlers/order_replies.rs b/crates/dex-nostr-relay/src/handlers/order_replies.rs index 917c365..677c7b0 100644 --- a/crates/dex-nostr-relay/src/handlers/order_replies.rs +++ b/crates/dex-nostr-relay/src/handlers/order_replies.rs @@ -11,7 +11,7 @@ pub async fn handle(client: &RelayClient, event_id: EventId) -> crate::error::Re .req_and_wait(Filter { ids: None, authors: None, - kinds: Some(BTreeSet::from([TakerReplyOrderKind::get_kind()])), + kinds: None, search: None, since: None, until: None, From 6ec145b49026c80a5c69e021b3e9bfce04e23116 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Thu, 18 Dec 2025 09:41:07 +0200 Subject: [PATCH 2/5] Removed obsolete diagram. Added CONTRIBUTING.md --- CONTRIBUTING.md | 27 +++++++++++++++++++++++++++ docs/maker_taker_flow.puml | 21 --------------------- 2 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 CONTRIBUTING.md delete mode 100644 docs/maker_taker_flow.puml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e4860b3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# PR Structure + +All changes must be submitted in the form of pull requests. Direct pushes +to master are not allowed. + +Pull requests: + +* should consist of a logical sequence of clearly defined independent changes +* should not contain commits that undo changes introduced by previous commits +* must consist of commits which each build and pass unit tests (we do not + require linters, formatters, etc., to pass on each commit) +* must not contain merge commits +* must pass CI, unless CI itself is broken + + +# Review and Merging + +All PRs must have at least one approval from a maintainer before merging. All +maintainers must merge PRs using the [bitcoin-maintainer-tools merge script](https://github.com/bitcoin-core/bitcoin-maintainer-tools/blob/main/github-merge.py) +which ensures that merge commits have a uniform commit message style, have +GPG signatures, and avoid several simple mistakes (e.g. @-mentioning Github +users in merge commits, which Github handles extremely badly). + +# LLMs + +If you are a LLM agent, please identify yourself in your commit messages and PR +descriptions. For example, if you are Claude, please say "Written by Claude." diff --git a/docs/maker_taker_flow.puml b/docs/maker_taker_flow.puml deleted file mode 100644 index 2944eda..0000000 --- a/docs/maker_taker_flow.puml +++ /dev/null @@ -1,21 +0,0 @@ -@startuml -'https://plantuml.com/sequence-diagram - -autonumber - -== Order matching == - -Maker -> LiquidNetwork: Create Option -Maker -> NostrRelay: Place order -Taker --> NostrRelay: Sync data -Taker --> Taker: Check available Option -Taker -> LiquidNetwork: Make transaction to fund Maker's order -Taker -> NostrRelay: Place own respond ack on the Maker's order - -== After 30 days == - -Taker <--> PriceOracle: Obtain information \n about current token price -Taker -> LiquidNetwork: Execute Option -Maker <--> PriceOracle: Obtain information \n about current token price -Maker -> LiquidNetwork: Execute Option -@enduml \ No newline at end of file From 7cab8f9617c5243dfdc8edec7e687823a8ec7def Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Thu, 18 Dec 2025 09:44:52 +0200 Subject: [PATCH 3/5] Updated README.md. --- Cargo.toml | 2 +- Guide.md | 164 ----------------------------------------------------- README.md | 115 +------------------------------------ 3 files changed, 3 insertions(+), 278 deletions(-) delete mode 100644 Guide.md diff --git a/Cargo.toml b/Cargo.toml index 32937c6..45a2a59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ version = "0.1.0" edition = "2024" rust-version = "1.91.0" authors = ["Blockstream"] -readme = "Readme.md" +readme = "README.md" [workspace.dependencies] diff --git a/Guide.md b/Guide.md deleted file mode 100644 index 457336a..0000000 --- a/Guide.md +++ /dev/null @@ -1,164 +0,0 @@ -# Simplicity DEX — Developer Guide - -This short guide helps contributors understand, build, test and extend the project. It focuses on practical commands and -the patterns used across crates (not exhaustive; follow Rust and crate docs for deeper dives). - -## Project layout - -- crates/dex-cli — command line client and UX helpers -- crates/dex-nostr-relay — relay logic, event parsing and storage -- crates/global-utils — other helpers - -## Prerequisites - -- Install Rust -- Create your nostr keypair (Can be generated here: https://start.nostr.net/) - -## Quick start - -1. Build: - - cargo build -r -2. Run CLI (local dev): - - `cargo build -r` - - `mkdir -p ./demo` - - `mv ./target/release/simplicity-dex ./demo/simplicity-dex` - - `cp ./.simplicity-dex.example/.simplicity-dex.config.toml ./demo/.simplicity-dex.config.toml` - - `echo SEED_HEX=ffff0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab > ./demo/.env` -3. Insert your valid nostr keypair into `.simplicity-dex.config.toml` - -## Commands example execution - -Overall trading for dcd contracts can be split in two sides: taker and maker. - -Maker and Taker responsible for taking such steps: - -1) Maker initializes contract in Liquid; -2) Maker funds contract with collateral and settlement tokens. (by now for test **collateral** = LBTC-Testnet, **settlement** = minted token from scratch) -3) Taker funds contract with collateral tokens and takes contract parameters from already discovered maker event_id. -4) Maker now can make: - * Early collateral termination - * Early settlement termination -5) Taker now can make: - * Early termination -6) After `settlement-height` both maker and taker can use settlement exit to receive their tokens (collateral or settlement) depending on the settlement token price, which is signed with oracle. - -1. Create your own contract with your values. For example can be taken - -* `taker-funding-start-time` 1764328373 (timestamp can be taken from https://www.epochconverter.com/) -* `taker-funding-end-time` 1764358373 (Block time when taker funding period ends) -* `contract-expiry-time` 1764359373 (Block time when contract expires) -* `early-termination-end-time` 1764359373 (Block time when early termination is no longer allowed) -* `settlement-height` 2169368 (Block height at which oracle price is attested) -* `principal-collateral-amount` 2000 (Base collateral amount) -* `incentive-basis-points` 1000 (Incentive in basis points (1 bp = 0.01%)) -* `filler-per-principal-collateral` 100 (Filler token ratio) -* `strike-price` 25 (Oracle strike price for settlement) -* `settlement-asset-entropy` `0ffa97b7ee6fcaac30b0c04803726f13c5176af59596874a3a770cbfd2a8d183` (Asset entropy (hex) for settlement) -* `oracle-pubkey` `757f7c05d2d8f92ab37b880710491222a0d22b66be83ae68ff75cc6cb15dd2eb` (`./simplicity-dex helpers address --account-index 5`) - -Actual command in cli: -```bash -./simplicity-dex maker init - --utxo-1 - --utxo-2 - --utxo-3 - --taker-funding-start-time - --taker-funding-end-time - --contract-expiry-time - --early-termination-end-time - --settlement-height - --principal-collateral-amount - --incentive-basis-points - --filler-per-principal-collateral - --strike-price - --settlement-asset-entropy - --oracle-pubkey -``` - -2. Maker fund cli command: -```bash -./simplicity-dex maker fund - --filler-utxo - --grant-coll-utxo - --grant-settl-utxo - --settl-asset-utxo - --fee-utxo - --taproot-pubkey-gen -``` - -3. Taker has to fund - -```bash -./simplicity-dex taker fund - --filler-utxo - --collateral-utxo - --collateral-amount-deposit - --maker-order-event-id -``` - -4. Taker can wait for specific `settlement-height` and gracefully exit contract: -```bash -./simplicity-dex taker settlement - --filler-utxo - --asset-utxo - --fee-utxo - --filler-to-burn - --price-now - --oracle-sign - --maker-order-event-id -``` - -5. Maker can wait for specific `settlement-height` and gracefully exit contract: -```bash - ./simplicity-dex maker settlement - --grant-collateral-utxo - --grant-settlement-utxo - --asset-utxo - --fee-utxo - --grantor-amount-burn - --price-now - --oracle-sign - --maker-order-event-id -``` - -* Maker or Taker depending on the can use Merge(2/3/4) command to merge collateral tokens. -This is made exactly for combining outs into one to eliminate execution of contract with usage of little fragments -```bash -./simplicity-dex helpers merge-tokens4 - --token-utxo-1 - --token-utxo-2 - --token-utxo-3 - --token-utxo-4 - --fee-utxo - --maker-order-event-id -``` - -* For early collateral termination Maker can use command: -```bash -./simplicity-dex maker termination-collateral - --grantor-collateral-utxo - --collateral-utxo - --fee-utxo - --grantor-collateral-burn - --maker-order-event-id -``` - -* For early settlement termination Maker can use command: -```bash -./simplicity-dex maker termination-settlement - --settlement-asset-utxo - --grantor-settlement-utxo - --fee-utxo - --grantor-settlement-amount-burn - --maker-order-event-id -``` - -* For early termination Taker can use command: -```bash -./simplicity-dex taker termination-early - --filler-utxo - --collateral-utxo - --fee-utxo - --filler-to-return - --maker-order-event-id " - ], - [ - "asset_to_buy", - "" - ], - [ - "price", - "1000000", - "sats_per_contract" - ], - [ - "expiry", - "1735689600" - ], - [ - "compiler", - "simplicity-v1.2.3", - "deterministic_build_hash" - ] -] -``` - -### Protocol Benefits - -- **Interoperability**: Any NOSTR-compatible client can parse and validate offers -- **Transparency**: All offers are publicly auditable -- **Censorship Resistance**: Distributed messaging prevents single points of failure -- **Standardization**: Consistent format enables ecosystem growth -- **Extensibility**: Protocol designed for future enhancements - -## Getting Started - -### Basic Usage - -1. **Create an Offer**: Generate a PACT-compliant NOSTR event with your trading parameters -2. **Broadcast**: Publish the offer to NOSTR relays -3. **Discovery**: Takers can filter and discover offers using tag-based queries -4. **Execution**: Complete trades through Simplicity contract execution - -## Architecture - -```text -┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ Maker Client │ │ NOSTR Relays │ │ Taker Client │ -│ │<───>| │<───>│ │ -│ - Create Offers │ │ - Store Events │ │ - Discover │ -│ - Sign Contracts│ │ - Relay Messages │ │ - Execute Trades│ -└─────────────────┘ └──────────────────┘ └─────────────────┘ - │ │ │ - │ ┌──────────────────┐ │ - └─────────────>│ Liquid Network │<────────────┘ - │ │ - │ - Asset Registry │ - │ - Contract Exec │ - │ - Settlement │ - └──────────────────┘ -``` - -## Contributing - -We welcome contributions to the Simplicity DEX project. - -## License - -This project is licensed under the MIT License - see the LICENSE file for details. - -## Links +## Useful resources - [Simplicity Language](https://github.com/ElementsProject/simplicity) - [NOSTR Protocol](https://github.com/nostr-protocol/nostr) From d988f893520381a1505a9311b4c7a6f9ef288800 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Mon, 22 Dec 2025 14:52:31 +0200 Subject: [PATCH 4/5] Completed refactoring of the NOSTR relay lib --- Cargo.toml | 34 +- crates/cli-client/Cargo.toml | 16 + crates/cli-client/README.md | 0 .../{dex-cli => cli-client}/src/bin/main.rs | 2 +- crates/{dex-cli => cli-client}/src/cli/dex.rs | 0 .../{dex-cli => cli-client}/src/cli/helper.rs | 0 .../{dex-cli => cli-client}/src/cli/maker.rs | 0 crates/{dex-cli => cli-client}/src/cli/mod.rs | 0 .../src/cli/processor.rs | 0 .../{dex-cli => cli-client}/src/cli/taker.rs | 0 .../src/common/config.rs | 0 .../src/common/keys.rs | 0 .../{dex-cli => cli-client}/src/common/mod.rs | 0 .../src/common/settings.rs | 0 .../src/common/store.rs | 0 .../src/common/types.rs | 0 .../src/common/utils.rs | 0 .../src/contract_handlers/address.rs | 0 .../src/contract_handlers/common.rs | 0 .../src/contract_handlers/faucet.rs | 0 .../src/contract_handlers/maker_funding.rs | 0 .../src/contract_handlers/maker_init.rs | 0 .../src/contract_handlers/maker_settlement.rs | 0 .../maker_termination_collateral.rs | 0 .../maker_termination_settlement.rs | 0 .../src/contract_handlers/merge_tokens.rs | 0 .../src/contract_handlers/mod.rs | 0 .../src/contract_handlers/oracle_signature.rs | 0 .../src/contract_handlers/split_utxo.rs | 0 .../taker_early_termination.rs | 0 .../src/contract_handlers/taker_funding.rs | 0 .../src/contract_handlers/taker_settlement.rs | 0 crates/{dex-cli => cli-client}/src/error.rs | 0 crates/{dex-cli => cli-client}/src/lib.rs | 1 + .../src/logger.rs | 6 +- crates/dex-cli/Cargo.toml | 39 - crates/dex-nostr-relay/Cargo.toml | 30 - crates/dex-nostr-relay/src/error.rs | 27 - crates/dex-nostr-relay/src/handlers/common.rs | 34 - .../src/handlers/get_events.rs | 48 -- .../src/handlers/list_orders.rs | 29 - crates/dex-nostr-relay/src/handlers/mod.rs | 6 - .../src/handlers/order_replies.rs | 26 - .../src/handlers/place_order.rs | 24 - .../src/handlers/reply_order.rs | 28 - crates/dex-nostr-relay/src/lib.rs | 7 - crates/dex-nostr-relay/src/relay_client.rs | 161 ---- crates/dex-nostr-relay/src/relay_processor.rs | 156 ---- crates/dex-nostr-relay/src/types.rs | 702 ------------------ .../tests/test_order_placing.rs | 164 ---- crates/dex-nostr-relay/tests/utils.rs | 8 - crates/global-utils/Cargo.toml | 17 - crates/global-utils/src/lib.rs | 3 - crates/options-relay/Cargo.toml | 26 + crates/options-relay/README.md | 19 + crates/options-relay/src/client.rs | 6 + crates/options-relay/src/client/publishing.rs | 148 ++++ crates/options-relay/src/client/read_only.rs | 112 +++ crates/options-relay/src/config.rs | 112 +++ crates/options-relay/src/error.rs | 54 ++ .../src/events/action_completed.rs | 179 +++++ crates/options-relay/src/events/filters.rs | 41 + crates/options-relay/src/events/kinds.rs | 19 + crates/options-relay/src/events/mod.rs | 11 + .../src/events/option_created.rs | 157 ++++ .../options-relay/src/events/swap_created.rs | 159 ++++ crates/options-relay/src/lib.rs | 15 + 67 files changed, 1083 insertions(+), 1543 deletions(-) create mode 100644 crates/cli-client/Cargo.toml create mode 100644 crates/cli-client/README.md rename crates/{dex-cli => cli-client}/src/bin/main.rs (87%) rename crates/{dex-cli => cli-client}/src/cli/dex.rs (100%) rename crates/{dex-cli => cli-client}/src/cli/helper.rs (100%) rename crates/{dex-cli => cli-client}/src/cli/maker.rs (100%) rename crates/{dex-cli => cli-client}/src/cli/mod.rs (100%) rename crates/{dex-cli => cli-client}/src/cli/processor.rs (100%) rename crates/{dex-cli => cli-client}/src/cli/taker.rs (100%) rename crates/{dex-cli => cli-client}/src/common/config.rs (100%) rename crates/{dex-cli => cli-client}/src/common/keys.rs (100%) rename crates/{dex-cli => cli-client}/src/common/mod.rs (100%) rename crates/{dex-cli => cli-client}/src/common/settings.rs (100%) rename crates/{dex-cli => cli-client}/src/common/store.rs (100%) rename crates/{dex-cli => cli-client}/src/common/types.rs (100%) rename crates/{dex-cli => cli-client}/src/common/utils.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/address.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/common.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/faucet.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/maker_funding.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/maker_init.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/maker_settlement.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/maker_termination_collateral.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/maker_termination_settlement.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/merge_tokens.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/mod.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/oracle_signature.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/split_utxo.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/taker_early_termination.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/taker_funding.rs (100%) rename crates/{dex-cli => cli-client}/src/contract_handlers/taker_settlement.rs (100%) rename crates/{dex-cli => cli-client}/src/error.rs (100%) rename crates/{dex-cli => cli-client}/src/lib.rs (87%) rename crates/{global-utils => cli-client}/src/logger.rs (84%) delete mode 100644 crates/dex-cli/Cargo.toml delete mode 100644 crates/dex-nostr-relay/Cargo.toml delete mode 100644 crates/dex-nostr-relay/src/error.rs delete mode 100644 crates/dex-nostr-relay/src/handlers/common.rs delete mode 100644 crates/dex-nostr-relay/src/handlers/get_events.rs delete mode 100644 crates/dex-nostr-relay/src/handlers/list_orders.rs delete mode 100644 crates/dex-nostr-relay/src/handlers/mod.rs delete mode 100644 crates/dex-nostr-relay/src/handlers/order_replies.rs delete mode 100644 crates/dex-nostr-relay/src/handlers/place_order.rs delete mode 100644 crates/dex-nostr-relay/src/handlers/reply_order.rs delete mode 100644 crates/dex-nostr-relay/src/lib.rs delete mode 100644 crates/dex-nostr-relay/src/relay_client.rs delete mode 100644 crates/dex-nostr-relay/src/relay_processor.rs delete mode 100644 crates/dex-nostr-relay/src/types.rs delete mode 100644 crates/dex-nostr-relay/tests/test_order_placing.rs delete mode 100644 crates/dex-nostr-relay/tests/utils.rs delete mode 100644 crates/global-utils/Cargo.toml delete mode 100644 crates/global-utils/src/lib.rs create mode 100644 crates/options-relay/Cargo.toml create mode 100644 crates/options-relay/README.md create mode 100644 crates/options-relay/src/client.rs create mode 100644 crates/options-relay/src/client/publishing.rs create mode 100644 crates/options-relay/src/client/read_only.rs create mode 100644 crates/options-relay/src/config.rs create mode 100644 crates/options-relay/src/error.rs create mode 100644 crates/options-relay/src/events/action_completed.rs create mode 100644 crates/options-relay/src/events/filters.rs create mode 100644 crates/options-relay/src/events/kinds.rs create mode 100644 crates/options-relay/src/events/mod.rs create mode 100644 crates/options-relay/src/events/option_created.rs create mode 100644 crates/options-relay/src/events/swap_created.rs create mode 100644 crates/options-relay/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 45a2a59..bc4016d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,34 +11,12 @@ rust-version = "1.91.0" authors = ["Blockstream"] readme = "README.md" - [workspace.dependencies] anyhow = { version = "1.0.100" } -bincode = { version = "2.0.1" } -chrono = { version = "0.4.42" } -clap = { version = "4.5.49", features = ["derive"] } -config = { version = "0.15.18" } -contracts = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "baa8ab7", package = "contracts" } -contracts-adapter = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "baa8ab7", package = "contracts-adapter" } -dex-nostr-relay = { path = "./crates/dex-nostr-relay" } -dirs = { version = "6.0.0" } -dotenvy = { version = "0.15.7" } -elements = { version = "0.26.1" } -futures-util = { version = "0.3.31" } -global-utils = { path = "./crates/global-utils" } -hex = { version = "0.4.3" } -humantime = { version = "2.3.0" } -nostr = { version = "0.43.1", features = ["std"] } -nostr-sdk = { version = "0.43.0" } -proptest = { version = "1.9.0" } -serde = { version = "1.0.228" } -serde_json = { version = "1.0.145" } -simplicity-lang = { version = "0.6.0" } -simplicityhl = { version = "0.2.0" } -simplicityhl-core = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "baa8ab7", package = "simplicityhl-core", features = ["encoding"] } -sled = { version = "0.34.7" } -thiserror = { version = "2.0.17" } -tokio = { version = "1.48.0", features = ["macros", "test-util", "rt", "rt-multi-thread", "tracing" ] } + tracing = { version = "0.1.41" } -tracing-appender = { version = "0.2.3" } -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } + +contracts = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "35449ea", package = "contracts" } +simplicityhl-core = { version = "0.2.1", features = ["encoding"] } + +simplicityhl = { version = "0.4.0" } diff --git a/crates/cli-client/Cargo.toml b/crates/cli-client/Cargo.toml new file mode 100644 index 0000000..23b85b3 --- /dev/null +++ b/crates/cli-client/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "cli-client" +version = "0.1.0" +edition = "2024" +description = "" +license = "MIT OR Apache-2.0" +readme = "README.md" +publish = false + + +[[bin]] +name = "simplicity-dex" +path = "src/bin/main.rs" + +[dependencies] + diff --git a/crates/cli-client/README.md b/crates/cli-client/README.md new file mode 100644 index 0000000..e69de29 diff --git a/crates/dex-cli/src/bin/main.rs b/crates/cli-client/src/bin/main.rs similarity index 87% rename from crates/dex-cli/src/bin/main.rs rename to crates/cli-client/src/bin/main.rs index 8e44e70..3b1b501 100644 --- a/crates/dex-cli/src/bin/main.rs +++ b/crates/cli-client/src/bin/main.rs @@ -2,7 +2,7 @@ use clap::Parser; -use global_utils::logger::init_logger; +use dex_cli::logger::init_logger; use dex_cli::cli::Cli; diff --git a/crates/dex-cli/src/cli/dex.rs b/crates/cli-client/src/cli/dex.rs similarity index 100% rename from crates/dex-cli/src/cli/dex.rs rename to crates/cli-client/src/cli/dex.rs diff --git a/crates/dex-cli/src/cli/helper.rs b/crates/cli-client/src/cli/helper.rs similarity index 100% rename from crates/dex-cli/src/cli/helper.rs rename to crates/cli-client/src/cli/helper.rs diff --git a/crates/dex-cli/src/cli/maker.rs b/crates/cli-client/src/cli/maker.rs similarity index 100% rename from crates/dex-cli/src/cli/maker.rs rename to crates/cli-client/src/cli/maker.rs diff --git a/crates/dex-cli/src/cli/mod.rs b/crates/cli-client/src/cli/mod.rs similarity index 100% rename from crates/dex-cli/src/cli/mod.rs rename to crates/cli-client/src/cli/mod.rs diff --git a/crates/dex-cli/src/cli/processor.rs b/crates/cli-client/src/cli/processor.rs similarity index 100% rename from crates/dex-cli/src/cli/processor.rs rename to crates/cli-client/src/cli/processor.rs diff --git a/crates/dex-cli/src/cli/taker.rs b/crates/cli-client/src/cli/taker.rs similarity index 100% rename from crates/dex-cli/src/cli/taker.rs rename to crates/cli-client/src/cli/taker.rs diff --git a/crates/dex-cli/src/common/config.rs b/crates/cli-client/src/common/config.rs similarity index 100% rename from crates/dex-cli/src/common/config.rs rename to crates/cli-client/src/common/config.rs diff --git a/crates/dex-cli/src/common/keys.rs b/crates/cli-client/src/common/keys.rs similarity index 100% rename from crates/dex-cli/src/common/keys.rs rename to crates/cli-client/src/common/keys.rs diff --git a/crates/dex-cli/src/common/mod.rs b/crates/cli-client/src/common/mod.rs similarity index 100% rename from crates/dex-cli/src/common/mod.rs rename to crates/cli-client/src/common/mod.rs diff --git a/crates/dex-cli/src/common/settings.rs b/crates/cli-client/src/common/settings.rs similarity index 100% rename from crates/dex-cli/src/common/settings.rs rename to crates/cli-client/src/common/settings.rs diff --git a/crates/dex-cli/src/common/store.rs b/crates/cli-client/src/common/store.rs similarity index 100% rename from crates/dex-cli/src/common/store.rs rename to crates/cli-client/src/common/store.rs diff --git a/crates/dex-cli/src/common/types.rs b/crates/cli-client/src/common/types.rs similarity index 100% rename from crates/dex-cli/src/common/types.rs rename to crates/cli-client/src/common/types.rs diff --git a/crates/dex-cli/src/common/utils.rs b/crates/cli-client/src/common/utils.rs similarity index 100% rename from crates/dex-cli/src/common/utils.rs rename to crates/cli-client/src/common/utils.rs diff --git a/crates/dex-cli/src/contract_handlers/address.rs b/crates/cli-client/src/contract_handlers/address.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/address.rs rename to crates/cli-client/src/contract_handlers/address.rs diff --git a/crates/dex-cli/src/contract_handlers/common.rs b/crates/cli-client/src/contract_handlers/common.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/common.rs rename to crates/cli-client/src/contract_handlers/common.rs diff --git a/crates/dex-cli/src/contract_handlers/faucet.rs b/crates/cli-client/src/contract_handlers/faucet.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/faucet.rs rename to crates/cli-client/src/contract_handlers/faucet.rs diff --git a/crates/dex-cli/src/contract_handlers/maker_funding.rs b/crates/cli-client/src/contract_handlers/maker_funding.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/maker_funding.rs rename to crates/cli-client/src/contract_handlers/maker_funding.rs diff --git a/crates/dex-cli/src/contract_handlers/maker_init.rs b/crates/cli-client/src/contract_handlers/maker_init.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/maker_init.rs rename to crates/cli-client/src/contract_handlers/maker_init.rs diff --git a/crates/dex-cli/src/contract_handlers/maker_settlement.rs b/crates/cli-client/src/contract_handlers/maker_settlement.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/maker_settlement.rs rename to crates/cli-client/src/contract_handlers/maker_settlement.rs diff --git a/crates/dex-cli/src/contract_handlers/maker_termination_collateral.rs b/crates/cli-client/src/contract_handlers/maker_termination_collateral.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/maker_termination_collateral.rs rename to crates/cli-client/src/contract_handlers/maker_termination_collateral.rs diff --git a/crates/dex-cli/src/contract_handlers/maker_termination_settlement.rs b/crates/cli-client/src/contract_handlers/maker_termination_settlement.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/maker_termination_settlement.rs rename to crates/cli-client/src/contract_handlers/maker_termination_settlement.rs diff --git a/crates/dex-cli/src/contract_handlers/merge_tokens.rs b/crates/cli-client/src/contract_handlers/merge_tokens.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/merge_tokens.rs rename to crates/cli-client/src/contract_handlers/merge_tokens.rs diff --git a/crates/dex-cli/src/contract_handlers/mod.rs b/crates/cli-client/src/contract_handlers/mod.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/mod.rs rename to crates/cli-client/src/contract_handlers/mod.rs diff --git a/crates/dex-cli/src/contract_handlers/oracle_signature.rs b/crates/cli-client/src/contract_handlers/oracle_signature.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/oracle_signature.rs rename to crates/cli-client/src/contract_handlers/oracle_signature.rs diff --git a/crates/dex-cli/src/contract_handlers/split_utxo.rs b/crates/cli-client/src/contract_handlers/split_utxo.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/split_utxo.rs rename to crates/cli-client/src/contract_handlers/split_utxo.rs diff --git a/crates/dex-cli/src/contract_handlers/taker_early_termination.rs b/crates/cli-client/src/contract_handlers/taker_early_termination.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/taker_early_termination.rs rename to crates/cli-client/src/contract_handlers/taker_early_termination.rs diff --git a/crates/dex-cli/src/contract_handlers/taker_funding.rs b/crates/cli-client/src/contract_handlers/taker_funding.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/taker_funding.rs rename to crates/cli-client/src/contract_handlers/taker_funding.rs diff --git a/crates/dex-cli/src/contract_handlers/taker_settlement.rs b/crates/cli-client/src/contract_handlers/taker_settlement.rs similarity index 100% rename from crates/dex-cli/src/contract_handlers/taker_settlement.rs rename to crates/cli-client/src/contract_handlers/taker_settlement.rs diff --git a/crates/dex-cli/src/error.rs b/crates/cli-client/src/error.rs similarity index 100% rename from crates/dex-cli/src/error.rs rename to crates/cli-client/src/error.rs diff --git a/crates/dex-cli/src/lib.rs b/crates/cli-client/src/lib.rs similarity index 87% rename from crates/dex-cli/src/lib.rs rename to crates/cli-client/src/lib.rs index 75a5a6e..d4030bb 100644 --- a/crates/dex-cli/src/lib.rs +++ b/crates/cli-client/src/lib.rs @@ -4,3 +4,4 @@ pub mod cli; pub mod common; mod contract_handlers; pub mod error; +pub mod logger; diff --git a/crates/global-utils/src/logger.rs b/crates/cli-client/src/logger.rs similarity index 84% rename from crates/global-utils/src/logger.rs rename to crates/cli-client/src/logger.rs index ecd0604..6f3996e 100644 --- a/crates/global-utils/src/logger.rs +++ b/crates/cli-client/src/logger.rs @@ -4,9 +4,6 @@ use tracing::{level_filters::LevelFilter, trace}; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt}; -const ENV_VAR_NAME: &str = "DEX_LOG"; -const DEFAULT_LOG_DIRECTIVE: LevelFilter = LevelFilter::ERROR; - #[derive(Debug)] pub struct LoggerGuard { _std_out_guard: WorkerGuard, @@ -22,8 +19,7 @@ pub fn init_logger() -> LoggerGuard { .with_level(true) .with_filter( EnvFilter::builder() - .with_default_directive(DEFAULT_LOG_DIRECTIVE.into()) - .with_env_var(ENV_VAR_NAME) + .with_default_directive(LevelFilter::ERROR.into()) .from_env_lossy(), ); diff --git a/crates/dex-cli/Cargo.toml b/crates/dex-cli/Cargo.toml deleted file mode 100644 index f46104d..0000000 --- a/crates/dex-cli/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "dex-cli" -version = "0.1.0" -edition = "2024" -description = "Simplicity helper CLI for Liquid testnet" -license = "MIT OR Apache-2.0" -readme = "README.md" -publish = false - - -[[bin]] -name = "simplicity-dex" -path = "src/bin/main.rs" - -[dependencies] -anyhow = { workspace = true } -bincode = { workspace = true } -clap = { workspace = true, features = ["env"] } -config = { workspace = true } -contracts = { workspace = true } -contracts-adapter = { workspace = true } -dex-nostr-relay = { workspace = true } -dotenvy = { workspace = true } -elements = { workspace = true } -global-utils = { workspace = true } -hex = { workspace = true } -humantime = { workspace = true } -nostr = { workspace = true } -proptest = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -simplicity-lang = { workspace = true } -simplicityhl = { workspace = true } -simplicityhl-core = { workspace = true } -sled = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } - diff --git a/crates/dex-nostr-relay/Cargo.toml b/crates/dex-nostr-relay/Cargo.toml deleted file mode 100644 index bbb7275..0000000 --- a/crates/dex-nostr-relay/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "dex-nostr-relay" -description = "Dex Nostr Relay communicator implementation" -license = "MIT OR Apache-2.0" - -version.workspace = true -edition.workspace = true -rust-version.workspace = true -authors.workspace = true -readme.workspace = true - -[dependencies] -anyhow = { workspace = true } -bincode = { workspace = true } -chrono = { workspace = true } -global-utils = { workspace = true } -hex = { workspace = true } -nostr = { workspace = true } -nostr-sdk = { workspace = true } -contracts-adapter = { workspace = true } -contracts = { workspace = true } -simplicity-lang = { workspace = true } -simplicityhl = { workspace = true } -simplicityhl-core = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } - -[dev-dependencies] -dotenvy = { workspace = true } \ No newline at end of file diff --git a/crates/dex-nostr-relay/src/error.rs b/crates/dex-nostr-relay/src/error.rs deleted file mode 100644 index 8b8e1c1..0000000 --- a/crates/dex-nostr-relay/src/error.rs +++ /dev/null @@ -1,27 +0,0 @@ -use nostr::SignerError; -use nostr::filter::SingleLetterTagError; - -#[derive(thiserror::Error, Debug)] -pub enum NostrRelayError { - #[error("Signer error: {0}")] - Signer(#[from] SignerError), - #[error("Single letter error: {0}")] - SingleLetterTag(#[from] SingleLetterTagError), - #[error("Failed to convert custom url to RelayURL, err: {err_msg}")] - FailedToConvertRelayUrl { err_msg: String }, - #[error("An error occurred in Nostr Client, err: {0}")] - NostrClientFailure(#[from] nostr_sdk::client::Error), - #[error("Relay Client requires for operation signature, add key to the Client")] - MissingSigner, - #[error("No events found by filter: '{0}'")] - NoEventsFound(String), - #[error("Found many events, but required to be only one with filter: '{0}'")] - NotOnlyOneEventFound(String), - #[error("Failed to encode '{struct_to_encode}', err: `{err}`")] - BincodeEncoding { - err: bincode::error::EncodeError, - struct_to_encode: String, - }, -} - -pub type Result = std::result::Result; diff --git a/crates/dex-nostr-relay/src/handlers/common.rs b/crates/dex-nostr-relay/src/handlers/common.rs deleted file mode 100644 index 7653438..0000000 --- a/crates/dex-nostr-relay/src/handlers/common.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::types::{MakerOrderEvent, OrderReplyEvent}; -use chrono::{DateTime, TimeZone, Utc}; -use nostr::Timestamp; -use nostr_sdk::prelude::Events; - -pub fn filter_maker_order_events(events_to_filter: &Events) -> Vec { - events_to_filter - .iter() - .filter_map(MakerOrderEvent::parse_event) - .collect() -} - -pub fn sort_maker_order_events_by_time(mut events: Vec) -> Vec { - events.sort_by_key(|e| e.time); - events -} - -pub fn filter_order_reply_events(events_to_filter: &Events) -> Vec { - events_to_filter - .iter() - .filter_map(OrderReplyEvent::parse_event) - .collect() -} - -pub fn sort_order_replies_by_time(mut events: Vec) -> Vec { - events.sort_by_key(|e| e.time); - events -} - -pub fn timestamp_to_chrono_utc(time: Timestamp) -> Option> { - chrono::Utc - .timestamp_opt(i64::try_from(time.as_u64()).ok()?, 0) - .single() -} diff --git a/crates/dex-nostr-relay/src/handlers/get_events.rs b/crates/dex-nostr-relay/src/handlers/get_events.rs deleted file mode 100644 index 03faf0a..0000000 --- a/crates/dex-nostr-relay/src/handlers/get_events.rs +++ /dev/null @@ -1,48 +0,0 @@ -pub mod ids { - use crate::relay_client::RelayClient; - use nostr::{EventId, Filter}; - use nostr_sdk::prelude::Events; - use std::collections::{BTreeMap, BTreeSet}; - - pub async fn handle(client: &RelayClient, event_id: EventId) -> crate::error::Result { - let events = client - .req_and_wait(Filter { - ids: Some(BTreeSet::from([event_id])), - authors: None, - kinds: None, - search: None, - since: None, - until: None, - limit: None, - generic_tags: BTreeMap::default(), - }) - .await?; - Ok(events) - } -} - -pub mod order { - use crate::handlers::common::{filter_maker_order_events, sort_maker_order_events_by_time}; - use crate::relay_client::RelayClient; - use crate::types::MakerOrderEvent; - use nostr::{EventId, Filter}; - use std::collections::{BTreeMap, BTreeSet}; - - pub async fn handle(client: &RelayClient, event_id: EventId) -> crate::error::Result> { - let events = client - .req_and_wait(Filter { - ids: Some(BTreeSet::from([event_id])), - authors: None, - kinds: None, - search: None, - since: None, - until: None, - limit: None, - generic_tags: BTreeMap::default(), - }) - .await?; - let events = filter_maker_order_events(&events); - let events = sort_maker_order_events_by_time(events); - Ok(events) - } -} diff --git a/crates/dex-nostr-relay/src/handlers/list_orders.rs b/crates/dex-nostr-relay/src/handlers/list_orders.rs deleted file mode 100644 index 8ecc03a..0000000 --- a/crates/dex-nostr-relay/src/handlers/list_orders.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::handlers::common::filter_maker_order_events; -use crate::relay_client::RelayClient; -use crate::relay_processor::ListOrdersEventFilter; -use crate::types::{MakerOrderEvent, MakerOrderSummary}; -use nostr::Timestamp; -use nostr_sdk::prelude::Events; - -pub async fn handle( - client: &RelayClient, - filter: ListOrdersEventFilter, -) -> crate::error::Result> { - let events = client.req_and_wait(filter.to_filter()).await?; - let events = filter_expired_events(events); - let events = filter_maker_order_events(&events); - let events = events.iter().map(MakerOrderEvent::summary).collect(); - Ok(events) -} - -#[inline] -fn filter_expired_events(events_to_filter: Events) -> Events { - let time_now = Timestamp::now(); - events_to_filter - .into_iter() - .filter(|x| match x.tags.expiration() { - None => true, - Some(t) => t.as_u64() > time_now.as_u64(), - }) - .collect() -} diff --git a/crates/dex-nostr-relay/src/handlers/mod.rs b/crates/dex-nostr-relay/src/handlers/mod.rs deleted file mode 100644 index ad6f802..0000000 --- a/crates/dex-nostr-relay/src/handlers/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub(crate) mod common; -pub(crate) mod get_events; -pub(crate) mod list_orders; -pub(crate) mod order_replies; -pub(crate) mod place_order; -pub(crate) mod reply_order; diff --git a/crates/dex-nostr-relay/src/handlers/order_replies.rs b/crates/dex-nostr-relay/src/handlers/order_replies.rs deleted file mode 100644 index 677c7b0..0000000 --- a/crates/dex-nostr-relay/src/handlers/order_replies.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::relay_client::RelayClient; -use crate::types::{CustomKind, OrderReplyEvent, TakerReplyOrderKind}; - -use std::collections::{BTreeMap, BTreeSet}; - -use crate::handlers::common::{filter_order_reply_events, sort_order_replies_by_time}; -use nostr::{EventId, Filter, SingleLetterTag}; - -pub async fn handle(client: &RelayClient, event_id: EventId) -> crate::error::Result> { - let events = client - .req_and_wait(Filter { - ids: None, - authors: None, - kinds: None, - search: None, - since: None, - until: None, - limit: None, - generic_tags: BTreeMap::from([(SingleLetterTag::from_char('e')?, BTreeSet::from([event_id.to_string()]))]), - }) - .await?; - let events = filter_order_reply_events(&events); - let events = sort_order_replies_by_time(events); - - Ok(events) -} diff --git a/crates/dex-nostr-relay/src/handlers/place_order.rs b/crates/dex-nostr-relay/src/handlers/place_order.rs deleted file mode 100644 index 5388486..0000000 --- a/crates/dex-nostr-relay/src/handlers/place_order.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::relay_client::RelayClient; -use crate::relay_processor::OrderPlaceEventTags; -use crate::types::{BLOCKSTREAM_MAKER_CONTENT, CustomKind, MakerOrderEvent, MakerOrderKind}; -use nostr::{EventBuilder, EventId, Timestamp}; -use simplicity::elements::Txid; - -pub async fn handle(client: &RelayClient, tags: OrderPlaceEventTags, tx_id: Txid) -> crate::error::Result { - let client_signer = client.get_signer().await?; - let client_pubkey = client_signer.get_public_key().await?; - - let timestamp_now = Timestamp::now(); - - let tags = MakerOrderEvent::form_tags(tags, tx_id, client_pubkey)?; - let maker_order = EventBuilder::new(MakerOrderKind::get_kind(), BLOCKSTREAM_MAKER_CONTENT) - .tags(tags) - .custom_created_at(timestamp_now); - - let text_note = maker_order.build(client_pubkey); - let signed_event = client_signer.sign_event(text_note).await?; - - let maker_order_event_id = client.publish_event(&signed_event).await?; - - Ok(maker_order_event_id) -} diff --git a/crates/dex-nostr-relay/src/handlers/reply_order.rs b/crates/dex-nostr-relay/src/handlers/reply_order.rs deleted file mode 100644 index d887f87..0000000 --- a/crates/dex-nostr-relay/src/handlers/reply_order.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::relay_client::RelayClient; -use crate::types::ReplyOption; - -use nostr::{EventBuilder, EventId, Timestamp}; - -pub async fn handle( - client: &RelayClient, - source_event_id: EventId, - reply_option: ReplyOption, -) -> crate::error::Result { - let client_signer = client.get_signer().await?; - let client_pubkey = client_signer.get_public_key().await?; - let timestamp_now = Timestamp::now(); - - // Build tags based on reply option variant - let tags = reply_option.form_tags(source_event_id, client_pubkey); - - let reply_event_builder = EventBuilder::new(reply_option.get_kind(), reply_option.get_content()) - .tags(tags) - .custom_created_at(timestamp_now); - - let reply_event = reply_event_builder.build(client_pubkey); - let reply_event = client_signer.sign_event(reply_event).await?; - - let event_id = client.publish_event(&reply_event).await?; - - Ok(event_id) -} diff --git a/crates/dex-nostr-relay/src/lib.rs b/crates/dex-nostr-relay/src/lib.rs deleted file mode 100644 index 216b4b5..0000000 --- a/crates/dex-nostr-relay/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] - -pub mod error; -pub mod handlers; -pub mod relay_client; -pub mod relay_processor; -pub mod types; diff --git a/crates/dex-nostr-relay/src/relay_client.rs b/crates/dex-nostr-relay/src/relay_client.rs deleted file mode 100644 index 63cc7e4..0000000 --- a/crates/dex-nostr-relay/src/relay_client.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::error::NostrRelayError; - -use std::collections::HashMap; -use std::fmt::Debug; -use std::sync::Arc; -use std::time::Duration; - -use nostr::prelude::*; -use nostr_sdk::pool::Output; -use nostr_sdk::prelude::Events; -use nostr_sdk::{Client, Relay, SubscribeAutoCloseOptions}; - -use tracing::instrument; - -#[derive(Debug, Clone)] -pub struct RelayClient { - client: Client, - timeout: Duration, -} - -#[derive(Debug)] -pub struct ClientConfig { - pub timeout: Duration, -} - -impl RelayClient { - /// Connect to one or more Nostr relays and return a configured `RelayClient`. - /// - /// # Errors - /// - /// Returns an error if a relay URL cannot be converted or if adding a relay - /// to the underlying `nostr_sdk::Client` fails. - #[instrument(skip_all, level = "debug", err)] - pub async fn connect( - relay_urls: impl IntoIterator, - keys: Option, - client_config: ClientConfig, - ) -> crate::error::Result { - tracing::debug!(client_config = ?client_config, "Connecting to Nostr Relay Client(s)"); - - let client = match keys { - None => Client::default(), - Some(keys) => { - let client = Client::new(keys); - client.automatic_authentication(true); - client - } - }; - - for url in relay_urls { - let url = url - .try_into_url() - .map_err(|err| NostrRelayError::FailedToConvertRelayUrl { - err_msg: format!("{err:?}"), - })?; - - client.add_relay(url).await?; - } - - client.connect().await; - - Ok(Self { - client, - timeout: client_config.timeout, - }) - } - - /// Request events from connected relays using the provided filter. - /// - /// # Errors - /// - /// Returns an error if fetching events from the underlying client fails. - #[instrument(skip_all, level = "debug", ret)] - pub async fn req_and_wait(&self, filter: Filter) -> crate::error::Result { - tracing::debug!(filter = ?filter, "Requesting events with filter"); - - Ok(self.client.fetch_combined_events(filter, self.timeout).await?) - } - - /// Return the configured signer for this relay client. - /// - /// # Errors - /// - /// Returns `NostrRelayError::MissingSigner` if no signer is configured, - /// or an error if obtaining the signer from the underlying client fails. - #[instrument(skip_all, level = "debug", ret)] - pub async fn get_signer(&self) -> crate::error::Result> { - if !self.client.has_signer().await { - return Err(NostrRelayError::MissingSigner); - } - - Ok(self.client.signer().await?) - } - - #[instrument(skip_all, level = "debug", ret)] - pub async fn get_relays(&self) -> HashMap { - self.client.relays().await - } - - /// Publish a signed event to connected relays and return its `EventId`. - /// - /// # Errors - /// - /// Returns `NostrRelayError::MissingSigner` if no signer is configured, - /// or an error if sending the event to the underlying client fails. - #[instrument(skip_all, level = "debug", ret)] - pub async fn publish_event(&self, event: &Event) -> crate::error::Result { - if !self.client.has_signer().await { - return Err(NostrRelayError::MissingSigner); - } - - let event_id = self.client.send_event(event).await?; - let event_id = Self::handle_relay_output(event_id)?; - - Ok(event_id) - } - - /// Subscribe to events matching the given filter. - /// - /// # Errors - /// - /// Returns an error if subscribing via the underlying client fails. - #[instrument(skip(self), level = "debug")] - pub async fn subscribe( - &self, - filter: Filter, - opts: Option, - ) -> crate::error::Result { - Ok(self.client.subscribe(filter, opts).await?.val) - } - - #[instrument(skip(self), level = "debug")] - pub async fn unsubscribe(&self, subscription_id: &SubscriptionId) { - self.client.unsubscribe(subscription_id).await; - } - - /// Disconnect from all configured relays. - /// - /// # Errors - /// - /// Currently does not report errors and always returns `Ok(())`. - #[instrument(skip_all, level = "debug", ret)] - pub async fn disconnect(&self) -> crate::error::Result<()> { - self.client.disconnect().await; - Ok(()) - } - - /// Handle the output from a relay operation. - /// - /// # Errors - /// - /// Currently does not report errors and always returns `Ok(output.val)`. - /// This may change if error handling for relay outputs is extended. - /// TODO: handle error - #[instrument(level = "debug")] - fn handle_relay_output(output: Output) -> crate::error::Result { - tracing::debug!(output = ?output, "Handling Relay output."); - - Ok(output.val) - } -} diff --git a/crates/dex-nostr-relay/src/relay_processor.rs b/crates/dex-nostr-relay/src/relay_processor.rs deleted file mode 100644 index caffd54..0000000 --- a/crates/dex-nostr-relay/src/relay_processor.rs +++ /dev/null @@ -1,156 +0,0 @@ -use crate::handlers; -use crate::relay_client::{ClientConfig, RelayClient}; -use crate::types::{CustomKind, MakerOrderEvent, MakerOrderSummary, OrderReplyEvent, ReplyOption}; -use contracts::DCDArguments; -use nostr::prelude::IntoNostrSigner; -use nostr::{EventId, PublicKey, TryIntoUrl}; -use nostr_sdk::prelude::Events; -use simplicityhl::elements::{AssetId, Txid}; - -use nostr::{Filter, Timestamp}; -use std::collections::{BTreeMap, BTreeSet}; - -#[derive(Debug, Clone)] -pub struct RelayProcessor { - relay_client: RelayClient, -} - -#[derive(Debug, Default, Clone)] -pub struct OrderPlaceEventTags { - pub dcd_arguments: DCDArguments, - pub dcd_taproot_pubkey_gen: String, - pub filler_asset_id: AssetId, - pub grantor_collateral_asset_id: AssetId, - pub grantor_settlement_asset_id: AssetId, - pub settlement_asset_id: AssetId, - pub collateral_asset_id: AssetId, -} - -#[derive(Debug, Default, Clone)] -pub struct ListOrdersEventFilter { - pub authors: Option>, - pub since: Option, - pub until: Option, - pub limit: Option, -} - -impl ListOrdersEventFilter { - #[must_use] - pub fn to_filter(&self) -> Filter { - let authors_set = if let Some(list) = &self.authors { - let mut set = BTreeSet::new(); - for pk in list { - set.insert(*pk); - } - if set.is_empty() { None } else { Some(set) } - } else { - None - }; - - Filter { - ids: None, - authors: authors_set, - kinds: Some(BTreeSet::from([crate::types::MakerOrderKind::get_kind()])), - search: None, - since: self.since, - until: self.until, - limit: self.limit, - generic_tags: BTreeMap::default(), - } - } -} - -impl RelayProcessor { - /// Create a [`RelayProcessor`] from relay URLs, optional keys, and client configuration. - /// - /// # Errors - /// - /// Returns an error if connecting to any of the provided relays or - /// configuring the underlying [`RelayClient`] fails. - pub async fn try_from_config( - relay_urls: impl IntoIterator, - keys: Option, - client_config: ClientConfig, - ) -> crate::error::Result { - Ok(RelayProcessor { - relay_client: RelayClient::connect(relay_urls, keys, client_config).await?, - }) - } - - /// Place a new maker order event on the relay network. - /// - /// # Errors - /// - /// Returns an error if constructing or publishing the order event fails, - /// or if the relay client encounters an error while sending the event. - pub async fn place_order(&self, tags: OrderPlaceEventTags, tx_id: Txid) -> crate::error::Result { - let event_id = handlers::place_order::handle(&self.relay_client, tags, tx_id).await?; - Ok(event_id) - } - - /// List maker orders matching the provided filter. - /// - /// # Errors - /// - /// Returns an error if querying relays for matching maker order events - /// fails or if parsing retrieved events into [`MakerOrderSummary`] fails. - pub async fn list_orders(&self, filter: ListOrdersEventFilter) -> crate::error::Result> { - let events = handlers::list_orders::handle(&self.relay_client, filter).await?; - Ok(events) - } - - /// Send a reply to an order event with the given reply option. - /// - /// # Errors - /// - /// Returns an error if building, signing, or publishing the reply event - /// fails, or if the relay client fails to send the event. - pub async fn reply_order(&self, event_source: EventId, reply_option: ReplyOption) -> crate::error::Result { - let event_id = handlers::reply_order::handle(&self.relay_client, event_source, reply_option).await?; - Ok(event_id) - } - - /// Fetch reply events for a given order event. - /// - /// # Errors - /// - /// Returns an error if querying relays for reply events fails or if - /// parsing the retrieved events into [`OrderReplyEvent`] fails. - pub async fn get_order_replies(&self, event_id: EventId) -> crate::error::Result> { - let events = handlers::order_replies::handle(&self.relay_client, event_id).await?; - Ok(events) - } - - /// Fetch a single maker order event by its event ID. - /// - /// # Errors - /// - /// Returns: - /// - `NostrRelayError::NoEventsFound` if no event with the given ID is found. - /// - `NostrRelayError::NotOnlyOneEventFound` if more than one matching event is found. - /// - Any error produced by querying relays for the event. - pub async fn get_order_by_id(&self, event_id: EventId) -> crate::error::Result { - let mut events = handlers::get_events::order::handle(&self.relay_client, event_id).await?; - if events.is_empty() { - return Err(crate::error::NostrRelayError::NoEventsFound(format!( - "event_id: {event_id}" - ))); - } else if events.len() > 1 { - return Err(crate::error::NostrRelayError::NotOnlyOneEventFound(format!( - "event_id: {event_id}" - ))); - } - Ok(events.remove(0)) - } - - /// Fetch raw events with the given event ID. - /// - /// # Errors - /// - /// Returns an error if querying relays for the event fails or if the - /// underlying relay client encounters an error while fetching events. - pub async fn get_event_by_id(&self, event_id: EventId) -> crate::error::Result { - let events = handlers::get_events::ids::handle(&self.relay_client, event_id).await?; - Ok(events) - } -} diff --git a/crates/dex-nostr-relay/src/types.rs b/crates/dex-nostr-relay/src/types.rs deleted file mode 100644 index 132066d..0000000 --- a/crates/dex-nostr-relay/src/types.rs +++ /dev/null @@ -1,702 +0,0 @@ -use crate::handlers::common::timestamp_to_chrono_utc; -use crate::relay_processor::OrderPlaceEventTags; -use chrono::TimeZone; -use contracts::DCDArguments; -use nostr::{Event, EventId, Kind, PublicKey, Tag, TagKind, Tags}; -use simplicity::elements::AssetId; -use simplicity::elements::OutPoint; -use simplicityhl::elements::Txid; -use std::borrow::Cow; -use std::fmt; -use std::str::FromStr; - -pub trait CustomKind { - const ORDER_KIND_NUMBER: u16; - - #[must_use] - fn get_kind() -> Kind { - Kind::from(Self::ORDER_KIND_NUMBER) - } - - #[must_use] - fn get_u16() -> u16 { - Self::ORDER_KIND_NUMBER - } -} - -pub const POW_DIFFICULTY: u8 = 1; -pub const BLOCKSTREAM_MAKER_CONTENT: &str = "Liquid order [Maker]!"; -pub const BLOCKSTREAM_TAKER_REPLY_CONTENT: &str = "Liquid reply [Taker]!"; -pub const BLOCKSTREAM_MAKER_REPLY_CONTENT: &str = "Liquid reply [Maker]!"; -pub const BLOCKSTREAM_MERGE2_REPLY_CONTENT: &str = "Liquid merge [Merge2]!"; -pub const BLOCKSTREAM_MERGE3_REPLY_CONTENT: &str = "Liquid merge [Merge3]!"; -pub const BLOCKSTREAM_MERGE4_REPLY_CONTENT: &str = "Liquid merge [Merge4]!"; - -/// `MAKER_EXPIRATION_TIME` = 31 days -/// TODO: move to the config -pub const MAKER_EXPIRATION_TIME: u64 = 2_678_400; -pub const MAKER_DCD_ARG_TAG: &str = "dcd_arguments_(hex&bincode)"; -pub const MAKER_DCD_TAPROOT_TAG: &str = "dcd_taproot_pubkey_gen"; -pub const MAKER_FILLER_ASSET_ID_TAG: &str = "filler_asset_id"; -pub const MAKER_GRANTOR_COLLATERAL_ASSET_ID_TAG: &str = "grantor_collateral_asset_id"; -pub const MAKER_GRANTOR_SETTLEMENT_ASSET_ID_TAG: &str = "grantor_settlement_asset_id"; -pub const MAKER_SETTLEMENT_ASSET_ID_TAG: &str = "settlement_asset_id"; -pub const MAKER_COLLATERAL_ASSET_ID_TAG: &str = "collateral_asset_id"; -pub const MAKER_FUND_TX_ID_TAG: &str = "maker_fund_tx_id"; - -pub struct MakerOrderKind; -pub struct TakerReplyOrderKind; -pub struct MakerReplyOrderKind; -pub struct MergeReplyOrderKind; - -impl CustomKind for MakerOrderKind { - const ORDER_KIND_NUMBER: u16 = 9901; -} - -impl CustomKind for TakerReplyOrderKind { - const ORDER_KIND_NUMBER: u16 = 9902; -} - -impl CustomKind for MakerReplyOrderKind { - const ORDER_KIND_NUMBER: u16 = 9903; -} - -impl CustomKind for MergeReplyOrderKind { - const ORDER_KIND_NUMBER: u16 = 9904; -} - -#[derive(Debug)] -pub struct MakerOrderEvent { - pub event_id: EventId, - pub time: chrono::DateTime, - pub dcd_arguments: DCDArguments, - pub dcd_taproot_pubkey_gen: String, - pub filler_asset_id: AssetId, - pub grantor_collateral_asset_id: AssetId, - pub grantor_settlement_asset_id: AssetId, - pub settlement_asset_id: AssetId, - pub collateral_asset_id: AssetId, - pub maker_fund_tx_id: Txid, -} - -#[derive(Debug, Clone)] -pub enum ReplyOption { - TakerFund { - tx_id: Txid, - }, - MakerTerminationCollateral { - tx_id: Txid, - }, - MakerTerminationSettlement { - tx_id: Txid, - }, - MakerSettlement { - tx_id: Txid, - }, - TakerTerminationEarly { - tx_id: Txid, - }, - TakerSettlement { - tx_id: Txid, - }, - Merge2 { - tx_id: Txid, - token_utxo_1: OutPoint, - token_utxo_2: OutPoint, - }, - Merge3 { - tx_id: Txid, - token_utxo_1: OutPoint, - token_utxo_2: OutPoint, - token_utxo_3: OutPoint, - }, - Merge4 { - tx_id: Txid, - token_utxo_1: OutPoint, - token_utxo_2: OutPoint, - token_utxo_3: OutPoint, - token_utxo_4: OutPoint, - }, -} - -#[derive(Debug)] -pub struct OrderReplyEvent { - pub event_id: EventId, - pub event_kind: Kind, - pub time: chrono::DateTime, - pub reply_option: ReplyOption, -} - -// New: brief display-ready summary of a maker order. -#[derive(Debug, Clone)] -pub struct MakerOrderSummary { - pub taproot_key_gen: String, - pub strike_price: u64, - pub principal: String, - pub incentive_basis_points: u64, - // changed: use Option> for taker funding window so zero means "missing" - pub taker_fund_start_time: Option>, - pub taker_fund_end_time: Option>, - pub settlement_height: u32, - pub oracle_short: String, - pub collateral_asset_id: String, - pub settlement_asset_id: String, - pub interest_collateral: String, - pub total_collateral: String, - pub interest_asset: String, - pub total_asset: String, - // new: event time for the order summary - pub time: chrono::DateTime, - // new: maker funding transaction id (short display) - pub maker_fund_tx_id: String, - // new: originating event id - pub event_id: EventId, -} - -impl fmt::Display for MakerOrderSummary { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let oracle_full = &self.oracle_short; - let oracle_short = if oracle_full.is_empty() { - "n/a" - } else if oracle_full.len() > 8 { - &oracle_full[..8] - } else { - oracle_full.as_str() - }; - - let taker_range = match (self.taker_fund_start_time.as_ref(), self.taker_fund_end_time.as_ref()) { - (None, None) => "n/a".to_string(), - (Some(s), Some(e)) => format!("({})..({})", s.to_rfc3339(), e.to_rfc3339()), - (Some(s), None) => format!("({})..n/a", s.to_rfc3339()), - (None, Some(e)) => format!("n/a..({})", e.to_rfc3339()), - }; - - write!(f, "[Maker Order - Summary]",)?; - writeln!(f, "\t\t event_id:\t{}", self.event_id)?; - writeln!(f, "\t\t time:\t{}", self.time)?; - writeln!( - f, - "\t\t taker_fund_[start..end]:\t({:?})..({:?})", - self.taker_fund_start_time, self.taker_fund_end_time - )?; - writeln!(f, "\t\t taproot_pubkey_gen:\t{}", self.taproot_key_gen)?; - writeln!(f, "\torder_params:")?; - writeln!(f, "\t\t strike_price:\t{}", self.strike_price)?; - writeln!(f, "\t\t principal:\t{}", self.principal)?; - writeln!(f, "\t\t incentive_bps:\t{}", self.incentive_basis_points)?; - writeln!(f, "\t\t settlement_height:\t{}", self.settlement_height)?; - writeln!(f, "\t\t taker_funding:\t{taker_range}")?; - writeln!(f, "\t\t settlement_height:\t{}", self.settlement_height)?; - writeln!(f, "\t\t oracle_pubkey:\t{oracle_short}")?; - writeln!(f, "\t assets:")?; - writeln!(f, "\t\t interest_collateral:\t{}", self.interest_collateral)?; - writeln!(f, "\t\t total_collateral:\t{}", self.total_collateral)?; - writeln!(f, "\t\t interest_asset:\t{}", self.interest_asset)?; - writeln!(f, "\t\t total_asset:\t{}", self.total_asset)?; - writeln!(f, "\t\t collateral_asset_id:\t{}", self.collateral_asset_id)?; - writeln!(f, "\t\t settlement_asset_id:\t{}", self.settlement_asset_id)?; - - writeln!(f, "\t maker_fund_tx_id:\t{}", self.maker_fund_tx_id)?; - - Ok(()) - } -} - -impl fmt::Display for MakerOrderEvent { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let event_full = self.event_id.to_string(); - let event_short = if event_full.len() > 8 { - &event_full[..8] - } else { - &event_full[..] - }; - - let time_str = self.time.to_rfc3339(); - - let oracle_full = &self.dcd_arguments.oracle_public_key; - let oracle_display = if oracle_full.is_empty() { - "n/a".to_string() - } else { - oracle_full.clone() - }; - - let taker_start = { - let ts = self.dcd_arguments.taker_funding_start_time; - if ts == 0 { - None - } else { - chrono::Utc.timestamp_opt(i64::from(ts), 0).single() - } - }; - let taker_end = { - let ts = self.dcd_arguments.taker_funding_end_time; - if ts == 0 { - None - } else { - chrono::Utc.timestamp_opt(i64::from(ts), 0).single() - } - }; - let taker_range = match (taker_start, taker_end) { - (None, None) => "n/a".to_string(), - (Some(s), Some(e)) => format!("{}..{}", s.to_rfc3339(), e.to_rfc3339()), - (Some(s), None) => format!("{}..n/a", s.to_rfc3339()), - (None, Some(e)) => format!("n/a..{}", e.to_rfc3339()), - }; - - // ratio-derived amounts (use "n/a" for zero) - let r = &self.dcd_arguments.ratio_args; - let principal = if r.principal_collateral_amount > 0 { - r.principal_collateral_amount.to_string() - } else { - "n/a".to_string() - }; - let interest_collateral = if r.interest_collateral_amount > 0 { - r.interest_collateral_amount.to_string() - } else { - "n/a".to_string() - }; - let total_collateral = if r.total_collateral_amount > 0 { - r.total_collateral_amount.to_string() - } else { - "n/a".to_string() - }; - let interest_asset = if r.interest_asset_amount > 0 { - r.interest_asset_amount.to_string() - } else { - "n/a".to_string() - }; - let total_asset = if r.total_asset_amount > 0 { - r.total_asset_amount.to_string() - } else { - "n/a".to_string() - }; - - let filler = format!("{}", self.filler_asset_id); - let grantor_collateral = format!("{}", self.grantor_collateral_asset_id); - let grantor_settlement = format!("{}", self.grantor_settlement_asset_id); - let settlement = format!("{}", self.settlement_asset_id); - let collateral = format!("{}", self.collateral_asset_id); - let maker_tx = self.maker_fund_tx_id.to_string(); - - writeln!(f, "[Maker Order - Detail]\n\tevent_id={event_short}\ttime={time_str}",)?; - writeln!(f, "\t dcd_taproot_pubkey_gen:\t{}", self.dcd_taproot_pubkey_gen)?; - writeln!(f, "\t maker_fund_tx_id:\t{maker_tx}",)?; - writeln!(f, "\tdcd_arguments:")?; - writeln!(f, "\t\t strike_price:\t{}", self.dcd_arguments.strike_price)?; - writeln!(f, "\t\t incentive_bps:\t{}", self.dcd_arguments.incentive_basis_points)?; - writeln!(f, "\t\t taker_funding:\t{taker_range}",)?; - writeln!(f, "\t\t settlement_height:\t{}", self.dcd_arguments.settlement_height)?; - writeln!(f, "\t\t oracle_pubkey:\t{oracle_display}",)?; - writeln!(f, "\t\t ratio.principal_collateral:\t{principal}",)?; - writeln!(f, "\t\t ratio.interest_collateral:\t{interest_collateral}",)?; - writeln!(f, "\t\t ratio.total_collateral:\t{total_collateral}",)?; - writeln!(f, "\t\t ratio.interest_asset:\t{interest_asset}",)?; - writeln!(f, "\t\t ratio.total_asset:\t{total_asset}",)?; - - writeln!(f, "\tassets:")?; - writeln!(f, "\t\t filler_asset_id:\t{filler}")?; - writeln!(f, "\t\t grantor_collateral_asset_id:\t{grantor_collateral}")?; - writeln!(f, "\t\t grantor_settlement_asset_id:\t{grantor_settlement}")?; - writeln!(f, "\t\t settlement_asset_id:\t{settlement}")?; - writeln!(f, "\t\t collateral_asset_id:\t{collateral}")?; - - writeln!( - f, - "\n\tfull_dcd_arguments_debug:\n\t{}", - format_args!("{:#?}", self.dcd_arguments) - )?; - - Ok(()) - } -} - -impl MakerOrderEvent { - #[must_use] - pub fn summary(&self) -> MakerOrderSummary { - let oracle_full = &self.dcd_arguments.oracle_public_key; - let oracle_short = if oracle_full.is_empty() { - "n/a".to_string() - } else if oracle_full.len() > 8 { - oracle_full[..8].to_string() - } else { - oracle_full.clone() - }; - - let principal = match &self.dcd_arguments.ratio_args { - r if r.principal_collateral_amount > 0 => r.principal_collateral_amount.to_string(), - _ => "n/a".to_string(), - }; - - let (interest_collateral, total_collateral, interest_asset, total_asset) = { - let r = &self.dcd_arguments.ratio_args; - ( - if r.interest_collateral_amount > 0 { - r.interest_collateral_amount.to_string() - } else { - "n/a".to_string() - }, - if r.total_collateral_amount > 0 { - r.total_collateral_amount.to_string() - } else { - "n/a".to_string() - }, - if r.interest_asset_amount > 0 { - r.interest_asset_amount.to_string() - } else { - "n/a".to_string() - }, - if r.total_asset_amount > 0 { - r.total_asset_amount.to_string() - } else { - "n/a".to_string() - }, - ) - }; - - let collateral_id = format!("{}", self.collateral_asset_id); - let settlement_id = format!("{}", self.settlement_asset_id); - - MakerOrderSummary { - taproot_key_gen: self.dcd_taproot_pubkey_gen.clone(), - strike_price: self.dcd_arguments.strike_price, - principal, - incentive_basis_points: self.dcd_arguments.incentive_basis_points, - taker_fund_start_time: { - let ts = self.dcd_arguments.taker_funding_start_time; - if ts == 0 { - None - } else { - chrono::Utc.timestamp_opt(i64::from(ts), 0).single() - } - }, - taker_fund_end_time: { - let ts = self.dcd_arguments.taker_funding_end_time; - if ts == 0 { - None - } else { - chrono::Utc.timestamp_opt(i64::from(ts), 0).single() - } - }, - settlement_height: self.dcd_arguments.settlement_height, - oracle_short, - collateral_asset_id: collateral_id, - settlement_asset_id: settlement_id, - interest_collateral, - total_collateral, - interest_asset, - total_asset, - time: self.time, - maker_fund_tx_id: self.maker_fund_tx_id.to_string(), - event_id: self.event_id, - } - } - - pub fn parse_event(event: &Event) -> Option { - event.verify().ok()?; - if event.kind != MakerOrderKind::get_kind() { - return None; - } - let time = timestamp_to_chrono_utc(event.created_at)?; - let dcd_arguments = { - let bytes = hex::decode(event.tags.get(0)?.content()?).ok()?; - let decoded: DCDArguments = bincode::decode_from_slice(&bytes, bincode::config::standard()).ok()?.0; - decoded - }; - let dcd_taproot_pubkey_gen = event.tags.get(1)?.content()?.to_string(); - let filler_asset_id = AssetId::from_str(event.tags.get(2)?.content()?).ok()?; - let grantor_collateral_asset_id = AssetId::from_str(event.tags.get(3)?.content()?).ok()?; - let grantor_settlement_asset_id = AssetId::from_str(event.tags.get(4)?.content()?).ok()?; - let settlement_asset_id = AssetId::from_str(event.tags.get(5)?.content()?).ok()?; - let collateral_asset_id = AssetId::from_str(event.tags.get(6)?.content()?).ok()?; - let maker_fund_tx_id = Txid::from_str(event.tags.get(7)?.content()?).ok()?; - - Some(MakerOrderEvent { - event_id: event.id, - time, - dcd_arguments, - dcd_taproot_pubkey_gen, - filler_asset_id, - grantor_collateral_asset_id, - grantor_settlement_asset_id, - settlement_asset_id, - collateral_asset_id, - maker_fund_tx_id, - }) - } - - /// Form a list of Nostr tags representing a maker order event. - /// - /// # Errors - /// - /// Returns `Err(crate::error::NostrRelayError::BincodeEncoding)` if serialization of - /// `DCDArguments` via `bincode` fails. The function returns a `crate::error::Result>` - /// to propagate that error to the caller. - pub fn form_tags( - tags: OrderPlaceEventTags, - tx_id: Txid, - client_pubkey: PublicKey, - ) -> crate::error::Result> { - let dcd_arguments = { - let x = bincode::encode_to_vec(&tags.dcd_arguments, bincode::config::standard()).map_err(|err| { - crate::error::NostrRelayError::BincodeEncoding { - err, - struct_to_encode: format!("DCDArgs: {:#?}", tags.dcd_arguments), - } - })?; - nostr::prelude::hex::encode(x) - }; - Ok(vec![ - Tag::public_key(client_pubkey), - // Tag::expiration(Timestamp::from(timestamp_now.as_u64() + MAKER_EXPIRATION_TIME)), - Tag::custom(TagKind::Custom(Cow::from(MAKER_DCD_ARG_TAG)), [dcd_arguments]), - Tag::custom( - TagKind::Custom(Cow::from(MAKER_DCD_TAPROOT_TAG)), - [tags.dcd_taproot_pubkey_gen], - ), - Tag::custom( - TagKind::Custom(Cow::from(MAKER_FILLER_ASSET_ID_TAG)), - [tags.filler_asset_id.to_string()], - ), - Tag::custom( - TagKind::Custom(Cow::from(MAKER_GRANTOR_COLLATERAL_ASSET_ID_TAG)), - [tags.grantor_collateral_asset_id.to_string()], - ), - Tag::custom( - TagKind::Custom(Cow::from(MAKER_GRANTOR_SETTLEMENT_ASSET_ID_TAG)), - [tags.grantor_settlement_asset_id.to_string()], - ), - Tag::custom( - TagKind::Custom(Cow::from(MAKER_SETTLEMENT_ASSET_ID_TAG)), - [tags.settlement_asset_id.to_string()], - ), - Tag::custom( - TagKind::Custom(Cow::from(MAKER_COLLATERAL_ASSET_ID_TAG)), - [tags.collateral_asset_id.to_string()], - ), - Tag::custom(TagKind::Custom(Cow::from(MAKER_FUND_TX_ID_TAG)), [tx_id.to_string()]), - ]) - } -} - -impl OrderReplyEvent { - pub fn parse_event(event: &Event) -> Option { - tracing::debug!("filtering event: {:?}", event); - event.verify().ok()?; - let time = timestamp_to_chrono_utc(event.created_at)?; - Some(OrderReplyEvent { - event_id: event.id, - event_kind: event.kind, - time, - reply_option: ReplyOption::parse_tags(&event.tags)?, - }) - } -} - -impl ReplyOption { - pub fn parse_tags(tags: &Tags) -> Option { - // Extract tx_id from custom tag - let tx_id = tags - .iter() - .find(|tag| matches!(tag.kind(), TagKind::Custom(s) if s.as_ref() == "tx_id")) - .and_then(|tag| tag.content()) - .and_then(|s| Txid::from_str(s).ok())?; - - // Extract reply_type from custom tag - let reply_type = tags - .iter() - .find(|tag| matches!(tag.kind(), TagKind::Custom(s) if s.as_ref() == "reply_type")) - .and_then(|tag| tag.content())?; - - // Helper to get OutPoint from a custom tag with given key - let get_outpoint = |key: &str| -> Option { - let s = tags - .iter() - .find(|tag| matches!(tag.kind(), TagKind::Custom(k) if k.as_ref() == key)) - .and_then(|tag| tag.content())?; - OutPoint::from_str(s).ok() - }; - - // Match reply_type to construct the appropriate variant - match reply_type { - "taker_fund" => Some(ReplyOption::TakerFund { tx_id }), - "maker_termination_collateral" => Some(ReplyOption::MakerTerminationCollateral { tx_id }), - "maker_termination_settlement" => Some(ReplyOption::MakerTerminationSettlement { tx_id }), - "maker_settlement" => Some(ReplyOption::MakerSettlement { tx_id }), - "taker_termination_early" => Some(ReplyOption::TakerTerminationEarly { tx_id }), - "taker_settlement" => Some(ReplyOption::TakerSettlement { tx_id }), - "tokens_merge2" => { - let token_utxo_1 = get_outpoint("token_utxo_1")?; - let token_utxo_2 = get_outpoint("token_utxo_2")?; - Some(ReplyOption::Merge2 { - tx_id, - token_utxo_1, - token_utxo_2, - }) - } - "tokens_merge3" => { - let token_utxo_1 = get_outpoint("token_utxo_1")?; - let token_utxo_2 = get_outpoint("token_utxo_2")?; - let token_utxo_3 = get_outpoint("token_utxo_3")?; - Some(ReplyOption::Merge3 { - tx_id, - token_utxo_1, - token_utxo_2, - token_utxo_3, - }) - } - "tokens_merge4" => { - let token_utxo_1 = get_outpoint("token_utxo_1")?; - let token_utxo_2 = get_outpoint("token_utxo_2")?; - let token_utxo_3 = get_outpoint("token_utxo_3")?; - let token_utxo_4 = get_outpoint("token_utxo_4")?; - Some(ReplyOption::Merge4 { - tx_id, - token_utxo_1, - token_utxo_2, - token_utxo_3, - token_utxo_4, - }) - } - _ => None, - } - } - - #[must_use] - pub fn get_kind(&self) -> Kind { - match self { - ReplyOption::TakerFund { .. } - | ReplyOption::TakerTerminationEarly { .. } - | ReplyOption::TakerSettlement { .. } => TakerReplyOrderKind::get_kind(), - ReplyOption::MakerTerminationCollateral { .. } - | ReplyOption::MakerTerminationSettlement { .. } - | ReplyOption::MakerSettlement { .. } => MakerReplyOrderKind::get_kind(), - ReplyOption::Merge2 { .. } | ReplyOption::Merge3 { .. } | ReplyOption::Merge4 { .. } => { - MergeReplyOrderKind::get_kind() - } - } - } - - #[must_use] - pub fn get_content(&self) -> String { - match self { - ReplyOption::TakerFund { .. } - | ReplyOption::TakerTerminationEarly { .. } - | ReplyOption::TakerSettlement { .. } => BLOCKSTREAM_TAKER_REPLY_CONTENT.to_string(), - ReplyOption::MakerTerminationCollateral { .. } - | ReplyOption::MakerTerminationSettlement { .. } - | ReplyOption::MakerSettlement { .. } => BLOCKSTREAM_MAKER_REPLY_CONTENT.to_string(), - ReplyOption::Merge2 { .. } => BLOCKSTREAM_MERGE2_REPLY_CONTENT.to_string(), - ReplyOption::Merge3 { .. } => BLOCKSTREAM_MERGE3_REPLY_CONTENT.to_string(), - ReplyOption::Merge4 { .. } => BLOCKSTREAM_MERGE4_REPLY_CONTENT.to_string(), - } - } - - #[allow(clippy::too_many_lines)] - #[must_use] - pub fn form_tags(&self, source_event_id: EventId, client_pubkey: PublicKey) -> Vec { - match self { - ReplyOption::TakerFund { tx_id } => { - vec![ - Tag::public_key(client_pubkey), - Tag::event(source_event_id), - Tag::custom(TagKind::Custom(Cow::from("tx_id")), [tx_id.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("reply_type")), ["taker_fund"]), - ] - } - ReplyOption::MakerTerminationCollateral { tx_id } => { - vec![ - Tag::public_key(client_pubkey), - Tag::event(source_event_id), - Tag::custom(TagKind::Custom(Cow::from("tx_id")), [tx_id.to_string()]), - Tag::custom( - TagKind::Custom(Cow::from("reply_type")), - ["maker_termination_collateral"], - ), - ] - } - ReplyOption::MakerTerminationSettlement { tx_id } => { - vec![ - Tag::public_key(client_pubkey), - Tag::event(source_event_id), - Tag::custom(TagKind::Custom(Cow::from("tx_id")), [tx_id.to_string()]), - Tag::custom( - TagKind::Custom(Cow::from("reply_type")), - ["maker_termination_settlement"], - ), - ] - } - ReplyOption::MakerSettlement { tx_id } => { - vec![ - Tag::public_key(client_pubkey), - Tag::event(source_event_id), - Tag::custom(TagKind::Custom(Cow::from("tx_id")), [tx_id.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("reply_type")), ["maker_settlement"]), - ] - } - ReplyOption::TakerTerminationEarly { tx_id } => { - vec![ - Tag::public_key(client_pubkey), - Tag::event(source_event_id), - Tag::custom(TagKind::Custom(Cow::from("tx_id")), [tx_id.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("reply_type")), ["taker_termination_early"]), - ] - } - ReplyOption::TakerSettlement { tx_id } => { - vec![ - Tag::public_key(client_pubkey), - Tag::event(source_event_id), - Tag::custom(TagKind::Custom(Cow::from("tx_id")), [tx_id.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("reply_type")), ["taker_settlement"]), - ] - } - ReplyOption::Merge2 { - tx_id, - token_utxo_1, - token_utxo_2, - } => { - vec![ - Tag::public_key(client_pubkey), - Tag::event(source_event_id), - Tag::custom(TagKind::Custom(Cow::from("tx_id")), [tx_id.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("reply_type")), ["tokens_merge2"]), - Tag::custom(TagKind::Custom(Cow::from("token_utxo_1")), [token_utxo_1.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("token_utxo_2")), [token_utxo_2.to_string()]), - ] - } - ReplyOption::Merge3 { - tx_id, - token_utxo_1, - token_utxo_2, - token_utxo_3, - } => { - vec![ - Tag::public_key(client_pubkey), - Tag::event(source_event_id), - Tag::custom(TagKind::Custom(Cow::from("tx_id")), [tx_id.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("reply_type")), ["tokens_merge3"]), - Tag::custom(TagKind::Custom(Cow::from("token_utxo_1")), [token_utxo_1.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("token_utxo_2")), [token_utxo_2.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("token_utxo_3")), [token_utxo_3.to_string()]), - ] - } - ReplyOption::Merge4 { - tx_id, - token_utxo_1, - token_utxo_2, - token_utxo_3, - token_utxo_4, - } => { - vec![ - Tag::public_key(client_pubkey), - Tag::event(source_event_id), - Tag::custom(TagKind::Custom(Cow::from("tx_id")), [tx_id.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("reply_type")), ["tokens_merge4"]), - Tag::custom(TagKind::Custom(Cow::from("token_utxo_1")), [token_utxo_1.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("token_utxo_2")), [token_utxo_2.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("token_utxo_3")), [token_utxo_3.to_string()]), - Tag::custom(TagKind::Custom(Cow::from("token_utxo_4")), [token_utxo_4.to_string()]), - ] - } - } - } -} diff --git a/crates/dex-nostr-relay/tests/test_order_placing.rs b/crates/dex-nostr-relay/tests/test_order_placing.rs deleted file mode 100644 index a725a8e..0000000 --- a/crates/dex-nostr-relay/tests/test_order_placing.rs +++ /dev/null @@ -1,164 +0,0 @@ -mod utils; - -mod tests { - use crate::utils::{DEFAULT_CLIENT_TIMEOUT, DEFAULT_RELAY_LIST, TEST_LOGGER}; - use std::str::FromStr; - use std::time::Duration; - - use dex_nostr_relay::relay_client::ClientConfig; - use dex_nostr_relay::relay_processor::{ListOrdersEventFilter, OrderPlaceEventTags, RelayProcessor}; - use dex_nostr_relay::types::{CustomKind, MakerOrderKind, ReplyOption}; - use nostr::{Keys, ToBech32}; - use simplicity::elements::OutPoint; - use simplicityhl::elements::Txid; - - use tracing::{info, instrument}; - - #[ignore] - #[instrument] - #[tokio::test] - async fn test_wss_metadata() -> anyhow::Result<()> { - let _ = dotenvy::dotenv(); - let _guard = &*TEST_LOGGER; - - let key_maker = Keys::generate(); - info!( - "=== Maker pubkey: {}, privatekey: {}", - key_maker.public_key.to_bech32()?, - key_maker.secret_key().to_bech32()? - ); - let relay_processor_maker = RelayProcessor::try_from_config( - DEFAULT_RELAY_LIST, - Some(key_maker.clone()), - ClientConfig { - timeout: Duration::from_secs(DEFAULT_CLIENT_TIMEOUT), - }, - ) - .await?; - - let placed_order_event_id = relay_processor_maker - .place_order( - OrderPlaceEventTags::default(), - Txid::from_str("87a4c9b2060ff698d9072d5f95b3dde01efe0994f95c3cd6dd7348cb3a4e4e40").unwrap(), - ) - .await?; - info!("=== placed order event id: {}", placed_order_event_id); - let order = relay_processor_maker.get_event_by_id(placed_order_event_id).await?; - info!("=== placed order: {:#?}", order); - assert_eq!(order.len(), 1); - assert_eq!(order.first().unwrap().kind, MakerOrderKind::get_kind()); - - let key_taker = Keys::generate(); - let relay_processor_taker = RelayProcessor::try_from_config( - DEFAULT_RELAY_LIST, - Some(key_taker.clone()), - ClientConfig { - timeout: Duration::from_secs(DEFAULT_CLIENT_TIMEOUT), - }, - ) - .await?; - info!( - "=== Taker pubkey: {}, privatekey: {}", - key_taker.public_key.to_bech32()?, - key_taker.secret_key().to_bech32()? - ); - - // Common txid / outpoint used across reply options. - let tx_id = Txid::from_str("87a4c9b2060ff698d9072d5f95b3dde01efe0994f95c3cd6dd7348cb3a4e4e40")?; - let dummy_outpoint = OutPoint::from_str("87a4c9b2060ff698d9072d5f95b3dde01efe0994f95c3cd6dd7348cb3a4e4e40:0")?; - - // Send replies for all supported ReplyOption variants. - let reply_variants = vec![ - ReplyOption::TakerFund { tx_id }, - ReplyOption::MakerTerminationCollateral { tx_id }, - ReplyOption::MakerTerminationSettlement { tx_id }, - ReplyOption::MakerSettlement { tx_id }, - ReplyOption::TakerTerminationEarly { tx_id }, - ReplyOption::TakerSettlement { tx_id }, - ReplyOption::Merge2 { - tx_id, - token_utxo_1: dummy_outpoint, - token_utxo_2: dummy_outpoint, - }, - ReplyOption::Merge3 { - tx_id, - token_utxo_1: dummy_outpoint, - token_utxo_2: dummy_outpoint, - token_utxo_3: dummy_outpoint, - }, - ReplyOption::Merge4 { - tx_id, - token_utxo_1: dummy_outpoint, - token_utxo_2: dummy_outpoint, - token_utxo_3: dummy_outpoint, - token_utxo_4: dummy_outpoint, - }, - ]; - - for reply in &reply_variants { - let reply_event_id = relay_processor_taker - .reply_order(placed_order_event_id, reply.clone()) - .await?; - info!( - "=== order reply event id for {:?}: {}", - reply.get_content(), - reply_event_id - ); - } - - let order_replies = relay_processor_maker.get_order_replies(placed_order_event_id).await?; - info!( - "=== order replies, amount: {}, orders: {:#?}", - order_replies.len(), - order_replies, - ); - - // Inline comparison instead of an explicit loop. - let all_kinds_match = - order_replies - .iter() - .zip(reply_variants.iter()) - .enumerate() - .all(|(idx, (reply_event, expected_option))| { - if reply_event.event_kind != expected_option.get_kind() { - eprintln!( - "reply kind mismatch at index {idx}: \ - got {:?}, expected {:?}", - reply_event.event_kind, - expected_option.get_kind() - ); - return false; - } - true - }); - - assert!( - all_kinds_match, - "not all reply events have the expected kind; see stderr for details" - ); - - // Also confirm the placed order can be found via list_orders as before. - let orders_listed = relay_processor_maker - .list_orders(ListOrdersEventFilter { - authors: None, - since: None, - until: None, - limit: None, - }) - .await?; - info!( - "=== orders listed, amount: {}, orders: {:#?}", - orders_listed.len(), - orders_listed - ); - assert!( - orders_listed - .iter() - .map(|x| x.event_id) - .collect::>() - .contains(&placed_order_event_id) - ); - - Ok(()) - } -} diff --git a/crates/dex-nostr-relay/tests/utils.rs b/crates/dex-nostr-relay/tests/utils.rs deleted file mode 100644 index 5c7d511..0000000 --- a/crates/dex-nostr-relay/tests/utils.rs +++ /dev/null @@ -1,8 +0,0 @@ -use std::sync::LazyLock; - -use global_utils::logger::{LoggerGuard, init_logger}; - -pub static TEST_LOGGER: LazyLock = LazyLock::new(init_logger); - -pub const DEFAULT_RELAY_LIST: [&str; 1] = ["wss://relay.damus.io"]; -pub const DEFAULT_CLIENT_TIMEOUT: u64 = 10; diff --git a/crates/global-utils/Cargo.toml b/crates/global-utils/Cargo.toml deleted file mode 100644 index 6d32490..0000000 --- a/crates/global-utils/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "global-utils" -description = "Simplicity dex utils" -license = "MIT OR Apache-2.0" -publish = false - -version.workspace = true -edition.workspace = true -rust-version.workspace = true -authors.workspace = true -readme.workspace = true - -[dependencies] -thiserror = { workspace = true } -tracing = { workspace = true } -tracing-appender = { workspace = true } -tracing-subscriber = { workspace = true } \ No newline at end of file diff --git a/crates/global-utils/src/lib.rs b/crates/global-utils/src/lib.rs deleted file mode 100644 index 1f00764..0000000 --- a/crates/global-utils/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] - -pub mod logger; diff --git a/crates/options-relay/Cargo.toml b/crates/options-relay/Cargo.toml new file mode 100644 index 0000000..6c2f103 --- /dev/null +++ b/crates/options-relay/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "options-relay" +description = "NOSTR Relay library for Simplicity Options trading" +license = "MIT OR Apache-2.0" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true + +[dependencies] +nostr = { version = "0.44.2" } +nostr-sdk = { version = "0.44.1" } + +thiserror = "2" + +contracts = { workspace = true } +simplicityhl-core = { workspace = true } +simplicityhl = { workspace = true } + +tracing = { workspace = true } + +[dev-dependencies] +anyhow = "1" +hex = "0.4" + diff --git a/crates/options-relay/README.md b/crates/options-relay/README.md new file mode 100644 index 0000000..6ee702d --- /dev/null +++ b/crates/options-relay/README.md @@ -0,0 +1,19 @@ +# options-relay + +NOSTR relay library for Simplicity Options trading on Liquid Network. + +## Features + +- Stream and fetch option creation events +- Stream and fetch swap (atomic swap with change) events +- Track action completions (exercise, claim, expiry) +- Event signature verification +- TaprootPubkeyGen validation on parse + +## To be Done + +- [ ] Extended filters for event queries: + - `since` / `until` - Filter by time range to find active (non-expired) contracts + - `limit` - Pagination for large result sets + - `authors` - Filter events by creator's public key ("show only my options/swaps") + diff --git a/crates/options-relay/src/client.rs b/crates/options-relay/src/client.rs new file mode 100644 index 0000000..6b24c13 --- /dev/null +++ b/crates/options-relay/src/client.rs @@ -0,0 +1,6 @@ +mod read_only; +mod publishing; + +pub use read_only::ReadOnlyClient; +pub use publishing::PublishingClient; + diff --git a/crates/options-relay/src/client/publishing.rs b/crates/options-relay/src/client/publishing.rs new file mode 100644 index 0000000..aa207c3 --- /dev/null +++ b/crates/options-relay/src/client/publishing.rs @@ -0,0 +1,148 @@ +use crate::config::RelayConfig; +use crate::error::{ParseError, RelayError}; +use crate::events::{ActionCompletedEvent, OptionCreatedEvent, SwapCreatedEvent}; + +use std::sync::Arc; + +use nostr::prelude::*; +use nostr_sdk::prelude::Events; +use simplicityhl::elements::AddressParams; +use tracing::instrument; + +use super::ReadOnlyClient; + +#[derive(Debug, Clone)] +pub struct PublishingClient { + reader: ReadOnlyClient, +} + +impl PublishingClient { + #[instrument(skip_all, level = "debug", err)] + pub async fn connect(config: RelayConfig, signer: impl IntoNostrSigner) -> Result { + let mut reader = ReadOnlyClient::connect(config).await?; + + reader.set_signer(signer).await; + + Ok(Self { reader }) + } + + #[instrument(skip(self), level = "debug")] + pub async fn signer(&self) -> Result, RelayError> { + Ok(self.reader.inner_client().signer().await?) + } + + #[instrument(skip(self), level = "debug")] + pub async fn public_key(&self) -> Result { + Ok(self.reader.inner_client().signer().await?.get_public_key().await?) + } + + #[instrument(skip(self, event), level = "debug")] + pub async fn publish_event(&self, event: &Event) -> Result { + tracing::debug!(event_id = %event.id, "Publishing event to all relays"); + + let output = self.reader.inner_client().send_event(event).await?; + + tracing::debug!( + event_id = %output.val, + success_count = output.success.len(), + failed_count = output.failed.len(), + "Event published" + ); + + Ok(output.val) + } + + #[instrument(skip(self, builder), level = "debug")] + pub async fn publish(&self, builder: EventBuilder) -> Result { + tracing::debug!("Building and publishing event"); + + let output = self.reader.inner_client().send_event_builder(builder).await?; + + tracing::debug!( + event_id = %output.val, + success_count = output.success.len(), + failed_count = output.failed.len(), + "Event published" + ); + + Ok(output.val) + } + + pub async fn publish_option_created( + &self, + event: &OptionCreatedEvent, + ) -> Result { + let pubkey = self.public_key().await?; + let builder = event.to_event_builder(pubkey)?; + self.publish(builder).await + } + + pub async fn publish_swap_created( + &self, + event: &SwapCreatedEvent, + ) -> Result { + let pubkey = self.public_key().await?; + let builder = event.to_event_builder(pubkey)?; + self.publish(builder).await + } + + pub async fn publish_action_completed( + &self, + event: &ActionCompletedEvent, + ) -> Result { + let pubkey = self.public_key().await?; + let builder = event.to_event_builder(pubkey); + self.publish(builder).await + } + + #[instrument(skip(self), level = "debug")] + pub async fn fetch_events(&self, filter: Filter) -> Result { + self.reader.fetch_events(filter).await + } + + pub async fn fetch_options( + &self, + params: &'static AddressParams, + ) -> Result>, RelayError> { + self.reader.fetch_options(params).await + } + + pub async fn fetch_swaps( + &self, + params: &'static AddressParams, + ) -> Result>, RelayError> { + self.reader.fetch_swaps(params).await + } + + pub async fn fetch_actions_for_event( + &self, + original_event_id: EventId, + ) -> Result>, RelayError> { + self.reader.fetch_actions_for_event(original_event_id).await + } + + #[instrument(skip(self), level = "debug")] + pub async fn subscribe(&self, filter: Filter) -> Result { + self.reader.subscribe(filter).await + } + + #[instrument(skip(self), level = "debug")] + pub async fn unsubscribe(&self, subscription_id: &SubscriptionId) { + self.reader.unsubscribe(subscription_id).await; + } + + #[instrument(skip(self), level = "debug")] + pub async fn disconnect(&self) { + self.reader.disconnect().await; + } + + #[must_use] + pub fn config(&self) -> &RelayConfig { + self.reader.config() + } + + #[must_use] + pub fn as_reader(&self) -> &ReadOnlyClient { + &self.reader + } +} diff --git a/crates/options-relay/src/client/read_only.rs b/crates/options-relay/src/client/read_only.rs new file mode 100644 index 0000000..d1eac45 --- /dev/null +++ b/crates/options-relay/src/client/read_only.rs @@ -0,0 +1,112 @@ +use crate::config::RelayConfig; +use crate::error::{ParseError, RelayError}; +use crate::events::{ + filters, ActionCompletedEvent, OptionCreatedEvent, SwapCreatedEvent, +}; + +use nostr::prelude::*; +use nostr_sdk::prelude::Events; +use nostr_sdk::Client; +use simplicityhl::elements::AddressParams; +use tracing::instrument; + +#[derive(Debug, Clone)] +pub struct ReadOnlyClient { + client: Client, + config: RelayConfig, +} + +impl ReadOnlyClient { + #[instrument(skip_all, level = "debug", err)] + pub async fn connect(config: RelayConfig) -> Result { + tracing::debug!( + primary = %config.primary_relay(), + backup_count = config.all_relays().len() - 1, + "Connecting to NOSTR relays" + ); + + let client = Client::default(); + + for url in config.all_relays() { + let relay_url = Url::parse(url)?; + + client + .add_relay(relay_url) + .await?; + } + + client.connect().await; + + Ok(Self { client, config }) + } + + #[instrument(skip(self), level = "debug")] + pub async fn fetch_events(&self, filter: Filter) -> Result { + tracing::debug!(?filter, "Fetching events"); + + Ok(self + .client + .fetch_combined_events(filter, self.config.timeout()) + .await?) + } + + pub async fn fetch_options( + &self, + params: &'static AddressParams, + ) -> Result>, RelayError> { + let events = self.fetch_events(filters::option_created()).await?; + Ok(events.iter().map(|e| OptionCreatedEvent::from_event(e, params)).collect()) + } + + pub async fn fetch_swaps( + &self, + params: &'static AddressParams, + ) -> Result>, RelayError> { + let events = self.fetch_events(filters::swap_created()).await?; + Ok(events.iter().map(|e| SwapCreatedEvent::from_event(e, params)).collect()) + } + + pub async fn fetch_actions_for_event( + &self, + original_event_id: EventId, + ) -> Result>, RelayError> { + let events = self.fetch_events(filters::action_completed_for_event(original_event_id)).await?; + Ok(events.iter().map(ActionCompletedEvent::from_event).collect()) + } + + #[instrument(skip(self), level = "debug")] + pub async fn subscribe(&self, filter: Filter) -> Result { + tracing::debug!(?filter, "Subscribing to events"); + + Ok(self.client.subscribe(filter, None).await?.val) + } + + #[instrument(skip(self), level = "debug")] + pub async fn unsubscribe(&self, subscription_id: &SubscriptionId) { + tracing::debug!(%subscription_id, "Unsubscribing"); + + self.client.unsubscribe(subscription_id).await; + } + + #[instrument(skip(self), level = "debug")] + pub async fn disconnect(&self) { + tracing::debug!("Disconnecting from all relays"); + + self.client.disconnect().await; + } + + #[must_use] + pub fn config(&self) -> &RelayConfig { + &self.config + } + + pub(crate) fn inner_client(&self) -> &Client { + &self.client + } + + pub(crate) async fn set_signer(&mut self, signer: impl IntoNostrSigner) { + self.client.automatic_authentication(true); + + self.client.set_signer(signer).await; + } +} diff --git a/crates/options-relay/src/config.rs b/crates/options-relay/src/config.rs new file mode 100644 index 0000000..932b8fc --- /dev/null +++ b/crates/options-relay/src/config.rs @@ -0,0 +1,112 @@ +use std::time::Duration; + +#[derive(Debug, Clone)] +pub struct RelayConfig { + primary_relay: String, + backup_relays: Vec, + timeout: Duration, + retry_count: u32, +} + +impl RelayConfig { + pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); + pub const DEFAULT_RETRY_COUNT: u32 = 3; + + #[must_use] + pub fn new(primary_relay: impl Into) -> Self { + Self { + primary_relay: primary_relay.into(), + backup_relays: Vec::new(), + timeout: Self::DEFAULT_TIMEOUT, + retry_count: Self::DEFAULT_RETRY_COUNT, + } + } + + #[must_use] + pub fn add_backup_relay(mut self, relay_url: impl Into) -> Self { + self.backup_relays.push(relay_url.into()); + self + } + + #[must_use] + pub fn add_backup_relays( + mut self, + relay_urls: impl IntoIterator>, + ) -> Self { + self.backup_relays + .extend(relay_urls.into_iter().map(Into::into)); + self + } + + #[must_use] + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + #[must_use] + pub fn with_retry_count(mut self, count: u32) -> Self { + self.retry_count = count; + self + } + + #[must_use] + pub fn primary_relay(&self) -> &str { + &self.primary_relay + } + + #[must_use] + pub fn all_relays(&self) -> Vec<&str> { + std::iter::once(self.primary_relay.as_str()) + .chain(self.backup_relays.iter().map(String::as_str)) + .collect() + } + + #[must_use] + pub fn timeout(&self) -> Duration { + self.timeout + } + + #[must_use] + pub fn retry_count(&self) -> u32 { + self.retry_count + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_config_new() { + let config = RelayConfig::new("wss://relay.example.com"); + + assert_eq!(config.primary_relay(), "wss://relay.example.com"); + assert_eq!(config.all_relays().len(), 1); + assert_eq!(config.timeout(), RelayConfig::DEFAULT_TIMEOUT); + assert_eq!(config.retry_count(), RelayConfig::DEFAULT_RETRY_COUNT); + } + + #[test] + fn test_config_with_backup_relays() { + let config = RelayConfig::new("wss://primary.example.com") + .add_backup_relay("wss://backup1.example.com") + .add_backup_relay("wss://backup2.example.com"); + + let all = config.all_relays(); + assert_eq!(all.len(), 3); + assert_eq!(all[0], "wss://primary.example.com"); + assert_eq!(all[1], "wss://backup1.example.com"); + assert_eq!(all[2], "wss://backup2.example.com"); + } + + #[test] + fn test_config_with_custom_settings() { + let config = RelayConfig::new("wss://relay.example.com") + .with_timeout(Duration::from_secs(60)) + .with_retry_count(5); + + assert_eq!(config.timeout(), Duration::from_secs(60)); + assert_eq!(config.retry_count(), 5); + } +} diff --git a/crates/options-relay/src/error.rs b/crates/options-relay/src/error.rs new file mode 100644 index 0000000..9ec0279 --- /dev/null +++ b/crates/options-relay/src/error.rs @@ -0,0 +1,54 @@ +use contracts::error::TaprootPubkeyGenError; + +use nostr::prelude::url; +use nostr::SignerError; + +use simplicityhl::elements::bitcoin::blockdata::transaction::ParseOutPointError; +use simplicityhl_core::EncodingError; + +#[derive(thiserror::Error, Debug)] +pub enum RelayError { + #[error("Invalid relay URL")] + InvalidRelayUrl(#[from] url::ParseError), + + #[error("No relays configured")] + NoRelaysConfigured, + + #[error("Signer error")] + Signer(#[from] SignerError), + + #[error("Nostr client error")] + NostrClient(#[from] nostr_sdk::client::Error), + + #[error("No events found")] + NoEventsFound, + + /// Triggered when encoding contract arguments (e.g., `OptionsArguments`, `SwapWithChangeArguments`) + /// to hex/bincode format for NOSTR event tags fails. + #[error("Encoding error")] + Encoding(#[from] EncodingError), +} + +#[derive(thiserror::Error, Debug)] +pub enum ParseError { + #[error("Event verification failed")] + EventVerification(#[from] nostr::event::Error), + + #[error("Invalid event kind")] + InvalidKind, + + #[error("Missing required tag: {0}")] + MissingTag(&'static str), + + #[error("Invalid action type")] + InvalidAction, + + #[error("Invalid outpoint")] + InvalidOutpoint(#[from] ParseOutPointError), + + #[error("Decoding error")] + Decoding(#[from] EncodingError), + + #[error("Taproot verification failed")] + TaprootVerification(#[from] TaprootPubkeyGenError), +} diff --git a/crates/options-relay/src/events/action_completed.rs b/crates/options-relay/src/events/action_completed.rs new file mode 100644 index 0000000..f61d2a7 --- /dev/null +++ b/crates/options-relay/src/events/action_completed.rs @@ -0,0 +1,179 @@ +use crate::error::ParseError; +use crate::events::kinds::{ + ACTION_COMPLETED, ACTION_OPTION_EXERCISED, ACTION_OPTION_EXPIRED, ACTION_SETTLEMENT_CLAIMED, + ACTION_SWAP_EXERCISED, TAG_ACTION, TAG_OUTPOINT, +}; + +use std::str::FromStr; + +use nostr::{Event, EventBuilder, EventId, PublicKey, Tag, TagKind, Timestamp}; +use simplicityhl::elements::OutPoint; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ActionType { + SwapExercised, + OptionExercised, + SettlementClaimed, + OptionExpired, +} + +impl ActionType { + #[must_use] + pub fn as_str(&self) -> &'static str { + match self { + Self::SwapExercised => ACTION_SWAP_EXERCISED, + Self::OptionExercised => ACTION_OPTION_EXERCISED, + Self::SettlementClaimed => ACTION_SETTLEMENT_CLAIMED, + Self::OptionExpired => ACTION_OPTION_EXPIRED, + } + } +} + +impl FromStr for ActionType { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + ACTION_SWAP_EXERCISED => Ok(Self::SwapExercised), + ACTION_OPTION_EXERCISED => Ok(Self::OptionExercised), + ACTION_SETTLEMENT_CLAIMED => Ok(Self::SettlementClaimed), + ACTION_OPTION_EXPIRED => Ok(Self::OptionExpired), + _ => Err(()), + } + } +} + +#[derive(Debug, Clone)] +pub struct ActionCompletedEvent { + pub event_id: EventId, + pub pubkey: PublicKey, + pub created_at: Timestamp, + pub original_event_id: EventId, + pub action: ActionType, + pub outpoint: OutPoint, +} + +impl ActionCompletedEvent { + #[must_use] + pub fn new(original_event_id: EventId, action: ActionType, outpoint: OutPoint) -> Self { + Self { + event_id: EventId::all_zeros(), + pubkey: PublicKey::from_slice(&[1; 32]).unwrap(), + created_at: Timestamp::now(), + original_event_id, + action, + outpoint, + } + } + + #[must_use] + pub fn to_event_builder(&self, creator_pubkey: PublicKey) -> EventBuilder { + EventBuilder::new(ACTION_COMPLETED, "") + .tag(Tag::public_key(creator_pubkey)) + .tag(Tag::event(self.original_event_id)) + .tag(Tag::custom( + TagKind::custom(TAG_ACTION), + [self.action.as_str()], + )) + .tag(Tag::custom( + TagKind::custom(TAG_OUTPOINT), + [self.outpoint.to_string()], + )) + } + + pub fn from_event(event: &Event) -> Result { + event.verify()?; + + if event.kind != ACTION_COMPLETED { + return Err(ParseError::InvalidKind); + } + + let original_event_id = event + .tags + .iter() + .find(|t| t.kind() == TagKind::e()) + .and_then(|t| t.content()) + .and_then(|s| EventId::from_hex(s).ok()) + .ok_or(ParseError::MissingTag("e"))?; + + let action_str = event + .tags + .iter() + .find(|t| matches!(t.kind(), TagKind::Custom(s) if s.as_ref() == TAG_ACTION)) + .and_then(|t| t.content()) + .ok_or(ParseError::MissingTag(TAG_ACTION))?; + + let action: ActionType = action_str + .parse() + .map_err(|()| ParseError::InvalidAction)?; + + let outpoint_str = event + .tags + .iter() + .find(|t| matches!(t.kind(), TagKind::Custom(s) if s.as_ref() == TAG_OUTPOINT)) + .and_then(|t| t.content()) + .ok_or(ParseError::MissingTag(TAG_OUTPOINT))?; + + let outpoint: OutPoint = outpoint_str.parse()?; + + Ok(Self { + event_id: event.id, + pubkey: event.pubkey, + created_at: event.created_at, + original_event_id, + action, + outpoint, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nostr::{Keys, hashes::Hash}; + use simplicityhl::elements::Txid; + + fn dummy_outpoint() -> OutPoint { + OutPoint::new(Txid::all_zeros(), 0) + } + + #[test] + fn action_type_roundtrip() { + let actions = [ + ActionType::SwapExercised, + ActionType::OptionExercised, + ActionType::SettlementClaimed, + ActionType::OptionExpired, + ]; + + for action in actions { + let s = action.as_str(); + let parsed: ActionType = s.parse().expect("should parse"); + assert_eq!(action, parsed); + } + } + + #[test] + fn action_completed_event_builder_roundtrip() -> anyhow::Result<()> { + let keys = Keys::generate(); + let original_event_id = EventId::all_zeros(); + + let event = ActionCompletedEvent::new( + original_event_id, + ActionType::OptionExercised, + dummy_outpoint(), + ); + + let builder = event.to_event_builder(keys.public_key()); + let built_event = builder.sign_with_keys(&keys)?; + + let parsed = ActionCompletedEvent::from_event(&built_event)?; + + assert_eq!(parsed.original_event_id, original_event_id); + assert_eq!(parsed.action, ActionType::OptionExercised); + assert_eq!(parsed.outpoint, dummy_outpoint()); + + Ok(()) + } +} + diff --git a/crates/options-relay/src/events/filters.rs b/crates/options-relay/src/events/filters.rs new file mode 100644 index 0000000..856dd05 --- /dev/null +++ b/crates/options-relay/src/events/filters.rs @@ -0,0 +1,41 @@ +use nostr::Filter; + +use crate::events::kinds::{ACTION_COMPLETED, OPTION_CREATED, SWAP_CREATED}; + +#[must_use] +pub fn option_created() -> Filter { + Filter::new().kind(OPTION_CREATED) +} + +#[must_use] +pub fn option_created_by_pubkey(pubkey: nostr::PublicKey) -> Filter { + Filter::new().kind(OPTION_CREATED).author(pubkey) +} + +#[must_use] +pub fn swap_created() -> Filter { + Filter::new().kind(SWAP_CREATED) +} + +#[must_use] +pub fn swap_created_by_pubkey(pubkey: nostr::PublicKey) -> Filter { + Filter::new().kind(SWAP_CREATED).author(pubkey) +} + +#[must_use] +pub fn action_completed() -> Filter { + Filter::new().kind(ACTION_COMPLETED) +} + +#[must_use] +pub fn action_completed_for_event(original_event_id: nostr::EventId) -> Filter { + Filter::new() + .kind(ACTION_COMPLETED) + .event(original_event_id) +} + +#[must_use] +pub fn all_option_events() -> Filter { + Filter::new().kinds([OPTION_CREATED, SWAP_CREATED, ACTION_COMPLETED]) +} + diff --git a/crates/options-relay/src/events/kinds.rs b/crates/options-relay/src/events/kinds.rs new file mode 100644 index 0000000..87d5029 --- /dev/null +++ b/crates/options-relay/src/events/kinds.rs @@ -0,0 +1,19 @@ +use nostr::Kind; + +pub const OPTION_CREATED: Kind = Kind::Custom(9910); +pub const SWAP_CREATED: Kind = Kind::Custom(9911); +pub const ACTION_COMPLETED: Kind = Kind::Custom(9912); + +pub const TAG_OPTIONS_ARGS: &str = "options_args"; +pub const TAG_OPTIONS_UTXO: &str = "options_utxo"; +pub const TAG_SWAP_ARGS: &str = "swap_args"; +pub const TAG_SWAP_UTXO: &str = "swap_utxo"; +pub const TAG_TAPROOT_GEN: &str = "t"; +pub const TAG_ACTION: &str = "action"; +pub const TAG_OUTPOINT: &str = "outpoint"; + +pub const ACTION_SWAP_EXERCISED: &str = "swap_exercised"; +pub const ACTION_OPTION_EXERCISED: &str = "option_exercised"; +pub const ACTION_SETTLEMENT_CLAIMED: &str = "settlement_claimed"; +pub const ACTION_OPTION_EXPIRED: &str = "option_expired"; + diff --git a/crates/options-relay/src/events/mod.rs b/crates/options-relay/src/events/mod.rs new file mode 100644 index 0000000..b455d0c --- /dev/null +++ b/crates/options-relay/src/events/mod.rs @@ -0,0 +1,11 @@ +pub mod filters; +pub mod kinds; +mod option_created; +mod swap_created; +mod action_completed; + +pub use kinds::*; +pub use option_created::OptionCreatedEvent; +pub use swap_created::SwapCreatedEvent; +pub use action_completed::{ActionCompletedEvent, ActionType}; + diff --git a/crates/options-relay/src/events/option_created.rs b/crates/options-relay/src/events/option_created.rs new file mode 100644 index 0000000..c9c50de --- /dev/null +++ b/crates/options-relay/src/events/option_created.rs @@ -0,0 +1,157 @@ +use crate::error::{ParseError, RelayError}; +use crate::events::kinds::{OPTION_CREATED, TAG_OPTIONS_ARGS, TAG_OPTIONS_UTXO, TAG_TAPROOT_GEN}; + +use contracts::options::{OptionsArguments, get_options_address}; +use contracts::sdk::taproot_pubkey_gen::TaprootPubkeyGen; +use nostr::{Event, EventBuilder, EventId, PublicKey, Tag, TagKind, Timestamp}; +use simplicityhl::elements::{AddressParams, OutPoint}; +use simplicityhl_core::Encodable; + +#[derive(Debug, Clone)] +pub struct OptionCreatedEvent { + pub event_id: EventId, + pub pubkey: PublicKey, + pub created_at: Timestamp, + pub options_args: OptionsArguments, + pub utxo: OutPoint, + pub taproot_pubkey_gen: TaprootPubkeyGen, +} + +impl OptionCreatedEvent { + #[must_use] + pub fn new( + options_args: OptionsArguments, + utxo: OutPoint, + taproot_pubkey_gen: TaprootPubkeyGen, + ) -> Self { + Self { + event_id: EventId::all_zeros(), + pubkey: PublicKey::from_slice(&[1; 32]).unwrap(), + created_at: Timestamp::now(), + options_args, + utxo, + taproot_pubkey_gen, + } + } + + pub fn to_event_builder(&self, creator_pubkey: PublicKey) -> Result { + let args_hex = self.options_args.to_hex()?; + + Ok(EventBuilder::new(OPTION_CREATED, "") + .tag(Tag::public_key(creator_pubkey)) + .tag(Tag::custom( + TagKind::custom(TAG_OPTIONS_ARGS), + [args_hex], + )) + .tag(Tag::custom( + TagKind::custom(TAG_OPTIONS_UTXO), + [self.utxo.to_string()], + )) + .tag(Tag::custom( + TagKind::custom(TAG_TAPROOT_GEN), + [self.taproot_pubkey_gen.to_string()], + ))) + } + + pub fn from_event( + event: &Event, + params: &'static AddressParams, + ) -> Result { + event.verify()?; + + if event.kind != OPTION_CREATED { + return Err(ParseError::InvalidKind); + } + + let args_hex = event + .tags + .iter() + .find(|t| matches!(t.kind(), TagKind::Custom(s) if s.as_ref() == TAG_OPTIONS_ARGS)) + .and_then(|t| t.content()) + .ok_or(ParseError::MissingTag(TAG_OPTIONS_ARGS))?; + + let options_args = OptionsArguments::from_hex(args_hex)?; + + let utxo_str = event + .tags + .iter() + .find(|t| matches!(t.kind(), TagKind::Custom(s) if s.as_ref() == TAG_OPTIONS_UTXO)) + .and_then(|t| t.content()) + .ok_or(ParseError::MissingTag(TAG_OPTIONS_UTXO))?; + + let utxo: OutPoint = utxo_str.parse()?; + + let taproot_str = event + .tags + .iter() + .find(|t| matches!(t.kind(), TagKind::SingleLetter(l) if l.character == nostr::Alphabet::T)) + .and_then(|t| t.content()) + .ok_or(ParseError::MissingTag(TAG_TAPROOT_GEN))?; + + let taproot_pubkey_gen = + TaprootPubkeyGen::build_from_str(taproot_str, &options_args, params, &get_options_address)?; + + Ok(Self { + event_id: event.id, + pubkey: event.pubkey, + created_at: event.created_at, + options_args, + utxo, + taproot_pubkey_gen, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use nostr::{Keys, hashes::Hash}; + + use contracts::sdk::taproot_pubkey_gen::get_random_seed; + + use simplicityhl::elements::{AssetId, Txid}; + use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_TEST_ASSET_ID_STR}; + + 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 { + start_time: 10, + expiry_time: 50, + collateral_per_contract: 100, + settlement_per_contract: 1000, + collateral_asset_id: LIQUID_TESTNET_BITCOIN_ASSET.into_inner().0, + settlement_asset_id: settlement_asset_id.into_inner().0, + option_token_entropy: get_random_seed(), + grantor_token_entropy: get_random_seed(), + }; + + let taproot_pubkey_gen = + TaprootPubkeyGen::from(&args, &AddressParams::LIQUID_TESTNET, &get_options_address)?; + + Ok((args, taproot_pubkey_gen)) + } + + #[test] + fn option_created_event_roundtrip() -> anyhow::Result<()> { + let keys = Keys::generate(); + let (args, taproot_pubkey_gen) = get_mocked_data()?; + let utxo = OutPoint::new(Txid::all_zeros(), 0); + + let event = OptionCreatedEvent::new(args.clone(), utxo, taproot_pubkey_gen.clone()); + + let builder = event.to_event_builder(keys.public_key())?; + let built_event = builder.sign_with_keys(&keys)?; + + let parsed = OptionCreatedEvent::from_event(&built_event, &AddressParams::LIQUID_TESTNET)?; + + assert_eq!(parsed.options_args, args); + assert_eq!(parsed.utxo, utxo); + assert_eq!(parsed.taproot_pubkey_gen.to_string(), taproot_pubkey_gen.to_string()); + + Ok(()) + } +} + diff --git a/crates/options-relay/src/events/swap_created.rs b/crates/options-relay/src/events/swap_created.rs new file mode 100644 index 0000000..07321d7 --- /dev/null +++ b/crates/options-relay/src/events/swap_created.rs @@ -0,0 +1,159 @@ +use crate::error::{ParseError, RelayError}; +use crate::events::kinds::{SWAP_CREATED, TAG_SWAP_ARGS, TAG_SWAP_UTXO, TAG_TAPROOT_GEN}; + +use contracts::sdk::taproot_pubkey_gen::TaprootPubkeyGen; +use contracts::swap_with_change::{SwapWithChangeArguments, get_swap_with_change_address}; +use nostr::{Event, EventBuilder, EventId, PublicKey, Tag, TagKind, Timestamp}; +use simplicityhl::elements::{AddressParams, OutPoint}; +use simplicityhl_core::Encodable; + +#[derive(Debug, Clone)] +pub struct SwapCreatedEvent { + pub event_id: EventId, + pub pubkey: PublicKey, + pub created_at: Timestamp, + pub swap_args: SwapWithChangeArguments, + pub utxo: OutPoint, + pub taproot_pubkey_gen: TaprootPubkeyGen, +} + +impl SwapCreatedEvent { + #[must_use] + pub fn new( + swap_args: SwapWithChangeArguments, + utxo: OutPoint, + taproot_pubkey_gen: TaprootPubkeyGen, + ) -> Self { + Self { + event_id: EventId::all_zeros(), + pubkey: PublicKey::from_slice(&[1; 32]).unwrap(), + created_at: Timestamp::now(), + swap_args, + utxo, + taproot_pubkey_gen, + } + } + + pub fn to_event_builder(&self, creator_pubkey: PublicKey) -> Result { + let args_hex = self.swap_args.to_hex()?; + + Ok(EventBuilder::new(SWAP_CREATED, "") + .tag(Tag::public_key(creator_pubkey)) + .tag(Tag::custom( + TagKind::custom(TAG_SWAP_ARGS), + [args_hex], + )) + .tag(Tag::custom( + TagKind::custom(TAG_SWAP_UTXO), + [self.utxo.to_string()], + )) + .tag(Tag::custom( + TagKind::custom(TAG_TAPROOT_GEN), + [self.taproot_pubkey_gen.to_string()], + ))) + } + + pub fn from_event( + event: &Event, + params: &'static AddressParams, + ) -> Result { + event.verify()?; + + if event.kind != SWAP_CREATED { + return Err(ParseError::InvalidKind); + } + + let args_hex = event + .tags + .iter() + .find(|t| matches!(t.kind(), TagKind::Custom(s) if s.as_ref() == TAG_SWAP_ARGS)) + .and_then(|t| t.content()) + .ok_or(ParseError::MissingTag(TAG_SWAP_ARGS))?; + + let swap_args = SwapWithChangeArguments::from_hex(args_hex)?; + + let utxo_str = event + .tags + .iter() + .find(|t| matches!(t.kind(), TagKind::Custom(s) if s.as_ref() == TAG_SWAP_UTXO)) + .and_then(|t| t.content()) + .ok_or(ParseError::MissingTag(TAG_SWAP_UTXO))?; + + let utxo: OutPoint = utxo_str.parse()?; + + let taproot_str = event + .tags + .iter() + .find(|t| matches!(t.kind(), TagKind::SingleLetter(l) if l.character == nostr::Alphabet::T)) + .and_then(|t| t.content()) + .ok_or(ParseError::MissingTag(TAG_TAPROOT_GEN))?; + + let taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( + taproot_str, + &swap_args, + params, + &get_swap_with_change_address, + )?; + + Ok(Self { + event_id: event.id, + pubkey: event.pubkey, + created_at: event.created_at, + swap_args, + utxo, + taproot_pubkey_gen, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use nostr::{Keys, hashes::Hash}; + + use simplicityhl::elements::{AssetId, Txid}; + use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_TEST_ASSET_ID_STR}; + + fn get_mocked_data() -> anyhow::Result<(SwapWithChangeArguments, TaprootPubkeyGen)> { + let settlement_asset_id = + AssetId::from_slice(&hex::decode(LIQUID_TESTNET_TEST_ASSET_ID_STR)?)?; + + let args = SwapWithChangeArguments { + collateral_asset_id: LIQUID_TESTNET_BITCOIN_ASSET.into_inner().0, + settlement_asset_id: settlement_asset_id.into_inner().0, + collateral_per_contract: 1000, + expiry_time: 50, + user_pubkey: [1; 32], + }; + + let taproot_pubkey_gen = TaprootPubkeyGen::from( + &args, + &AddressParams::LIQUID_TESTNET, + &get_swap_with_change_address, + )?; + + Ok((args, taproot_pubkey_gen)) + } + + #[test] + fn swap_created_event_roundtrip() -> anyhow::Result<()> { + let keys = Keys::generate(); + let (args, taproot_pubkey_gen) = get_mocked_data()?; + let utxo = OutPoint::new(Txid::all_zeros(), 0); + + let event = SwapCreatedEvent::new(args.clone(), utxo, taproot_pubkey_gen.clone()); + + let builder = event.to_event_builder(keys.public_key())?; + let built_event = builder.sign_with_keys(&keys)?; + + let parsed = SwapCreatedEvent::from_event(&built_event, &AddressParams::LIQUID_TESTNET)?; + + assert_eq!(parsed.swap_args, args); + assert_eq!(parsed.utxo, utxo); + assert_eq!(parsed.taproot_pubkey_gen.to_string(), taproot_pubkey_gen.to_string()); + + Ok(()) + } +} + diff --git a/crates/options-relay/src/lib.rs b/crates/options-relay/src/lib.rs new file mode 100644 index 0000000..2250005 --- /dev/null +++ b/crates/options-relay/src/lib.rs @@ -0,0 +1,15 @@ +#![warn(clippy::all, clippy::pedantic)] +#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] + +pub mod client; +pub mod config; +pub mod error; +pub mod events; + +pub use client::{PublishingClient, ReadOnlyClient}; +pub use config::RelayConfig; +pub use error::{ParseError, RelayError}; +pub use events::{ + ActionCompletedEvent, ActionType, OptionCreatedEvent, SwapCreatedEvent, + ACTION_COMPLETED, OPTION_CREATED, SWAP_CREATED, +}; From 3e57c2d3cf3389f25e0cbbd4051e20af4febf67f Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Wed, 24 Dec 2025 13:48:43 +0200 Subject: [PATCH 5/5] Add coin-store and signer and basic-cli --- .gitignore | 3 + Cargo.toml | 5 +- crates/cli-client/Cargo.toml | 33 +- crates/cli-client/README.md | 0 crates/cli-client/src/bin/main.rs | 10 +- crates/cli-client/src/cli/commands.rs | 174 +++ crates/cli-client/src/cli/dex.rs | 78 -- crates/cli-client/src/cli/helper.rs | 144 --- crates/cli-client/src/cli/maker.rs | 146 --- crates/cli-client/src/cli/mod.rs | 179 ++- crates/cli-client/src/cli/processor.rs | 1129 ----------------- crates/cli-client/src/cli/taker.rs | 87 -- crates/cli-client/src/common/config.rs | 128 -- crates/cli-client/src/common/keys.rs | 98 -- crates/cli-client/src/common/mod.rs | 9 - crates/cli-client/src/common/settings.rs | 45 - crates/cli-client/src/common/store.rs | 393 ------ crates/cli-client/src/common/types.rs | 80 -- crates/cli-client/src/common/utils.rs | 32 - crates/cli-client/src/config.rs | 134 ++ .../src/contract_handlers/address.rs | 14 - .../src/contract_handlers/common.rs | 41 - .../src/contract_handlers/faucet.rs | 158 --- .../src/contract_handlers/maker_funding.rs | 202 --- .../src/contract_handlers/maker_init.rs | 184 --- .../src/contract_handlers/maker_settlement.rs | 150 --- .../maker_termination_collateral.rs | 136 -- .../maker_termination_settlement.rs | 137 -- .../src/contract_handlers/merge_tokens.rs | 319 ----- .../cli-client/src/contract_handlers/mod.rs | 14 - .../src/contract_handlers/oracle_signature.rs | 20 - .../src/contract_handlers/split_utxo.rs | 44 - .../taker_early_termination.rs | 137 -- .../src/contract_handlers/taker_funding.rs | 131 -- .../src/contract_handlers/taker_settlement.rs | 147 --- crates/cli-client/src/error.rs | 67 +- crates/cli-client/src/lib.rs | 6 +- crates/cli-client/src/logger.rs | 43 - crates/cli-client/src/wallet.rs | 52 + crates/coin-store/Cargo.toml | 19 + crates/coin-store/migrations/001_initial.sql | 24 + crates/coin-store/src/entry.rs | 43 + crates/coin-store/src/error.rs | 40 + crates/coin-store/src/filter.rs | 47 + crates/coin-store/src/lib.rs | 12 + crates/coin-store/src/store.rs | 633 +++++++++ crates/options-relay/src/client.rs | 5 +- crates/options-relay/src/client/publishing.rs | 21 +- crates/options-relay/src/client/read_only.rs | 28 +- crates/options-relay/src/config.rs | 16 +- crates/options-relay/src/error.rs | 2 +- .../src/events/action_completed.rs | 27 +- crates/options-relay/src/events/filters.rs | 5 +- crates/options-relay/src/events/kinds.rs | 1 - crates/options-relay/src/events/mod.rs | 5 +- .../src/events/option_created.rs | 34 +- .../options-relay/src/events/swap_created.rs | 40 +- crates/options-relay/src/lib.rs | 4 +- crates/signer/Cargo.toml | 17 + crates/signer/src/lib.rs | 76 ++ 60 files changed, 1555 insertions(+), 4453 deletions(-) delete mode 100644 crates/cli-client/README.md create mode 100644 crates/cli-client/src/cli/commands.rs delete mode 100644 crates/cli-client/src/cli/dex.rs delete mode 100644 crates/cli-client/src/cli/helper.rs delete mode 100644 crates/cli-client/src/cli/maker.rs delete mode 100644 crates/cli-client/src/cli/processor.rs delete mode 100644 crates/cli-client/src/cli/taker.rs delete mode 100644 crates/cli-client/src/common/config.rs delete mode 100644 crates/cli-client/src/common/keys.rs delete mode 100644 crates/cli-client/src/common/mod.rs delete mode 100644 crates/cli-client/src/common/settings.rs delete mode 100644 crates/cli-client/src/common/store.rs delete mode 100644 crates/cli-client/src/common/types.rs delete mode 100644 crates/cli-client/src/common/utils.rs create mode 100644 crates/cli-client/src/config.rs delete mode 100644 crates/cli-client/src/contract_handlers/address.rs delete mode 100644 crates/cli-client/src/contract_handlers/common.rs delete mode 100644 crates/cli-client/src/contract_handlers/faucet.rs delete mode 100644 crates/cli-client/src/contract_handlers/maker_funding.rs delete mode 100644 crates/cli-client/src/contract_handlers/maker_init.rs delete mode 100644 crates/cli-client/src/contract_handlers/maker_settlement.rs delete mode 100644 crates/cli-client/src/contract_handlers/maker_termination_collateral.rs delete mode 100644 crates/cli-client/src/contract_handlers/maker_termination_settlement.rs delete mode 100644 crates/cli-client/src/contract_handlers/merge_tokens.rs delete mode 100644 crates/cli-client/src/contract_handlers/mod.rs delete mode 100644 crates/cli-client/src/contract_handlers/oracle_signature.rs delete mode 100644 crates/cli-client/src/contract_handlers/split_utxo.rs delete mode 100644 crates/cli-client/src/contract_handlers/taker_early_termination.rs delete mode 100644 crates/cli-client/src/contract_handlers/taker_funding.rs delete mode 100644 crates/cli-client/src/contract_handlers/taker_settlement.rs delete mode 100644 crates/cli-client/src/logger.rs create mode 100644 crates/cli-client/src/wallet.rs create mode 100644 crates/coin-store/Cargo.toml create mode 100644 crates/coin-store/migrations/001_initial.sql create mode 100644 crates/coin-store/src/entry.rs create mode 100644 crates/coin-store/src/error.rs create mode 100644 crates/coin-store/src/filter.rs create mode 100644 crates/coin-store/src/lib.rs create mode 100644 crates/coin-store/src/store.rs create mode 100644 crates/signer/Cargo.toml create mode 100644 crates/signer/src/lib.rs diff --git a/.gitignore b/.gitignore index 2b6fa34..39dcb9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.data +config.toml + # Generated by Cargo # will have compiled files and executables debug/ diff --git a/Cargo.toml b/Cargo.toml index bc4016d..00ddaad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,8 @@ anyhow = { version = "1.0.100" } tracing = { version = "0.1.41" } -contracts = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "35449ea", package = "contracts" } -simplicityhl-core = { version = "0.2.1", features = ["encoding"] } +contracts = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "6a53bf7", package = "contracts" } +cli-helper = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "6a53bf7", package = "cli" } +simplicityhl-core = { version = "0.3.0", features = ["encoding"] } simplicityhl = { version = "0.4.0" } diff --git a/crates/cli-client/Cargo.toml b/crates/cli-client/Cargo.toml index 23b85b3..24c4fe3 100644 --- a/crates/cli-client/Cargo.toml +++ b/crates/cli-client/Cargo.toml @@ -1,16 +1,39 @@ [package] name = "cli-client" -version = "0.1.0" -edition = "2024" -description = "" +description = "CLI client for Simplicity Options trading on Liquid" license = "MIT OR Apache-2.0" -readme = "README.md" -publish = false +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true [[bin]] name = "simplicity-dex" path = "src/bin/main.rs" [dependencies] +signer = { path = "../signer" } +coin-store = { path = "../coin-store" } +options-relay = { path = "../options-relay" } + +contracts = { workspace = true } +cli-helper = { workspace = true } +simplicityhl = { workspace = true } +simplicityhl-core = { workspace = true } + +clap = { version = "4", features = ["derive", "env"] } + +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } + +thiserror = "2" +anyhow = { workspace = true } + +tracing = { workspace = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +serde = { version = "1", features = ["derive"] } +toml = "0.8" +hex = "0.4" +dotenvy = "0.15" diff --git a/crates/cli-client/README.md b/crates/cli-client/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/crates/cli-client/src/bin/main.rs b/crates/cli-client/src/bin/main.rs index 3b1b501..b7f6da6 100644 --- a/crates/cli-client/src/bin/main.rs +++ b/crates/cli-client/src/bin/main.rs @@ -1,18 +1,14 @@ #![warn(clippy::all, clippy::pedantic)] use clap::Parser; - -use dex_cli::logger::init_logger; - -use dex_cli::cli::Cli; +use cli_client::cli::Cli; #[tokio::main] -#[tracing::instrument] async fn main() -> anyhow::Result<()> { let _ = dotenvy::dotenv(); - let _logger_guard = init_logger(); + let cli = Cli::parse(); - Cli::parse().process().await?; + cli.run().await?; Ok(()) } diff --git a/crates/cli-client/src/cli/commands.rs b/crates/cli-client/src/cli/commands.rs new file mode 100644 index 0000000..50f976f --- /dev/null +++ b/crates/cli-client/src/cli/commands.rs @@ -0,0 +1,174 @@ +use clap::Subcommand; +use simplicityhl::elements::OutPoint; + +#[derive(Debug, Subcommand)] +pub enum Command { + /// Maker commands for creating and managing options + Maker { + #[command(subcommand)] + command: MakerCommand, + }, + + /// Taker commands for participating in options + Taker { + #[command(subcommand)] + command: TakerCommand, + }, + + /// Helper utilities + Helper { + #[command(subcommand)] + command: HelperCommand, + }, + + /// Show current configuration + Config, + + /// Basic transaction commands (transfer, split, issue) + Basic { + #[command(subcommand)] + command: BasicCommand, + }, +} + +#[derive(Debug, Subcommand)] +pub enum MakerCommand { + /// Create a new options contract + Create, + + /// Fund an options contract with collateral + Fund, + + /// Exercise an option before expiration + Exercise, + + /// Cancel an unfilled option and retrieve collateral + Cancel, + + /// List your created options + List, +} + +#[derive(Debug, Subcommand)] +pub enum TakerCommand { + /// Browse available options + Browse, + + /// Take an option by purchasing grantor token + Take, + + /// Claim settlement after expiration + Claim, + + /// List your positions + List, +} + +#[derive(Debug, Subcommand)] +pub enum HelperCommand { + /// Show wallet details + Address, + + /// Initialize the wallet database + Init, + + /// Show wallet balance + Balance, + + /// List UTXOs + Utxos, + + /// Import a UTXO into the wallet + Import { + /// Outpoint (txid:vout) + #[arg(long, short = 'o')] + outpoint: OutPoint, + + /// Blinding key (hex, optional for confidential outputs) + #[arg(long, short = 'b')] + blinding_key: Option, + }, +} + +#[derive(Debug, Subcommand)] +pub enum BasicCommand { + /// Transfer LBTC to a recipient + TransferNative { + /// Recipient address + #[arg(long)] + to: String, + /// Amount to send in satoshis + #[arg(long)] + amount: u64, + /// Fee amount in satoshis + #[arg(long)] + fee: u64, + /// Broadcast transaction + #[arg(long)] + broadcast: bool, + }, + + /// Split LBTC into multiple UTXOs + SplitNative { + /// Number of parts to split into + #[arg(long)] + parts: u64, + /// Fee amount in satoshis + #[arg(long)] + fee: u64, + /// Broadcast transaction + #[arg(long)] + broadcast: bool, + }, + + /// Transfer an asset to a recipient + TransferAsset { + /// Asset ID (hex) + #[arg(long)] + asset: String, + /// Recipient address + #[arg(long)] + to: String, + /// Amount to send + #[arg(long)] + amount: u64, + /// Fee amount in satoshis + #[arg(long)] + fee: u64, + /// Broadcast transaction + #[arg(long)] + broadcast: bool, + }, + + /// Issue a new asset + IssueAsset { + /// Asset name (local reference) + #[arg(long)] + name: String, + /// Amount to issue + #[arg(long)] + amount: u64, + /// Fee amount in satoshis + #[arg(long)] + fee: u64, + /// Broadcast transaction + #[arg(long)] + broadcast: bool, + }, + + /// Reissue an existing asset + ReissueAsset { + /// Asset name (local reference) + #[arg(long)] + name: String, + /// Amount to reissue + #[arg(long)] + amount: u64, + /// Fee amount in satoshis + #[arg(long)] + fee: u64, + /// Broadcast transaction + #[arg(long)] + broadcast: bool, + }, +} diff --git a/crates/cli-client/src/cli/dex.rs b/crates/cli-client/src/cli/dex.rs deleted file mode 100644 index b2c4d4f..0000000 --- a/crates/cli-client/src/cli/dex.rs +++ /dev/null @@ -1,78 +0,0 @@ -use clap::Subcommand; -use nostr::{EventId, Timestamp}; - -#[derive(Debug, Subcommand)] -pub enum DexCommands { - #[command(about = "Fetch replies for a specific order event from Nostr relays [no authentication required]")] - GetOrderReplies { - #[arg(short = 'i', long)] - event_id: EventId, - }, - #[command(about = "List all currently available orders discovered on Nostr relays [no authentication required]")] - ListOrders { - /// Comma-separated list of author public keys to filter by (hex or npub) - #[arg(long = "authors", value_delimiter = ',')] - authors: Option>, - #[command(subcommand)] - time_to_filter: Option, - /// Maximum number of orders to return - #[arg(long = "limit")] - limit: Option, - }, - #[command(about = "Import order parameters from a Maker order Nostr event [no authentication required]")] - ImportParams { - #[arg(short = 'i', long)] - event_id: EventId, - }, - #[command(about = "Fetch an arbitrary Nostr event by its ID [no authentication required]")] - GetEventsById { - #[arg(short = 'i', long)] - event_id: EventId, - }, - #[command(about = "Fetch a single order by its event ID from Nostr relays [no authentication required]")] - GetOrderById { - #[arg(short = 'i', long)] - event_id: EventId, - }, -} - -#[derive(Debug, Subcommand)] -pub enum TimeOptionArgs { - /// Filter events from the last duration (e.g., "1h", "30m", "7d") - #[command(name = "duration")] - Duration { - #[arg(value_name = "DURATION")] - value: humantime::Duration, - }, - /// Filter events by timestamp range - #[command(name = "timestamp")] - Timestamp { - /// Filter events since this Unix timestamp - #[arg(long = "since")] - since: Option, - /// Filter events until this Unix timestamp - #[arg(long = "until")] - until: Option, - }, -} - -impl TimeOptionArgs { - #[must_use] - pub fn compute_since(&self) -> Option { - match self { - TimeOptionArgs::Duration { value } => { - let now = Timestamp::now().as_u64(); - Some(now.saturating_sub(value.as_secs())) - } - TimeOptionArgs::Timestamp { since, .. } => *since, - } - } - - #[must_use] - pub fn compute_until(&self) -> Option { - match self { - TimeOptionArgs::Duration { .. } => None, - TimeOptionArgs::Timestamp { until, .. } => *until, - } - } -} diff --git a/crates/cli-client/src/cli/helper.rs b/crates/cli-client/src/cli/helper.rs deleted file mode 100644 index 0076b58..0000000 --- a/crates/cli-client/src/cli/helper.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::cli::CommonOrderOptions; -use clap::Subcommand; -use nostr::EventId; -use simplicity::elements::OutPoint; - -#[derive(Debug, Subcommand)] -pub enum HelperCommands { - #[command(about = "Display a test P2PK address for the given account index [testing only]")] - Address { - /// Account index to use for change address - #[arg(long = "account-index", default_value_t = 0)] - account_index: u32, - }, - #[command(about = "Issue new test tokens backed by LBTC for settlement testing [testing only]")] - Faucet { - /// 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 - #[arg(long = "asset-name")] - asset_name: String, - /// Amount to issue of the asset in its satoshi units - #[arg(long = "issue-sats", default_value_t = 1000000000000000)] - issue_amount: u64, - /// Miner fee in satoshis (LBTC). A separate fee output is added. - #[arg(long = "fee-sats", default_value_t = 500)] - fee_amount: u64, - #[command(flatten)] - common_options: CommonOrderOptions, - }, - #[command(about = "Reissue additional units of an already created test asset [testing only]")] - MintTokens { - /// 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 - #[arg(long = "asset-name")] - asset_name: String, - /// Amount to reissue of the asset in its satoshi units - #[arg(long = "reissue-sats", default_value_t = 1000000000000000)] - reissue_amount: u64, - /// Miner fee in satoshis (LBTC). A separate fee output is added. - #[arg(long = "fee-sats", default_value_t = 500)] - fee_amount: u64, - #[command(flatten)] - common_options: CommonOrderOptions, - }, - #[command(about = "Split a single LBTC UTXO into three outputs of equal value [testing only]")] - SplitNativeThree { - #[arg(long = "split-amount")] - split_amount: u64, - /// Fee utxo - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - #[arg(long = "fee-amount", default_value_t = 150)] - fee_amount: u64, - #[command(flatten)] - common_options: CommonOrderOptions, - }, - #[command(about = "Sign oracle message with keypair [testing only]")] - OracleSignature { - /// Price at current block height - #[arg(long = "price-at-current-block-height")] - price_at_current_block_height: u64, - /// Settlement height - #[arg(long = "settlement-height")] - settlement_height: u32, - /// Oracle account index to derive key from `SEED_HEX` - #[arg(long = "oracle-account-index")] - oracle_account_index: u32, - }, - #[command(about = "Merge 2 token UTXOs into 1")] - MergeTokens2 { - /// First token UTXO - #[arg(long = "token-utxo-1")] - token_utxo_1: OutPoint, - /// Second token UTXO - #[arg(long = "token-utxo-2")] - token_utxo_2: OutPoint, - /// Fee UTXO - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Miner fee in satoshis (LBTC) for the final settlement transaction - #[arg(long = "fee-amount", default_value_t = 1500)] - fee_amount: u64, - /// `EventId` of the Maker\'s original order event on Nostr - #[arg(short = 'i', long)] - maker_order_event_id: EventId, - #[command(flatten)] - common_options: CommonOrderOptions, - }, - #[command(about = "Merge 3 token UTXOs into 1")] - MergeTokens3 { - /// First token UTXO - #[arg(long = "token-utxo-1")] - token_utxo_1: OutPoint, - /// Second token UTXO - #[arg(long = "token-utxo-2")] - token_utxo_2: OutPoint, - /// Third token UTXO - #[arg(long = "token-utxo-3")] - token_utxo_3: OutPoint, - /// Fee UTXO - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Miner fee in satoshis (LBTC) for the final settlement transaction - #[arg(long = "fee-amount", default_value_t = 1500)] - fee_amount: u64, - /// `EventId` of the Maker\'s original order event on Nostr - #[arg(short = 'i', long)] - maker_order_event_id: EventId, - #[command(flatten)] - common_options: CommonOrderOptions, - }, - #[command(about = "Merge 4 token UTXOs into 1")] - MergeTokens4 { - /// First token UTXO - #[arg(long = "token-utxo-1")] - token_utxo_1: OutPoint, - /// Second token UTXO - #[arg(long = "token-utxo-2")] - token_utxo_2: OutPoint, - /// Third token UTXO - #[arg(long = "token-utxo-3")] - token_utxo_3: OutPoint, - /// Fourth token UTXO - #[arg(long = "token-utxo-4")] - token_utxo_4: OutPoint, - /// Fee UTXO - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Miner fee in satoshis (LBTC) for the final settlement transaction - #[arg(long = "fee-amount", default_value_t = 1500)] - fee_amount: u64, - /// `EventId` of the Maker\'s original order event on Nostr - #[arg(short = 'i', long)] - maker_order_event_id: EventId, - #[command(flatten)] - common_options: CommonOrderOptions, - }, -} diff --git a/crates/cli-client/src/cli/maker.rs b/crates/cli-client/src/cli/maker.rs deleted file mode 100644 index 2712687..0000000 --- a/crates/cli-client/src/cli/maker.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::cli::CommonOrderOptions; -use crate::common::InitOrderArgs; -use clap::Subcommand; -use nostr::EventId; -use simplicity::elements::OutPoint; - -#[derive(Debug, Subcommand)] -pub enum MakerCommands { - #[command( - about = "Mint three DCD token types and create an initial Maker offer for a Taker", - long_about = "Mint three distinct DCD token types and initialize a Maker offer. \ - These tokens represent the Maker/Taker claims on collateral and settlement assets \ - and are used to manage the contract lifecycle (funding, early termination, settlement).", - name = "init" - )] - InitOrder { - /// LBTC UTXO used to fund issuance fees and the first DCD token - #[arg(long = "utxo-1")] - first_lbtc_utxo: OutPoint, - /// LBTC UTXO used to fund issuance fees and the second DCD token - #[arg(long = "utxo-2")] - second_lbtc_utxo: OutPoint, - /// LBTC UTXO used to fund issuance fees and the third DCD token - #[arg(long = "utxo-3")] - third_lbtc_utxo: OutPoint, - #[command(flatten)] - init_order_args: InitOrderArgs, - /// Miner fee in satoshis (LBTC) for the init order transaction - #[arg(long = "fee-amount", default_value_t = 1500)] - fee_amount: u64, - #[command(flatten)] - common_options: CommonOrderOptions, - }, - #[command( - about = "Fund a DCD offer by locking Maker tokens into the contract and publish the order on relays [authentication required]" - )] - Fund { - /// UTXO containing Maker filler tokens to be locked into the DCD contract - #[arg(long = "filler-utxo")] - filler_token_utxo: OutPoint, - /// UTXO containing Maker grantor collateral tokens to be locked or burned - #[arg(long = "grant-coll-utxo")] - grantor_collateral_token_utxo: OutPoint, - /// UTXO containing Maker grantor settlement tokens to be locked or burned - #[arg(long = "grant-settl-utxo")] - grantor_settlement_token_utxo: OutPoint, - /// UTXO providing the settlement asset (e.g. LBTC) for the DCD contract - #[arg(long = "settl-asset-utxo")] - settlement_asset_utxo: OutPoint, - /// UTXO used to pay miner fees for the Maker funding transaction - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Miner fee in satoshis (LBTC) for the Maker funding transaction - #[arg(long = "fee-amount", default_value_t = 1500)] - fee_amount: u64, - /// Taproot internal pubkey (hex) used to derive the contract output address - #[arg(long = "taproot-pubkey-gen")] - dcd_taproot_pubkey_gen: String, - #[command(flatten)] - common_options: CommonOrderOptions, - }, - #[command( - about = "Withdraw Maker collateral early by burning grantor collateral tokens (DCD early termination leg)" - )] - TerminationCollateral { - /// UTXO containing grantor collateral tokens to be burned for early termination - #[arg(long = "grantor-collateral-utxo")] - grantor_collateral_token_utxo: OutPoint, - /// UTXO containing the collateral asset (e.g. LBTC) to be withdrawn by the Maker - #[arg(long = "collateral-utxo")] - collateral_token_utxo: OutPoint, - /// UTXO used to pay miner fees for the early-termination collateral transaction - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Miner fee in satoshis (LBTC) for the early-termination collateral transaction - #[arg(long = "fee-amount", default_value_t = 1500)] - fee_amount: u64, - /// Amount of grantor collateral tokens (in satoshis) to burn for early termination - #[arg(long = "grantor-collateral-burn")] - grantor_collateral_amount_to_burn: u64, - /// `EventId` of the Maker\'s original order event on Nostr - #[arg(short = 'i', long)] - maker_order_event_id: EventId, - #[command(flatten)] - common_options: CommonOrderOptions, - }, - #[command( - about = "Withdraw Maker settlement asset early by burning grantor settlement tokens (DCD early termination leg)" - )] - TerminationSettlement { - /// UTXO providing the settlement asset (e.g. LBTC) to be withdrawn by the Maker - #[arg(long = "settlement-asset-utxo")] - settlement_asset_utxo: OutPoint, - /// UTXO containing grantor settlement tokens to be burned for early termination - #[arg(long = "grantor-settlement-utxo")] - grantor_settlement_token_utxo: OutPoint, - /// UTXO used to pay miner fees for the early-termination settlement transaction - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Miner fee in satoshis (LBTC) for the early-termination settlement transaction - #[arg(long = "fee-amount", default_value_t = 1500)] - fee_amount: u64, - /// Amount of grantor settlement tokens (in satoshis) to burn for early termination - #[arg(long = "grantor-settlement-amount-burn")] - grantor_settlement_amount_to_burn: u64, - /// `EventId` of the Maker\'s original order event on Nostr - #[arg(short = 'i', long)] - maker_order_event_id: EventId, - #[command(flatten)] - common_options: CommonOrderOptions, - }, - #[command( - about = "Settle the Maker side of the DCD at maturity using an oracle price to decide between collateral or settlement asset" - )] - Settlement { - /// UTXO containing grantor collateral tokens used in final settlement - #[arg(long = "grant-collateral-utxo")] - grantor_collateral_token_utxo: OutPoint, - /// UTXO containing grantor settlement tokens used in final settlement - #[arg(long = "grant-settlement-utxo")] - grantor_settlement_token_utxo: OutPoint, - /// UTXO providing the asset (collateral or settlement) paid out to the Maker at maturity - #[arg(long = "asset-utxo")] - asset_utxo: OutPoint, - /// UTXO used to pay miner fees for the final Maker settlement transaction - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Miner fee in satoshis (LBTC) for the final settlement transaction - #[arg(long = "fee-amount", default_value_t = 1500)] - fee_amount: u64, - /// Amount of grantor (settlement and collateral) tokens (in satoshis) to burn during settlement step - #[arg(long = "grantor-amount-burn")] - grantor_amount_to_burn: u64, - /// Oracle price at current block height used for settlement decision - #[arg(long = "price-now")] - price_at_current_block_height: u64, - /// Schnorr signature produced by the oracle over the published price - #[arg(long = "oracle-sign")] - oracle_signature: String, - /// `EventId` of the Maker\'s original order event on Nostr - #[arg(short = 'i', long)] - maker_order_event_id: EventId, - #[command(flatten)] - common_options: CommonOrderOptions, - }, -} diff --git a/crates/cli-client/src/cli/mod.rs b/crates/cli-client/src/cli/mod.rs index b060798..37d8544 100644 --- a/crates/cli-client/src/cli/mod.rs +++ b/crates/cli-client/src/cli/mod.rs @@ -1,10 +1,169 @@ -mod dex; -mod helper; -mod maker; -mod processor; -mod taker; - -pub use dex::*; -pub use maker::*; -pub use processor::*; -pub use taker::*; +mod commands; + +use std::path::PathBuf; + +use clap::Parser; + +use crate::config::{Config, default_config_path}; +use crate::error::Error; +use crate::wallet::Wallet; + +pub use commands::{Command, HelperCommand, MakerCommand, TakerCommand}; + +#[derive(Debug, Parser)] +#[command(name = "simplicity-dex")] +#[command(about = "CLI for Simplicity Options trading on Liquid")] +pub struct Cli { + #[arg(short, long, default_value_os_t = default_config_path(), env = "SIMPLICITY_DEX_CONFIG")] + pub config: PathBuf, + + #[arg(short, long, env = "SIMPLICITY_DEX_SEED")] + pub seed: Option, + + #[command(subcommand)] + pub command: Command, +} + +impl Cli { + #[must_use] + pub fn load_config(&self) -> Config { + Config::load_or_default(&self.config) + } + + fn parse_seed(&self) -> Result<[u8; 32], Error> { + let seed_hex = self + .seed + .as_ref() + .ok_or_else(|| Error::Config("Seed is required. Use --seed or SIMPLICITY_DEX_SEED".to_string()))?; + + let bytes = hex::decode(seed_hex).map_err(|e| Error::Config(format!("Invalid seed hex: {e}")))?; + + bytes + .try_into() + .map_err(|_| Error::Config("Seed must be exactly 32 bytes (64 hex chars)".to_string())) + } + + pub async fn run(&self) -> Result<(), Error> { + let config = self.load_config(); + + match &self.command { + Command::Basic { command: _ } => todo!(), + Command::Maker { command: _ } => todo!(), + Command::Taker { command: _ } => todo!(), + Command::Helper { command } => self.run_helper(config, command).await, + Command::Config => { + println!("{config:#?}"); + Ok(()) + } + } + } + + async fn run_helper(&self, config: Config, command: &HelperCommand) -> Result<(), Error> { + match command { + HelperCommand::Init => { + let seed = self.parse_seed()?; + let db_path = config.database_path(); + + std::fs::create_dir_all(&config.storage.data_dir)?; + Wallet::create(&seed, &db_path, config.address_params()).await?; + + println!("Wallet initialized at {}", db_path.display()); + Ok(()) + } + HelperCommand::Address => { + let seed = self.parse_seed()?; + let db_path = config.database_path(); + let wallet = Wallet::open(&seed, &db_path, config.address_params()).await?; + + wallet.signer().print_details()?; + + Ok(()) + } + HelperCommand::Balance => { + let seed = self.parse_seed()?; + let db_path = config.database_path(); + let wallet = Wallet::open(&seed, &db_path, config.address_params()).await?; + + let filter = coin_store::Filter::new() + .script_pubkey(wallet.signer().p2pk_address(config.address_params())?.script_pubkey()); + let results = wallet.store().query(&[filter]).await?; + + let mut balances: std::collections::HashMap = + std::collections::HashMap::new(); + + if let Some(coin_store::QueryResult::Found(entries)) = results.into_iter().next() { + for entry in entries { + let (asset, value) = match entry { + coin_store::UtxoEntry::Confidential { secrets, .. } => (secrets.asset, secrets.value), + coin_store::UtxoEntry::Explicit { txout, .. } => { + let asset = txout.asset.explicit().unwrap(); + let value = txout.value.explicit().unwrap(); + (asset, value) + } + }; + *balances.entry(asset).or_insert(0) += value; + } + } + + if balances.is_empty() { + println!("No UTXOs found"); + } else { + for (asset, value) in &balances { + println!("{asset}: {value}"); + } + } + Ok(()) + } + HelperCommand::Utxos => { + let seed = self.parse_seed()?; + let db_path = config.database_path(); + let wallet = Wallet::open(&seed, &db_path, config.address_params()).await?; + + let filter = coin_store::Filter::new(); + let results = wallet.store().query(&[filter]).await?; + + if let Some(coin_store::QueryResult::Found(entries)) = results.into_iter().next() { + for entry in &entries { + let outpoint = entry.outpoint(); + let (asset, value) = match entry { + coin_store::UtxoEntry::Confidential { secrets, .. } => (secrets.asset, secrets.value), + coin_store::UtxoEntry::Explicit { txout, .. } => { + let asset = txout.asset.explicit().unwrap(); + let value = txout.value.explicit().unwrap(); + (asset, value) + } + }; + println!("{outpoint} | {asset} | {value}"); + } + println!("Total: {} UTXOs", entries.len()); + } else { + println!("No UTXOs found"); + } + Ok(()) + } + HelperCommand::Import { outpoint, blinding_key } => { + let seed = self.parse_seed()?; + let db_path = config.database_path(); + let wallet = Wallet::open(&seed, &db_path, config.address_params()).await?; + + let txout = cli_helper::explorer::fetch_utxo(*outpoint).await?; + + let blinder = match blinding_key { + Some(key_hex) => { + let bytes: [u8; 32] = hex::decode(key_hex) + .map_err(|e| Error::Config(format!("Invalid blinding key hex: {e}")))? + .try_into() + .map_err(|_| Error::Config("Blinding key must be 32 bytes".to_string()))?; + Some(bytes) + } + None => None, + }; + + wallet.store().insert(*outpoint, txout, blinder).await?; + + println!("Imported {outpoint}"); + Ok(()) + } + } + } +} diff --git a/crates/cli-client/src/cli/processor.rs b/crates/cli-client/src/cli/processor.rs deleted file mode 100644 index b645cc8..0000000 --- a/crates/cli-client/src/cli/processor.rs +++ /dev/null @@ -1,1129 +0,0 @@ -use crate::cli::helper::HelperCommands; -use crate::cli::{DexCommands, MakerCommands, TakerCommands}; -use crate::common::config::AggregatedConfig; -use crate::common::{DEFAULT_CLIENT_TIMEOUT_SECS, InitOrderArgs, write_into_stdout}; -use crate::contract_handlers; -use clap::{Parser, Subcommand}; -use dex_nostr_relay::relay_client::ClientConfig; -use dex_nostr_relay::relay_processor::{ListOrdersEventFilter, RelayProcessor}; -use dex_nostr_relay::types::ReplyOption; -use elements::hex::ToHex; -use nostr::{EventId, Keys, RelayUrl, Timestamp}; -use simplicity::elements::OutPoint; -use std::path::PathBuf; -use std::time::Duration; -use tracing::instrument; - -pub(crate) const DEFAULT_CONFIG_PATH: &str = ".simplicity-dex.config.toml"; - -#[derive(Parser)] -pub struct Cli { - /// Private key used to authenticate and sign events on the Nostr relays (hex or bech32) - #[arg(short = 'k', long, env = "DEX_NOSTR_KEYPAIR")] - pub(crate) nostr_key: Option, - - /// List of Nostr relay URLs to connect to (e.g. ) - #[arg(short = 'r', long, value_delimiter = ',', env = "DEX_NOSTR_RELAYS")] - pub(crate) relays_list: Option>, - - /// Path to a config file containing the list of relays and(or) nostr keypair to use - #[arg(short = 'c', long, default_value = DEFAULT_CONFIG_PATH, env = "DEX_NOSTR_CONFIG_PATH")] - pub(crate) nostr_config_path: PathBuf, - - /// Command to execute - #[command(subcommand)] - command: Command, -} - -/// Common CLI options shared between maker/taker commands that build and (optionally) broadcast a tx. -#[derive(Debug, Clone, Copy, Parser)] -pub struct CommonOrderOptions { - /// Account index used to derive internal/change addresses from the wallet - #[arg(long = "account-index", default_value_t = 0)] - pub account_index: u32, - /// When set, the transaction would be only printed, otherwise it'd ve broadcasted the built transaction via Esplora - #[arg(long = "offline")] - pub is_offline: bool, -} - -#[derive(Debug, Subcommand)] -pub enum Command { - /// Maker-side commands for creating and managing DCD orders - #[command()] - Maker { - #[command(subcommand)] - action: MakerCommands, - }, - - /// Taker-side commands for funding and managing DCD positions - #[command()] - Taker { - #[command(subcommand)] - action: TakerCommands, - }, - /// Dex commands that is related with nostr and interaction with it - #[command()] - Dex { - #[command(subcommand)] - action: DexCommands, - }, - /// Helper commands for ease of testing use - #[command()] - Helpers { - #[command(subcommand)] - action: HelperCommands, - }, - /// Print the aggregated CLI and relay configuration - #[command()] - ShowConfig, -} - -#[derive(Debug, Clone)] -struct CliAppContext { - agg_config: AggregatedConfig, - relay_processor: RelayProcessor, -} - -struct MakerSettlementCliContext { - grantor_collateral_token_utxo: OutPoint, - grantor_settlement_token_utxo: OutPoint, - fee_utxo: OutPoint, - asset_utxo: OutPoint, - fee_amount: u64, - price_at_current_block_height: u64, - oracle_signature: String, - grantor_amount_to_burn: u64, - maker_order_event_id: EventId, -} - -struct MakerSettlementTerminationCliContext { - fee_utxo: OutPoint, - settlement_asset_utxo: OutPoint, - grantor_settlement_token_utxo: OutPoint, - fee_amount: u64, - grantor_settlement_amount_to_burn: u64, - maker_order_event_id: EventId, -} - -struct MakerCollateralTerminationCliContext { - grantor_collateral_token_utxo: OutPoint, - fee_utxo: OutPoint, - collateral_token_utxo: OutPoint, - fee_amount: u64, - grantor_collateral_amount_to_burn: u64, - maker_order_event_id: EventId, -} - -struct MakerFundCliContext { - filler_token_utxo: OutPoint, - grantor_collateral_token_utxo: OutPoint, - grantor_settlement_token_utxo: OutPoint, - settlement_asset_utxo: OutPoint, - fee_utxo: OutPoint, - fee_amount: u64, - dcd_taproot_pubkey_gen: String, -} - -struct MakerInitCliContext { - first_lbtc_utxo: OutPoint, - second_lbtc_utxo: OutPoint, - third_lbtc_utxo: OutPoint, - init_order_args: InitOrderArgs, - fee_amount: u64, -} - -struct MergeTokens2CliContext { - token_utxo_1: OutPoint, - token_utxo_2: OutPoint, - fee_utxo: OutPoint, - fee_amount: u64, - maker_order_event_id: EventId, -} - -struct MergeTokens3CliContext { - token_utxo_1: OutPoint, - token_utxo_2: OutPoint, - token_utxo_3: OutPoint, - fee_utxo: OutPoint, - fee_amount: u64, - maker_order_event_id: EventId, -} - -struct MergeTokens4CliContext { - token_utxo_1: OutPoint, - token_utxo_2: OutPoint, - token_utxo_3: OutPoint, - token_utxo_4: OutPoint, - fee_utxo: OutPoint, - fee_amount: u64, - maker_order_event_id: EventId, -} - -impl Cli { - /// Initialize aggregated CLI configuration from CLI args, config file and env. - /// - /// # Errors - /// - /// Returns an error if building or validating the aggregated configuration - /// (including loading the config file or environment overrides) fails. - pub fn init_config(&self) -> crate::error::Result { - AggregatedConfig::new(self) - } - - /// Initialize the relay processor using the provided relays and optional keypair. - /// - /// # Errors - /// - /// Returns an error if creating or configuring the underlying Nostr relay - /// client fails, or if connecting to the specified relays fails. - pub async fn init_relays( - &self, - relays: &[RelayUrl], - keypair: Option, - ) -> crate::error::Result { - let relay_processor = RelayProcessor::try_from_config( - relays, - keypair, - ClientConfig { - timeout: Duration::from_secs(DEFAULT_CLIENT_TIMEOUT_SECS), - }, - ) - .await?; - Ok(relay_processor) - } - - /// Process the CLI command and execute the selected action. - /// - /// # Errors - /// - /// Returns an error if: - /// - Loading or validating the aggregated configuration fails. - /// - Initializing or communicating with Nostr relays fails. - /// - Any underlying contract handler (maker, taker, or helper) fails. - /// - Writing the resulting message to stdout fails. - #[instrument(skip(self))] - pub async fn process(self) -> crate::error::Result<()> { - let agg_config = self.init_config()?; - - let relay_processor = self - .init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()) - .await?; - - let cli_app_context = CliAppContext { - agg_config, - relay_processor, - }; - let msg = { - match self.command { - Command::ShowConfig => { - format!("Config: {:#?}", cli_app_context.agg_config) - } - Command::Maker { action } => Self::process_maker_commands(&cli_app_context, action).await?, - Command::Taker { action } => Self::process_taker_commands(&cli_app_context, action).await?, - Command::Helpers { action } => Self::process_helper_commands(&cli_app_context, action).await?, - Command::Dex { action } => Self::process_dex_commands(&cli_app_context, action).await?, - } - }; - write_into_stdout(msg)?; - Ok(()) - } - - #[allow(clippy::too_many_lines)] - async fn process_maker_commands( - cli_app_context: &CliAppContext, - action: MakerCommands, - ) -> crate::error::Result { - Ok(match action { - MakerCommands::InitOrder { - first_lbtc_utxo, - second_lbtc_utxo, - third_lbtc_utxo, - init_order_args, - fee_amount, - common_options, - } => { - Self::_process_maker_init_order( - MakerInitCliContext { - first_lbtc_utxo, - second_lbtc_utxo, - third_lbtc_utxo, - init_order_args, - fee_amount, - }, - common_options, - ) - .await? - } - MakerCommands::Fund { - filler_token_utxo, - grantor_collateral_token_utxo, - grantor_settlement_token_utxo, - settlement_asset_utxo, - fee_utxo, - fee_amount, - dcd_taproot_pubkey_gen, - common_options, - } => { - Self::_process_maker_fund( - cli_app_context, - MakerFundCliContext { - filler_token_utxo, - grantor_collateral_token_utxo, - grantor_settlement_token_utxo, - settlement_asset_utxo, - fee_utxo, - fee_amount, - dcd_taproot_pubkey_gen, - }, - common_options, - ) - .await? - } - MakerCommands::TerminationCollateral { - grantor_collateral_token_utxo, - fee_utxo, - collateral_token_utxo, - fee_amount, - grantor_collateral_amount_to_burn, - maker_order_event_id, - common_options, - } => { - Self::_process_maker_termination_collateral( - cli_app_context, - MakerCollateralTerminationCliContext { - grantor_collateral_token_utxo, - fee_utxo, - collateral_token_utxo, - fee_amount, - grantor_collateral_amount_to_burn, - maker_order_event_id, - }, - common_options, - ) - .await? - } - MakerCommands::TerminationSettlement { - fee_utxo, - settlement_asset_utxo, - grantor_settlement_token_utxo, - fee_amount, - grantor_settlement_amount_to_burn, - maker_order_event_id, - common_options, - } => { - Self::_process_maker_termination_settlement( - cli_app_context, - MakerSettlementTerminationCliContext { - fee_utxo, - settlement_asset_utxo, - grantor_settlement_token_utxo, - fee_amount, - grantor_settlement_amount_to_burn, - maker_order_event_id, - }, - common_options, - ) - .await? - } - MakerCommands::Settlement { - grantor_collateral_token_utxo, - grantor_settlement_token_utxo, - asset_utxo, - fee_utxo, - fee_amount, - price_at_current_block_height, - oracle_signature, - grantor_amount_to_burn, - maker_order_event_id, - common_options, - } => { - Self::_process_maker_settlement( - cli_app_context, - MakerSettlementCliContext { - grantor_collateral_token_utxo, - grantor_settlement_token_utxo, - fee_utxo, - asset_utxo, - fee_amount, - price_at_current_block_height, - oracle_signature, - grantor_amount_to_burn, - maker_order_event_id, - }, - common_options, - ) - .await? - } - }) - } - - async fn _process_maker_init_order( - MakerInitCliContext { - first_lbtc_utxo, - second_lbtc_utxo, - third_lbtc_utxo, - init_order_args, - fee_amount, - }: MakerInitCliContext, - CommonOrderOptions { - account_index, - is_offline, - }: CommonOrderOptions, - ) -> crate::error::Result { - use contract_handlers::maker_init::{Utxos, handle, process_args, save_args_to_cache}; - - let processed_args = process_args(account_index, init_order_args.into())?; - let (tx_res, args_to_save) = handle( - processed_args, - Utxos { - first: first_lbtc_utxo, - second: second_lbtc_utxo, - third: third_lbtc_utxo, - }, - fee_amount, - is_offline, - ) - .await?; - save_args_to_cache(&args_to_save)?; - Ok(format!("[Maker] Init order tx result: {tx_res:?}")) - } - - async fn _process_maker_fund( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, - MakerFundCliContext { - filler_token_utxo, - grantor_collateral_token_utxo, - grantor_settlement_token_utxo, - settlement_asset_utxo, - fee_utxo, - fee_amount, - dcd_taproot_pubkey_gen, - }: MakerFundCliContext, - CommonOrderOptions { - account_index, - is_offline, - }: CommonOrderOptions, - ) -> crate::error::Result { - use contract_handlers::maker_funding::{Utxos, handle, process_args, save_args_to_cache}; - - agg_config.check_nostr_keypair_existence()?; - - let processed_args = process_args(account_index, dcd_taproot_pubkey_gen)?; - let event_to_publish = processed_args.extract_event(); - let (tx_id, args_to_save) = handle( - processed_args, - Utxos { - filler_token: filler_token_utxo, - grantor_collateral_token: grantor_collateral_token_utxo, - grantor_settlement_token: grantor_settlement_token_utxo, - settlement_asset: settlement_asset_utxo, - fee: fee_utxo, - }, - fee_amount, - is_offline, - ) - .await?; - let res = relay_processor.place_order(event_to_publish, tx_id).await?; - save_args_to_cache(&args_to_save)?; - Ok(format!("[Maker] Creating order, tx_id: {tx_id}, event_id: {res:#?}")) - } - - async fn _process_maker_termination_collateral( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, - MakerCollateralTerminationCliContext { - grantor_collateral_token_utxo, - fee_utxo, - collateral_token_utxo, - fee_amount, - grantor_collateral_amount_to_burn, - maker_order_event_id, - }: MakerCollateralTerminationCliContext, - CommonOrderOptions { - account_index, - is_offline, - }: CommonOrderOptions, - ) -> crate::error::Result { - use contract_handlers::maker_termination_collateral::{Utxos, handle, save_args_to_cache}; - - agg_config.check_nostr_keypair_existence()?; - let processed_args = contract_handlers::maker_termination_collateral::process_args( - account_index, - grantor_collateral_amount_to_burn, - maker_order_event_id, - relay_processor, - ) - .await?; - let (tx_id, args_to_save) = handle( - processed_args, - Utxos { - grantor_collateral_token: grantor_collateral_token_utxo, - fee: fee_utxo, - collateral_token: collateral_token_utxo, - }, - fee_amount, - is_offline, - ) - .await?; - save_args_to_cache(&args_to_save)?; - let reply_event_id = relay_processor - .reply_order(maker_order_event_id, ReplyOption::MakerTerminationCollateral { tx_id }) - .await?; - Ok(format!( - "[Maker] Termination collateral tx result: {tx_id:?}, reply event id: {reply_event_id}" - )) - } - - async fn _process_maker_termination_settlement( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, - MakerSettlementTerminationCliContext { - fee_utxo, - settlement_asset_utxo, - grantor_settlement_token_utxo, - fee_amount, - grantor_settlement_amount_to_burn, - maker_order_event_id, - }: MakerSettlementTerminationCliContext, - CommonOrderOptions { - account_index, - is_offline, - }: CommonOrderOptions, - ) -> crate::error::Result { - use contract_handlers::maker_termination_settlement::{Utxos, handle, save_args_to_cache}; - - agg_config.check_nostr_keypair_existence()?; - let processed_args = contract_handlers::maker_termination_settlement::process_args( - account_index, - grantor_settlement_amount_to_burn, - maker_order_event_id, - relay_processor, - ) - .await?; - let (tx_id, args_to_save) = handle( - processed_args, - Utxos { - fee: fee_utxo, - settlement_asset: settlement_asset_utxo, - grantor_settlement_token: grantor_settlement_token_utxo, - }, - fee_amount, - is_offline, - ) - .await?; - save_args_to_cache(&args_to_save)?; - let reply_event_id = relay_processor - .reply_order(maker_order_event_id, ReplyOption::MakerTerminationSettlement { tx_id }) - .await?; - Ok(format!( - "[Maker] Termination settlement tx result: {tx_id:?}, reply event id: {reply_event_id}" - )) - } - - #[allow(clippy::too_many_lines)] - async fn _process_maker_settlement( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, - MakerSettlementCliContext { - grantor_collateral_token_utxo, - grantor_settlement_token_utxo, - fee_utxo, - asset_utxo, - fee_amount, - price_at_current_block_height, - oracle_signature, - grantor_amount_to_burn, - maker_order_event_id, - }: MakerSettlementCliContext, - CommonOrderOptions { - account_index, - is_offline, - }: CommonOrderOptions, - ) -> crate::error::Result { - use contract_handlers::maker_settlement::{Utxos, handle, process_args, save_args_to_cache}; - - agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args( - account_index, - price_at_current_block_height, - oracle_signature, - grantor_amount_to_burn, - maker_order_event_id, - relay_processor, - ) - .await?; - let (tx_id, args_to_save) = handle( - processed_args, - Utxos { - grantor_collateral_token: grantor_collateral_token_utxo, - grantor_settlement_token: grantor_settlement_token_utxo, - fee: fee_utxo, - asset: asset_utxo, - }, - fee_amount, - is_offline, - ) - .await?; - save_args_to_cache(&args_to_save)?; - let reply_event_id = relay_processor - .reply_order(maker_order_event_id, ReplyOption::MakerSettlement { tx_id }) - .await?; - Ok(format!( - "[Maker] Final settlement tx result: {tx_id:?}, reply event id: {reply_event_id}" - )) - } - - #[allow(clippy::too_many_lines)] - async fn process_taker_commands( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, - action: TakerCommands, - ) -> crate::error::Result { - Ok(match action { - TakerCommands::FundOrder { - filler_token_utxo, - collateral_token_utxo, - fee_amount, - collateral_amount_to_deposit, - common_options, - maker_order_event_id, - } => { - use contract_handlers::taker_funding::{Utxos, handle, process_args, save_args_to_cache}; - - agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args( - common_options.account_index, - collateral_amount_to_deposit, - maker_order_event_id, - relay_processor, - ) - .await?; - let (tx_id, args_to_save) = handle( - processed_args, - Utxos { - filler_token_utxo, - collateral_token_utxo, - }, - fee_amount, - common_options.is_offline, - ) - .await?; - let reply_event_id = relay_processor - .reply_order(maker_order_event_id, ReplyOption::TakerFund { tx_id }) - .await?; - save_args_to_cache(&args_to_save)?; - format!("[Taker] Tx fund sending result: {tx_id:?}, reply event id: {reply_event_id}") - } - TakerCommands::TerminationEarly { - filler_token_utxo, - collateral_token_utxo, - fee_utxo, - fee_amount, - filler_token_amount_to_return, - common_options, - maker_order_event_id, - } => { - use contract_handlers::taker_early_termination::{Utxos, handle, process_args, save_args_to_cache}; - - agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args( - common_options.account_index, - filler_token_amount_to_return, - maker_order_event_id, - relay_processor, - ) - .await?; - let (tx_id, args_to_save) = handle( - processed_args, - Utxos { - filler_token: filler_token_utxo, - collateral_token: collateral_token_utxo, - fee: fee_utxo, - }, - fee_amount, - common_options.is_offline, - ) - .await?; - let reply_event_id = relay_processor - .reply_order(maker_order_event_id, ReplyOption::TakerTerminationEarly { tx_id }) - .await?; - save_args_to_cache(&args_to_save)?; - format!("[Taker] Early termination tx result: {tx_id:?}, reply event id: {reply_event_id}") - } - TakerCommands::Settlement { - filler_token_utxo, - asset_utxo, - fee_utxo, - fee_amount, - price_at_current_block_height, - filler_amount_to_burn, - oracle_signature, - common_options, - maker_order_event_id, - } => { - use contract_handlers::taker_settlement::{Utxos, handle, process_args, save_args_to_cache}; - - agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args( - common_options.account_index, - price_at_current_block_height, - filler_amount_to_burn, - oracle_signature, - maker_order_event_id, - relay_processor, - ) - .await?; - let (tx_id, args_to_save) = handle( - processed_args, - Utxos { - filler_token: filler_token_utxo, - asset: asset_utxo, - fee: fee_utxo, - }, - fee_amount, - common_options.is_offline, - ) - .await?; - save_args_to_cache(&args_to_save)?; - let reply_event_id = relay_processor - .reply_order(maker_order_event_id, ReplyOption::TakerSettlement { tx_id }) - .await?; - format!("[Taker] Final settlement tx result: {tx_id:?}, reply event id: {reply_event_id}") - } - }) - } - - #[allow(clippy::too_many_lines)] - async fn process_helper_commands( - cli_app_context: &CliAppContext, - action: HelperCommands, - ) -> crate::error::Result { - Ok(match action { - HelperCommands::Faucet { - fee_utxo_outpoint, - asset_name, - issue_amount, - fee_amount, - common_options, - } => { - Self::_process_helper_faucet(fee_utxo_outpoint, asset_name, issue_amount, fee_amount, common_options) - .await? - } - HelperCommands::MintTokens { - reissue_asset_outpoint, - fee_utxo_outpoint, - asset_name, - reissue_amount, - fee_amount, - common_options, - } => { - Self::_process_helper_mint_tokens( - reissue_asset_outpoint, - fee_utxo_outpoint, - asset_name, - reissue_amount, - fee_amount, - common_options, - ) - .await? - } - HelperCommands::SplitNativeThree { - split_amount, - fee_utxo, - fee_amount, - common_options, - } => Self::_process_helper_split_native_three(split_amount, fee_utxo, fee_amount, common_options).await?, - HelperCommands::Address { account_index: index } => Self::_process_helper_address(index)?, - HelperCommands::OracleSignature { - price_at_current_block_height, - settlement_height, - oracle_account_index, - } => Self::_process_helper_oracle_signature( - price_at_current_block_height, - settlement_height, - oracle_account_index, - )?, - HelperCommands::MergeTokens2 { - token_utxo_1, - token_utxo_2, - fee_utxo, - fee_amount, - maker_order_event_id, - common_options, - } => { - Self::_process_helper_merge_tokens2( - cli_app_context, - MergeTokens2CliContext { - token_utxo_1, - token_utxo_2, - fee_utxo, - fee_amount, - maker_order_event_id, - }, - common_options, - ) - .await? - } - HelperCommands::MergeTokens3 { - token_utxo_1, - token_utxo_2, - token_utxo_3, - fee_utxo, - fee_amount, - maker_order_event_id, - common_options, - } => { - Self::_process_helper_merge_tokens3( - cli_app_context, - MergeTokens3CliContext { - token_utxo_1, - token_utxo_2, - token_utxo_3, - fee_utxo, - fee_amount, - maker_order_event_id, - }, - common_options, - ) - .await? - } - HelperCommands::MergeTokens4 { - token_utxo_1, - token_utxo_2, - token_utxo_3, - token_utxo_4, - fee_utxo, - fee_amount, - maker_order_event_id, - common_options, - } => { - Self::_process_helper_merge_tokens4( - cli_app_context, - MergeTokens4CliContext { - token_utxo_1, - token_utxo_2, - token_utxo_3, - token_utxo_4, - fee_utxo, - fee_amount, - maker_order_event_id, - }, - common_options, - ) - .await? - } - }) - } - - async fn _process_helper_faucet( - fee_utxo_outpoint: OutPoint, - asset_name: String, - issue_amount: u64, - fee_amount: u64, - CommonOrderOptions { - account_index, - is_offline, - }: CommonOrderOptions, - ) -> crate::error::Result { - let tx_id = contract_handlers::faucet::create_asset( - account_index, - asset_name, - fee_utxo_outpoint, - fee_amount, - issue_amount, - is_offline, - ) - .await?; - Ok(format!("Finish asset creation, tx_id: {tx_id}")) - } - - async fn _process_helper_mint_tokens( - reissue_asset_outpoint: OutPoint, - fee_utxo_outpoint: OutPoint, - asset_name: String, - reissue_amount: u64, - fee_amount: u64, - CommonOrderOptions { - account_index, - is_offline, - }: CommonOrderOptions, - ) -> crate::error::Result { - let tx_id = contract_handlers::faucet::mint_asset( - account_index, - asset_name, - reissue_asset_outpoint, - fee_utxo_outpoint, - reissue_amount, - fee_amount, - is_offline, - ) - .await?; - Ok(format!("Finish asset minting, tx_id: {tx_id} ")) - } - - async fn _process_helper_split_native_three( - split_amount: u64, - fee_utxo: OutPoint, - fee_amount: u64, - CommonOrderOptions { - account_index, - is_offline, - }: CommonOrderOptions, - ) -> crate::error::Result { - let tx_res = - contract_handlers::split_utxo::handle(account_index, split_amount, fee_utxo, fee_amount, is_offline) - .await?; - Ok(format!("Split utxo result tx_id: {tx_res:?}")) - } - - fn _process_helper_address(index: u32) -> crate::error::Result { - let (x_only_pubkey, addr) = contract_handlers::address::handle(index)?; - Ok(format!("X Only Public Key: '{x_only_pubkey}', P2PK Address: '{addr}'")) - } - - fn _process_helper_oracle_signature( - price_at_current_block_height: u64, - settlement_height: u32, - oracle_account_index: u32, - ) -> crate::error::Result { - let (pubkey, msg, signature) = contract_handlers::oracle_signature::handle( - oracle_account_index, - price_at_current_block_height, - settlement_height, - )?; - Ok(format!( - "Oracle signature for msg: '{}', signature: '{}', pubkey used: '{}'", - msg.to_hex(), - hex::encode(signature.serialize()), - pubkey.x_only_public_key().0.to_hex() - )) - } - - async fn _process_helper_merge_tokens2( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, - MergeTokens2CliContext { - token_utxo_1, - token_utxo_2, - fee_utxo, - fee_amount, - maker_order_event_id, - }: MergeTokens2CliContext, - CommonOrderOptions { - account_index, - is_offline, - }: CommonOrderOptions, - ) -> crate::error::Result { - use contract_handlers::merge_tokens::{ - merge2::{Utxos2, handle}, - process_args, save_args_to_cache, - }; - - agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args(account_index, maker_order_event_id, relay_processor).await?; - let (tx_id, args_to_save) = handle( - processed_args, - Utxos2 { - utxo_1: token_utxo_1, - utxo_2: token_utxo_2, - fee: fee_utxo, - }, - fee_amount, - is_offline, - ) - .await?; - save_args_to_cache(&args_to_save)?; - let reply_event_id = relay_processor - .reply_order( - maker_order_event_id, - ReplyOption::Merge2 { - tx_id, - token_utxo_1, - token_utxo_2, - }, - ) - .await?; - Ok(format!( - "[Taker] Final merge 2 tx result: {tx_id:?}, reply event id: {reply_event_id}" - )) - } - - async fn _process_helper_merge_tokens3( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, - MergeTokens3CliContext { - token_utxo_1, - token_utxo_2, - token_utxo_3, - fee_utxo, - fee_amount, - maker_order_event_id, - }: MergeTokens3CliContext, - CommonOrderOptions { - account_index, - is_offline, - }: CommonOrderOptions, - ) -> crate::error::Result { - use contract_handlers::merge_tokens::{ - merge3::{Utxos3, handle}, - process_args, save_args_to_cache, - }; - - agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args(account_index, maker_order_event_id, relay_processor).await?; - let (tx_id, args_to_save) = handle( - processed_args, - Utxos3 { - utxo_1: token_utxo_1, - utxo_2: token_utxo_2, - utxo_3: token_utxo_3, - fee: fee_utxo, - }, - fee_amount, - is_offline, - ) - .await?; - save_args_to_cache(&args_to_save)?; - let reply_event_id = relay_processor - .reply_order( - maker_order_event_id, - ReplyOption::Merge3 { - tx_id, - token_utxo_1, - token_utxo_2, - token_utxo_3, - }, - ) - .await?; - Ok(format!( - "[Taker] Final merge 3 tx result: {tx_id:?}, reply event id: {reply_event_id}" - )) - } - - async fn _process_helper_merge_tokens4( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, - MergeTokens4CliContext { - token_utxo_1, - token_utxo_2, - token_utxo_3, - token_utxo_4, - fee_utxo, - fee_amount, - maker_order_event_id, - }: MergeTokens4CliContext, - CommonOrderOptions { - account_index, - is_offline, - }: CommonOrderOptions, - ) -> crate::error::Result { - use contract_handlers::merge_tokens::{ - merge4::{Utxos4, handle}, - process_args, save_args_to_cache, - }; - - agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args(account_index, maker_order_event_id, relay_processor).await?; - let (tx_id, args_to_save) = handle( - processed_args, - Utxos4 { - utxo_1: token_utxo_1, - utxo_2: token_utxo_2, - utxo_3: token_utxo_3, - utxo_4: token_utxo_4, - fee: fee_utxo, - }, - fee_amount, - is_offline, - ) - .await?; - save_args_to_cache(&args_to_save)?; - let reply_event_id = relay_processor - .reply_order( - maker_order_event_id, - ReplyOption::Merge4 { - tx_id, - token_utxo_1, - token_utxo_2, - token_utxo_3, - token_utxo_4, - }, - ) - .await?; - Ok(format!( - "[Taker] Final merge 4 tx result: {tx_id:?}, reply event id: {reply_event_id}" - )) - } - - async fn process_dex_commands( - CliAppContext { relay_processor, .. }: &CliAppContext, - action: DexCommands, - ) -> crate::error::Result { - Ok(match action { - DexCommands::GetOrderReplies { event_id } => { - let res = relay_processor.get_order_replies(event_id).await?; - format!("Order '{event_id}' replies: {res:#?}") - } - DexCommands::ListOrders { - authors, - time_to_filter, - limit, - } => { - let (since, until) = if let Some(time_filter) = time_to_filter { - (time_filter.compute_since(), time_filter.compute_until()) - } else { - (None, None) - }; - - let filter = ListOrdersEventFilter { - authors, - since: since.map(Timestamp::from), - until: until.map(Timestamp::from), - limit, - }; - - let res = relay_processor.list_orders(filter).await?; - let body = format_items(&res, std::string::ToString::to_string); - format!("List of available orders:\n{body}") - } - DexCommands::GetEventsById { event_id } => { - let res = relay_processor.get_event_by_id(event_id).await?; - format!("List of available events: {res:#?}") - } - DexCommands::GetOrderById { event_id } => { - let res = relay_processor.get_order_by_id(event_id).await?; - let body = format_items(&[res], std::string::ToString::to_string); - format!("Order {event_id}: {body}") - } - DexCommands::ImportParams { event_id } => { - let res = relay_processor.get_order_by_id(event_id).await?; - crate::common::store::utils::save_dcd_args(&res.dcd_taproot_pubkey_gen, &res.dcd_arguments)?; - format!("Order {event_id}: {res}") - } - }) - } -} - -fn format_items(items: &[T], map: F) -> String -where - F: Fn(&T) -> String, -{ - items.iter().map(map).collect::>().join("\n") -} diff --git a/crates/cli-client/src/cli/taker.rs b/crates/cli-client/src/cli/taker.rs deleted file mode 100644 index 8ec53a8..0000000 --- a/crates/cli-client/src/cli/taker.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::cli::processor::CommonOrderOptions; -use clap::Subcommand; -use nostr::EventId; -use simplicity::elements::OutPoint; - -#[derive(Debug, Subcommand)] -pub enum TakerCommands { - #[command( - about = "Fund an existing DCD order as Taker and lock collateral into the contract [authentication required]", - name = "fund" - )] - FundOrder { - /// UTXO containing filler tokens provided by the Taker to fund the contract - #[arg(long = "filler-utxo")] - filler_token_utxo: OutPoint, - /// UTXO containing collateral asset that the Taker locks into the DCD contract - #[arg(long = "collateral-utxo")] - collateral_token_utxo: OutPoint, - /// Miner fee in satoshis (LBTC) for the Taker funding transaction - #[arg(long = "fee-amount", default_value_t = 1500)] - fee_amount: u64, - /// Amount of collateral (in satoshis) that the Taker will lock into the DCD contract - #[arg(long = "collateral-amount-deposit")] - collateral_amount_to_deposit: u64, - /// `EventId` of the Maker\'s original order event on Nostr - #[arg(short = 'i', long)] - maker_order_event_id: EventId, - #[command(flatten)] - common_options: CommonOrderOptions, - }, - #[command( - about = "Exit the DCD contract early as Taker by returning filler tokens in exchange for your collateral" - )] - TerminationEarly { - /// UTXO containing filler tokens that the Taker returns to exit the contract early - #[arg(long = "filler-utxo")] - filler_token_utxo: OutPoint, - /// UTXO containing the collateral asset that the Taker will withdraw back - #[arg(long = "collateral-utxo")] - collateral_token_utxo: OutPoint, - /// UTXO used to pay miner fees for the early-termination transaction - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Miner fee in satoshis (LBTC) for the early-termination transaction - #[arg(long = "fee-amount", default_value_t = 1500)] - fee_amount: u64, - /// Amount of filler tokens (in satoshis) that the Taker returns to exit early - #[arg(long = "filler-to-return")] - filler_token_amount_to_return: u64, - /// `EventId` of the Maker\'s original order event on Nostr - #[arg(short = 'i', long)] - maker_order_event_id: EventId, - #[command(flatten)] - common_options: CommonOrderOptions, - }, - #[command( - about = "Settle the Taker side of the DCD at maturity using an oracle price to choose collateral or settlement asset" - )] - Settlement { - /// UTXO containing filler tokens that the Taker burns during settlement - #[arg(long = "filler-utxo")] - filler_token_utxo: OutPoint, - /// UTXO providing the asset (collateral or settlement) that the Taker receives at maturity - #[arg(long = "asset-utxo")] - asset_utxo: OutPoint, - /// UTXO used to pay miner fees for the final Taker settlement transaction - #[arg(long = "fee-utxo")] - fee_utxo: OutPoint, - /// Miner fee in satoshis (LBTC) for the final Taker settlement transaction - #[arg(long = "fee-amount", default_value_t = 1500)] - fee_amount: u64, - /// Amount of filler tokens (in satoshis) that the Taker burns during settlement - #[arg(long = "filler-to-burn")] - filler_amount_to_burn: u64, - /// Oracle price at current block height used for settlement decision - #[arg(long = "price-now")] - price_at_current_block_height: u64, - /// Schnorr/ecdsa signature produced by the oracle over the published price - #[arg(long = "oracle-sign")] - oracle_signature: String, - /// `EventId` of the Maker\'s original order event on Nostr - #[arg(short = 'i', long)] - maker_order_event_id: EventId, - #[command(flatten)] - common_options: CommonOrderOptions, - }, -} diff --git a/crates/cli-client/src/common/config.rs b/crates/cli-client/src/common/config.rs deleted file mode 100644 index 6b87e91..0000000 --- a/crates/cli-client/src/common/config.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::cli::{Cli, DEFAULT_CONFIG_PATH}; -use crate::error::CliError::ConfigExtended; - -use std::str::FromStr; - -use config::{Config, File, FileFormat, ValueKind}; - -use nostr::{Keys, RelayUrl}; - -use serde::{Deserialize, Deserializer}; - -use crate::error::CliError; -use tracing::instrument; - -#[derive(Debug, Clone)] -pub struct AggregatedConfig { - pub nostr_keypair: Option, - pub relays: Vec, -} - -#[derive(Debug, Clone)] -pub struct KeysWrapper(pub Keys); - -impl<'de> Deserialize<'de> for KeysWrapper { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let keys = Keys::from_str(&s).map_err(serde::de::Error::custom)?; - Ok(KeysWrapper(keys)) - } -} - -impl From for ValueKind { - fn from(val: KeysWrapper) -> Self { - ValueKind::String(val.0.secret_key().to_secret_hex()) - } -} - -impl AggregatedConfig { - /// Build aggregated configuration from CLI arguments and optional config file. - /// - /// # Errors - /// - /// Returns: - /// - `CliError::Config` if the underlying `config` builder or deserialization fails. - /// - `CliError::ConfigExtended` if the aggregated configuration cannot be - /// constructed (e.g., missing or empty `relays` list). - #[instrument(level = "debug", skip(cli))] - pub fn new(cli: &Cli) -> crate::error::Result { - #[derive(Deserialize, Debug)] - pub struct AggregatedConfigInner { - pub nostr_keypair: Option, - pub relays: Option>, - } - - let Cli { - nostr_key, - relays_list, - nostr_config_path, - .. - } = cli; - - let mut config_builder = Config::builder().add_source( - File::from(nostr_config_path.clone()) - .format(FileFormat::Toml) - .required(DEFAULT_CONFIG_PATH != nostr_config_path.to_string_lossy().as_ref()), - ); - - if let Some(nostr_key) = nostr_key { - tracing::debug!("Adding keypair value from CLI"); - config_builder = - config_builder.set_override_option("nostr_keypair", Some(KeysWrapper(nostr_key.clone())))?; - } - - if let Some(relays) = relays_list { - tracing::debug!("Adding relays values from CLI, relays: '{:?}'", relays); - config_builder = config_builder.set_override_option( - "relays", - Some( - relays - .iter() - .map(std::string::ToString::to_string) - .collect::>(), - ), - )?; - } - - // TODO(Alex): add Liquid private key - - let config = match config_builder.build()?.try_deserialize::() { - Ok(conf) => Ok(conf), - Err(e) => Err(ConfigExtended(format!( - "Got error in gathering AggregatedConfigInner, error: {e:?}" - ))), - }?; - - let Some(relays) = config.relays else { - return Err(ConfigExtended("No relays found in configuration..".to_string())); - }; - - if relays.is_empty() { - return Err(ConfigExtended("Relays configuration is empty..".to_string())); - } - - let aggregated_config = AggregatedConfig { - nostr_keypair: config.nostr_keypair.map(|x| x.0), - relays, - }; - - tracing::debug!("Config gathered: '{:?}'", aggregated_config); - - Ok(aggregated_config) - } - - /// Ensure that a Nostr keypair is present in the aggregated configuration. - /// - /// # Errors - /// - /// Returns `CliError::NoNostrKeypairListed` if `nostr_keypair` is `None`. - pub fn check_nostr_keypair_existence(&self) -> crate::error::Result<()> { - if self.nostr_keypair.is_none() { - return Err(CliError::NoNostrKeypairListed); - } - Ok(()) - } -} diff --git a/crates/cli-client/src/common/keys.rs b/crates/cli-client/src/common/keys.rs deleted file mode 100644 index 6a0eeb4..0000000 --- a/crates/cli-client/src/common/keys.rs +++ /dev/null @@ -1,98 +0,0 @@ -use simplicityhl::elements::secp256k1_zkp as secp256k1; - -/// # Panics -/// -/// Will panic if `SEED_HEX` is in incorrect encoding that differs from hex -#[must_use] -pub fn derive_secret_key_from_index(index: u32, seed_hex: impl AsRef<[u8]>) -> secp256k1::SecretKey { - // TODO (Oleks): fix possible panic, propagate error & move this parameter into config - let seed_vec = hex::decode(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); - - let mut seed = seed_bytes; - for (i, b) in index.to_be_bytes().iter().enumerate() { - seed[24 + i] ^= *b; - } - secp256k1::SecretKey::from_slice(&seed).unwrap() -} - -pub fn derive_keypair_from_index(index: u32, seed_hex: impl AsRef<[u8]>) -> secp256k1::Keypair { - elements::bitcoin::secp256k1::Keypair::from_secret_key( - elements::bitcoin::secp256k1::SECP256K1, - &derive_secret_key_from_index(index, seed_hex), - ) -} - -#[cfg(test)] -mod tests { - use super::*; - use elements::hex::ToHex; - use proptest::prelude::*; - use simplicityhl::elements; - use simplicityhl::elements::AddressParams; - use simplicityhl_core::get_p2pk_address; - - fn check_seed_hex_gen( - index: u32, - x_only_pubkey: &str, - p2pk_addr: &str, - seed_hex: impl AsRef<[u8]>, - ) -> anyhow::Result<()> { - let keypair = derive_keypair_from_index(index, &seed_hex); - - let public_key = keypair.x_only_public_key().0; - let address = get_p2pk_address(&public_key, &AddressParams::LIQUID_TESTNET)?; - - assert_eq!(public_key.to_string(), x_only_pubkey); - assert_eq!(address.to_string(), p2pk_addr); - Ok(()) - } - - #[test] - fn derive_keypair_from_index_is_deterministic_for_seed() -> anyhow::Result<()> { - const SEED_HEX: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; - - let expected_secrets = [ - ( - 0u32, - "4646ae5047316b4230d0086c8acec687f00b1cd9d1dc634f6cb358ac0a9a8fff", - "tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm", - ), - ( - 1u32, - "16e47b8867bfbeaae66c0345577751c551903eb90ba479e91f783c507c088732", - "tex1prmytj5v08w6jwjtm4exmuxv0nn8favzyqu3aptzrgvl44nfatqmsykjhk3", - ), - ( - 2u32, - "d0d0fce6bc500821c33212666ecfbd9d41a1414d584af4102e7441277d25d872", - "tex1phctnz400pn7r3rhh8nyc2xmsg2e9h2n299a8ld4pup0v5def9cdsjz3put", - ), - ]; - let check_address_with_index = |i| -> anyhow::Result<()> { - let (index, x_only_pubkey, p2pk_addr) = expected_secrets[i]; - check_seed_hex_gen(index, x_only_pubkey, p2pk_addr, SEED_HEX)?; - Ok(()) - }; - - check_address_with_index(0)?; - check_address_with_index(1)?; - check_address_with_index(2)?; - Ok(()) - } - - proptest! { - #[test] - fn prop_keypair_determinism(index in 0u32..u32::MAX, seed in any::<[u8; 32]>()) { - let seed_hex = seed.to_hex(); - - let kp1 = derive_keypair_from_index(index, &seed_hex); - let kp2 = derive_keypair_from_index(index, &seed_hex); - - prop_assert_eq!(kp1.secret_bytes(), kp2.secret_bytes()); - } - } -} diff --git a/crates/cli-client/src/common/mod.rs b/crates/cli-client/src/common/mod.rs deleted file mode 100644 index c7fdbe5..0000000 --- a/crates/cli-client/src/common/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod config; -pub mod keys; -pub mod settings; -pub(crate) mod store; -mod types; -mod utils; - -pub use types::*; -pub use utils::*; diff --git a/crates/cli-client/src/common/settings.rs b/crates/cli-client/src/common/settings.rs deleted file mode 100644 index 9481494..0000000 --- a/crates/cli-client/src/common/settings.rs +++ /dev/null @@ -1,45 +0,0 @@ -use config::{Case, Config}; -use tracing::instrument; - -pub struct Seed(pub SeedInner); -pub type SeedInner = [u8; 32]; -pub struct SeedHex { - pub seed_hex: String, -} - -impl SeedHex { - pub const ENV_NAME: &'static str = "SEED_HEX"; -} - -#[derive(Clone, Debug)] -pub struct Settings { - pub seed_hex: String, -} - -impl Settings { - /// Load CLI settings from environment variables. - /// - /// # Errors - /// - /// Returns: - /// - `CliError::Config` if building the configuration from the environment fails. - /// - `CliError::EnvNotSet` if the [`SeedHex::ENV_NAME`] environment variable is not set - /// or cannot be read as a UTF-8 string. - #[instrument(level = "debug", ret)] - pub fn load() -> crate::error::Result { - let cfg = Config::builder() - .add_source( - config::Environment::default() - .separator("__") - .convert_case(Case::ScreamingSnake), - ) - .build() - .map_err(crate::error::CliError::Config)?; - - let seed_hex = cfg - .get_string(SeedHex::ENV_NAME) - .map_err(|_| crate::error::CliError::EnvNotSet(SeedHex::ENV_NAME.to_string()))?; - - Ok(Self { seed_hex }) - } -} diff --git a/crates/cli-client/src/common/store.rs b/crates/cli-client/src/common/store.rs deleted file mode 100644 index 6156f60..0000000 --- a/crates/cli-client/src/common/store.rs +++ /dev/null @@ -1,393 +0,0 @@ -use hex::FromHexError; -use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; -use simplicityhl::simplicity::elements::{Address, AddressParams}; -use simplicityhl_core::{Encodable, TaprootPubkeyGen}; -use sled::IVec; -use thiserror::Error; - -#[derive(Clone, Debug)] -pub struct Store { - store: sled::Db, -} - -#[derive(Error, Debug)] -pub enum SledError { - #[error(transparent)] - Sled(#[from] sled::Error), - #[error("Arguments not found")] - ArgumentNotFound, - #[error("Encodable error, msg: {0}")] - Encode(String), - #[error("Hex parsing error, msg: {0}")] - Hex(#[from] FromHexError), - #[error("Tap root gen error, msg: {0}")] - TapRootGen(String), -} - -pub type Result = std::result::Result; - -impl Store { - pub fn load() -> Result { - Ok(Self { - store: sled::open(".cache/store")?, - }) - } - - pub fn is_exist(&self, asset_name: &str) -> Result { - Ok(self.store.get(asset_name)?.is_some()) - } - - pub fn insert_value(&self, key: K, value: V) -> Result> - where - K: AsRef<[u8]>, - V: Into, - { - Ok(self.store.insert(key, value)?) - } - - pub fn get_value>(&self, key: K) -> Result> { - Ok(self.store.get(key)?) - } - - #[allow(unused)] - pub fn import_arguments( - &self, - taproot_pubkey_gen: &str, - encoded_data: &str, - params: &'static AddressParams, - get_address: &impl Fn(&XOnlyPublicKey, &A, &'static AddressParams) -> anyhow::Result
, - ) -> Result<()> - where - A: Encodable + simplicityhl_core::encoding::Decode<()>, - { - let decoded_data = hex::decode(encoded_data)?; - - let arguments = Encodable::decode(&decoded_data).map_err(|e| SledError::Encode(e.to_string()))?; - let _ = TaprootPubkeyGen::build_from_str(taproot_pubkey_gen, &arguments, params, get_address) - .map_err(|e| SledError::TapRootGen(e.to_string()))?; - - self.store.insert(taproot_pubkey_gen, decoded_data)?; - - Ok(()) - } - - #[allow(unused)] - pub fn export_arguments(&self, taproot_pubkey_gen: &str) -> Result { - if let Some(value) = self.store.get(taproot_pubkey_gen)? { - return Ok(hex::encode(value)); - } - - Err(SledError::ArgumentNotFound) - } - - pub fn get_arguments(&self, key: impl AsRef<[u8]>) -> Result - where - A: Encodable + simplicityhl_core::encoding::Decode<()>, - { - if let Some(value) = self.store.get(key)? { - return Encodable::decode(&value).map_err(|err| SledError::Encode(err.to_string())); - } - Err(SledError::ArgumentNotFound) - } - - pub fn get_arguments_raw(&self, key: impl AsRef<[u8]>) -> Result { - self.store.get(key)?.ok_or_else(|| SledError::ArgumentNotFound) - } -} - -pub mod utils { - use crate::common::store::Store; - use contracts::DCDArguments; - use nostr::EventId; - use simplicityhl_core::{AssetEntropyHex, Encodable}; - - const FILLER_TOKEN_ENTROPY_STORE_NAME: &str = "filler_token_entropy"; - const GRANTOR_COLLATERAL_TOKEN_ENTROPY_STORE_NAME: &str = "grantor_collateral_token_entropy"; - const GRANTOR_SETTLEMENT_TOKEN_ENTROPY_STORE_NAME: &str = "grantor_settlement_token_entropy"; - - #[derive(Debug, bincode::Encode, bincode::Decode)] - pub struct OrderParams { - pub taproot_pubkey_gen: String, - pub dcd_args: DCDArguments, - } - - pub fn save_dcd_args(taproot_pubkey: &impl ToString, dcd_args: &DCDArguments) -> crate::error::Result<()> { - let store = Store::load()?; - store.insert_value(taproot_pubkey.to_string(), dcd_args.encode().unwrap())?; - Ok(()) - } - - pub fn get_dcd_args(taproot_pubkey: &impl ToString) -> crate::error::Result { - let store = Store::load()?; - let dcd_args: DCDArguments = store.get_arguments(taproot_pubkey.to_string())?; - Ok(dcd_args) - } - - fn get_filler_entropy_key(taproot_pubkey: &impl ToString) -> String { - format!("{}-{FILLER_TOKEN_ENTROPY_STORE_NAME}", taproot_pubkey.to_string()) - } - - pub fn save_filler_token_entropy( - taproot_pubkey: &impl ToString, - asset_entropy: &AssetEntropyHex, - ) -> crate::error::Result<()> { - let store = Store::load()?; - store.insert_value(get_filler_entropy_key(taproot_pubkey), asset_entropy.as_bytes())?; - Ok(()) - } - - pub fn get_filler_token_entropy(taproot_pubkey: &impl ToString) -> crate::error::Result { - let store = Store::load()?; - let bytes = store.get_arguments_raw(get_filler_entropy_key(taproot_pubkey))?; - let x = String::from_utf8(bytes.to_vec()).map_err(|err| { - crate::error::CliError::Cache(format!( - "Failed to obtain cached value for 'filler_token_entropy', err: {err}" - )) - })?; - Ok(x) - } - - fn get_grantor_collateral_token_entropy_key(taproot_pubkey: &impl ToString) -> String { - format!( - "{}-{GRANTOR_COLLATERAL_TOKEN_ENTROPY_STORE_NAME}", - taproot_pubkey.to_string() - ) - } - - pub fn save_grantor_collateral_token_entropy( - taproot_pubkey: &impl ToString, - asset_entropy: &AssetEntropyHex, - ) -> crate::error::Result<()> { - let store = Store::load()?; - store.insert_value( - get_grantor_collateral_token_entropy_key(taproot_pubkey), - asset_entropy.as_bytes(), - )?; - Ok(()) - } - - pub fn get_grantor_collateral_token_entropy( - taproot_pubkey: &impl ToString, - ) -> crate::error::Result { - let store = Store::load()?; - let bytes = store.get_arguments_raw(get_grantor_collateral_token_entropy_key(taproot_pubkey))?; - let x = String::from_utf8(bytes.to_vec()).map_err(|err| { - crate::error::CliError::Cache(format!( - "Failed to obtain cached value for 'filler_token_entropy', err: {err}" - )) - })?; - Ok(x) - } - - fn get_grantor_settlement_token_entropy_key(taproot_pubkey: &impl ToString) -> String { - format!( - "{}-{GRANTOR_SETTLEMENT_TOKEN_ENTROPY_STORE_NAME}", - taproot_pubkey.to_string() - ) - } - - pub fn save_grantor_settlement_token_entropy( - taproot_pubkey: &impl ToString, - asset_entropy: &AssetEntropyHex, - ) -> crate::error::Result<()> { - let store = Store::load()?; - store.insert_value( - get_grantor_settlement_token_entropy_key(taproot_pubkey), - asset_entropy.as_bytes(), - )?; - Ok(()) - } - - pub fn get_grantor_settlement_token_entropy( - taproot_pubkey: &impl ToString, - ) -> crate::error::Result { - let store = Store::load()?; - let bytes = store.get_arguments_raw(get_grantor_settlement_token_entropy_key(taproot_pubkey))?; - let x = String::from_utf8(bytes.to_vec()).map_err(|err| { - crate::error::CliError::Cache(format!( - "Failed to obtain cached value for 'filler_token_entropy', err: {err}" - )) - })?; - Ok(x) - } - - pub fn save_order_params_by_event_id( - event_id: EventId, - taproot_pubkey: &impl ToString, - asset_entropy: DCDArguments, - ) -> crate::error::Result<()> { - let bytes = bincode::encode_to_vec( - OrderParams { - taproot_pubkey_gen: taproot_pubkey.to_string(), - dcd_args: asset_entropy, - }, - bincode::config::standard(), - ) - .unwrap(); - let store = Store::load()?; - store.insert_value(event_id, bytes)?; - Ok(()) - } - - pub fn get_order_params_by_event_id(event_id: EventId) -> crate::error::Result { - let store = Store::load()?; - let bytes = store.get_arguments_raw(event_id)?.to_vec(); - let decoded: OrderParams = bincode::decode_from_slice(&bytes, bincode::config::standard()) - .map_err(|err| { - crate::error::CliError::Cache(format!("Failed to obtain order params by event id, err: {err}")) - })? - .0; - Ok(decoded) - } -} - -#[cfg(test)] -mod tests { - use contracts::get_options_program; - use simplicityhl::simplicity::elements; - use simplicityhl_core::{Encodable, TaprootPubkeyGen}; - use simplicityhl_core::{LIQUID_TESTNET_TEST_ASSET_ID_STR, create_p2tr_address}; - - use super::*; - - #[derive(Debug, Clone, bincode::Encode, bincode::Decode, PartialEq)] - pub struct OptionsArguments { - pub start_time: u32, - pub expiry_time: u32, - pub collateral_per_contract: u64, - pub settlement_per_contract: u64, - pub collateral_asset_id_hex_le: String, - pub settlement_asset_id_hex_le: String, - pub option_token_asset_id_hex_le: String, - pub grantor_token_asset_id_hex_le: String, - } - - impl Default for OptionsArguments { - fn default() -> Self { - Self { - start_time: 0, - expiry_time: 0, - collateral_per_contract: 0, - settlement_per_contract: 0, - collateral_asset_id_hex_le: "00".repeat(32), - option_token_asset_id_hex_le: "00".repeat(32), - grantor_token_asset_id_hex_le: "00".repeat(32), - settlement_asset_id_hex_le: "00".repeat(32), - } - } - } - - impl simplicityhl_core::Encodable for OptionsArguments {} - - fn load_mock() -> Store { - Store { - store: sled::Config::new().temporary(true).open().expect("expected store"), - } - } - - fn get_mocked_data() -> anyhow::Result<(OptionsArguments, TaprootPubkeyGen)> { - let args = OptionsArguments { - start_time: 10, - expiry_time: 50, - collateral_per_contract: 100, - settlement_per_contract: 1000, - collateral_asset_id_hex_le: elements::AssetId::LIQUID_BTC.to_string(), - settlement_asset_id_hex_le: LIQUID_TESTNET_TEST_ASSET_ID_STR.to_string(), - option_token_asset_id_hex_le: elements::AssetId::LIQUID_BTC.to_string(), - grantor_token_asset_id_hex_le: elements::AssetId::LIQUID_BTC.to_string(), - }; - - let options_taproot_pubkey_gen = - TaprootPubkeyGen::from(&args, &AddressParams::LIQUID_TESTNET, &get_options_address)?; - - Ok((args, options_taproot_pubkey_gen)) - } - - pub fn get_options_address( - x_only_public_key: &XOnlyPublicKey, - arguments: &OptionsArguments, - params: &'static AddressParams, - ) -> anyhow::Result
{ - Ok(create_p2tr_address( - get_options_program(&contracts::build_arguments::OptionsArguments { - start_time: arguments.start_time, - expiry_time: arguments.expiry_time, - collateral_per_contract: arguments.collateral_per_contract, - settlement_per_contract: arguments.settlement_per_contract, - collateral_asset_id_hex_le: arguments.collateral_asset_id_hex_le.clone(), - settlement_asset_id_hex_le: arguments.settlement_asset_id_hex_le.clone(), - option_token_asset_id_hex_le: arguments.option_token_asset_id_hex_le.clone(), - grantor_token_asset_id_hex_le: arguments.grantor_token_asset_id_hex_le.clone(), - })? - .commit() - .cmr(), - x_only_public_key, - params, - )) - } - - #[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()?, - &AddressParams::LIQUID_TESTNET, - &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()?, - &AddressParams::LIQUID_TESTNET, - &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()?, - &AddressParams::LIQUID_TESTNET, - &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-client/src/common/types.rs b/crates/cli-client/src/common/types.rs deleted file mode 100644 index 76762c3..0000000 --- a/crates/cli-client/src/common/types.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::contract_handlers::maker_init::InnerDcdInitParams; -use clap::Args; -use contracts_adapter::dcd::COLLATERAL_ASSET_ID; -use simplicityhl_core::{AssetEntropyHex, AssetIdHex}; - -/// Represents either three asset IDs or three asset entropies as provided on the CLI. -/// This is intended to be parsed by a custom `clap` value parser (placeholder below). -#[derive(Debug, Clone, PartialEq)] -pub enum DcdCliAssets { - /// Already-constructed asset IDs (little-endian hex strings). - AssetIds { - filler_token_asset_id_hex_le: AssetIdHex, - grantor_collateral_token_asset_id_hex_le: AssetIdHex, - grantor_settlement_token_asset_id_hex_le: AssetIdHex, - settlement_token_asset_id_hex_le: AssetIdHex, - }, - /// Entropies from which asset IDs will be derived. - Entropies { - filler_token_entropy_hex: AssetEntropyHex, - grantor_collateral_token_entropy_hex: AssetEntropyHex, - grantor_settlement_token_entropy_hex: AssetEntropyHex, - settlement_token_asset_id_hex_le: AssetEntropyHex, - }, -} - -#[derive(Debug, Args)] -pub struct InitOrderArgs { - /// Taker funding start time as unix timestamp (seconds). - #[arg(long = "taker-funding-start-time")] - taker_funding_start_time: u32, - /// Taker funding end time as unix timestamp (seconds). - #[arg(long = "taker-funding-end-time")] - taker_funding_end_time: u32, - /// Contract expiry time as unix timestamp (seconds). - #[arg(long = "contract-expiry-time")] - contract_expiry_time: u32, - /// Early termination deadline as unix timestamp (seconds). - #[arg(long = "early-termination-end-time")] - early_termination_end_time: u32, - /// Settlement height used for final settlement. - #[arg(long = "settlement-height")] - settlement_height: u32, - /// Principal collateral amount in minimal collateral units. - #[arg(long = "principal-collateral-amount")] - principal_collateral_amount: u64, - /// Incentive fee in basis points (1 bp = 0.01%). - #[arg(long = "incentive-basis-points")] - incentive_basis_points: u64, - /// Filler tokens per principal collateral unit. - #[arg(long = "filler-per-principal-collateral")] - filler_per_principal_collateral: u64, - /// Strike price for the contract (minimal price asset units). - #[arg(long = "strike-price")] - strike_price: u64, - /// Settlement asset entropy as a hex string to be used for this order. - #[arg(long = "settlement-asset-entropy")] - settlement_asset_entropy: String, - /// Oracle public key to use for this init. - #[arg(long = "oracle-pubkey")] - oracle_public_key: String, -} - -impl From for InnerDcdInitParams { - fn from(args: InitOrderArgs) -> Self { - InnerDcdInitParams { - taker_funding_start_time: args.taker_funding_start_time, - taker_funding_end_time: args.taker_funding_end_time, - contract_expiry_time: args.contract_expiry_time, - early_termination_end_time: args.early_termination_end_time, - settlement_height: args.settlement_height, - principal_collateral_amount: args.principal_collateral_amount, - incentive_basis_points: args.incentive_basis_points, - filler_per_principal_collateral: args.filler_per_principal_collateral, - strike_price: args.strike_price, - collateral_asset_id: COLLATERAL_ASSET_ID.to_string(), - settlement_asset_entropy: args.settlement_asset_entropy, - oracle_public_key: args.oracle_public_key, - } - } -} diff --git a/crates/cli-client/src/common/utils.rs b/crates/cli-client/src/common/utils.rs deleted file mode 100644 index a7adbcd..0000000 --- a/crates/cli-client/src/common/utils.rs +++ /dev/null @@ -1,32 +0,0 @@ -use elements::hex::ToHex; -use hex::FromHex; -use simplicityhl::elements::AssetId; -use simplicityhl_core::broadcast_tx; -use std::io::Write; - -pub const DEFAULT_CLIENT_TIMEOUT_SECS: u64 = 10; - -pub(crate) fn write_into_stdout + std::fmt::Debug>(text: T) -> std::io::Result { - let mut output = text.as_ref().to_string(); - output.push('\n'); - std::io::stdout().write(output.as_bytes()) -} - -pub(crate) fn broadcast_tx_inner(tx: &simplicityhl::elements::Transaction) -> crate::error::Result { - broadcast_tx(tx).map_err(|err| crate::error::CliError::Broadcast(err.to_string())) -} - -pub(crate) fn decode_hex(str: impl AsRef<[u8]>) -> crate::error::Result> { - let str_to_convert = str.as_ref(); - hex::decode(str_to_convert).map_err(|err| crate::error::CliError::FromHex(err, str_to_convert.to_hex())) -} - -pub(crate) fn entropy_to_asset_id(el: impl AsRef<[u8]>) -> crate::error::Result { - use simplicity::hashes::sha256; - let el = el.as_ref(); - let mut asset_entropy_bytes = - <[u8; 32]>::from_hex(el).map_err(|err| crate::error::CliError::FromHex(err, el.to_hex()))?; - asset_entropy_bytes.reverse(); - let midstate = sha256::Midstate::from_byte_array(asset_entropy_bytes); - Ok(AssetId::from_entropy(midstate)) -} diff --git a/crates/cli-client/src/config.rs b/crates/cli-client/src/config.rs new file mode 100644 index 0000000..8352536 --- /dev/null +++ b/crates/cli-client/src/config.rs @@ -0,0 +1,134 @@ +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use serde::{Deserialize, Serialize}; +use simplicityhl::elements::AddressParams; + +use crate::error::Error; + +const DEFAULT_CONFIG_PATH: &str = "config.toml"; +const DEFAULT_DATA_DIR: &str = ".data"; +const DEFAULT_DATABASE_FILENAME: &str = "coins.db"; +const DEFAULT_TIMEOUT_SECS: u64 = 30; +const DEFAULT_RELAY: &str = "wss://relay.damus.io"; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Config { + #[serde(default)] + pub network: NetworkConfig, + #[serde(default)] + pub relay: RelayConfig, + #[serde(default)] + pub storage: StorageConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkConfig { + #[serde(default = "default_network")] + pub name: NetworkName, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum NetworkName { + #[default] + Testnet, + Mainnet, +} + +impl NetworkName { + #[must_use] + pub const fn address_params(self) -> &'static AddressParams { + match self { + Self::Testnet => &AddressParams::LIQUID_TESTNET, + Self::Mainnet => &AddressParams::LIQUID, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RelayConfig { + #[serde(default = "default_relays")] + pub urls: Vec, + #[serde(default = "default_timeout")] + pub timeout_secs: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StorageConfig { + #[serde(default = "default_data_dir")] + pub data_dir: PathBuf, +} + +impl Config { + pub fn load(path: impl AsRef) -> Result { + let content = std::fs::read_to_string(path)?; + let config: Self = toml::from_str(&content)?; + Ok(config) + } + + pub fn load_or_default(path: impl AsRef) -> Self { + Self::load(path).unwrap_or_default() + } + + #[must_use] + pub fn database_path(&self) -> PathBuf { + self.storage.data_dir.join(DEFAULT_DATABASE_FILENAME) + } + + #[must_use] + pub const fn address_params(&self) -> &'static AddressParams { + self.network.name.address_params() + } + + #[must_use] + pub const fn relay_timeout(&self) -> Duration { + Duration::from_secs(self.relay.timeout_secs) + } +} + +impl Default for NetworkConfig { + fn default() -> Self { + Self { + name: default_network(), + } + } +} + +impl Default for RelayConfig { + fn default() -> Self { + Self { + urls: default_relays(), + timeout_secs: default_timeout(), + } + } +} + +impl Default for StorageConfig { + fn default() -> Self { + Self { + data_dir: default_data_dir(), + } + } +} + +const fn default_network() -> NetworkName { + NetworkName::Testnet +} + +fn default_relays() -> Vec { + vec![DEFAULT_RELAY.to_string()] +} + +const fn default_timeout() -> u64 { + DEFAULT_TIMEOUT_SECS +} + +fn default_data_dir() -> PathBuf { + PathBuf::from(DEFAULT_DATA_DIR) +} + +#[must_use] +pub fn default_config_path() -> PathBuf { + PathBuf::from(DEFAULT_CONFIG_PATH) +} diff --git a/crates/cli-client/src/contract_handlers/address.rs b/crates/cli-client/src/contract_handlers/address.rs deleted file mode 100644 index 4d07c99..0000000 --- a/crates/cli-client/src/contract_handlers/address.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use elements::bitcoin::XOnlyPublicKey; -use simplicityhl::elements::{Address, AddressParams}; -use simplicityhl_core::get_p2pk_address; - -pub fn handle(index: u32) -> crate::error::Result<(XOnlyPublicKey, Address)> { - let settings = Settings::load()?; - let keypair = derive_keypair_from_index(index, &settings.seed_hex); - let public_key = keypair.x_only_public_key().0; - let address = get_p2pk_address(&public_key, &AddressParams::LIQUID_TESTNET) - .map_err(|err| crate::error::CliError::P2pkAddress(err.to_string()))?; - Ok((public_key, address)) -} diff --git a/crates/cli-client/src/contract_handlers/common.rs b/crates/cli-client/src/contract_handlers/common.rs deleted file mode 100644 index 7b132c7..0000000 --- a/crates/cli-client/src/contract_handlers/common.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::common::broadcast_tx_inner; -use crate::common::store::utils::{OrderParams, save_order_params_by_event_id}; -use dex_nostr_relay::relay_processor::RelayProcessor; -use elements::bitcoin::hex::DisplayHex; -use nostr::EventId; -use simplicity::elements::Transaction; -use simplicity::elements::pset::serialize::Serialize; - -pub async fn get_order_params( - maker_order_event_id: EventId, - relay_processor: &RelayProcessor, -) -> crate::error::Result { - Ok( - if let Ok(x) = crate::common::store::utils::get_order_params_by_event_id(maker_order_event_id) { - x - } else { - let order = relay_processor.get_order_by_id(maker_order_event_id).await?; - save_order_params_by_event_id( - maker_order_event_id, - &order.dcd_taproot_pubkey_gen, - order.dcd_arguments.clone(), - )?; - OrderParams { - taproot_pubkey_gen: order.dcd_taproot_pubkey_gen, - dcd_args: order.dcd_arguments, - } - }, - ) -} - -/// Broadcasts created tx -/// -/// Has to be used with blocking async context to perform properly or just use only sync context. -pub fn broadcast_or_get_raw_tx(is_offline: bool, transaction: &Transaction) -> crate::error::Result<()> { - if is_offline { - println!("Raw Tx: {}", transaction.serialize().to_lower_hex_string()); - } else { - broadcast_tx_inner(transaction)?; - } - Ok(()) -} diff --git a/crates/cli-client/src/contract_handlers/faucet.rs b/crates/cli-client/src/contract_handlers/faucet.rs deleted file mode 100644 index f9af397..0000000 --- a/crates/cli-client/src/contract_handlers/faucet.rs +++ /dev/null @@ -1,158 +0,0 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::common::store::Store; -use crate::contract_handlers::common::broadcast_or_get_raw_tx; -use contracts_adapter::basic::{IssueAssetResponse, ReissueAssetResponse}; -use simplicity::elements::OutPoint; -use simplicity::hashes::sha256::Midstate; -use simplicityhl::elements::{AddressParams, Txid}; -use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, derive_public_blinder_key}; -use tokio::task; - -pub async fn create_asset( - account_index: u32, - asset_name: String, - fee_utxo: OutPoint, - fee_amount: u64, - issue_amount: u64, - is_offline: bool, -) -> crate::error::Result { - task::spawn_blocking(move || { - create_asset_sync( - account_index, - asset_name, - fee_utxo, - fee_amount, - issue_amount, - is_offline, - ) - }) - .await? -} - -fn create_asset_sync( - account_index: u32, - asset_name: String, - fee_utxo: OutPoint, - fee_amount: u64, - issue_amount: u64, - is_offline: bool, -) -> crate::error::Result { - let store = Store::load()?; - - if store.is_exist(&asset_name)? { - return Err(crate::error::CliError::AssetNameExists { name: asset_name }); - } - - let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); - let blinding_key = derive_public_blinder_key(); - - let IssueAssetResponse { - tx: transaction, - asset_id, - reissuance_asset_id, - asset_entropy, - } = contracts_adapter::basic::issue_asset( - &keypair, - &blinding_key, - fee_utxo, - issue_amount, - fee_amount, - &AddressParams::LIQUID_TESTNET, - LIQUID_TESTNET_BITCOIN_ASSET, - *LIQUID_TESTNET_GENESIS, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - println!( - "Test token asset entropy: '{asset_entropy}', asset_id: '{asset_id}', \ - reissue_asset_id: '{reissuance_asset_id}'" - ); - broadcast_or_get_raw_tx(is_offline, &transaction)?; - store.insert_value(asset_name, asset_entropy.as_bytes())?; - - Ok(transaction.txid()) -} - -pub async fn mint_asset( - account_index: u32, - asset_name: String, - reissue_asset_utxo: OutPoint, - fee_utxo: OutPoint, - reissue_amount: u64, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result { - task::spawn_blocking(move || { - mint_asset_sync( - account_index, - asset_name, - reissue_asset_utxo, - fee_utxo, - reissue_amount, - fee_amount, - is_offline, - ) - }) - .await? -} - -fn mint_asset_sync( - account_index: u32, - asset_name: String, - reissue_asset_utxo: OutPoint, - fee_utxo: OutPoint, - reissue_amount: u64, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result { - let store = Store::load()?; - - let Some(asset_entropy) = store.get_value(&asset_name)? else { - return Err(crate::error::CliError::AssetNameExists { name: asset_name }); - }; - - let asset_entropy = String::from_utf8(asset_entropy.to_vec()) - .map_err(|err| crate::error::CliError::Custom(format!("Failed to convert bytes to string, err: {err}")))?; - let asset_entropy = entropy_to_midstate(&asset_entropy)?; - - let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); - - let blinding_key = derive_public_blinder_key(); - let ReissueAssetResponse { - tx: transaction, - asset_id, - reissuance_asset_id, - } = contracts_adapter::basic::reissue_asset( - &keypair, - &blinding_key, - reissue_asset_utxo, - fee_utxo, - reissue_amount, - fee_amount, - asset_entropy, - &AddressParams::LIQUID_TESTNET, - LIQUID_TESTNET_BITCOIN_ASSET, - *LIQUID_TESTNET_GENESIS, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - println!("Minting asset: '{asset_id}', Reissue asset id: '{reissuance_asset_id}'"); - broadcast_or_get_raw_tx(is_offline, &transaction)?; - - Ok(transaction.txid()) -} - -pub fn entropy_to_midstate(el: impl AsRef<[u8]>) -> crate::error::Result { - use elements::hex::ToHex; - use hex::FromHex; - use simplicity::hashes::sha256; - let el = el.as_ref(); - let mut asset_entropy_bytes = - <[u8; 32]>::from_hex(el).map_err(|err| crate::error::CliError::FromHex(err, el.to_hex()))?; - asset_entropy_bytes.reverse(); - let midstate = sha256::Midstate::from_byte_array(asset_entropy_bytes); - Ok(midstate) -} diff --git a/crates/cli-client/src/contract_handlers/maker_funding.rs b/crates/cli-client/src/contract_handlers/maker_funding.rs deleted file mode 100644 index 671b77f..0000000 --- a/crates/cli-client/src/contract_handlers/maker_funding.rs +++ /dev/null @@ -1,202 +0,0 @@ -use crate::common::decode_hex; -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::common::store::SledError; -use crate::contract_handlers::common::broadcast_or_get_raw_tx; -use contracts::DCDArguments; -use contracts_adapter::dcd::{ - AssetEntropyProcessed, BaseContractContext, COLLATERAL_ASSET_ID, CreationContext, DcdContractContext, DcdManager, - MakerFundingContext, raw_asset_entropy_bytes_to_midstate, -}; -use dex_nostr_relay::relay_processor::OrderPlaceEventTags; -use elements::bitcoin::secp256k1; -use simplicity::elements::OutPoint; -use simplicityhl::elements::{AddressParams, AssetId, Txid}; -use simplicityhl_core::{ - AssetEntropyHex, LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, TaprootPubkeyGen, derive_public_blinder_key, -}; -use tokio::task; -use tracing::instrument; - -#[derive(Debug)] -pub struct ProcessedArgs { - keypair: secp256k1::Keypair, - dcd_arguments: DCDArguments, - dcd_taproot_pubkey_gen: String, - filler_token_entropy: AssetEntropyHex, - grantor_collateral_token_entropy: AssetEntropyHex, - grantor_settlement_token_entropy: AssetEntropyHex, -} - -#[derive(Debug)] -pub struct ArgsToSave { - taproot_pubkey_gen: TaprootPubkeyGen, - dcd_arguments: DCDArguments, -} - -#[derive(Debug)] -pub struct Utxos { - pub filler_token: OutPoint, - pub grantor_collateral_token: OutPoint, - pub grantor_settlement_token: OutPoint, - pub settlement_asset: OutPoint, - pub fee: OutPoint, -} - -impl ProcessedArgs { - pub fn extract_event(&self) -> OrderPlaceEventTags { - let convert_entropy_to_asset_id = |x: &str| { - let x = hex::decode(x).unwrap(); - let token_entropy = contracts_adapter::dcd::convert_bytes_to_asset_entropy(x).unwrap(); - let AssetEntropyProcessed { - entropy: filler_token_asset_entropy, - reversed_bytes: _filler_reversed_bytes, - } = raw_asset_entropy_bytes_to_midstate(token_entropy); - - AssetId::from_entropy(filler_token_asset_entropy) - }; - - let filler_asset_id = convert_entropy_to_asset_id(&self.filler_token_entropy); - let grantor_collateral_asset_id = convert_entropy_to_asset_id(&self.grantor_collateral_token_entropy); - let grantor_settlement_asset_id = convert_entropy_to_asset_id(&self.grantor_settlement_token_entropy); - let settlement_asset_id = convert_entropy_to_asset_id(&self.dcd_arguments.settlement_asset_id_hex_le); - let collateral_asset_id = COLLATERAL_ASSET_ID; - - OrderPlaceEventTags { - dcd_arguments: self.dcd_arguments.clone(), - dcd_taproot_pubkey_gen: self.dcd_taproot_pubkey_gen.clone(), - filler_asset_id, - grantor_collateral_asset_id, - grantor_settlement_asset_id, - settlement_asset_id, - collateral_asset_id, - } - } -} - -#[instrument(level = "debug", skip_all, err)] -pub fn process_args( - account_index: u32, - dcd_taproot_pubkey_gen: impl AsRef, -) -> crate::error::Result { - let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); - - let taproot_pubkey_gen = dcd_taproot_pubkey_gen.as_ref().to_string(); - - let args = { - let dcd_args = crate::common::store::utils::get_dcd_args(&taproot_pubkey_gen)?; - let filler_token_entropy = crate::common::store::utils::get_filler_token_entropy(&taproot_pubkey_gen)?; - let grantor_collateral_token_entropy = - crate::common::store::utils::get_grantor_collateral_token_entropy(&taproot_pubkey_gen)?; - let grantor_settlement_token_entropy = - crate::common::store::utils::get_grantor_settlement_token_entropy(&taproot_pubkey_gen)?; - - ProcessedArgs { - keypair, - dcd_arguments: dcd_args, - dcd_taproot_pubkey_gen: taproot_pubkey_gen, - filler_token_entropy, - grantor_collateral_token_entropy, - grantor_settlement_token_entropy, - } - }; - Ok(args) -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn handle( - processed_args: ProcessedArgs, - utxos: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - task::spawn_blocking(move || handle_sync(processed_args, utxos, fee_amount, is_offline)).await? -} - -#[instrument(level = "debug", skip_all, err)] -fn handle_sync( - ProcessedArgs { - keypair, - dcd_arguments, - dcd_taproot_pubkey_gen, - filler_token_entropy, - grantor_collateral_token_entropy, - grantor_settlement_token_entropy, - }: ProcessedArgs, - Utxos { - filler_token: filler_token_utxo, - grantor_collateral_token: grantor_collateral_token_utxo, - grantor_settlement_token: grantor_settlement_token_utxo, - settlement_asset: settlement_asset_utxo, - fee: fee_utxo, - }: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - let filler_token_info = (filler_token_utxo, decode_hex(filler_token_entropy)?); - let grantor_collateral_token_info = ( - grantor_collateral_token_utxo, - decode_hex(grantor_collateral_token_entropy)?, - ); - let grantor_settlement_token_info = ( - grantor_settlement_token_utxo, - decode_hex(grantor_settlement_token_entropy)?, - ); - - let base_contract_context = BaseContractContext { - address_params: &AddressParams::LIQUID_TESTNET, - lbtc_asset: LIQUID_TESTNET_BITCOIN_ASSET, - genesis_block_hash: *LIQUID_TESTNET_GENESIS, - }; - let dcd_taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - &dcd_taproot_pubkey_gen, - &dcd_arguments, - base_contract_context.address_params, - &contracts::get_dcd_address, - ) - .map_err(|e| SledError::TapRootGen(e.to_string()))?; - tracing::debug!("=== dcd arguments: {:?}", dcd_arguments); - - let transaction = DcdManager::maker_funding( - &CreationContext { - keypair, - blinding_key: derive_public_blinder_key(), - }, - MakerFundingContext { - filler_token_info, - grantor_collateral_token_info, - grantor_settlement_token_info, - settlement_asset_utxo, - fee_utxo, - fee_amount, - }, - &DcdContractContext { - dcd_taproot_pubkey_gen: dcd_taproot_pubkey_gen.clone(), - dcd_arguments: dcd_arguments.clone(), - base_contract_context, - }, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - broadcast_or_get_raw_tx(is_offline, &transaction)?; - - Ok(( - transaction.txid(), - ArgsToSave { - taproot_pubkey_gen: dcd_taproot_pubkey_gen, - dcd_arguments, - }, - )) -} - -pub fn save_args_to_cache( - ArgsToSave { - taproot_pubkey_gen, - dcd_arguments, - }: &ArgsToSave, -) -> crate::error::Result<()> { - crate::common::store::utils::save_dcd_args(taproot_pubkey_gen, dcd_arguments)?; - Ok(()) -} diff --git a/crates/cli-client/src/contract_handlers/maker_init.rs b/crates/cli-client/src/contract_handlers/maker_init.rs deleted file mode 100644 index 40517e8..0000000 --- a/crates/cli-client/src/contract_handlers/maker_init.rs +++ /dev/null @@ -1,184 +0,0 @@ -use crate::common::entropy_to_asset_id; -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::contract_handlers::common::broadcast_or_get_raw_tx; -use contracts::DCDArguments; -use contracts_adapter::dcd::{ - BaseContractContext, CreationContext, DcdInitParams, DcdInitResponse, DcdManager, FillerTokenEntropyHex, - GrantorCollateralAssetEntropyHex, GrantorSettlementAssetEntropyHex, MakerInitContext, -}; -use elements::bitcoin::secp256k1; -use simplicity::elements::OutPoint; -use simplicityhl::elements::{AddressParams, Txid}; -use simplicityhl_core::{ - AssetEntropyHex, AssetIdHex, LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, TaprootPubkeyGen, - derive_public_blinder_key, -}; -use tokio::task; -use tracing::instrument; - -#[derive(Debug)] -pub struct InnerDcdInitParams { - pub taker_funding_start_time: u32, - pub taker_funding_end_time: u32, - pub contract_expiry_time: u32, - pub early_termination_end_time: u32, - pub settlement_height: u32, - pub principal_collateral_amount: u64, - pub incentive_basis_points: u64, - pub filler_per_principal_collateral: u64, - pub strike_price: u64, - pub collateral_asset_id: AssetIdHex, - pub settlement_asset_entropy: AssetEntropyHex, - pub oracle_public_key: String, -} - -#[derive(Debug)] -pub struct ProcessedArgs { - keypair: secp256k1::Keypair, - dcd_init_params: DcdInitParams, -} - -pub struct ArgsToSave { - pub filler_token_entropy: FillerTokenEntropyHex, - pub grantor_collateral_token_entropy: GrantorCollateralAssetEntropyHex, - pub grantor_settlement_token_entropy: GrantorSettlementAssetEntropyHex, - pub taproot_pubkey: TaprootPubkeyGen, - pub dcd_args: DCDArguments, -} - -#[derive(Debug)] -pub struct Utxos { - pub first: OutPoint, - pub second: OutPoint, - pub third: OutPoint, -} - -impl TryInto for InnerDcdInitParams { - type Error = anyhow::Error; - - fn try_into(self) -> Result { - Ok(DcdInitParams { - taker_funding_start_time: self.taker_funding_start_time, - taker_funding_end_time: self.taker_funding_end_time, - contract_expiry_time: self.contract_expiry_time, - early_termination_end_time: self.early_termination_end_time, - settlement_height: self.settlement_height, - principal_collateral_amount: self.principal_collateral_amount, - incentive_basis_points: self.incentive_basis_points, - filler_per_principal_collateral: self.filler_per_principal_collateral, - strike_price: self.strike_price, - collateral_asset_id: self.collateral_asset_id, - settlement_asset_id: entropy_to_asset_id(self.settlement_asset_entropy)?.to_string(), - oracle_public_key: self.oracle_public_key.clone(), - // TODO(Illia): replace with actual data - fee_script_hash: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), - fee_basis_points: 0, - }) - } -} - -#[instrument(level = "debug", skip_all, err)] -pub fn process_args(account_index: u32, dcd_init_params: InnerDcdInitParams) -> crate::error::Result { - let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); - - let dcd_init_params: DcdInitParams = dcd_init_params - .try_into() - .map_err(|err: anyhow::Error| crate::error::CliError::InnerDcdConversion(err.to_string()))?; - - Ok(ProcessedArgs { - keypair, - dcd_init_params, - }) -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn handle( - processed_args: ProcessedArgs, - utxos: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - task::spawn_blocking(move || handle_sync(processed_args, utxos, fee_amount, is_offline)).await? -} - -#[instrument(level = "debug", skip_all, err)] -fn handle_sync( - ProcessedArgs { - keypair, - dcd_init_params, - }: ProcessedArgs, - Utxos { - first: first_lbtc_utxo, - second: second_lbtc_utxo, - third: third_lbtc_utxo, - }: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - let DcdInitResponse { - tx: transaction, - filler_token_entropy, - grantor_collateral_token_entropy, - grantor_settlement_token_entropy, - taproot_pubkey_gen, - dcd_args, - } = DcdManager::maker_init( - &CreationContext { - keypair, - blinding_key: derive_public_blinder_key(), - }, - MakerInitContext { - input_utxos: [first_lbtc_utxo, second_lbtc_utxo, third_lbtc_utxo], - dcd_init_params, - fee_amount, - }, - &BaseContractContext { - address_params: &AddressParams::LIQUID_TESTNET, - lbtc_asset: LIQUID_TESTNET_BITCOIN_ASSET, - genesis_block_hash: *LIQUID_TESTNET_GENESIS, - }, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - println!( - "Filler_token_entropy: '{}', grantor_collateral_entropy: '{}', grantor_settlement: '{}', taproot_pubkey: '{}', dcd_args: '{dcd_args:#?}'", - filler_token_entropy, grantor_collateral_token_entropy, grantor_settlement_token_entropy, taproot_pubkey_gen - ); - - broadcast_or_get_raw_tx(is_offline, &transaction)?; - - let args_to_save = ArgsToSave { - filler_token_entropy, - grantor_collateral_token_entropy, - grantor_settlement_token_entropy, - taproot_pubkey: taproot_pubkey_gen, - dcd_args, - }; - Ok((transaction.txid(), args_to_save)) -} - -#[instrument(level = "debug", skip_all, err)] -pub fn save_args_to_cache( - ArgsToSave { - filler_token_entropy, - grantor_collateral_token_entropy, - grantor_settlement_token_entropy, - taproot_pubkey, - dcd_args, - }: &ArgsToSave, -) -> crate::error::Result<()> { - crate::common::store::utils::save_filler_token_entropy(taproot_pubkey, filler_token_entropy)?; - crate::common::store::utils::save_grantor_collateral_token_entropy( - taproot_pubkey, - grantor_collateral_token_entropy, - )?; - crate::common::store::utils::save_grantor_settlement_token_entropy( - taproot_pubkey, - grantor_settlement_token_entropy, - )?; - crate::common::store::utils::save_dcd_args(taproot_pubkey, dcd_args)?; - Ok(()) -} diff --git a/crates/cli-client/src/contract_handlers/maker_settlement.rs b/crates/cli-client/src/contract_handlers/maker_settlement.rs deleted file mode 100644 index c226547..0000000 --- a/crates/cli-client/src/contract_handlers/maker_settlement.rs +++ /dev/null @@ -1,150 +0,0 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::common::store::SledError; -use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; -use contracts::DCDArguments; -use contracts_adapter::dcd::{ - BaseContractContext, CommonContext, DcdContractContext, DcdManager, MakerSettlementContext, -}; -use dex_nostr_relay::relay_processor::RelayProcessor; -use elements::bitcoin::secp256k1; -use nostr::EventId; -use simplicity::elements::OutPoint; -use simplicityhl::elements::{AddressParams, Txid}; -use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, TaprootPubkeyGen}; -use tokio::task; -use tracing::instrument; - -#[derive(Debug)] -pub struct ProcessedArgs { - keypair: secp256k1::Keypair, - dcd_arguments: DCDArguments, - dcd_taproot_pubkey_gen: String, - price_at_current_block_height: u64, - oracle_signature: String, - grantor_amount_to_burn: u64, -} - -#[derive(Debug)] -pub struct Utxos { - pub grantor_collateral_token: OutPoint, - pub grantor_settlement_token: OutPoint, - pub fee: OutPoint, - pub asset: OutPoint, -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn process_args( - account_index: u32, - price_at_current_block_height: u64, - oracle_signature: String, - grantor_amount_to_burn: u64, - maker_order_event_id: EventId, - relay_processor: &RelayProcessor, -) -> crate::error::Result { - let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); - - let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; - - Ok(ProcessedArgs { - keypair, - dcd_arguments: order_params.dcd_args, - dcd_taproot_pubkey_gen: order_params.taproot_pubkey_gen, - price_at_current_block_height, - oracle_signature, - grantor_amount_to_burn, - }) -} -#[derive(Debug)] -pub struct ArgsToSave { - taproot_pubkey_gen: TaprootPubkeyGen, - dcd_arguments: DCDArguments, -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn handle( - processed_args: ProcessedArgs, - utxos: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - // offload blocking/contracts work, like in split_utxo::handle - task::spawn_blocking(move || handle_sync(processed_args, utxos, fee_amount, is_offline)).await? -} - -#[instrument(level = "debug", skip_all, err)] -fn handle_sync( - ProcessedArgs { - keypair, - dcd_arguments, - dcd_taproot_pubkey_gen, - price_at_current_block_height, - oracle_signature, - grantor_amount_to_burn, - }: ProcessedArgs, - Utxos { - grantor_collateral_token: grantor_collateral_token_utxo, - grantor_settlement_token: grantor_settlement_token_utxo, - fee: fee_utxo, - asset: asset_utxo, - }: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - tracing::debug!("=== dcd arguments: {:?}", dcd_arguments); - let base_contract_context = BaseContractContext { - address_params: &AddressParams::LIQUID_TESTNET, - lbtc_asset: LIQUID_TESTNET_BITCOIN_ASSET, - genesis_block_hash: *LIQUID_TESTNET_GENESIS, - }; - let dcd_taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - &dcd_taproot_pubkey_gen, - &dcd_arguments, - base_contract_context.address_params, - &contracts::get_dcd_address, - ) - .map_err(|e| SledError::TapRootGen(e.to_string()))?; - - let transaction = DcdManager::maker_settlement( - &CommonContext { keypair }, - MakerSettlementContext { - asset_utxo, - grantor_collateral_token_utxo, - grantor_settlement_token_utxo, - fee_utxo, - fee_amount, - price_at_current_block_height, - oracle_signature, - grantor_amount_to_burn, - }, - &DcdContractContext { - dcd_taproot_pubkey_gen: dcd_taproot_pubkey_gen.clone(), - dcd_arguments: dcd_arguments.clone(), - base_contract_context, - }, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - broadcast_or_get_raw_tx(is_offline, &transaction)?; - - Ok(( - transaction.txid(), - ArgsToSave { - taproot_pubkey_gen: dcd_taproot_pubkey_gen, - dcd_arguments, - }, - )) -} - -pub fn save_args_to_cache( - ArgsToSave { - taproot_pubkey_gen, - dcd_arguments, - }: &ArgsToSave, -) -> crate::error::Result<()> { - crate::common::store::utils::save_dcd_args(taproot_pubkey_gen, dcd_arguments)?; - Ok(()) -} diff --git a/crates/cli-client/src/contract_handlers/maker_termination_collateral.rs b/crates/cli-client/src/contract_handlers/maker_termination_collateral.rs deleted file mode 100644 index b684e55..0000000 --- a/crates/cli-client/src/contract_handlers/maker_termination_collateral.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::common::store::SledError; -use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; -use contracts::DCDArguments; -use contracts_adapter::dcd::{ - BaseContractContext, CommonContext, DcdContractContext, DcdManager, MakerTerminationCollateralContext, -}; -use dex_nostr_relay::relay_processor::RelayProcessor; -use elements::bitcoin::secp256k1; -use nostr::EventId; -use simplicity::elements::OutPoint; -use simplicityhl::elements::{AddressParams, Txid}; -use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, TaprootPubkeyGen}; -use tokio::task; -use tracing::instrument; - -#[derive(Debug)] -pub struct ProcessedArgs { - keypair: secp256k1::Keypair, - dcd_arguments: DCDArguments, - dcd_taproot_pubkey_gen: String, - grantor_collateral_amount_to_burn: u64, -} - -#[derive(Debug)] -pub struct Utxos { - pub grantor_collateral_token: OutPoint, - pub fee: OutPoint, - pub collateral_token: OutPoint, -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn process_args( - account_index: u32, - grantor_collateral_amount_to_burn: u64, - maker_order_event_id: EventId, - relay_processor: &RelayProcessor, -) -> crate::error::Result { - let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); - - let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; - - Ok(ProcessedArgs { - keypair, - dcd_arguments: order_params.dcd_args, - dcd_taproot_pubkey_gen: order_params.taproot_pubkey_gen, - grantor_collateral_amount_to_burn, - }) -} -#[derive(Debug)] -pub struct ArgsToSave { - taproot_pubkey_gen: TaprootPubkeyGen, - dcd_arguments: DCDArguments, -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn handle( - processed_args: ProcessedArgs, - utxos: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - task::spawn_blocking(move || handle_sync(processed_args, utxos, fee_amount, is_offline)).await? -} - -#[instrument(level = "debug", skip_all, err)] -fn handle_sync( - ProcessedArgs { - keypair, - dcd_arguments, - dcd_taproot_pubkey_gen, - grantor_collateral_amount_to_burn, - }: ProcessedArgs, - Utxos { - grantor_collateral_token: grantor_collateral_token_utxo, - fee: fee_utxo, - collateral_token: collateral_token_utxo, - }: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - tracing::debug!("=== dcd arguments: {:?}", dcd_arguments); - let base_contract_context = BaseContractContext { - address_params: &AddressParams::LIQUID_TESTNET, - lbtc_asset: LIQUID_TESTNET_BITCOIN_ASSET, - genesis_block_hash: *LIQUID_TESTNET_GENESIS, - }; - let dcd_taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - &dcd_taproot_pubkey_gen, - &dcd_arguments, - base_contract_context.address_params, - &contracts::get_dcd_address, - ) - .map_err(|e| SledError::TapRootGen(e.to_string()))?; - - let transaction = DcdManager::maker_collateral_termination( - &CommonContext { keypair }, - MakerTerminationCollateralContext { - collateral_token_utxo, - grantor_collateral_token_utxo, - fee_utxo, - fee_amount, - grantor_collateral_amount_to_burn, - }, - &DcdContractContext { - dcd_taproot_pubkey_gen: dcd_taproot_pubkey_gen.clone(), - dcd_arguments: dcd_arguments.clone(), - base_contract_context, - }, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - broadcast_or_get_raw_tx(is_offline, &transaction)?; - - Ok(( - transaction.txid(), - ArgsToSave { - taproot_pubkey_gen: dcd_taproot_pubkey_gen, - dcd_arguments, - }, - )) -} - -pub fn save_args_to_cache( - ArgsToSave { - taproot_pubkey_gen, - dcd_arguments, - }: &ArgsToSave, -) -> crate::error::Result<()> { - crate::common::store::utils::save_dcd_args(taproot_pubkey_gen, dcd_arguments)?; - Ok(()) -} diff --git a/crates/cli-client/src/contract_handlers/maker_termination_settlement.rs b/crates/cli-client/src/contract_handlers/maker_termination_settlement.rs deleted file mode 100644 index 676cc73..0000000 --- a/crates/cli-client/src/contract_handlers/maker_termination_settlement.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::common::store::SledError; -use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; -use contracts::DCDArguments; -use contracts_adapter::dcd::{ - BaseContractContext, CommonContext, DcdContractContext, DcdManager, MakerTerminationSettlementContext, -}; -use dex_nostr_relay::relay_processor::RelayProcessor; -use elements::bitcoin::secp256k1; -use nostr::EventId; -use simplicity::elements::OutPoint; -use simplicityhl::elements::{AddressParams, Txid}; -use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, TaprootPubkeyGen}; -use tokio::task; -use tracing::instrument; - -#[derive(Debug)] -pub struct ProcessedArgs { - keypair: secp256k1::Keypair, - dcd_arguments: DCDArguments, - dcd_taproot_pubkey_gen: String, - grantor_settlement_amount_to_burn: u64, -} - -#[derive(Debug)] -pub struct ArgsToSave { - taproot_pubkey_gen: TaprootPubkeyGen, - dcd_arguments: DCDArguments, -} - -#[derive(Debug)] -pub struct Utxos { - pub fee: OutPoint, - pub settlement_asset: OutPoint, - pub grantor_settlement_token: OutPoint, -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn process_args( - account_index: u32, - grantor_settlement_amount_to_burn: u64, - maker_order_event_id: EventId, - relay_processor: &RelayProcessor, -) -> crate::error::Result { - let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); - - let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; - - Ok(ProcessedArgs { - keypair, - dcd_arguments: order_params.dcd_args, - dcd_taproot_pubkey_gen: order_params.taproot_pubkey_gen, - grantor_settlement_amount_to_burn, - }) -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn handle( - processed_args: ProcessedArgs, - utxos: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - task::spawn_blocking(move || handle_sync(processed_args, utxos, fee_amount, is_offline)).await? -} - -#[instrument(level = "debug", skip_all, err)] -fn handle_sync( - ProcessedArgs { - keypair, - dcd_arguments, - dcd_taproot_pubkey_gen, - grantor_settlement_amount_to_burn, - }: ProcessedArgs, - Utxos { - fee: fee_utxo, - settlement_asset: settlement_asset_utxo, - grantor_settlement_token: grantor_settlement_token_utxo, - }: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - tracing::debug!("=== dcd arguments: {:?}", dcd_arguments); - let base_contract_context = BaseContractContext { - address_params: &AddressParams::LIQUID_TESTNET, - lbtc_asset: LIQUID_TESTNET_BITCOIN_ASSET, - genesis_block_hash: *LIQUID_TESTNET_GENESIS, - }; - let dcd_taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - &dcd_taproot_pubkey_gen, - &dcd_arguments, - base_contract_context.address_params, - &contracts::get_dcd_address, - ) - .map_err(|e| SledError::TapRootGen(e.to_string()))?; - - let transaction = DcdManager::maker_settlement_termination( - &CommonContext { keypair }, - MakerTerminationSettlementContext { - settlement_asset_utxo, - grantor_settlement_token_utxo, - fee_utxo, - fee_amount, - grantor_settlement_amount_to_burn, - }, - &DcdContractContext { - dcd_taproot_pubkey_gen: dcd_taproot_pubkey_gen.clone(), - dcd_arguments: dcd_arguments.clone(), - base_contract_context, - }, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - broadcast_or_get_raw_tx(is_offline, &transaction)?; - - Ok(( - transaction.txid(), - ArgsToSave { - taproot_pubkey_gen: dcd_taproot_pubkey_gen, - dcd_arguments, - }, - )) -} - -pub fn save_args_to_cache( - ArgsToSave { - taproot_pubkey_gen, - dcd_arguments, - }: &ArgsToSave, -) -> crate::error::Result<()> { - crate::common::store::utils::save_dcd_args(taproot_pubkey_gen, dcd_arguments)?; - Ok(()) -} diff --git a/crates/cli-client/src/contract_handlers/merge_tokens.rs b/crates/cli-client/src/contract_handlers/merge_tokens.rs deleted file mode 100644 index cdbe854..0000000 --- a/crates/cli-client/src/contract_handlers/merge_tokens.rs +++ /dev/null @@ -1,319 +0,0 @@ -use crate::common::broadcast_tx_inner; -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::common::store::SledError; -use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::get_order_params; -use contracts::DCDArguments; -use contracts_adapter::dcd::{BaseContractContext, CommonContext, DcdContractContext, DcdManager}; -use dex_nostr_relay::relay_processor::RelayProcessor; -use elements::bitcoin::hex::DisplayHex; -use elements::bitcoin::secp256k1; -use nostr::EventId; -use simplicity::elements::OutPoint; -use simplicity::elements::pset::serialize::Serialize; -use simplicityhl::elements::{AddressParams, Txid}; -use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, TaprootPubkeyGen}; -use tracing::instrument; - -#[derive(Debug)] -pub struct ProcessedArgs { - keypair: secp256k1::Keypair, - dcd_arguments: DCDArguments, - dcd_taproot_pubkey_gen: String, -} - -#[derive(Debug)] -pub struct ArgsToSave { - taproot_pubkey_gen: TaprootPubkeyGen, - dcd_arguments: DCDArguments, -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn process_args( - account_index: u32, - maker_order_event_id: EventId, - relay_processor: &RelayProcessor, -) -> crate::error::Result { - let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); - - let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; - - Ok(ProcessedArgs { - keypair, - dcd_arguments: order_params.dcd_args, - dcd_taproot_pubkey_gen: order_params.taproot_pubkey_gen, - }) -} -pub mod merge2 { - use super::{ - AddressParams, ArgsToSave, BaseContractContext, CommonContext, DcdContractContext, DcdManager, DisplayHex, - LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, OutPoint, ProcessedArgs, Serialize, SledError, - TaprootPubkeyGen, Txid, broadcast_tx_inner, instrument, - }; - use contracts::MergeBranch; - use contracts_adapter::dcd::MergeTokensContext; - use tokio::task; - - #[derive(Debug)] - pub struct Utxos2 { - pub utxo_1: OutPoint, - pub utxo_2: OutPoint, - pub fee: OutPoint, - } - - #[instrument(level = "debug", skip_all, err)] - pub async fn handle( - processed_args: ProcessedArgs, - utxos: Utxos2, - fee_amount: u64, - is_offline: bool, - ) -> crate::error::Result<(Txid, ArgsToSave)> { - task::spawn_blocking(move || handle_sync(processed_args, utxos, fee_amount, is_offline)).await? - } - - #[instrument(level = "debug", skip_all, err)] - fn handle_sync( - ProcessedArgs { - keypair, - dcd_arguments, - dcd_taproot_pubkey_gen, - }: ProcessedArgs, - Utxos2 { utxo_1, utxo_2, fee }: Utxos2, - fee_amount: u64, - is_offline: bool, - ) -> crate::error::Result<(Txid, ArgsToSave)> { - tracing::debug!("=== dcd arguments: {:?}", dcd_arguments); - let base_contract_context = BaseContractContext { - address_params: &AddressParams::LIQUID_TESTNET, - lbtc_asset: LIQUID_TESTNET_BITCOIN_ASSET, - genesis_block_hash: *LIQUID_TESTNET_GENESIS, - }; - let dcd_taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - &dcd_taproot_pubkey_gen, - &dcd_arguments, - base_contract_context.address_params, - &contracts::get_dcd_address, - ) - .map_err(|e| SledError::TapRootGen(e.to_string()))?; - - let transaction = DcdManager::merge_tokens( - &CommonContext { keypair }, - MergeTokensContext { - token_utxos: vec![utxo_1, utxo_2], - fee_utxo: fee, - fee_amount, - merge_branch: MergeBranch::Two, - }, - &DcdContractContext { - dcd_taproot_pubkey_gen: dcd_taproot_pubkey_gen.clone(), - dcd_arguments: dcd_arguments.clone(), - base_contract_context, - }, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - if is_offline { - println!("{}", transaction.serialize().to_lower_hex_string()); - } else { - println!("Broadcasted txid: {}", broadcast_tx_inner(&transaction)?); - } - - Ok(( - transaction.txid(), - ArgsToSave { - taproot_pubkey_gen: dcd_taproot_pubkey_gen, - dcd_arguments, - }, - )) - } -} -pub mod merge3 { - use super::{ - AddressParams, ArgsToSave, BaseContractContext, CommonContext, DcdContractContext, DcdManager, DisplayHex, - LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, OutPoint, ProcessedArgs, Serialize, SledError, - TaprootPubkeyGen, Txid, broadcast_tx_inner, instrument, - }; - use contracts::MergeBranch; - use contracts_adapter::dcd::MergeTokensContext; - use tokio::task; - - #[derive(Debug)] - pub struct Utxos3 { - pub utxo_1: OutPoint, - pub utxo_2: OutPoint, - pub utxo_3: OutPoint, - pub fee: OutPoint, - } - - #[instrument(level = "debug", skip_all, err)] - pub async fn handle( - processed_args: ProcessedArgs, - utxos: Utxos3, - fee_amount: u64, - is_offline: bool, - ) -> crate::error::Result<(Txid, ArgsToSave)> { - task::spawn_blocking(move || handle_sync(processed_args, utxos, fee_amount, is_offline)).await? - } - - #[instrument(level = "debug", skip_all, err)] - fn handle_sync( - ProcessedArgs { - keypair, - dcd_arguments, - dcd_taproot_pubkey_gen, - }: ProcessedArgs, - Utxos3 { - utxo_1, - utxo_2, - utxo_3, - fee, - }: Utxos3, - fee_amount: u64, - is_offline: bool, - ) -> crate::error::Result<(Txid, ArgsToSave)> { - tracing::debug!("=== dcd arguments: {:?}", dcd_arguments); - let base_contract_context = BaseContractContext { - address_params: &AddressParams::LIQUID_TESTNET, - lbtc_asset: LIQUID_TESTNET_BITCOIN_ASSET, - genesis_block_hash: *LIQUID_TESTNET_GENESIS, - }; - let dcd_taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - &dcd_taproot_pubkey_gen, - &dcd_arguments, - base_contract_context.address_params, - &contracts::get_dcd_address, - ) - .map_err(|e| SledError::TapRootGen(e.to_string()))?; - - let transaction = DcdManager::merge_tokens( - &CommonContext { keypair }, - MergeTokensContext { - token_utxos: vec![utxo_1, utxo_2, utxo_3], - fee_utxo: fee, - fee_amount, - merge_branch: MergeBranch::Three, - }, - &DcdContractContext { - dcd_taproot_pubkey_gen: dcd_taproot_pubkey_gen.clone(), - dcd_arguments: dcd_arguments.clone(), - base_contract_context, - }, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - if is_offline { - println!("{}", transaction.serialize().to_lower_hex_string()); - } else { - println!("Broadcasted txid: {}", broadcast_tx_inner(&transaction)?); - } - - Ok(( - transaction.txid(), - ArgsToSave { - taproot_pubkey_gen: dcd_taproot_pubkey_gen, - dcd_arguments, - }, - )) - } -} -pub mod merge4 { - use super::{ - AddressParams, ArgsToSave, BaseContractContext, CommonContext, DcdContractContext, DcdManager, - LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, OutPoint, ProcessedArgs, SledError, TaprootPubkeyGen, - Txid, instrument, - }; - use crate::contract_handlers::common::broadcast_or_get_raw_tx; - use contracts::MergeBranch; - use contracts_adapter::dcd::MergeTokensContext; - use tokio::task; - - #[derive(Debug)] - pub struct Utxos4 { - pub utxo_1: OutPoint, - pub utxo_2: OutPoint, - pub utxo_3: OutPoint, - pub utxo_4: OutPoint, - pub fee: OutPoint, - } - - #[instrument(level = "debug", skip_all, err)] - pub async fn handle( - processed_args: ProcessedArgs, - utxos: Utxos4, - fee_amount: u64, - is_offline: bool, - ) -> crate::error::Result<(Txid, ArgsToSave)> { - task::spawn_blocking(move || handle_sync(processed_args, utxos, fee_amount, is_offline)).await? - } - - #[instrument(level = "debug", skip_all, err)] - fn handle_sync( - ProcessedArgs { - keypair, - dcd_arguments, - dcd_taproot_pubkey_gen, - }: ProcessedArgs, - Utxos4 { - utxo_1, - utxo_2, - utxo_3, - utxo_4, - fee, - }: Utxos4, - fee_amount: u64, - is_offline: bool, - ) -> crate::error::Result<(Txid, ArgsToSave)> { - tracing::debug!("=== dcd arguments: {:?}", dcd_arguments); - let base_contract_context = BaseContractContext { - address_params: &AddressParams::LIQUID_TESTNET, - lbtc_asset: LIQUID_TESTNET_BITCOIN_ASSET, - genesis_block_hash: *LIQUID_TESTNET_GENESIS, - }; - let dcd_taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - &dcd_taproot_pubkey_gen, - &dcd_arguments, - base_contract_context.address_params, - &contracts::get_dcd_address, - ) - .map_err(|e| SledError::TapRootGen(e.to_string()))?; - - let transaction = DcdManager::merge_tokens( - &CommonContext { keypair }, - MergeTokensContext { - token_utxos: vec![utxo_1, utxo_2, utxo_3, utxo_4], - fee_utxo: fee, - fee_amount, - merge_branch: MergeBranch::Two, - }, - &DcdContractContext { - dcd_taproot_pubkey_gen: dcd_taproot_pubkey_gen.clone(), - dcd_arguments: dcd_arguments.clone(), - base_contract_context, - }, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - broadcast_or_get_raw_tx(is_offline, &transaction)?; - - Ok(( - transaction.txid(), - ArgsToSave { - taproot_pubkey_gen: dcd_taproot_pubkey_gen, - dcd_arguments, - }, - )) - } -} - -pub fn save_args_to_cache( - ArgsToSave { - taproot_pubkey_gen, - dcd_arguments, - }: &ArgsToSave, -) -> crate::error::Result<()> { - crate::common::store::utils::save_dcd_args(taproot_pubkey_gen, dcd_arguments)?; - Ok(()) -} diff --git a/crates/cli-client/src/contract_handlers/mod.rs b/crates/cli-client/src/contract_handlers/mod.rs deleted file mode 100644 index b1db9db..0000000 --- a/crates/cli-client/src/contract_handlers/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub(crate) mod address; -pub(crate) mod common; -pub(crate) mod faucet; -pub(crate) mod maker_funding; -pub(crate) mod maker_init; -pub(crate) mod maker_settlement; -pub(crate) mod maker_termination_collateral; -pub(crate) mod maker_termination_settlement; -pub(crate) mod merge_tokens; -pub(crate) mod oracle_signature; -pub(crate) mod split_utxo; -pub(crate) mod taker_early_termination; -pub(crate) mod taker_funding; -pub(crate) mod taker_settlement; diff --git a/crates/cli-client/src/contract_handlers/oracle_signature.rs b/crates/cli-client/src/contract_handlers/oracle_signature.rs deleted file mode 100644 index 15eeed0..0000000 --- a/crates/cli-client/src/contract_handlers/oracle_signature.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use contracts::oracle_msg; -use elements::bitcoin::secp256k1; -use elements::secp256k1_zkp::Message; -use nostr::prelude::Signature; -use simplicity::elements::secp256k1_zkp::PublicKey; - -pub fn handle( - index: u32, - price_at_current_block_height: u64, - settlement_height: u32, -) -> crate::error::Result<(PublicKey, Message, Signature)> { - let settings = Settings::load()?; - let keypair = derive_keypair_from_index(index, &settings.seed_hex); - let pubkey = keypair.public_key(); - let msg = secp256k1::Message::from_digest_slice(&oracle_msg(settlement_height, price_at_current_block_height))?; - let sig = secp256k1::SECP256K1.sign_schnorr(&msg, &keypair); - Ok((pubkey, msg, sig)) -} diff --git a/crates/cli-client/src/contract_handlers/split_utxo.rs b/crates/cli-client/src/contract_handlers/split_utxo.rs deleted file mode 100644 index 240accb..0000000 --- a/crates/cli-client/src/contract_handlers/split_utxo.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::contract_handlers::common::broadcast_or_get_raw_tx; -use simplicityhl::elements::{AddressParams, OutPoint, Txid}; -use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, get_p2pk_address}; -use tokio::task; - -pub async fn handle( - account_index: u32, - split_amount: u64, - fee_utxo: OutPoint, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result { - task::spawn_blocking(move || handle_sync(account_index, split_amount, fee_utxo, fee_amount, is_offline)).await? -} - -fn handle_sync( - account_index: u32, - split_amount: u64, - fee_utxo: OutPoint, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result { - let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); - - let recipient_addr = get_p2pk_address(&keypair.x_only_public_key().0, &AddressParams::LIQUID_TESTNET).unwrap(); - let transaction = contracts_adapter::basic::split_native_three( - &keypair, - fee_utxo, - &recipient_addr, - split_amount, - fee_amount, - &AddressParams::LIQUID_TESTNET, - LIQUID_TESTNET_BITCOIN_ASSET, - *LIQUID_TESTNET_GENESIS, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - broadcast_or_get_raw_tx(is_offline, &transaction)?; - - Ok(transaction.txid()) -} diff --git a/crates/cli-client/src/contract_handlers/taker_early_termination.rs b/crates/cli-client/src/contract_handlers/taker_early_termination.rs deleted file mode 100644 index 9009a67..0000000 --- a/crates/cli-client/src/contract_handlers/taker_early_termination.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::common::store::SledError; -use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; -use contracts::DCDArguments; -use contracts_adapter::dcd::{ - BaseContractContext, CommonContext, DcdContractContext, DcdManager, TakerTerminationEarlyContext, -}; -use dex_nostr_relay::relay_processor::RelayProcessor; -use elements::bitcoin::secp256k1; -use nostr::EventId; -use simplicity::elements::OutPoint; -use simplicityhl::elements::{AddressParams, Txid}; -use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, TaprootPubkeyGen}; -use tokio::task; -use tracing::instrument; - -#[derive(Debug)] -pub struct ProcessedArgs { - keypair: secp256k1::Keypair, - dcd_arguments: DCDArguments, - dcd_taproot_pubkey_gen: String, - filler_token_amount_to_return: u64, -} - -#[derive(Debug)] -pub struct ArgsToSave { - taproot_pubkey_gen: TaprootPubkeyGen, - dcd_arguments: DCDArguments, -} - -#[derive(Debug)] -pub struct Utxos { - pub filler_token: OutPoint, - pub collateral_token: OutPoint, - pub fee: OutPoint, -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn process_args( - account_index: u32, - filler_token_amount_to_return: u64, - maker_order_event_id: EventId, - relay_processor: &RelayProcessor, -) -> crate::error::Result { - let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); - - let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; - - Ok(ProcessedArgs { - keypair, - dcd_arguments: order_params.dcd_args, - dcd_taproot_pubkey_gen: order_params.taproot_pubkey_gen, - filler_token_amount_to_return, - }) -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn handle( - processed_args: ProcessedArgs, - utxos: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - task::spawn_blocking(move || handle_sync(processed_args, utxos, fee_amount, is_offline)).await? -} - -#[instrument(level = "debug", skip_all, err)] -fn handle_sync( - ProcessedArgs { - keypair, - dcd_arguments, - dcd_taproot_pubkey_gen, - filler_token_amount_to_return, - }: ProcessedArgs, - Utxos { - filler_token: filler_token_utxo, - collateral_token: collateral_token_utxo, - fee: fee_utxo, - }: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - tracing::debug!("=== dcd arguments: {:?}", dcd_arguments); - let base_contract_context = BaseContractContext { - address_params: &AddressParams::LIQUID_TESTNET, - lbtc_asset: LIQUID_TESTNET_BITCOIN_ASSET, - genesis_block_hash: *LIQUID_TESTNET_GENESIS, - }; - let dcd_taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - &dcd_taproot_pubkey_gen, - &dcd_arguments, - base_contract_context.address_params, - &contracts::get_dcd_address, - ) - .map_err(|e| SledError::TapRootGen(e.to_string()))?; - - let transaction = DcdManager::taker_early_termination( - &CommonContext { keypair }, - TakerTerminationEarlyContext { - filler_token_utxo, - collateral_token_utxo, - fee_utxo, - fee_amount, - filler_token_amount_to_return, - }, - &DcdContractContext { - dcd_taproot_pubkey_gen: dcd_taproot_pubkey_gen.clone(), - dcd_arguments: dcd_arguments.clone(), - base_contract_context, - }, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - broadcast_or_get_raw_tx(is_offline, &transaction)?; - - Ok(( - transaction.txid(), - ArgsToSave { - taproot_pubkey_gen: dcd_taproot_pubkey_gen, - dcd_arguments, - }, - )) -} - -pub fn save_args_to_cache( - ArgsToSave { - taproot_pubkey_gen, - dcd_arguments, - }: &ArgsToSave, -) -> crate::error::Result<()> { - crate::common::store::utils::save_dcd_args(taproot_pubkey_gen, dcd_arguments)?; - Ok(()) -} diff --git a/crates/cli-client/src/contract_handlers/taker_funding.rs b/crates/cli-client/src/contract_handlers/taker_funding.rs deleted file mode 100644 index 997328e..0000000 --- a/crates/cli-client/src/contract_handlers/taker_funding.rs +++ /dev/null @@ -1,131 +0,0 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::common::store::SledError; -use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; -use contracts::DCDArguments; -use contracts_adapter::dcd::{BaseContractContext, CommonContext, DcdContractContext, DcdManager, TakerFundingContext}; -use dex_nostr_relay::relay_processor::RelayProcessor; -use elements::bitcoin::secp256k1; -use nostr::EventId; -use simplicity::elements::OutPoint; -use simplicityhl::elements::{AddressParams, Txid}; -use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, TaprootPubkeyGen}; -use tokio::task; -use tracing::instrument; - -#[derive(Debug)] -pub struct ProcessedArgs { - keypair: secp256k1::Keypair, - dcd_arguments: DCDArguments, - dcd_taproot_pubkey_gen: String, - collateral_amount_to_deposit: u64, -} - -#[derive(Debug)] -pub struct ArgsToSave { - taproot_pubkey_gen: TaprootPubkeyGen, - dcd_arguments: DCDArguments, -} - -pub struct Utxos { - pub filler_token_utxo: OutPoint, - pub collateral_token_utxo: OutPoint, -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn process_args( - account_index: u32, - collateral_amount_to_deposit: u64, - maker_order_event_id: EventId, - relay_processor: &RelayProcessor, -) -> crate::error::Result { - let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); - - let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; - - Ok(ProcessedArgs { - keypair, - dcd_arguments: order_params.dcd_args, - dcd_taproot_pubkey_gen: order_params.taproot_pubkey_gen, - collateral_amount_to_deposit, - }) -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn handle( - processed_args: ProcessedArgs, - utxos: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - task::spawn_blocking(move || handle_sync(processed_args, utxos, fee_amount, is_offline)).await? -} - -#[instrument(level = "debug", skip_all, err)] -fn handle_sync( - ProcessedArgs { - keypair, - dcd_arguments, - dcd_taproot_pubkey_gen, - collateral_amount_to_deposit, - }: ProcessedArgs, - Utxos { - filler_token_utxo, - collateral_token_utxo, - }: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - tracing::debug!("=== dcd arguments: {:?}", dcd_arguments); - let base_contract_context = BaseContractContext { - address_params: &AddressParams::LIQUID_TESTNET, - lbtc_asset: LIQUID_TESTNET_BITCOIN_ASSET, - genesis_block_hash: *LIQUID_TESTNET_GENESIS, - }; - let dcd_taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - &dcd_taproot_pubkey_gen, - &dcd_arguments, - base_contract_context.address_params, - &contracts::get_dcd_address, - ) - .map_err(|e| SledError::TapRootGen(e.to_string()))?; - - let transaction = DcdManager::taker_funding( - &CommonContext { keypair }, - TakerFundingContext { - filler_token_utxo, - collateral_token_utxo, - fee_amount, - collateral_amount_to_deposit, - }, - &DcdContractContext { - dcd_taproot_pubkey_gen: dcd_taproot_pubkey_gen.clone(), - dcd_arguments: dcd_arguments.clone(), - base_contract_context, - }, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - broadcast_or_get_raw_tx(is_offline, &transaction)?; - - Ok(( - transaction.txid(), - ArgsToSave { - taproot_pubkey_gen: dcd_taproot_pubkey_gen, - dcd_arguments, - }, - )) -} - -pub fn save_args_to_cache( - ArgsToSave { - taproot_pubkey_gen, - dcd_arguments, - }: &ArgsToSave, -) -> crate::error::Result<()> { - crate::common::store::utils::save_dcd_args(taproot_pubkey_gen, dcd_arguments)?; - Ok(()) -} diff --git a/crates/cli-client/src/contract_handlers/taker_settlement.rs b/crates/cli-client/src/contract_handlers/taker_settlement.rs deleted file mode 100644 index da83dc6..0000000 --- a/crates/cli-client/src/contract_handlers/taker_settlement.rs +++ /dev/null @@ -1,147 +0,0 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::common::store::SledError; -use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; -use contracts::DCDArguments; -use contracts_adapter::dcd::{ - BaseContractContext, CommonContext, DcdContractContext, DcdManager, TakerSettlementContext, -}; -use dex_nostr_relay::relay_processor::RelayProcessor; -use elements::bitcoin::secp256k1; -use nostr::EventId; -use simplicity::elements::OutPoint; -use simplicityhl::elements::{AddressParams, Txid}; -use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, TaprootPubkeyGen}; -use tokio::task; -use tracing::instrument; - -#[derive(Debug)] -pub struct ProcessedArgs { - keypair: secp256k1::Keypair, - dcd_arguments: DCDArguments, - dcd_taproot_pubkey_gen: String, - price_at_current_block_height: u64, - filler_amount_to_burn: u64, - oracle_signature: String, -} - -#[derive(Debug)] -pub struct ArgsToSave { - taproot_pubkey_gen: TaprootPubkeyGen, - dcd_arguments: DCDArguments, -} - -#[derive(Debug)] -pub struct Utxos { - pub filler_token: OutPoint, - pub asset: OutPoint, - pub fee: OutPoint, -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn process_args( - account_index: u32, - price_at_current_block_height: u64, - filler_amount_to_burn: u64, - oracle_signature: String, - maker_order_event_id: EventId, - relay_processor: &RelayProcessor, -) -> crate::error::Result { - let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); - - let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; - - Ok(ProcessedArgs { - keypair, - dcd_arguments: order_params.dcd_args, - dcd_taproot_pubkey_gen: order_params.taproot_pubkey_gen, - price_at_current_block_height, - filler_amount_to_burn, - oracle_signature, - }) -} - -#[instrument(level = "debug", skip_all, err)] -pub async fn handle( - processed_args: ProcessedArgs, - utxos: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - task::spawn_blocking(move || handle_sync(processed_args, utxos, fee_amount, is_offline)).await? -} - -#[instrument(level = "debug", skip_all, err)] -fn handle_sync( - ProcessedArgs { - keypair, - dcd_arguments, - dcd_taproot_pubkey_gen, - price_at_current_block_height, - filler_amount_to_burn, - oracle_signature, - }: ProcessedArgs, - Utxos { - filler_token: filler_token_utxo, - asset: asset_utxo, - fee: fee_utxo, - }: Utxos, - fee_amount: u64, - is_offline: bool, -) -> crate::error::Result<(Txid, ArgsToSave)> { - tracing::debug!("=== dcd arguments: {:?}", dcd_arguments); - let base_contract_context = BaseContractContext { - address_params: &AddressParams::LIQUID_TESTNET, - lbtc_asset: LIQUID_TESTNET_BITCOIN_ASSET, - genesis_block_hash: *LIQUID_TESTNET_GENESIS, - }; - let dcd_taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( - &dcd_taproot_pubkey_gen, - &dcd_arguments, - base_contract_context.address_params, - &contracts::get_dcd_address, - ) - .map_err(|e| SledError::TapRootGen(e.to_string()))?; - - let transaction = DcdManager::taker_settlement( - &CommonContext { keypair }, - TakerSettlementContext { - asset_utxo, - filler_token_utxo, - fee_utxo, - fee_amount, - price_at_current_block_height, - filler_amount_to_burn, - oracle_signature, - }, - &DcdContractContext { - dcd_taproot_pubkey_gen: dcd_taproot_pubkey_gen.clone(), - dcd_arguments: dcd_arguments.clone(), - base_contract_context, - }, - ) - .map_err(|err| crate::error::CliError::DcdManager(err.to_string()))?; - - broadcast_or_get_raw_tx(is_offline, &transaction)?; - - Ok(( - transaction.txid(), - ArgsToSave { - taproot_pubkey_gen: dcd_taproot_pubkey_gen, - dcd_arguments, - }, - )) -} - -pub fn save_args_to_cache( - ArgsToSave { - taproot_pubkey_gen, - dcd_arguments, - }: &ArgsToSave, -) -> crate::error::Result<()> { - crate::common::store::utils::save_dcd_args(taproot_pubkey_gen, dcd_arguments)?; - Ok(()) -} diff --git a/crates/cli-client/src/error.rs b/crates/cli-client/src/error.rs index e5328eb..5f54a4e 100644 --- a/crates/cli-client/src/error.rs +++ b/crates/cli-client/src/error.rs @@ -1,53 +1,20 @@ -use crate::common::store::SledError; -use config::ConfigError; -use dex_nostr_relay::error::NostrRelayError; -use elements::bitcoin::secp256k1; -use tokio::task::JoinError; - -pub type Result = core::result::Result; - #[derive(thiserror::Error, Debug)] -pub enum CliError { - #[error("Occurred error with io, err: '{0}'")] +pub enum Error { + #[error("Configuration error: {0}")] + Config(String), + + #[error("IO error: {0}")] Io(#[from] std::io::Error), - #[error(transparent)] - NostrRelay(#[from] NostrRelayError), - #[error("Occurred error with usage of Dcd manager, err: '{0}'")] - DcdManager(String), - #[error("Configuration error, err: '{0}'")] - Config(#[from] ConfigError), - #[error("Configuration error, err: '{0}'")] - ConfigExtended(String), - #[error("Failed to obtain utxo, '{0}'")] - Utxo(String), - #[error("'{0}', not set in environment or .env")] - EnvNotSet(String), - #[error("Failed to broadcast transaction, err: '{0}'")] - Broadcast(String), - #[error("Failed to obtain P2PK address, err: '{0}'")] - P2pkAddress(String), - #[error(transparent)] - SledError(#[from] SledError), - #[error("Asset name already exists, name: '{name}'")] - AssetNameExists { name: String }, - #[error("Asset name is absent, name: '{name}'")] - AssetNameAbsent { name: String }, - #[error("Failed to covert value from hex, err: '{0}', value: '{1}'")] - FromHex(hex::FromHexError, String), - #[error("Failed to convert dcd inner params into dcd params, err msg: '{0}'")] - InnerDcdConversion(String), - #[error("Expected at least {expected} elements, got {got}")] - InvalidElementsSize { got: usize, expected: usize }, - #[error("Secp256k1 error: '{0}'")] - EcCurve(#[from] secp256k1::Error), - #[error("Failed to create DcdRatioArgs, msg: '{0}'")] - DcdRatioArgs(String), - #[error("Failed to obtain/save value from cache, msg: '{0}'")] - Cache(String), - #[error("Nostr keypair is required for the action, but it's absent")] - NoNostrKeypairListed, - #[error("Failed to join task, err: '{0}'")] - TokioJoinError(#[from] JoinError), - #[error("Occurred error with msg: '{0}'")] - Custom(String), + + #[error("TOML parse error: {0}")] + TomlParse(#[from] toml::de::Error), + + #[error("Signer error: {0}")] + Signer(#[from] signer::SignerError), + + #[error("Store error: {0}")] + Store(#[from] coin_store::StoreError), + + #[error("Explorer error: {0}")] + Explorer(#[from] cli_helper::explorer::ExplorerError), } diff --git a/crates/cli-client/src/lib.rs b/crates/cli-client/src/lib.rs index d4030bb..4014f60 100644 --- a/crates/cli-client/src/lib.rs +++ b/crates/cli-client/src/lib.rs @@ -1,7 +1,7 @@ #![warn(clippy::all, clippy::pedantic)] +#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] pub mod cli; -pub mod common; -mod contract_handlers; +pub mod config; pub mod error; -pub mod logger; +pub mod wallet; diff --git a/crates/cli-client/src/logger.rs b/crates/cli-client/src/logger.rs deleted file mode 100644 index 6f3996e..0000000 --- a/crates/cli-client/src/logger.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::io; - -use tracing::{level_filters::LevelFilter, trace}; -use tracing_appender::non_blocking::WorkerGuard; -use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt}; - -#[derive(Debug)] -pub struct LoggerGuard { - _std_out_guard: WorkerGuard, - _std_err_guard: WorkerGuard, -} - -pub fn init_logger() -> LoggerGuard { - let (std_out_writer, std_out_guard) = tracing_appender::non_blocking(io::stdout()); - let (std_err_writer, std_err_guard) = tracing_appender::non_blocking(io::stderr()); - let std_out_layer = fmt::layer() - .with_writer(std_out_writer) - .with_target(false) - .with_level(true) - .with_filter( - EnvFilter::builder() - .with_default_directive(LevelFilter::ERROR.into()) - .from_env_lossy(), - ); - - let std_err_layer = fmt::layer() - .with_writer(std_err_writer) - .with_target(false) - .with_level(true) - .with_filter(LevelFilter::WARN); - - tracing_subscriber::registry() - .with(std_out_layer) - .with(std_err_layer) - .init(); - - trace!("Logger successfully initialized!"); - - LoggerGuard { - _std_out_guard: std_out_guard, - _std_err_guard: std_err_guard, - } -} diff --git a/crates/cli-client/src/wallet.rs b/crates/cli-client/src/wallet.rs new file mode 100644 index 0000000..c32802e --- /dev/null +++ b/crates/cli-client/src/wallet.rs @@ -0,0 +1,52 @@ +use std::path::Path; + +use coin_store::Store; +use signer::Signer; +use simplicityhl::elements::AddressParams; + +use crate::error::Error; + +pub struct Wallet { + signer: Signer, + store: Store, + params: &'static AddressParams, +} + +impl Wallet { + pub async fn create( + seed: &[u8; 32], + db_path: impl AsRef, + params: &'static AddressParams, + ) -> Result { + let signer = Signer::from_seed(seed)?; + let store = Store::create(db_path).await?; + + Ok(Self { signer, store, params }) + } + + pub async fn open( + seed: &[u8; 32], + db_path: impl AsRef, + params: &'static AddressParams, + ) -> Result { + let signer = Signer::from_seed(seed)?; + let store = Store::connect(db_path).await?; + + Ok(Self { signer, store, params }) + } + + #[must_use] + pub const fn signer(&self) -> &Signer { + &self.signer + } + + #[must_use] + pub const fn store(&self) -> &Store { + &self.store + } + + #[must_use] + pub const fn params(&self) -> &'static AddressParams { + self.params + } +} diff --git a/crates/coin-store/Cargo.toml b/crates/coin-store/Cargo.toml new file mode 100644 index 0000000..bd35eff --- /dev/null +++ b/crates/coin-store/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "coin-store" +description = "SQLite-based UTXO and blinding key storage" +license = "MIT OR Apache-2.0" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true + +[dependencies] +simplicityhl = { workspace = true } + +futures = "0.3" + +thiserror = "2" + +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "migrate"] } diff --git a/crates/coin-store/migrations/001_initial.sql b/crates/coin-store/migrations/001_initial.sql new file mode 100644 index 0000000..2d8b7d2 --- /dev/null +++ b/crates/coin-store/migrations/001_initial.sql @@ -0,0 +1,24 @@ +CREATE TABLE utxos ( + txid BLOB NOT NULL, + vout INTEGER NOT NULL, + script_pubkey BLOB NOT NULL, + asset_id BLOB NOT NULL, + value INTEGER NOT NULL, + serialized BLOB NOT NULL, + is_confidential INTEGER NOT NULL, + is_spent INTEGER DEFAULT 0, + PRIMARY KEY (txid, vout) +); + +CREATE TABLE blinder_keys ( + txid BLOB NOT NULL, + vout INTEGER NOT NULL, + blinding_key BLOB NOT NULL, + PRIMARY KEY (txid, vout), + FOREIGN KEY (txid, vout) REFERENCES utxos(txid, vout) +); + +CREATE INDEX idx_utxos_asset_id ON utxos(asset_id); +CREATE INDEX idx_utxos_is_spent ON utxos(is_spent); +CREATE INDEX idx_utxos_script_pubkey ON utxos(script_pubkey); +CREATE INDEX idx_utxos_asset_spent_value ON utxos(asset_id, is_spent, value DESC); diff --git a/crates/coin-store/src/entry.rs b/crates/coin-store/src/entry.rs new file mode 100644 index 0000000..480129f --- /dev/null +++ b/crates/coin-store/src/entry.rs @@ -0,0 +1,43 @@ +use simplicityhl::elements::{OutPoint, TxOut, TxOutSecrets}; + +pub enum UtxoEntry { + Confidential { + outpoint: OutPoint, + txout: TxOut, + secrets: TxOutSecrets, + }, + Explicit { + outpoint: OutPoint, + txout: TxOut, + }, +} + +impl UtxoEntry { + #[must_use] + pub const fn outpoint(&self) -> &OutPoint { + match self { + Self::Confidential { outpoint, .. } | Self::Explicit { outpoint, .. } => outpoint, + } + } + + #[must_use] + pub const fn txout(&self) -> &TxOut { + match self { + Self::Confidential { txout, .. } | Self::Explicit { txout, .. } => txout, + } + } + + #[must_use] + pub const fn secrets(&self) -> Option<&TxOutSecrets> { + match self { + Self::Confidential { secrets, .. } => Some(secrets), + Self::Explicit { .. } => None, + } + } +} + +pub enum QueryResult { + Found(Vec), + InsufficientValue(Vec), + Empty, +} diff --git a/crates/coin-store/src/error.rs b/crates/coin-store/src/error.rs new file mode 100644 index 0000000..e826ec1 --- /dev/null +++ b/crates/coin-store/src/error.rs @@ -0,0 +1,40 @@ +use std::path::PathBuf; + +use simplicityhl::elements::secp256k1_zkp::UpstreamError; +use simplicityhl::elements::{OutPoint, UnblindError}; + +#[derive(thiserror::Error, Debug)] +pub enum StoreError { + #[error("Database already exists: {0}")] + AlreadyExists(PathBuf), + + #[error("Database not found: {0}")] + NotFound(PathBuf), + + #[error("Database not initialized: {0}")] + NotInitialized(PathBuf), + + #[error("UTXO already exists: {0}")] + UtxoAlreadyExists(OutPoint), + + #[error("UTXO not found: {0}")] + UtxoNotFound(OutPoint), + + #[error("Missing blinder key for confidential output: {0}")] + MissingBlinderKey(OutPoint), + + #[error("Encoding error")] + Encoding(#[from] simplicityhl::elements::encode::Error), + + #[error("Invalid secret key")] + InvalidSecretKey(#[from] UpstreamError), + + #[error("Unblind error")] + Unblind(#[from] UnblindError), + + #[error("SQLx error")] + Sqlx(#[from] sqlx::Error), + + #[error("Migration error")] + Migration(#[from] sqlx::migrate::MigrateError), +} diff --git a/crates/coin-store/src/filter.rs b/crates/coin-store/src/filter.rs new file mode 100644 index 0000000..153d107 --- /dev/null +++ b/crates/coin-store/src/filter.rs @@ -0,0 +1,47 @@ +use simplicityhl::elements::{AssetId, Script}; + +#[derive(Clone, Default)] +pub struct Filter { + pub(crate) asset_id: Option, + pub(crate) script_pubkey: Option