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 ListOrders {
97 #[arg(short, long)]
99 status: Option<String>,
100 #[arg(short, long)]
102 currency: Option<String>,
103 #[arg(short, long)]
105 kind: Option<String>,
106 },
107 NewOrder {
109 #[arg(short, long)]
111 kind: String,
112 #[arg(short, long)]
114 #[clap(default_value_t = 0)]
115 amount: i64,
116 #[arg(short = 'c', long)]
118 fiat_code: String,
119 #[arg(short, long)]
121 #[clap(value_parser=check_fiat_range)]
122 fiat_amount: (i64, Option<i64>),
123 #[arg(short = 'm', long)]
125 payment_method: String,
126 #[arg(short, long)]
128 #[clap(default_value_t = 0)]
129 #[clap(allow_hyphen_values = true)]
130 premium: i64,
131 #[arg(short, long)]
133 invoice: Option<String>,
134 #[arg(short, long)]
136 #[clap(default_value_t = 0)]
137 expiration_days: i64,
138 },
139 TakeSell {
141 #[arg(short, long)]
143 order_id: Uuid,
144 #[arg(short, long)]
146 invoice: Option<String>,
147 #[arg(short, long)]
149 amount: Option<u32>,
150 },
151 TakeBuy {
153 #[arg(short, long)]
155 order_id: Uuid,
156 #[arg(short, long)]
158 amount: Option<u32>,
159 },
160 AddInvoice {
162 #[arg(short, long)]
164 order_id: Uuid,
165 #[arg(short, long)]
167 invoice: String,
168 },
169 GetDm {
171 #[arg(short, long)]
173 #[clap(default_value_t = 30)]
174 since: i64,
175 #[arg(short, long)]
177 from_user: bool,
178 },
179 GetDmUser {
181 #[arg(short, long)]
183 #[clap(default_value_t = 30)]
184 since: i64,
185 },
186 GetAdminDm {
188 #[arg(short, long)]
190 #[clap(default_value_t = 30)]
191 since: i64,
192 #[arg(short, long)]
194 from_user: bool,
195 },
196 SendDm {
198 #[arg(short, long)]
200 pubkey: String,
201 #[arg(short, long)]
203 order_id: Uuid,
204 #[arg(short, long)]
206 message: String,
207 },
208 DmToUser {
210 #[arg(short, long)]
212 pubkey: String,
213 #[arg(short, long)]
215 order_id: Uuid,
216 #[arg(short, long)]
218 message: String,
219 },
220 FiatSent {
222 #[arg(short, long)]
224 order_id: Uuid,
225 },
226 Release {
228 #[arg(short, long)]
230 order_id: Uuid,
231 },
232 Cancel {
234 #[arg(short, long)]
236 order_id: Uuid,
237 },
238 Rate {
240 #[arg(short, long)]
242 order_id: Uuid,
243 #[arg(short, long)]
245 rating: u8,
246 },
247 Restore {},
249 Dispute {
251 #[arg(short, long)]
253 order_id: Uuid,
254 },
255 AdmCancel {
257 #[arg(short, long)]
259 order_id: Uuid,
260 },
261 AdmSettle {
263 #[arg(short, long)]
265 order_id: Uuid,
266 },
267 AdmListDisputes {},
269 AdmAddSolver {
271 #[arg(short, long)]
273 npubkey: String,
274 },
275 AdmTakeDispute {
277 #[arg(short, long)]
279 dispute_id: Uuid,
280 },
281 AdmSendDm {
283 #[arg(short, long)]
285 pubkey: String,
286 #[arg(short, long)]
288 message: String,
289 },
290 ConversationKey {
292 #[arg(short, long)]
294 pubkey: String,
295 },
296 GetLastTradeIndex {},
298}
299
300fn get_env_var(cli: &Cli) {
301 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
325fn check_fiat_range(s: &str) -> Result<(i64, Option<i64>)> {
327 if s.contains('-') {
328 let values: Vec<&str> = s.split('-').collect();
330
331 if values.len() > 2 {
333 return Err(Error::msg("Wrong amount syntax"));
334 };
335
336 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 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_env_var(cli);
374
375 let pool = connect().await?;
377
378 let identity_keys = User::get_identity_keys(&pool)
380 .await
381 .map_err(|e| anyhow::anyhow!("Failed to get identity keys: {}", e))?;
382
383 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 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 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 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 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 Commands::GetLastTradeIndex {} => {
426 execute_last_trade_index(&ctx.identity_keys, ctx.mostro_pubkey, ctx).await
427 }
428 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 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 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 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 Commands::Restore {} => {
515 execute_restore(&ctx.identity_keys, ctx.mostro_pubkey, &ctx.client).await
516 }
517 }
518 }
519}