mostro_client/
cli.rs

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