mostro_client/
cli.rs

1pub mod add_invoice;
2pub mod adm_send_dm;
3pub mod conversation_key;
4pub mod dm_to_user;
5pub mod get_dm;
6pub mod get_dm_user;
7pub mod last_trade_index;
8pub mod list_disputes;
9pub mod list_orders;
10pub mod new_order;
11pub mod orders_info;
12pub mod rate_user;
13pub mod restore;
14pub mod send_dm;
15pub mod send_msg;
16pub mod take_dispute;
17pub mod take_order;
18
19use crate::cli::add_invoice::execute_add_invoice;
20use crate::cli::adm_send_dm::execute_adm_send_dm;
21use crate::cli::conversation_key::execute_conversation_key;
22use crate::cli::dm_to_user::execute_dm_to_user;
23use crate::cli::get_dm::execute_get_dm;
24use crate::cli::get_dm_user::execute_get_dm_user;
25use crate::cli::last_trade_index::execute_last_trade_index;
26use crate::cli::list_disputes::execute_list_disputes;
27use crate::cli::list_orders::execute_list_orders;
28use crate::cli::new_order::execute_new_order;
29use crate::cli::orders_info::execute_orders_info;
30use crate::cli::rate_user::execute_rate_user;
31use crate::cli::restore::execute_restore;
32use crate::cli::send_dm::execute_send_dm;
33use crate::cli::take_dispute::execute_take_dispute;
34use crate::cli::take_order::execute_take_order;
35use crate::db::{connect, User};
36use crate::util;
37
38use anyhow::{Error, Result};
39use clap::{Parser, Subcommand};
40use mostro_core::prelude::*;
41use nostr_sdk::prelude::*;
42use sqlx::SqlitePool;
43use std::{
44    env::{set_var, var},
45    str::FromStr,
46};
47use take_dispute::*;
48use uuid::Uuid;
49
50#[derive(Debug)]
51pub struct Context {
52    pub client: Client,
53    pub identity_keys: Keys,
54    pub trade_keys: Keys,
55    pub trade_index: i64,
56    pub pool: SqlitePool,
57    pub context_keys: Option<Keys>,
58    pub mostro_pubkey: PublicKey,
59}
60
61#[derive(Parser)]
62#[command(
63    name = "mostro-cli",
64    about = "A simple CLI to use Mostro P2P",
65    author,
66    help_template = "\
67{before-help}{name} 🧌
68
69{about-with-newline}
70{author-with-newline}
71{usage-heading} {usage}
72
73{all-args}{after-help}
74",
75    version
76)]
77#[command(propagate_version = true)]
78#[command(arg_required_else_help(true))]
79pub struct Cli {
80    #[command(subcommand)]
81    pub command: Option<Commands>,
82    #[arg(short, long)]
83    pub verbose: bool,
84    #[arg(short, long)]
85    pub mostropubkey: Option<String>,
86    #[arg(short, long)]
87    pub relays: Option<String>,
88    #[arg(short, long)]
89    pub pow: Option<String>,
90    #[arg(short, long)]
91    pub secret: bool,
92}
93
94#[derive(Subcommand, Clone)]
95#[clap(rename_all = "lower")]
96pub enum Commands {
97    /// Requests open orders from Mostro pubkey
98    ListOrders {
99        /// Status of the order
100        #[arg(short, long)]
101        status: Option<String>,
102        /// Currency selected
103        #[arg(short, long)]
104        currency: Option<String>,
105        /// Choose an order kind
106        #[arg(short, long)]
107        kind: Option<String>,
108    },
109    /// Create a new buy/sell order on Mostro
110    NewOrder {
111        /// Choose an order kind
112        #[arg(short, long)]
113        kind: String,
114        /// Sats amount - leave empty for market price
115        #[arg(short, long)]
116        #[clap(default_value_t = 0)]
117        amount: i64,
118        /// Currency selected
119        #[arg(short = 'c', long)]
120        fiat_code: String,
121        /// Fiat amount
122        #[arg(short, long)]
123        #[clap(value_parser=check_fiat_range)]
124        fiat_amount: (i64, Option<i64>),
125        /// Payment method
126        #[arg(short = 'm', long)]
127        payment_method: String,
128        /// Premium on price
129        #[arg(short, long)]
130        #[clap(default_value_t = 0)]
131        #[clap(allow_hyphen_values = true)]
132        premium: i64,
133        /// Invoice string
134        #[arg(short, long)]
135        invoice: Option<String>,
136        /// Expiration time of a pending Order, in days
137        #[arg(short, long)]
138        #[clap(default_value_t = 0)]
139        expiration_days: i64,
140    },
141    /// Take a sell order from a Mostro pubkey
142    TakeSell {
143        /// Order id
144        #[arg(short, long)]
145        order_id: Uuid,
146        /// Invoice string
147        #[arg(short, long)]
148        invoice: Option<String>,
149        /// Amount of fiat to buy
150        #[arg(short, long)]
151        amount: Option<u32>,
152    },
153    /// Take a buy order from a Mostro pubkey
154    TakeBuy {
155        /// Order id
156        #[arg(short, long)]
157        order_id: Uuid,
158        /// Amount of fiat to sell
159        #[arg(short, long)]
160        amount: Option<u32>,
161    },
162    /// Buyer add a new invoice to receive the payment
163    AddInvoice {
164        /// Order id
165        #[arg(short, long)]
166        order_id: Uuid,
167        /// Invoice string
168        #[arg(short, long)]
169        invoice: String,
170    },
171    /// Get the latest direct messages
172    GetDm {
173        /// Since time of the messages in minutes
174        #[arg(short, long)]
175        #[clap(default_value_t = 30)]
176        since: i64,
177        /// If true, get messages from counterparty, otherwise from Mostro
178        #[arg(short, long)]
179        from_user: bool,
180    },
181    /// Get direct messages sent to any trade keys
182    GetDmUser {
183        /// Since time of the messages in minutes
184        #[arg(short, long)]
185        #[clap(default_value_t = 30)]
186        since: i64,
187    },
188    /// Get the latest direct messages for admin
189    GetAdminDm {
190        /// Since time of the messages in minutes
191        #[arg(short, long)]
192        #[clap(default_value_t = 30)]
193        since: i64,
194        /// If true, get messages from counterparty, otherwise from Mostro
195        #[arg(short, long)]
196        from_user: bool,
197    },
198    /// Send direct message to a user
199    SendDm {
200        /// Pubkey of the counterpart
201        #[arg(short, long)]
202        pubkey: String,
203        /// Order id
204        #[arg(short, long)]
205        order_id: Uuid,
206        /// Message to send
207        #[arg(short, long)]
208        message: String,
209    },
210    /// Send gift wrapped direct message to a user
211    DmToUser {
212        /// Pubkey of the recipient
213        #[arg(short, long)]
214        pubkey: String,
215        /// Order id to get ephemeral keys
216        #[arg(short, long)]
217        order_id: Uuid,
218        /// Message to send
219        #[arg(short, long)]
220        message: String,
221    },
222    /// Send fiat sent message to confirm payment to other user
223    FiatSent {
224        /// Order id
225        #[arg(short, long)]
226        order_id: Uuid,
227    },
228    /// Settle the hold invoice and pay to buyer.
229    Release {
230        /// Order id
231        #[arg(short, long)]
232        order_id: Uuid,
233    },
234    /// Cancel a pending order
235    Cancel {
236        /// Order id
237        #[arg(short, long)]
238        order_id: Uuid,
239    },
240    /// Rate counterpart after a successful trade
241    Rate {
242        /// Order id
243        #[arg(short, long)]
244        order_id: Uuid,
245        /// Rating from 1 to 5
246        #[arg(short, long)]
247        rating: u8,
248    },
249    /// Restore session to recover all pending orders and disputes
250    Restore {},
251    /// Start a dispute
252    Dispute {
253        /// Order id
254        #[arg(short, long)]
255        order_id: Uuid,
256    },
257    /// Cancel an order (only admin)
258    AdmCancel {
259        /// Order id
260        #[arg(short, long)]
261        order_id: Uuid,
262    },
263    /// Settle a seller's hold invoice (only admin)
264    AdmSettle {
265        /// Order id
266        #[arg(short, long)]
267        order_id: Uuid,
268    },
269    /// Requests open disputes from Mostro pubkey
270    ListDisputes {},
271    /// Add a new dispute's solver (only admin)
272    AdmAddSolver {
273        /// npubkey
274        #[arg(short, long)]
275        npubkey: String,
276    },
277    /// Admin or solver take a Pending dispute (only admin)
278    AdmTakeDispute {
279        /// Dispute id
280        #[arg(short, long)]
281        dispute_id: Uuid,
282    },
283    /// Send gift wrapped direct message to a user (only admin)
284    AdmSendDm {
285        /// Pubkey of the recipient
286        #[arg(short, long)]
287        pubkey: String,
288        /// Message to send
289        #[arg(short, long)]
290        message: String,
291    },
292    /// Get the conversation key for direct messaging with a user
293    ConversationKey {
294        /// Pubkey of the counterpart
295        #[arg(short, long)]
296        pubkey: String,
297    },
298    /// Get last trade index of user
299    GetLastTradeIndex {},
300    /// Request detailed information for specific orders
301    OrdersInfo {
302        /// Order IDs to request information for
303        #[arg(short, long)]
304        order_ids: Vec<Uuid>,
305    },
306}
307
308fn get_env_var(cli: &Cli) {
309    // Init logger
310    if cli.verbose {
311        set_var("RUST_LOG", "info");
312        pretty_env_logger::init();
313    }
314
315    if let Some(ref mostro_pubkey) = cli.mostropubkey {
316        set_var("MOSTRO_PUBKEY", mostro_pubkey.clone());
317    }
318    let _pubkey = var("MOSTRO_PUBKEY").expect("$MOSTRO_PUBKEY env var needs to be set");
319
320    if let Some(ref relays) = cli.relays {
321        set_var("RELAYS", relays.clone());
322    }
323
324    if let Some(ref pow) = cli.pow {
325        set_var("POW", pow.clone());
326    }
327
328    if cli.secret {
329        set_var("SECRET", "true");
330    }
331}
332
333// Check range with two values value
334fn check_fiat_range(s: &str) -> Result<(i64, Option<i64>)> {
335    if s.contains('-') {
336        // Get values from CLI
337        let values: Vec<&str> = s.split('-').collect();
338
339        // Check if more than two values
340        if values.len() > 2 {
341            return Err(Error::msg("Wrong amount syntax"));
342        };
343
344        // Get ranged command
345        let min = values[0]
346            .parse::<i64>()
347            .map_err(|e| anyhow::anyhow!("Invalid min value: {}", e))?;
348        let max = values[1]
349            .parse::<i64>()
350            .map_err(|e| anyhow::anyhow!("Invalid max value: {}", e))?;
351
352        // Check min below max
353        if min >= max {
354            return Err(Error::msg("Range of values must be 100-200 for example..."));
355        };
356        Ok((min, Some(max)))
357    } else {
358        match s.parse::<i64>() {
359            Ok(s) => Ok((s, None)),
360            Err(e) => Err(e.into()),
361        }
362    }
363}
364
365pub async fn run() -> Result<()> {
366    let cli = Cli::parse();
367
368    let ctx = init_context(&cli).await?;
369
370    if let Some(cmd) = &cli.command {
371        cmd.run(&ctx).await?;
372    }
373
374    Ok(())
375}
376
377async fn init_context(cli: &Cli) -> Result<Context> {
378    // Get environment variables
379    get_env_var(cli);
380
381    // Initialize database pool
382    let pool = connect().await?;
383
384    // Get identity keys
385    let identity_keys = User::get_identity_keys(&pool)
386        .await
387        .map_err(|e| anyhow::anyhow!("Failed to get identity keys: {}", e))?;
388
389    // Get trade keys
390    let (trade_keys, trade_index) = User::get_next_trade_keys(&pool)
391        .await
392        .map_err(|e| anyhow::anyhow!("Failed to get trade keys: {}", e))?;
393
394    // Load private key of admin - only required for admin commands
395    // For regular user commands, this will be None
396    let context_keys = if is_admin_command(&cli.command) {
397        Some(
398            std::env::var("ADMIN_NSEC")
399                .map_err(|e| {
400                    anyhow::anyhow!("ADMIN_NSEC not set (required for admin commands): {}", e)
401                })?
402                .parse::<Keys>()
403                .map_err(|e| anyhow::anyhow!("Failed to parse ADMIN_NSEC: {}", e))?,
404        )
405    } else {
406        None
407    };
408
409    // Resolve Mostro pubkey from env (required for all flows)
410    let mostro_pubkey = PublicKey::from_str(
411        &std::env::var("MOSTRO_PUBKEY")
412            .map_err(|e| anyhow::anyhow!("Failed to get MOSTRO_PUBKEY: {}", e))?,
413    )?;
414
415    // Connect to Nostr relays
416    let client = util::connect_nostr().await?;
417
418    Ok(Context {
419        client,
420        identity_keys,
421        trade_keys,
422        trade_index,
423        pool,
424        context_keys,
425        mostro_pubkey,
426    })
427}
428
429fn is_admin_command(command: &Option<Commands>) -> bool {
430    matches!(
431        command,
432        Some(Commands::AdmCancel { .. })
433            | Some(Commands::AdmSettle { .. })
434            | Some(Commands::AdmAddSolver { .. })
435            | Some(Commands::AdmTakeDispute { .. })
436            | Some(Commands::AdmSendDm { .. })
437            | Some(Commands::GetAdminDm { .. })
438    )
439}
440
441impl Commands {
442    pub async fn run(&self, ctx: &Context) -> Result<()> {
443        match self {
444            // Simple order message commands
445            Commands::FiatSent { order_id }
446            | Commands::Release { order_id }
447            | Commands::Dispute { order_id }
448            | Commands::Cancel { order_id } => {
449                crate::util::run_simple_order_msg(self.clone(), Some(*order_id), ctx).await
450            }
451            // Last trade index commands
452            Commands::GetLastTradeIndex {} => {
453                execute_last_trade_index(&ctx.identity_keys, ctx.mostro_pubkey, ctx).await
454            }
455            // DM commands with pubkey parsing
456            Commands::SendDm {
457                pubkey,
458                order_id,
459                message,
460            } => execute_send_dm(PublicKey::from_str(pubkey)?, ctx, order_id, message).await,
461            Commands::DmToUser {
462                pubkey,
463                order_id,
464                message,
465            } => {
466                execute_dm_to_user(
467                    PublicKey::from_str(pubkey)?,
468                    &ctx.client,
469                    order_id,
470                    message,
471                    &ctx.pool,
472                )
473                .await
474            }
475            Commands::AdmSendDm { pubkey, message } => {
476                execute_adm_send_dm(PublicKey::from_str(pubkey)?, ctx, message).await
477            }
478            Commands::ConversationKey { pubkey } => {
479                execute_conversation_key(&ctx.trade_keys, PublicKey::from_str(pubkey)?).await
480            }
481
482            // Order management commands
483            Commands::ListOrders {
484                status,
485                currency,
486                kind,
487            } => execute_list_orders(kind, currency, status, ctx).await,
488            Commands::NewOrder {
489                kind,
490                fiat_code,
491                amount,
492                fiat_amount,
493                payment_method,
494                premium,
495                invoice,
496                expiration_days,
497            } => {
498                execute_new_order(
499                    kind,
500                    fiat_code,
501                    fiat_amount,
502                    amount,
503                    payment_method,
504                    premium,
505                    invoice,
506                    ctx,
507                    expiration_days,
508                )
509                .await
510            }
511            Commands::TakeSell {
512                order_id,
513                invoice,
514                amount,
515            } => execute_take_order(order_id, Action::TakeSell, invoice, *amount, ctx).await,
516            Commands::TakeBuy { order_id, amount } => {
517                execute_take_order(order_id, Action::TakeBuy, &None, *amount, ctx).await
518            }
519            Commands::AddInvoice { order_id, invoice } => {
520                execute_add_invoice(order_id, invoice, ctx).await
521            }
522            Commands::Rate { order_id, rating } => execute_rate_user(order_id, rating, ctx).await,
523
524            // DM retrieval commands
525            Commands::GetDm { since, from_user } => {
526                execute_get_dm(since, false, from_user, ctx).await
527            }
528            Commands::GetDmUser { since } => execute_get_dm_user(since, ctx).await,
529            Commands::GetAdminDm { since, from_user } => {
530                execute_get_dm(since, true, from_user, ctx).await
531            }
532
533            // Admin commands
534            Commands::ListDisputes {} => execute_list_disputes(ctx).await,
535            Commands::AdmAddSolver { npubkey } => execute_admin_add_solver(npubkey, ctx).await,
536            Commands::AdmSettle { order_id } => execute_admin_settle_dispute(order_id, ctx).await,
537            Commands::AdmCancel { order_id } => execute_admin_cancel_dispute(order_id, ctx).await,
538            Commands::AdmTakeDispute { dispute_id } => execute_take_dispute(dispute_id, ctx).await,
539
540            // Simple commands
541            Commands::Restore {} => {
542                execute_restore(&ctx.identity_keys, ctx.mostro_pubkey, ctx).await
543            }
544            Commands::OrdersInfo { order_ids } => execute_orders_info(order_ids, ctx).await,
545        }
546    }
547}