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: 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    AdmListDisputes {},
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 user or admin - must be present in .env file
395    let context_keys = std::env::var("NSEC_PRIVKEY")
396        .map_err(|e| anyhow::anyhow!("NSEC_PRIVKEY not set: {}", e))?
397        .parse::<Keys>()
398        .map_err(|e| anyhow::anyhow!("Failed to get context keys: {}", e))?;
399
400    // Resolve Mostro pubkey from env (required for all flows)
401    let mostro_pubkey = PublicKey::from_str(
402        &std::env::var("MOSTRO_PUBKEY")
403            .map_err(|e| anyhow::anyhow!("Failed to get MOSTRO_PUBKEY: {}", e))?,
404    )?;
405
406    // Connect to Nostr relays
407    let client = util::connect_nostr().await?;
408
409    Ok(Context {
410        client,
411        identity_keys,
412        trade_keys,
413        trade_index,
414        pool,
415        context_keys,
416        mostro_pubkey,
417    })
418}
419
420impl Commands {
421    pub async fn run(&self, ctx: &Context) -> Result<()> {
422        match self {
423            // Simple order message commands
424            Commands::FiatSent { order_id }
425            | Commands::Release { order_id }
426            | Commands::Dispute { order_id }
427            | Commands::Cancel { order_id } => {
428                crate::util::run_simple_order_msg(self.clone(), Some(*order_id), ctx).await
429            }
430            // Last trade index commands
431            Commands::GetLastTradeIndex {} => {
432                execute_last_trade_index(&ctx.identity_keys, ctx.mostro_pubkey, ctx).await
433            }
434            // DM commands with pubkey parsing
435            Commands::SendDm {
436                pubkey,
437                order_id,
438                message,
439            } => execute_send_dm(PublicKey::from_str(pubkey)?, ctx, order_id, message).await,
440            Commands::DmToUser {
441                pubkey,
442                order_id,
443                message,
444            } => {
445                execute_dm_to_user(
446                    PublicKey::from_str(pubkey)?,
447                    &ctx.client,
448                    order_id,
449                    message,
450                    &ctx.pool,
451                )
452                .await
453            }
454            Commands::AdmSendDm { pubkey, message } => {
455                execute_adm_send_dm(PublicKey::from_str(pubkey)?, ctx, message).await
456            }
457            Commands::ConversationKey { pubkey } => {
458                execute_conversation_key(&ctx.trade_keys, PublicKey::from_str(pubkey)?).await
459            }
460
461            // Order management commands
462            Commands::ListOrders {
463                status,
464                currency,
465                kind,
466            } => execute_list_orders(kind, currency, status, ctx).await,
467            Commands::NewOrder {
468                kind,
469                fiat_code,
470                amount,
471                fiat_amount,
472                payment_method,
473                premium,
474                invoice,
475                expiration_days,
476            } => {
477                execute_new_order(
478                    kind,
479                    fiat_code,
480                    fiat_amount,
481                    amount,
482                    payment_method,
483                    premium,
484                    invoice,
485                    ctx,
486                    expiration_days,
487                )
488                .await
489            }
490            Commands::TakeSell {
491                order_id,
492                invoice,
493                amount,
494            } => execute_take_order(order_id, Action::TakeSell, invoice, *amount, ctx).await,
495            Commands::TakeBuy { order_id, amount } => {
496                execute_take_order(order_id, Action::TakeBuy, &None, *amount, ctx).await
497            }
498            Commands::AddInvoice { order_id, invoice } => {
499                execute_add_invoice(order_id, invoice, ctx).await
500            }
501            Commands::Rate { order_id, rating } => execute_rate_user(order_id, rating, ctx).await,
502
503            // DM retrieval commands
504            Commands::GetDm { since, from_user } => {
505                execute_get_dm(since, false, from_user, ctx).await
506            }
507            Commands::GetDmUser { since } => execute_get_dm_user(since, ctx).await,
508            Commands::GetAdminDm { since, from_user } => {
509                execute_get_dm(since, true, from_user, ctx).await
510            }
511
512            // Admin commands
513            Commands::AdmListDisputes {} => execute_list_disputes(ctx).await,
514            Commands::AdmAddSolver { npubkey } => execute_admin_add_solver(npubkey, ctx).await,
515            Commands::AdmSettle { order_id } => execute_admin_settle_dispute(order_id, ctx).await,
516            Commands::AdmCancel { order_id } => execute_admin_cancel_dispute(order_id, ctx).await,
517            Commands::AdmTakeDispute { dispute_id } => execute_take_dispute(dispute_id, ctx).await,
518
519            // Simple commands
520            Commands::Restore {} => {
521                execute_restore(&ctx.identity_keys, ctx.mostro_pubkey, ctx).await
522            }
523            Commands::OrdersInfo { order_ids } => execute_orders_info(order_ids, ctx).await,
524        }
525    }
526}