mostro_client/
db.rs

1use crate::util::get_mcli_path;
2use anyhow::Result;
3use mostro_core::order::SmallOrder;
4use mostro_core::NOSTR_REPLACEABLE_EVENT_KIND;
5use nip06::FromMnemonic;
6use nostr_sdk::prelude::*;
7use sqlx::pool::Pool;
8use sqlx::Sqlite;
9use sqlx::SqlitePool;
10use std::fs::File;
11use std::path::Path;
12
13pub async fn connect() -> Result<Pool<Sqlite>> {
14    let mcli_dir = get_mcli_path();
15    let mcli_db_path = format!("{}/mcli.db", mcli_dir);
16    let db_url = format!("sqlite://{}", mcli_db_path);
17    let pool: Pool<Sqlite>;
18    if !Path::exists(Path::new(&mcli_db_path)) {
19        if let Err(res) = File::create(&mcli_db_path) {
20            println!("Error in creating db file: {}", res);
21            return Err(res.into());
22        }
23        pool = SqlitePool::connect(&db_url).await?;
24        println!("Creating database file with orders table...");
25        sqlx::query(
26            r#"
27          CREATE TABLE IF NOT EXISTS orders (
28              id TEXT PRIMARY KEY,
29              kind TEXT NOT NULL,
30              status TEXT NOT NULL,
31              amount INTEGER NOT NULL,
32              min_amount INTEGER,
33              max_amount INTEGER,
34              fiat_code TEXT NOT NULL,
35              fiat_amount INTEGER NOT NULL,
36              payment_method TEXT NOT NULL,
37              premium INTEGER NOT NULL,
38              trade_keys TEXT,
39              counterparty_pubkey TEXT,
40              is_mine BOOLEAN,
41              buyer_invoice TEXT,
42              buyer_token INTEGER,
43              seller_token INTEGER,
44              request_id INTEGER,
45              created_at INTEGER,
46              expires_at INTEGER
47          );
48          CREATE TABLE IF NOT EXISTS users (
49              i0_pubkey char(64) PRIMARY KEY,
50              mnemonic TEXT,
51              last_trade_index INTEGER,
52              created_at INTEGER
53          );
54          "#,
55        )
56        .execute(&pool)
57        .await?;
58
59        let mnemonic = match Mnemonic::generate(12) {
60            Ok(m) => m.to_string(),
61            Err(e) => {
62                println!("Error generating mnemonic: {}", e);
63                return Err(e.into());
64            }
65        };
66        let user = User::new(mnemonic, &pool).await?;
67        println!("User created with pubkey: {}", user.i0_pubkey);
68    } else {
69        pool = SqlitePool::connect(&db_url).await?;
70    }
71
72    Ok(pool)
73}
74
75#[derive(Debug, Default, Clone, sqlx::FromRow)]
76pub struct User {
77    /// The user's ID is the identity pubkey
78    pub i0_pubkey: String,
79    pub mnemonic: String,
80    pub last_trade_index: Option<i64>,
81    pub created_at: i64,
82}
83
84impl User {
85    pub async fn new(mnemonic: String, pool: &SqlitePool) -> Result<Self> {
86        let mut user = User::default();
87        let account = NOSTR_REPLACEABLE_EVENT_KIND as u32;
88        let i0_keys =
89            Keys::from_mnemonic_advanced(&mnemonic, None, Some(account), Some(0), Some(0))?;
90        user.i0_pubkey = i0_keys.public_key().to_string();
91        user.created_at = chrono::Utc::now().timestamp();
92        user.mnemonic = mnemonic;
93        sqlx::query(
94            r#"
95                  INSERT INTO users (i0_pubkey, mnemonic, created_at)
96                  VALUES (?, ?, ?)
97                "#,
98        )
99        .bind(&user.i0_pubkey)
100        .bind(&user.mnemonic)
101        .bind(user.created_at)
102        .execute(pool)
103        .await?;
104
105        Ok(user)
106    }
107    // Chainable setters
108    pub fn set_mnemonic(&mut self, mnemonic: String) -> &mut Self {
109        self.mnemonic = mnemonic;
110        self
111    }
112
113    pub fn set_last_trade_index(&mut self, last_trade_index: i64) -> &mut Self {
114        self.last_trade_index = Some(last_trade_index);
115        self
116    }
117
118    // Applying changes to the database
119    pub async fn save(&self, pool: &SqlitePool) -> Result<()> {
120        sqlx::query(
121            r#"
122              UPDATE users 
123              SET mnemonic = ?, last_trade_index = ?
124              WHERE i0_pubkey = ?
125              "#,
126        )
127        .bind(&self.mnemonic)
128        .bind(self.last_trade_index)
129        .bind(&self.i0_pubkey)
130        .execute(pool)
131        .await?;
132
133        println!(
134            "User with i0 pubkey {} updated in the database.",
135            self.i0_pubkey
136        );
137
138        Ok(())
139    }
140
141    pub async fn get(pool: &SqlitePool) -> Result<User> {
142        let user = sqlx::query_as::<_, User>(
143            r#"
144            SELECT i0_pubkey, mnemonic, last_trade_index, created_at
145            FROM users
146            LIMIT 1
147            "#,
148        )
149        .fetch_one(pool)
150        .await?;
151
152        Ok(user)
153    }
154
155    pub async fn get_next_trade_index(pool: SqlitePool) -> Result<i64> {
156        let user = User::get(&pool).await?;
157        match user.last_trade_index {
158            Some(index) => Ok(index + 1),
159            None => Ok(1),
160        }
161    }
162
163    pub async fn get_identity_keys(pool: &SqlitePool) -> Result<Keys> {
164        let user = User::get(pool).await?;
165        let account = NOSTR_REPLACEABLE_EVENT_KIND as u32;
166        let keys =
167            Keys::from_mnemonic_advanced(&user.mnemonic, None, Some(account), Some(0), Some(0))?;
168
169        Ok(keys)
170    }
171
172    pub async fn get_next_trade_keys(pool: &SqlitePool) -> Result<(Keys, i64)> {
173        let trade_index = User::get_next_trade_index(pool.clone()).await?;
174
175        let user = User::get(pool).await?;
176        let account = NOSTR_REPLACEABLE_EVENT_KIND as u32;
177        match trade_index.try_into() {
178            Ok(index) => {
179                let keys = Keys::from_mnemonic_advanced(
180                    &user.mnemonic,
181                    None,
182                    Some(account),
183                    Some(0),
184                    Some(index),
185                )?;
186                Ok((keys, trade_index))
187            }
188            Err(e) => {
189                println!("Error: {}", e);
190                Err(e.into())
191            }
192        }
193    }
194
195    pub async fn get_trade_keys(pool: &SqlitePool, index: i64) -> Result<Keys> {
196        if index < 0 {
197            return Err(anyhow::anyhow!("Trade index cannot be negative"));
198        }
199        let user = User::get(pool).await?;
200        let account = NOSTR_REPLACEABLE_EVENT_KIND as u32;
201        let keys = Keys::from_mnemonic_advanced(
202            &user.mnemonic,
203            None,
204            Some(account),
205            Some(0),
206            Some(index as u32),
207        )?;
208        Ok(keys)
209    }
210}
211
212#[derive(Debug, Default, Clone, sqlx::FromRow)]
213pub struct Order {
214    pub id: Option<String>,
215    pub kind: Option<String>,
216    pub status: Option<String>,
217    pub amount: i64,
218    pub fiat_code: String,
219    pub min_amount: Option<i64>,
220    pub max_amount: Option<i64>,
221    pub fiat_amount: i64,
222    pub payment_method: String,
223    pub premium: i64,
224    pub trade_keys: Option<String>,
225    pub counterparty_pubkey: Option<String>,
226    pub is_mine: Option<bool>,
227    pub buyer_invoice: Option<String>,
228    pub buyer_token: Option<u16>,
229    pub seller_token: Option<u16>,
230    pub request_id: Option<i64>,
231    pub created_at: Option<i64>,
232    pub expires_at: Option<i64>,
233}
234
235impl Order {
236    pub async fn new(
237        pool: &SqlitePool,
238        order: SmallOrder,
239        trade_keys: &Keys,
240        request_id: Option<i64>,
241    ) -> Result<Self> {
242        let trade_keys_hex = trade_keys.secret_key().to_secret_hex();
243        let id = match order.id {
244            Some(id) => id.to_string(),
245            None => uuid::Uuid::new_v4().to_string(),
246        };
247        let order = Order {
248            id: Some(id),
249            kind: order.kind.as_ref().map(|k| k.to_string()),
250            status: order.status.as_ref().map(|s| s.to_string()),
251            amount: order.amount,
252            fiat_code: order.fiat_code,
253            min_amount: order.min_amount,
254            max_amount: order.max_amount,
255            fiat_amount: order.fiat_amount,
256            payment_method: order.payment_method,
257            premium: order.premium,
258            trade_keys: Some(trade_keys_hex),
259            counterparty_pubkey: None,
260            is_mine: Some(true),
261            buyer_invoice: None,
262            buyer_token: None,
263            seller_token: None,
264            request_id,
265            created_at: Some(chrono::Utc::now().timestamp()),
266            expires_at: None,
267        };
268
269        sqlx::query(
270            r#"
271                  INSERT INTO orders (id, kind, status, amount, min_amount, max_amount,
272                  fiat_code, fiat_amount, payment_method, premium, trade_keys,
273                  counterparty_pubkey, is_mine, buyer_invoice, buyer_token, seller_token,
274                  request_id, created_at, expires_at)
275                  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
276                "#,
277        )
278        .bind(&order.id)
279        .bind(&order.kind)
280        .bind(&order.status)
281        .bind(order.amount)
282        .bind(order.min_amount)
283        .bind(order.max_amount)
284        .bind(&order.fiat_code)
285        .bind(order.fiat_amount)
286        .bind(&order.payment_method)
287        .bind(order.premium)
288        .bind(&order.trade_keys)
289        .bind(&order.counterparty_pubkey)
290        .bind(order.is_mine)
291        .bind(&order.buyer_invoice)
292        .bind(order.buyer_token)
293        .bind(order.seller_token)
294        .bind(order.request_id)
295        .bind(order.created_at)
296        .bind(order.expires_at)
297        .execute(pool)
298        .await?;
299
300        Ok(order)
301    }
302
303    // Setters encadenables
304    pub fn set_kind(&mut self, kind: String) -> &mut Self {
305        self.kind = Some(kind);
306        self
307    }
308
309    pub fn set_status(&mut self, status: String) -> &mut Self {
310        self.status = Some(status);
311        self
312    }
313
314    pub fn set_amount(&mut self, amount: i64) -> &mut Self {
315        self.amount = amount;
316        self
317    }
318
319    pub fn set_fiat_code(&mut self, fiat_code: String) -> &mut Self {
320        self.fiat_code = fiat_code;
321        self
322    }
323
324    pub fn set_min_amount(&mut self, min_amount: i64) -> &mut Self {
325        self.min_amount = Some(min_amount);
326        self
327    }
328
329    pub fn set_max_amount(&mut self, max_amount: i64) -> &mut Self {
330        self.max_amount = Some(max_amount);
331        self
332    }
333
334    pub fn set_fiat_amount(&mut self, fiat_amount: i64) -> &mut Self {
335        self.fiat_amount = fiat_amount;
336        self
337    }
338
339    pub fn set_payment_method(&mut self, payment_method: String) -> &mut Self {
340        self.payment_method = payment_method;
341        self
342    }
343
344    pub fn set_premium(&mut self, premium: i64) -> &mut Self {
345        self.premium = premium;
346        self
347    }
348
349    pub fn set_counterparty_pubkey(&mut self, counterparty_pubkey: String) -> &mut Self {
350        self.counterparty_pubkey = Some(counterparty_pubkey);
351        self
352    }
353
354    pub fn set_trade_keys(&mut self, trade_keys: String) -> &mut Self {
355        self.trade_keys = Some(trade_keys);
356        self
357    }
358
359    pub fn set_is_mine(&mut self, is_mine: bool) -> &mut Self {
360        self.is_mine = Some(is_mine);
361        self
362    }
363
364    // Applying changes to the database
365    pub async fn save(&self, pool: &SqlitePool) -> Result<()> {
366        // Validation if an identity document is present
367        if let Some(ref id) = self.id {
368            sqlx::query(
369                r#"
370              UPDATE orders 
371              SET kind = ?, status = ?, amount = ?, fiat_code = ?, min_amount = ?, max_amount = ?, 
372                  fiat_amount = ?, payment_method = ?, premium = ?, trade_keys = ?, counterparty_pubkey = ?,
373                  is_mine = ?, buyer_invoice = ?, created_at = ?, expires_at = ?, buyer_token = ?,
374                seller_token = ?
375              WHERE id = ?
376              "#,
377            )
378            .bind(&self.kind)
379            .bind(&self.status)
380            .bind(self.amount)
381            .bind(&self.fiat_code)
382            .bind(self.min_amount)
383            .bind(self.max_amount)
384            .bind(self.fiat_amount)
385            .bind(&self.payment_method)
386            .bind(self.premium)
387            .bind(&self.trade_keys)
388            .bind(&self.counterparty_pubkey)
389            .bind(self.is_mine)
390            .bind(&self.buyer_invoice)
391            .bind(self.created_at)
392            .bind(self.expires_at)
393            .bind(self.buyer_token)
394            .bind(self.seller_token)
395            .bind(id)
396            .execute(pool)
397            .await?;
398
399            println!("Order with id {} updated in the database.", id);
400        } else {
401            return Err(anyhow::anyhow!("Order must have an ID to be updated."));
402        }
403
404        Ok(())
405    }
406
407    pub async fn save_new_id(
408        pool: &SqlitePool,
409        id: String,
410        new_id: String,
411    ) -> anyhow::Result<bool> {
412        let rows_affected = sqlx::query(
413            r#"
414          UPDATE orders
415          SET id = ?
416          WHERE id = ?
417        "#,
418        )
419        .bind(&new_id)
420        .bind(&id)
421        .execute(pool)
422        .await?
423        .rows_affected();
424
425        Ok(rows_affected > 0)
426    }
427
428    pub async fn get_by_id(pool: &SqlitePool, id: &str) -> Result<Order> {
429        let order = sqlx::query_as::<_, Order>(
430            r#"
431            SELECT * FROM orders WHERE id = ?
432            LIMIT 1
433            "#,
434        )
435        .bind(id)
436        .fetch_one(pool)
437        .await?;
438
439        if order.id.is_none() {
440            return Err(anyhow::anyhow!("Order not found"));
441        }
442
443        Ok(order)
444    }
445
446    pub async fn get_all(pool: &SqlitePool) -> Result<Vec<Order>> {
447        let orders = sqlx::query_as::<_, Order>(r#"SELECT * FROM orders"#)
448            .fetch_all(pool)
449            .await?;
450        Ok(orders)
451    }
452}