1pub mod add_invoice;
2pub mod conversation_key;
3pub mod get_dm;
4pub mod list_disputes;
5pub mod list_orders;
6pub mod new_order;
7pub mod rate_user;
8pub mod send_dm;
9pub mod send_msg;
10pub mod take_buy;
11pub mod take_dispute;
12pub mod take_sell;
13
14use crate::cli::add_invoice::execute_add_invoice;
15use crate::cli::conversation_key::execute_conversation_key;
16use crate::cli::get_dm::execute_get_dm;
17use crate::cli::list_disputes::execute_list_disputes;
18use crate::cli::list_orders::execute_list_orders;
19use crate::cli::new_order::execute_new_order;
20use crate::cli::rate_user::execute_rate_user;
21use crate::cli::send_dm::execute_send_dm;
22use crate::cli::send_msg::execute_send_msg;
23use crate::cli::take_buy::execute_take_buy;
24use crate::cli::take_dispute::execute_take_dispute;
25use crate::cli::take_sell::execute_take_sell;
26use crate::db::{connect, User};
27use crate::util;
28
29use anyhow::{Error, Result};
30use clap::{Parser, Subcommand};
31use nostr_sdk::prelude::*;
32use std::{
33 env::{set_var, var},
34 str::FromStr,
35};
36use uuid::Uuid;
37
38#[derive(Parser)]
39#[command(
40 name = "mostro-cli",
41 about = "A simple CLI to use Mostro P2P",
42 author,
43 help_template = "\
44{before-help}{name} 🧌
45
46{about-with-newline}
47{author-with-newline}
48{usage-heading} {usage}
49
50{all-args}{after-help}
51",
52 version
53)]
54#[command(propagate_version = true)]
55#[command(arg_required_else_help(true))]
56pub struct Cli {
57 #[command(subcommand)]
58 pub command: Option<Commands>,
59 #[arg(short, long)]
60 pub verbose: bool,
61 #[arg(short, long)]
62 pub nsec: Option<String>,
63 #[arg(short, long)]
64 pub mostropubkey: Option<String>,
65 #[arg(short, long)]
66 pub relays: Option<String>,
67 #[arg(short, long)]
68 pub pow: Option<String>,
69}
70
71#[derive(Subcommand, Clone)]
72#[clap(rename_all = "lower")]
73pub enum Commands {
74 ListOrders {
76 #[arg(short, long)]
78 status: Option<String>,
79 #[arg(short, long)]
81 currency: Option<String>,
82 #[arg(short, long)]
84 kind: Option<String>,
85 },
86 NewOrder {
88 #[arg(short, long)]
90 kind: String,
91 #[arg(short, long)]
93 #[clap(default_value_t = 0)]
94 amount: i64,
95 #[arg(short = 'c', long)]
97 fiat_code: String,
98 #[arg(short, long)]
100 #[clap(value_parser=check_fiat_range)]
101 fiat_amount: (i64, Option<i64>),
102 #[arg(short = 'm', long)]
104 payment_method: String,
105 #[arg(short, long)]
107 #[clap(default_value_t = 0)]
108 premium: i64,
109 #[arg(short, long)]
111 invoice: Option<String>,
112 #[arg(short, long)]
114 #[clap(default_value_t = 0)]
115 expiration_days: i64,
116 },
117 TakeSell {
119 #[arg(short, long)]
121 order_id: Uuid,
122 #[arg(short, long)]
124 invoice: Option<String>,
125 #[arg(short, long)]
127 amount: Option<u32>,
128 },
129 TakeBuy {
131 #[arg(short, long)]
133 order_id: Uuid,
134 #[arg(short, long)]
136 amount: Option<u32>,
137 },
138 AddInvoice {
140 #[arg(short, long)]
142 order_id: Uuid,
143 #[arg(short, long)]
145 invoice: String,
146 },
147 GetDm {
149 #[arg(short, long)]
151 #[clap(default_value_t = 30)]
152 since: i64,
153 #[arg(short)]
155 from_user: bool,
156 },
157 SendDm {
159 #[arg(short, long)]
161 pubkey: String,
162 #[arg(short, long)]
164 order_id: Uuid,
165 #[arg(short, long)]
167 message: String,
168 },
169 FiatSent {
171 #[arg(short, long)]
173 order_id: Uuid,
174 },
175 Release {
177 #[arg(short, long)]
179 order_id: Uuid,
180 },
181 Cancel {
183 #[arg(short, long)]
185 order_id: Uuid,
186 },
187 Rate {
189 #[arg(short, long)]
191 order_id: Uuid,
192 #[arg(short, long)]
194 rating: u8,
195 },
196 Dispute {
198 #[arg(short, long)]
200 order_id: Uuid,
201 },
202 AdmCancel {
204 #[arg(short, long)]
206 order_id: Uuid,
207 },
208 AdmSettle {
210 #[arg(short, long)]
212 order_id: Uuid,
213 },
214 AdmListDisputes {},
216 AdmAddSolver {
218 #[arg(short, long)]
220 npubkey: String,
221 },
222 AdmTakeDispute {
224 #[arg(short, long)]
226 dispute_id: Uuid,
227 },
228 ConversationKey {
230 #[arg(short, long)]
232 pubkey: String,
233 },
234}
235
236fn check_fiat_range(s: &str) -> Result<(i64, Option<i64>)> {
238 if s.contains('-') {
239 let min: i64;
240 let max: i64;
241
242 let values: Vec<&str> = s.split('-').collect();
244
245 if values.len() > 2 {
247 return Err(Error::msg("Wrong amount syntax"));
248 };
249
250 if let Err(e) = values[0].parse::<i64>() {
252 return Err(e.into());
253 } else {
254 min = values[0].parse().unwrap();
255 }
256
257 if let Err(e) = values[1].parse::<i64>() {
258 return Err(e.into());
259 } else {
260 max = values[1].parse().unwrap();
261 }
262
263 if min >= max {
265 return Err(Error::msg("Range of values must be 100-200 for example..."));
266 };
267 Ok((min, Some(max)))
268 } else {
269 match s.parse::<i64>() {
270 Ok(s) => Ok((s, None)),
271 Err(e) => Err(e.into()),
272 }
273 }
274}
275
276pub async fn run() -> Result<()> {
277 let cli = Cli::parse();
278
279 if cli.verbose {
281 set_var("RUST_LOG", "info");
282 pretty_env_logger::init();
283 }
284
285 if cli.mostropubkey.is_some() {
286 set_var("MOSTRO_PUBKEY", cli.mostropubkey.unwrap());
287 }
288 let pubkey = var("MOSTRO_PUBKEY").expect("$MOSTRO_PUBKEY env var needs to be set");
289
290 if cli.nsec.is_some() {
291 set_var("NSEC_PRIVKEY", cli.nsec.unwrap());
292 }
293
294 if cli.relays.is_some() {
295 set_var("RELAYS", cli.relays.unwrap());
296 }
297
298 if cli.pow.is_some() {
299 set_var("POW", cli.pow.unwrap());
300 }
301
302 let pool = connect().await?;
303 let identity_keys = User::get_identity_keys(&pool)
304 .await
305 .map_err(|e| anyhow::anyhow!("Failed to get identity keys: {}", e))?;
306
307 let (trade_keys, trade_index) = User::get_next_trade_keys(&pool)
308 .await
309 .map_err(|e| anyhow::anyhow!("Failed to get trade keys: {}", e))?;
310
311 let mostro_key = PublicKey::from_str(&pubkey)?;
313
314 let client = util::connect_nostr().await?;
316
317 if let Some(cmd) = cli.command {
318 match &cmd {
319 Commands::ConversationKey { pubkey } => {
320 execute_conversation_key(&trade_keys, PublicKey::from_str(pubkey)?).await?
321 }
322 Commands::ListOrders {
323 status,
324 currency,
325 kind,
326 } => execute_list_orders(kind, currency, status, mostro_key, &client).await?,
327 Commands::TakeSell {
328 order_id,
329 invoice,
330 amount,
331 } => {
332 execute_take_sell(
333 order_id,
334 invoice,
335 *amount,
336 &identity_keys,
337 &trade_keys,
338 trade_index,
339 mostro_key,
340 &client,
341 )
342 .await?
343 }
344 Commands::TakeBuy { order_id, amount } => {
345 execute_take_buy(
346 order_id,
347 *amount,
348 &identity_keys,
349 &trade_keys,
350 trade_index,
351 mostro_key,
352 &client,
353 )
354 .await?
355 }
356 Commands::AddInvoice { order_id, invoice } => {
357 execute_add_invoice(order_id, invoice, &identity_keys, mostro_key, &client).await?
358 }
359 Commands::GetDm { since, from_user } => {
360 execute_get_dm(
361 since,
362 identity_keys,
363 trade_keys,
364 trade_index,
365 mostro_key,
366 &client,
367 *from_user,
368 )
369 .await?
370 }
371 Commands::FiatSent { order_id }
372 | Commands::Release { order_id }
373 | Commands::Dispute { order_id }
374 | Commands::AdmCancel { order_id }
375 | Commands::AdmSettle { order_id }
376 | Commands::Cancel { order_id } => {
377 execute_send_msg(
378 cmd.clone(),
379 Some(*order_id),
380 Some(&identity_keys),
381 mostro_key,
382 &client,
383 None,
384 )
385 .await?
386 }
387 Commands::AdmAddSolver { npubkey } => {
388 execute_send_msg(
389 cmd.clone(),
390 None,
391 Some(&identity_keys),
392 mostro_key,
393 &client,
394 Some(npubkey),
395 )
396 .await?
397 }
398 Commands::NewOrder {
399 kind,
400 fiat_code,
401 amount,
402 fiat_amount,
403 payment_method,
404 premium,
405 invoice,
406 expiration_days,
407 } => {
408 execute_new_order(
409 kind,
410 fiat_code,
411 fiat_amount,
412 amount,
413 payment_method,
414 premium,
415 invoice,
416 &identity_keys,
417 &trade_keys,
418 trade_index,
419 mostro_key,
420 &client,
421 expiration_days,
422 )
423 .await?
424 }
425 Commands::Rate { order_id, rating } => {
426 execute_rate_user(order_id, rating, &identity_keys, mostro_key, &client).await?;
427 }
428 Commands::AdmTakeDispute { dispute_id } => {
429 execute_take_dispute(dispute_id, &identity_keys, &trade_keys, mostro_key, &client)
430 .await?
431 }
432 Commands::AdmListDisputes {} => execute_list_disputes(mostro_key, &client).await?,
433 Commands::SendDm {
434 pubkey,
435 order_id,
436 message,
437 } => {
438 let pubkey = PublicKey::from_str(pubkey)?;
439 execute_send_dm(pubkey, &client, order_id, message).await?
440 }
441 };
442 }
443
444 println!("Bye Bye!");
445
446 Ok(())
447}