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 ListOrders {
85 #[arg(short, long)]
87 status: Option<String>,
88 #[arg(short, long)]
90 currency: Option<String>,
91 #[arg(short, long)]
93 kind: Option<String>,
94 },
95 NewOrder {
97 #[arg(short, long)]
99 kind: String,
100 #[arg(short, long)]
102 #[clap(default_value_t = 0)]
103 amount: i64,
104 #[arg(short = 'c', long)]
106 fiat_code: String,
107 #[arg(short, long)]
109 #[clap(value_parser=check_fiat_range)]
110 fiat_amount: (i64, Option<i64>),
111 #[arg(short = 'm', long)]
113 payment_method: String,
114 #[arg(short, long)]
116 #[clap(default_value_t = 0)]
117 #[clap(allow_hyphen_values = true)]
118 premium: i64,
119 #[arg(short, long)]
121 invoice: Option<String>,
122 #[arg(short, long)]
124 #[clap(default_value_t = 0)]
125 expiration_days: i64,
126 },
127 TakeSell {
129 #[arg(short, long)]
131 order_id: Uuid,
132 #[arg(short, long)]
134 invoice: Option<String>,
135 #[arg(short, long)]
137 amount: Option<u32>,
138 },
139 TakeBuy {
141 #[arg(short, long)]
143 order_id: Uuid,
144 #[arg(short, long)]
146 amount: Option<u32>,
147 },
148 AddInvoice {
150 #[arg(short, long)]
152 order_id: Uuid,
153 #[arg(short, long)]
155 invoice: String,
156 },
157 GetDm {
159 #[arg(short, long)]
161 #[clap(default_value_t = 30)]
162 since: i64,
163 #[arg(short)]
165 from_user: bool,
166 },
167 GetDmUser {
169 #[arg(short, long)]
171 #[clap(default_value_t = 30)]
172 since: i64,
173 },
174 GetAdminDm {
176 #[arg(short, long)]
178 #[clap(default_value_t = 30)]
179 since: i64,
180 #[arg(short)]
182 from_user: bool,
183 },
184 SendDm {
186 #[arg(short, long)]
188 pubkey: String,
189 #[arg(short, long)]
191 order_id: Uuid,
192 #[arg(short, long)]
194 message: String,
195 },
196 DmToUser {
198 #[arg(short, long)]
200 pubkey: String,
201 #[arg(short, long)]
203 order_id: Uuid,
204 #[arg(short, long)]
206 message: String,
207 },
208 FiatSent {
210 #[arg(short, long)]
212 order_id: Uuid,
213 },
214 Release {
216 #[arg(short, long)]
218 order_id: Uuid,
219 },
220 Cancel {
222 #[arg(short, long)]
224 order_id: Uuid,
225 },
226 Rate {
228 #[arg(short, long)]
230 order_id: Uuid,
231 #[arg(short, long)]
233 rating: u8,
234 },
235 Restore {},
237 Dispute {
239 #[arg(short, long)]
241 order_id: Uuid,
242 },
243 AdmCancel {
245 #[arg(short, long)]
247 order_id: Uuid,
248 },
249 AdmSettle {
251 #[arg(short, long)]
253 order_id: Uuid,
254 },
255 AdmListDisputes {},
257 AdmAddSolver {
259 #[arg(short, long)]
261 npubkey: String,
262 },
263 AdmTakeDispute {
265 #[arg(short, long)]
267 dispute_id: Uuid,
268 },
269 AdmSendDm {
271 #[arg(short, long)]
273 pubkey: String,
274 #[arg(short, long)]
276 message: String,
277 },
278 ConversationKey {
280 #[arg(short, long)]
282 pubkey: String,
283 },
284}
285
286fn check_fiat_range(s: &str) -> Result<(i64, Option<i64>)> {
288 if s.contains('-') {
289 let min: i64;
290 let max: i64;
291
292 let values: Vec<&str> = s.split('-').collect();
294
295 if values.len() > 2 {
297 return Err(Error::msg("Wrong amount syntax"));
298 };
299
300 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 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 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 let mostro_key = PublicKey::from_str(&pubkey)?;
363
364 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}