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: Option<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 ListOrders {
99 #[arg(short, long)]
101 status: Option<String>,
102 #[arg(short, long)]
104 currency: Option<String>,
105 #[arg(short, long)]
107 kind: Option<String>,
108 },
109 NewOrder {
111 #[arg(short, long)]
113 kind: String,
114 #[arg(short, long)]
116 #[clap(default_value_t = 0)]
117 amount: i64,
118 #[arg(short = 'c', long)]
120 fiat_code: String,
121 #[arg(short, long)]
123 #[clap(value_parser=check_fiat_range)]
124 fiat_amount: (i64, Option<i64>),
125 #[arg(short = 'm', long)]
127 payment_method: String,
128 #[arg(short, long)]
130 #[clap(default_value_t = 0)]
131 #[clap(allow_hyphen_values = true)]
132 premium: i64,
133 #[arg(short, long)]
135 invoice: Option<String>,
136 #[arg(short, long)]
138 #[clap(default_value_t = 0)]
139 expiration_days: i64,
140 },
141 TakeSell {
143 #[arg(short, long)]
145 order_id: Uuid,
146 #[arg(short, long)]
148 invoice: Option<String>,
149 #[arg(short, long)]
151 amount: Option<u32>,
152 },
153 TakeBuy {
155 #[arg(short, long)]
157 order_id: Uuid,
158 #[arg(short, long)]
160 amount: Option<u32>,
161 },
162 AddInvoice {
164 #[arg(short, long)]
166 order_id: Uuid,
167 #[arg(short, long)]
169 invoice: String,
170 },
171 GetDm {
173 #[arg(short, long)]
175 #[clap(default_value_t = 30)]
176 since: i64,
177 #[arg(short, long)]
179 from_user: bool,
180 },
181 GetDmUser {
183 #[arg(short, long)]
185 #[clap(default_value_t = 30)]
186 since: i64,
187 },
188 GetAdminDm {
190 #[arg(short, long)]
192 #[clap(default_value_t = 30)]
193 since: i64,
194 #[arg(short, long)]
196 from_user: bool,
197 },
198 SendDm {
200 #[arg(short, long)]
202 pubkey: String,
203 #[arg(short, long)]
205 order_id: Uuid,
206 #[arg(short, long)]
208 message: String,
209 },
210 DmToUser {
212 #[arg(short, long)]
214 pubkey: String,
215 #[arg(short, long)]
217 order_id: Uuid,
218 #[arg(short, long)]
220 message: String,
221 },
222 FiatSent {
224 #[arg(short, long)]
226 order_id: Uuid,
227 },
228 Release {
230 #[arg(short, long)]
232 order_id: Uuid,
233 },
234 Cancel {
236 #[arg(short, long)]
238 order_id: Uuid,
239 },
240 Rate {
242 #[arg(short, long)]
244 order_id: Uuid,
245 #[arg(short, long)]
247 rating: u8,
248 },
249 Restore {},
251 Dispute {
253 #[arg(short, long)]
255 order_id: Uuid,
256 },
257 AdmCancel {
259 #[arg(short, long)]
261 order_id: Uuid,
262 },
263 AdmSettle {
265 #[arg(short, long)]
267 order_id: Uuid,
268 },
269 ListDisputes {},
271 AdmAddSolver {
273 #[arg(short, long)]
275 npubkey: String,
276 },
277 AdmTakeDispute {
279 #[arg(short, long)]
281 dispute_id: Uuid,
282 },
283 AdmSendDm {
285 #[arg(short, long)]
287 pubkey: String,
288 #[arg(short, long)]
290 message: String,
291 },
292 ConversationKey {
294 #[arg(short, long)]
296 pubkey: String,
297 },
298 GetLastTradeIndex {},
300 OrdersInfo {
302 #[arg(short, long)]
304 order_ids: Vec<Uuid>,
305 },
306}
307
308fn get_env_var(cli: &Cli) {
309 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
333fn check_fiat_range(s: &str) -> Result<(i64, Option<i64>)> {
335 if s.contains('-') {
336 let values: Vec<&str> = s.split('-').collect();
338
339 if values.len() > 2 {
341 return Err(Error::msg("Wrong amount syntax"));
342 };
343
344 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 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_env_var(cli);
380
381 let pool = connect().await?;
383
384 let identity_keys = User::get_identity_keys(&pool)
386 .await
387 .map_err(|e| anyhow::anyhow!("Failed to get identity keys: {}", e))?;
388
389 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 let context_keys = if is_admin_command(&cli.command) {
397 Some(
398 std::env::var("ADMIN_NSEC")
399 .map_err(|e| {
400 anyhow::anyhow!("ADMIN_NSEC not set (required for admin commands): {}", e)
401 })?
402 .parse::<Keys>()
403 .map_err(|e| anyhow::anyhow!("Failed to parse ADMIN_NSEC: {}", e))?,
404 )
405 } else {
406 None
407 };
408
409 let mostro_pubkey = PublicKey::from_str(
411 &std::env::var("MOSTRO_PUBKEY")
412 .map_err(|e| anyhow::anyhow!("Failed to get MOSTRO_PUBKEY: {}", e))?,
413 )?;
414
415 let client = util::connect_nostr().await?;
417
418 Ok(Context {
419 client,
420 identity_keys,
421 trade_keys,
422 trade_index,
423 pool,
424 context_keys,
425 mostro_pubkey,
426 })
427}
428
429fn is_admin_command(command: &Option<Commands>) -> bool {
430 matches!(
431 command,
432 Some(Commands::AdmCancel { .. })
433 | Some(Commands::AdmSettle { .. })
434 | Some(Commands::AdmAddSolver { .. })
435 | Some(Commands::AdmTakeDispute { .. })
436 | Some(Commands::AdmSendDm { .. })
437 | Some(Commands::GetAdminDm { .. })
438 )
439}
440
441impl Commands {
442 pub async fn run(&self, ctx: &Context) -> Result<()> {
443 match self {
444 Commands::FiatSent { order_id }
446 | Commands::Release { order_id }
447 | Commands::Dispute { order_id }
448 | Commands::Cancel { order_id } => {
449 crate::util::run_simple_order_msg(self.clone(), Some(*order_id), ctx).await
450 }
451 Commands::GetLastTradeIndex {} => {
453 execute_last_trade_index(&ctx.identity_keys, ctx.mostro_pubkey, ctx).await
454 }
455 Commands::SendDm {
457 pubkey,
458 order_id,
459 message,
460 } => execute_send_dm(PublicKey::from_str(pubkey)?, ctx, order_id, message).await,
461 Commands::DmToUser {
462 pubkey,
463 order_id,
464 message,
465 } => {
466 execute_dm_to_user(
467 PublicKey::from_str(pubkey)?,
468 &ctx.client,
469 order_id,
470 message,
471 &ctx.pool,
472 )
473 .await
474 }
475 Commands::AdmSendDm { pubkey, message } => {
476 execute_adm_send_dm(PublicKey::from_str(pubkey)?, ctx, message).await
477 }
478 Commands::ConversationKey { pubkey } => {
479 execute_conversation_key(&ctx.trade_keys, PublicKey::from_str(pubkey)?).await
480 }
481
482 Commands::ListOrders {
484 status,
485 currency,
486 kind,
487 } => execute_list_orders(kind, currency, status, ctx).await,
488 Commands::NewOrder {
489 kind,
490 fiat_code,
491 amount,
492 fiat_amount,
493 payment_method,
494 premium,
495 invoice,
496 expiration_days,
497 } => {
498 execute_new_order(
499 kind,
500 fiat_code,
501 fiat_amount,
502 amount,
503 payment_method,
504 premium,
505 invoice,
506 ctx,
507 expiration_days,
508 )
509 .await
510 }
511 Commands::TakeSell {
512 order_id,
513 invoice,
514 amount,
515 } => execute_take_order(order_id, Action::TakeSell, invoice, *amount, ctx).await,
516 Commands::TakeBuy { order_id, amount } => {
517 execute_take_order(order_id, Action::TakeBuy, &None, *amount, ctx).await
518 }
519 Commands::AddInvoice { order_id, invoice } => {
520 execute_add_invoice(order_id, invoice, ctx).await
521 }
522 Commands::Rate { order_id, rating } => execute_rate_user(order_id, rating, ctx).await,
523
524 Commands::GetDm { since, from_user } => {
526 execute_get_dm(since, false, from_user, ctx).await
527 }
528 Commands::GetDmUser { since } => execute_get_dm_user(since, ctx).await,
529 Commands::GetAdminDm { since, from_user } => {
530 execute_get_dm(since, true, from_user, ctx).await
531 }
532
533 Commands::ListDisputes {} => execute_list_disputes(ctx).await,
535 Commands::AdmAddSolver { npubkey } => execute_admin_add_solver(npubkey, ctx).await,
536 Commands::AdmSettle { order_id } => execute_admin_settle_dispute(order_id, ctx).await,
537 Commands::AdmCancel { order_id } => execute_admin_cancel_dispute(order_id, ctx).await,
538 Commands::AdmTakeDispute { dispute_id } => execute_take_dispute(dispute_id, ctx).await,
539
540 Commands::Restore {} => {
542 execute_restore(&ctx.identity_keys, ctx.mostro_pubkey, ctx).await
543 }
544 Commands::OrdersInfo { order_ids } => execute_orders_info(order_ids, ctx).await,
545 }
546 }
547}