| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- //! Payments
- use std::str::FromStr;
- use cashu::quote_id::QuoteId;
- use cashu::{Amount, Id, SecretKey};
- use crate::database::mint::test::unique_string;
- use crate::database::mint::{Database, Error, KeysDatabase};
- use crate::database::MintSignaturesDatabase;
- use crate::mint::{MeltPaymentRequest, MeltQuote, MintQuote, Operation};
- use crate::payment::PaymentIdentifier;
- /// Add a mint quote
- pub async fn add_mint_quote<DB>(db: DB)
- where
- DB: Database<Error> + KeysDatabase<Err = Error>,
- {
- let mint_quote = MintQuote::new(
- None,
- "".to_owned(),
- cashu::CurrencyUnit::Sat,
- None,
- 0,
- PaymentIdentifier::CustomId(unique_string()),
- None,
- 0.into(),
- 0.into(),
- cashu::PaymentMethod::Bolt12,
- 0,
- vec![],
- vec![],
- );
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- assert!(tx.add_mint_quote(mint_quote.clone()).await.is_ok());
- tx.commit().await.unwrap();
- }
- /// Dup mint quotes fails
- pub async fn add_mint_quote_only_once<DB>(db: DB)
- where
- DB: Database<Error> + KeysDatabase<Err = Error>,
- {
- let mint_quote = MintQuote::new(
- None,
- "".to_owned(),
- cashu::CurrencyUnit::Sat,
- None,
- 0,
- PaymentIdentifier::CustomId(unique_string()),
- None,
- 0.into(),
- 0.into(),
- cashu::PaymentMethod::Bolt12,
- 0,
- vec![],
- vec![],
- );
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- assert!(tx.add_mint_quote(mint_quote.clone()).await.is_ok());
- tx.commit().await.unwrap();
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- assert!(tx.add_mint_quote(mint_quote).await.is_err());
- tx.commit().await.unwrap();
- }
- /// Register payments
- pub async fn register_payments<DB>(db: DB)
- where
- DB: Database<Error> + KeysDatabase<Err = Error>,
- {
- let mint_quote = MintQuote::new(
- None,
- "".to_owned(),
- cashu::CurrencyUnit::Sat,
- None,
- 0,
- PaymentIdentifier::CustomId(unique_string()),
- None,
- 0.into(),
- 0.into(),
- cashu::PaymentMethod::Bolt12,
- 0,
- vec![],
- vec![],
- );
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- assert!(tx.add_mint_quote(mint_quote.clone()).await.is_ok());
- let p1 = unique_string();
- let p2 = unique_string();
- let new_paid_amount = tx
- .increment_mint_quote_amount_paid(&mint_quote.id, 100.into(), p1.clone())
- .await
- .unwrap();
- assert_eq!(new_paid_amount, 100.into());
- let new_paid_amount = tx
- .increment_mint_quote_amount_paid(&mint_quote.id, 250.into(), p2.clone())
- .await
- .unwrap();
- assert_eq!(new_paid_amount, 350.into());
- tx.commit().await.unwrap();
- let mint_quote_from_db = db
- .get_mint_quote(&mint_quote.id)
- .await
- .unwrap()
- .expect("mint_quote_from_db");
- assert_eq!(mint_quote_from_db.amount_paid(), 350.into());
- assert_eq!(
- mint_quote_from_db
- .payments
- .iter()
- .map(|x| (x.payment_id.clone(), x.amount))
- .collect::<Vec<_>>(),
- vec![(p1, 100.into()), (p2, 250.into())]
- );
- }
- /// Read mint and payments from db and tx objects
- pub async fn read_mint_from_db_and_tx<DB>(db: DB)
- where
- DB: Database<Error> + KeysDatabase<Err = Error>,
- {
- let mint_quote = MintQuote::new(
- None,
- "".to_owned(),
- cashu::CurrencyUnit::Sat,
- None,
- 0,
- PaymentIdentifier::CustomId(unique_string()),
- None,
- 0.into(),
- 0.into(),
- cashu::PaymentMethod::Bolt12,
- 0,
- vec![],
- vec![],
- );
- let p1 = unique_string();
- let p2 = unique_string();
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- tx.add_mint_quote(mint_quote.clone()).await.unwrap();
- let new_paid_amount = tx
- .increment_mint_quote_amount_paid(&mint_quote.id, 100.into(), p1.clone())
- .await
- .unwrap();
- assert_eq!(new_paid_amount, 100.into());
- let new_paid_amount = tx
- .increment_mint_quote_amount_paid(&mint_quote.id, 250.into(), p2.clone())
- .await
- .unwrap();
- assert_eq!(new_paid_amount, 350.into());
- tx.commit().await.unwrap();
- let mint_quote_from_db = db
- .get_mint_quote(&mint_quote.id)
- .await
- .unwrap()
- .expect("mint_quote_from_db");
- assert_eq!(mint_quote_from_db.amount_paid(), 350.into());
- assert_eq!(
- mint_quote_from_db
- .payments
- .iter()
- .map(|x| (x.payment_id.clone(), x.amount))
- .collect::<Vec<_>>(),
- vec![(p1, 100.into()), (p2, 250.into())]
- );
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- let mint_quote_from_tx = tx
- .get_mint_quote(&mint_quote.id)
- .await
- .unwrap()
- .expect("mint_quote_from_tx");
- assert_eq!(mint_quote_from_db, mint_quote_from_tx);
- }
- /// Reject duplicate payments in the same txs
- pub async fn reject_duplicate_payments_same_tx<DB>(db: DB)
- where
- DB: Database<Error> + KeysDatabase<Err = Error>,
- {
- let mint_quote = MintQuote::new(
- None,
- "".to_owned(),
- cashu::CurrencyUnit::Sat,
- None,
- 0,
- PaymentIdentifier::CustomId(unique_string()),
- None,
- 0.into(),
- 0.into(),
- cashu::PaymentMethod::Bolt12,
- 0,
- vec![],
- vec![],
- );
- let p1 = unique_string();
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- tx.add_mint_quote(mint_quote.clone()).await.unwrap();
- let amount_paid = tx
- .increment_mint_quote_amount_paid(&mint_quote.id, 100.into(), p1.clone())
- .await
- .unwrap();
- assert!(tx
- .increment_mint_quote_amount_paid(&mint_quote.id, 100.into(), p1)
- .await
- .is_err());
- tx.commit().await.unwrap();
- let mint_quote_from_db = db
- .get_mint_quote(&mint_quote.id)
- .await
- .unwrap()
- .expect("mint_from_db");
- assert_eq!(mint_quote_from_db.amount_paid(), amount_paid);
- assert_eq!(mint_quote_from_db.payments.len(), 1);
- }
- /// Reject duplicate payments in different txs
- pub async fn reject_duplicate_payments_diff_tx<DB>(db: DB)
- where
- DB: Database<Error> + KeysDatabase<Err = Error>,
- {
- let p1 = unique_string();
- let mint_quote = MintQuote::new(
- None,
- "".to_owned(),
- cashu::CurrencyUnit::Sat,
- None,
- 0,
- PaymentIdentifier::CustomId(unique_string()),
- None,
- 0.into(),
- 0.into(),
- cashu::PaymentMethod::Bolt12,
- 0,
- vec![],
- vec![],
- );
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- tx.add_mint_quote(mint_quote.clone()).await.unwrap();
- let amount_paid = tx
- .increment_mint_quote_amount_paid(&mint_quote.id, 100.into(), p1.clone())
- .await
- .unwrap();
- tx.commit().await.unwrap();
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- assert!(tx
- .increment_mint_quote_amount_paid(&mint_quote.id, 100.into(), p1)
- .await
- .is_err());
- tx.commit().await.unwrap(); // although in theory nothing has changed, let's try it out
- let mint_quote_from_db = db
- .get_mint_quote(&mint_quote.id)
- .await
- .unwrap()
- .expect("mint_from_db");
- assert_eq!(mint_quote_from_db.amount_paid(), amount_paid);
- assert_eq!(mint_quote_from_db.payments.len(), 1);
- }
- /// Successful melt with unique blinded messages
- pub async fn add_melt_request_unique_blinded_messages<DB>(db: DB)
- where
- DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>,
- {
- let inputs_amount = Amount::from(100u64);
- let inputs_fee = Amount::from(1u64);
- let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
- // Create a dummy blinded message
- let blinded_secret = SecretKey::generate().public_key();
- let blinded_message = cashu::BlindedMessage {
- blinded_secret,
- keyset_id,
- amount: Amount::from(100u64),
- witness: None,
- };
- let blinded_messages = vec![blinded_message];
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
- tx.add_melt_quote(quote.clone()).await.unwrap();
- tx.add_melt_request("e.id, inputs_amount, inputs_fee)
- .await
- .unwrap();
- tx.add_blinded_messages(Some("e.id), &blinded_messages, &Operation::new_melt())
- .await
- .unwrap();
- tx.commit().await.unwrap();
- // Verify retrieval
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- let retrieved = tx
- .get_melt_request_and_blinded_messages("e.id)
- .await
- .unwrap()
- .unwrap();
- assert_eq!(retrieved.inputs_amount, inputs_amount);
- assert_eq!(retrieved.inputs_fee, inputs_fee);
- assert_eq!(retrieved.change_outputs.len(), 1);
- assert_eq!(retrieved.change_outputs[0].amount, Amount::from(100u64));
- tx.commit().await.unwrap();
- }
- /// Reject melt with duplicate blinded message (already signed)
- pub async fn reject_melt_duplicate_blinded_signature<DB>(db: DB)
- where
- DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>,
- {
- let quote_id1 = QuoteId::new_uuid();
- let inputs_amount = Amount::from(100u64);
- let inputs_fee = Amount::from(1u64);
- let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
- // Create a dummy blinded message
- let blinded_secret = SecretKey::generate().public_key();
- let blinded_message = cashu::BlindedMessage {
- blinded_secret,
- keyset_id,
- amount: Amount::from(100u64),
- witness: None,
- };
- let blinded_messages = vec![blinded_message.clone()];
- // First, "sign" it by adding to blind_signature (simulate successful mint)
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- let c = SecretKey::generate().public_key();
- let blind_sig = cashu::BlindSignature {
- amount: Amount::from(100u64),
- keyset_id,
- c,
- dleq: None,
- };
- let blinded_secrets = vec![blinded_message.blinded_secret];
- tx.add_blind_signatures(&blinded_secrets, &[blind_sig], Some(quote_id1))
- .await
- .unwrap();
- tx.commit().await.unwrap();
- // Now try to add melt request with the same blinded message - should fail due to constraint
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- let quote2 = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
- tx.add_melt_quote(quote2.clone()).await.unwrap();
- tx.add_melt_request("e2.id, inputs_amount, inputs_fee)
- .await
- .unwrap();
- let result = tx
- .add_blinded_messages(Some("e2.id), &blinded_messages, &Operation::new_melt())
- .await;
- assert!(result.is_err() && matches!(result.unwrap_err(), Error::Duplicate));
- tx.rollback().await.unwrap(); // Rollback to avoid partial state
- }
- /// Reject duplicate blinded message insert via DB constraint (different quotes)
- pub async fn reject_duplicate_blinded_message_db_constraint<DB>(db: DB)
- where
- DB: Database<Error> + KeysDatabase<Err = Error>,
- {
- let inputs_amount = Amount::from(100u64);
- let inputs_fee = Amount::from(1u64);
- let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
- // Create a dummy blinded message
- let blinded_secret = SecretKey::generate().public_key();
- let blinded_message = cashu::BlindedMessage {
- blinded_secret,
- keyset_id,
- amount: Amount::from(100u64),
- witness: None,
- };
- let blinded_messages = vec![blinded_message];
- // First insert succeeds
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
- tx.add_melt_quote(quote.clone()).await.unwrap();
- tx.add_melt_request("e.id, inputs_amount, inputs_fee)
- .await
- .unwrap();
- assert!(tx
- .add_blinded_messages(Some("e.id), &blinded_messages, &Operation::new_melt())
- .await
- .is_ok());
- tx.commit().await.unwrap();
- // Second insert with same blinded_message but different quote_id should fail due to unique constraint on blinded_message
- let mut tx = Database::begin_transaction(&db).await.unwrap();
- let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
- tx.add_melt_quote(quote.clone()).await.unwrap();
- tx.add_melt_request("e.id, inputs_amount, inputs_fee)
- .await
- .unwrap();
- let result = tx
- .add_blinded_messages(Some("e.id), &blinded_messages, &Operation::new_melt())
- .await;
- // Expect a database error due to unique violation
- assert!(result.is_err()); // Specific error might be DB-specific, e.g., SqliteError or PostgresError
- tx.rollback().await.unwrap();
- }
- /// Cleanup of melt request after processing
- pub async fn cleanup_melt_request_after_processing<DB>(db: DB)
- where
- DB: Database<Error> + KeysDatabase<Err = Error>,
- {
- let inputs_amount = Amount::from(100u64);
- let inputs_fee = Amount::from(1u64);
- let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
- // Create dummy blinded message
- let blinded_secret = SecretKey::generate().public_key();
- let blinded_message = cashu::BlindedMessage {
- blinded_secret,
- keyset_id,
- amount: Amount::from(100u64),
- witness: None,
- };
- let blinded_messages = vec![blinded_message];
- // Insert melt request
- let mut tx1 = Database::begin_transaction(&db).await.unwrap();
- let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
- tx1.add_melt_quote(quote.clone()).await.unwrap();
- tx1.add_melt_request("e.id, inputs_amount, inputs_fee)
- .await
- .unwrap();
- tx1.add_blinded_messages(Some("e.id), &blinded_messages, &Operation::new_melt())
- .await
- .unwrap();
- tx1.commit().await.unwrap();
- // Simulate processing: get and delete
- let mut tx2 = Database::begin_transaction(&db).await.unwrap();
- let _retrieved = tx2
- .get_melt_request_and_blinded_messages("e.id)
- .await
- .unwrap()
- .unwrap();
- tx2.delete_melt_request("e.id).await.unwrap();
- tx2.commit().await.unwrap();
- // Verify melt_request is deleted
- let mut tx3 = Database::begin_transaction(&db).await.unwrap();
- let retrieved = tx3
- .get_melt_request_and_blinded_messages("e.id)
- .await
- .unwrap();
- assert!(retrieved.is_none());
- tx3.commit().await.unwrap();
- }
|