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 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 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 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 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 }
104 }
105 }
106
107 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 }
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 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 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 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 sqlx::query(
310 r#"
311 INSERT INTO orders (id, kind, status, amount, min_amount, max_amount,
312 fiat_code, fiat_amount, payment_method, premium, trade_keys,
313 counterparty_pubkey, is_mine, buyer_invoice, request_id, created_at, expires_at)
314 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
315 "#,
316 )
317 .bind(&order.id)
318 .bind(&order.kind)
319 .bind(&order.status)
320 .bind(order.amount)
321 .bind(order.min_amount)
322 .bind(order.max_amount)
323 .bind(&order.fiat_code)
324 .bind(order.fiat_amount)
325 .bind(&order.payment_method)
326 .bind(order.premium)
327 .bind(&order.trade_keys)
328 .bind(&order.counterparty_pubkey)
329 .bind(order.is_mine)
330 .bind(&order.buyer_invoice)
331 .bind(order.request_id)
332 .bind(order.created_at)
333 .bind(order.expires_at)
334 .execute(pool)
335 .await?;
336
337 Ok(order)
338 }
339
340 pub fn set_kind(&mut self, kind: String) -> &mut Self {
342 self.kind = Some(kind);
343 self
344 }
345
346 pub fn set_status(&mut self, status: String) -> &mut Self {
347 self.status = Some(status);
348 self
349 }
350
351 pub fn set_amount(&mut self, amount: i64) -> &mut Self {
352 self.amount = amount;
353 self
354 }
355
356 pub fn set_fiat_code(&mut self, fiat_code: String) -> &mut Self {
357 self.fiat_code = fiat_code;
358 self
359 }
360
361 pub fn set_min_amount(&mut self, min_amount: i64) -> &mut Self {
362 self.min_amount = Some(min_amount);
363 self
364 }
365
366 pub fn set_max_amount(&mut self, max_amount: i64) -> &mut Self {
367 self.max_amount = Some(max_amount);
368 self
369 }
370
371 pub fn set_fiat_amount(&mut self, fiat_amount: i64) -> &mut Self {
372 self.fiat_amount = fiat_amount;
373 self
374 }
375
376 pub fn set_payment_method(&mut self, payment_method: String) -> &mut Self {
377 self.payment_method = payment_method;
378 self
379 }
380
381 pub fn set_premium(&mut self, premium: i64) -> &mut Self {
382 self.premium = premium;
383 self
384 }
385
386 pub fn set_counterparty_pubkey(&mut self, counterparty_pubkey: String) -> &mut Self {
387 self.counterparty_pubkey = Some(counterparty_pubkey);
388 self
389 }
390
391 pub fn set_trade_keys(&mut self, trade_keys: String) -> &mut Self {
392 self.trade_keys = Some(trade_keys);
393 self
394 }
395
396 pub fn set_is_mine(&mut self, is_mine: bool) -> &mut Self {
397 self.is_mine = Some(is_mine);
398 self
399 }
400
401 pub async fn save(&self, pool: &SqlitePool) -> Result<()> {
403 if let Some(ref id) = self.id {
405 sqlx::query(
406 r#"
407 UPDATE orders
408 SET kind = ?, status = ?, amount = ?, fiat_code = ?, min_amount = ?, max_amount = ?,
409 fiat_amount = ?, payment_method = ?, premium = ?, trade_keys = ?, counterparty_pubkey = ?,
410 is_mine = ?, buyer_invoice = ?, expires_at = ?
411 WHERE id = ?
412 "#,
413 )
414 .bind(&self.kind)
415 .bind(&self.status)
416 .bind(self.amount)
417 .bind(&self.fiat_code)
418 .bind(self.min_amount)
419 .bind(self.max_amount)
420 .bind(self.fiat_amount)
421 .bind(&self.payment_method)
422 .bind(self.premium)
423 .bind(&self.trade_keys)
424 .bind(&self.counterparty_pubkey)
425 .bind(self.is_mine)
426 .bind(&self.buyer_invoice)
427 .bind(self.expires_at)
428 .bind(id)
429 .execute(pool)
430 .await?;
431
432 println!("Order with id {} updated in the database.", id);
433 } else {
434 return Err(anyhow::anyhow!("Order must have an ID to be updated."));
435 }
436
437 Ok(())
438 }
439
440 pub async fn save_new_id(
441 pool: &SqlitePool,
442 id: String,
443 new_id: String,
444 ) -> anyhow::Result<bool> {
445 let rows_affected = sqlx::query(
446 r#"
447 UPDATE orders
448 SET id = ?
449 WHERE id = ?
450 "#,
451 )
452 .bind(&new_id)
453 .bind(&id)
454 .execute(pool)
455 .await?
456 .rows_affected();
457
458 Ok(rows_affected > 0)
459 }
460
461 pub async fn get_by_id(pool: &SqlitePool, id: &str) -> Result<Order> {
462 let order = sqlx::query_as::<_, Order>(
463 r#"
464 SELECT * FROM orders WHERE id = ?
465 LIMIT 1
466 "#,
467 )
468 .bind(id)
469 .fetch_one(pool)
470 .await?;
471
472 if order.id.is_none() {
473 return Err(anyhow::anyhow!("Order not found"));
474 }
475
476 Ok(order)
477 }
478
479 pub async fn get_all_trade_keys(pool: &SqlitePool) -> Result<Vec<String>> {
480 let trade_keys: Vec<String> = sqlx::query_scalar::<_, Option<String>>(
481 "SELECT DISTINCT trade_keys FROM orders WHERE trade_keys IS NOT NULL",
482 )
483 .fetch_all(pool)
484 .await?
485 .into_iter()
486 .flatten()
487 .collect();
488
489 Ok(trade_keys)
490 }
491
492 pub async fn delete_by_id(pool: &SqlitePool, id: &str) -> Result<bool> {
493 let rows_affected = sqlx::query(
494 r#"
495 DELETE FROM orders
496 WHERE id = ?
497 "#,
498 )
499 .bind(id)
500 .execute(pool)
501 .await?
502 .rows_affected();
503
504 Ok(rows_affected > 0)
505 }
506}