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