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 ListOrders {
95 #[arg(short, long)]
97 status: Option<String>,
98 #[arg(short, long)]
100 currency: Option<String>,
101 #[arg(short, long)]
103 kind: Option<String>,
104 },
105 NewOrder {
107 #[arg(short, long)]
109 kind: String,
110 #[arg(short, long)]
112 #[clap(default_value_t = 0)]
113 amount: i64,
114 #[arg(short = 'c', long)]
116 fiat_code: String,
117 #[arg(short, long)]
119 #[clap(value_parser=check_fiat_range)]
120 fiat_amount: (i64, Option<i64>),
121 #[arg(short = 'm', long)]
123 payment_method: String,
124 #[arg(short, long)]
126 #[clap(default_value_t = 0)]
127 #[clap(allow_hyphen_values = true)]
128 premium: i64,
129 #[arg(short, long)]
131 invoice: Option<String>,
132 #[arg(short, long)]
134 #[clap(default_value_t = 0)]
135 expiration_days: i64,
136 },
137 TakeSell {
139 #[arg(short, long)]
141 order_id: Uuid,
142 #[arg(short, long)]
144 invoice: Option<String>,
145 #[arg(short, long)]
147 amount: Option<u32>,
148 },
149 TakeBuy {
151 #[arg(short, long)]
153 order_id: Uuid,
154 #[arg(short, long)]
156 amount: Option<u32>,
157 },
158 AddInvoice {
160 #[arg(short, long)]
162 order_id: Uuid,
163 #[arg(short, long)]
165 invoice: String,
166 },
167 GetDm {
169 #[arg(short, long)]
171 #[clap(default_value_t = 30)]
172 since: i64,
173 #[arg(short, long)]
175 from_user: bool,
176 },
177 GetDmUser {
179 #[arg(short, long)]
181 #[clap(default_value_t = 30)]
182 since: i64,
183 },
184 GetAdminDm {
186 #[arg(short, long)]
188 #[clap(default_value_t = 30)]
189 since: i64,
190 #[arg(short, long)]
192 from_user: bool,
193 },
194 SendDm {
196 #[arg(short, long)]
198 pubkey: String,
199 #[arg(short, long)]
201 order_id: Uuid,
202 #[arg(short, long)]
204 message: String,
205 },
206 DmToUser {
208 #[arg(short, long)]
210 pubkey: String,
211 #[arg(short, long)]
213 order_id: Uuid,
214 #[arg(short, long)]
216 message: String,
217 },
218 FiatSent {
220 #[arg(short, long)]
222 order_id: Uuid,
223 },
224 Release {
226 #[arg(short, long)]
228 order_id: Uuid,
229 },
230 Cancel {
232 #[arg(short, long)]
234 order_id: Uuid,
235 },
236 Rate {
238 #[arg(short, long)]
240 order_id: Uuid,
241 #[arg(short, long)]
243 rating: u8,
244 },
245 Restore {},
247 Dispute {
249 #[arg(short, long)]
251 order_id: Uuid,
252 },
253 AdmCancel {
255 #[arg(short, long)]
257 order_id: Uuid,
258 },
259 AdmSettle {
261 #[arg(short, long)]
263 order_id: Uuid,
264 },
265 AdmListDisputes {},
267 AdmAddSolver {
269 #[arg(short, long)]
271 npubkey: String,
272 },
273 AdmTakeDispute {
275 #[arg(short, long)]
277 dispute_id: Uuid,
278 },
279 AdmSendDm {
281 #[arg(short, long)]
283 pubkey: String,
284 #[arg(short, long)]
286 message: String,
287 },
288 ConversationKey {
290 #[arg(short, long)]
292 pubkey: String,
293 },
294}
295
296fn get_env_var(cli: &Cli) {
297 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
321fn check_fiat_range(s: &str) -> Result<(i64, Option<i64>)> {
323 if s.contains('-') {
324 let values: Vec<&str> = s.split('-').collect();
326
327 if values.len() > 2 {
329 return Err(Error::msg("Wrong amount syntax"));
330 };
331
332 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 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_env_var(cli);
370
371 let pool = connect().await?;
373
374 let identity_keys = User::get_identity_keys(&pool)
376 .await
377 .map_err(|e| anyhow::anyhow!("Failed to get identity keys: {}", e))?;
378
379 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 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 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 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 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 Commands::SendDm {
423 pubkey,
424 order_id,
425 message,
426 } => execute_send_dm(PublicKey::from_str(pubkey)?, ctx, order_id, message).await,
427 Commands::DmToUser {
428 pubkey,
429 order_id,
430 message,
431 } => {
432 execute_dm_to_user(
433 PublicKey::from_str(pubkey)?,
434 &ctx.client,
435 order_id,
436 message,
437 &ctx.pool,
438 )
439 .await
440 }
441 Commands::AdmSendDm { pubkey, message } => {
442 execute_adm_send_dm(PublicKey::from_str(pubkey)?, ctx, message).await
443 }
444 Commands::ConversationKey { pubkey } => {
445 execute_conversation_key(&ctx.trade_keys, PublicKey::from_str(pubkey)?).await
446 }
447
448 Commands::ListOrders {
450 status,
451 currency,
452 kind,
453 } => execute_list_orders(kind, currency, status, ctx).await,
454 Commands::NewOrder {
455 kind,
456 fiat_code,
457 amount,
458 fiat_amount,
459 payment_method,
460 premium,
461 invoice,
462 expiration_days,
463 } => {
464 execute_new_order(
465 kind,
466 fiat_code,
467 fiat_amount,
468 amount,
469 payment_method,
470 premium,
471 invoice,
472 ctx,
473 expiration_days,
474 )
475 .await
476 }
477 Commands::TakeSell {
478 order_id,
479 invoice,
480 amount,
481 } => execute_take_order(order_id, Action::TakeSell, invoice, *amount, ctx).await,
482 Commands::TakeBuy { order_id, amount } => {
483 execute_take_order(order_id, Action::TakeBuy, &None, *amount, ctx).await
484 }
485 Commands::AddInvoice { order_id, invoice } => {
486 execute_add_invoice(order_id, invoice, ctx).await
487 }
488 Commands::Rate { order_id, rating } => execute_rate_user(order_id, rating, ctx).await,
489
490 Commands::GetDm { since, from_user } => {
492 execute_get_dm(Some(since), false, from_user, ctx).await
493 }
494 Commands::GetDmUser { since } => execute_get_dm_user(since, ctx).await,
495 Commands::GetAdminDm { since, from_user } => {
496 execute_get_dm(Some(since), true, from_user, ctx).await
497 }
498
499 Commands::AdmListDisputes {} => execute_list_disputes(ctx).await,
501 Commands::AdmAddSolver { npubkey } => execute_admin_add_solver(npubkey, ctx).await,
502 Commands::AdmSettle { order_id } => execute_admin_settle_dispute(order_id, ctx).await,
503 Commands::AdmCancel { order_id } => execute_admin_cancel_dispute(order_id, ctx).await,
504 Commands::AdmTakeDispute { dispute_id } => execute_take_dispute(dispute_id, ctx).await,
505
506 Commands::Restore {} => {
508 execute_restore(&ctx.identity_keys, ctx.mostro_pubkey, &ctx.client).await
509 }
510 }
511 }
512}