From f07f5ff49d26e7c2d15dfbed368a7774469e2c87 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Mon, 3 Nov 2025 10:44:37 +0200 Subject: [PATCH 1/3] Initialized project --- .gitignore | 41 +++++++++++++++++++++++++++++++++++++++++ Readme.md | 6 ++++++ 2 files changed, 47 insertions(+) create mode 100644 .gitignore create mode 100644 Readme.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..001b8ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Ignoring .js files, as typescript firstly compiles into it +/tests/*.js + +# Disable logs that have obtained during run +/logs +*.log +.env + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ +.vscode/ + +# frontend code +node_modules/ +dist/ + +# Mac OSX temporary files +.DS_Store +**/.DS_Store + +# Logs +logs/* \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..ead6363 --- /dev/null +++ b/Readme.md @@ -0,0 +1,6 @@ +## Liquid Nostr Options custom DEX + +Decentralized Exchange, which can help traders to trade asset pairs in Liquid network. (LBTC ↔ MEX). Tokens [listed][1]. Liquid options [docs][2]. + +[1]: https://liquid.network/assets/featured +[2]: https://blockstream.com/assets/downloads/pdf/options-whitepaper.pdf \ No newline at end of file From ef1b0af072ebc84d23901e8131468f87a5cc16e6 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Mon, 17 Nov 2025 16:16:54 +0200 Subject: [PATCH 2/3] Project restructure. Dex Nostr Relayer PoC. --- .github/ISSUE_TEMPLATE/bug-report.yml | 28 +++ .github/ISSUE_TEMPLATE/feature-request.yml | 11 ++ .github/ISSUE_TEMPLATE/other-issue.md | 4 + .github/workflows/ci.yml | 48 +++++ .simplicity-dex.example/keypair.txt | 1 + .simplicity-dex.example/relays.txt | 1 + Cargo.toml | 31 ++++ Makefile | 27 +++ Readme.md | 104 ++++++++++- crates/dex-cli/Cargo.toml | 18 ++ crates/dex-cli/src/cli.rs | 167 ++++++++++++++++++ crates/dex-cli/src/error.rs | 15 ++ crates/dex-cli/src/lib.rs | 3 + crates/dex-cli/src/main.rs | 15 ++ crates/dex-cli/src/utils.rs | 80 +++++++++ crates/dex-nostr-relay/Cargo.toml | 16 ++ crates/dex-nostr-relay/src/error.rs | 18 ++ .../src/handlers/get_events.rs | 24 +++ .../src/handlers/list_orders.rs | 38 ++++ crates/dex-nostr-relay/src/handlers/mod.rs | 5 + .../src/handlers/order_replies.rs | 23 +++ .../src/handlers/place_order.rs | 35 ++++ .../src/handlers/reply_order.rs | 35 ++++ crates/dex-nostr-relay/src/lib.rs | 5 + crates/dex-nostr-relay/src/relay_client.rs | 123 +++++++++++++ crates/dex-nostr-relay/src/relay_processor.rs | 63 +++++++ crates/dex-nostr-relay/src/types.rs | 30 ++++ .../tests/test_order_placing.rs | 92 ++++++++++ crates/dex-nostr-relay/tests/utils.rs | 8 + crates/global-utils/Cargo.toml | 13 ++ crates/global-utils/src/lib.rs | 1 + crates/global-utils/src/logger.rs | 47 +++++ docs/maker_taker_flow.puml | 21 +++ rustfmt.toml | 27 +++ 34 files changed, 1173 insertions(+), 4 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 .github/ISSUE_TEMPLATE/other-issue.md create mode 100644 .github/workflows/ci.yml create mode 100644 .simplicity-dex.example/keypair.txt create mode 100644 .simplicity-dex.example/relays.txt create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 crates/dex-cli/Cargo.toml create mode 100644 crates/dex-cli/src/cli.rs create mode 100644 crates/dex-cli/src/error.rs create mode 100644 crates/dex-cli/src/lib.rs create mode 100644 crates/dex-cli/src/main.rs create mode 100644 crates/dex-cli/src/utils.rs create mode 100644 crates/dex-nostr-relay/Cargo.toml create mode 100644 crates/dex-nostr-relay/src/error.rs create mode 100644 crates/dex-nostr-relay/src/handlers/get_events.rs create mode 100644 crates/dex-nostr-relay/src/handlers/list_orders.rs create mode 100644 crates/dex-nostr-relay/src/handlers/mod.rs create mode 100644 crates/dex-nostr-relay/src/handlers/order_replies.rs create mode 100644 crates/dex-nostr-relay/src/handlers/place_order.rs create mode 100644 crates/dex-nostr-relay/src/handlers/reply_order.rs create mode 100644 crates/dex-nostr-relay/src/lib.rs create mode 100644 crates/dex-nostr-relay/src/relay_client.rs create mode 100644 crates/dex-nostr-relay/src/relay_processor.rs create mode 100644 crates/dex-nostr-relay/src/types.rs create mode 100644 crates/dex-nostr-relay/tests/test_order_placing.rs create mode 100644 crates/dex-nostr-relay/tests/utils.rs create mode 100644 crates/global-utils/Cargo.toml create mode 100644 crates/global-utils/src/lib.rs create mode 100644 crates/global-utils/src/logger.rs create mode 100644 docs/maker_taker_flow.puml create mode 100644 rustfmt.toml diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..3d78020 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,28 @@ +name: Bug Report +description: File a bug report +labels: ['bug'] +body: + - type: markdown + attributes: + value: Thanks for taking the time to fill out this bug report! + - type: input + id: version + attributes: + label: "Project version" + placeholder: "1.2.3" + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A brief description of what happened and what you expected to happen + validations: + required: true + - type: textarea + id: reproduction-steps + attributes: + label: "Minimal reproduction steps" + description: "The minimal steps needed to reproduce the bug" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..a7778e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,11 @@ +name: Feature request +description: Suggest a new feature +labels: ['feature'] +body: + - type: textarea + id: feature-description + attributes: + label: "Describe the feature" + description: "A description of what you would like to see in the project" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/other-issue.md b/.github/ISSUE_TEMPLATE/other-issue.md new file mode 100644 index 0000000..7115534 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other-issue.md @@ -0,0 +1,4 @@ +--- +name: Other issue +about: Other kind of issue +--- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d34ade0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: Linting Checks and Tests + +on: + push: + branches: + - main + - dev + pull_request: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-and-test: + name: Lint and Test (Ubuntu) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: clippy, rustfmt + + - name: Cache cargo registry, git and target + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Run Rust fmt + run: cargo fmt --all --check + + - name: Run clippy + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + + - name: Run tests + run: cargo test --workspace --all-features --locked --no-fail-fast diff --git a/.simplicity-dex.example/keypair.txt b/.simplicity-dex.example/keypair.txt new file mode 100644 index 0000000..d130f25 --- /dev/null +++ b/.simplicity-dex.example/keypair.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.simplicity-dex.example/relays.txt b/.simplicity-dex.example/relays.txt new file mode 100644 index 0000000..6a6b8a4 --- /dev/null +++ b/.simplicity-dex.example/relays.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..10e1ee0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[workspace] +resolver = "3" +members = [ + "crates/*" +] + +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.91.0" +authors = ["Blockstream"] +readme = "README.md" + + +[workspace.dependencies] +anyhow = { version = "1.0.100" } +clap = { version = "4.5.49", features = ["derive"] } +dirs = {version = "6.0.0"} +futures-util = { version = "0.3.31" } +global-utils = { path = "crates/global-utils" } +nostr = { version = "0.43.1", features = ["std"] } +nostr-sdk = { version = "0.43.0" } +dex-nostr-relay = { path = "./crates/dex-nostr-relay"} +serde = { version = "1.0.228", features = ["derive"] } +serde_json = { version = "1.0.145" } +thiserror = { version = "2.0.17" } +tokio = { version = "1.48.0", features = ["macros", "test-util", "rt", "rt-multi-thread"] } +tracing = { version = "0.1.41" } +tracing-appender = { version = "0.2.3" } +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +url = { version = "2.5.7" } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..46ee2af --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +## This help screen +help: + @printf "Available targets:\n\n" + @awk '/^[a-zA-Z\-\_0-9%:\\]+/ { \ + helpMessage = match(lastLine, /^## (.*)/); \ + if (helpMessage) { \ + helpCommand = $$1; \ + helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ + gsub("\\\\", "", helpCommand); \ + gsub(":+$$", "", helpCommand); \ + printf " \x1b[32;01m%-35s\x1b[0m %s\n", helpCommand, helpMessage; \ + } \ + } \ + { lastLine = $$0 }' $(MAKEFILE_LIST) | sort -u + @printf "\n" + +## Format code +fmt: + cargo +nightly fmt --all + +## Show lints +clippy: + cargo clippy -- -Dclippy::pedantic + +## Show lints for all features +clippy_all_features: + cargo clippy --workspace --all-targets --all-features -- -D warnings \ No newline at end of file diff --git a/Readme.md b/Readme.md index ead6363..eb1ae4e 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,102 @@ -## Liquid Nostr Options custom DEX +# Simplicity DEX -Decentralized Exchange, which can help traders to trade asset pairs in Liquid network. (LBTC ↔ MEX). Tokens [listed][1]. Liquid options [docs][2]. +A distributed exchange built on the NOSTR protocol, leveraging Simplicity smart contracts and the PACT (PACT for Auditable Contract Transactions) messaging protocol. -[1]: https://liquid.network/assets/featured -[2]: https://blockstream.com/assets/downloads/pdf/options-whitepaper.pdf \ No newline at end of file +## Overview + +Simplicity DEX is a decentralized exchange that combines the power of Simplicity smart contracts with the distributed messaging capabilities of NOSTR. By utilizing the PACT protocol, we enable secure, auditable, and transparent trading of digital assets without relying on centralized intermediaries. + +## Key Features + +- **Decentralized Architecture**: Built on NOSTR for censorship-resistant, distributed messaging +- **Simplicity Smart Contracts**: Leveraging Bitcoin's Simplicity language for provably secure contract execution +- **PACT Protocol**: Standardized format for auditable contract transactions +- **Open Ecosystem**: Compatible with any NOSTR client for maximum interoperability +- **Maker Identity Registry**: On-chain reputation system for market makers + +## DEX Messaging Protocol + +The core of our DEX is the **PACT (PACT for Auditable Contract Transactions)** protocol, which defines the format of trading offers. This protocol is fully adapted to be compatible with the NOSTR event structure. + +### Offer Structure + +A PACT offer is implemented as a standard NOSTR event with kind `30078` (non-standard, ephemeral event kind for DEX offers). The event structure maps to PACT requirements as follows: + +| NOSTR Field | PACT Field | Data Type | Required | Description | +|---------------|---------------|-----------------------|----------|-------------------------------------------------------------------------------------------------------------------| +| `id` | Event ID | string (64-char hex) | Yes | SHA-256 hash of canonical serialized event data (excluding `sig`). Serves as unique, content-addressed identifier | +| `pubkey` | Maker Key | string (64-char hex) | Yes | 32-byte x-only Schnorr public key of market maker. Must be registered in on-chain Maker Identity Registry | +| `created_at` | Timestamp | integer | Yes | Unix timestamp (seconds) when offer was created | +| `description` | Description | string | No | Human-readable description of instrument and complex terms | +| `kind` | Event Type | integer | Yes | Event type identifier. Value `1` reserved for standard offers. Enables future protocol extensions | +| `tags` | Metadata | array of arrays | Yes | Structured machine-readable metadata for filtering and discovery | +| `content` | Contract Code | string | Yes | Stringified JSON containing full Simplicity contract code | +| `sig` | Signature | string (128-char hex) | Yes | 64-byte Schnorr signature proving authenticity and integrity | + +### Tag Examples + +The `tags` field contains structured metadata as key-value pairs: + +```json +[ + ["asset_to_sell", ""], + ["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 + +- [Simplicity Language](https://github.com/ElementsProject/simplicity) +- [NOSTR Protocol](https://github.com/nostr-protocol/nostr) +- [Liquid Network](https://liquid.net/) + +## Disclaimer + +This software is experimental and should be used with caution. Always verify contract code and understand the risks before trading. diff --git a/crates/dex-cli/Cargo.toml b/crates/dex-cli/Cargo.toml new file mode 100644 index 0000000..6cc0d2e --- /dev/null +++ b/crates/dex-cli/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "simplicity-dex" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = { workspace = true } +nostr = { workspace = true } +global-utils = { workspace = true } +futures-util = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +clap = { workspace = true } +dirs = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +dex-nostr-relay = { workspace = true } + diff --git a/crates/dex-cli/src/cli.rs b/crates/dex-cli/src/cli.rs new file mode 100644 index 0000000..78ac270 --- /dev/null +++ b/crates/dex-cli/src/cli.rs @@ -0,0 +1,167 @@ +use crate::utils::{ + DEFAULT_CLIENT_TIMEOUT_SECS, check_file_existence, default_key_path, default_relays_path, get_valid_key_from_file, + get_valid_urls_from_file, write_into_stdout, +}; +use clap::{Parser, Subcommand}; +use nostr::{EventId, PublicKey}; + +use dex_nostr_relay::relay_client::ClientConfig; +use dex_nostr_relay::relay_processor::{OrderPlaceEventTags, OrderReplyEventTags, RelayProcessor}; +use std::path::PathBuf; +use std::time::Duration; +use tracing::instrument; + +#[derive(Parser)] +pub struct Cli { + /// Specify private key for posting authorized events on Nostr Relay + #[arg( + short = 'k', + long, + value_parser = check_file_existence + )] + key_path: Option, + /// Specify file with list of relays to use + #[arg( + short = 'r', + long, + value_parser = check_file_existence + )] + relays_path: Option, + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + #[command(about = "Commands collection for the maker role")] + Maker { + #[command(subcommand)] + action: MakerCommands, + }, + #[command(about = "Commands collection for the taker role")] + Taker { + #[command(subcommand)] + action: TakerCommands, + }, + #[command(about = "Get replies for a specific order by its ID [no authentication required]")] + GetOrderReplies { + #[arg(short = 'i', long)] + event_id: EventId, + }, + #[command(about = "List available orders from relays [no authentication required]")] + ListOrders, + #[command(about = "Get events by its ID [no authentication required]")] + GetEventsById { + #[arg(short = 'i', long)] + event_id: EventId, + }, +} + +#[derive(Debug, Subcommand)] +enum MakerCommands { + #[command(about = "Create order as Maker on Relays specified [authentication required]")] + CreateOrder { + #[arg(short = 's', long, default_value = "")] + asset_to_sell: String, + #[arg(short = 'b', long, default_value = "")] + asset_to_buy: String, + #[arg(short = 'p', long, default_value = "0")] + price: u64, + #[arg(short = 'e', long, default_value = "0")] + expiry: u64, + #[arg(short = 'c', long, default_value = "")] + compiler_name: String, + #[arg(short = 's', long, default_value = "")] + compiler_build_hash: String, + }, +} + +#[derive(Debug, Subcommand)] +enum TakerCommands { + #[command(about = "Reply order as Taker on Relays specified [authentication required]")] + ReplyOrder { + #[arg(short = 'i', long)] + maker_event_id: EventId, + #[arg(short = 'p', long, help = " Pubkey in bech32 or hex format")] + maker_pubkey: PublicKey, + #[arg(short = 't', long, help = "Txid from funding transaction step", required = false)] + tx_id: String, + }, +} + +impl Cli { + #[instrument(skip(self))] + pub async fn process(self) -> crate::error::Result<()> { + let keys = { + match get_valid_key_from_file(&self.key_path.unwrap_or(default_key_path())) { + Ok(keys) => Some(keys), + Err(err) => { + tracing::warn!("Failed to parse key, {err}"); + None + } + } + }; + let relays_urls = get_valid_urls_from_file(&self.relays_path.unwrap_or(default_relays_path()))?; + let relay_processor = RelayProcessor::try_from_config( + relays_urls, + keys, + ClientConfig { + timeout: Duration::from_secs(DEFAULT_CLIENT_TIMEOUT_SECS), + }, + ) + .await?; + + let msg = { + match self.command { + Command::Maker { action } => match action { + MakerCommands::CreateOrder { + asset_to_sell, + asset_to_buy, + price, + expiry, + compiler_name, + compiler_build_hash, + } => { + let res = relay_processor + .place_order(OrderPlaceEventTags { + asset_to_sell, + asset_to_buy, + price, + expiry, + compiler_name, + compiler_build_hash, + }) + .await?; + format!("Creating order result: {res:#?}") + } + }, + Command::Taker { action } => match action { + TakerCommands::ReplyOrder { + maker_event_id, + maker_pubkey, + tx_id, + } => { + let res = relay_processor + .reply_order(maker_event_id, maker_pubkey, OrderReplyEventTags { tx_id }) + .await?; + format!("Replying order result: {res:#?}") + } + }, + Command::GetOrderReplies { event_id } => { + let res = relay_processor.get_order_replies(event_id).await?; + format!("Order '{event_id}' replies: {res:#?}") + } + Command::ListOrders => { + let res = relay_processor.list_orders().await?; + format!("List of available orders: {res:#?}") + } + Command::GetEventsById { event_id } => { + let res = relay_processor.get_events_by_id(event_id).await?; + format!("List of available events: {res:#?}") + } + } + }; + write_into_stdout(msg)?; + Ok(()) + } +} diff --git a/crates/dex-cli/src/error.rs b/crates/dex-cli/src/error.rs new file mode 100644 index 0000000..19d1340 --- /dev/null +++ b/crates/dex-cli/src/error.rs @@ -0,0 +1,15 @@ +use crate::utils::FileError; + +use dex_nostr_relay::error::NostrRelayError; + +pub type Result = core::result::Result; + +#[derive(thiserror::Error, Debug)] +pub enum CliError { + #[error("Occurred error with io, err: {0}")] + Io(#[from] std::io::Error), + #[error(transparent)] + File(#[from] FileError), + #[error(transparent)] + NostrRelay(#[from] NostrRelayError), +} diff --git a/crates/dex-cli/src/lib.rs b/crates/dex-cli/src/lib.rs new file mode 100644 index 0000000..fe25d5b --- /dev/null +++ b/crates/dex-cli/src/lib.rs @@ -0,0 +1,3 @@ +pub mod cli; +pub mod error; +mod utils; diff --git a/crates/dex-cli/src/main.rs b/crates/dex-cli/src/main.rs new file mode 100644 index 0000000..bdb09a3 --- /dev/null +++ b/crates/dex-cli/src/main.rs @@ -0,0 +1,15 @@ +use clap::Parser; + +use global_utils::logger::init_logger; + +use simplicity_dex::cli::Cli; + +#[tokio::main] +#[tracing::instrument] +async fn main() -> anyhow::Result<()> { + let _logger_guard = init_logger(); + + Cli::parse().process().await?; + + Ok(()) +} diff --git a/crates/dex-cli/src/utils.rs b/crates/dex-cli/src/utils.rs new file mode 100644 index 0000000..69f3599 --- /dev/null +++ b/crates/dex-cli/src/utils.rs @@ -0,0 +1,80 @@ +use nostr::{Keys, RelayUrl}; +use std::collections::HashSet; +use std::io::BufRead; +use std::str::FromStr; +use std::{io::Write, path::PathBuf}; + +const DEFAULT_RELAYS_FILEPATH: &str = ".simplicity-dex/relays.txt"; +const DEFAULT_KEY_PATH: &str = ".simplicity-dex/keypair.txt"; +pub const DEFAULT_CLIENT_TIMEOUT_SECS: u64 = 10; + +pub 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 fn default_key_path() -> PathBuf { + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from("../../..")) + .join(DEFAULT_KEY_PATH) +} + +pub fn default_relays_path() -> PathBuf { + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from("../../..")) + .join(DEFAULT_RELAYS_FILEPATH) +} +#[derive(Debug, thiserror::Error)] +pub enum FileError { + #[error("Unable to parse url: {1}, error: {0}")] + UrlParseError(nostr::types::url::Error, String), + #[error("Got error on reading/writing to file: {1}, error: {0}")] + ProblemWithFile(std::io::Error, PathBuf), + #[error("Incorrect path to the file, please check validity of the path (err: path is not a file), got path: {0}")] + IncorrectPathToFile(PathBuf), + #[error("File is empty, got path: {0}")] + EmptyFile(PathBuf), + #[error("File is empty, got path: {0}")] + KeyParseError(nostr::key::Error, String), +} + +pub fn check_file_existence(path: &str) -> Result { + let path = PathBuf::from(path); + + if path.is_file() { + Ok(path) + } else { + Err(FileError::IncorrectPathToFile(path.clone()).to_string()) + } +} + +pub fn get_valid_urls_from_file(filepath: &PathBuf) -> Result, FileError> { + let file = std::fs::File::open(filepath).map_err(|x| FileError::ProblemWithFile(x, filepath.clone()))?; + let reader = std::io::BufReader::new(file); + let mut set = HashSet::new(); + for x in reader.lines() { + let line = x.map_err(|x| FileError::ProblemWithFile(x, filepath.clone()))?; + match RelayUrl::parse(&line) { + Ok(url) => { + set.insert(url); + } + Err(e) => { + return Err(FileError::UrlParseError(e, line)); + } + } + } + Ok(set.into_iter().collect::>()) +} + +pub fn get_valid_key_from_file(filepath: &PathBuf) -> Result { + let file = std::fs::File::open(filepath).map_err(|x| FileError::ProblemWithFile(x, filepath.clone()))?; + let reader = std::io::BufReader::new(file); + let key = reader + .lines() + .next() + .ok_or_else(|| FileError::EmptyFile(filepath.clone()))? + .map_err(|x| FileError::ProblemWithFile(x, filepath.clone()))?; + let key = Keys::from_str(&key).map_err(|e| FileError::KeyParseError(e, key))?; + Ok(key) +} diff --git a/crates/dex-nostr-relay/Cargo.toml b/crates/dex-nostr-relay/Cargo.toml new file mode 100644 index 0000000..894e5fa --- /dev/null +++ b/crates/dex-nostr-relay/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "dex-nostr-relay" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +tokio = { workspace = true } +global-utils = { workspace = true } +nostr-sdk = { workspace = true } +nostr = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/dex-nostr-relay/src/error.rs b/crates/dex-nostr-relay/src/error.rs new file mode 100644 index 0000000..c28e6c2 --- /dev/null +++ b/crates/dex-nostr-relay/src/error.rs @@ -0,0 +1,18 @@ +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, +} + +pub type Result = std::result::Result; diff --git a/crates/dex-nostr-relay/src/handlers/get_events.rs b/crates/dex-nostr-relay/src/handlers/get_events.rs new file mode 100644 index 0000000..94c5324 --- /dev/null +++ b/crates/dex-nostr-relay/src/handlers/get_events.rs @@ -0,0 +1,24 @@ +pub mod ids { + use crate::relay_client::RelayClient; + + use std::collections::{BTreeMap, BTreeSet}; + + use nostr::{EventId, Filter}; + use nostr_sdk::prelude::Events; + + 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) + } +} diff --git a/crates/dex-nostr-relay/src/handlers/list_orders.rs b/crates/dex-nostr-relay/src/handlers/list_orders.rs new file mode 100644 index 0000000..ed1baa6 --- /dev/null +++ b/crates/dex-nostr-relay/src/handlers/list_orders.rs @@ -0,0 +1,38 @@ +use crate::types::{CustomKind, MakerOrderKind}; + +use crate::relay_client::RelayClient; + +use std::collections::{BTreeMap, BTreeSet}; + +use nostr::{Filter, Timestamp}; +use nostr_sdk::prelude::Events; + +pub async fn handle(client: &RelayClient) -> crate::error::Result { + let events = client + .req_and_wait(Filter { + ids: None, + authors: None, + kinds: Some(BTreeSet::from([MakerOrderKind::get_kind()])), + search: None, + since: None, + until: None, + limit: None, + generic_tags: BTreeMap::default(), + }) + .await?; + + let events = filter_expired_events(events); + 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 => false, + 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 new file mode 100644 index 0000000..62df426 --- /dev/null +++ b/crates/dex-nostr-relay/src/handlers/mod.rs @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..5de0059 --- /dev/null +++ b/crates/dex-nostr-relay/src/handlers/order_replies.rs @@ -0,0 +1,23 @@ +use crate::relay_client::RelayClient; +use crate::types::{CustomKind, TakerOrderKind}; + +use std::collections::{BTreeMap, BTreeSet}; + +use nostr::{EventId, Filter, SingleLetterTag}; +use nostr_sdk::prelude::Events; + +pub async fn handle(client: &RelayClient, event_id: EventId) -> crate::error::Result { + let events = client + .req_and_wait(Filter { + ids: None, + authors: None, + kinds: Some(BTreeSet::from([TakerOrderKind::get_kind()])), + search: None, + since: None, + until: None, + limit: None, + generic_tags: BTreeMap::from([(SingleLetterTag::from_char('e')?, BTreeSet::from([event_id.to_string()]))]), + }) + .await?; + Ok(events) +} diff --git a/crates/dex-nostr-relay/src/handlers/place_order.rs b/crates/dex-nostr-relay/src/handlers/place_order.rs new file mode 100644 index 0000000..68d50fb --- /dev/null +++ b/crates/dex-nostr-relay/src/handlers/place_order.rs @@ -0,0 +1,35 @@ +use crate::relay_client::RelayClient; +use crate::relay_processor::OrderPlaceEventTags; +use crate::types::{BLOCKSTREAM_MAKER_CONTENT, CustomKind, MAKER_EXPIRATION_TIME, MakerOrderKind}; + +use std::borrow::Cow; + +use nostr::{EventBuilder, EventId, Tag, TagKind, Timestamp}; + +pub async fn handle(client: &RelayClient, tags: OrderPlaceEventTags) -> 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 maker_order = EventBuilder::new(MakerOrderKind::get_kind(), BLOCKSTREAM_MAKER_CONTENT) + .tags([ + Tag::public_key(client_pubkey), + Tag::expiration(Timestamp::from(timestamp_now.as_u64() + MAKER_EXPIRATION_TIME)), + Tag::custom( + TagKind::Custom(Cow::from("compiler")), + [tags.compiler_name, tags.compiler_build_hash], + ), + Tag::custom(TagKind::Custom(Cow::from("asset_to_buy")), [tags.asset_to_buy]), + Tag::custom(TagKind::Custom(Cow::from("asset_to_sell")), [tags.asset_to_sell]), + Tag::custom(TagKind::Custom(Cow::from("price")), [tags.price.to_string()]), + ]) + .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 new file mode 100644 index 0000000..dc5a532 --- /dev/null +++ b/crates/dex-nostr-relay/src/handlers/reply_order.rs @@ -0,0 +1,35 @@ +use crate::relay_client::RelayClient; +use crate::relay_processor::OrderReplyEventTags; +use crate::types::{BLOCKSTREAM_TAKER_CONTENT, CustomKind, TakerOrderKind}; + +use std::borrow::Cow; + +use nostr::{EventBuilder, EventId, NostrSigner, PublicKey, Tag, TagKind, Timestamp}; + +pub async fn handle( + client: &RelayClient, + maker_event_id: EventId, + maker_pubkey: PublicKey, + tags: OrderReplyEventTags, +) -> 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 taker_response = EventBuilder::new(TakerOrderKind::get_kind(), BLOCKSTREAM_TAKER_CONTENT) + .tags([ + Tag::public_key(client_pubkey), + Tag::event(maker_event_id), + Tag::custom(TagKind::Custom(Cow::from("maker_pubkey")), [maker_pubkey]), + Tag::custom(TagKind::Custom(Cow::from("tx_id")), [tags.tx_id]), + ]) + .custom_created_at(timestamp_now); + + let reply_event = taker_response.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 new file mode 100644 index 0000000..cee9880 --- /dev/null +++ b/crates/dex-nostr-relay/src/lib.rs @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..db8bc52 --- /dev/null +++ b/crates/dex-nostr-relay/src/relay_client.rs @@ -0,0 +1,123 @@ +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)] +pub struct RelayClient { + client: Client, + timeout: Duration, +} + +#[derive(Debug)] +pub struct ClientConfig { + pub timeout: Duration, +} + +impl RelayClient { + #[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, + }) + } + + #[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?) + } + + #[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 + } + + #[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) + } + + #[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; + } + + #[instrument(skip_all, level = "debug", ret)] + pub async fn disconnect(&self) -> crate::error::Result<()> { + self.client.disconnect().await; + + Ok(()) + } + + /// 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 new file mode 100644 index 0000000..cf9f554 --- /dev/null +++ b/crates/dex-nostr-relay/src/relay_processor.rs @@ -0,0 +1,63 @@ +use crate::handlers; +use crate::relay_client::{ClientConfig, RelayClient}; + +use nostr::prelude::IntoNostrSigner; +use nostr::{EventId, PublicKey, TryIntoUrl}; + +use nostr_sdk::prelude::Events; + +pub struct RelayProcessor { + relay_client: RelayClient, +} + +#[derive(Debug, Default, Clone)] +pub struct OrderPlaceEventTags { + pub asset_to_sell: String, + pub asset_to_buy: String, + pub price: u64, + pub expiry: u64, + pub compiler_name: String, + pub compiler_build_hash: String, +} + +#[derive(Debug, Default, Clone)] +pub struct OrderReplyEventTags { + pub tx_id: String, +} + +impl RelayProcessor { + 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?, + }) + } + + pub async fn place_order(&self, tags: OrderPlaceEventTags) -> crate::error::Result { + handlers::place_order::handle(&self.relay_client, tags).await + } + + pub async fn list_orders(&self) -> crate::error::Result { + handlers::list_orders::handle(&self.relay_client).await + } + + pub async fn reply_order( + &self, + maker_event_id: EventId, + maker_pubkey: PublicKey, + tags: OrderReplyEventTags, + ) -> crate::error::Result { + handlers::reply_order::handle(&self.relay_client, maker_event_id, maker_pubkey, tags).await + } + + pub async fn get_order_replies(&self, event_id: EventId) -> crate::error::Result { + handlers::order_replies::handle(&self.relay_client, event_id).await + } + + pub async fn get_events_by_id(&self, event_id: EventId) -> crate::error::Result { + handlers::get_events::ids::handle(&self.relay_client, event_id).await + } +} diff --git a/crates/dex-nostr-relay/src/types.rs b/crates/dex-nostr-relay/src/types.rs new file mode 100644 index 0000000..bff1d57 --- /dev/null +++ b/crates/dex-nostr-relay/src/types.rs @@ -0,0 +1,30 @@ +use nostr::Kind; + +pub trait CustomKind { + const ORDER_KIND_NUMBER: u16; + + fn get_kind() -> Kind { + Kind::from(Self::ORDER_KIND_NUMBER) + } + + fn get_u16() -> u16 { + Self::ORDER_KIND_NUMBER + } +} + +pub const BLOCKSTREAM_MAKER_CONTENT: &str = "Liquid order [Maker]"; +pub const BLOCKSTREAM_TAKER_CONTENT: &str = "Liquid order [Taker]"; + +// TODO: move to the config +pub const MAKER_EXPIRATION_TIME: u64 = 60; + +pub struct MakerOrderKind; +pub struct TakerOrderKind; + +impl CustomKind for MakerOrderKind { + const ORDER_KIND_NUMBER: u16 = 9901; +} + +impl CustomKind for TakerOrderKind { + const ORDER_KIND_NUMBER: u16 = 9902; +} diff --git a/crates/dex-nostr-relay/tests/test_order_placing.rs b/crates/dex-nostr-relay/tests/test_order_placing.rs new file mode 100644 index 0000000..3cb0735 --- /dev/null +++ b/crates/dex-nostr-relay/tests/test_order_placing.rs @@ -0,0 +1,92 @@ +mod utils; + +mod tests { + use crate::utils::{DEFAULT_CLIENT_TIMEOUT, DEFAULT_RELAY_LIST, TEST_LOGGER}; + + use std::time::Duration; + + use nostr::{EventId, Keys, ToBech32}; + + use dex_nostr_relay::relay_client::ClientConfig; + use dex_nostr_relay::relay_processor::{OrderPlaceEventTags, OrderReplyEventTags, RelayProcessor}; + use dex_nostr_relay::types::{CustomKind, MakerOrderKind, TakerOrderKind}; + + use tracing::{info, instrument}; + + #[instrument] + #[tokio::test] + async fn test_wss_metadata() -> anyhow::Result<()> { + 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()) + .await?; + info!("=== placed order event id: {}", placed_order_event_id); + let order = relay_processor_maker.get_events_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()? + ); + let reply_event_id = relay_processor_taker + .reply_order( + placed_order_event_id, + key_maker.public_key, + OrderReplyEventTags::default(), + ) + .await?; + info!("=== order reply event id: {}", 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 + ); + assert_eq!(order_replies.len(), 1); + assert_eq!(order_replies.first().unwrap().kind, TakerOrderKind::get_kind()); + + let orders_listed = relay_processor_maker.list_orders().await?; + info!( + "=== orders listed, amount: {}, orders: {:#?}", + orders_listed.len(), + orders_listed + ); + assert!( + orders_listed + .iter() + .map(|x| x.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 new file mode 100644 index 0000000..5c7d511 --- /dev/null +++ b/crates/dex-nostr-relay/tests/utils.rs @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..187a262 --- /dev/null +++ b/crates/global-utils/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "global-utils" +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 new file mode 100644 index 0000000..d991728 --- /dev/null +++ b/crates/global-utils/src/lib.rs @@ -0,0 +1 @@ +pub mod logger; diff --git a/crates/global-utils/src/logger.rs b/crates/global-utils/src/logger.rs new file mode 100644 index 0000000..ecd0604 --- /dev/null +++ b/crates/global-utils/src/logger.rs @@ -0,0 +1,47 @@ +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}; + +const ENV_VAR_NAME: &str = "DEX_LOG"; +const DEFAULT_LOG_DIRECTIVE: LevelFilter = LevelFilter::ERROR; + +#[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(DEFAULT_LOG_DIRECTIVE.into()) + .with_env_var(ENV_VAR_NAME) + .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/docs/maker_taker_flow.puml b/docs/maker_taker_flow.puml new file mode 100644 index 0000000..2944eda --- /dev/null +++ b/docs/maker_taker_flow.puml @@ -0,0 +1,21 @@ +@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 diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..5e4210e --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,27 @@ +# WARNING: Formatting in this project is non-standard and unfortunetely `cargo fmt` does not support "out of the box" formatting. +# Here you can find the closest possible set of settings for `cargo fmt`, but it is not even close to desirable. +# use '+nightly' option for formatting (cargo +nightly fmt) + +edition = "2024" +style_edition = "2024" +max_width = 120 +tab_spaces = 4 +newline_style = "Unix" +fn_params_layout = "Tall" +match_arm_leading_pipes = "Preserve" +reorder_imports = true +reorder_modules = true +# unstable features below +# unstable_features = true +# format_code_in_doc_comments = true +# imports_granularity = "Crate" +# group_imports = "StdExternalCrate" + +#wrap_comments = true +#where_single_line = false +#blank_lines_upper_bound = 2 +#brace_style = "AlwaysNextLine" +#control_brace_style = "AlwaysNextLine" +#empty_item_single_line = true +#use_small_heuristics = "Off" + From 4e12acc003aadad2c3248af68a81860d4c261e3c Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Mon, 17 Nov 2025 16:19:46 +0200 Subject: [PATCH 3/3] Fixed typos in README.md --- Readme.md | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/Readme.md b/Readme.md index eb1ae4e..3381aa6 100644 --- a/Readme.md +++ b/Readme.md @@ -1,10 +1,13 @@ # Simplicity DEX -A distributed exchange built on the NOSTR protocol, leveraging Simplicity smart contracts and the PACT (PACT for Auditable Contract Transactions) messaging protocol. +A distributed exchange built on the NOSTR protocol, leveraging Simplicity smart contracts and the PACT (PACT for +Auditable Contract Transactions) messaging protocol. ## Overview -Simplicity DEX is a decentralized exchange that combines the power of Simplicity smart contracts with the distributed messaging capabilities of NOSTR. By utilizing the PACT protocol, we enable secure, auditable, and transparent trading of digital assets without relying on centralized intermediaries. +Simplicity DEX is a decentralized exchange that combines the power of Simplicity smart contracts with the distributed +messaging capabilities of NOSTR. By utilizing the PACT protocol, we enable secure, auditable, and transparent trading of +digital assets without relying on centralized intermediaries. ## Key Features @@ -16,11 +19,13 @@ Simplicity DEX is a decentralized exchange that combines the power of Simplicity ## DEX Messaging Protocol -The core of our DEX is the **PACT (PACT for Auditable Contract Transactions)** protocol, which defines the format of trading offers. This protocol is fully adapted to be compatible with the NOSTR event structure. +The core of our DEX is the **PACT (PACT for Auditable Contract Transactions)** protocol, which defines the format of +trading offers. This protocol is fully adapted to be compatible with the NOSTR event structure. ### Offer Structure -A PACT offer is implemented as a standard NOSTR event with kind `30078` (non-standard, ephemeral event kind for DEX offers). The event structure maps to PACT requirements as follows: +A PACT offer is implemented as a standard NOSTR event with kind `30078` (non-standard, ephemeral event kind for DEX +offers). The event structure maps to PACT requirements as follows: | NOSTR Field | PACT Field | Data Type | Required | Description | |---------------|---------------|-----------------------|----------|-------------------------------------------------------------------------------------------------------------------| @@ -39,11 +44,28 @@ The `tags` field contains structured metadata as key-value pairs: ```json [ - ["asset_to_sell", ""], - ["asset_to_buy", ""], - ["price", "1000000", "sats_per_contract"], - ["expiry", "1735689600"], - ["compiler", "simplicity-v1.2.3", "deterministic_build_hash"] + [ + "asset_to_sell", + "" + ], + [ + "asset_to_buy", + "" + ], + [ + "price", + "1000000", + "sats_per_contract" + ], + [ + "expiry", + "1735689600" + ], + [ + "compiler", + "simplicity-v1.2.3", + "deterministic_build_hash" + ] ] ``` @@ -99,4 +121,5 @@ This project is licensed under the MIT License - see the LICENSE file for detail ## Disclaimer -This software is experimental and should be used with caution. Always verify contract code and understand the risks before trading. +This software is experimental and should be used with caution. Always verify contract code and understand the risks +before trading.