mostro_client/
db.rs

1use crate::util::get_mcli_path;
2use anyhow::Result;
3use mostro_core::prelude::*;
4use nip06::FromMnemonic;
5use nostr_sdk::prelude::*;
6use sqlx::pool::Pool;
7use sqlx::Sqlite;
8use sqlx::SqlitePool;
9use std::fs::File;
10use std::path::Path;
11
12pub async fn connect() -> Result<Pool<Sqlite>> {
13    let mcli_dir = get_mcli_path();
14    let mcli_db_path = format!("{}/mcli.db", mcli_dir);
15    let db_url = format!("sqlite://{}", mcli_db_path);
16    let pool: Pool<Sqlite>;
17    if !Path::exists(Path::new(&mcli_db_path)) {
18        if let Err(res) = File::create(&mcli_db_path) {
19            println!("Error in creating db file: {}", res);
20            return Err(res.into());
21        }
22        pool = SqlitePool::connect(&db_url).await?;
23        println!("Creating database file with orders table...");
24        sqlx::query(
25            r#"
26          CREATE TABLE IF NOT EXISTS orders (
27              id TEXT PRIMARY KEY,
28              kind TEXT NOT NULL,
29              status TEXT NOT NULL,
30              amount INTEGER NOT NULL,
31              min_amount INTEGER,
32              max_amount INTEGER,
33              fiat_code TEXT NOT NULL,
34              fiat_amount INTEGER NOT NULL,
35              payment_method TEXT NOT NULL,
36              premium INTEGER NOT NULL,
37              trade_keys TEXT,
38              counterparty_pubkey TEXT,
39              is_mine BOOLEAN,
40              buyer_invoice TEXT,
41              request_id INTEGER,
42              created_at INTEGER,
43              expires_at INTEGER
44          );
45          CREATE TABLE IF NOT EXISTS users (
46              i0_pubkey char(64) PRIMARY KEY,
47              mnemonic TEXT,
48              last_trade_index INTEGER,
49              created_at INTEGER
50          );
51          "#,
52        )
53        .execute(&pool)
54        .await?;
55
56        let mnemonic = match Mnemonic::generate(12) {
57            Ok(m) => m.to_string(),
58            Err(e) => {
59                println!("Error generating mnemonic: {}", e);
60                return Err(e.into());
61            }
62        };
63        let user = User::new(mnemonic, &pool).await?;
64        println!("User created with pubkey: {}", user.i0_pubkey);
65    } else {
66        pool = SqlitePool::connect(&db_url).await?;
67
68        // Migration: Drop buyer_token and seller_token columns if they exist
69        migrate_remove_token_columns(&pool).await?;
70    }
71
72    Ok(pool)
73}
74
75async fn migrate_remove_token_columns(pool: &SqlitePool) -> Result<()> {
76    println!("Checking for legacy token columns...");
77
78    // Check if buyer_token column exists
79    let buyer_token_exists = sqlx::query_scalar::<_, i64>(
80        "SELECT COUNT(*) FROM pragma_table_info('orders') WHERE name = 'buyer_token'",
81    )
82    .fetch_one(pool)
83    .await?;
84
85    // Check if seller_token column exists
86    let seller_token_exists = sqlx::query_scalar::<_, i64>(
87        "SELECT COUNT(*) FROM pragma_table_info('orders') WHERE name = 'seller_token'",
88    )
89    .fetch_one(pool)
90    .await?;
91
92    // Drop buyer_token column if it exists
93    if buyer_token_exists > 0 {
94        println!("Removing legacy buyer_token column...");
95        match sqlx::query("ALTER TABLE orders DROP COLUMN buyer_token")
96            .execute(pool)
97            .await
98        {
99            Ok(_) => println!("Successfully removed buyer_token column"),
100            Err(e) => {
101                println!("Warning: Could not remove buyer_token column: {}", e);
102                // Continue execution - this is not critical
103            }
104        }
105    }
106
107    // Drop seller_token column if it exists
108    if seller_token_exists > 0 {
109        println!("Removing legacy seller_token column...");
110        match sqlx::query("ALTER TABLE orders DROP COLUMN seller_token")
111            .execute(pool)
112            .await
113        {
114            Ok(_) => println!("Successfully removed seller_token column"),
115            Err(e) => {
116                println!("Warning: Could not remove seller_token column: {}", e);
117                // Continue execution - this is not critical
118            }
119        }
120    }
121
122    if buyer_token_exists == 0 && seller_token_exists == 0 {
123        println!("No legacy token columns found - database is up to date");
124    }
125
126    Ok(())
127}
128
129#[derive(Debug, Default, Clone, sqlx::FromRow)]
130pub struct User {
131    /// The user's ID is the identity pubkey
132    pub i0_pubkey: String,
133    pub mnemonic: String,
134    pub last_trade_index: Option<i64>,
135    pub created_at: i64,
136}
137
138impl User {
139    pub async fn new(mnemonic: String, pool: &SqlitePool) -> Result<Self> {
140        let mut user = User::default();
141        let account = NOSTR_REPLACEABLE_EVENT_KIND as u32;
142        let i0_keys =
143            Keys::from_mnemonic_advanced(&mnemonic, None, Some(account), Some(0), Some(0))?;
144        user.i0_pubkey = i0_keys.public_key().to_string();
145        user.created_at = chrono::Utc::now().timestamp();
146        user.mnemonic = mnemonic;
147        sqlx::query(
148            r#"
149                  INSERT INTO users (i0_pubkey, mnemonic, created_at)
150                  VALUES (?, ?, ?)
151                "#,
152        )
153        .bind(&user.i0_pubkey)
154        .bind(&user.mnemonic)
155        .bind(user.created_at)
156        .execute(pool)
157        .await?;
158
159        Ok(user)
160    }
161    // Chainable setters
162    pub fn set_mnemonic(&mut self, mnemonic: String) -> &mut Self {
163        self.mnemonic = mnemonic;
164        self
165    }
166
167    pub fn set_last_trade_index(&mut self, last_trade_index: i64) -> &mut Self {
168        self.last_trade_index = Some(last_trade_index);
169        self
170    }
171
172    // Applying changes to the database
173    pub async fn save(&self, pool: &SqlitePool) -> Result<()> {
174        sqlx::query(
175            r#"
176              UPDATE users 
177              SET mnemonic = ?, last_trade_index = ?
178              WHERE i0_pubkey = ?
179              "#,
180        )
181        .bind(&self.mnemonic)
182        .bind(self.last_trade_index)
183        .bind(&self.i0_pubkey)
184        .execute(pool)
185        .await?;
186
187        println!(
188            "User with i0 pubkey {} updated in the database.",
189            self.i0_pubkey
190        );
191
192        Ok(())
193    }
194
195    pub async fn get(pool: &SqlitePool) -> Result<User> {
196        let user = sqlx::query_as::<_, User>(
197            r#"
198            SELECT i0_pubkey, mnemonic, last_trade_index, created_at
199            FROM users
200            LIMIT 1
201            "#,
202        )
203        .fetch_one(pool)
204        .await?;
205
206        Ok(user)
207    }
208
209    pub async fn get_last_trade_index(pool: SqlitePool) -> Result<i64> {
210        let user = User::get(&pool).await?;
211        match user.last_trade_index {
212            Some(index) => Ok(index),
213            None => Ok(0),
214        }
215    }
216
217    pub async fn get_next_trade_index(pool: SqlitePool) -> Result<i64> {
218        let last_trade_index = User::get_last_trade_index(pool).await?;
219        Ok(last_trade_index + 1)
220    }
221
222    pub async fn get_identity_keys(pool: &SqlitePool) -> Result<Keys> {
223        let user = User::get(pool).await?;
224        let account = NOSTR_REPLACEABLE_EVENT_KIND as u32;
225        let keys =
226            Keys::from_mnemonic_advanced(&user.mnemonic, None, Some(account), Some(0), Some(0))?;
227
228        Ok(keys)
229    }
230
231    pub async fn get_next_trade_keys(pool: &SqlitePool) -> Result<(Keys, i64)> {
232        let trade_index = User::get_next_trade_index(pool.clone()).await?;
233        let trade_keys = User::get_trade_keys(pool, trade_index).await?;
234
235        Ok((trade_keys, trade_index))
236    }
237
238    pub async fn get_trade_keys(pool: &SqlitePool, index: i64) -> Result<Keys> {
239        if index < 0 {
240            return Err(anyhow::anyhow!("Trade index cannot be negative"));
241        }
242        let user = User::get(pool).await?;
243        let account = NOSTR_REPLACEABLE_EVENT_KIND as u32;
244        let keys = Keys::from_mnemonic_advanced(
245            &user.mnemonic,
246            None,
247            Some(account),
248            Some(0),
249            Some(index as u32),
250        )?;
251
252        Ok(keys)
253    }
254}
255
256#[derive(Debug, Default, Clone, sqlx::FromRow)]
257pub struct Order {
258    pub id: Option<String>,
259    pub kind: Option<String>,
260    pub status: Option<String>,
261    pub amount: i64,
262    pub fiat_code: String,
263    pub min_amount: Option<i64>,
264    pub max_amount: Option<i64>,
265    pub fiat_amount: i64,
266    pub payment_method: String,
267    pub premium: i64,
268    pub trade_keys: Option<String>,
269    pub counterparty_pubkey: Option<String>,
270    pub is_mine: Option<bool>,
271    pub buyer_invoice: Option<String>,
272    pub request_id: Option<i64>,
273    pub created_at: Option<i64>,
274    pub expires_at: Option<i64>,
275}
276
277impl Order {
278    pub async fn new(
279        pool: &SqlitePool,
280        order: SmallOrder,
281        trade_keys: &Keys,
282        request_id: Option<i64>,
283    ) -> Result<Self> {
284        let trade_keys_hex = trade_keys.secret_key().to_secret_hex();
285        let id = match order.id {
286            Some(id) => id.to_string(),
287            None => uuid::Uuid::new_v4().to_string(),
288        };
289        let order = Order {
290            id: Some(id),
291            kind: order.kind.as_ref().map(|k| k.to_string()),
292            status: order.status.as_ref().map(|s| s.to_string()),
293            amount: order.amount,
294            fiat_code: order.fiat_code,
295            min_amount: order.min_amount,
296            max_amount: order.max_amount,
297            fiat_amount: order.fiat_amount,
298            payment_method: order.payment_method,
299            premium: order.premium,
300            trade_keys: Some(trade_keys_hex),
301            counterparty_pubkey: None,
302            is_mine: Some(true),
303            buyer_invoice: None,
304            request_id,
305            created_at: Some(chrono::Utc::now().timestamp()),
306            expires_at: None,
307        };
308
309        // Try insert; if id already exists, perform an update instead
310        let insert_result = order.insert_db(pool).await;
311
312        if let Err(e) = insert_result {
313            // If the error is due to unique constraint (id already present), update instead
314            // SQLite uses error code 1555 (constraint failed) or 2067 (unique constraint failed)
315            let is_unique_violation = match e.as_database_error() {
316                Some(db_err) => {
317                    let code = db_err.code().map(|c| c.to_string()).unwrap_or_default();
318                    code == "1555" || code == "2067"
319                }
320                None => false,
321            };
322
323            if is_unique_violation {
324                order.update_db(pool).await?;
325            } else {
326                return Err(e.into());
327            }
328        }
329
330        Ok(order)
331    }
332
333    async fn insert_db(&self, pool: &SqlitePool) -> Result<(), sqlx::Error> {
334        sqlx::query(
335            r#"
336			      INSERT INTO orders (id, kind, status, amount, min_amount, max_amount,
337			      fiat_code, fiat_amount, payment_method, premium, trade_keys,
338			      counterparty_pubkey, is_mine, buyer_invoice, request_id, created_at, expires_at)
339			      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
340			    "#,
341        )
342        .bind(&self.id)
343        .bind(&self.kind)
344        .bind(&self.status)
345        .bind(self.amount)
346        .bind(self.min_amount)
347        .bind(self.max_amount)
348        .bind(&self.fiat_code)
349        .bind(self.fiat_amount)
350        .bind(&self.payment_method)
351        .bind(self.premium)
352        .bind(&self.trade_keys)
353        .bind(&self.counterparty_pubkey)
354        .bind(self.is_mine)
355        .bind(&self.buyer_invoice)
356        .bind(self.request_id)
357        .bind(self.created_at)
358        .bind(self.expires_at)
359        .execute(pool)
360        .await?
361        .rows_affected();
362        Ok(())
363    }
364
365    async fn update_db(&self, pool: &SqlitePool) -> Result<(), sqlx::Error> {
366        sqlx::query(
367			r#"
368			  UPDATE orders 
369			  SET kind = ?, status = ?, amount = ?, min_amount = ?, max_amount = ?,
370			      fiat_code = ?, fiat_amount = ?, payment_method = ?, premium = ?, trade_keys = ?,
371			      counterparty_pubkey = ?, is_mine = ?, buyer_invoice = ?, request_id = ?, created_at = ?, expires_at = ?
372			  WHERE id = ?
373			"#,
374		)
375		.bind(&self.kind)
376		.bind(&self.status)
377		.bind(self.amount)
378		.bind(self.min_amount)
379		.bind(self.max_amount)
380		.bind(&self.fiat_code)
381		.bind(self.fiat_amount)
382		.bind(&self.payment_method)
383		.bind(self.premium)
384		.bind(&self.trade_keys)
385		.bind(&self.counterparty_pubkey)
386		.bind(self.is_mine)
387		.bind(&self.buyer_invoice)
388		.bind(self.request_id)
389		.bind(self.created_at)
390		.bind(self.expires_at)
391		.bind(&self.id)
392		.execute(pool)
393		.await?
394		.rows_affected();
395        Ok(())
396    }
397
398    // Setters encadenables
399    pub fn set_kind(&mut self, kind: String) -> &mut Self {
400        self.kind = Some(kind);
401        self
402    }
403
404    pub fn set_status(&mut self, status: String) -> &mut Self {
405        self.status = Some(status);
406        self
407    }
408
409    pub fn set_amount(&mut self, amount: i64) -> &mut Self {
410        self.amount = amount;
411        self
412    }
413
414    pub fn set_fiat_code(&mut self, fiat_code: String) -> &mut Self {
415        self.fiat_code = fiat_code;
416        self
417    }
418
419    pub fn set_min_amount(&mut self, min_amount: i64) -> &mut Self {
420        self.min_amount = Some(min_amount);
421        self
422    }
423
424    pub fn set_max_amount(&mut self, max_amount: i64) -> &mut Self {
425        self.max_amount = Some(max_amount);
426        self
427    }
428
429    pub fn set_fiat_amount(&mut self, fiat_amount: i64) -> &mut Self {
430        self.fiat_amount = fiat_amount;
431        self
432    }
433
434    pub fn set_payment_method(&mut self, payment_method: String) -> &mut Self {
435        self.payment_method = payment_method;
436        self
437    }
438
439    pub fn set_premium(&mut self, premium: i64) -> &mut Self {
440        self.premium = premium;
441        self
442    }
443
444    pub fn set_counterparty_pubkey(&mut self, counterparty_pubkey: String) -> &mut Self {
445        self.counterparty_pubkey = Some(counterparty_pubkey);
446        self
447    }
448
449    pub fn set_trade_keys(&mut self, trade_keys: String) -> &mut Self {
450        self.trade_keys = Some(trade_keys);
451        self
452    }
453
454    pub fn set_is_mine(&mut self, is_mine: bool) -> &mut Self {
455        self.is_mine = Some(is_mine);
456        self
457    }
458
459    // Applying changes to the database
460    pub async fn save(&self, pool: &SqlitePool) -> Result<()> {
461        // Validation if an identity document is present
462        if let Some(ref id) = self.id {
463            sqlx::query(
464                r#"
465              UPDATE orders 
466              SET kind = ?, status = ?, amount = ?, fiat_code = ?, min_amount = ?, max_amount = ?, 
467                  fiat_amount = ?, payment_method = ?, premium = ?, trade_keys = ?, counterparty_pubkey = ?,
468                  is_mine = ?, buyer_invoice = ?, expires_at = ?
469              WHERE id = ?
470              "#,
471            )
472            .bind(&self.kind)
473            .bind(&self.status)
474            .bind(self.amount)
475            .bind(&self.fiat_code)
476            .bind(self.min_amount)
477            .bind(self.max_amount)
478            .bind(self.fiat_amount)
479            .bind(&self.payment_method)
480            .bind(self.premium)
481            .bind(&self.trade_keys)
482            .bind(&self.counterparty_pubkey)
483            .bind(self.is_mine)
484            .bind(&self.buyer_invoice)
485            .bind(self.expires_at)
486            .bind(id)
487            .execute(pool)
488            .await?;
489
490            println!("Order with id {} updated in the database.", id);
491        } else {
492            return Err(anyhow::anyhow!("Order must have an ID to be updated."));
493        }
494
495        Ok(())
496    }
497
498    pub async fn save_new_id(
499        pool: &SqlitePool,
500        id: String,
501        new_id: String,
502    ) -> anyhow::Result<bool> {
503        let rows_affected = sqlx::query(
504            r#"
505          UPDATE orders
506          SET id = ?
507          WHERE id = ?
508        "#,
509        )
510        .bind(&new_id)
511        .bind(&id)
512        .execute(pool)
513        .await?
514        .rows_affected();
515
516        Ok(rows_affected > 0)
517    }
518
519    pub async fn get_by_id(pool: &SqlitePool, id: &str) -> Result<Order> {
520        let order = sqlx::query_as::<_, Order>(
521            r#"
522            SELECT * FROM orders WHERE id = ?
523            LIMIT 1
524            "#,
525        )
526        .bind(id)
527        .fetch_one(pool)
528        .await?;
529
530        if order.id.is_none() {
531            return Err(anyhow::anyhow!("Order not found"));
532        }
533
534        Ok(order)
535    }
536
537    pub async fn get_all_trade_keys(pool: &SqlitePool) -> Result<Vec<String>> {
538        let trade_keys: Vec<String> = sqlx::query_scalar::<_, Option<String>>(
539            "SELECT DISTINCT trade_keys FROM orders WHERE trade_keys IS NOT NULL",
540        )
541        .fetch_all(pool)
542        .await?
543        .into_iter()
544        .flatten()
545        .collect();
546
547        Ok(trade_keys)
548    }
549
550    pub async fn delete_by_id(pool: &SqlitePool, id: &str) -> Result<bool> {
551        let rows_affected = sqlx::query(
552            r#"
553          DELETE FROM orders
554          WHERE id = ?
555        "#,
556        )
557        .bind(id)
558        .execute(pool)
559        .await?
560        .rows_affected();
561
562        Ok(rows_affected > 0)
563    }
564}