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