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: 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 AdmListDisputes {},
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 = std::env::var("NSEC_PRIVKEY")
396 .map_err(|e| anyhow::anyhow!("NSEC_PRIVKEY not set: {}", e))?
397 .parse::<Keys>()
398 .map_err(|e| anyhow::anyhow!("Failed to get context keys: {}", e))?;
399
400 let mostro_pubkey = PublicKey::from_str(
402 &std::env::var("MOSTRO_PUBKEY")
403 .map_err(|e| anyhow::anyhow!("Failed to get MOSTRO_PUBKEY: {}", e))?,
404 )?;
405
406 let client = util::connect_nostr().await?;
408
409 Ok(Context {
410 client,
411 identity_keys,
412 trade_keys,
413 trade_index,
414 pool,
415 context_keys,
416 mostro_pubkey,
417 })
418}
419
420impl Commands {
421 pub async fn run(&self, ctx: &Context) -> Result<()> {
422 match self {
423 Commands::FiatSent { order_id }
425 | Commands::Release { order_id }
426 | Commands::Dispute { order_id }
427 | Commands::Cancel { order_id } => {
428 crate::util::run_simple_order_msg(self.clone(), Some(*order_id), ctx).await
429 }
430 Commands::GetLastTradeIndex {} => {
432 execute_last_trade_index(&ctx.identity_keys, ctx.mostro_pubkey, ctx).await
433 }
434 Commands::SendDm {
436 pubkey,
437 order_id,
438 message,
439 } => execute_send_dm(PublicKey::from_str(pubkey)?, ctx, order_id, message).await,
440 Commands::DmToUser {
441 pubkey,
442 order_id,
443 message,
444 } => {
445 execute_dm_to_user(
446 PublicKey::from_str(pubkey)?,
447 &ctx.client,
448 order_id,
449 message,
450 &ctx.pool,
451 )
452 .await
453 }
454 Commands::AdmSendDm { pubkey, message } => {
455 execute_adm_send_dm(PublicKey::from_str(pubkey)?, ctx, message).await
456 }
457 Commands::ConversationKey { pubkey } => {
458 execute_conversation_key(&ctx.trade_keys, PublicKey::from_str(pubkey)?).await
459 }
460
461 Commands::ListOrders {
463 status,
464 currency,
465 kind,
466 } => execute_list_orders(kind, currency, status, ctx).await,
467 Commands::NewOrder {
468 kind,
469 fiat_code,
470 amount,
471 fiat_amount,
472 payment_method,
473 premium,
474 invoice,
475 expiration_days,
476 } => {
477 execute_new_order(
478 kind,
479 fiat_code,
480 fiat_amount,
481 amount,
482 payment_method,
483 premium,
484 invoice,
485 ctx,
486 expiration_days,
487 )
488 .await
489 }
490 Commands::TakeSell {
491 order_id,
492 invoice,
493 amount,
494 } => execute_take_order(order_id, Action::TakeSell, invoice, *amount, ctx).await,
495 Commands::TakeBuy { order_id, amount } => {
496 execute_take_order(order_id, Action::TakeBuy, &None, *amount, ctx).await
497 }
498 Commands::AddInvoice { order_id, invoice } => {
499 execute_add_invoice(order_id, invoice, ctx).await
500 }
501 Commands::Rate { order_id, rating } => execute_rate_user(order_id, rating, ctx).await,
502
503 Commands::GetDm { since, from_user } => {
505 execute_get_dm(since, false, from_user, ctx).await
506 }
507 Commands::GetDmUser { since } => execute_get_dm_user(since, ctx).await,
508 Commands::GetAdminDm { since, from_user } => {
509 execute_get_dm(since, true, from_user, ctx).await
510 }
511
512 Commands::AdmListDisputes {} => execute_list_disputes(ctx).await,
514 Commands::AdmAddSolver { npubkey } => execute_admin_add_solver(npubkey, ctx).await,
515 Commands::AdmSettle { order_id } => execute_admin_settle_dispute(order_id, ctx).await,
516 Commands::AdmCancel { order_id } => execute_admin_cancel_dispute(order_id, ctx).await,
517 Commands::AdmTakeDispute { dispute_id } => execute_take_dispute(dispute_id, ctx).await,
518
519 Commands::Restore {} => {
521 execute_restore(&ctx.identity_keys, ctx.mostro_pubkey, ctx).await
522 }
523 Commands::OrdersInfo { order_ids } => execute_orders_info(order_ids, ctx).await,
524 }
525 }
526}