From 7ce2b03953b2118d2af1def21a2bd026a141ed91 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Wed, 11 Feb 2026 15:53:09 +0100 Subject: [PATCH 1/9] feat: added a command to send dm with attachment to admin to help disputes solving - added some docs for AI generated code --- Cargo.lock | 3 + Cargo.toml | 3 + docs/architecture.md | 130 ++++++++++++++++ docs/commands.md | 212 ++++++++++++++++++++++++++ docs/database.md | 158 ++++++++++++++++++++ docs/overview.md | 40 +++++ src/cli.rs | 22 +++ src/cli/send_admin_dm_attach.rs | 257 ++++++++++++++++++++++++++++++++ 8 files changed, 825 insertions(+) create mode 100644 docs/architecture.md create mode 100644 docs/commands.md create mode 100644 docs/database.md create mode 100644 docs/overview.md create mode 100644 src/cli/send_admin_dm_attach.rs diff --git a/Cargo.lock b/Cargo.lock index d03a272..abf2116 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1662,6 +1662,8 @@ version = "0.14.5" dependencies = [ "anyhow", "bip39", + "bitcoin", + "chacha20poly1305", "chrono", "clap", "comfy-table", @@ -1673,6 +1675,7 @@ dependencies = [ "mostro-core", "nostr-sdk", "pretty_env_logger", + "rand_core 0.6.4", "reqwest", "rstest", "serde", diff --git a/Cargo.toml b/Cargo.toml index 2a0be9e..f57517c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,9 @@ pretty_env_logger = "0.5.0" sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio-rustls"] } bip39 = { version = "2.2.0", features = ["rand"] } dirs = "6.0.0" +chacha20poly1305 = "0.10.1" +rand_core = "0.6.4" +bitcoin = "0.32.2" [package.metadata.release] # (Default: true) Set to false to prevent automatically running `cargo publish`. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..1a55cac --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,130 @@ +## Architecture & Code Structure + +This document describes the internal structure of `mostro-cli`, how major modules interact, and the main data/control flows. It is intentionally high-level to stay stable across refactors. + +### Entry point and CLI wiring + +- **`src/main.rs`** + - Initializes the async runtime and delegates to `cli::run()`. + - Very thin; most logic is in `src/cli.rs`. + +- **`src/cli.rs`** + - Declares submodules for each logical command group: `add_invoice`, `adm_send_dm`, `conversation_key`, `dm_to_user`, `get_dm`, `get_dm_user`, `last_trade_index`, `list_disputes`, `list_orders`, `new_order`, `orders_info`, `rate_user`, `restore`, `send_dm`, `send_admin_dm_attach`, `send_msg`, `take_dispute`, `take_order`. + - Defines: + - `Context`: runtime dependencies required by commands (Nostr client, keys, trade index, DB pool, optional admin context keys, and Mostro pubkey). + - `Cli`: top-level arguments parsed by `clap` (subcommand, verbosity, Mostro pubkey override, relay list, PoW, secret mode). + - `Commands`: enum containing all subcommands and their structured arguments. + - `run()`: + - Parses CLI args via `Cli::parse()`. + - Constructs a `Context` using `init_context(&cli)`. + - Dispatches: `if let Some(cmd) = &cli.command { cmd.run(&ctx).await?; }`. + - `init_context()`: + - Sets environment variables from CLI flags (e.g. `MOSTRO_PUBKEY`, `RELAYS`, `POW`, `SECRET`). + - Connects to SQLite via `db::connect()`. + - Loads identity and per-trade keys from the local `users` table. + - Optionally loads `ADMIN_NSEC` into `context_keys` for admin commands. + - Resolves `MOSTRO_PUBKEY` via CLI flag or environment. + - Connects to Nostr relays via `util::connect_nostr()`. + +### Utilities and shared infrastructure + +- **`src/util/mod.rs`** + - Organizes utility modules: + - `events`: event filtering and retrieval from Nostr. + - `messaging`: higher-level DM helpers (gift-wrapped messages, admin keys, etc.). + - `misc`: small helpers such as `get_mcli_path` and string utilities. + - `net`: Nostr network connection setup. + - `storage`: thin storage helpers for orders and DMs. + - `types`: small shared enums/wrappers. + - Re-exports commonly used symbols (`create_filter`, `send_dm`, `connect_nostr`, `save_order`, etc.) so other modules can import from `crate::util` directly. + +- **`src/util/storage.rs`** + - `save_order(order, trade_keys, request_id, trade_index, pool)`: + - Wraps `Order::new` to insert/update an order row. + - Logs created order IDs. + - Updates the `User`'s `last_trade_index` in the `users` table. + - `run_simple_order_msg(command, order_id, ctx)`: + - Convenience wrapper that forwards to `cli::send_msg::execute_send_msg(...)` for simple order messages (e.g. `FiatSent`, `Release`, `Cancel`, `Dispute`). + - `admin_send_dm(ctx, msg)`: + - Uses `util::messaging::get_admin_keys` and `util::send_dm` to send an admin DM via Nostr. + +- **`src/util/types.rs`** + - `Event` enum: + - Wraps `SmallOrder`, `Dispute`, and a `Message` tuple `(Message, u64, PublicKey)` for use in parsers and event handling. + - `ListKind` enum: + - Identifies what is being listed: `Orders`, `Disputes`, `DirectMessagesUser`, `DirectMessagesAdmin`, `PrivateDirectMessagesUser`. + - `MessageType` (internal to `util`) distinguishes DM/gift-wrap styles. + +### Database layer + +- **`src/db.rs`** + - Connection: + - `connect()` creates or opens `mcli.db` in the CLI data directory from `get_mcli_path()`. + - On first run, it creates the `orders` and `users` tables via raw SQL. + - On subsequent runs, it applies a small migration to drop legacy `buyer_token`/`seller_token` columns if present. + - Models: + - `User`: + - Represents the local identity (mnemonic, root pubkey, last trade index). + - Handles creation (`User::new`), loading (`User::get`), updating (`save`), and key derivation helpers (identity keys and per-trade keys using `nip06`). + - `Order`: + - Represents cached orders with fields mapped to the `orders` table. + - Provides `new`, `insert_db`, `update_db`, fluent setters, `save`, `save_new_id`, `get_by_id`, `get_all_trade_keys`, and `delete_by_id`. + - See `database.md` for schema details. + +### Parsers and protocol types + +- **`src/parser/*`** + - Interpret raw Nostr events into higher-level `Event` variants based on `mostro_core` types. + - Responsibility split: + - `orders.rs`: parsing order-related events. + - `disputes.rs`: parsing dispute events. + - `dms.rs`: parsing direct messages. + - `common.rs`: shared parsing helpers. + - `mod.rs`: module glue. + +### Lightning integration + +- **`src/lightning/mod.rs`** + - Houses Lightning Network–specific helpers used by order flows and invoice handling (exact functions depend on the current version of the file). + - Typically used by `add_invoice`, `new_order`, and `take_order` modules. + +### Command modules + +Each file in `src/cli/` encapsulates the logic of a specific feature or a group of related commands: + +- Order-related: `add_invoice.rs`, `list_orders.rs`, `new_order.rs`, `take_order.rs`, `orders_info.rs`, `rate_user.rs`, `restore.rs`, `last_trade_index.rs`. +- Disputes and admin: `list_disputes.rs`, `take_dispute.rs`, `adm_send_dm.rs`. +- Messaging: `send_dm.rs`, `send_msg.rs`, `dm_to_user.rs`, `get_dm.rs`, `get_dm_user.rs`, `send_admin_dm_attach.rs`, `conversation_key.rs`. + +Each module exports an `execute_*` function that `Commands::run` calls. This keeps `src/cli.rs` as a central router while pushing feature logic into focused files. + +### Typical flow: creating a new order + +1. User runs `mostro-cli neworder ...`. +2. `clap` parses CLI arguments into `Cli` and `Commands::NewOrder { ... }`. +3. `cli::run()` calls `init_context()` to build `Context` (DB, keys, Nostr client, Mostro pubkey). +4. `Commands::run` matches `Commands::NewOrder` and calls `execute_new_order(...)`. +5. The handler: + - Uses `mostro_core` types to construct an order message. + - Sends it to the Mostro backend over Nostr via `util::connect_nostr`/messaging helpers. + - Persists or updates the local representation via `util::save_order` and `db::Order`. + +### Extension guidelines + +When adding new features: + +- **New command**: + - Add a variant to `Commands` in `src/cli.rs`. + - Add the corresponding `execute_*` function in a `src/cli/*.rs` module. + - Extend the `Commands::run` match arm. + - Update `docs/commands.md` to keep documentation in sync. + +- **New database fields / tables**: + - Update `connect()` schema creation and migrations in `src/db.rs`. + - Extend the relevant model structs and methods. + - Update `docs/database.md`. + +- **New protocol/event type**: + - Extend `util::types::Event` and the relevant parser module in `src/parser/*`. + - Adjust listing or DM flows as needed. + diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 0000000..d811f5b --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,212 @@ +## CLI Commands Reference + +This document lists all `mostro-cli` subcommands defined in `src/cli.rs`, their main arguments, and the Rust handler that implements each command. Use this when wiring new commands or changing existing flows. + +All commands are part of the `Commands` enum and are dispatched via `Commands::run(&self, ctx: &Context)`. + +### Orders + +- **`listorders`** + - **Description**: Requests open orders from the configured Mostro pubkey. + - **Args**: + - `--status `: Optional order status filter. + - `--currency `: Optional fiat currency code. + - `--kind `: Optional order kind (buy/sell). + - **Handler**: `execute_list_orders(kind, currency, status, ctx)` in `src/cli/list_orders.rs`. + +- **`neworder`** + - **Description**: Create a new buy/sell order on Mostro. + - **Args**: + - `--kind `: Order kind (e.g. `buy` or `sell`). + - `--amount `: Sats amount; `0` means market price. + - `-c, --fiat-code `: Fiat currency code. + - `--fiat-amount `: Fiat amount or range, validated by `check_fiat_range`. + - `-m, --payment-method `: Payment method identifier. + - `--premium `: Premium on the price (can be negative). + - `--invoice `: Optional Lightning invoice. + - `--expiration-days `: Expiration time in days for pending orders. + - **Handler**: `execute_new_order(...)` in `src/cli/new_order.rs`. + +- **`takesell`** + - **Description**: Take a sell order from a Mostro pubkey. + - **Args**: + - `--order-id `: Order identifier. + - `--invoice `: Optional Lightning invoice. + - `--amount `: Fiat amount to buy. + - **Handler**: `execute_take_order(order_id, Action::TakeSell, invoice, amount, ctx)` in `src/cli/take_order.rs`. + +- **`takebuy`** + - **Description**: Take a buy order from a Mostro pubkey. + - **Args**: + - `--order-id `: Order identifier. + - `--amount `: Fiat amount to sell. + - **Handler**: `execute_take_order(order_id, Action::TakeBuy, &None, amount, ctx)` in `src/cli/take_order.rs`. + +- **`addinvoice`** + - **Description**: Buyer adds a new invoice to receive the payment. + - **Args**: + - `--order-id `: Order identifier. + - `--invoice `: Lightning invoice. + - **Handler**: `execute_add_invoice(order_id, invoice, ctx)` in `src/cli/add_invoice.rs`. + +- **`fiatsent`** + - **Description**: Send a "fiat sent" message to confirm payment to the counterparty. + - **Args**: + - `--order-id `: Order identifier. + - **Handler**: `util::run_simple_order_msg(Commands::FiatSent { .. }, Some(order_id), ctx)`. + +- **`release`** + - **Description**: Settle the hold invoice and pay to the buyer. + - **Args**: + - `--order-id `: Order identifier. + - **Handler**: `util::run_simple_order_msg(Commands::Release { .. }, Some(order_id), ctx)`. + +- **`cancel`** + - **Description**: Cancel a pending order. + - **Args**: + - `--order-id `: Order identifier. + - **Handler**: `util::run_simple_order_msg(Commands::Cancel { .. }, Some(order_id), ctx)`. + +- **`ordersinfo`** + - **Description**: Request detailed information for specific orders. + - **Args**: + - `--order-ids ...`: One or more order IDs. + - **Handler**: `execute_orders_info(order_ids, ctx)` in `src/cli/orders_info.rs`. + +### Disputes + +- **`dispute`** + - **Description**: Start a dispute for an order. + - **Args**: + - `--order-id `: Order identifier. + - **Handler**: `util::run_simple_order_msg(Commands::Dispute { .. }, Some(order_id), ctx)`. + +- **`listdisputes`** + - **Description**: Request open disputes from the Mostro pubkey. + - **Args**: None. + - **Handler**: `execute_list_disputes(ctx)` in `src/cli/list_disputes.rs`. + +### Direct messages (user) + +- **`getdm`** + - **Description**: Get the latest direct messages. + - **Args**: + - `--since `: Minutes back from now to query (default: 30). + - `--from-user`: If true, get messages from the counterparty instead of Mostro. + - **Handler**: `execute_get_dm(since, false, from_user, ctx)` in `src/cli/get_dm.rs`. + +- **`getdmuser`** + - **Description**: Get direct messages sent to any trade keys. + - **Args**: + - `--since `: Minutes back from now to query (default: 30). + - **Handler**: `execute_get_dm_user(since, ctx)` in `src/cli/get_dm_user.rs`. + +- **`senddm`** + - **Description**: Send a direct message to a user. + - **Args**: + - `--pubkey `: Pubkey of the counterpart. + - `--order-id `: Order identifier (for context). + - `--message ...`: Message parts; joined with spaces. + - **Handler**: `execute_send_dm(PublicKey::from_str(pubkey)?, ctx, order_id, &msg)` in `src/cli/send_dm.rs`. + +- **`dmtouser`** + - **Description**: Send a gift-wrapped direct message to a user. + - **Args**: + - `--pubkey `: Recipient pubkey. + - `--order-id `: Order id to derive ephemeral keys. + - `--message ...`: Message parts; joined with spaces. + - **Handler**: `execute_dm_to_user(PublicKey::from_str(pubkey)?, &ctx.client, order_id, &msg, &ctx.pool)` in `src/cli/dm_to_user.rs`. + +### Direct messages (admin / solver) + +- **`getadmindm`** + - **Description**: Get the latest direct messages for admin. + - **Args**: + - `--since `: Minutes back from now to query (default: 30). + - `--from-user`: If true, get messages from the counterparty instead of Mostro. + - **Handler**: `execute_get_dm(since, true, from_user, ctx)` in `src/cli/get_dm.rs`. + +- **`admsenddm`** *(admin only)* + - **Description**: Send a gift-wrapped direct message to a user as admin/solver. + - **Args**: + - `--pubkey `: Recipient pubkey. + - `--message ...`: Message parts; joined with spaces. + - **Handler**: `execute_adm_send_dm(PublicKey::from_str(pubkey)?, ctx, &msg)` in `src/cli/adm_send_dm.rs`. + +- **`sendadmindmattach`** *(admin only)* + - **Description**: Send an admin DM with an encrypted attachment stored on a Blossom server. + - **Args**: + - `--pubkey `: Admin recipient pubkey. + - `--order-id `: Order id to derive the correct trade key. + - `--file `: Path to the file to encrypt and upload. + - **Handler**: `execute_send_admin_dm_attach(PublicKey::from_str(pubkey)?, ctx, order_id, file)` in `src/cli/send_admin_dm_attach.rs`. + +### Identity & keys + +- **`conversationkey`** + - **Description**: Get the conversation key for direct messaging with a user. + - **Args**: + - `--pubkey `: Counterparty pubkey. + - **Handler**: `execute_conversation_key(&ctx.trade_keys, PublicKey::from_str(pubkey)?)` in `src/cli/conversation_key.rs`. + +- **`getlasttradeindex`** + - **Description**: Get last trade index of the user. + - **Args**: None. + - **Handler**: `execute_last_trade_index(&ctx.identity_keys, ctx.mostro_pubkey, ctx)` in `src/cli/last_trade_index.rs`. + +- **`getlasttradeprivkey`** + - **Description**: Get private key of the last trade index public key. + - **Args**: None. + - **Handler**: `execute_last_trade_index_private_key(ctx)` in `src/cli/last_trade_index.rs`. + +### Session & restore + +- **`restore`** + - **Description**: Restore session to recover all pending orders and disputes. + - **Args**: None. + - **Handler**: `execute_restore(&ctx.identity_keys, ctx.mostro_pubkey, ctx)` in `src/cli/restore.rs`. + +### Admin / solver dispute management + +- **`admcancel`** *(admin only)* + - **Description**: Cancel an order / dispute as admin. + - **Args**: + - `--order-id `: Order identifier. + - **Handler**: `execute_admin_cancel_dispute(order_id, ctx)` in `src/cli/take_dispute.rs`. + +- **`admsettle`** *(admin only)* + - **Description**: Settle a seller's hold invoice. + - **Args**: + - `--order-id `: Order identifier. + - **Handler**: `execute_admin_settle_dispute(order_id, ctx)` in `src/cli/take_dispute.rs`. + +- **`admaddsolver`** *(admin only)* + - **Description**: Add a new dispute solver. + - **Args**: + - `--npubkey `: Nostr pubkey of the solver. + - **Handler**: `execute_admin_add_solver(npubkey, ctx)` in `src/cli/take_dispute.rs`. + +- **`admtakedispute`** *(admin/solver only)* + - **Description**: Admin or solver takes a pending dispute. + - **Args**: + - `--dispute-id `: Dispute identifier. + - **Handler**: `execute_take_dispute(dispute_id, ctx)` in `src/cli/take_dispute.rs`. + +### Rating + +- **`rate`** + - **Description**: Rate counterpart after a successful trade. + - **Args**: + - `--order-id `: Order identifier. + - `--rating `: Rating from 1 to 5. + - **Handler**: `execute_rate_user(order_id, rating, ctx)` in `src/cli/rate_user.rs`. + +--- + +If you add a new variant to the `Commands` enum: + +1. Add the subcommand and its arguments in `src/cli.rs`. +2. Add its handler function in an appropriate `src/cli/*.rs` module. +3. Extend the `Commands::run` match to call the handler. +4. Update this `commands.md` file so documentation stays in sync for humans and AI tools. + diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 0000000..ee9d243 --- /dev/null +++ b/docs/database.md @@ -0,0 +1,158 @@ +## Database Schema & Persistence + +`mostro-cli` uses a local SQLite database called `mcli.db` to store: + +- User identity (mnemonic, root pubkey, last trade index). +- Cached orders and associated metadata. + +This file is created under the CLI data directory returned by `util::get_mcli_path()`. + +### Connection and initialization + +- Implemented in `src/db.rs`: + - `connect()`: + - Builds `mcli_db_path = format!("{}/mcli.db", get_mcli_path())`. + - If the file does not exist: + - Creates the file. + - Initializes a `SqlitePool`. + - Runs a SQL batch that creates `orders` and `users` tables. + - Generates a fresh 12-word mnemonic, derives the identity keys, and inserts a `User` row. + - If the file exists: + - Connects with `SqlitePool::connect(&db_url)`. + - Runs `migrate_remove_token_columns` to remove legacy `buyer_token` and `seller_token` columns if present. + +### Tables + +#### `orders` + +- **DDL (from `db.rs`)**: + + ```sql + CREATE TABLE IF NOT EXISTS orders ( + id TEXT PRIMARY KEY, + kind TEXT NOT NULL, + status TEXT NOT NULL, + amount INTEGER NOT NULL, + min_amount INTEGER, + max_amount INTEGER, + fiat_code TEXT NOT NULL, + fiat_amount INTEGER NOT NULL, + payment_method TEXT NOT NULL, + premium INTEGER NOT NULL, + trade_keys TEXT, + counterparty_pubkey TEXT, + is_mine BOOLEAN, + buyer_invoice TEXT, + request_id INTEGER, + created_at INTEGER, + expires_at INTEGER + ); + ``` + +- **Purpose**: + - Local cache of orders relevant to the CLI user. + - Stores the core financial parameters plus: + - Trade keys (secret key hex for the order). + - Whether the order belongs to this user. + - Buyer invoice and request id. + - Timestamps (`created_at`, `expires_at`). + +- **Model**: `db::Order` + - Fields mirror the columns (with `Option` where null is allowed). + - Key methods: + - `Order::new(pool, SmallOrder, trade_keys, request_id)`: + - Derives an ID (from `SmallOrder.id` or a generated UUID). + - Fills all fields based on a `mostro_core::SmallOrder` and the current trade keys. + - Attempts `insert_db`; if a uniqueness error is detected, falls back to `update_db`. + - `insert_db(&self, pool)`: + - Performs the `INSERT INTO orders (...) VALUES (...)`. + - `update_db(&self, pool)`: + - Performs an `UPDATE` when an order already exists. + - Fluent setters (`set_kind`, `set_status`, `set_amount`, `set_fiat_code`, etc.) for in-memory mutation before saving. + - `save(&self, pool)`: + - Updates an existing order row by ID. + - `save_new_id(pool, id, new_id)`: + - Updates the primary key for an order. + - `get_by_id(pool, id)`: + - Loads a single order (and returns an error if no ID is present). + - `get_all_trade_keys(pool)`: + - Returns distinct non-null `trade_keys` for all orders. + - `delete_by_id(pool, id)`: + - Deletes an order row. + +- **Usage**: + - Many command handlers persist or update orders via `util::save_order`, which internally calls `Order::new` and updates `User::last_trade_index`. + +#### `users` + +- **DDL (from `db.rs`)**: + + ```sql + CREATE TABLE IF NOT EXISTS users ( + i0_pubkey char(64) PRIMARY KEY, + mnemonic TEXT, + last_trade_index INTEGER, + created_at INTEGER + ); + ``` + +- **Purpose**: + - Persist the local Mostro CLI identity: + - Root pubkey for the account (`i0_pubkey`). + - BIP39 mnemonic. + - Last used trade index (to derive per-trade Nostr keys deterministically). + - Creation timestamp. + +- **Model**: `db::User` + - Key methods: + - `User::new(mnemonic, pool)`: + - Derives the account keys from the mnemonic with `nip06::FromMnemonic` / `nostr_sdk::Keys::from_mnemonic_advanced`. + - Inserts a `users` row with `i0_pubkey`, `mnemonic`, and `created_at`. + - `save(&self, pool)`: + - Updates `mnemonic` and `last_trade_index` for the stored user. + - `get(pool)`: + - Fetches the single user row (LIMIT 1). + - `get_last_trade_index(pool)` / `get_next_trade_index(pool)`: + - Helpers for working with the trade index counter. + - `get_identity_keys(pool)`: + - Re-derives the identity `Keys` from the stored mnemonic. + - `get_trade_keys(pool, index)`: + - Derives per-trade keys for a given index using the same mnemonic. + - `get_next_trade_keys(pool)`: + - Computes the next index and returns `(trade_keys, trade_index)`. + +- **Usage**: + - `cli::init_context()`: + - Uses `User::get_identity_keys` and `User::get_next_trade_keys` to create identity and trade key pairs. + - `util::save_order()`: + - After saving an order, it loads `User`, sets `last_trade_index`, and calls `save` to persist progress through the keyspace. + +### Migrations + +- **`migrate_remove_token_columns(pool)`** in `db.rs`: + - Checks for the presence of `buyer_token` and `seller_token` columns via `pragma_table_info('orders')`. + - If either exists, attempts to drop them with `ALTER TABLE orders DROP COLUMN ...`. + - Logs warnings instead of failing hard so older databases can continue working even if some engines do not support the `DROP COLUMN` syntax. + +### Helper utilities + +- **`util::storage`**: + - `save_order(order, trade_keys, request_id, trade_index, pool)`: + - Central place for persisting `Order` and updating the `User` record. + - `run_simple_order_msg(...)` and `admin_send_dm(...)` are not strictly DB-related but are often used alongside order persistence. + +### Extension guidelines + +- When adding a new column to `orders` or `users`: + - Update the `CREATE TABLE` statement in `connect()`. + - Extend the corresponding struct fields in `Order` or `User`. + - Update `insert_db`, `update_db`, and `save` statements. + - Add a migration helper if the change is not backward compatible with existing databases. + - Update this `database.md` file. + +- When introducing a new table: + - Add a `CREATE TABLE` clause to the initialization block in `connect()`. + - Create a new model struct with `sqlx::FromRow`. + - Provide CRUD helpers similar to `Order` and `User`. + - Document it here for clarity. + diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..d27fbc2 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,40 @@ +## mostro-cli Overview + +`mostro-cli` is a command-line client for interacting with the Mostro P2P Bitcoin/fiat marketplace. It talks to the Mostro backend over Nostr, manages a local SQLite database of orders and user identity, and wraps Mostro protocol flows into ergonomic commands. + +The CLI is heavily inspired by the Mostro backend documentation (`mostro/docs` in the main repo), but this documentation is specific to the CLI and is meant to give humans and AI assistants enough context to safely extend the tool. + +### High-level responsibilities + +- **Order lifecycle**: create, take, cancel, dispute, and settle orders using `mostro_core` types and the Mostro protocol. +- **Direct messaging**: send and receive Nostr DMs between users, admins, and solvers, including gift-wrapped messages and encrypted attachments. +- **Local persistence**: keep a local cache of orders and a deterministic identity in a SQLite database (`mcli.db`) under the CLI data directory. +- **Admin / solver tooling**: expose admin-only and solver-only flows (e.g. taking disputes, adding solvers, admin DMs) when run with the proper keys. + +### Key crates and technologies + +- **Rust + Tokio**: async CLI built on the Rust ecosystem. +- **clap**: command-line parsing, subcommands, help text, and argument validation. +- **nostr-sdk**: Nostr client for relay connectivity, DMs, and events. +- **mostro_core**: shared protocol types for orders, disputes, and messages. +- **sqlx + SQLite**: local storage for users and orders (`mcli.db`). + +### Core modules (top-level) + +- **`src/main.rs`**: entrypoint; wires `mostro-cli` to the `cli::run()` async function. +- **`src/cli.rs`**: defines the `Cli` struct, the `Commands` enum (all subcommands), context initialization, and the main dispatch logic for commands. +- **`src/db.rs`**: database connection and schema management, plus `User` and `Order` models and helpers. +- **`src/util/*`**: shared utilities for events, Nostr networking, messaging, storage helpers, and type wrappers. +- **`src/parser/*`**: parsing helpers for events (orders, disputes, DMs) into higher-level types. +- **`src/lightning/*`**: Lightning-related helpers used by invoice / payment flows. + +### How to read this docs folder + +This docs folder is optimized for AI-assisted development: + +- **`commands.md`**: One-stop reference for all CLI commands, arguments, and their handler functions. Useful when adding, renaming, or refactoring commands. +- **`architecture.md`**: Overview of module structure and main data / control flows. +- **`database.md`**: SQLite schema (`orders`, `users`), how they are used, and migration notes. + +If you add new subcommands, modules, or tables, please also update the relevant markdown file so that future contributors (and AI tools) have an up-to-date view of the system. + diff --git a/src/cli.rs b/src/cli.rs index 54f7fd0..cf8260f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -11,6 +11,7 @@ pub mod new_order; pub mod orders_info; pub mod rate_user; pub mod restore; +pub mod send_admin_dm_attach; pub mod send_dm; pub mod send_msg; pub mod take_dispute; @@ -31,6 +32,7 @@ use crate::cli::new_order::execute_new_order; use crate::cli::orders_info::execute_orders_info; use crate::cli::rate_user::execute_rate_user; use crate::cli::restore::execute_restore; +use crate::cli::send_admin_dm_attach::execute_send_admin_dm_attach; use crate::cli::send_dm::execute_send_dm; use crate::cli::take_dispute::execute_take_dispute; use crate::cli::take_order::execute_take_order; @@ -288,6 +290,18 @@ pub enum Commands { #[arg(short, long, num_args = 1..)] message: Vec, }, + /// Send admin DM with encrypted attachment to a Blossom server + SendAdminDmAttach { + /// Pubkey of the admin recipient + #[arg(short, long)] + pubkey: String, + /// Order id to derive the correct trade key + #[arg(short, long)] + order_id: Uuid, + /// Path to file to encrypt and upload + #[arg(short, long)] + file: std::path::PathBuf, + }, /// Get the conversation key for direct messaging with a user ConversationKey { /// Pubkey of the counterpart @@ -493,6 +507,14 @@ impl Commands { let msg = message.join(" "); execute_adm_send_dm(PublicKey::from_str(pubkey)?, ctx, &msg).await } + Commands::SendAdminDmAttach { + pubkey, + order_id, + file, + } => { + execute_send_admin_dm_attach(PublicKey::from_str(pubkey)?, ctx, order_id, file) + .await + } Commands::ConversationKey { pubkey } => { execute_conversation_key(&ctx.trade_keys, PublicKey::from_str(pubkey)?).await } diff --git a/src/cli/send_admin_dm_attach.rs b/src/cli/send_admin_dm_attach.rs new file mode 100644 index 0000000..ea5eeb7 --- /dev/null +++ b/src/cli/send_admin_dm_attach.rs @@ -0,0 +1,257 @@ +use std::fs; +use std::path::PathBuf; + +use anyhow::Result; +use chacha20poly1305::aead::{Aead, KeyInit}; +use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; +use nostr_sdk::prelude::*; +use rand_core::OsRng; +use rand_core::RngCore; +use uuid::Uuid; + +use crate::cli::Context; +use crate::db::Order; +use crate::parser::common::{ + create_emoji_field_row, create_field_value_header, create_standard_table, +}; + +const MAX_FILE_SIZE_BYTES: u64 = 25 * 1024 * 1024; + +const BLOSSOM_SERVERS: &[&str] = &[ + "https://blossom.primal.net", + "https://blossom.band", + "https://nostr.media", + "https://blossom.sector01.com", + "https://24242.io", + "https://otherstuff.shaving.kiwi", + "https://blossom.f7z.io", + "https://nosto.re", + "https://blossom.poster.place", +]; + +fn derive_shared_key(trade_keys: &Keys, admin_pubkey: &PublicKey) -> [u8; 32] { + use bitcoin::secp256k1::ecdh::shared_secret_point; + use bitcoin::secp256k1::{Parity, PublicKey as SecpPublicKey}; + + let sk = trade_keys.secret_key(); + let xonly = admin_pubkey + .xonly() + .expect("failed to get x-only public key for admin"); + let secp_pk = SecpPublicKey::from_x_only_public_key(xonly, Parity::Even); + let mut point_bytes = shared_secret_point(&secp_pk, &sk).as_slice().to_vec(); + point_bytes.resize(32, 0); + point_bytes + .try_into() + .expect("shared secret point must be at least 32 bytes") +} + +fn encrypt_blob(shared_key: [u8; 32], plaintext: &[u8]) -> Result<(Vec, String)> { + let key = Key::from_slice(&shared_key); + let cipher = ChaCha20Poly1305::new(key); + + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + let ciphertext = cipher + .encrypt(nonce, plaintext) + .map_err(|e| anyhow::anyhow!("encryption failed: {e}"))?; + + if ciphertext.len() < 16 { + return Err(anyhow::anyhow!( + "ciphertext too short, expected at least 16 bytes auth tag" + )); + } + + let (encrypted_data, auth_tag) = ciphertext.split_at(plaintext.len()); + + let mut blob = Vec::with_capacity(12 + encrypted_data.len() + auth_tag.len()); + blob.extend_from_slice(&nonce_bytes); + blob.extend_from_slice(encrypted_data); + blob.extend_from_slice(auth_tag); + + let nonce_hex = nonce_bytes + .iter() + .map(|b| format!("{b:02x}")) + .collect::(); + + Ok((blob, nonce_hex)) +} + +async fn upload_to_blossom(encrypted_blob: Vec) -> Result { + use reqwest::StatusCode; + + let client = reqwest::Client::new(); + + for server in BLOSSOM_SERVERS { + let url = format!("{}/upload", server.trim_end_matches('/')); + let res = client + .put(&url) + .header("Content-Type", "application/octet-stream") + .body(encrypted_blob.clone()) + .send() + .await; + + match res { + Ok(resp) if resp.status() == StatusCode::OK => { + let body = resp + .text() + .await + .map_err(|e| anyhow::anyhow!("failed to read blossom response: {e}"))?; + if body.trim().starts_with("blossom://") { + return Ok(body.trim().to_string()); + } + } + Ok(resp) => { + eprintln!( + "Blossom upload failed on {server} with status {}", + resp.status() + ); + } + Err(e) => { + eprintln!("Blossom upload error on {server}: {e}"); + } + } + } + + Err(anyhow::anyhow!("all Blossom servers failed")) +} + +pub async fn execute_send_admin_dm_attach( + receiver: PublicKey, + ctx: &Context, + order_id: &Uuid, + file_path: &PathBuf, +) -> Result<()> { + println!("📎 Send Admin DM Attachment"); + println!("═══════════════════════════════════════"); + + let metadata = fs::metadata(file_path) + .map_err(|e| anyhow::anyhow!("failed to read file metadata: {e}"))?; + if !metadata.is_file() { + anyhow::bail!("path is not a regular file: {}", file_path.display()); + } + if metadata.len() > MAX_FILE_SIZE_BYTES { + anyhow::bail!( + "file too large ({} bytes, max is {} bytes)", + metadata.len(), + MAX_FILE_SIZE_BYTES + ); + } + + let file_bytes = fs::read(file_path) + .map_err(|e| anyhow::anyhow!("failed to read file {}: {e}", file_path.display()))?; + + let order = Order::get_by_id(&ctx.pool, &order_id.to_string()) + .await + .map_err(|_| anyhow::anyhow!("order {} not found", order_id))?; + + let trade_keys = match order.trade_keys.as_ref() { + Some(trade_keys) => Keys::parse(trade_keys)?, + None => anyhow::bail!("No trade_keys found for this order"), + }; + + let mut table = create_standard_table(); + table.set_header(create_field_value_header()); + table.add_row(create_emoji_field_row( + "📋 ", + "Order ID", + &order_id.to_string(), + )); + table.add_row(create_emoji_field_row( + "🔑 ", + "Trade Pubkey", + &trade_keys.public_key().to_hex(), + )); + table.add_row(create_emoji_field_row( + "🎯 ", + "Admin Pubkey", + &receiver.to_string(), + )); + table.add_row(create_emoji_field_row( + "📄 ", + "File", + &file_path.to_string_lossy(), + )); + table.add_row(create_emoji_field_row( + "📏 ", + "Size", + &format!("{} bytes", file_bytes.len()), + )); + + println!("{table}"); + println!("💡 Encrypting file and uploading to Blossom...\n"); + + let shared_key = derive_shared_key(&trade_keys, &receiver); + let (encrypted_blob, nonce_hex) = encrypt_blob(shared_key, &file_bytes)?; + let encrypted_size = encrypted_blob.len(); + let blossom_url = upload_to_blossom(encrypted_blob).await?; + + let filename = file_path + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("attachment.bin") + .to_string(); + + // Best-effort MIME type detection based on the file extension. + // We keep this intentionally simple to avoid extra dependencies and + // fall back to application/octet-stream when unknown. + let mime_type = file_path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_ascii_lowercase()) + .map(|ext| match ext.as_str() { + "txt" => "text/plain", + "md" => "text/markdown", + "json" => "application/json", + "csv" => "text/csv", + "jpg" | "jpeg" => "image/jpeg", + "png" => "image/png", + "gif" => "image/gif", + "webp" => "image/webp", + "pdf" => "application/pdf", + "zip" => "application/zip", + "tar" => "application/x-tar", + "gz" | "tgz" => "application/gzip", + "mp3" => "audio/mpeg", + "mp4" => "video/mp4", + "mov" => "video/quicktime", + _ => "application/octet-stream", + }) + .unwrap_or("application/octet-stream") + .to_string(); + + let payload_json = serde_json::json!({ + "type": "file_encrypted", + "blossom_url": blossom_url, + "nonce": nonce_hex, + "mime_type": mime_type, + "original_size": file_bytes.len(), + "filename": filename, + "encrypted_size": encrypted_size, + "file_type": "document", + }); + + let content = serde_json::to_string(&payload_json) + .map_err(|e| anyhow::anyhow!("failed to serialize attachment payload: {e}"))?; + + let pow: u8 = std::env::var("POW") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap_or(0); + + let rumor = EventBuilder::text_note(content) + .pow(pow) + .build(trade_keys.public_key()); + + let event = EventBuilder::gift_wrap(&trade_keys, &receiver, rumor, Tags::new()).await?; + + ctx.client + .send_event(&event) + .await + .map_err(|e| anyhow::anyhow!("failed to send gift wrap event: {e}"))?; + + println!("✅ Encrypted attachment sent successfully to admin!"); + + Ok(()) +} From e926ffda06b3d1530ca22e3d422af369f9d28543 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Wed, 11 Feb 2026 23:14:25 +0100 Subject: [PATCH 2/9] fix(sendadmindmattach): Blossom upload auth and response handling - Use BUD-01 Blossom auth: kind 24242 with tags t=upload, expiration, x=sha256 (servers reject NIP-98 kind 27235; require human-readable content) - Add Content-Type fallback: try application/zip then image/png when application/octet-stream is rejected (415/400) - Accept 200 OK JSON blob descriptor: parse response and use 'url' field when present (blossom.primal.net, nostr.media return JSON not blossom://) - Add nip98 and base64 deps for auth; add bitcoin_hashes for payload hash Co-authored-by: Cursor --- Cargo.lock | 2 + Cargo.toml | 4 +- src/cli/send_admin_dm_attach.rs | 132 +++++++++++++++++++++++++------- src/util/misc.rs | 2 +- 4 files changed, 111 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abf2116..cd2c01f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1661,8 +1661,10 @@ name = "mostro-cli" version = "0.14.5" dependencies = [ "anyhow", + "base64 0.22.1", "bip39", "bitcoin", + "bitcoin_hashes 0.14.0", "chacha20poly1305", "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index f57517c..a7cab1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.99" clap = { version = "4.5.46", features = ["derive"] } -nostr-sdk = { version = "0.43.0", features = ["nip06", "nip44", "nip59"] } +nostr-sdk = { version = "0.43.0", features = ["nip06", "nip44", "nip59", "nip98"] } serde = "1.0.219" serde_json = "1.0.143" tokio = { version = "1.47.1", features = ["full"] } @@ -50,6 +50,8 @@ dirs = "6.0.0" chacha20poly1305 = "0.10.1" rand_core = "0.6.4" bitcoin = "0.32.2" +bitcoin_hashes = { version = "0.14", default-features = false } +base64 = "0.22" [package.metadata.release] # (Default: true) Set to false to prevent automatically running `cargo publish`. diff --git a/src/cli/send_admin_dm_attach.rs b/src/cli/send_admin_dm_attach.rs index ea5eeb7..e4dba00 100644 --- a/src/cli/send_admin_dm_attach.rs +++ b/src/cli/send_admin_dm_attach.rs @@ -2,6 +2,10 @@ use std::fs; use std::path::PathBuf; use anyhow::Result; +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine; +use bitcoin_hashes::sha256::Hash as Sha256Hash; +use bitcoin_hashes::Hash; use chacha20poly1305::aead::{Aead, KeyInit}; use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; use nostr_sdk::prelude::*; @@ -38,7 +42,7 @@ fn derive_shared_key(trade_keys: &Keys, admin_pubkey: &PublicKey) -> [u8; 32] { .xonly() .expect("failed to get x-only public key for admin"); let secp_pk = SecpPublicKey::from_x_only_public_key(xonly, Parity::Even); - let mut point_bytes = shared_secret_point(&secp_pk, &sk).as_slice().to_vec(); + let mut point_bytes = shared_secret_point(&secp_pk, sk).as_slice().to_vec(); point_bytes.resize(32, 0); point_bytes .try_into() @@ -78,39 +82,113 @@ fn encrypt_blob(shared_key: [u8; 32], plaintext: &[u8]) -> Result<(Vec, Stri Ok((blob, nonce_hex)) } -async fn upload_to_blossom(encrypted_blob: Vec) -> Result { +/// Upload encrypted blob to a Blossom server. +/// Blossom BUD-01 requires kind 24242 with: content (human-readable), tags ["t","upload"], +/// ["expiration", ""], ["x", ""]. +async fn upload_to_blossom(trade_keys: &Keys, encrypted_blob: Vec) -> Result { use reqwest::StatusCode; let client = reqwest::Client::new(); + let payload_hash = Sha256Hash::hash(&encrypted_blob); + let payload_hex = payload_hash.to_string(); + + // Expiration: 1 hour from now (BUD-01 requires expiration in the future) + let expiration = Timestamp::from(Timestamp::now().as_u64() + 3600); for server in BLOSSOM_SERVERS { - let url = format!("{}/upload", server.trim_end_matches('/')); - let res = client - .put(&url) - .header("Content-Type", "application/octet-stream") - .body(encrypted_blob.clone()) - .send() - .await; - - match res { - Ok(resp) if resp.status() == StatusCode::OK => { - let body = resp - .text() - .await - .map_err(|e| anyhow::anyhow!("failed to read blossom response: {e}"))?; - if body.trim().starts_with("blossom://") { - return Ok(body.trim().to_string()); - } - } - Ok(resp) => { - eprintln!( - "Blossom upload failed on {server} with status {}", - resp.status() - ); + let url_str = format!("{}/upload", server.trim_end_matches('/')); + let upload_url = match Url::parse(&url_str) { + Ok(u) => u, + Err(e) => { + eprintln!("Blossom invalid URL {url_str}: {e}"); + continue; } + }; + + let normalized_url_str = upload_url.as_str(); + + // BUD-01: kind 24242, content human-readable, tags: t=upload, expiration, x=sha256 + let tags = [ + Tag::hashtag("upload"), // ["t", "upload"] + Tag::expiration(expiration), + Tag::custom(TagKind::x(), [payload_hex.clone()]), + ]; + let event = match EventBuilder::new(Kind::BlossomAuth, "Upload Blob") + .tags(tags) + .sign_with_keys(trade_keys) + { + Ok(e) => e, Err(e) => { - eprintln!("Blossom upload error on {server}: {e}"); + eprintln!("Blossom auth event build failed for {server}: {e}"); + continue; + } + }; + let auth_header = format!("Nostr {}", BASE64.encode(event.as_json())); + + // Many Blossom servers reject application/octet-stream. Try application/zip first, then image/png. + let content_types = ["application/zip", "image/png"]; + let mut last_status = None; + let mut last_error_body = String::new(); + + for content_type in content_types { + let res = client + .put(normalized_url_str) + .header("Content-Type", content_type) + .header("Authorization", &auth_header) + .body(encrypted_blob.clone()) + .send() + .await; + + match res { + Ok(resp) if resp.status() == StatusCode::OK => { + let body = resp + .text() + .await + .map_err(|e| anyhow::anyhow!("failed to read blossom response: {e}"))?; + let body = body.trim(); + // BUD-02: server may return JSON blob descriptor with "url" field + if let Ok(v) = serde_json::from_str::(body) { + if let Some(url) = v.get("url").and_then(|u| u.as_str()) { + return Ok(url.to_string()); + } + } + if body.starts_with("blossom://") { + return Ok(body.to_string()); + } + last_status = Some(StatusCode::OK); + last_error_body = body.to_string(); + break; + } + Ok(resp) => { + let status = resp.status(); + last_status = Some(status); + last_error_body = resp.text().await.unwrap_or_default(); + // If rejected for content type, try next; otherwise report and break + if status != StatusCode::UNSUPPORTED_MEDIA_TYPE + && status != StatusCode::BAD_REQUEST + { + break; + } + } + Err(e) => { + last_status = None; + last_error_body = e.to_string(); + break; + } + } + } + + if let Some(status) = last_status { + let status_text = status.canonical_reason().unwrap_or("Unknown"); + eprintln!( + "Blossom upload failed on {server} with status {} {}", + status, status_text + ); + if !last_error_body.is_empty() && last_error_body.len() < 500 { + eprintln!(" Response body: {}", last_error_body); } + } else { + eprintln!("Blossom upload error on {server}: {}", last_error_body); } } @@ -185,7 +263,7 @@ pub async fn execute_send_admin_dm_attach( let shared_key = derive_shared_key(&trade_keys, &receiver); let (encrypted_blob, nonce_hex) = encrypt_blob(shared_key, &file_bytes)?; let encrypted_size = encrypted_blob.len(); - let blossom_url = upload_to_blossom(encrypted_blob).await?; + let blossom_url = upload_to_blossom(&trade_keys, encrypted_blob).await?; let filename = file_path .file_name() diff --git a/src/util/misc.rs b/src/util/misc.rs index c57a7cc..00f094e 100644 --- a/src/util/misc.rs +++ b/src/util/misc.rs @@ -10,7 +10,7 @@ pub fn uppercase_first(s: &str) -> String { pub fn get_mcli_path() -> String { let home_dir = dirs::home_dir().expect("Couldn't get home directory"); - let mcli_path = format!("{}/.mcliUserA", home_dir.display()); + let mcli_path = format!("{}/.mcliUserB", home_dir.display()); if !Path::new(&mcli_path).exists() { match fs::create_dir(&mcli_path) { Ok(_) => println!("Directory {} created.", mcli_path), From dbeb35162cc2fc0e73b8d3bd43ff556aa1c77546 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Fri, 13 Feb 2026 14:18:17 +0100 Subject: [PATCH 3/9] docs: added specifications for direct message and direct message with attachment --- docs/send_admin_dm_attach_flow.md | 456 ++++++++++++++++++++++++++++++ docs/senddm_flow.md | 297 +++++++++++++++++++ 2 files changed, 753 insertions(+) create mode 100644 docs/send_admin_dm_attach_flow.md create mode 100644 docs/senddm_flow.md diff --git a/docs/send_admin_dm_attach_flow.md b/docs/send_admin_dm_attach_flow.md new file mode 100644 index 0000000..c46bee0 --- /dev/null +++ b/docs/send_admin_dm_attach_flow.md @@ -0,0 +1,456 @@ +## `sendadmindmattach` flow (`src/cli/send_admin_dm_attach.rs`) + +This document explains the full flow for the `sendadmindmattach` command: from CLI invocation, through Blossom upload, to the final encrypted DM sent on Nostr, including protocols and keys involved. + +### 1. CLI entrypoint + +- **Command**: `sendadmindmattach` *(admin only)* +- **Defined in**: `src/cli.rs` (`Commands::SendAdminDmAttach`) +- **Documented in**: `docs/commands.md` +- **Args**: + - `--pubkey `: Admin / solver recipient pubkey. + - `--order-id `: Order ID to pick the correct trade keys. + - `--file `: Path to the file to encrypt and upload. +- **Handler**: + - `execute_send_admin_dm_attach(PublicKey::from_str(pubkey)?, ctx, order_id, file)` in `src/cli/send_admin_dm_attach.rs`. + +The global `Context` (`src/cli.rs`) provides: + +- `identity_keys: Keys`: user’s i0 identity keys. +- `trade_keys: Keys`: current per‑trade keys. +- `client: Client`: Nostr client connected to relays from `RELAYS`. +- `pool: SqlitePool`: database for orders and users. +- `context_keys: Option`: admin keys when `ADMIN_NSEC` is configured. +- `mostro_pubkey: PublicKey`: Mostro service key (not directly used here). + +### 2. Handler: `execute_send_admin_dm_attach` + +File: `src/cli/send_admin_dm_attach.rs` + +```198:335:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs +pub async fn execute_send_admin_dm_attach( + receiver: PublicKey, + ctx: &Context, + order_id: &Uuid, + file_path: &PathBuf, +) -> Result<()> { + // 1) Validate file and show summary + // 2) Load order and per-order trade keys from DB + // 3) Derive shared key with admin pubkey and encrypt file (ChaCha20-Poly1305) + // 4) Upload encrypted blob to Blossom with BUD-01 auth + // 5) Build DM payload JSON referencing Blossom URL + crypto metadata + // 6) Gift-wrap DM and send over Nostr +} +``` + +Step‑by‑step: + +1. **File validation & UI**: + - `fs::metadata(file_path)`: + - Ensures the path is a regular file. + - Enforces a hard size limit: `MAX_FILE_SIZE_BYTES = 25 MiB`. + - Reads file data into memory: + + ```220:221:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs + let file_bytes = fs::read(file_path) + .map_err(|e| anyhow::anyhow!("failed to read file {}: {e}", file_path.display()))?; + ``` + + - Builds and prints a table with: + - Order ID + - Trade pubkey + - Admin pubkey (receiver) + - File path + - File size + +2. **Resolve per‑order trade keys**: + + ```223:230:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs + let order = Order::get_by_id(&ctx.pool, &order_id.to_string()) + .await + .map_err(|_| anyhow::anyhow!("order {} not found", order_id))?; + + let trade_keys = match order.trade_keys.as_ref() { + Some(trade_keys) => Keys::parse(trade_keys)?, + None => anyhow::bail!("No trade_keys found for this order"), + }; + ``` + + - Orders persisted in SQLite store a `trade_keys` string. + - `Keys::parse` reconstructs the **per‑order ephemeral keypair**. + - These trade keys are used for: + - Encrypting the attachment (via ECDH shared secret). + - Sending/signing the Nostr DM (gift‑wrapped text note). + +### 3. ECDH shared key & symmetric encryption + +#### 3.1 Derive ECDH shared key + +```36:50:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs +fn derive_shared_key(trade_keys: &Keys, admin_pubkey: &PublicKey) -> [u8; 32] { + use bitcoin::secp256k1::ecdh::shared_secret_point; + use bitcoin::secp256k1::{Parity, PublicKey as SecpPublicKey}; + + let sk = trade_keys.secret_key(); + let xonly = admin_pubkey + .xonly() + .expect("failed to get x-only public key for admin"); + let secp_pk = SecpPublicKey::from_x_only_public_key(xonly, Parity::Even); + let mut point_bytes = shared_secret_point(&secp_pk, sk).as_slice().to_vec(); + point_bytes.resize(32, 0); + point_bytes + .try_into() + .expect("shared secret point must be at least 32 bytes") +} +``` + +- **Inputs**: + - `trade_keys.secret_key()`: ECDSA secp256k1 secret for the order. + - `admin_pubkey`: receiver’s Nostr pubkey (converted to x‑only form). +- **Operation**: + - Computes an ECDH shared secret point using secp256k1. + - Takes the x‑coordinate bytes, truncates/pads to 32 bytes. +- **Output**: + - A 32‑byte shared secret `[u8; 32]` used as the **symmetric key** for file encryption. + +#### 3.2 Encrypt blob with ChaCha20‑Poly1305 + +```52:83:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs +fn encrypt_blob(shared_key: [u8; 32], plaintext: &[u8]) -> Result<(Vec, String)> { + let key = Key::from_slice(&shared_key); + let cipher = ChaCha20Poly1305::new(key); + + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + let ciphertext = cipher + .encrypt(nonce, plaintext) + .map_err(|e| anyhow::anyhow!("encryption failed: {e}"))?; + + if ciphertext.len() < 16 { + return Err(anyhow::anyhow!( + "ciphertext too short, expected at least 16 bytes auth tag" + )); + } + + let (encrypted_data, auth_tag) = ciphertext.split_at(plaintext.len()); + + let mut blob = Vec::with_capacity(12 + encrypted_data.len() + auth_tag.len()); + blob.extend_from_slice(&nonce_bytes); + blob.extend_from_slice(encrypted_data); + blob.extend_from_slice(auth_tag); + + let nonce_hex = nonce_bytes + .iter() + .map(|b| format!("{b:02x}")) + .collect::(); + + Ok((blob, nonce_hex)) +} +``` + +Encryption details: + +- Algorithm: **ChaCha20‑Poly1305 AEAD**. +- Nonce: random 12‑byte value from `OsRng`. +- Ciphertext layout: + - First 12 bytes: nonce. + - Next `plaintext.len()` bytes: encrypted payload. + - Final 16 bytes: Poly1305 authentication tag. +- Returned values: + - `blob`: `nonce || encrypted_data || auth_tag` (the bytes sent to Blossom). + - `nonce_hex`: hex‑encoded nonce, needed later to decrypt the blob. + +In the handler: + +```263:266:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs +let shared_key = derive_shared_key(&trade_keys, &receiver); +let (encrypted_blob, nonce_hex) = encrypt_blob(shared_key, &file_bytes)?; +let encrypted_size = encrypted_blob.len(); +let blossom_url = upload_to_blossom(&trade_keys, encrypted_blob).await?; +``` + +### 4. Blossom upload (BUD‑01/BUD‑02, kind 24242) + +The encrypted blob is uploaded to a **Blossom** server. Upload is authenticated using Blossom’s BUD‑01/BUD‑02 spec: + +- **Auth event kind**: `24242` (`Kind::BlossomAuth`). +- **Auth tags**: + - `["t", "upload"]`: verb for upload action. + - `["expiration", ""]`: NIP‑40 expiration, must be in the future. + - `["x", ""]`: SHA256 of the encrypted blob. + +Implementation: + +```85:196:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs +async fn upload_to_blossom(trade_keys: &Keys, encrypted_blob: Vec) -> Result { + use reqwest::StatusCode; + + let client = reqwest::Client::new(); + let payload_hash = Sha256Hash::hash(&encrypted_blob); + let payload_hex = payload_hash.to_string(); + + // Expiration: 1 hour from now (BUD-01 requires expiration in the future) + let expiration = Timestamp::from(Timestamp::now().as_u64() + 3600); + + for server in BLOSSOM_SERVERS { + let url_str = format!("{}/upload", server.trim_end_matches('/')); + let upload_url = match Url::parse(&url_str) { + Ok(u) => u, + Err(e) => { + eprintln!("Blossom invalid URL {url_str}: {e}"); + continue; + } + }; + + let normalized_url_str = upload_url.as_str(); + + // BUD-01: kind 24242, content human-readable, tags: t=upload, expiration, x=sha256 + let tags = [ + Tag::hashtag("upload"), // ["t", "upload"] + Tag::expiration(expiration), + Tag::custom(TagKind::x(), [payload_hex.clone()]), + ]; + let event = match EventBuilder::new(Kind::BlossomAuth, "Upload Blob") + .tags(tags) + .sign_with_keys(trade_keys) + { + Ok(e) => e, + Err(e) => { + eprintln!("Blossom auth event build failed for {server}: {e}"); + continue; + } + }; + let auth_header = format!("Nostr {}", BASE64.encode(event.as_json())); + + // Many Blossom servers reject application/octet-stream. Try application/zip first, then image/png. + let content_types = ["application/zip", "image/png"]; + let mut last_status = None; + let mut last_error_body = String::new(); + + for content_type in content_types { + let res = client + .put(normalized_url_str) + .header("Content-Type", content_type) + .header("Authorization", &auth_header) + .body(encrypted_blob.clone()) + .send() + .await; + + match res { + Ok(resp) if resp.status() == StatusCode::OK => { + let body = resp + .text() + .await + .map_err(|e| anyhow::anyhow!("failed to read blossom response: {e}"))?; + let body = body.trim(); + // BUD-02: server may return JSON blob descriptor with "url" field + if let Ok(v) = serde_json::from_str::(body) { + if let Some(url) = v.get("url").and_then(|u| u.as_str()) { + return Ok(url.to_string()); + } + } + if body.starts_with("blossom://") { + return Ok(body.to_string()); + } + last_status = Some(StatusCode::OK); + last_error_body = body.to_string(); + break; + } + Ok(resp) => { + let status = resp.status(); + last_status = Some(status); + last_error_body = resp.text().await.unwrap_or_default(); + // If rejected for content type, try next; otherwise report and break + if status != StatusCode::UNSUPPORTED_MEDIA_TYPE + && status != StatusCode::BAD_REQUEST + { + break; + } + } + Err(e) => { + last_status = None; + last_error_body = e.to_string(); + break; + } + } + } + + if let Some(status) = last_status { + let status_text = status.canonical_reason().unwrap_or("Unknown"); + eprintln!( + "Blossom upload failed on {server} with status {} {}", + status, status_text + ); + if !last_error_body.is_empty() && last_error_body.len() < 500 { + eprintln!(" Response body: {}", last_error_body); + } + } else { + eprintln!("Blossom upload error on {server}: {}", last_error_body); + } + } + + Err(anyhow::anyhow!("all Blossom servers failed")) +} +``` + +Blossom protocol details: + +- **Auth event**: + - `kind = 24242` (`Kind::BlossomAuth`). + - `content = "Upload Blob"` (human‑readable). + - `tags`: + - `["t", "upload"]` – operation verb. + - `["expiration", ""]` – must be in the future. + - `["x", ""]` – encoded via `TagKind::x()`. + - Encoded as: + - `Authorization: Nostr `. + +- **Request**: + - `PUT /upload` with body = `encrypted_blob`. + - `Content-Type` fallback: + - Tries `application/zip`, then `image/png` if the server responds with 400/415 for content type. + +- **Response handling**: + - 200 OK: + - If body is JSON with `"url"`: returns that URL (BUD‑02 Blob Descriptor). + - Else if body starts with `blossom://`: returns that URI. + - Non‑200 or no usable response: logs error and tries next server. + +The function ultimately returns a **public URL** (`blossom_url`) pointing to the encrypted blob. + +### 5. DM payload and gift wrap to admin + +Once the encrypted blob is uploaded and we have `blossom_url`, the DM payload is built: + +```268:311:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs +let filename = file_path + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("attachment.bin") + .to_string(); + +// Best-effort MIME type detection based on the file extension. +let mime_type = file_path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_ascii_lowercase()) + .map(|ext| match ext.as_str() { + "txt" => "text/plain", + "md" => "text/markdown", + "json" => "application/json", + "csv" => "text/csv", + "jpg" | "jpeg" => "image/jpeg", + "png" => "image/png", + "gif" => "image/gif", + "webp" => "image/webp", + "pdf" => "application/pdf", + "zip" => "application/zip", + "tar" => "application/x-tar", + "gz" | "tgz" => "application/gzip", + "mp3" => "audio/mpeg", + "mp4" => "video/mp4", + "mov" => "video/quicktime", + _ => "application/octet-stream", + }) + .unwrap_or("application/octet-stream") + .to_string(); + +let payload_json = serde_json::json!({ + "type": "file_encrypted", + "blossom_url": blossom_url, + "nonce": nonce_hex, + "mime_type": mime_type, + "original_size": file_bytes.len(), + "filename": filename, + "encrypted_size": encrypted_size, + "file_type": "document", +}); + +let content = serde_json::to_string(&payload_json) + .map_err(|e| anyhow::anyhow!("failed to serialize attachment payload: {e}"))?; +``` + +This JSON is the **Mostro DM payload** describing the encrypted attachment: + +- `type = "file_encrypted"` – payload kind. +- `blossom_url` – where the admin can fetch the encrypted blob. +- `nonce` – hex nonce for ChaCha20‑Poly1305. +- `mime_type` – hint about original file type. +- `original_size` / `encrypted_size` – size bookkeeping. +- `filename` – original filename. + +#### 5.1 Gift‑wrapped DM event + +```316:331:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs +let pow: u8 = std::env::var("POW") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap_or(0); + +let rumor = EventBuilder::text_note(content) + .pow(pow) + .build(trade_keys.public_key()); + +let event = EventBuilder::gift_wrap(&trade_keys, &receiver, rumor, Tags::new()).await?; + +ctx.client + .send_event(&event) + .await + .map_err(|e| anyhow::anyhow!("failed to send gift wrap event: {e}"))?; + +println!("✅ Encrypted attachment sent successfully to admin!"); +``` + +- **Rumor event** (inner): + - `kind`: `TextNote` + - `content`: the `payload_json` string above. + - `pubkey`: `trade_keys.public_key()` (per‑order trade identity). + - Optional POW from `POW` env var. + +- **Outer GiftWrap event** (NIP‑59): + - Created via `EventBuilder::gift_wrap(&trade_keys, &receiver, rumor, Tags::new())`. + - **Signer / sender**: `trade_keys` (per‑order). + - **Recipient**: `receiver` (admin / solver Nostr pubkey). + - **Tags**: currently empty (no extra expiration here; optional). + +- **Relaying**: + - Final event is sent with `ctx.client.send_event(&event).await`. + - `ctx.client` is connected to configured Nostr relays via `RELAYS` env var. + +### 6. Keys and protocols summary + +- **Keys**: + - `trade_keys` (per‑order): + - Used for: + - ECDH shared secret with admin pubkey for file encryption. + - Nostr DM identity for the rumor + giftwrap. + - (Indirectly) for signing Blossom auth event (kind 24242). + - `receiver`: + - Admin / solver Nostr pubkey; DM destination for the final NIP‑59 GiftWrap. + +- **Protocols**: + - **ChaCha20‑Poly1305**: + - Symmetric AEAD encryption of the file using ECDH‑derived key. + - **Blossom (BUD‑01/BUD‑02)**: + - Auth event: kind 24242, tags `t=upload`, `expiration`, `x=`. + - Upload endpoint: `PUT /upload` with binary body and auth header. + - Response: JSON Blob Descriptor (with `url`) or `blossom://…` URI. + - **Mostro DM payload**: + - JSON with attachment metadata: + - `type`, `blossom_url`, `nonce`, `mime_type`, sizes, `filename`. + - **Nostr**: + - NIP‑13: optional POW on the rumor text note. + - NIP‑40: optional expiration (not used on the DM here). + - NIP‑59: GiftWrap envelope: + - Wraps the text‑note rumor for the admin’s pubkey. + +End‑to‑end, `sendadmindmattach`: + +1. Derives a shared ECDH key between trade and admin. +2. Encrypts the file with ChaCha20‑Poly1305. +3. Authenticates to Blossom with a kind‑24242 auth event (BUD‑01) and uploads the encrypted blob (BUD‑02). +4. Builds a Mostro DM payload with Blossom URL + crypto metadata. +5. Sends a NIP‑59 gift‑wrapped text note from the trade keys to the admin pubkey with that payload as content. + diff --git a/docs/senddm_flow.md b/docs/senddm_flow.md new file mode 100644 index 0000000..18eb55a --- /dev/null +++ b/docs/senddm_flow.md @@ -0,0 +1,297 @@ +## `senddm` flow (`src/cli/send_dm.rs`) + +This document explains the full flow for the `senddm` command, from CLI invocation to the Nostr event sent on the relays, including which keys and protocol pieces are involved. + +### 1. CLI entrypoint + +- **Command**: `senddm` +- **Defined in**: `src/cli.rs` (`Commands::SendDm`) +- **Documented in**: `docs/commands.md` +- **Args**: + - `--pubkey `: Recipient pubkey. + - `--order-id `: Order identifier (used to select the correct trade key). + - `--message ...`: One or more message parts, joined with spaces. + +The CLI argument parser constructs a `Commands::SendDm { pubkey, order_id, message }` variant and then calls: + +- `Commands::run(&self, ctx: &Context)` in `src/cli.rs`, which dispatches to: + - `execute_send_dm(PublicKey::from_str(pubkey)?, ctx, order_id, &msg)` in `src/cli/send_dm.rs`. + +The shared `Context` (`src/cli.rs`) contains: + +- `identity_keys: Keys`: long‑term i0 identity keys for the user. +- `trade_keys: Keys`: current ephemeral trade keys (derived from identity via BIP32/NIP‑06). +- `client: Client`: connected Nostr client (relays from `RELAYS` env var). +- `pool: SqlitePool`: SQLite connection (orders and users). +- `mostro_pubkey: PublicKey`: Mostro service pubkey. +- `context_keys: Option`: admin keys when running admin commands. + +### 2. High‑level handler (`execute_send_dm`) + +File: `src/cli/send_dm.rs` + +```12:69:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_dm.rs +pub async fn execute_send_dm( + receiver: PublicKey, + ctx: &Context, + order_id: &Uuid, + message: &str, +) -> Result<()> { + // 1) Print a summary table (order id, recipient, message) + // 2) Build a Mostro-core Message (Action::SendDm, Payload::TextMessage) + // 3) Resolve the trade keys for this order from the DB + // 4) Delegate to util::send_dm to construct and send the Nostr event +} +``` + +Step‑by‑step: + +1. **UI / logging**: + - Builds a table with: + - `Order ID` + - `Recipient` (receiver pubkey) + - `Message` + - Prints it to the terminal as human‑friendly confirmation. + +2. **Mostro protocol payload**: + - Constructs a Mostro‑core `Message`: + + ```35:42:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_dm.rs + let message = Message::new_dm( + None, + None, + Action::SendDm, + Some(Payload::TextMessage(message.to_string())), + ) + .as_json() + .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; + ``` + + - Semantics: + - `Action::SendDm`: high‑level Mostro action. + - `Payload::TextMessage(...)`: plain text content to deliver. + - `request_id` and some other fields are `None` here, since this is a simple DM. + - The result is a **JSON string** (Mostro protocol message) that will be used as the encrypted DM payload on Nostr. + +3. **Resolve trade keys for this order**: + + ```44:53:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_dm.rs + let trade_keys = + if let Ok(order_to_vote) = Order::get_by_id(&ctx.pool, &order_id.to_string()).await { + match order_to_vote.trade_keys.as_ref() { + Some(trade_keys) => Keys::parse(trade_keys)?, + None => { + anyhow::bail!("No trade_keys found for this order"); + } + } + } else { + return Err(anyhow::anyhow!("order {} not found", order_id)); + }; + ``` + + - Orders in the DB store a serialized `trade_keys` field (per‑order ephemeral keys). + - These keys are: + - Derived from the user’s mnemonic (see `src/db.rs` and NIP‑06 support). + - Used as the **Nostr keypair for this trade**. + - Used for DM encryption and as the sender identity on Nostr. + +4. **Delegate to `util::send_dm`**: + + ```56:64:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_dm.rs + send_dm( + &ctx.client, + Some(&trade_keys), + &trade_keys, + &receiver, + message, + None, + false, + ) + .await?; + ``` + + - `client`: connected Nostr client (relays from `RELAYS`). + - `identity_keys: Some(&trade_keys)`: used for signing when we choose the "signed gift wrap" mode. + - `trade_keys`: the per‑order trade keys used for DM encryption / gift wrap. + - `receiver`: target Nostr pubkey (user or service). + - `payload`: the serialized Mostro `Message` JSON built above. + - `expiration: None`: no extra NIP‑40 expiration tags. + - `to_user: false`: this controls which DM mode is used (see below). + +### 3. Low‑level DM construction (`util::send_dm`) + +File: `src/util/messaging.rs` + +```201:253:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/util/messaging.rs +pub async fn send_dm( + client: &Client, + identity_keys: Option<&Keys>, + trade_keys: &Keys, + receiver_pubkey: &PublicKey, + payload: String, + expiration: Option, + to_user: bool, +) -> Result<()> { + let pow: u8 = var("POW") + .unwrap_or('0'.to_string()) + .parse() + .map_err(|e| anyhow::anyhow!("Failed to parse POW: {}", e))?; + let private = var("SECRET") + .unwrap_or("false".to_string()) + .parse::() + .map_err(|e| anyhow::anyhow!("Failed to parse SECRET: {}", e))?; + + let message_type = determine_message_type(to_user, private); + + let event = match message_type { + MessageType::PrivateDirectMessage => { + create_private_dm_event(trade_keys, receiver_pubkey, payload, pow).await? + } + MessageType::PrivateGiftWrap => { + create_gift_wrap_event( + trade_keys, + identity_keys, + receiver_pubkey, + payload, + pow, + expiration, + false, + ) + .await? + } + MessageType::SignedGiftWrap => { + create_gift_wrap_event( + trade_keys, + identity_keys, + receiver_pubkey, + payload, + pow, + expiration, + true, + ) + .await? + } + }; + + client.send_event(&event).await?; + Ok(()) +} +``` + +Key points: + +- **POW**: + - `POW` env var (default `"0"`) controls proof‑of‑work difficulty for the outer Nostr event. +- **SECRET**: + - `SECRET` env var (`"true"/"false"`) controls whether messages are sent as private DMs vs gift wraps. + +- **Message type decision**: + + ```129:134:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/util/messaging.rs + fn determine_message_type(to_user: bool, private: bool) -> MessageType { + match (to_user, private) { + (true, _) => MessageType::PrivateDirectMessage, + (false, true) => MessageType::PrivateGiftWrap, + (false, false) => MessageType::SignedGiftWrap, + } + } + ``` + + For `senddm`: + - `to_user` is **`false`** + - `SECRET` defaults to **`false`** + - So we use **`MessageType::SignedGiftWrap`** + +#### 3.1 Signed gift wrap DM (default `senddm` mode) + +For `MessageType::SignedGiftWrap` we use `create_gift_wrap_event(..., signed = true)`: + +```162:199:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/util/messaging.rs +async fn create_gift_wrap_event( + trade_keys: &Keys, + identity_keys: Option<&Keys>, + receiver_pubkey: &PublicKey, + payload: String, + pow: u8, + expiration: Option, + signed: bool, +) -> Result { + let message = Message::from_json(&payload) + .map_err(|e| anyhow::anyhow!("Failed to deserialize message: {e}"))?; + + let content = if signed { + let _identity_keys = identity_keys + .ok_or_else(|| Error::msg("identity_keys required for signed messages"))?; + let sig = Message::sign(payload, trade_keys); + serde_json::to_string(&(message, sig)) + .map_err(|e| anyhow::anyhow!("Failed to serialize message: {e}"))? + } else { + let content: (Message, Option) = (message, None); + serde_json::to_string(&content) + .map_err(|e| anyhow::anyhow!("Failed to serialize message: {e}"))? + }; + + let rumor = EventBuilder::text_note(content) + .pow(pow) + .build(trade_keys.public_key()); + + let tags = create_expiration_tags(expiration); + + let signer_keys = if signed { + identity_keys.ok_or_else(|| Error::msg("identity_keys required for signed messages"))? + } else { + trade_keys + }; + + Ok(EventBuilder::gift_wrap(signer_keys, receiver_pubkey, rumor, tags).await?) +} +``` + +Protocol behaviour: + +- **Inner content**: + - Parses the Mostro `Message` from `payload`. + - If `signed = true`: + - Computes a Mostro‑level signature: `Message::sign(payload, trade_keys)`. + - Wraps `(message, sig)` into JSON. + - Builds a text‑note rumor event: + - `kind`: `TextNote` + - `content`: JSON `(Message, Signature)` or `(Message, None)`. + - `pubkey`: `trade_keys.public_key()` + - Optional POW as configured by `POW`. + +- **Outer NIP‑59 Gift Wrap**: + - `EventBuilder::gift_wrap(...)` wraps the rumor into a **GiftWrap** event (NIP‑59). + - `signer_keys`: + - For `senddm` default, `signed = true`, so `signer_keys = identity_keys`, passed as `Some(&trade_keys)`. + - This means the outer event is also signed by the **trade keys**. + - `receiver_pubkey`: the `receiver` passed to `execute_send_dm` (user/Mostro/other). + - `tags`: optional NIP‑40 expiration (unused here). + +- **Relaying**: + - The final event is sent via: + + ```251:252:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/util/messaging.rs + client.send_event(&event).await?; + ``` + + - `client` is a `nostr_sdk::Client` connected to all relays in `RELAYS`. + +### 4. Keys and protocols summary + +- **Keys**: + - `identity_keys` (i0): long‑term user identity (stored in DB). + - `trade_keys`: per‑order ephemeral keys used for: + - DM identity on Nostr. + - Signing Mostro messages (`Message::sign`). + - `receiver_pubkey`: DM target (user or service). + +- **Protocols**: + - **Mostro application protocol**: + - `Message::new_dm` + `Action::SendDm` + `Payload::TextMessage`. + - **Nostr**: + - NIP‑13 (optional POW). + - NIP‑40 (optional expiration tags, not used here). + - NIP‑59 Gift Wrap for encapsulating the Mostro message. + + From 3b4e585cc113fc75409cee0e58eeffa2c0d4c6cc Mon Sep 17 00:00:00 2001 From: arkanoider Date: Sun, 1 Mar 2026 22:49:41 +0100 Subject: [PATCH 4/9] feat: feat(getdmuser): show shared-key DMs (identity + admin) in getdmuser - Derive shared key from (trade_keys, identity_keys) and fetch/unwrap gift wraps so DMs sent to the user's identity appear in getdmuser. - Also derive (trade_keys, mostro_pubkey) and fetch so admin replies from the send_admin_dm_attach flow are shown. - Reuse derive_shared_key_bytes from util/messaging (same ECDH as send_admin_dm_attach). No longer use get_all_trade_and_counterparty_keys; counterparty_pubkey is not used in this setup. --- .cursor/commands/build.md | 13 +++ .cursor/commands/pull_request.md | 31 ++++++ .cursor/commands/update_docs.md | 12 ++ docs/architecture.md | 10 +- docs/commands.md | 4 +- docs/database.md | 2 + docs/overview.md | 2 +- docs/send_admin_dm_attach_flow.md | 128 ++++------------------ src/cli/add_invoice.rs | 2 +- src/cli/dm_to_user.rs | 21 +++- src/cli/get_dm.rs | 2 +- src/cli/get_dm_user.rs | 93 ++++++++++++++-- src/cli/last_trade_index.rs | 2 +- src/cli/list_orders.rs | 4 +- src/cli/new_order.rs | 4 +- src/cli/rate_user.rs | 4 +- src/cli/send_admin_dm_attach.rs | 42 ++----- src/cli/take_dispute.rs | 8 +- src/db.rs | 20 ++++ src/util/messaging.rs | 176 ++++++++++++++++++++++++++++++ src/util/mod.rs | 4 +- 21 files changed, 413 insertions(+), 171 deletions(-) create mode 100644 .cursor/commands/build.md create mode 100644 .cursor/commands/pull_request.md create mode 100644 .cursor/commands/update_docs.md diff --git a/.cursor/commands/build.md b/.cursor/commands/build.md new file mode 100644 index 0000000..ccadd46 --- /dev/null +++ b/.cursor/commands/build.md @@ -0,0 +1,13 @@ +# build mostro-cli using coding standards + + +## Overview + +Build and test mostro-cli, fixing all errors reported by cargo and clippy. + +## Steps + +- execute cargo fmt --all +- execute cargo clippy --all-targets --all-features +- execute cargo test +- execute cargo build diff --git a/.cursor/commands/pull_request.md b/.cursor/commands/pull_request.md new file mode 100644 index 0000000..6a0d2ae --- /dev/null +++ b/.cursor/commands/pull_request.md @@ -0,0 +1,31 @@ +# Create PR + +## Overview + +Create a well-structured pull request with proper description, labels, and reviewers. + +## Steps + +1. **Prepare branch** + - Ensure all changes are committed + - Push branch to remote + - Verify branch is up to date with main + +2. **Write PR description** + - Summarize changes clearly + - Include context and motivation + - List any breaking changes + - Add screenshots if UI changes + +3. **Set up PR** + - Create PR with descriptive title + - Add appropriate labels + - Assign reviewers + - Link related issues + +## PR Template + +- [ ] Feature A +- [ ] Bug fix B +- [ ] Unit tests pass +- [ ] Manual testing completed diff --git a/.cursor/commands/update_docs.md b/.cursor/commands/update_docs.md new file mode 100644 index 0000000..a42997b --- /dev/null +++ b/.cursor/commands/update_docs.md @@ -0,0 +1,12 @@ +# Update AI docs for automatic code generation with context + +## Overview + +Keep Markdown documents updated to provide AI context, improving the quality of generated code. + +## Steps + +- Identify the latest changes in Git history for files in the docs folder +- Analyze all new changes up to the latest commit +- Document new features, fixes, and refactorings in the docs +- Add contextual notes for structural changes (e.g., update DATABASE.md for DB schema changes) diff --git a/docs/architecture.md b/docs/architecture.md index 1a55cac..e9ac5f7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -31,12 +31,12 @@ This document describes the internal structure of `mostro-cli`, how major module - **`src/util/mod.rs`** - Organizes utility modules: - `events`: event filtering and retrieval from Nostr. - - `messaging`: higher-level DM helpers (gift-wrapped messages, admin keys, etc.). + - `messaging`: higher-level DM helpers (gift-wrapped messages, admin keys, **shared-key derivation and custom wraps**). - `misc`: small helpers such as `get_mcli_path` and string utilities. - `net`: Nostr network connection setup. - `storage`: thin storage helpers for orders and DMs. - `types`: small shared enums/wrappers. - - Re-exports commonly used symbols (`create_filter`, `send_dm`, `connect_nostr`, `save_order`, etc.) so other modules can import from `crate::util` directly. + - Re-exports commonly used symbols (`create_filter`, `send_dm`, `connect_nostr`, `save_order`, **`derive_shared_keys`, `derive_shared_key_hex`, `keys_from_shared_hex`, `send_admin_chat_message_via_shared_key`**, etc.) so other modules can import from `crate::util` directly. - **`src/util/storage.rs`** - `save_order(order, trade_keys, request_id, trade_index, pool)`: @@ -68,7 +68,7 @@ This document describes the internal structure of `mostro-cli`, how major module - Handles creation (`User::new`), loading (`User::get`), updating (`save`), and key derivation helpers (identity keys and per-trade keys using `nip06`). - `Order`: - Represents cached orders with fields mapped to the `orders` table. - - Provides `new`, `insert_db`, `update_db`, fluent setters, `save`, `save_new_id`, `get_by_id`, `get_all_trade_keys`, and `delete_by_id`. + - Provides `new`, `insert_db`, `update_db`, fluent setters, `save`, `save_new_id`, `get_by_id`, `get_all_trade_keys`, **`get_all_trade_and_counterparty_keys`** (distinct `(trade_keys, counterparty_pubkey)` pairs for orders where both are set), and `delete_by_id`. - See `database.md` for schema details. ### Parsers and protocol types @@ -82,6 +82,10 @@ This document describes the internal structure of `mostro-cli`, how major module - `common.rs`: shared parsing helpers. - `mod.rs`: module glue. +- **Shared-key custom wraps** (`src/util/messaging.rs`): + - **Sending**: `derive_shared_keys(local_keys, counterparty_pubkey)` yields a `Keys` whose public key is used as the NIP-59 gift-wrap recipient; inner content is a signed text note encrypted with NIP-44 to that pubkey. Used by `dmtouser` and `sendadmindmattach`. + - **Receiving**: `unwrap_giftwrap_with_shared_key(shared_keys, event)` decrypts with NIP-44 and returns `(content, timestamp, sender_pubkey)`; `fetch_gift_wraps_for_shared_key(client, shared_keys)` fetches Kind::GiftWrap events with `#p` = shared key pubkey and unwraps them. Use when implementing flows that read shared-key DMs. + ### Lightning integration - **`src/lightning/mod.rs`** diff --git a/docs/commands.md b/docs/commands.md index d811f5b..9885a5f 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -110,10 +110,10 @@ All commands are part of the `Commands` enum and are dispatched via `Commands::r - **Handler**: `execute_send_dm(PublicKey::from_str(pubkey)?, ctx, order_id, &msg)` in `src/cli/send_dm.rs`. - **`dmtouser`** - - **Description**: Send a gift-wrapped direct message to a user. + - **Description**: Send a direct message to a user via a **shared-key custom wrap**. Derives an ECDH shared key from the order’s trade keys and the recipient pubkey; the message is sent as a NIP-59 gift wrap addressed to the shared key’s public key (NIP-44 encrypted), so both sides can decrypt. - **Args**: - `--pubkey `: Recipient pubkey. - - `--order-id `: Order id to derive ephemeral keys. + - `--order-id `: Order id to derive trade keys and shared key. - `--message ...`: Message parts; joined with spaces. - **Handler**: `execute_dm_to_user(PublicKey::from_str(pubkey)?, &ctx.client, order_id, &msg, &ctx.pool)` in `src/cli/dm_to_user.rs`. diff --git a/docs/database.md b/docs/database.md index ee9d243..a75bc2e 100644 --- a/docs/database.md +++ b/docs/database.md @@ -77,6 +77,8 @@ This file is created under the CLI data directory returned by `util::get_mcli_pa - Loads a single order (and returns an error if no ID is present). - `get_all_trade_keys(pool)`: - Returns distinct non-null `trade_keys` for all orders. + - `get_all_trade_and_counterparty_keys(pool)`: + - Returns distinct `(trade_keys, counterparty_pubkey)` pairs for orders where both columns are non-null; useful for deriving per-order shared keys when fetching or sending shared-key DMs. - `delete_by_id(pool, id)`: - Deletes an order row. diff --git a/docs/overview.md b/docs/overview.md index d27fbc2..404a1fc 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -7,7 +7,7 @@ The CLI is heavily inspired by the Mostro backend documentation (`mostro/docs` i ### High-level responsibilities - **Order lifecycle**: create, take, cancel, dispute, and settle orders using `mostro_core` types and the Mostro protocol. -- **Direct messaging**: send and receive Nostr DMs between users, admins, and solvers, including gift-wrapped messages and encrypted attachments. +- **Direct messaging**: send and receive Nostr DMs between users, admins, and solvers, including gift-wrapped messages, shared-key custom wraps (ECDH-derived key, NIP-44 inside NIP-59) for `dmtouser` and admin attachment DMs, and encrypted attachments. - **Local persistence**: keep a local cache of orders and a deterministic identity in a SQLite database (`mcli.db`) under the CLI data directory. - **Admin / solver tooling**: expose admin-only and solver-only flows (e.g. taking disputes, adding solvers, admin DMs) when run with the proper keys. diff --git a/docs/send_admin_dm_attach_flow.md b/docs/send_admin_dm_attach_flow.md index c46bee0..7ff885a 100644 --- a/docs/send_admin_dm_attach_flow.md +++ b/docs/send_admin_dm_attach_flow.md @@ -320,115 +320,30 @@ Blossom protocol details: The function ultimately returns a **public URL** (`blossom_url`) pointing to the encrypted blob. -### 5. DM payload and gift wrap to admin - -Once the encrypted blob is uploaded and we have `blossom_url`, the DM payload is built: - -```268:311:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs -let filename = file_path - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("attachment.bin") - .to_string(); - -// Best-effort MIME type detection based on the file extension. -let mime_type = file_path - .extension() - .and_then(|ext| ext.to_str()) - .map(|ext| ext.to_ascii_lowercase()) - .map(|ext| match ext.as_str() { - "txt" => "text/plain", - "md" => "text/markdown", - "json" => "application/json", - "csv" => "text/csv", - "jpg" | "jpeg" => "image/jpeg", - "png" => "image/png", - "gif" => "image/gif", - "webp" => "image/webp", - "pdf" => "application/pdf", - "zip" => "application/zip", - "tar" => "application/x-tar", - "gz" | "tgz" => "application/gzip", - "mp3" => "audio/mpeg", - "mp4" => "video/mp4", - "mov" => "video/quicktime", - _ => "application/octet-stream", - }) - .unwrap_or("application/octet-stream") - .to_string(); - -let payload_json = serde_json::json!({ - "type": "file_encrypted", - "blossom_url": blossom_url, - "nonce": nonce_hex, - "mime_type": mime_type, - "original_size": file_bytes.len(), - "filename": filename, - "encrypted_size": encrypted_size, - "file_type": "document", -}); - -let content = serde_json::to_string(&payload_json) - .map_err(|e| anyhow::anyhow!("failed to serialize attachment payload: {e}"))?; -``` - -This JSON is the **Mostro DM payload** describing the encrypted attachment: - -- `type = "file_encrypted"` – payload kind. -- `blossom_url` – where the admin can fetch the encrypted blob. -- `nonce` – hex nonce for ChaCha20‑Poly1305. -- `mime_type` – hint about original file type. -- `original_size` / `encrypted_size` – size bookkeeping. -- `filename` – original filename. - -#### 5.1 Gift‑wrapped DM event - -```316:331:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_admin_dm_attach.rs -let pow: u8 = std::env::var("POW") - .unwrap_or_else(|_| "0".to_string()) - .parse() - .unwrap_or(0); - -let rumor = EventBuilder::text_note(content) - .pow(pow) - .build(trade_keys.public_key()); - -let event = EventBuilder::gift_wrap(&trade_keys, &receiver, rumor, Tags::new()).await?; - -ctx.client - .send_event(&event) - .await - .map_err(|e| anyhow::anyhow!("failed to send gift wrap event: {e}"))?; - -println!("✅ Encrypted attachment sent successfully to admin!"); -``` +### 5. DM payload and shared-key custom wrap to admin -- **Rumor event** (inner): - - `kind`: `TextNote` - - `content`: the `payload_json` string above. - - `pubkey`: `trade_keys.public_key()` (per‑order trade identity). - - Optional POW from `POW` env var. +Once the encrypted blob is uploaded and we have `blossom_url`, the DM payload is built as JSON (`type`, `blossom_url`, `nonce`, `mime_type`, sizes, `filename`). This content is then sent using **shared-key custom wrap** (same pattern as `dmtouser`): -- **Outer GiftWrap event** (NIP‑59): - - Created via `EventBuilder::gift_wrap(&trade_keys, &receiver, rumor, Tags::new())`. - - **Signer / sender**: `trade_keys` (per‑order). - - **Recipient**: `receiver` (admin / solver Nostr pubkey). - - **Tags**: currently empty (no extra expiration here; optional). +- **Shared key**: The same ECDH shared key used for file encryption (trade keys + admin pubkey) is turned into a `Keys` via `Keys::new(SecretKey::from_slice(&shared_key)?)`. +- **Send**: `send_admin_chat_message_via_shared_key(&ctx.client, &trade_keys, &shared_keys, &content)` in `src/util/messaging.rs`: + - Builds an inner text-note event (sender = trade keys), signs it, encrypts it with NIP-44 to the **shared key’s public key**, and wraps it in a NIP-59 GiftWrap event tagged with that pubkey (`#p`). + - Both the sender (trade keys) and the admin (who can derive the same shared key) can later fetch and decrypt the event by filtering GiftWrap by the shared key pubkey and using `unwrap_giftwrap_with_shared_key`. -- **Relaying**: - - Final event is sent with `ctx.client.send_event(&event).await`. - - `ctx.client` is connected to configured Nostr relays via `RELAYS` env var. +So the attachment metadata is not sent as a plain NIP-59 gift wrap to the admin pubkey; it is sent to the **shared key’s public key**, enabling symmetric decryption for both parties. Relaying is via `ctx.client.send_event(&event)`. ### 6. Keys and protocols summary - **Keys**: - `trade_keys` (per‑order): - Used for: - - ECDH shared secret with admin pubkey for file encryption. - - Nostr DM identity for the rumor + giftwrap. - - (Indirectly) for signing Blossom auth event (kind 24242). + - ECDH shared secret with admin pubkey (for file encryption and for the shared-key DM). + - Nostr identity for the inner text note and for signing the outer NIP‑59 wrap. + - Signing the Blossom auth event (kind 24242). + - **Shared key** (ECDH from trade_keys + admin pubkey): + - Same 32-byte secret used for ChaCha20‑Poly1305 file encryption. + - Wrapped as `Keys` and used as the **recipient** of the DM: the NIP-59 GiftWrap is addressed to the shared key’s public key, and the inner content is NIP-44 encrypted to it, so both sender and admin can derive the key and decrypt. - `receiver`: - - Admin / solver Nostr pubkey; DM destination for the final NIP‑59 GiftWrap. + - Admin / solver Nostr pubkey; used to derive the shared key and as the human-facing destination (the actual Nostr event recipient is the shared key pubkey). - **Protocols**: - **ChaCha20‑Poly1305**: @@ -441,16 +356,15 @@ println!("✅ Encrypted attachment sent successfully to admin!"); - JSON with attachment metadata: - `type`, `blossom_url`, `nonce`, `mime_type`, sizes, `filename`. - **Nostr**: - - NIP‑13: optional POW on the rumor text note. - - NIP‑40: optional expiration (not used on the DM here). - - NIP‑59: GiftWrap envelope: - - Wraps the text‑note rumor for the admin’s pubkey. + - NIP‑13: optional POW on the inner text note. + - NIP‑44: encryption of the inner event to the shared key’s public key. + - NIP‑59: GiftWrap envelope addressed to the **shared key pubkey** (not directly to the admin), so both parties that know the ECDH secret can fetch and decrypt. End‑to‑end, `sendadmindmattach`: -1. Derives a shared ECDH key between trade and admin. -2. Encrypts the file with ChaCha20‑Poly1305. +1. Derives a shared ECDH key between trade keys and admin pubkey. +2. Encrypts the file with ChaCha20‑Poly1305 using that key. 3. Authenticates to Blossom with a kind‑24242 auth event (BUD‑01) and uploads the encrypted blob (BUD‑02). -4. Builds a Mostro DM payload with Blossom URL + crypto metadata. -5. Sends a NIP‑59 gift‑wrapped text note from the trade keys to the admin pubkey with that payload as content. +4. Builds a Mostro DM payload JSON with Blossom URL and crypto metadata. +5. Sends a **shared-key custom wrap** (NIP-44 inner content, NIP-59 GiftWrap addressed to the shared key’s public key) via `send_admin_chat_message_via_shared_key`, so both the sender and the admin can decrypt the attachment metadata and fetch the blob. diff --git a/src/cli/add_invoice.rs b/src/cli/add_invoice.rs index e577eea..76dc784 100644 --- a/src/cli/add_invoice.rs +++ b/src/cli/add_invoice.rs @@ -43,7 +43,7 @@ pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context) )); println!("{table}"); println!("💡 Sending lightning invoice to Mostro...\n"); - // Check invoice string + // Parse invoice (Lightning address or BOLT11) and build payload let ln_addr = LightningAddress::from_str(invoice); let payload = if ln_addr.is_ok() { Some(Payload::PaymentRequest(None, invoice.to_string(), None)) diff --git a/src/cli/dm_to_user.rs b/src/cli/dm_to_user.rs index 33dcd51..660dcb2 100644 --- a/src/cli/dm_to_user.rs +++ b/src/cli/dm_to_user.rs @@ -1,7 +1,10 @@ use crate::parser::common::{ print_info_line, print_key_value, print_section_header, print_success_message, }; -use crate::{db::Order, util::send_gift_wrap_dm}; +use crate::{ + db::Order, + util::{derive_shared_keys, send_admin_chat_message_via_shared_key}, +}; use anyhow::Result; use nostr_sdk::prelude::*; use sqlx::SqlitePool; @@ -24,16 +27,26 @@ pub async fn execute_dm_to_user( None => anyhow::bail!("No trade_keys found for this order"), }; - // Send the DM + // Derive per-dispute shared keys between our trade keys and the receiver pubkey + let shared_keys = derive_shared_keys(Some(&trade_keys), Some(&receiver)) + .ok_or_else(|| anyhow::anyhow!("Failed to derive shared key for this DM"))?; + + // Print summary and send shared-key wrap DM print_section_header("💬 Direct Message to User"); print_key_value("📋", "Order ID", &order_id.to_string()); print_key_value("🔑", "Trade Keys", &trade_keys.public_key().to_hex()); print_key_value("🎯", "Recipient", &receiver.to_string()); print_key_value("💬", "Message", message); - print_info_line("💡", "Sending gift wrap message..."); + print_key_value( + "🔑", + "Shared Key Pubkey", + &shared_keys.public_key().to_hex(), + ); + print_info_line("💡", "Sending shared-key custom wrap message..."); println!(); - send_gift_wrap_dm(client, &trade_keys, &receiver, message).await?; + // Send as shared-key custom wrap so both parties can decrypt via the shared key + send_admin_chat_message_via_shared_key(client, &trade_keys, &shared_keys, message).await?; print_success_message("Gift wrap message sent successfully!"); diff --git a/src/cli/get_dm.rs b/src/cli/get_dm.rs index d232350..9860139 100644 --- a/src/cli/get_dm.rs +++ b/src/cli/get_dm.rs @@ -22,7 +22,7 @@ pub async fn execute_get_dm( print_key_value("💡", "Action", "Fetching direct messages..."); println!(); - // Get the list kind + // Determine DM list to fetch (admin/user and from-user flag) let list_kind = match (admin, from_user) { (true, true) => ListKind::PrivateDirectMessagesUser, (true, false) => ListKind::DirectMessagesAdmin, diff --git a/src/cli/get_dm_user.rs b/src/cli/get_dm_user.rs index c0bb1b2..e644fcb 100644 --- a/src/cli/get_dm_user.rs +++ b/src/cli/get_dm_user.rs @@ -4,6 +4,7 @@ use crate::parser::common::{ print_info_line, print_key_value, print_no_data_message, print_section_header, }; use crate::parser::dms::print_direct_messages; +use crate::util::messaging::{derive_shared_key_bytes, fetch_gift_wraps_for_shared_key}; use crate::util::{fetch_events_list, Event, ListKind}; use anyhow::Result; use mostro_core::prelude::*; @@ -13,7 +14,7 @@ pub async fn execute_get_dm_user(since: &i64, ctx: &Context) -> Result<()> { // Get all trade keys from orders let mut trade_keys_hex = Order::get_all_trade_keys(&ctx.pool).await?; - // Include admin pubkey so we also fetch messages sent TO admin + // Include Mostro pubkey so we also fetch messages addressed to Mostro let admin_pubkey_hex = ctx.mostro_pubkey.to_hex(); if !trade_keys_hex.iter().any(|k| k == &admin_pubkey_hex) { trade_keys_hex.push(admin_pubkey_hex); @@ -48,20 +49,96 @@ pub async fn execute_get_dm_user(since: &i64, ctx: &Context) -> Result<()> { ) .await?; - // Extract (Message, u64) tuples from Event::MessageTuple variants + // Extract (Message, u64, PublicKey) tuples from Event::MessageTuple variants (classic DMs) let mut dm_events: Vec<(Message, u64, PublicKey)> = Vec::new(); - // Check if the direct messages are empty - if direct_messages.is_empty() { - print_no_data_message("You don't have any direct messages in your trade keys"); - return Ok(()); - } - // Extract the direct messages for event in direct_messages { if let Event::MessageTuple(tuple) = event { dm_events.push(*tuple); } } + // Also fetch and decrypt shared-key custom wraps (shared key = trade_keys + identity_keys) + let trade_keys_hex_list = Order::get_all_trade_keys(&ctx.pool).await?; + let identity_pubkey = ctx.identity_keys.public_key(); + for trade_hex in trade_keys_hex_list { + let trade_keys = match Keys::parse(&trade_hex) { + Ok(k) => k, + Err(e) => { + log::warn!("get_dm_user: could not parse trade_keys: {}", e); + continue; + } + }; + let shared_key = match derive_shared_key_bytes(&trade_keys, &identity_pubkey) { + Ok(b) => b, + Err(e) => { + log::warn!( + "get_dm_user: could not derive shared key (trade + identity): {}", + e + ); + continue; + } + }; + let shared_keys = match SecretKey::from_slice(&shared_key) { + Ok(sk) => Keys::new(sk), + Err(e) => { + log::warn!("get_dm_user: could not build Keys from shared key: {}", e); + continue; + } + }; + let shared_msgs = match fetch_gift_wraps_for_shared_key(&ctx.client, &shared_keys).await { + Ok(m) => m, + Err(e) => { + log::warn!( + "get_dm_user: failed to fetch gift wraps for shared key: {}", + e + ); + continue; + } + }; + for (content, ts, sender_pubkey) in shared_msgs { + let parsed: (Message, Option) = match serde_json::from_str(&content) { + Ok(m) => m, + Err(e) => { + log::warn!("get_dm_user: could not parse shared-key DM content: {}", e); + continue; + } + }; + dm_events.push((parsed.0, ts as u64, sender_pubkey)); + } + + // Also fetch shared-key wraps for (trade_keys + mostro_pubkey) so we see admin replies + // (send_admin_dm_attach uses that derivation when we send to admin; admin uses same key to reply) + let shared_key_admin = match derive_shared_key_bytes(&trade_keys, &ctx.mostro_pubkey) { + Ok(b) => b, + Err(e) => { + log::warn!( + "get_dm_user: could not derive shared key (trade + mostro): {}", + e + ); + continue; + } + }; + let shared_keys_admin = match SecretKey::from_slice(&shared_key_admin) { + Ok(sk) => Keys::new(sk), + Err(e) => { + log::warn!("get_dm_user: could not build Keys from shared key (admin): {}", e); + continue; + } + }; + if let Ok(admin_msgs) = fetch_gift_wraps_for_shared_key(&ctx.client, &shared_keys_admin).await { + for (content, ts, sender_pubkey) in admin_msgs { + if let Ok((parsed, _)) = serde_json::from_str::<(Message, Option)>(&content) { + dm_events.push((parsed, ts as u64, sender_pubkey)); + } + } + } + } + + if dm_events.is_empty() { + print_no_data_message("You don't have any direct messages in your trade keys"); + return Ok(()); + } + print_direct_messages(&dm_events, Some(ctx.mostro_pubkey)).await?; Ok(()) } diff --git a/src/cli/last_trade_index.rs b/src/cli/last_trade_index.rs index 2e0b523..d69fd96 100644 --- a/src/cli/last_trade_index.rs +++ b/src/cli/last_trade_index.rs @@ -34,7 +34,7 @@ pub async fn execute_last_trade_index( false, ); - // Log the sent message + // Print request summary print_section_header("🔢 Last Trade Index Request"); print_key_value("👤", "User", &identity_keys.public_key().to_string()); print_key_value("🎯", "Target", &mostro_key.to_string()); diff --git a/src/cli/list_orders.rs b/src/cli/list_orders.rs index 9b446ec..b690525 100644 --- a/src/cli/list_orders.rs +++ b/src/cli/list_orders.rs @@ -20,7 +20,7 @@ pub async fn execute_list_orders( // Default kind is none let mut kind_checked: Option = None; - // New check against strings + // Parse and validate status from CLI string if let Some(s) = status { status_checked = Some( Status::from_str(s) @@ -34,7 +34,7 @@ pub async fn execute_list_orders( if let Some(status) = &status_checked { print_key_value("📊", "Status Filter", &format!("{:?}", status)); } - // New check against strings for kind + // Parse and validate kind from CLI string if let Some(k) = kind { kind_checked = Some( mostro_core::order::Kind::from_str(k) diff --git a/src/cli/new_order.rs b/src/cli/new_order.rs index 9ef934a..5f32cf7 100644 --- a/src/cli/new_order.rs +++ b/src/cli/new_order.rs @@ -57,7 +57,7 @@ pub async fn execute_new_order( }; // Get the type of neworder - // if both tuple field are valid than it's a range order + // If both tuple fields are set, treat as a range order // otherwise use just fiat amount value as before let amt = if fiat_amount.1.is_some() { (0, Some(fiat_amount.0), fiat_amount.1) @@ -118,7 +118,7 @@ pub async fn execute_new_order( Some(order_content), ); - // Send dm to receiver pubkey + // Print summary and send DM to Mostro println!("🆕 Create New Order"); println!("═══════════════════════════════════════"); diff --git a/src/cli/rate_user.rs b/src/cli/rate_user.rs index 4418303..deb00b6 100644 --- a/src/cli/rate_user.rs +++ b/src/cli/rate_user.rs @@ -12,7 +12,7 @@ use crate::{ util::{print_dm_events, send_dm, wait_for_dm}, }; -// Get the user rate +/// Build rating payload for the given score and order id. fn get_user_rate(rating: &u8, order_id: &Uuid) -> Result { if let Some(rating) = RATING_BOUNDARIES.iter().find(|r| r == &rating) { print_section_header("⭐ Rate User"); @@ -72,7 +72,7 @@ pub async fn execute_rate_user(order_id: &Uuid, rating: &u8, ctx: &Context) -> R let recv_event = wait_for_dm(ctx, Some(&trade_keys), sent_message).await?; // Parse the incoming DM - // use a fake request id + // Use a fake request id let fake_request_id = Uuid::new_v4().as_u128() as u64; print_dm_events(recv_event, fake_request_id, ctx, Some(&trade_keys)).await?; diff --git a/src/cli/send_admin_dm_attach.rs b/src/cli/send_admin_dm_attach.rs index e4dba00..c3d8f5c 100644 --- a/src/cli/send_admin_dm_attach.rs +++ b/src/cli/send_admin_dm_attach.rs @@ -18,6 +18,8 @@ use crate::db::Order; use crate::parser::common::{ create_emoji_field_row, create_field_value_header, create_standard_table, }; +use crate::util::messaging::derive_shared_key_bytes; +use crate::util::send_admin_chat_message_via_shared_key; const MAX_FILE_SIZE_BYTES: u64 = 25 * 1024 * 1024; @@ -33,22 +35,6 @@ const BLOSSOM_SERVERS: &[&str] = &[ "https://blossom.poster.place", ]; -fn derive_shared_key(trade_keys: &Keys, admin_pubkey: &PublicKey) -> [u8; 32] { - use bitcoin::secp256k1::ecdh::shared_secret_point; - use bitcoin::secp256k1::{Parity, PublicKey as SecpPublicKey}; - - let sk = trade_keys.secret_key(); - let xonly = admin_pubkey - .xonly() - .expect("failed to get x-only public key for admin"); - let secp_pk = SecpPublicKey::from_x_only_public_key(xonly, Parity::Even); - let mut point_bytes = shared_secret_point(&secp_pk, sk).as_slice().to_vec(); - point_bytes.resize(32, 0); - point_bytes - .try_into() - .expect("shared secret point must be at least 32 bytes") -} - fn encrypt_blob(shared_key: [u8; 32], plaintext: &[u8]) -> Result<(Vec, String)> { let key = Key::from_slice(&shared_key); let cipher = ChaCha20Poly1305::new(key); @@ -260,7 +246,7 @@ pub async fn execute_send_admin_dm_attach( println!("{table}"); println!("💡 Encrypting file and uploading to Blossom...\n"); - let shared_key = derive_shared_key(&trade_keys, &receiver); + let shared_key = derive_shared_key_bytes(&trade_keys, &receiver)?; let (encrypted_blob, nonce_hex) = encrypt_blob(shared_key, &file_bytes)?; let encrypted_size = encrypted_blob.len(); let blossom_url = upload_to_blossom(&trade_keys, encrypted_blob).await?; @@ -313,21 +299,13 @@ pub async fn execute_send_admin_dm_attach( let content = serde_json::to_string(&payload_json) .map_err(|e| anyhow::anyhow!("failed to serialize attachment payload: {e}"))?; - let pow: u8 = std::env::var("POW") - .unwrap_or_else(|_| "0".to_string()) - .parse() - .unwrap_or(0); - - let rumor = EventBuilder::text_note(content) - .pow(pow) - .build(trade_keys.public_key()); - - let event = EventBuilder::gift_wrap(&trade_keys, &receiver, rumor, Tags::new()).await?; - - ctx.client - .send_event(&event) - .await - .map_err(|e| anyhow::anyhow!("failed to send gift wrap event: {e}"))?; + send_admin_chat_message_via_shared_key( + &ctx.client, + &trade_keys, + &Keys::new(SecretKey::from_slice(&shared_key)?), + &content, + ) + .await?; println!("✅ Encrypted attachment sent successfully to admin!"); diff --git a/src/cli/take_dispute.rs b/src/cli/take_dispute.rs index 22193e2..5600286 100644 --- a/src/cli/take_dispute.rs +++ b/src/cli/take_dispute.rs @@ -26,7 +26,7 @@ pub async fn execute_admin_add_solver(npubkey: &str, ctx: &Context) -> Result<() let _admin_keys = get_admin_keys(ctx)?; - // Create takebuy message + // Build admin dispute message let take_dispute_message = Message::new_dispute( Some(Uuid::new_v4()), None, @@ -64,7 +64,7 @@ pub async fn execute_admin_cancel_dispute(dispute_id: &Uuid, ctx: &Context) -> R let _admin_keys = get_admin_keys(ctx)?; - // Create takebuy message + // Build admin dispute message let take_dispute_message = Message::new_dispute(Some(*dispute_id), None, None, Action::AdminCancel, None) .as_json() @@ -97,7 +97,7 @@ pub async fn execute_admin_settle_dispute(dispute_id: &Uuid, ctx: &Context) -> R let _admin_keys = get_admin_keys(ctx)?; - // Create takebuy message + // Build admin dispute message let take_dispute_message = Message::new_dispute(Some(*dispute_id), None, None, Action::AdminSettle, None) .as_json() @@ -128,7 +128,7 @@ pub async fn execute_take_dispute(dispute_id: &Uuid, ctx: &Context) -> Result<() let admin_keys = get_admin_keys(ctx)?; - // Create takebuy message + // Build admin dispute message let take_dispute_message = Message::new_dispute( Some(*dispute_id), None, diff --git a/src/db.rs b/src/db.rs index 1bdcc0e..0305d9b 100644 --- a/src/db.rs +++ b/src/db.rs @@ -542,6 +542,26 @@ impl Order { Ok(trade_keys) } + /// Return distinct pairs of (trade_keys, counterparty_pubkey) for orders where both are set. + pub async fn get_all_trade_and_counterparty_keys( + pool: &SqlitePool, + ) -> Result> { + let rows: Vec<(Option, Option)> = sqlx::query_as( + "SELECT DISTINCT trade_keys, counterparty_pubkey FROM orders \ + WHERE trade_keys IS NOT NULL AND counterparty_pubkey IS NOT NULL", + ) + .fetch_all(pool) + .await?; + + Ok(rows + .into_iter() + .filter_map(|(tk, cp)| match (tk, cp) { + (Some(tk), Some(cp)) => Some((tk, cp)), + _ => None, + }) + .collect()) + } + pub async fn delete_by_id(pool: &SqlitePool, id: &str) -> Result { let rows_affected = sqlx::query( r#" diff --git a/src/util/messaging.rs b/src/util/messaging.rs index f8aaca1..d08f0f4 100644 --- a/src/util/messaging.rs +++ b/src/util/messaging.rs @@ -10,6 +10,7 @@ use std::env::var; use crate::cli::Context; use crate::parser::dms::print_commands_results; use crate::parser::parse_dm_events; +use crate::util::events::FETCH_EVENTS_TIMEOUT; use crate::util::types::MessageType; /// Helper function to retrieve and validate admin keys from context @@ -26,6 +27,181 @@ pub fn get_admin_keys(ctx: &Context) -> Result<&Keys> { Ok(admin_keys) } +/// Derive shared ECDH keys from a local keypair and a counterparty public key. +pub fn derive_shared_keys( + admin_keys: Option<&Keys>, + counterparty_pubkey: Option<&PublicKey>, +) -> Option { + let admin = admin_keys?; + let cp_pk = counterparty_pubkey?; + let shared_bytes = nostr_sdk::util::generate_shared_key(admin.secret_key(), cp_pk).ok()?; + let sk = nostr_sdk::SecretKey::from_slice(&shared_bytes).ok()?; + Some(Keys::new(sk)) +} + +/// Convenience wrapper: derive a shared key and return its secret as a hex string. +pub fn derive_shared_key_hex( + admin_keys: Option<&Keys>, + counterparty_pubkey_str: Option<&str>, +) -> Option { + let cp_pk = counterparty_pubkey_str.and_then(|s| PublicKey::parse(s).ok()); + let keys = derive_shared_keys(admin_keys, cp_pk.as_ref())?; + Some(keys.secret_key().to_secret_hex()) +} + +/// Rebuild a `Keys` from a stored shared-key hex string. +pub fn keys_from_shared_hex(hex: &str) -> Option { + nostr_sdk::Keys::parse(hex).ok() +} + +/// Derive shared secret bytes (ECDH) using the same algorithm as send_admin_dm_attach. +/// Used so the receive path can decrypt DMs sent via that flow. Returns 32 bytes suitable +/// for ChaCha20-Poly1305 or for building Keys via Keys::new(SecretKey::from_slice(&bytes)). +pub fn derive_shared_key_bytes(local_keys: &Keys, other_pubkey: &PublicKey) -> Result<[u8; 32]> { + use bitcoin::secp256k1::ecdh::shared_secret_point; + use bitcoin::secp256k1::{Parity, PublicKey as SecpPublicKey}; + + let sk = local_keys.secret_key(); + let xonly = other_pubkey + .xonly() + .map_err(|_| anyhow::anyhow!("failed to get x-only public key"))?; + let secp_pk = SecpPublicKey::from_x_only_public_key(xonly, Parity::Even); + let mut point_bytes = shared_secret_point(&secp_pk, sk).as_slice().to_vec(); + point_bytes.resize(32, 0); + point_bytes + .try_into() + .map_err(|_| anyhow::anyhow!("shared secret point must be at least 32 bytes")) +} + +/// Build a NIP-59 gift wrap event to a recipient pubkey (e.g. shared key pubkey). +/// Rumor content is Mostro protocol format: JSON of (Message, Option). +async fn build_custom_wrap_event( + sender: &Keys, + recipient_pubkey: &PublicKey, + message: &str, +) -> Result { + let inner_message = EventBuilder::text_note(message) + .build(sender.public_key()) + .sign(sender) + .await?; + + // Ephemeral key for the custom wrap + let ephem_key = Keys::generate(); + + // Encrypt the inner message with the ephemeral key using NIP-44 + let encrypted_content = nip44::encrypt( + ephem_key.secret_key(), + recipient_pubkey, + inner_message.as_json(), + nip44::Version::V2, + )?; + + // Build tags for the wrapper event, the recipient pubkey is the shared key pubkey + let tag = Tag::public_key(*recipient_pubkey); + + // Reuse POW behaviour from existing DM helpers + let pow: u8 = var("POW") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap_or(0); + + // Build the wrapped event + let wrapped_event = EventBuilder::new(nostr_sdk::Kind::GiftWrap, encrypted_content) + .tag(tag) + .custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK)) + .pow(pow) + .sign_with_keys(&ephem_key)?; + + Ok(wrapped_event) +} + +/// Send a chat message via a per-dispute shared key (ECDH-derived). +/// The gift wrap is addressed to the shared key's public key so both parties +/// (who derive the same shared key) can fetch and decrypt the event. +pub async fn send_admin_chat_message_via_shared_key( + client: &Client, + admin_keys: &Keys, + shared_keys: &Keys, + content: &str, +) -> Result<()> { + let content = content.trim(); + if content.is_empty() { + return Err(anyhow::anyhow!("Cannot send empty chat message")); + } + let recipient_pubkey = shared_keys.public_key(); + let event = build_custom_wrap_event(admin_keys, &recipient_pubkey, content).await?; + client.send_event(&event).await?; + Ok(()) +} + +/// Unwrap a custom Mostro P2P giftwrap addressed to a shared key. +/// Decrypts with the shared key using NIP-44 and returns (content, timestamp, sender_pubkey). +pub async fn unwrap_giftwrap_with_shared_key( + shared_keys: &Keys, + event: &Event, +) -> Result<(String, i64, PublicKey)> { + let decrypted = nip44::decrypt(shared_keys.secret_key(), &event.pubkey, &event.content) + .map_err(|e| anyhow::anyhow!("Failed to decrypt gift wrap with shared key: {e}"))?; + + let inner_event = Event::from_json(&decrypted) + .map_err(|e| anyhow::anyhow!("Invalid inner chat event: {e}"))?; + + inner_event + .verify() + .map_err(|e| anyhow::anyhow!("Invalid inner chat event signature: {e}"))?; + + Ok(( + inner_event.content, + inner_event.created_at.as_u64() as i64, + inner_event.pubkey, + )) +} + +/// Fetch gift wrap events addressed to a specific shared key's public key, +/// decrypt each with the shared key, and return (content, timestamp, sender_pubkey). +pub async fn fetch_gift_wraps_for_shared_key( + client: &Client, + shared_keys: &Keys, +) -> Result> { + let now = Timestamp::now().as_u64(); + let seven_days_secs: u64 = 7 * 24 * 60 * 60; + let wide_since = now.saturating_sub(seven_days_secs); + + let shared_pubkey = shared_keys.public_key(); + let filter = Filter::new() + .kind(nostr_sdk::Kind::GiftWrap) + .pubkey(shared_pubkey) + .since(Timestamp::from(wide_since)) + .limit(100); + + let events = client + .fetch_events(filter, FETCH_EVENTS_TIMEOUT) + .await + .map_err(|e| anyhow::anyhow!("Failed to fetch chat events for shared key: {e}"))?; + + let mut messages = Vec::new(); + for wrapped in events.iter() { + let to_shared = wrapped.tags.public_keys().any(|pk| *pk == shared_pubkey); + if !to_shared { + continue; + } + match unwrap_giftwrap_with_shared_key(shared_keys, wrapped).await { + Ok((content, ts, sender_pubkey)) => { + messages.push((content, ts, sender_pubkey)); + } + Err(e) => { + log::warn!( + "Failed to unwrap gift wrap for shared key {}: {}", + wrapped.id, + e + ); + } + } + } + messages.sort_by_key(|(_, ts, _)| *ts); + Ok(messages) +} + pub async fn send_admin_gift_wrap_dm( client: &Client, admin_keys: &Keys, diff --git a/src/util/mod.rs b/src/util/mod.rs index 70ec064..3d78765 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -8,7 +8,9 @@ pub mod types; // Re-export commonly used items to preserve existing import paths pub use events::{create_filter, fetch_events_list, FETCH_EVENTS_TIMEOUT}; pub use messaging::{ - print_dm_events, send_admin_gift_wrap_dm, send_dm, send_gift_wrap_dm, wait_for_dm, + derive_shared_key_hex, derive_shared_keys, keys_from_shared_hex, print_dm_events, + send_admin_chat_message_via_shared_key, send_admin_gift_wrap_dm, send_dm, send_gift_wrap_dm, + wait_for_dm, }; pub use misc::{get_mcli_path, uppercase_first}; pub use net::connect_nostr; From 81a87aafd1bc430c0ff192ce6ea15faef519025b Mon Sep 17 00:00:00 2001 From: arkanoider Date: Mon, 2 Mar 2026 21:27:27 +0100 Subject: [PATCH 5/9] feat: --- src/cli.rs | 12 ++- src/cli/get_dm_user.rs | 200 +++++++++++++++++------------------------ src/util/messaging.rs | 10 +-- 3 files changed, 96 insertions(+), 126 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index cf8260f..ba2fe2c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -181,6 +181,12 @@ pub enum Commands { }, /// Get direct messages sent to any trade keys GetDmUser { + /// Pubkey of the user to get direct messages from + #[arg(short, long)] + pubkey: String, + /// Order id to get the trade keys from + #[arg(short, long)] + order_id: Uuid, /// Since time of the messages in minutes #[arg(short, long)] #[clap(default_value_t = 30)] @@ -565,7 +571,11 @@ impl Commands { Commands::GetDm { since, from_user } => { execute_get_dm(since, false, from_user, ctx).await } - Commands::GetDmUser { since } => execute_get_dm_user(since, ctx).await, + Commands::GetDmUser { + pubkey, + order_id, + since, + } => execute_get_dm_user(PublicKey::from_str(pubkey)?, *order_id, since, ctx).await, Commands::GetAdminDm { since, from_user } => { execute_get_dm(since, true, from_user, ctx).await } diff --git a/src/cli/get_dm_user.rs b/src/cli/get_dm_user.rs index e644fcb..e4fe728 100644 --- a/src/cli/get_dm_user.rs +++ b/src/cli/get_dm_user.rs @@ -3,142 +3,102 @@ use crate::db::Order; use crate::parser::common::{ print_info_line, print_key_value, print_no_data_message, print_section_header, }; -use crate::parser::dms::print_direct_messages; use crate::util::messaging::{derive_shared_key_bytes, fetch_gift_wraps_for_shared_key}; -use crate::util::{fetch_events_list, Event, ListKind}; use anyhow::Result; -use mostro_core::prelude::*; use nostr_sdk::prelude::*; +use uuid::Uuid; -pub async fn execute_get_dm_user(since: &i64, ctx: &Context) -> Result<()> { - // Get all trade keys from orders - let mut trade_keys_hex = Order::get_all_trade_keys(&ctx.pool).await?; - - // Include Mostro pubkey so we also fetch messages addressed to Mostro - let admin_pubkey_hex = ctx.mostro_pubkey.to_hex(); - if !trade_keys_hex.iter().any(|k| k == &admin_pubkey_hex) { - trade_keys_hex.push(admin_pubkey_hex); - } - // De-duplicate any repeated keys coming from DB/admin - trade_keys_hex.sort(); - trade_keys_hex.dedup(); - - // Check if the trade keys are empty - if trade_keys_hex.is_empty() { - print_no_data_message("No trade keys found in orders"); - return Ok(()); - } - +/// Fetch user-to-user chat messages over a shared conversation key. +/// +/// CLI parameters: +/// - `pubkey`: counterparty pubkey +/// - `order_id`: order used to look up the trade keys +/// - `since`: minutes back in time to include +pub async fn execute_get_dm_user( + pubkey: PublicKey, + order_id: Uuid, + since: &i64, + ctx: &Context, +) -> Result<()> { print_section_header("📨 Fetch User Direct Messages"); - print_key_value( - "🔍", - "Searching for DMs in trade keys", - &format!("{}", trade_keys_hex.len()), - ); + print_key_value("👥", "Counterparty", &pubkey.to_string()); + print_key_value("📋", "Order ID", &order_id.to_string()); print_key_value("⏰", "Since", &format!("{} minutes ago", since)); - print_info_line("💡", "Fetching direct messages..."); + print_info_line("💡", "Fetching shared-key chat messages..."); println!(); - let direct_messages = fetch_events_list( - ListKind::DirectMessagesUser, - None, - None, - None, - ctx, - Some(since), - ) - .await?; + // 1. Get the order and its trade keys + let order = Order::get_by_id(&ctx.pool, &order_id.to_string()) + .await + .map_err(|e| anyhow::anyhow!("Failed to load order {order_id}: {e}"))?; - // Extract (Message, u64, PublicKey) tuples from Event::MessageTuple variants (classic DMs) - let mut dm_events: Vec<(Message, u64, PublicKey)> = Vec::new(); - for event in direct_messages { - if let Event::MessageTuple(tuple) = event { - dm_events.push(*tuple); - } + let trade_keys_str = order + .trade_keys + .clone() + .ok_or_else(|| anyhow::anyhow!("Missing trade keys for order {order_id}"))?; + let trade_keys = + Keys::parse(&trade_keys_str).map_err(|e| anyhow::anyhow!("Invalid trade keys: {e}"))?; + + // 2. Derive the shared conversation key (trade private key + counterparty pubkey) + let shared_key_bytes = derive_shared_key_bytes(&trade_keys, &pubkey).map_err(|e| { + log::warn!( + "get_dm_user: could not derive shared key (trade + counterparty): {}", + e + ); + anyhow::anyhow!("Could not derive shared key for chat with counterparty") + })?; + + let shared_keys = SecretKey::from_slice(&shared_key_bytes) + .map(Keys::new) + .map_err(|e| anyhow::anyhow!("Could not build Keys from shared key: {e}"))?; + + // 3. Fetch all gift wraps addressed to this shared key and decrypt them + let mut messages = fetch_gift_wraps_for_shared_key(&ctx.client, &shared_keys).await?; + + // 4. Apply "since" filter (minutes back from now) + if *since > 0 { + let cutoff_ts = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(*since)) + .unwrap() + .timestamp(); + messages.retain(|(_, ts, _)| (*ts as i64) >= cutoff_ts); } - // Also fetch and decrypt shared-key custom wraps (shared key = trade_keys + identity_keys) - let trade_keys_hex_list = Order::get_all_trade_keys(&ctx.pool).await?; - let identity_pubkey = ctx.identity_keys.public_key(); - for trade_hex in trade_keys_hex_list { - let trade_keys = match Keys::parse(&trade_hex) { - Ok(k) => k, - Err(e) => { - log::warn!("get_dm_user: could not parse trade_keys: {}", e); - continue; - } - }; - let shared_key = match derive_shared_key_bytes(&trade_keys, &identity_pubkey) { - Ok(b) => b, - Err(e) => { - log::warn!( - "get_dm_user: could not derive shared key (trade + identity): {}", - e - ); - continue; - } - }; - let shared_keys = match SecretKey::from_slice(&shared_key) { - Ok(sk) => Keys::new(sk), - Err(e) => { - log::warn!("get_dm_user: could not build Keys from shared key: {}", e); - continue; - } - }; - let shared_msgs = match fetch_gift_wraps_for_shared_key(&ctx.client, &shared_keys).await { - Ok(m) => m, - Err(e) => { - log::warn!( - "get_dm_user: failed to fetch gift wraps for shared key: {}", - e - ); - continue; - } - }; - for (content, ts, sender_pubkey) in shared_msgs { - let parsed: (Message, Option) = match serde_json::from_str(&content) { - Ok(m) => m, - Err(e) => { - log::warn!("get_dm_user: could not parse shared-key DM content: {}", e); - continue; - } - }; - dm_events.push((parsed.0, ts as u64, sender_pubkey)); - } + // 5. Keep only messages sent by the counterparty (not our own side) + messages.retain(|(_, _, sender_pk)| *sender_pk == pubkey); + + if messages.is_empty() { + print_no_data_message("📭 No chat messages found for this shared conversation key."); + return Ok(()); + } - // Also fetch shared-key wraps for (trade_keys + mostro_pubkey) so we see admin replies - // (send_admin_dm_attach uses that derivation when we send to admin; admin uses same key to reply) - let shared_key_admin = match derive_shared_key_bytes(&trade_keys, &ctx.mostro_pubkey) { - Ok(b) => b, - Err(e) => { - log::warn!( - "get_dm_user: could not derive shared key (trade + mostro): {}", - e - ); - continue; - } + // 6. Pretty-print the messages + println!(""); + print_section_header("💬 Shared-Key Chat Messages"); + + for (idx, (content, ts, sender_pk)) in messages.iter().enumerate() { + let date = match chrono::DateTime::from_timestamp(*ts as i64, 0) { + Some(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(), + None => "Invalid timestamp".to_string(), }; - let shared_keys_admin = match SecretKey::from_slice(&shared_key_admin) { - Ok(sk) => Keys::new(sk), - Err(e) => { - log::warn!("get_dm_user: could not build Keys from shared key (admin): {}", e); - continue; - } + + // Mark messages from the counterparty vs our own future messages (if any) + let from_label = if *sender_pk == pubkey { + format!("👤 Counterparty ({sender_pk})") + } else { + format!("🧑 You ({sender_pk})") }; - if let Ok(admin_msgs) = fetch_gift_wraps_for_shared_key(&ctx.client, &shared_keys_admin).await { - for (content, ts, sender_pubkey) in admin_msgs { - if let Ok((parsed, _)) = serde_json::from_str::<(Message, Option)>(&content) { - dm_events.push((parsed, ts as u64, sender_pubkey)); - } - } - } - } - if dm_events.is_empty() { - print_no_data_message("You don't have any direct messages in your trade keys"); - return Ok(()); + println!("📄 Message {}:", idx + 1); + println!("─────────────────────────────────────"); + println!("⏰ Time: {}", date); + println!("📨 From: {}", from_label); + println!("📝 Content:"); + for line in content.lines() { + println!(" {}", line); + } + println!(); } - print_direct_messages(&dm_events, Some(ctx.mostro_pubkey)).await?; Ok(()) } diff --git a/src/util/messaging.rs b/src/util/messaging.rs index d08f0f4..ee2a64b 100644 --- a/src/util/messaging.rs +++ b/src/util/messaging.rs @@ -76,13 +76,13 @@ pub fn derive_shared_key_bytes(local_keys: &Keys, other_pubkey: &PublicKey) -> R /// Build a NIP-59 gift wrap event to a recipient pubkey (e.g. shared key pubkey). /// Rumor content is Mostro protocol format: JSON of (Message, Option). async fn build_custom_wrap_event( - sender: &Keys, + sender_keys: &Keys, recipient_pubkey: &PublicKey, message: &str, ) -> Result { let inner_message = EventBuilder::text_note(message) - .build(sender.public_key()) - .sign(sender) + .build(sender_keys.public_key()) + .sign(sender_keys) .await?; // Ephemeral key for the custom wrap @@ -120,7 +120,7 @@ async fn build_custom_wrap_event( /// (who derive the same shared key) can fetch and decrypt the event. pub async fn send_admin_chat_message_via_shared_key( client: &Client, - admin_keys: &Keys, + sender_keys: &Keys, shared_keys: &Keys, content: &str, ) -> Result<()> { @@ -129,7 +129,7 @@ pub async fn send_admin_chat_message_via_shared_key( return Err(anyhow::anyhow!("Cannot send empty chat message")); } let recipient_pubkey = shared_keys.public_key(); - let event = build_custom_wrap_event(admin_keys, &recipient_pubkey, content).await?; + let event = build_custom_wrap_event(sender_keys, &recipient_pubkey, content).await?; client.send_event(&event).await?; Ok(()) } From cb10d7770a9fea6e556572842aba5597ffd2bc0d Mon Sep 17 00:00:00 2001 From: arkanoider Date: Mon, 2 Mar 2026 22:02:18 +0100 Subject: [PATCH 6/9] chore: typo on folder --- src/util/misc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/misc.rs b/src/util/misc.rs index 00f094e..efcf073 100644 --- a/src/util/misc.rs +++ b/src/util/misc.rs @@ -10,7 +10,7 @@ pub fn uppercase_first(s: &str) -> String { pub fn get_mcli_path() -> String { let home_dir = dirs::home_dir().expect("Couldn't get home directory"); - let mcli_path = format!("{}/.mcliUserB", home_dir.display()); + let mcli_path = format!("{}/.mcli", home_dir.display()); if !Path::new(&mcli_path).exists() { match fs::create_dir(&mcli_path) { Ok(_) => println!("Directory {} created.", mcli_path), From 7b270a6db41aac2cc4dc4d2372a4e0dde2f7fe5f Mon Sep 17 00:00:00 2001 From: arkanoider Date: Mon, 2 Mar 2026 22:07:26 +0100 Subject: [PATCH 7/9] chore: typos --- src/cli/get_dm_user.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/cli/get_dm_user.rs b/src/cli/get_dm_user.rs index e4fe728..0225482 100644 --- a/src/cli/get_dm_user.rs +++ b/src/cli/get_dm_user.rs @@ -61,7 +61,7 @@ pub async fn execute_get_dm_user( .checked_sub_signed(chrono::Duration::minutes(*since)) .unwrap() .timestamp(); - messages.retain(|(_, ts, _)| (*ts as i64) >= cutoff_ts); + messages.retain(|(_, ts, _)| (*ts) >= cutoff_ts); } // 5. Keep only messages sent by the counterparty (not our own side) @@ -73,11 +73,10 @@ pub async fn execute_get_dm_user( } // 6. Pretty-print the messages - println!(""); print_section_header("💬 Shared-Key Chat Messages"); for (idx, (content, ts, sender_pk)) in messages.iter().enumerate() { - let date = match chrono::DateTime::from_timestamp(*ts as i64, 0) { + let date = match chrono::DateTime::from_timestamp(*ts, 0) { Some(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(), None => "Invalid timestamp".to_string(), }; From 762fb0209929e4acd54bf02571399c19a86989ce Mon Sep 17 00:00:00 2001 From: arkanoider Date: Mon, 2 Mar 2026 22:25:39 +0100 Subject: [PATCH 8/9] fix: Align DM docs/messages and validate POW env parsing --- Cargo.lock | 1574 +++++++++++++++++-------------- Cargo.toml | 13 +- docs/commands.md | 8 +- docs/senddm_flow.md | 16 +- src/cli/dm_to_user.rs | 2 +- src/cli/send_admin_dm_attach.rs | 11 +- src/util/messaging.rs | 6 +- 7 files changed, 899 insertions(+), 731 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd2c01f..a10227d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,20 +2,11 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -40,9 +31,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -64,9 +55,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -79,43 +70,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "argon2" @@ -141,28 +133,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "async-utility" version = "0.3.1" @@ -177,9 +147,9 @@ dependencies = [ [[package]] name = "async-wsocket" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7d8c7d34a225ba919dd9ba44d4b9106d20142da545e086be8ae21d1897e043" +checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069" dependencies = [ "async-utility", "futures", @@ -210,25 +180,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" [[package]] -name = "autocfg" -version = "1.4.0" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] -name = "backtrace" -version = "0.3.74" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base58ck" @@ -237,15 +198,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" dependencies = [ "bitcoin-internals 0.3.0", - "bitcoin_hashes 0.14.0", + "bitcoin_hashes 0.14.1", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -254,23 +209,23 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bech32" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bip39" -version = "2.2.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ - "bitcoin_hashes 0.13.0", + "bitcoin_hashes 0.14.1", "rand 0.8.5", "rand_core 0.6.4", "serde", @@ -279,27 +234,30 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.7" +version = "0.32.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda569d741b895131a88ee5589a467e73e9c4718e958ac9308e4f7dc44b6945" +checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", "bech32", "bitcoin-internals 0.3.0", - "bitcoin-io 0.1.3", + "bitcoin-io 0.1.4", "bitcoin-units", - "bitcoin_hashes 0.14.0", - "hex-conservative 0.2.1", + "bitcoin_hashes 0.14.1", + "hex-conservative 0.2.2", "hex_lit", "secp256k1", "serde", ] [[package]] -name = "bitcoin-internals" -version = "0.2.0" +name = "bitcoin-consensus-encoding" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" +checksum = "e8d7ca3dc8ff835693ad73bf1596240c06f974a31eeb3f611aaedf855f1f2725" +dependencies = [ + "bitcoin-internals 0.5.0", +] [[package]] name = "bitcoin-internals" @@ -312,15 +270,21 @@ dependencies = [ [[package]] name = "bitcoin-internals" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b854212e29b96c8f0fe04cab11d57586c8f3257de0d146c76cb3b42b3eb9118" +checksum = "a90bbbfa552b49101a230fb2668f3f9ef968c81e6f83cf577e1d4b80f689e1aa" + +[[package]] +name = "bitcoin-internals" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" [[package]] name = "bitcoin-io" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin-io" @@ -328,7 +292,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26792cd2bf245069a1c5acb06aa7ad7abe1de69b507c90b490bca81e0665d0ee" dependencies = [ - "bitcoin-internals 0.4.0", + "bitcoin-internals 0.4.2", ] [[package]] @@ -343,42 +307,42 @@ dependencies = [ [[package]] name = "bitcoin_hashes" -version = "0.13.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ - "bitcoin-internals 0.2.0", - "hex-conservative 0.1.2", + "bitcoin-io 0.1.4", + "hex-conservative 0.2.2", + "serde", ] [[package]] name = "bitcoin_hashes" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "7e5d09f16329cd545d7e6008b2c6b2af3a90bc678cf41ac3d2f6755943301b16" dependencies = [ - "bitcoin-io 0.1.3", - "hex-conservative 0.2.1", - "serde", + "bitcoin-io 0.2.0", + "hex-conservative 0.3.2", ] [[package]] name = "bitcoin_hashes" -version = "0.16.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e5d09f16329cd545d7e6008b2c6b2af3a90bc678cf41ac3d2f6755943301b16" +checksum = "9a8a45c2b41c457a9a9e4670422fcbdf109afb3b22bc920b4045e8bdfd788a3d" dependencies = [ - "bitcoin-io 0.2.0", - "hex-conservative 0.3.0", + "bitcoin-consensus-encoding", + "bitcoin-internals 0.5.0", ] [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -392,15 +356,16 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.2" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", + "cpufeatures", ] [[package]] @@ -423,9 +388,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -435,9 +400,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cbc" @@ -450,18 +415,19 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.6" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -495,9 +461,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -519,9 +485,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.46" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -529,9 +495,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.46" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -541,9 +507,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -553,21 +519,21 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "comfy-table" -version = "7.2.1" +version = "7.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" dependencies = [ "crossterm", "unicode-segmentation", @@ -591,9 +557,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "constant_time_eq" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "core-foundation-sys" @@ -603,18 +569,18 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc" -version = "3.2.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -627,9 +593,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -674,9 +640,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -685,15 +651,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", @@ -730,7 +696,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -746,9 +712,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -761,9 +727,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" dependencies = [ "serde", ] @@ -792,18 +758,18 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -819,20 +785,26 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -849,12 +821,6 @@ dependencies = [ "spin", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.1.5" @@ -863,18 +829,18 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -887,9 +853,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -897,15 +863,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -925,15 +891,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -942,15 +908,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -960,9 +926,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -972,7 +938,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -988,36 +953,43 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] [[package]] -name = "gimli" -version = "0.31.1" +name = "getrandom" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] [[package]] name = "glob" @@ -1039,22 +1011,28 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", ] [[package]] @@ -1065,9 +1043,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -1077,24 +1055,18 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-conservative" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" - -[[package]] -name = "hex-conservative" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ "arrayvec", ] [[package]] name = "hex-conservative" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afe881d0527571892c4034822e59bb10c6c991cce6abe8199b6f5cf10766f55" +checksum = "830e599c2904b08f0834ee6337d8fe8f0ed4a63b5d9e7a7f49c0ffa06d08d360" dependencies = [ "arrayvec", ] @@ -1125,21 +1097,20 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "http" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1155,12 +1126,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -1168,30 +1139,32 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "humantime" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1199,32 +1172,30 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", - "rustls 0.23.20", + "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", - "webpki-roots 0.26.7", + "webpki-roots 1.0.6", ] [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1241,14 +1212,15 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -1264,21 +1236,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1287,104 +1260,72 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] [[package]] -name = "icu_provider_macros" -version = "1.5.0" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1393,9 +1334,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1403,19 +1344,21 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array", @@ -1433,28 +1376,17 @@ dependencies = [ "web-sys", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1462,32 +1394,32 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1502,26 +1434,34 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.175" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags", "libc", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -1557,21 +1497,21 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lnurl-rs" @@ -1581,7 +1521,7 @@ checksum = "41eacdd87b675792f7752f3dd0937a00241a504c3956c47f72986490662e1db4" dependencies = [ "aes", "anyhow", - "base64 0.22.1", + "base64", "bech32", "bitcoin", "cbc", @@ -1594,25 +1534,24 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.16.0" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" [[package]] name = "lru-slab" @@ -1632,28 +1571,29 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -1661,10 +1601,10 @@ name = "mostro-cli" version = "0.14.5" dependencies = [ "anyhow", - "base64 0.22.1", + "base64", "bip39", "bitcoin", - "bitcoin_hashes 0.14.0", + "bitcoin_hashes 0.20.0", "chacha20poly1305", "chrono", "clap", @@ -1677,7 +1617,7 @@ dependencies = [ "mostro-core", "nostr-sdk", "pretty_env_logger", - "rand_core 0.6.4", + "rand_core 0.10.0", "reqwest", "rstest", "serde", @@ -1691,12 +1631,12 @@ dependencies = [ [[package]] name = "mostro-core" -version = "0.6.56" +version = "0.6.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d9108f750c13c52c13c8cc1c447022f404222fa7cd06e576f398c38624bb55" +checksum = "4c030ffbe3f2a86cbf32c0b608bae1f86e67de323762969c6f3c0e1ebe13aee7" dependencies = [ "argon2", - "base64 0.22.1", + "base64", "bitcoin", "bitcoin_hashes 0.16.0", "blake3", @@ -1720,18 +1660,18 @@ checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" [[package]] name = "nostr" -version = "0.43.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30e6dcb36d88017587b0b5578d1ed3398afe8e4f45fdb910e48b8675aaf6f68" +checksum = "62a97d745f1bd8d5e05a978632bbb87b0614567d5142906fe7c86fb2440faac6" dependencies = [ - "base64 0.22.1", + "base64", "bech32", "bip39", - "bitcoin_hashes 0.14.0", + "bitcoin_hashes 0.14.1", "cbc", "chacha20", "chacha20poly1305", - "getrandom 0.2.15", + "getrandom 0.2.17", "instant", "scrypt", "secp256k1", @@ -1754,9 +1694,9 @@ dependencies = [ [[package]] name = "nostr-relay-pool" -version = "0.43.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "265d9b44771ed15db93b183a0c93dbb703b2b0d0b74dffb5c2a081be52373a5a" +checksum = "2b2f43b70d13dfc50508a13cd902e11f4625312b2ce0e4b7c4c2283fd04001bd" dependencies = [ "async-utility", "async-wsocket", @@ -1784,11 +1724,10 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -1830,19 +1769,16 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.36.7" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "once_cell" -version = "1.20.2" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -1864,9 +1800,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1874,15 +1810,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1917,15 +1853,15 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1956,9 +1892,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "poly1305" @@ -1971,11 +1913,20 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] @@ -1990,6 +1941,16 @@ dependencies = [ "log", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -2001,9 +1962,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2020,9 +1981,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.20", + "rustls", "socket2", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -2035,15 +1996,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.2", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.20", + "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -2060,23 +2021,23 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -2096,7 +2057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2116,7 +2077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2125,23 +2086,38 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "getrandom 0.3.2", + "bitflags", ] [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags", ] @@ -2152,16 +2128,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2171,9 +2147,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2182,9 +2158,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "relative-path" @@ -2194,11 +2170,11 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-core", "http", @@ -2212,7 +2188,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.20", + "rustls", "rustls-pki-types", "serde", "serde_json", @@ -2227,29 +2203,28 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.3", + "webpki-roots 1.0.6", ] [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.17", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rsa" -version = "0.9.7" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest", @@ -2294,12 +2269,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -2317,67 +2286,47 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.21.12" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", + "once_cell", "ring", - "rustls-webpki 0.101.7", - "sct", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] -name = "rustls" -version = "0.23.20" +name = "rustls-pki-types" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -2386,15 +2335,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa20" @@ -2432,16 +2381,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sdd" version = "3.0.10" @@ -2454,7 +2393,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes 0.14.0", + "bitcoin_hashes 0.14.1", "rand 0.8.5", "secp256k1-sys", "serde", @@ -2486,9 +2425,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -2496,18 +2435,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2516,14 +2455,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -2540,11 +2480,12 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" dependencies = [ - "futures", + "futures-executor", + "futures-util", "log", "once_cell", "parking_lot", @@ -2554,9 +2495,9 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" dependencies = [ "proc-macro2", "quote", @@ -2576,9 +2517,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -2593,10 +2534,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2610,32 +2552,35 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2676,7 +2621,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "crc", "crossbeam-queue", @@ -2686,24 +2631,24 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown", + "hashbrown 0.15.5", "hashlink", "indexmap", "log", "memchr", "once_cell", "percent-encoding", - "rustls 0.23.20", + "rustls", "serde", "serde_json", "sha2", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", "url", - "webpki-roots 0.26.7", + "webpki-roots 0.26.11", ] [[package]] @@ -2751,7 +2696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64 0.22.1", + "base64", "bitflags", "byteorder", "bytes", @@ -2781,7 +2726,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", "whoami", ] @@ -2793,7 +2738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64 0.22.1", + "base64", "bitflags", "byteorder", "crc", @@ -2818,7 +2763,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", "whoami", ] @@ -2842,16 +2787,16 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", "url", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stringprep" @@ -2878,9 +2823,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.94" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2898,9 +2843,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -2927,11 +2872,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.18", ] [[package]] @@ -2947,9 +2892,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -2958,9 +2903,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -2968,9 +2913,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -2983,29 +2928,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -3014,11 +2956,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.20", + "rustls", "tokio", ] @@ -3036,9 +2978,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -3047,12 +2989,10 @@ dependencies = [ [[package]] name = "tokio-test" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" dependencies = [ - "async-stream", - "bytes", "futures-core", "tokio", "tokio-stream", @@ -3066,28 +3006,28 @@ checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ "futures-util", "log", - "rustls 0.23.20", + "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tungstenite", - "webpki-roots 0.26.7", + "webpki-roots 0.26.11", ] [[package]] name = "toml_datetime" -version = "0.7.1" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a197c0ec7d131bfc6f7e82c8442ba1595aeab35da7adbf05b6b73cd06a16b6be" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.5" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ad0b7ae9cfeef5605163839cb9221f453399f15cfb5c10be9885fcf56611f9" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", "toml_datetime", @@ -3097,18 +3037,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -3121,9 +3061,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", @@ -3151,9 +3091,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3163,9 +3103,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -3174,9 +3114,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -3199,18 +3139,18 @@ dependencies = [ "httparse", "log", "rand 0.9.2", - "rustls 0.23.20", + "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.12", + "thiserror 2.0.18", "utf-8", ] [[package]] name = "typenum" -version = "1.17.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-bidi" @@ -3220,24 +3160,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" @@ -3247,9 +3187,15 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "universal-hash" @@ -3269,32 +3215,33 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.8.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" dependencies = [ - "base64 0.21.7", + "base64", "flate2", "log", "once_cell", - "rustls 0.21.12", - "rustls-webpki 0.101.7", + "rustls", + "rustls-pki-types", "serde", "serde_json", "url", - "webpki-roots 0.25.4", + "webpki-roots 0.26.11", ] [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -3303,12 +3250,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -3323,29 +3264,17 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.4.1", "js-sys", "rand 0.9.2", - "serde", - "uuid-macro-internal", + "serde_core", "wasm-bindgen", ] -[[package]] -name = "uuid-macro-internal" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9384a660318abfbd7f8932c34d67e4d1ec511095f95972ddc01e19d7ba8413f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "vcpkg" version = "0.2.15" @@ -3369,17 +3298,26 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -3390,37 +3328,25 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -3429,9 +3355,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3439,31 +3365,65 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -3481,35 +3441,29 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - -[[package]] -name = "webpki-roots" -version = "0.26.7" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "rustls-pki-types", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "redox_syscall", + "libredox", "wasite", ] @@ -3531,11 +3485,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3546,18 +3500,62 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] [[package]] name = "windows-sys" @@ -3579,11 +3577,20 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -3610,13 +3617,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3629,6 +3653,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3641,6 +3671,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3653,12 +3689,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3671,6 +3719,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3683,6 +3737,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3695,6 +3755,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3707,43 +3773,121 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", ] [[package]] -name = "write16" -version = "1.0.0" +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3751,9 +3895,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -3763,19 +3907,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", @@ -3784,18 +3927,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", @@ -3805,15 +3948,26 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -3822,11 +3976,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index a7cab1a..2bff8be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,12 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.99" clap = { version = "4.5.46", features = ["derive"] } -nostr-sdk = { version = "0.43.0", features = ["nip06", "nip44", "nip59", "nip98"] } +nostr-sdk = { version = "0.43.0", features = [ + "nip06", + "nip44", + "nip59", + "nip98", +] } serde = "1.0.219" serde_json = "1.0.143" tokio = { version = "1.47.1", features = ["full"] } @@ -48,9 +53,9 @@ sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio-rustls"] } bip39 = { version = "2.2.0", features = ["rand"] } dirs = "6.0.0" chacha20poly1305 = "0.10.1" -rand_core = "0.6.4" -bitcoin = "0.32.2" -bitcoin_hashes = { version = "0.14", default-features = false } +rand_core = "0.10.0" +bitcoin = "0.32.8" +bitcoin_hashes = { version = "0.20.0", default-features = false } base64 = "0.22" [package.metadata.release] diff --git a/docs/commands.md b/docs/commands.md index 9885a5f..fdf6d1a 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -169,15 +169,15 @@ All commands are part of the `Commands` enum and are dispatched via `Commands::r ### Admin / solver dispute management - **`admcancel`** *(admin only)* - - **Description**: Cancel an order / dispute as admin. + - **Description**: Cancel a dispute. - **Args**: - - `--order-id `: Order identifier. + - `--dispute-id `: Dispute identifier. - **Handler**: `execute_admin_cancel_dispute(order_id, ctx)` in `src/cli/take_dispute.rs`. - **`admsettle`** *(admin only)* - - **Description**: Settle a seller's hold invoice. + - **Description**: Settle a dispute. - **Args**: - - `--order-id `: Order identifier. + - `--dispute-id `: Dispute identifier. - **Handler**: `execute_admin_settle_dispute(order_id, ctx)` in `src/cli/take_dispute.rs`. - **`admaddsolver`** *(admin only)* diff --git a/docs/senddm_flow.md b/docs/senddm_flow.md index 18eb55a..4a8db74 100644 --- a/docs/senddm_flow.md +++ b/docs/senddm_flow.md @@ -30,7 +30,7 @@ The shared `Context` (`src/cli.rs`) contains: File: `src/cli/send_dm.rs` -```12:69:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_dm.rs +```rust pub async fn execute_send_dm( receiver: PublicKey, ctx: &Context, @@ -56,7 +56,7 @@ Step‑by‑step: 2. **Mostro protocol payload**: - Constructs a Mostro‑core `Message`: - ```35:42:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_dm.rs + ```rust let message = Message::new_dm( None, None, @@ -75,7 +75,7 @@ Step‑by‑step: 3. **Resolve trade keys for this order**: - ```44:53:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_dm.rs + ```rust let trade_keys = if let Ok(order_to_vote) = Order::get_by_id(&ctx.pool, &order_id.to_string()).await { match order_to_vote.trade_keys.as_ref() { @@ -97,7 +97,7 @@ Step‑by‑step: 4. **Delegate to `util::send_dm`**: - ```56:64:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/cli/send_dm.rs + ```rust send_dm( &ctx.client, Some(&trade_keys), @@ -122,7 +122,7 @@ Step‑by‑step: File: `src/util/messaging.rs` -```201:253:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/util/messaging.rs +```rust pub async fn send_dm( client: &Client, identity_keys: Option<&Keys>, @@ -187,7 +187,7 @@ Key points: - **Message type decision**: - ```129:134:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/util/messaging.rs + ```rust fn determine_message_type(to_user: bool, private: bool) -> MessageType { match (to_user, private) { (true, _) => MessageType::PrivateDirectMessage, @@ -206,7 +206,7 @@ Key points: For `MessageType::SignedGiftWrap` we use `create_gift_wrap_event(..., signed = true)`: -```162:199:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/util/messaging.rs +```rust async fn create_gift_wrap_event( trade_keys: &Keys, identity_keys: Option<&Keys>, @@ -271,7 +271,7 @@ Protocol behaviour: - **Relaying**: - The final event is sent via: - ```251:252:/home/pinballwizard/rust_prj/mostro_p2p/mostro-cli/src/util/messaging.rs + ```rust client.send_event(&event).await?; ``` diff --git a/src/cli/dm_to_user.rs b/src/cli/dm_to_user.rs index 660dcb2..7f2d840 100644 --- a/src/cli/dm_to_user.rs +++ b/src/cli/dm_to_user.rs @@ -48,7 +48,7 @@ pub async fn execute_dm_to_user( // Send as shared-key custom wrap so both parties can decrypt via the shared key send_admin_chat_message_via_shared_key(client, &trade_keys, &shared_keys, message).await?; - print_success_message("Gift wrap message sent successfully!"); + print_success_message("Shared-key custom wrap message sent successfully!"); Ok(()) } diff --git a/src/cli/send_admin_dm_attach.rs b/src/cli/send_admin_dm_attach.rs index c3d8f5c..5307b5f 100644 --- a/src/cli/send_admin_dm_attach.rs +++ b/src/cli/send_admin_dm_attach.rs @@ -5,12 +5,11 @@ use anyhow::Result; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; use bitcoin_hashes::sha256::Hash as Sha256Hash; -use bitcoin_hashes::Hash; use chacha20poly1305::aead::{Aead, KeyInit}; use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; use nostr_sdk::prelude::*; -use rand_core::OsRng; -use rand_core::RngCore; +use rand::rngs::OsRng; +use rand::RngCore; use uuid::Uuid; use crate::cli::Context; @@ -76,7 +75,11 @@ async fn upload_to_blossom(trade_keys: &Keys, encrypted_blob: Vec) -> Result let client = reqwest::Client::new(); let payload_hash = Sha256Hash::hash(&encrypted_blob); - let payload_hex = payload_hash.to_string(); + let payload_hex = payload_hash + .to_byte_array() + .iter() + .map(|b| format!("{b:02x}")) + .collect::(); // Expiration: 1 hour from now (BUD-01 requires expiration in the future) let expiration = Timestamp::from(Timestamp::now().as_u64() + 3600); diff --git a/src/util/messaging.rs b/src/util/messaging.rs index ee2a64b..5145e15 100644 --- a/src/util/messaging.rs +++ b/src/util/messaging.rs @@ -99,11 +99,11 @@ async fn build_custom_wrap_event( // Build tags for the wrapper event, the recipient pubkey is the shared key pubkey let tag = Tag::public_key(*recipient_pubkey); - // Reuse POW behaviour from existing DM helpers + // Reuse POW behaviour from existing DM helpers, but fail on invalid values let pow: u8 = var("POW") .unwrap_or_else(|_| "0".to_string()) .parse() - .unwrap_or(0); + .map_err(|e| anyhow::anyhow!("Failed to parse POW: {}", e))?; // Build the wrapped event let wrapped_event = EventBuilder::new(nostr_sdk::Kind::GiftWrap, encrypted_content) @@ -230,7 +230,7 @@ async fn send_gift_wrap_dm_internal( let pow: u8 = var("POW") .unwrap_or_else(|_| "0".to_string()) .parse() - .unwrap_or(0); + .map_err(|e| anyhow::anyhow!("Failed to parse POW: {}", e))?; let dm_message = Message::new_dm( None, From b74adc49902a9663abfa13f3aa016aaf08c487c1 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Mon, 2 Mar 2026 22:30:09 +0100 Subject: [PATCH 9/9] fix: rabbit rants --- src/cli/get_dm_user.rs | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/cli/get_dm_user.rs b/src/cli/get_dm_user.rs index 0225482..5450380 100644 --- a/src/cli/get_dm_user.rs +++ b/src/cli/get_dm_user.rs @@ -52,19 +52,34 @@ pub async fn execute_get_dm_user( .map(Keys::new) .map_err(|e| anyhow::anyhow!("Could not build Keys from shared key: {e}"))?; - // 3. Fetch all gift wraps addressed to this shared key and decrypt them + // 3. Enforce the 7-day lookback window used by fetch_gift_wraps_for_shared_key + let max_minutes: i64 = 7 * 24 * 60; + if *since > max_minutes { + return Err(anyhow::anyhow!( + "Lookback window is limited to 7 days ({} minutes); requested {} minutes", + max_minutes, + since + )); + } + + // 4. Fetch all gift wraps addressed to this shared key and decrypt them let mut messages = fetch_gift_wraps_for_shared_key(&ctx.client, &shared_keys).await?; - // 4. Apply "since" filter (minutes back from now) + // 5. Apply "since" filter (minutes back from now) if *since > 0 { let cutoff_ts = chrono::Utc::now() .checked_sub_signed(chrono::Duration::minutes(*since)) - .unwrap() + .ok_or_else(|| { + anyhow::anyhow!( + "Invalid 'since' value {}; could not compute cutoff timestamp", + since + ) + })? .timestamp(); messages.retain(|(_, ts, _)| (*ts) >= cutoff_ts); } - // 5. Keep only messages sent by the counterparty (not our own side) + // 6. Keep only messages sent by the counterparty (not our own side) messages.retain(|(_, _, sender_pk)| *sender_pk == pubkey); if messages.is_empty() { @@ -72,7 +87,7 @@ pub async fn execute_get_dm_user( return Ok(()); } - // 6. Pretty-print the messages + // 7. Pretty-print the messages print_section_header("💬 Shared-Key Chat Messages"); for (idx, (content, ts, sender_pk)) in messages.iter().enumerate() { @@ -81,12 +96,8 @@ pub async fn execute_get_dm_user( None => "Invalid timestamp".to_string(), }; - // Mark messages from the counterparty vs our own future messages (if any) - let from_label = if *sender_pk == pubkey { - format!("👤 Counterparty ({sender_pk})") - } else { - format!("🧑 You ({sender_pk})") - }; + // Messages are already filtered to only those from the counterparty. + let from_label = format!("👤 Counterparty ({sender_pk})"); println!("📄 Message {}:", idx + 1); println!("─────────────────────────────────────");