mostro_client/
util.rs

1use crate::nip33::{dispute_from_tags, order_from_tags};
2
3use anyhow::{Error, Result};
4use base64::engine::general_purpose;
5use base64::Engine;
6use dotenvy::var;
7use log::{error, info};
8use mostro_core::dispute::Dispute;
9use mostro_core::message::Message;
10use mostro_core::order::Kind as MostroKind;
11use mostro_core::order::{SmallOrder, Status};
12use mostro_core::NOSTR_REPLACEABLE_EVENT_KIND;
13use nip44::v2::{decrypt_to_bytes, encrypt_to_bytes, ConversationKey};
14use nostr_sdk::prelude::*;
15use std::thread::sleep;
16use std::time::Duration;
17use std::{fs, path::Path};
18
19pub async fn send_dm(
20    client: &Client,
21    identity_keys: Option<&Keys>,
22    trade_keys: &Keys,
23    receiver_pubkey: &PublicKey,
24    payload: String,
25    expiration: Option<Timestamp>,
26    to_user: bool,
27) -> Result<()> {
28    let pow: u8 = var("POW").unwrap_or('0'.to_string()).parse().unwrap();
29    let event = if to_user {
30        // Derive conversation key
31        let ck = ConversationKey::derive(trade_keys.secret_key(), receiver_pubkey);
32        // Encrypt payload
33        let encrypted_content = encrypt_to_bytes(&ck, payload.as_bytes())?;
34        // Encode with base64
35        let b64decoded_content = general_purpose::STANDARD.encode(encrypted_content);
36        // Compose builder
37        EventBuilder::new(Kind::PrivateDirectMessage, b64decoded_content)
38            .pow(pow)
39            .tag(Tag::public_key(*receiver_pubkey))
40            .sign_with_keys(trade_keys)?
41    } else {
42        let identity_keys = identity_keys
43            .ok_or_else(|| Error::msg("identity_keys required when to_user is false"))?;
44
45        let message = Message::from_json(&payload).unwrap();
46        // We sign the message
47        let sig = message.get_inner_message_kind().sign(trade_keys);
48        // We compose the content
49        let content = (message, sig);
50        let content = serde_json::to_string(&content).unwrap();
51        // We create the rumor
52        let rumor = EventBuilder::text_note(content)
53            .pow(pow)
54            .build(trade_keys.public_key());
55        let mut tags: Vec<Tag> = Vec::with_capacity(1 + usize::from(expiration.is_some()));
56
57        if let Some(timestamp) = expiration {
58            tags.push(Tag::expiration(timestamp));
59        }
60        let tags = Tags::new(tags);
61
62        EventBuilder::gift_wrap(identity_keys, receiver_pubkey, rumor, tags).await?
63    };
64
65    info!("Sending event: {event:#?}");
66    client.send_event(event).await?;
67
68    Ok(())
69}
70
71pub async fn connect_nostr() -> Result<Client> {
72    let my_keys = Keys::generate();
73
74    let relays = var("RELAYS").expect("RELAYS is not set");
75    let relays = relays.split(',').collect::<Vec<&str>>();
76    // Create new client
77    let client = Client::new(my_keys);
78    // Add relays
79    for r in relays.into_iter() {
80        client.add_relay(r).await?;
81    }
82    // Connect to relays and keep connection alive
83    client.connect().await;
84
85    Ok(client)
86}
87
88pub async fn send_message_sync(
89    client: &Client,
90    identity_keys: Option<&Keys>,
91    trade_keys: &Keys,
92    receiver_pubkey: PublicKey,
93    message: Message,
94    wait_for_dm: bool,
95    to_user: bool,
96) -> Result<Vec<(Message, u64)>> {
97    let message_json = message.as_json()?;
98    // Send dm to receiver pubkey
99    println!(
100        "SENDING DM with trade keys: {:?}",
101        trade_keys.public_key().to_hex()
102    );
103    send_dm(
104        client,
105        identity_keys,
106        trade_keys,
107        &receiver_pubkey,
108        message_json,
109        None,
110        to_user,
111    )
112    .await?;
113    // FIXME: This is a hack to wait for the DM to be sent
114    sleep(Duration::from_secs(2));
115
116    let dm: Vec<(Message, u64)> = if wait_for_dm {
117        get_direct_messages(client, trade_keys, 15, to_user).await
118    } else {
119        Vec::new()
120    };
121
122    Ok(dm)
123}
124
125pub async fn get_direct_messages(
126    client: &Client,
127    my_key: &Keys,
128    since: i64,
129    from_user: bool,
130) -> Vec<(Message, u64)> {
131    // We use a fake timestamp to thwart time-analysis attacks
132    let fake_since = 2880;
133    let fake_since_time = chrono::Utc::now()
134        .checked_sub_signed(chrono::Duration::minutes(fake_since))
135        .unwrap()
136        .timestamp() as u64;
137
138    let fake_timestamp = Timestamp::from(fake_since_time);
139    let filters = if from_user {
140        let since_time = chrono::Utc::now()
141            .checked_sub_signed(chrono::Duration::minutes(since))
142            .unwrap()
143            .timestamp() as u64;
144        let timestamp = Timestamp::from(since_time);
145        Filter::new()
146            .kind(Kind::PrivateDirectMessage)
147            .pubkey(my_key.public_key())
148            .since(timestamp)
149    } else {
150        Filter::new()
151            .kind(Kind::GiftWrap)
152            .pubkey(my_key.public_key())
153            .since(fake_timestamp)
154    };
155
156    info!("Request events with event kind : {:?} ", filters.kinds);
157
158    let mut direct_messages: Vec<(Message, u64)> = Vec::new();
159
160    if let Ok(mostro_req) = client
161        .fetch_events(vec![filters], Duration::from_secs(15))
162        .await
163    {
164        // Buffer vector for direct messages
165        // Vector for single order id check - maybe multiple relay could send the same order id? Check unique one...
166        let mut id_list = Vec::<EventId>::new();
167
168        for dm in mostro_req.iter() {
169            if !id_list.contains(&dm.id) {
170                id_list.push(dm.id);
171                let created_at: Timestamp;
172                let message: Message;
173                if from_user {
174                    let ck = ConversationKey::derive(my_key.secret_key(), &dm.pubkey);
175                    let b64decoded_content =
176                        match general_purpose::STANDARD.decode(dm.content.as_bytes()) {
177                            Ok(b64decoded_content) => b64decoded_content,
178                            Err(_) => {
179                                continue;
180                            }
181                        };
182                    // Decrypt
183                    let unencrypted_content = decrypt_to_bytes(&ck, &b64decoded_content).unwrap();
184                    let message_str =
185                        String::from_utf8(unencrypted_content).expect("Found invalid UTF-8");
186                    message = Message::from_json(&message_str).unwrap();
187                    created_at = dm.created_at;
188                } else {
189                    let unwrapped_gift = match nip59::extract_rumor(my_key, dm).await {
190                        Ok(u) => u,
191                        Err(_) => {
192                            println!("Error unwrapping gift");
193                            continue;
194                        }
195                    };
196                    let (rumor_message, sig): (Message, nostr_sdk::secp256k1::schnorr::Signature) =
197                        serde_json::from_str(&unwrapped_gift.rumor.content).unwrap();
198                    if !rumor_message
199                        .get_inner_message_kind()
200                        .verify_signature(unwrapped_gift.rumor.pubkey, sig)
201                    {
202                        println!("Signature verification failed");
203                        continue;
204                    }
205                    message = rumor_message;
206                    created_at = unwrapped_gift.rumor.created_at;
207                }
208                // Here we discard messages older than the real since parameter
209                let since_time = chrono::Utc::now()
210                    .checked_sub_signed(chrono::Duration::minutes(30))
211                    .unwrap()
212                    .timestamp() as u64;
213                if created_at.as_u64() < since_time {
214                    continue;
215                }
216                direct_messages.push((message, created_at.as_u64()));
217            }
218        }
219        // Return element sorted by second tuple element ( Timestamp )
220        direct_messages.sort_by(|a, b| a.1.cmp(&b.1));
221    }
222
223    direct_messages
224}
225
226pub async fn get_orders_list(
227    pubkey: PublicKey,
228    status: Status,
229    currency: Option<String>,
230    kind: Option<MostroKind>,
231    client: &Client,
232) -> Result<Vec<SmallOrder>> {
233    let since_time = chrono::Utc::now()
234        .checked_sub_signed(chrono::Duration::days(7))
235        .unwrap()
236        .timestamp() as u64;
237
238    let timestamp = Timestamp::from(since_time);
239
240    let filters = Filter::new()
241        .author(pubkey)
242        .limit(50)
243        .since(timestamp)
244        .custom_tag(SingleLetterTag::lowercase(Alphabet::Z), vec!["order"])
245        .kind(Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND));
246
247    info!(
248        "Request to mostro id : {:?} with event kind : {:?} ",
249        filters.authors, filters.kinds
250    );
251
252    // Extracted Orders List
253    let mut complete_events_list = Vec::<SmallOrder>::new();
254    let mut requested_orders_list = Vec::<SmallOrder>::new();
255
256    // Send all requests to relays
257    if let Ok(mostro_req) = client
258        .fetch_events(vec![filters], Duration::from_secs(15))
259        .await
260    {
261        // Scan events to extract all orders
262        for el in mostro_req.iter() {
263            let order = order_from_tags(el.tags.clone());
264
265            if order.is_err() {
266                error!("{order:?}");
267                continue;
268            }
269            let mut order = order?;
270
271            info!("Found Order id : {:?}", order.id.unwrap());
272
273            if order.id.is_none() {
274                info!("Order ID is none");
275                continue;
276            }
277
278            if order.kind.is_none() {
279                info!("Order kind is none");
280                continue;
281            }
282
283            if order.status.is_none() {
284                info!("Order status is none");
285                continue;
286            }
287
288            // Get created at field from Nostr event
289            order.created_at = Some(el.created_at.as_u64() as i64);
290
291            complete_events_list.push(order.clone());
292
293            if order.status.ne(&Some(status)) {
294                continue;
295            }
296
297            if currency.is_some() && order.fiat_code.ne(&currency.clone().unwrap()) {
298                continue;
299            }
300
301            if kind.is_some() && order.kind.ne(&kind) {
302                continue;
303            }
304            // Add just requested orders requested by filtering
305            requested_orders_list.push(order);
306        }
307    }
308
309    // Order all element ( orders ) received to filter - discard disaligned messages
310    // if an order has an older message with the state we received is discarded for the latest one
311    requested_orders_list.retain(|keep| {
312        !complete_events_list
313            .iter()
314            .any(|x| x.id == keep.id && x.created_at > keep.created_at)
315    });
316    // Sort by id to remove duplicates
317    requested_orders_list.sort_by(|a, b| b.id.cmp(&a.id));
318    requested_orders_list.dedup_by(|a, b| a.id == b.id);
319
320    // Finally sort list by creation time
321    requested_orders_list.sort_by(|a, b| b.created_at.cmp(&a.created_at));
322
323    Ok(requested_orders_list)
324}
325
326pub async fn get_disputes_list(pubkey: PublicKey, client: &Client) -> Result<Vec<Dispute>> {
327    let since_time = chrono::Utc::now()
328        .checked_sub_signed(chrono::Duration::days(7))
329        .unwrap()
330        .timestamp() as u64;
331
332    let timestamp = Timestamp::from(since_time);
333
334    let filter = Filter::new()
335        .author(pubkey)
336        .limit(50)
337        .since(timestamp)
338        .custom_tag(SingleLetterTag::lowercase(Alphabet::Z), vec!["dispute"])
339        .kind(Kind::Custom(NOSTR_REPLACEABLE_EVENT_KIND));
340
341    // Extracted Orders List
342    let mut disputes_list = Vec::<Dispute>::new();
343
344    // Send all requests to relays
345    if let Ok(mostro_req) = client
346        .fetch_events(vec![filter], Duration::from_secs(15))
347        .await
348    {
349        // Scan events to extract all disputes
350        for d in mostro_req.iter() {
351            let dispute = dispute_from_tags(d.tags.clone());
352
353            if dispute.is_err() {
354                error!("{dispute:?}");
355                continue;
356            }
357            let mut dispute = dispute?;
358
359            info!("Found Dispute id : {:?}", dispute.id);
360
361            // Get created at field from Nostr event
362            dispute.created_at = d.created_at.as_u64() as i64;
363            disputes_list.push(dispute);
364        }
365    }
366
367    let buffer_dispute_list = disputes_list.clone();
368    // Order all element ( orders ) received to filter - discard disaligned messages
369    // if an order has an older message with the state we received is discarded for the latest one
370    disputes_list.retain(|keep| {
371        !buffer_dispute_list
372            .iter()
373            .any(|x| x.id == keep.id && x.created_at > keep.created_at)
374    });
375
376    // Sort by id to remove duplicates
377    disputes_list.sort_by(|a, b| b.id.cmp(&a.id));
378    disputes_list.dedup_by(|a, b| a.id == b.id);
379
380    // Finally sort list by creation time
381    disputes_list.sort_by(|a, b| b.created_at.cmp(&a.created_at));
382
383    Ok(disputes_list)
384}
385
386/// Uppercase first letter of a string.
387pub fn uppercase_first(s: &str) -> String {
388    let mut c = s.chars();
389    match c.next() {
390        None => String::new(),
391        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
392    }
393}
394
395pub fn get_mcli_path() -> String {
396    let home_dir = dirs::home_dir().expect("Couldn't get home directory");
397    let mcli_path = format!("{}/.mcli", home_dir.display());
398    if !Path::new(&mcli_path).exists() {
399        fs::create_dir(&mcli_path).expect("Couldn't create mostro-cli directory in HOME");
400        println!("Directory {} created.", mcli_path);
401    }
402
403    mcli_path
404}