mint.rs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. //! Payments
  2. use std::str::FromStr;
  3. use cashu::quote_id::QuoteId;
  4. use cashu::{Amount, Id, SecretKey};
  5. use crate::database::mint::test::unique_string;
  6. use crate::database::mint::{Database, Error, KeysDatabase};
  7. use crate::database::MintSignaturesDatabase;
  8. use crate::mint::{MeltPaymentRequest, MeltQuote, MintQuote, Operation};
  9. use crate::payment::PaymentIdentifier;
  10. /// Add a mint quote
  11. pub async fn add_mint_quote<DB>(db: DB)
  12. where
  13. DB: Database<Error> + KeysDatabase<Err = Error>,
  14. {
  15. let mint_quote = MintQuote::new(
  16. None,
  17. "".to_owned(),
  18. cashu::CurrencyUnit::Sat,
  19. None,
  20. 0,
  21. PaymentIdentifier::CustomId(unique_string()),
  22. None,
  23. 0.into(),
  24. 0.into(),
  25. cashu::PaymentMethod::Bolt12,
  26. 0,
  27. vec![],
  28. vec![],
  29. );
  30. let mut tx = Database::begin_transaction(&db).await.unwrap();
  31. assert!(tx.add_mint_quote(mint_quote.clone()).await.is_ok());
  32. tx.commit().await.unwrap();
  33. }
  34. /// Dup mint quotes fails
  35. pub async fn add_mint_quote_only_once<DB>(db: DB)
  36. where
  37. DB: Database<Error> + KeysDatabase<Err = Error>,
  38. {
  39. let mint_quote = MintQuote::new(
  40. None,
  41. "".to_owned(),
  42. cashu::CurrencyUnit::Sat,
  43. None,
  44. 0,
  45. PaymentIdentifier::CustomId(unique_string()),
  46. None,
  47. 0.into(),
  48. 0.into(),
  49. cashu::PaymentMethod::Bolt12,
  50. 0,
  51. vec![],
  52. vec![],
  53. );
  54. let mut tx = Database::begin_transaction(&db).await.unwrap();
  55. assert!(tx.add_mint_quote(mint_quote.clone()).await.is_ok());
  56. tx.commit().await.unwrap();
  57. let mut tx = Database::begin_transaction(&db).await.unwrap();
  58. assert!(tx.add_mint_quote(mint_quote).await.is_err());
  59. tx.commit().await.unwrap();
  60. }
  61. /// Register payments
  62. pub async fn register_payments<DB>(db: DB)
  63. where
  64. DB: Database<Error> + KeysDatabase<Err = Error>,
  65. {
  66. let mint_quote = MintQuote::new(
  67. None,
  68. "".to_owned(),
  69. cashu::CurrencyUnit::Sat,
  70. None,
  71. 0,
  72. PaymentIdentifier::CustomId(unique_string()),
  73. None,
  74. 0.into(),
  75. 0.into(),
  76. cashu::PaymentMethod::Bolt12,
  77. 0,
  78. vec![],
  79. vec![],
  80. );
  81. let mut tx = Database::begin_transaction(&db).await.unwrap();
  82. assert!(tx.add_mint_quote(mint_quote.clone()).await.is_ok());
  83. let p1 = unique_string();
  84. let p2 = unique_string();
  85. let new_paid_amount = tx
  86. .increment_mint_quote_amount_paid(&mint_quote.id, 100.into(), p1.clone())
  87. .await
  88. .unwrap();
  89. assert_eq!(new_paid_amount, 100.into());
  90. let new_paid_amount = tx
  91. .increment_mint_quote_amount_paid(&mint_quote.id, 250.into(), p2.clone())
  92. .await
  93. .unwrap();
  94. assert_eq!(new_paid_amount, 350.into());
  95. tx.commit().await.unwrap();
  96. let mint_quote_from_db = db
  97. .get_mint_quote(&mint_quote.id)
  98. .await
  99. .unwrap()
  100. .expect("mint_quote_from_db");
  101. assert_eq!(mint_quote_from_db.amount_paid(), 350.into());
  102. assert_eq!(
  103. mint_quote_from_db
  104. .payments
  105. .iter()
  106. .map(|x| (x.payment_id.clone(), x.amount))
  107. .collect::<Vec<_>>(),
  108. vec![(p1, 100.into()), (p2, 250.into())]
  109. );
  110. }
  111. /// Read mint and payments from db and tx objects
  112. pub async fn read_mint_from_db_and_tx<DB>(db: DB)
  113. where
  114. DB: Database<Error> + KeysDatabase<Err = Error>,
  115. {
  116. let mint_quote = MintQuote::new(
  117. None,
  118. "".to_owned(),
  119. cashu::CurrencyUnit::Sat,
  120. None,
  121. 0,
  122. PaymentIdentifier::CustomId(unique_string()),
  123. None,
  124. 0.into(),
  125. 0.into(),
  126. cashu::PaymentMethod::Bolt12,
  127. 0,
  128. vec![],
  129. vec![],
  130. );
  131. let p1 = unique_string();
  132. let p2 = unique_string();
  133. let mut tx = Database::begin_transaction(&db).await.unwrap();
  134. tx.add_mint_quote(mint_quote.clone()).await.unwrap();
  135. let new_paid_amount = tx
  136. .increment_mint_quote_amount_paid(&mint_quote.id, 100.into(), p1.clone())
  137. .await
  138. .unwrap();
  139. assert_eq!(new_paid_amount, 100.into());
  140. let new_paid_amount = tx
  141. .increment_mint_quote_amount_paid(&mint_quote.id, 250.into(), p2.clone())
  142. .await
  143. .unwrap();
  144. assert_eq!(new_paid_amount, 350.into());
  145. tx.commit().await.unwrap();
  146. let mint_quote_from_db = db
  147. .get_mint_quote(&mint_quote.id)
  148. .await
  149. .unwrap()
  150. .expect("mint_quote_from_db");
  151. assert_eq!(mint_quote_from_db.amount_paid(), 350.into());
  152. assert_eq!(
  153. mint_quote_from_db
  154. .payments
  155. .iter()
  156. .map(|x| (x.payment_id.clone(), x.amount))
  157. .collect::<Vec<_>>(),
  158. vec![(p1, 100.into()), (p2, 250.into())]
  159. );
  160. let mut tx = Database::begin_transaction(&db).await.unwrap();
  161. let mint_quote_from_tx = tx
  162. .get_mint_quote(&mint_quote.id)
  163. .await
  164. .unwrap()
  165. .expect("mint_quote_from_tx");
  166. assert_eq!(mint_quote_from_db, mint_quote_from_tx);
  167. }
  168. /// Reject duplicate payments in the same txs
  169. pub async fn reject_duplicate_payments_same_tx<DB>(db: DB)
  170. where
  171. DB: Database<Error> + KeysDatabase<Err = Error>,
  172. {
  173. let mint_quote = MintQuote::new(
  174. None,
  175. "".to_owned(),
  176. cashu::CurrencyUnit::Sat,
  177. None,
  178. 0,
  179. PaymentIdentifier::CustomId(unique_string()),
  180. None,
  181. 0.into(),
  182. 0.into(),
  183. cashu::PaymentMethod::Bolt12,
  184. 0,
  185. vec![],
  186. vec![],
  187. );
  188. let p1 = unique_string();
  189. let mut tx = Database::begin_transaction(&db).await.unwrap();
  190. tx.add_mint_quote(mint_quote.clone()).await.unwrap();
  191. let amount_paid = tx
  192. .increment_mint_quote_amount_paid(&mint_quote.id, 100.into(), p1.clone())
  193. .await
  194. .unwrap();
  195. assert!(tx
  196. .increment_mint_quote_amount_paid(&mint_quote.id, 100.into(), p1)
  197. .await
  198. .is_err());
  199. tx.commit().await.unwrap();
  200. let mint_quote_from_db = db
  201. .get_mint_quote(&mint_quote.id)
  202. .await
  203. .unwrap()
  204. .expect("mint_from_db");
  205. assert_eq!(mint_quote_from_db.amount_paid(), amount_paid);
  206. assert_eq!(mint_quote_from_db.payments.len(), 1);
  207. }
  208. /// Reject duplicate payments in different txs
  209. pub async fn reject_duplicate_payments_diff_tx<DB>(db: DB)
  210. where
  211. DB: Database<Error> + KeysDatabase<Err = Error>,
  212. {
  213. let p1 = unique_string();
  214. let mint_quote = MintQuote::new(
  215. None,
  216. "".to_owned(),
  217. cashu::CurrencyUnit::Sat,
  218. None,
  219. 0,
  220. PaymentIdentifier::CustomId(unique_string()),
  221. None,
  222. 0.into(),
  223. 0.into(),
  224. cashu::PaymentMethod::Bolt12,
  225. 0,
  226. vec![],
  227. vec![],
  228. );
  229. let mut tx = Database::begin_transaction(&db).await.unwrap();
  230. tx.add_mint_quote(mint_quote.clone()).await.unwrap();
  231. let amount_paid = tx
  232. .increment_mint_quote_amount_paid(&mint_quote.id, 100.into(), p1.clone())
  233. .await
  234. .unwrap();
  235. tx.commit().await.unwrap();
  236. let mut tx = Database::begin_transaction(&db).await.unwrap();
  237. assert!(tx
  238. .increment_mint_quote_amount_paid(&mint_quote.id, 100.into(), p1)
  239. .await
  240. .is_err());
  241. tx.commit().await.unwrap(); // although in theory nothing has changed, let's try it out
  242. let mint_quote_from_db = db
  243. .get_mint_quote(&mint_quote.id)
  244. .await
  245. .unwrap()
  246. .expect("mint_from_db");
  247. assert_eq!(mint_quote_from_db.amount_paid(), amount_paid);
  248. assert_eq!(mint_quote_from_db.payments.len(), 1);
  249. }
  250. /// Successful melt with unique blinded messages
  251. pub async fn add_melt_request_unique_blinded_messages<DB>(db: DB)
  252. where
  253. DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>,
  254. {
  255. let inputs_amount = Amount::from(100u64);
  256. let inputs_fee = Amount::from(1u64);
  257. let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
  258. // Create a dummy blinded message
  259. let blinded_secret = SecretKey::generate().public_key();
  260. let blinded_message = cashu::BlindedMessage {
  261. blinded_secret,
  262. keyset_id,
  263. amount: Amount::from(100u64),
  264. witness: None,
  265. };
  266. let blinded_messages = vec![blinded_message];
  267. let mut tx = Database::begin_transaction(&db).await.unwrap();
  268. let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
  269. tx.add_melt_quote(quote.clone()).await.unwrap();
  270. tx.add_melt_request(&quote.id, inputs_amount, inputs_fee)
  271. .await
  272. .unwrap();
  273. tx.add_blinded_messages(Some(&quote.id), &blinded_messages, &Operation::new_melt())
  274. .await
  275. .unwrap();
  276. tx.commit().await.unwrap();
  277. // Verify retrieval
  278. let mut tx = Database::begin_transaction(&db).await.unwrap();
  279. let retrieved = tx
  280. .get_melt_request_and_blinded_messages(&quote.id)
  281. .await
  282. .unwrap()
  283. .unwrap();
  284. assert_eq!(retrieved.inputs_amount, inputs_amount);
  285. assert_eq!(retrieved.inputs_fee, inputs_fee);
  286. assert_eq!(retrieved.change_outputs.len(), 1);
  287. assert_eq!(retrieved.change_outputs[0].amount, Amount::from(100u64));
  288. tx.commit().await.unwrap();
  289. }
  290. /// Reject melt with duplicate blinded message (already signed)
  291. pub async fn reject_melt_duplicate_blinded_signature<DB>(db: DB)
  292. where
  293. DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>,
  294. {
  295. let quote_id1 = QuoteId::new_uuid();
  296. let inputs_amount = Amount::from(100u64);
  297. let inputs_fee = Amount::from(1u64);
  298. let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
  299. // Create a dummy blinded message
  300. let blinded_secret = SecretKey::generate().public_key();
  301. let blinded_message = cashu::BlindedMessage {
  302. blinded_secret,
  303. keyset_id,
  304. amount: Amount::from(100u64),
  305. witness: None,
  306. };
  307. let blinded_messages = vec![blinded_message.clone()];
  308. // First, "sign" it by adding to blind_signature (simulate successful mint)
  309. let mut tx = Database::begin_transaction(&db).await.unwrap();
  310. let c = SecretKey::generate().public_key();
  311. let blind_sig = cashu::BlindSignature {
  312. amount: Amount::from(100u64),
  313. keyset_id,
  314. c,
  315. dleq: None,
  316. };
  317. let blinded_secrets = vec![blinded_message.blinded_secret];
  318. tx.add_blind_signatures(&blinded_secrets, &[blind_sig], Some(quote_id1))
  319. .await
  320. .unwrap();
  321. tx.commit().await.unwrap();
  322. // Now try to add melt request with the same blinded message - should fail due to constraint
  323. let mut tx = Database::begin_transaction(&db).await.unwrap();
  324. let quote2 = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
  325. tx.add_melt_quote(quote2.clone()).await.unwrap();
  326. tx.add_melt_request(&quote2.id, inputs_amount, inputs_fee)
  327. .await
  328. .unwrap();
  329. let result = tx
  330. .add_blinded_messages(Some(&quote2.id), &blinded_messages, &Operation::new_melt())
  331. .await;
  332. assert!(result.is_err() && matches!(result.unwrap_err(), Error::Duplicate));
  333. tx.rollback().await.unwrap(); // Rollback to avoid partial state
  334. }
  335. /// Reject duplicate blinded message insert via DB constraint (different quotes)
  336. pub async fn reject_duplicate_blinded_message_db_constraint<DB>(db: DB)
  337. where
  338. DB: Database<Error> + KeysDatabase<Err = Error>,
  339. {
  340. let inputs_amount = Amount::from(100u64);
  341. let inputs_fee = Amount::from(1u64);
  342. let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
  343. // Create a dummy blinded message
  344. let blinded_secret = SecretKey::generate().public_key();
  345. let blinded_message = cashu::BlindedMessage {
  346. blinded_secret,
  347. keyset_id,
  348. amount: Amount::from(100u64),
  349. witness: None,
  350. };
  351. let blinded_messages = vec![blinded_message];
  352. // First insert succeeds
  353. let mut tx = Database::begin_transaction(&db).await.unwrap();
  354. let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
  355. tx.add_melt_quote(quote.clone()).await.unwrap();
  356. tx.add_melt_request(&quote.id, inputs_amount, inputs_fee)
  357. .await
  358. .unwrap();
  359. assert!(tx
  360. .add_blinded_messages(Some(&quote.id), &blinded_messages, &Operation::new_melt())
  361. .await
  362. .is_ok());
  363. tx.commit().await.unwrap();
  364. // Second insert with same blinded_message but different quote_id should fail due to unique constraint on blinded_message
  365. let mut tx = Database::begin_transaction(&db).await.unwrap();
  366. let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
  367. tx.add_melt_quote(quote.clone()).await.unwrap();
  368. tx.add_melt_request(&quote.id, inputs_amount, inputs_fee)
  369. .await
  370. .unwrap();
  371. let result = tx
  372. .add_blinded_messages(Some(&quote.id), &blinded_messages, &Operation::new_melt())
  373. .await;
  374. // Expect a database error due to unique violation
  375. assert!(result.is_err()); // Specific error might be DB-specific, e.g., SqliteError or PostgresError
  376. tx.rollback().await.unwrap();
  377. }
  378. /// Cleanup of melt request after processing
  379. pub async fn cleanup_melt_request_after_processing<DB>(db: DB)
  380. where
  381. DB: Database<Error> + KeysDatabase<Err = Error>,
  382. {
  383. let inputs_amount = Amount::from(100u64);
  384. let inputs_fee = Amount::from(1u64);
  385. let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
  386. // Create dummy blinded message
  387. let blinded_secret = SecretKey::generate().public_key();
  388. let blinded_message = cashu::BlindedMessage {
  389. blinded_secret,
  390. keyset_id,
  391. amount: Amount::from(100u64),
  392. witness: None,
  393. };
  394. let blinded_messages = vec![blinded_message];
  395. // Insert melt request
  396. let mut tx1 = Database::begin_transaction(&db).await.unwrap();
  397. let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
  398. tx1.add_melt_quote(quote.clone()).await.unwrap();
  399. tx1.add_melt_request(&quote.id, inputs_amount, inputs_fee)
  400. .await
  401. .unwrap();
  402. tx1.add_blinded_messages(Some(&quote.id), &blinded_messages, &Operation::new_melt())
  403. .await
  404. .unwrap();
  405. tx1.commit().await.unwrap();
  406. // Simulate processing: get and delete
  407. let mut tx2 = Database::begin_transaction(&db).await.unwrap();
  408. let _retrieved = tx2
  409. .get_melt_request_and_blinded_messages(&quote.id)
  410. .await
  411. .unwrap()
  412. .unwrap();
  413. tx2.delete_melt_request(&quote.id).await.unwrap();
  414. tx2.commit().await.unwrap();
  415. // Verify melt_request is deleted
  416. let mut tx3 = Database::begin_transaction(&db).await.unwrap();
  417. let retrieved = tx3
  418. .get_melt_request_and_blinded_messages(&quote.id)
  419. .await
  420. .unwrap();
  421. assert!(retrieved.is_none());
  422. tx3.commit().await.unwrap();
  423. }