Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 60 additions & 125 deletions Cargo.lock

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,32 @@ name = "mostro-cli"
path = "src/main.rs"

[dependencies]
anyhow = "1.0.99"
clap = { version = "4.5.46", features = ["derive"] }
anyhow = "1.0.100"
clap = { version = "4.5.54", features = ["derive"] }
nostr-sdk = { version = "0.43.0", features = ["nip06", "nip44", "nip59"] }
serde = "1.0.219"
serde_json = "1.0.143"
tokio = { version = "1.47.1", features = ["full"] }
comfy-table = "7.2.1"
chrono = "0.4.42"
log = "0.4.28"
serde = "1.0.228"
serde_json = "1.0.149"
tokio = { version = "1.49.0", features = ["full"] }
comfy-table = "7.2.2"
chrono = "0.4.43"
log = "0.4.29"
futures = "0.3"
uuid = { version = "1.18.1", features = [
uuid = { version = "1.19.0", features = [
"v4",
"fast-rng",
"macro-diagnostics",
"serde",
] }
lightning-invoice = { version = "0.33.2", features = ["std"] }
lightning-invoice = { version = "0.34.0", features = ["std"] }
reqwest = { version = "0.12.23", default-features = false, features = [
"json",
"rustls-tls",
] }
mostro-core = "0.6.56"
mostro-core = "0.7.0"
lnurl-rs = { version = "0.9.0", default-features = false, features = ["ureq"] }
pretty_env_logger = "0.5.0"
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio-rustls"] }
bip39 = { version = "2.2.0", features = ["rand"] }
bip39 = { version = "2.2.2", features = ["rand"] }
dirs = "6.0.0"

[package.metadata.release]
Expand Down
99 changes: 99 additions & 0 deletions docs/CODING_STANDARDS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Coding Standards

This document outlines the coding standards and best practices for the Mostro CLI project. These guidelines ensure code quality, maintainability, and consistency across the codebase.

## Core Principles

### 1. Readability and Reuse
**Priority**: Code should be written for humans first, machines second.
- **Clear naming**: Use descriptive names for functions, variables, and modules (e.g., `parse_dm_events` vs `pde`).
- **Function reuse**: Extract common logic into reusable functions. Place shared utilities in appropriate modules (`src/util/`, `src/parser/`, etc.).
- **Module organization**: Group related functionality together (CLI commands in `src/cli/`, Protocol parsing in `src/parser/`, Utilities in `src/util/`).

### 2. Avoid Code Duplication (DRY Principle)
**Don't Repeat Yourself**: If the same logic appears in multiple places, extract it.
- **Extract common patterns**: Create helper functions for repeated operations like DM sending.
- **Centralize constants**: Import from `mostro-core::prelude` instead of hardcoding values.

### 3. Simplicity
**Keep It Simple**: Prefer straightforward solutions over clever ones.
- **Avoid premature optimization**: Write clear code first, optimize only when needed.
- **Prefer explicit over implicit**: Use `Option` and `Result` types explicitly rather than hiding errors with `unwrap()`.

### 4. Function Length Limit
**Maximum 300 lines per function**: If a function exceeds this limit, split it into smaller, single-responsibility functions.

## Rust-Specific Guidelines

### Error Handling
- **Use `Result<T, E>`**: Functions that can fail should return `Result`.
- **Use `anyhow::Result`**: For application-level errors, use `anyhow::Result<T>`.
- **Propagate errors**: Use the `?` operator to propagate errors up the call stack.
- **Add context**: Use `.context("...")` from `anyhow` to add meaningful error messages.

### Type Safety
- **Use strong types**: Prefer newtypes or enums (`Action`, `Status`) over primitive types.
- **Leverage enums**: Use enums for state machines and role management.

### Async/Await
- **Prefer async/await**: Use `async fn` for I/O and network operations.
- **Handle timeouts**: Use `tokio::time::timeout` for network operations.

### Documentation
- **Document public APIs**: Use `///` doc comments for public functions and types.
- **Explain "why"**: Document the reasoning behind complex logic, not just "what".

## Nostr and Mostro-Specific Guidelines

### Event Kinds
- **Use constants**: Always use constants from `mostro-core::prelude` (e.g., `NOSTR_ORDER_EVENT_KIND`).
- **Never hardcode**: Avoid hardcoding event kind numbers like 38383.

### Message Handling
- **Parse DMs consistently**: Use `parse_dm_events` for all DM parsing.
- **Support multiple message types**: Handle both GiftWrap (NIP-59) and PrivateDirectMessage (NIP-17).

### Key Management
- **Identity vs Trade Keys**:
- **Identity keys** (index 0): User's main identity, used for signing.
- **Trade keys** (index 1+): Ephemeral keys for each trade, ensuring privacy.

## Code Organization Patterns

### Module Structure
```text
src/
├── main.rs # Entry point
├── cli.rs # CLI definitions and Context
├── db.rs # Database models (User, Order)
├── cli/ # CLI command implementations
├── parser/ # Event parsing and display
└── util/ # Core utilities (events, messaging, net)
```

### Re-export Pattern
Use `mod.rs` files to re-export commonly used items from submodules to keep imports clean.

## Database Patterns
- **User Model**: Use chainable setters and the `.save()` pattern.
- **Order Management**: Use `Order::new()` to handle both insertion and updates.

## CLI Command Pattern
All CLI commands follow a standard flow:
1. Validate inputs.
2. Get required keys (Identity/Trade).
3. Build the Mostro message.
4. Send to Mostro (NIP-59).
5. Wait for response (Subscription).
6. Parse and display results.

## Summary Checklist
- [ ] Code is readable and well-named.
- [ ] No code duplication (DRY).
- [ ] Functions are under 300 lines.
- [ ] Errors are handled properly (`Result`, `?`).
- [ ] Event kinds use constants from `mostro-core`.
- [ ] Both GiftWrap and PrivateDirectMessage are supported.
- [ ] Public APIs are documented.
- [ ] Code passes `cargo fmt` and `cargo clippy`.
- [ ] Tests pass (`cargo test`).
63 changes: 63 additions & 0 deletions docs/CREATING_ORDERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Creating Orders in Mostro CLI

This document explains how to create new buy and sell orders using Mostro CLI.

## Overview

Creating an order involves:
1. Specifying order parameters (type, amount, currency, etc.)
2. Building a Mostro message
3. Sending it to the Mostro coordinator via Nostr
4. Receiving confirmation
5. Waiting for someone to take the order

## Order Types

### Sell Order (Maker sells Bitcoin)
User wants to **sell Bitcoin** for fiat currency.
```bash
mostro-cli neworder -k sell -c USD -f 100 -a 50000 -m "PayPal"
```

### Buy Order (Maker buys Bitcoin)
User wants to **buy Bitcoin** with fiat currency.
```bash
mostro-cli neworder -k buy -c EUR -f 200 -m "Bank Transfer" -i lnbc...
```

## Order Parameters

### Required Parameters
| Parameter | Flag | Description | Example |
|-----------|------|-------------|---------|
| Kind | `-k`, `--kind` | "buy" or "sell" | `sell` |
| Fiat Code | `-c`, `--fiat-code` | Currency code | `USD`, `EUR`, `ARS` |
| Fiat Amount | `-f`, `--fiat-amount` | Amount in fiat | `100` or `100-500` (range) |
| Payment Method | `-m`, `--payment-method` | How to pay | `"PayPal"`, `"Bank Transfer"` |

### Optional Parameters
| Parameter | Flag | Description | Default |
|-----------|------|-------------|---------|
| Amount | `-a`, `--amount` | Bitcoin in sats | 0 (market price) |
| Premium | `-p`, `--premium` | Price premium % | 0 |
| Invoice | `-i`, `--invoice` | Lightning invoice (buy orders) | None |
| Expiration Days | `-e`, `--expiration-days` | Days until expired | 0 |

## Order Examples

### 1. Simple Sell Order (Market Price)
```bash
mostro-cli neworder -k sell -c USD -f 100 -m "PayPal"
```

### 2. Range Order (Flexible Amount)
```bash
mostro-cli neworder -k sell -c USD -f 100-500 -m "PayPal,Venmo"
```

### 3. Buy Order with Lightning Invoice
```bash
mostro-cli neworder -k buy -c USD -f 50 -i lnbc500u1p3.... -m "Cash App"
```

For technical details on the code flow and message structures, see [ORDER_FLOW_TECHNICAL.md](./ORDER_FLOW_TECHNICAL.md).
32 changes: 32 additions & 0 deletions docs/DATABASE_SCHEMA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Database Schema

Mostro CLI uses a local SQLite database (`mcli.db`) to store user identity and trade history.

## Table: `users`
Stores the BIP-39 mnemonic and identity information.

| Column | Type | Description |
|--------|------|-------------|
| `i0_pubkey` | `CHAR(64)` | Primary Key. The user's identity pubkey. |
| `mnemonic` | `TEXT` | The 12-word seed phrase. |
| `last_trade_index` | `INTEGER` | The last derived trade key index. |
| `created_at` | `INTEGER` | Timestamp of creation. |

## Table: `orders`
Stores details of orders created or taken by the user.

| Column | Type | Description |
|--------|------|-------------|
| `id` | `TEXT` | Primary Key. Order UUID. |
| `kind` | `TEXT` | "buy" or "sell". |
| `status` | `TEXT` | Current status (pending, active, etc.). |
| `amount` | `INTEGER` | Satoshis. |
| `fiat_code` | `TEXT` | e.g., "USD". |
| `fiat_amount` | `INTEGER` | Fiat units. |
| `trade_keys` | `TEXT` | Hex-encoded private key for this trade. |
| `is_mine` | `BOOLEAN` | True if the user created the order. |
| `created_at` | `INTEGER` | Creation timestamp. |

## Implementation Reference
- `src/db.rs`: Contains the `User` and `Order` structs and SQL queries.
- `src/util/storage.rs`: Helper functions for database interaction.
43 changes: 43 additions & 0 deletions docs/DISPUTE_MANAGEMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Dispute Management

This document covers how disputes are handled in Mostro CLI by both users and administrators.

## User Dispute Flow

When a trade goes wrong (e.g., fiat sent but Bitcoin not released), either party can initiate a dispute.

### Initiate a Dispute
```bash
mostro-cli dispute -o <order-id>
```
Mostro changes the order status to `Dispute`. This prevents any further automated transitions and flags the trade for manual intervention.

## Admin/Solver Flow

Admins or designated solvers use special commands to resolve conflicts. These commands require the `ADMIN_NSEC` environment variable to be set.

### 1. List Active Disputes
```bash
mostro-cli listdisputes
```

### 2. Take a Dispute
Before resolving, an admin must "take" the dispute to indicate they are handling it.
```bash
mostro-cli admtakedispute -d <dispute-id>
```

### 3. Settle (Pay Buyer)
If the buyer proved they sent fiat, the admin settles the hold invoice to pay the buyer.
```bash
mostro-cli admsettle -o <order-id>
```

### 4. Cancel (Refund Seller)
If the buyer failed to pay, the admin cancels the order to refund the locked Bitcoin to the seller.
```bash
mostro-cli admcancel -o <order-id>
```

## Security
Admin commands are gated by public key verification on the Mostro coordinator side. The CLI must sign these messages with the private key corresponding to a registered admin pubkey.
89 changes: 89 additions & 0 deletions docs/KEY_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Key Management Implementation

This document provides technical details, code examples, and security practices for Mostro CLI key management.

## Database Storage

### User Table
Stores the root secret and identity info.

```sql
CREATE TABLE users (
i0_pubkey char(64) PRIMARY KEY, -- Identity public key (hex)
mnemonic TEXT, -- BIP-39 mnemonic
last_trade_index INTEGER, -- Last used trade index
created_at INTEGER
);
```

### Order Table
Stores trade-specific keys.

```sql
CREATE TABLE orders (
id TEXT PRIMARY KEY,
trade_keys TEXT, -- Private key for this trade (hex)
-- ... other fields
);
```

## Implementation Details

### Deriving Identity Keys
```rust
pub async fn get_identity_keys(pool: &SqlitePool) -> Result<Keys> {
let user = User::get(pool).await?;
let account = NOSTR_ORDER_EVENT_KIND as u32; // 38383

Keys::from_mnemonic_advanced(
&user.mnemonic,
None,
Some(account),
Some(0),
Some(0) // Identity is always index 0
)
}
```

### Deriving Trade Keys
```rust
pub async fn get_trade_keys(pool: &SqlitePool, index: i64) -> Result<Keys> {
let user = User::get(pool).await?;
let account = NOSTR_ORDER_EVENT_KIND as u32;

Keys::from_mnemonic_advanced(
&user.mnemonic,
None,
Some(account),
Some(0),
Some(index as u32) // Incremental index 1, 2, 3...
)
}
```

## Security Best Practices

### DO ✅
- **Use unique keys**: Always use `get_next_trade_keys()` for new orders.
- **Sign with identity**: Prove authenticity while maintaining sender privacy.
- **Update indices**: Ensure `last_trade_index` is updated after successful creation.

### DON'T ❌
- **Reuse keys**: Never use the same trade key for two different orders.
- **Author with identity**: Never set the `pubkey` of a public event to your identity key.
- **Lose mnemonic**: Keys cannot be recovered without the seed phrase.
Comment on lines 70 to 80
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify identity vs trade-key signing guidance.
“Sign with identity” and “Never set the pubkey of a public event to your identity key” read as contradictory in Nostr. Please clarify when identity signing is appropriate (e.g., authentication/registration only) and that public events should be authored/signed with trade keys.

✏️ Suggested rewording
-### DO ✅
-- **Use unique keys**: Always use `get_next_trade_keys()` for new orders.
-- **Sign with identity**: Prove authenticity while maintaining sender privacy.
-- **Update indices**: Ensure `last_trade_index` is updated after successful creation.
+### DO ✅
+- **Use unique keys**: Always use `get_next_trade_keys()` for new orders.
+- **Sign public events with trade keys**: Preserve privacy by authoring events with per-trade keys.
+- **Use identity key only for authentication/registration** (if required by protocol).
+- **Update indices**: Ensure `last_trade_index` is updated after successful creation.
🤖 Prompt for AI Agents
In `@docs/KEY_IMPLEMENTATION.md` around lines 66 - 74, Clarify the apparent
contradiction by updating the DO/DON'T section to state that identity keys
(identity key / identity signing) are only to be used for explicit
authentication or registration workflows (e.g., proving account ownership),
while all public trade-related events must be authored and signed with
trade-specific keys obtained via get_next_trade_keys(); explicitly mention that
pubkey in public events must refer to the trade key, not the identity key, and
keep guidance about never reusing trade keys and updating last_trade_index after
creating a trade event.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's correct! Identity key is used to seal the nip59 giftwrap, you can read about here. So identity is used in every message but never revealed.
Read about here --> https://mostro.network/protocol/key_management.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🌐 Web query:

mostro.network protocol key management NIP-59 giftwrap

💡 Result:

Summary — how Mostro uses NIP-59 (Gift Wrap) for key management

  • Mostro clients generate new (throwaway) Nostr private keys per order/operation and use those ephemeral keys to publish NIP‑59 “gift wrap” events so buyers/sellers and Mostrod cannot be linked to a single long‑lived key. This is explicit in Mostro’s protocol/FAQ and privacy docs. [1][2]

  • Mechanism: Mostro wraps a rumor (unsigned inner event) inside a Seal (signed by the true author), then places that encrypted Seal inside a Gift Wrap (outer event) signed with the ephemeral key. Encryption uses NIP‑44 conventions (conversation/shared‑key derived via ECDH). This three‑layer pattern (rumor → seal → gift wrap) is the NIP‑59 design Mostro implements. Clients should rotate throwaway keys, randomize timestamps, and publish only to recipient relays where possible to reduce metadata leakage. [3][4]

  • Practical notes: Mostro’s implementation automates key generation/rotation for each order; private keys are not shared with the Mostro instance; the gift‑wrap content is NIP‑44 encrypted and the seal reveals the true author only inside the encrypted seal. For developer examples and SDK usage (wrapping/unwrapping), see NDK/Rust examples referenced in the NIP‑59 docs and Mostro docs. [3][4][1]

Sources
[1] Mostro — “How it works” / order flow (key rotation per order).
[2] Mostro — “Privacy in Mostro” (explains ephemeral keys / not sharing private keys).
[3] NIP‑59 specification (Gift Wrap; rumor → seal → gift wrap; uses NIP‑44).
[4] Kind 1059 / implementation notes and SDK examples (gift‑wrap usage).


@arkanoider, thank you for the clarification! That makes perfect sense now. The identity key signs the encrypted seal within the NIP-59 giftwrap, so while it authenticates every message, it's never exposed publicly. The public events are authored with trade keys, maintaining privacy.

Your current documentation is technically correct, but it might be helpful to add a brief note explicitly mentioning the NIP-59 giftwrap mechanism to make this clearer for readers unfamiliar with the protocol. Something like:

Sign with identity: Prove authenticity via NIP-59 seal (encrypted, never publicly revealed) while maintaining sender privacy.

This would help prevent confusion like mine! But the current version is accurate given the link you provided for additional context.


✏️ Learnings added
Learnt from: arkanoider
Repo: MostroP2P/mostro-cli PR: 154
File: docs/KEY_IMPLEMENTATION.md:70-80
Timestamp: 2026-01-17T13:10:54.496Z
Learning: In mostro-cli, the identity key is used to sign the NIP-59 giftwrap seal for authentication. The seal is encrypted within the giftwrap, so the identity key signature is never publicly revealed while still proving message authenticity. Public events are authored with trade keys for privacy.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: arkanoider
Repo: MostroP2P/mostro-cli PR: 135
File: src/cli/get_dm.rs:24-29
Timestamp: 2025-09-13T10:31:42.313Z
Learning: In mostro-cli, index 0 represents the master key and should be skipped when iterating through trade keys. The loop should start from index 1 when fetching user trade keys in get_dm.rs.

Learnt from: arkanoider
Repo: MostroP2P/mostro-cli PR: 135
File: src/cli.rs:301-306
Timestamp: 2025-09-12T20:02:14.291Z
Learning: The mostro-cli is designed as a test client that allows users to test both regular user operations and admin operations from the terminal. Therefore, MOSTRO_PUBKEY should always be required regardless of whether NSEC_PRIVKEY is present, as both user and admin operations need to know which Mostro instance to interact with.

Learnt from: arkanoider
Repo: MostroP2P/mostro-cli PR: 146
File: src/cli/dm_to_user.rs:25-32
Timestamp: 2025-10-18T11:48:11.944Z
Learning: In mostro-cli's DM functionality (dm_to_user.rs and similar), message content consists of ephemeral public keys that change every time and contains no sensitive personal data. Logging message content to stdout is acceptable and not a privacy concern.

Learnt from: arkanoider
Repo: MostroP2P/mostro-cli PR: 135
File: src/cli/add_invoice.rs:57-79
Timestamp: 2025-09-09T19:18:57.161Z
Learning: arkanoider prefers to bubble up errors with anyhow::Result instead of using tokio::spawn with eprintln! error handling in Nostr DM sending scenarios, as the spawn is often overkill for simple send operations.


## Key Recovery

If the local database is lost but the mnemonic is saved:
1. **Identity**: Re-deriving index 0 restores the original `npub`.
2. **Trade History**: Re-deriving indices 1, 2, 3... restores access to trade messages.
3. **Session Sync**: Use `mostro-cli restore` to fetch active orders and their associated trade indices from the Mostro coordinator.

## Troubleshooting

### "Cannot decrypt message"
Usually means the CLI is trying to use the wrong trade key. Ensure you are loading the key associated with the specific `order_id` from the database.

### "Trade index mismatch"
Occurs when the local database index is behind Mostro's records. Run `mostro-cli restore` to synchronize.
Loading