Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5b090d9
feat: improving parsing functions
arkanoider Sep 28, 2025
5ad663a
chore: going on to uniform message fetching calls
arkanoider Sep 29, 2025
fbe1c9a
chore: almost remove wait for dm function with fetch event
arkanoider Sep 29, 2025
ebe1bfc
chore: almost remove wait for dm function with fetch event
arkanoider Sep 29, 2025
55c6d3f
chore: testing all patterns with new code
arkanoider Sep 30, 2025
4fcb3bf
fix: some fixes on message management
arkanoider Oct 5, 2025
29baccb
fix: added a check to avoid UNIQUE constraint error - now if an order…
arkanoider Oct 7, 2025
ac3f69b
feat: add invalid fiat currency managed
arkanoider Oct 7, 2025
60e7638
chore: rabbit fixes
arkanoider Oct 7, 2025
47ff00e
chore: rabbit fixes
arkanoider Oct 7, 2025
d9d356a
feat: added correct since time calculation for get dm command - impro…
arkanoider Oct 8, 2025
807ce79
chore: added trade index number printout in new order and take order …
arkanoider Oct 8, 2025
08ea97e
chore: rabbit improvement
arkanoider Oct 8, 2025
07e4165
chore: rabbit improvement
arkanoider Oct 8, 2025
c55069e
chore: fix on rabbit improvements
arkanoider Oct 8, 2025
275facf
fix: moved some logic parts in wait dm function to avoid issues with …
arkanoider Oct 9, 2025
61636b1
feat: first testable mostro cli version with last_trade_index command…
arkanoider Oct 9, 2025
39f4b26
feat: improved reception logic and small fix for last trade index
arkanoider Oct 9, 2025
51f8588
chore: updateded README.md
arkanoider Oct 9, 2025
6301df0
chore: fix by rabbit
arkanoider Oct 9, 2025
6bc8f73
feat: fixed some wrong key in decryption
arkanoider Oct 9, 2025
a3be582
chore: fix for clippy gh actions
arkanoider Oct 9, 2025
4edef4d
chore: fix in README.md
arkanoider Oct 9, 2025
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
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ uuid = { version = "1.18.1", features = [
dotenvy = "0.15.6"
lightning-invoice = { version = "0.33.2", features = ["std"] }
reqwest = { version = "0.12.23", features = ["json"] }
mostro-core = "0.6.50"
mostro-core = "0.6.56"
lnurl-rs = "0.9.0"
pretty_env_logger = "0.5.0"
openssl = { version = "0.10.73", features = ["vendored"] }
Expand Down
68 changes: 37 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,58 @@

Very simple command line interface that show all new replaceable events from [Mostro](https://github.com/MostroP2P/mostro)

## Requirements:
## Requirements

0. You need Rust version 1.64 or higher to compile.
1. You will need a lightning network node

## Install dependencies:
## Install dependencies

To compile on Ubuntu/Pop!\_OS, please install [cargo](https://www.rust-lang.org/tools/install), then run the following commands:

```
$ sudo apt update
$ sudo apt install -y cmake build-essential pkg-config
```bash
sudo apt update
sudo apt install -y cmake build-essential pkg-config
```

## Install

To install you need to fill the env vars (`.env`) on the with your own private key and add a Mostro pubkey.

```
$ git clone https://github.com/MostroP2P/mostro-cli.git
$ cd mostro-cli
$ cp .env-sample .env
$ cargo run
```bash
git clone https://github.com/MostroP2P/mostro-cli.git
cd mostro-cli
cp .env-sample .env
cargo run
```

# Usage
## Usage

```
```bash
Commands:
listorders Requests open orders from Mostro pubkey
neworder Create a new buy/sell order on Mostro
takesell Take a sell order from a Mostro pubkey
takebuy Take a buy order from a Mostro pubkey
addinvoice Buyer add a new invoice to receive the payment
getdm Get the latest direct messages from Mostro
fiatsent Send fiat sent message to confirm payment to other user
release Settle the hold invoice and pay to buyer
cancel Cancel a pending order
rate Rate counterpart after a successful trade
dispute Start a dispute
admcancel Cancel an order (only admin)
admsettle Settle a seller's hold invoice (only admin)
admlistdisputes Requests open disputes from Mostro pubkey
admaddsolver Add a new dispute's solver (only admin)
admtakedispute Admin or solver take a Pending dispute (only admin)
help Print this message or the help of the given subcommand(s)
listorders Requests open orders from Mostro pubkey
neworder Create a new buy/sell order on Mostro
takesell Take a sell order from a Mostro pubkey
takebuy Take a buy order from a Mostro pubkey
addinvoice Buyer add a new invoice to receive the payment
getdm Get the latest direct messages
getadmindm Get the latest direct messages for admin
senddm Send direct message to a user
fiatsent Send fiat sent message to confirm payment to other user
release Settle the hold invoice and pay to buyer
cancel Cancel a pending order
rate Rate counterpart after a successful trade
restore Restore session to recover all pending orders and disputes
dispute Start a dispute
admcancel Cancel an order (only admin)
admsettle Settle a seller's hold invoice (only admin)
admlistdisputes Requests open disputes from Mostro pubkey
admaddsolver Add a new dispute's solver (only admin)
admtakedispute Admin or solver take a Pending dispute (only admin)
admsenddm Send gift wrapped direct message to a user (only admin)
conversationkey Get the conversation key for direct messaging with a user
getlasttradeindex Get last trade index of user
help Print this message or the help of the given subcommand(s)

Options:
-v, --verbose
Expand All @@ -60,9 +66,9 @@ Options:
-V, --version Print version
```

# Examples
## Examples

```
```bash
$ mostro-cli -m npub1ykvsmrmw2hk7jgxgy64zr8tfkx4nnjhq9eyfxdlg3caha3ph0skq6jr3z0 -r 'wss://nos.lol,wss://relay.damus.io,wss://nostr-pub.wellorder.net,wss://nostr.mutinywallet.com,wss://relay.nostr.band,wss://nostr.cizmar.net,wss://140.f7z.io,wss://nostrrelay.com,wss://relay.nostrr.de' listorders

# You can set the env vars to avoid the -m, -n and -r flags
Expand Down
15 changes: 11 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod conversation_key;
pub mod dm_to_user;
pub mod get_dm;
pub mod get_dm_user;
pub mod last_trade_index;
pub mod list_disputes;
pub mod list_orders;
pub mod new_order;
Expand All @@ -20,6 +21,7 @@ use crate::cli::conversation_key::execute_conversation_key;
use crate::cli::dm_to_user::execute_dm_to_user;
use crate::cli::get_dm::execute_get_dm;
use crate::cli::get_dm_user::execute_get_dm_user;
use crate::cli::last_trade_index::execute_last_trade_index;
use crate::cli::list_disputes::execute_list_disputes;
use crate::cli::list_orders::execute_list_orders;
use crate::cli::new_order::execute_new_order;
Expand Down Expand Up @@ -291,6 +293,8 @@ pub enum Commands {
#[arg(short, long)]
pubkey: String,
},
/// Get last trade index of user
GetLastTradeIndex {},
}

fn get_env_var(cli: &Cli) {
Expand Down Expand Up @@ -415,9 +419,12 @@ impl Commands {
| Commands::Release { order_id }
| Commands::Dispute { order_id }
| Commands::Cancel { order_id } => {
crate::util::run_simple_order_msg(self.clone(), order_id, ctx).await
crate::util::run_simple_order_msg(self.clone(), Some(*order_id), ctx).await
}
// Last trade index commands
Commands::GetLastTradeIndex {} => {
execute_last_trade_index(&ctx.identity_keys, ctx.mostro_pubkey, ctx).await
}

// DM commands with pubkey parsing
Commands::SendDm {
pubkey,
Expand Down Expand Up @@ -489,11 +496,11 @@ impl Commands {

// DM retrieval commands
Commands::GetDm { since, from_user } => {
execute_get_dm(Some(since), false, from_user, ctx).await
execute_get_dm(since, false, from_user, ctx).await
}
Commands::GetDmUser { since } => execute_get_dm_user(since, ctx).await,
Commands::GetAdminDm { since, from_user } => {
execute_get_dm(Some(since), true, from_user, ctx).await
execute_get_dm(since, true, from_user, ctx).await
}
Comment on lines 497 to 504
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 7, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix borrow when calling execute_get_dm

execute_get_dm now takes &i64. Passing since by value won’t compile (expected &i64, found i64). Borrow it at both call sites.

-                execute_get_dm(since, false, from_user, ctx).await
+                execute_get_dm(&since, false, from_user, ctx).await-                execute_get_dm(since, true, from_user, ctx).await
+                execute_get_dm(&since, true, from_user, ctx).await
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// DM retrieval commands
Commands::GetDm { since, from_user } => {
execute_get_dm(Some(since), false, from_user, ctx).await
execute_get_dm(since, false, from_user, ctx).await
}
Commands::GetDmUser { since } => execute_get_dm_user(since, ctx).await,
Commands::GetAdminDm { since, from_user } => {
execute_get_dm(Some(since), true, from_user, ctx).await
execute_get_dm(since, true, from_user, ctx).await
}
// DM retrieval 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::GetAdminDm { since, from_user } => {
execute_get_dm(&since, true, from_user, ctx).await
}
🤖 Prompt for AI Agents
In src/cli.rs around lines 490 to 497, the calls to execute_get_dm pass since by
value but the function now expects &i64; change the two call sites to pass a
reference (e.g. &since) instead of since so the types match (update both the
Commands::GetDm and Commands::GetAdminDm invocations).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Since is passed correctly also the:
cargo clippy --all-targets --all-features -- -D warnings

command is happy!

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

🏁 Script executed:

#!/bin/bash
# Check the actual signature of execute_get_dm function
echo "=== Checking execute_get_dm signature ==="
rg -n "pub async fn execute_get_dm" src/cli/get_dm.rs -A 3

echo ""
echo "=== Checking call sites in src/cli.rs ==="
rg -n "execute_get_dm\(" src/cli.rs -B 1 -A 1

Length of output: 482


@arkanoider, thank you for confirming! Just to be clear: the diff shown above passes since by value (without the &), but execute_get_dm expects &i64. If you've already added the borrow operator (&since) in your local code and clippy is happy, that's perfect! ✅

If you haven't applied the fix yet, you'll need to change both call sites to:

  • execute_get_dm(&since, false, from_user, ctx).await
  • execute_get_dm(&since, true, from_user, ctx).await

Once that's done, the types will align correctly and clippy will pass.

🐰


🧬 Code Graph Analysis Results

[src/cli/adm_send_dm.rs:6-17]

pub async fn execute_adm_send_dm(receiver: PublicKey, ctx: &Context, message: &str) -> Result<()> {
    println!(
        "SENDING DM with admin keys: {}",
        ctx.context_keys.public_key().to_hex()
    );

    send_admin_gift_wrap_dm(&ctx.client, &ctx.context_keys, &receiver, message).await?;

    println!("Admin gift wrap message sent to {}", receiver);

    Ok(())
}

[src/cli/conversation_key.rs:5-17]

pub async fn execute_conversation_key(trade_keys: &Keys, receiver: PublicKey) -> Result<()> {
    // Derive conversation key
    let ck = ConversationKey::derive(trade_keys.secret_key(), &receiver)?;
    let key = ck.as_bytes();
    let mut ck_hex = vec![];
    for i in key {
        ck_hex.push(format!("{:02x}", i));
    }
    let ck_hex = ck_hex.join("");
    println!("Conversation key: {:?}", ck_hex);

    Ok(())
}

[src/cli/list_disputes.rs:7-21]

pub async fn execute_list_disputes(ctx: &Context) -> Result<()> {
    // Print mostro pubkey
    println!(
        "Requesting disputes from mostro pubId - {}",
        &ctx.mostro_pubkey
    );

    // Get orders from relays
    let table_of_disputes =
        fetch_events_list(ListKind::Disputes, None, None, None, ctx, None).await?;
    let table = print_disputes_table(table_of_disputes)?;
    println!("{table}");

    Ok(())
}

[src/cli/restore.rs:7-32]

pub async fn execute_restore(
    identity_keys: &Keys,
    mostro_key: PublicKey,
    client: &Client,
) -> Result<()> {
    let restore_message = Message::new_restore(None);
    let message_json = restore_message
        .as_json()
        .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

    // Send the restore message to Mostro server
    send_dm(
        client,
        Some(identity_keys),
        identity_keys,
        &mostro_key,
        message_json,
        None,
        false,
    )
    .await?;

    println!("Restore message sent successfully. Recovering pending orders and disputes...");

    Ok(())
}

[src/cli/dm_to_user.rs:7-33]

pub async fn execute_dm_to_user(
    receiver: PublicKey,
    client: &Client,
    order_id: &Uuid,
    message: &str,
    pool: &SqlitePool,
) -> Result<()> {
    // Get the order
    let order = Order::get_by_id(pool, &order_id.to_string())
        .await
        .map_err(|_| anyhow::anyhow!("order {} not found", order_id))?;
    // Get the trade keys
    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"),
    };

    // Send the DM
    println!(
        "SENDING DM with trade keys: {}",
        trade_keys.public_key().to_hex()
    );

    send_gift_wrap_dm(client, &trade_keys, &receiver, message).await?;

    Ok(())
}

[src/cli/get_dm.rs:10-38]

pub async fn execute_get_dm(
    since: &i64,
    admin: bool,
    from_user: &bool,
    ctx: &Context,
) -> Result<()> {
    // Get the list kind
    let list_kind = match (admin, from_user) {
        (true, true) => ListKind::PrivateDirectMessagesUser,
        (true, false) => ListKind::DirectMessagesAdmin,
        (false, true) => ListKind::PrivateDirectMessagesUser,
        (false, false) => ListKind::DirectMessagesUser,
    };

    // Fetch the requested events
    let all_fetched_events =
        fetch_events_list(list_kind, None, None, None, ctx, Some(since)).await?;

    // Extract (Message, u64) tuples from Event::MessageTuple variants
    let mut dm_events: Vec<(Message, u64)> = Vec::new();
    for event in all_fetched_events {
        if let Event::MessageTuple(tuple) = event {
            dm_events.push(*tuple);
        }
    }

    print_direct_messages(&dm_events, &ctx.pool).await?;
    Ok(())
}

[src/cli/send_dm.rs:8-47]

pub async fn execute_send_dm(
    receiver: PublicKey,
    ctx: &Context,
    order_id: &Uuid,
    message: &str,
) -> Result<()> {
    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"))?;

    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));
        };

    send_dm(
        &ctx.client,
        None,
        &trade_keys,
        &receiver,
        message,
        None,
        false,
    )
    .await?;

    Ok(())
}

[src/cli/rate_user.rs:19-59]

pub async fn execute_rate_user(order_id: &Uuid, rating: &u8, ctx: &Context) -> Result<()> {
    // Check boundaries
    let rating_content = get_user_rate(rating)?;

    // Get the trade keys
    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 => {
                    return Err(anyhow::anyhow!("No trade_keys found for this order"));
                }
            }
        } else {
            return Err(anyhow::anyhow!("order {} not found", order_id));
        };

    // Create rating message of counterpart
    let rate_message = Message::new_order(
        Some(*order_id),
        None,
        None,
        Action::RateUser,
        Some(rating_content),
    )
    .as_json()
    .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

    send_dm(
        &ctx.client,
        Some(&ctx.identity_keys),
        &trade_keys,
        &ctx.mostro_pubkey,
        rate_message,
        None,
        false,
    )
    .await?;

    Ok(())
}

[src/cli/list_orders.rs:9-72]

pub async fn execute_list_orders(
    kind: &Option<String>,
    currency: &Option<String>,
    status: &Option<String>,
    ctx: &Context,
) -> Result<()> {
    // Used to get upper currency string to check against a list of tickers
    let mut upper_currency: Option<String> = None;
    // Default status is pending
    let mut status_checked: Option<Status> = Some(Status::Pending);
    // Default kind is none
    let mut kind_checked: Option<mostro_core::order::Kind> = None;

    // New check against strings
    if let Some(s) = status {
        status_checked = Some(
            Status::from_str(s)
                .map_err(|e| anyhow::anyhow!("Not valid status '{}': {:?}", s, e))?,
        );
    }

    // Print status requested
    if let Some(status) = &status_checked {
        println!("You are searching orders with status {:?}", status);
    }
    // New check against strings for kind
    if let Some(k) = kind {
        kind_checked = Some(
            mostro_core::order::Kind::from_str(k)
                .map_err(|e| anyhow::anyhow!("Not valid order kind '{}': {:?}", k, e))?,
        );
        if let Some(kind) = &kind_checked {
            println!("You are searching {} orders", kind);
        }
    }

    // Uppercase currency
    if let Some(curr) = currency {
        upper_currency = Some(curr.to_uppercase());
        if let Some(currency) = &upper_currency {
            println!("You are searching orders with currency {}", currency);
        }
    }

    println!(
        "Requesting orders from mostro pubId - {}",
        &ctx.mostro_pubkey
    );

    // Get orders from relays
    let table_of_orders = fetch_events_list(
        ListKind::Orders,
        status_checked,
        upper_currency,
        kind_checked,
        ctx,
        None,
    )
    .await?;
    let table = print_orders_table(table_of_orders)?;
    println!("{table}");

    Ok(())
}

[src/cli/take_order.rs:54-124]

pub async fn execute_take_order(
    order_id: &Uuid,
    action: Action,
    invoice: &Option<String>,
    amount: Option<u32>,
    ctx: &Context,
) -> Result<()> {
    let action_name = match action {
        Action::TakeBuy => "take buy",
        Action::TakeSell => "take sell",
        _ => return Err(anyhow::anyhow!("Invalid action for take order")),
    };

    println!(
        "Request of {} order {} from mostro pubId {}",
        action_name, order_id, ctx.mostro_pubkey
    );

    // Create payload based on action type
    let payload = create_take_order_payload(action.clone(), invoice, amount)?;

    // Create request id
    let request_id = Uuid::new_v4().as_u128() as u64;

    // Create message
    let take_order_message = Message::new_order(
        Some(*order_id),
        Some(request_id),
        Some(ctx.trade_index),
        action.clone(),
        payload,
    );

    // Send dm to receiver pubkey
    println!(
        "SENDING DM with trade keys: {:?}",
        ctx.trade_keys.public_key().to_hex()
    );

    let message_json = take_order_message
        .as_json()
        .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

    // Send the DM
    // This is so we can wait for the gift wrap event in the main thread
    let _ = send_dm(
        &ctx.client,
        Some(&ctx.identity_keys),
        &ctx.trade_keys,
        &ctx.mostro_pubkey,
        message_json,
        None,
        false,
    )
    .await;

    // Wait for the DM to be sent from mostro
    let recv_event = wait_for_dm(ctx, None).await?;

    // Parse the incoming DM
    let messages = parse_dm_events(recv_event, &ctx.trade_keys, None).await;
    if let Some(message) = messages.first() {
        let message = message.0.get_inner_message_kind();
        if message.request_id == Some(request_id) {
            if let Err(e) = print_commands_results(message, None, ctx).await {
                println!("Error in print_commands_results: {}", e);
            }
        }
    }
    Ok(())
}

[src/cli/add_invoice.rs:12-84]

pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context) -> Result<()> {
    let order = Order::get_by_id(&ctx.pool, &order_id.to_string()).await?;
    let trade_keys = order
        .trade_keys
        .clone()
        .ok_or(anyhow::anyhow!("Missing trade keys"))?;
    let order_trade_keys = Keys::parse(&trade_keys)?;
    println!(
        "Order trade keys: {:?}",
        order_trade_keys.public_key().to_hex()
    );

    println!(
        "Sending a lightning invoice for order {} to mostro pubId {}",
        order_id, ctx.mostro_pubkey
    );
    // Check invoice string
    let ln_addr = LightningAddress::from_str(invoice);
    let payload = if ln_addr.is_ok() {
        Some(Payload::PaymentRequest(None, invoice.to_string(), None))
    } else {
        match is_valid_invoice(invoice) {
            Ok(i) => Some(Payload::PaymentRequest(None, i.to_string(), None)),
            Err(e) => {
                return Err(anyhow::anyhow!("Invalid invoice: {}", e));
            }
        }
    };

    // Create request id
    let request_id = Uuid::new_v4().as_u128() as u64;
    // Create AddInvoice message
    let add_invoice_message = Message::new_order(
        Some(*order_id),
        Some(request_id),
        None,
        Action::AddInvoice,
        payload,
    );

    // Serialize the message
    let message_json = add_invoice_message
        .as_json()
        .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

    // Send the DM
    let _ = send_dm(
        &ctx.client,
        Some(&ctx.identity_keys),
        &order_trade_keys,
        &ctx.mostro_pubkey,
        message_json,
        None,
        false,
    )
    .await;

    // Wait for the DM to be sent from mostro
    let recv_event = wait_for_dm(ctx, Some(&order_trade_keys)).await?;

    // Parse the incoming DM
    let messages = parse_dm_events(recv_event, &order_trade_keys, None).await;
    if let Some(message) = messages.first() {
        let message = message.0.get_inner_message_kind();
        if message.request_id == Some(request_id) {
            if let Err(e) = print_commands_results(message, Some(order.clone()), ctx).await {
                println!("Error in print_commands_results: {}", e);
            }
        }
    }

    Ok(())
}

[src/cli/take_dispute.rs:64-85]

pub async fn execute_take_dispute(dispute_id: &Uuid, ctx: &Context) -> Result<()> {
    println!(
        "Request of take dispute {} from mostro pubId {}",
        dispute_id,
        ctx.mostro_pubkey.clone()
    );
    // Create takebuy message
    let take_dispute_message = Message::new_dispute(
        Some(*dispute_id),
        None,
        None,
        Action::AdminTakeDispute,
        None,
    )
    .as_json()
    .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

    println!("Admin keys: {:?}", ctx.context_keys.public_key.to_string());

    admin_send_dm(ctx, take_dispute_message).await?;
    Ok(())
}

[src/cli/take_dispute.rs:7-26]

pub async fn execute_admin_add_solver(npubkey: &str, ctx: &Context) -> Result<()> {
    println!(
        "Request of add solver with pubkey {} from mostro pubId {}",
        npubkey, &ctx.mostro_pubkey
    );
    // Create takebuy message
    let take_dispute_message = Message::new_dispute(
        Some(Uuid::new_v4()),
        None,
        None,
        Action::AdminAddSolver,
        Some(Payload::TextMessage(npubkey.to_string())),
    )
    .as_json()
    .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

    admin_send_dm(ctx, take_dispute_message).await?;

    Ok(())
}

[src/cli/take_dispute.rs:47-62]

pub async fn execute_admin_settle_dispute(dispute_id: &Uuid, ctx: &Context) -> Result<()> {
    println!(
        "Request of take dispute {} from mostro pubId {}",
        dispute_id,
        ctx.mostro_pubkey.clone()
    );
    // Create takebuy message
    let take_dispute_message =
        Message::new_dispute(Some(*dispute_id), None, None, Action::AdminSettle, None)
            .as_json()
            .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

    println!("Admin keys: {:?}", ctx.context_keys.public_key.to_string());
    admin_send_dm(ctx, take_dispute_message).await?;
    Ok(())
}

[src/cli/add_invoice.rs:12-84]

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes but since is yet a reference coming from cli command no need to reference it again

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

🏁 Script executed:

#!/bin/bash
# Check the actual pattern matching code in src/cli.rs
echo "=== Checking the actual pattern match in src/cli.rs ==="
rg -n "Commands::GetDm.*since.*from_user" src/cli.rs -A 2 -B 1

echo ""
echo "=== Checking the Commands enum definition ==="
rg -n "GetDm {" src/cli.rs -A 5

echo ""
echo "=== Checking execute_get_dm signature ==="
rg -n "pub async fn execute_get_dm" src/cli/get_dm.rs -A 3

Length of output: 508


🏁 Script executed:

#!/bin/bash
# Check the Commands enum GetDm variant definition
echo "=== Checking GetDm enum variant ==="
rg -n "GetDm \{" src/cli.rs -A 5

echo ""
echo "=== Alternative: check entire Commands enum around GetDm ==="
rg -n "/// Get the latest direct messages" src/cli.rs -A 10

echo ""
echo "=== Check if the code currently compiles ==="
cd /tmp && cargo check --manifest-path="${GITHUB_WORKSPACE}/Cargo.toml" 2>&1 | head -50

Length of output: 1710


@arkanoider, I understand the confusion, but there's actually a type mismatch here. Let me clarify:

When you pattern match on Commands::GetDm { since, from_user }, the since variable is an owned i64 value (not a reference), because the enum field is defined as:

GetDm {
    since: i64,  // ← owned value
    from_user: bool,
}

However, execute_get_dm expects a reference:

pub async fn execute_get_dm(
    since: &i64,  // ← expects a reference
    ...
)

So when you call execute_get_dm(since, ...), you're passing an i64 where an &i64 is expected. This is a type error in Rust.

You need to borrow since at the call sites:

Commands::GetDm { since, from_user } => {
    execute_get_dm(&since, false, from_user, ctx).await
}
Commands::GetAdminDm { since, from_user } => {
    execute_get_dm(&since, true, from_user, ctx).await
}

If your local code compiles without the &, then you might have already made this fix locally, or perhaps you've modified the function signature to accept i64 instead of &i64. Could you double-check what you have locally?

🐰


🧬 Code Graph Analysis Results
  • File: src/cli/adm_send_dm.rs, Lines 6-17
pub async fn execute_adm_send_dm(receiver: PublicKey, ctx: &Context, message: &str) -> Result<()> {
    println!(
        "SENDING DM with admin keys: {}",
        ctx.context_keys.public_key().to_hex()
    );

    send_admin_gift_wrap_dm(&ctx.client, &ctx.context_keys, &receiver, message).await?;

    println!("Admin gift wrap message sent to {}", receiver);

    Ok(())
}
  • File: src/cli/conversation_key.rs, Lines 5-17
pub async fn execute_conversation_key(trade_keys: &Keys, receiver: PublicKey) -> Result<()> {
    // Derive conversation key
    let ck = ConversationKey::derive(trade_keys.secret_key(), &receiver)?;
    let key = ck.as_bytes();
    let mut ck_hex = vec![];
    for i in key {
        ck_hex.push(format!("{:02x}", i));
    }
    let ck_hex = ck_hex.join("");
    println!("Conversation key: {:?}", ck_hex);

    Ok(())
}
  • File: src/cli/list_disputes.rs, Lines 7-21
pub async fn execute_list_disputes(ctx: &Context) -> Result<()> {
    // Print mostro pubkey
    println!(
        "Requesting disputes from mostro pubId - {}",
        &ctx.mostro_pubkey
    );

    // Get orders from relays
    let table_of_disputes =
        fetch_events_list(ListKind::Disputes, None, None, None, ctx, None).await?;
    let table = print_disputes_table(table_of_disputes)?;
    println!("{table}");

    Ok(())
}
  • File: src/cli/restore.rs, Lines 7-32
pub async fn execute_restore(
    identity_keys: &Keys,
    mostro_key: PublicKey,
    client: &Client,
) -> Result<()> {
    let restore_message = Message::new_restore(None);
    let message_json = restore_message
        .as_json()
        .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

    // Send the restore message to Mostro server
    send_dm(
        client,
        Some(identity_keys),
        identity_keys,
        &mostro_key,
        message_json,
        None,
        false,
    )
    .await?;

    println!("Restore message sent successfully. Recovering pending orders and disputes...");

    Ok(())
}
  • File: src/cli/dm_to_user.rs, Lines 7-33
pub async fn execute_dm_to_user(
    receiver: PublicKey,
    client: &Client,
    order_id: &Uuid,
    message: &str,
    pool: &SqlitePool,
) -> Result<()> {
    // Get the order
    let order = Order::get_by_id(pool, &order_id.to_string())
        .await
        .map_err(|_| anyhow::anyhow!("order {} not found", order_id))?;
    // Get the trade keys
    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"),
    };

    // Send the DM
    println!(
        "SENDING DM with trade keys: {}",
        trade_keys.public_key().to_hex()
    );

    send_gift_wrap_dm(client, &trade_keys, &receiver, message).await?;

    Ok(())
}
  • File: src/cli/get_dm.rs, Lines 10-38
pub async fn execute_get_dm(
    since: &i64,
    admin: bool,
    from_user: &bool,
    ctx: &Context,
) -> Result<()> {
    // Get the list kind
    let list_kind = match (admin, from_user) {
        (true, true) => ListKind::PrivateDirectMessagesUser,
        (true, false) => ListKind::DirectMessagesAdmin,
        (false, true) => ListKind::PrivateDirectMessagesUser,
        (false, false) => ListKind::DirectMessagesUser,
    };

    // Fetch the requested events
    let all_fetched_events =
        fetch_events_list(list_kind, None, None, None, ctx, Some(since)).await?;

    // Extract (Message, u64) tuples from Event::MessageTuple variants
    let mut dm_events: Vec<(Message, u64)> = Vec::new();
    for event in all_fetched_events {
        if let Event::MessageTuple(tuple) = event {
            dm_events.push(*tuple);
        }
    }

    print_direct_messages(&dm_events, &ctx.pool).await?;
    Ok(())
}
  • File: src/cli/send_dm.rs, Lines 8-47
pub async fn execute_send_dm(
    receiver: PublicKey,
    ctx: &Context,
    order_id: &Uuid,
    message: &str,
) -> Result<()> {
    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"))?;

    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));
        };

    send_dm(
        &ctx.client,
        None,
        &trade_keys,
        &receiver,
        message,
        None,
        false,
    )
    .await?;

    Ok(())
}
  • File: src/cli/rate_user.rs, Lines 19-59
pub async fn execute_rate_user(order_id: &Uuid, rating: &u8, ctx: &Context) -> Result<()> {
    // Check boundaries
    let rating_content = get_user_rate(rating)?;

    // Get the trade keys
    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 => {
                    return Err(anyhow::anyhow!("No trade_keys found for this order"));
                }
            }
        } else {
            return Err(anyhow::anyhow!("order {} not found", order_id));
        };

    // Create rating message of counterpart
    let rate_message = Message::new_order(
        Some(*order_id),
        None,
        None,
        Action::RateUser,
        Some(rating_content),
    )
    .as_json()
    .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

    send_dm(
        &ctx.client,
        Some(&ctx.identity_keys),
        &trade_keys,
        &ctx.mostro_pubkey,
        rate_message,
        None,
        false,
    )
    .await?;

    Ok(())
}
  • File: src/cli/list_orders.rs, Lines 9-72
pub async fn execute_list_orders(
    kind: &Option<String>,
    currency: &Option<String>,
    status: &Option<String>,
    ctx: &Context,
) -> Result<()> {
    // Used to get upper currency string to check against a list of tickers
    let mut upper_currency: Option<String> = None;
    // Default status is pending
    let mut status_checked: Option<Status> = Some(Status::Pending);
    // Default kind is none
    let mut kind_checked: Option<mostro_core::order::Kind> = None;

    // New check against strings
    if let Some(s) = status {
        status_checked = Some(
            Status::from_str(s)
                .map_err(|e| anyhow::anyhow!("Not valid status '{}': {:?}", s, e))?,
        );
    }

    // Print status requested
    if let Some(status) = &status_checked {
        println!("You are searching orders with status {:?}", status);
    }
    // New check against strings for kind
    if let Some(k) = kind {
        kind_checked = Some(
            mostro_core::order::Kind::from_str(k)
                .map_err(|e| anyhow::anyhow!("Not valid order kind '{}': {:?}", k, e))?,
        );
        if let Some(kind) = &kind_checked {
            println!("You are searching {} orders", kind);
        }
    }

    // Uppercase currency
    if let Some(curr) = currency {
        upper_currency = Some(curr.to_uppercase());
        if let Some(currency) = &upper_currency {
            println!("You are searching orders with currency {}", currency);
        }
    }

    println!(
        "Requesting orders from mostro pubId - {}",
        &ctx.mostro_pubkey
    );

    // Get orders from relays
    let table_of_orders = fetch_events_list(
        ListKind::Orders,
        status_checked,
        upper_currency,
        kind_checked,
        ctx,
        None,
    )
    .await?;
    let table = print_orders_table(table_of_orders)?;
    println!("{table}");

    Ok(())
}
  • File: src/cli/take_order.rs, Lines 54-124
pub async fn execute_take_order(
    order_id: &Uuid,
    action: Action,
    invoice: &Option<String>,
    amount: Option<u32>,
    ctx: &Context,
) -> Result<()> {
    let action_name = match action {
        Action::TakeBuy => "take buy",
        Action::TakeSell => "take sell",
        _ => return Err(anyhow::anyhhow!("Invalid action for take order")),
    };

    println!(
        "Request of {} order {} from mostro pubId {}",
        action_name, order_id, ctx.mostro_pubkey
    );

    // Create payload based on action type
    let payload = create_take_order_payload(action.clone(), invoice, amount)?;

    // Create request id
    let request_id = Uuid::new_v4().as_u128() as u64;

    // Create message
    let take_order_message = Message::new_order(
        Some(*order_id),
        Some(request_id),
        Some(ctx.trade_index),
        action.clone(),
        payload,
    );

    // Send dm to receiver pubkey
    println!(
        "SENDING DM with trade keys: {:?}",
        ctx.trade_keys.public_key().to_hex()
    );

    let message_json = take_order_message
        .as_json()
        .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

    // Send the DM
    // This is so we can wait for the gift wrap event in the main thread
    send_dm(
        &ctx.client,
        Some(&ctx.identity_keys),
        &ctx.trade_keys,
        &ctx.mostro_pubkey,
        message_json,
        None,
        false,
    )
    .await?;

    // Wait for the DM to be sent from mostro
    let recv_event = wait_for_dm(ctx, None).await?;

    // Parse the incoming DM
    let messages = parse_dm_events(recv_event, &ctx.trade_keys, None).await;
    if let Some(message) = messages.first() {
        let message = message.0.get_inner_message_kind();
        if message.request_id == Some(request_id) {
            if let Err(e) = print_commands_results(message, None, ctx).await {
                println!("Error in print_commands_results: {}", e);
            }
        }
    }
    Ok(())
}
  • File: src/cli/add_invoice.rs, Lines 12-84
pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context) -> Result<()> {
    let order = Order::get_by_id(&ctx.pool, &order_id.to_string()).await?;
    let trade_keys = order
        .trade_keys
        .clone()
        .ok_or(anyhow::anyhow!("Missing trade keys"))?;
    let order_trade_keys = Keys::parse(&trade_keys)?;
    println!(
        "Order trade keys: {:?}",
        order_trade_keys.public_key().to_hex()
    );

    println!(
        "Sending a lightning invoice for order {} to mostro pubId {}",
        order_id, ctx.mostro_pubkey
    );
    // Check invoice string
    let ln_addr = LightningAddress::from_str(invoice);
    let payload = if ln_addr.is_ok() {
        Some(Payload::PaymentRequest(None, invoice.to_string(), None))
    } else {
        match is_valid_invoice(invoice) {
            Ok(i) => Some(Payload::PaymentRequest(None, i.to_string(), None)),
            Err(e) => {
                return Err(anyhow::anyhow!("Invalid invoice: {}", e));
            }
        }
    };

    // Create request id
    let request_id = Uuid::new_v4().as_u128() as u64;
    // Create AddInvoice message
    let add_invoice_message = Message::new_order(
        Some(*order_id),
        Some(request_id),
        None,
        Action::AddInvoice,
        payload,
    );

    // Serialize the message
    let message_json = add_invoice_message
        .as_json()
        .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

    // Send the DM
    send_dm(
        &ctx.client,
        Some(&ctx.identity_keys),
        &order_trade_keys,
        &ctx.mostro_pubkey,
        message_json,
        None,
        false,
    )
    .await?;

    // Wait for the DM to be sent from mostro
    let recv_event = wait_for_dm(ctx, Some(&order_trade_keys)).await?;

    // Parse the incoming DM
    let messages = parse_dm_events(recv_event, &order_trade_keys, None).await;
    if let Some(message) = messages.first() {
        let message = message.0.get_inner_message_kind();
        if message.request_id == Some(request_id) {
            if let Some(order) = Order::get_by_id(&ctx.pool, &order_id.to_string()).await.ok() {
                if let Err(e) = print_commands_results(message, Some(order.clone()), ctx).await {
                    println!("Error in print_commands_results: {}", e);
                }
            }
        }
    }

    Ok(())
}


// Admin commands
Expand Down
56 changes: 18 additions & 38 deletions src/cli/add_invoice.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::util::{send_dm, wait_for_dm};
use crate::util::{print_dm_events, send_dm, wait_for_dm};
use crate::{cli::Context, db::Order, lightning::is_valid_invoice};
use anyhow::Result;
use lnurl::lightning_address::LightningAddress;
Expand All @@ -8,11 +8,14 @@ use std::str::FromStr;
use uuid::Uuid;

pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context) -> Result<()> {
// Get order from order id
let order = Order::get_by_id(&ctx.pool, &order_id.to_string()).await?;
// Get trade keys of specific order
let trade_keys = order
.trade_keys
.clone()
.ok_or(anyhow::anyhow!("Missing trade keys"))?;

let order_trade_keys = Keys::parse(&trade_keys)?;
println!(
"Order trade keys: {:?}",
Expand Down Expand Up @@ -52,45 +55,22 @@ pub async fn execute_add_invoice(order_id: &Uuid, invoice: &str, ctx: &Context)
.as_json()
.map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

// Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0
let subscription = Filter::new()
.pubkey(order_trade_keys.clone().public_key())
.kind(nostr_sdk::Kind::GiftWrap)
.limit(0);

let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1));
ctx.client.subscribe(subscription, Some(opts)).await?;
// Send the DM
let sent_message = send_dm(
&ctx.client,
Some(&ctx.identity_keys),
&order_trade_keys,
&ctx.mostro_pubkey,
message_json,
None,
false,
);

// Clone the keys and client for the async call
let identity_keys_clone = ctx.identity_keys.clone();
let client_clone = ctx.client.clone();
let mostro_pubkey_clone = ctx.mostro_pubkey;
let order_trade_keys_clone = order_trade_keys.clone();
// Wait for the DM to be sent from mostro
let recv_event = wait_for_dm(ctx, Some(&order_trade_keys), sent_message).await?;

// Spawn a new task to send the DM
// This is so we can wait for the gift wrap event in the main thread
tokio::spawn(async move {
let _ = send_dm(
&client_clone,
Some(&identity_keys_clone),
&order_trade_keys,
&mostro_pubkey_clone,
message_json,
None,
false,
)
.await;
});
// Parse the incoming DM
print_dm_events(recv_event, request_id, ctx, Some(&order_trade_keys)).await?;

// Wait for the DM to be sent from mostro and update the order
wait_for_dm(
&ctx.client,
&order_trade_keys_clone,
request_id,
None,
Some(order),
&ctx.pool,
)
.await?;
Ok(())
}
5 changes: 3 additions & 2 deletions src/cli/get_dm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
};

pub async fn execute_get_dm(
since: Option<&i64>,
since: &i64,
admin: bool,
from_user: &bool,
ctx: &Context,
Expand All @@ -22,7 +22,8 @@ pub async fn execute_get_dm(
};

// Fetch the requested events
let all_fetched_events = { fetch_events_list(list_kind, None, None, None, ctx, since).await? };
let all_fetched_events =
fetch_events_list(list_kind, None, None, None, ctx, Some(since)).await?;

// Extract (Message, u64) tuples from Event::MessageTuple variants
let mut dm_events: Vec<(Message, u64)> = Vec::new();
Expand Down
60 changes: 60 additions & 0 deletions src/cli/last_trade_index.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use anyhow::Result;
use mostro_core::prelude::*;
use nostr_sdk::prelude::*;

use crate::{
cli::Context,
parser::{dms::print_commands_results, parse_dm_events},
util::{send_dm, wait_for_dm},
};

pub async fn execute_last_trade_index(
identity_keys: &Keys,
mostro_key: PublicKey,
ctx: &Context,
) -> Result<()> {
let kind = MessageKind::new(None, None, None, Action::LastTradeIndex, None);
let last_trade_index_message = Message::Restore(kind);
let message_json = last_trade_index_message
.as_json()
.map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;

// Send the last trade index message to Mostro server
let sent_message = send_dm(
&ctx.client,
Some(identity_keys),
identity_keys,
&mostro_key,
message_json,
None,
false,
);

// Log the sent message
println!(
"Sent request to Mostro to get last trade index of user {}",
identity_keys.public_key()
);

// Wait for incoming DM
let recv_event = wait_for_dm(ctx, Some(identity_keys), sent_message).await?;

// Parse the incoming DM
let messages = parse_dm_events(recv_event, identity_keys, None).await;
if let Some((message, _, _)) = messages.first() {
let message = message.get_inner_message_kind();
if message.action == Action::LastTradeIndex {
print_commands_results(message, None, ctx).await?
} else {
return Err(anyhow::anyhow!(
"Received response with mismatched action. Expected: {:?}, Got: {:?}",
Action::LastTradeIndex,
message.action
));
}
} else {
return Err(anyhow::anyhow!("No response received from Mostro"));
}

Ok(())
}
Loading