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