mostro_client/cli/
new_order.rs

1use crate::cli::Context;
2use crate::parser::orders::print_order_preview;
3use crate::util::{send_dm, uppercase_first, wait_for_dm};
4use anyhow::Result;
5use mostro_core::prelude::*;
6use nostr_sdk::prelude::*;
7use std::collections::HashMap;
8use std::io::{stdin, stdout, BufRead, Write};
9use std::process;
10use std::str::FromStr;
11use uuid::Uuid;
12
13pub type FiatNames = HashMap<String, String>;
14
15#[allow(clippy::too_many_arguments)]
16pub async fn execute_new_order(
17    kind: &str,
18    fiat_code: &str,
19    fiat_amount: &(i64, Option<i64>),
20    amount: &i64,
21    payment_method: &str,
22    premium: &i64,
23    invoice: &Option<String>,
24    ctx: &Context,
25    expiration_days: &i64,
26) -> Result<()> {
27    // Uppercase currency
28    let fiat_code = fiat_code.to_uppercase();
29    // Check if fiat currency selected is available on Yadio and eventually force user to set amount
30    // this is in the case of crypto <--> crypto offer for example
31    if *amount == 0 {
32        // Get Fiat list
33        let api_req_string = "https://api.yadio.io/currencies".to_string();
34        let fiat_list_check = reqwest::get(api_req_string)
35            .await?
36            .json::<FiatNames>()
37            .await?
38            .contains_key(&fiat_code);
39        if !fiat_list_check {
40            println!("{} is not present in the fiat market, please specify an amount with -a flag to fix the rate", fiat_code);
41            process::exit(0);
42        }
43    }
44    let kind = uppercase_first(kind);
45    // New check against strings
46    let kind_checked = mostro_core::order::Kind::from_str(&kind)
47        .map_err(|_| anyhow::anyhow!("Invalid order kind"))?;
48    let expires_at = match *expiration_days {
49        0 => None,
50        _ => {
51            let now = chrono::Utc::now();
52            let expires_at = now + chrono::Duration::days(*expiration_days);
53            Some(expires_at.timestamp())
54        }
55    };
56
57    // Get the type of neworder
58    // if both tuple field are valid than it's a range order
59    // otherwise use just fiat amount value as before
60    let amt = if fiat_amount.1.is_some() {
61        (0, Some(fiat_amount.0), fiat_amount.1)
62    } else {
63        (fiat_amount.0, None, None)
64    };
65
66    let small_order = SmallOrder::new(
67        None,
68        Some(kind_checked),
69        Some(Status::Pending),
70        *amount,
71        fiat_code.clone(),
72        amt.1,
73        amt.2,
74        amt.0,
75        payment_method.to_owned(),
76        *premium,
77        None,
78        None,
79        invoice.as_ref().to_owned().cloned(),
80        Some(0),
81        expires_at,
82    );
83
84    // Create new order for mostro
85    let order_content = Payload::Order(small_order.clone());
86
87    // Print order preview
88    let ord_preview = print_order_preview(order_content.clone())
89        .map_err(|e| anyhow::anyhow!("Failed to generate order preview: {}", e))?;
90    println!("{ord_preview}");
91    let mut user_input = String::new();
92    let _input = stdin();
93    print!("Check your order! Is it correct? (Y/n) > ");
94    stdout().flush()?;
95
96    let mut answer = stdin().lock();
97    answer.read_line(&mut user_input)?;
98
99    match user_input.to_lowercase().as_str().trim_end() {
100        "y" | "" => {}
101        "n" => {
102            println!("Ok you have cancelled the order, create another one please");
103            process::exit(0);
104        }
105        &_ => {
106            println!("Can't get what you're sayin!");
107            process::exit(0);
108        }
109    };
110    let request_id = Uuid::new_v4().as_u128() as u64;
111    // Create NewOrder message
112    let message = Message::new_order(
113        None,
114        Some(request_id),
115        Some(ctx.trade_index),
116        Action::NewOrder,
117        Some(order_content),
118    );
119
120    // Send dm to receiver pubkey
121    println!(
122        "SENDING DM with trade keys: {:?}",
123        ctx.trade_keys.public_key().to_hex()
124    );
125
126    // Serialize the message
127    let message_json = message
128        .as_json()
129        .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?;
130
131    // Clone the keys and client for the async call
132    let identity_keys_clone = ctx.identity_keys.clone();
133    let trade_keys_clone = ctx.trade_keys.clone();
134    let client_clone = ctx.client.clone();
135    let mostro_pubkey_clone = ctx.mostro_pubkey;
136
137    // Subscribe to gift wrap events - ONLY NEW ONES WITH LIMIT 0
138    let subscription = Filter::new()
139        .pubkey(ctx.trade_keys.public_key())
140        .kind(nostr_sdk::Kind::GiftWrap)
141        .limit(0);
142
143    let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEvents(1));
144
145    ctx.client.subscribe(subscription, Some(opts)).await?;
146
147    // Spawn a new task to send the DM
148    // This is so we can wait for the gift wrap event in the main thread
149    tokio::spawn(async move {
150        let _ = send_dm(
151            &client_clone,
152            Some(&identity_keys_clone),
153            &trade_keys_clone,
154            &mostro_pubkey_clone,
155            message_json,
156            None,
157            false,
158        )
159        .await;
160    });
161
162    // Wait for the DM to be sent from mostro
163    wait_for_dm(
164        &ctx.client,
165        &ctx.trade_keys,
166        request_id,
167        Some(ctx.trade_index),
168        None,
169        &ctx.pool,
170    )
171    .await?;
172
173    Ok(())
174}