|
1 | 1 | //! Payments |
2 | 2 |
|
| 3 | +use std::str::FromStr; |
| 4 | + |
| 5 | +use cashu::quote_id::QuoteId; |
| 6 | +use cashu::{Amount, Id, SecretKey}; |
| 7 | + |
3 | 8 | use crate::database::mint::test::unique_string; |
4 | 9 | use crate::database::mint::{Database, Error, KeysDatabase}; |
5 | | -use crate::mint::MintQuote; |
| 10 | +use crate::database::MintSignaturesDatabase; |
| 11 | +use crate::mint::{MeltPaymentRequest, MeltQuote, MintQuote}; |
6 | 12 | use crate::payment::PaymentIdentifier; |
7 | 13 |
|
8 | 14 | /// Add a mint quote |
@@ -404,3 +410,193 @@ where |
404 | 410 | .await |
405 | 411 | .is_err()); |
406 | 412 | } |
| 413 | +/// Successful melt with unique blinded messages |
| 414 | +pub async fn add_melt_request_unique_blinded_messages<DB>(db: DB) |
| 415 | +where |
| 416 | + DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>, |
| 417 | +{ |
| 418 | + let inputs_amount = Amount::from(100u64); |
| 419 | + let inputs_fee = Amount::from(1u64); |
| 420 | + let keyset_id = Id::from_str("001711afb1de20cb").unwrap(); |
| 421 | + |
| 422 | + // Create a dummy blinded message |
| 423 | + let blinded_secret = SecretKey::generate().public_key(); |
| 424 | + let blinded_message = cashu::BlindedMessage { |
| 425 | + blinded_secret, |
| 426 | + keyset_id, |
| 427 | + amount: Amount::from(100u64), |
| 428 | + witness: None, |
| 429 | + }; |
| 430 | + let blinded_messages = vec![blinded_message]; |
| 431 | + |
| 432 | + let mut tx = Database::begin_transaction(&db).await.unwrap(); |
| 433 | + let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11); |
| 434 | + tx.add_melt_quote(quote.clone()).await.unwrap(); |
| 435 | + tx.add_melt_request("e.id, inputs_amount, inputs_fee) |
| 436 | + .await |
| 437 | + .unwrap(); |
| 438 | + tx.add_blinded_messages(Some("e.id), &blinded_messages) |
| 439 | + .await |
| 440 | + .unwrap(); |
| 441 | + tx.commit().await.unwrap(); |
| 442 | + |
| 443 | + // Verify retrieval |
| 444 | + let mut tx = Database::begin_transaction(&db).await.unwrap(); |
| 445 | + let retrieved = tx |
| 446 | + .get_melt_request_and_blinded_messages("e.id) |
| 447 | + .await |
| 448 | + .unwrap() |
| 449 | + .unwrap(); |
| 450 | + assert_eq!(retrieved.inputs_amount, inputs_amount); |
| 451 | + assert_eq!(retrieved.inputs_fee, inputs_fee); |
| 452 | + assert_eq!(retrieved.change_outputs.len(), 1); |
| 453 | + assert_eq!(retrieved.change_outputs[0].amount, Amount::from(100u64)); |
| 454 | + tx.commit().await.unwrap(); |
| 455 | +} |
| 456 | + |
| 457 | +/// Reject melt with duplicate blinded message (already signed) |
| 458 | +pub async fn reject_melt_duplicate_blinded_signature<DB>(db: DB) |
| 459 | +where |
| 460 | + DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>, |
| 461 | +{ |
| 462 | + let quote_id1 = QuoteId::new_uuid(); |
| 463 | + let inputs_amount = Amount::from(100u64); |
| 464 | + let inputs_fee = Amount::from(1u64); |
| 465 | + let keyset_id = Id::from_str("001711afb1de20cb").unwrap(); |
| 466 | + |
| 467 | + // Create a dummy blinded message |
| 468 | + let blinded_secret = SecretKey::generate().public_key(); |
| 469 | + let blinded_message = cashu::BlindedMessage { |
| 470 | + blinded_secret, |
| 471 | + keyset_id, |
| 472 | + amount: Amount::from(100u64), |
| 473 | + witness: None, |
| 474 | + }; |
| 475 | + let blinded_messages = vec![blinded_message.clone()]; |
| 476 | + |
| 477 | + // First, "sign" it by adding to blind_signature (simulate successful mint) |
| 478 | + let mut tx = Database::begin_transaction(&db).await.unwrap(); |
| 479 | + let c = SecretKey::generate().public_key(); |
| 480 | + let blind_sig = cashu::BlindSignature { |
| 481 | + amount: Amount::from(100u64), |
| 482 | + keyset_id, |
| 483 | + c, |
| 484 | + dleq: None, |
| 485 | + }; |
| 486 | + let blinded_secrets = vec![blinded_message.blinded_secret]; |
| 487 | + tx.add_blind_signatures(&blinded_secrets, &[blind_sig], Some(quote_id1)) |
| 488 | + .await |
| 489 | + .unwrap(); |
| 490 | + tx.commit().await.unwrap(); |
| 491 | + |
| 492 | + // Now try to add melt request with the same blinded message - should fail due to constraint |
| 493 | + let mut tx = Database::begin_transaction(&db).await.unwrap(); |
| 494 | + let quote2 = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11); |
| 495 | + tx.add_melt_quote(quote2.clone()).await.unwrap(); |
| 496 | + tx.add_melt_request("e2.id, inputs_amount, inputs_fee) |
| 497 | + .await |
| 498 | + .unwrap(); |
| 499 | + let result = tx |
| 500 | + .add_blinded_messages(Some("e2.id), &blinded_messages) |
| 501 | + .await; |
| 502 | + assert!(result.is_err() && matches!(result.unwrap_err(), Error::Duplicate)); |
| 503 | + tx.rollback().await.unwrap(); // Rollback to avoid partial state |
| 504 | +} |
| 505 | + |
| 506 | +/// Reject duplicate blinded message insert via DB constraint (different quotes) |
| 507 | +pub async fn reject_duplicate_blinded_message_db_constraint<DB>(db: DB) |
| 508 | +where |
| 509 | + DB: Database<Error> + KeysDatabase<Err = Error>, |
| 510 | +{ |
| 511 | + let inputs_amount = Amount::from(100u64); |
| 512 | + let inputs_fee = Amount::from(1u64); |
| 513 | + let keyset_id = Id::from_str("001711afb1de20cb").unwrap(); |
| 514 | + |
| 515 | + // Create a dummy blinded message |
| 516 | + let blinded_secret = SecretKey::generate().public_key(); |
| 517 | + let blinded_message = cashu::BlindedMessage { |
| 518 | + blinded_secret, |
| 519 | + keyset_id, |
| 520 | + amount: Amount::from(100u64), |
| 521 | + witness: None, |
| 522 | + }; |
| 523 | + let blinded_messages = vec![blinded_message]; |
| 524 | + |
| 525 | + // First insert succeeds |
| 526 | + let mut tx = Database::begin_transaction(&db).await.unwrap(); |
| 527 | + let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11); |
| 528 | + tx.add_melt_quote(quote.clone()).await.unwrap(); |
| 529 | + tx.add_melt_request("e.id, inputs_amount, inputs_fee) |
| 530 | + .await |
| 531 | + .unwrap(); |
| 532 | + assert!(tx |
| 533 | + .add_blinded_messages(Some("e.id), &blinded_messages) |
| 534 | + .await |
| 535 | + .is_ok()); |
| 536 | + tx.commit().await.unwrap(); |
| 537 | + |
| 538 | + // Second insert with same blinded_message but different quote_id should fail due to unique constraint on blinded_message |
| 539 | + let mut tx = Database::begin_transaction(&db).await.unwrap(); |
| 540 | + let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11); |
| 541 | + tx.add_melt_quote(quote.clone()).await.unwrap(); |
| 542 | + tx.add_melt_request("e.id, inputs_amount, inputs_fee) |
| 543 | + .await |
| 544 | + .unwrap(); |
| 545 | + let result = tx |
| 546 | + .add_blinded_messages(Some("e.id), &blinded_messages) |
| 547 | + .await; |
| 548 | + // Expect a database error due to unique violation |
| 549 | + assert!(result.is_err()); // Specific error might be DB-specific, e.g., SqliteError or PostgresError |
| 550 | + tx.rollback().await.unwrap(); |
| 551 | +} |
| 552 | + |
| 553 | +/// Cleanup of melt request after processing |
| 554 | +pub async fn cleanup_melt_request_after_processing<DB>(db: DB) |
| 555 | +where |
| 556 | + DB: Database<Error> + KeysDatabase<Err = Error>, |
| 557 | +{ |
| 558 | + let inputs_amount = Amount::from(100u64); |
| 559 | + let inputs_fee = Amount::from(1u64); |
| 560 | + let keyset_id = Id::from_str("001711afb1de20cb").unwrap(); |
| 561 | + |
| 562 | + // Create dummy blinded message |
| 563 | + let blinded_secret = SecretKey::generate().public_key(); |
| 564 | + let blinded_message = cashu::BlindedMessage { |
| 565 | + blinded_secret, |
| 566 | + keyset_id, |
| 567 | + amount: Amount::from(100u64), |
| 568 | + witness: None, |
| 569 | + }; |
| 570 | + let blinded_messages = vec![blinded_message]; |
| 571 | + |
| 572 | + // Insert melt request |
| 573 | + let mut tx1 = Database::begin_transaction(&db).await.unwrap(); |
| 574 | + let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11); |
| 575 | + tx1.add_melt_quote(quote.clone()).await.unwrap(); |
| 576 | + tx1.add_melt_request("e.id, inputs_amount, inputs_fee) |
| 577 | + .await |
| 578 | + .unwrap(); |
| 579 | + tx1.add_blinded_messages(Some("e.id), &blinded_messages) |
| 580 | + .await |
| 581 | + .unwrap(); |
| 582 | + tx1.commit().await.unwrap(); |
| 583 | + |
| 584 | + // Simulate processing: get and delete |
| 585 | + let mut tx2 = Database::begin_transaction(&db).await.unwrap(); |
| 586 | + let _retrieved = tx2 |
| 587 | + .get_melt_request_and_blinded_messages("e.id) |
| 588 | + .await |
| 589 | + .unwrap() |
| 590 | + .unwrap(); |
| 591 | + tx2.delete_melt_request("e.id).await.unwrap(); |
| 592 | + tx2.commit().await.unwrap(); |
| 593 | + |
| 594 | + // Verify melt_request is deleted |
| 595 | + let mut tx3 = Database::begin_transaction(&db).await.unwrap(); |
| 596 | + let retrieved = tx3 |
| 597 | + .get_melt_request_and_blinded_messages("e.id) |
| 598 | + .await |
| 599 | + .unwrap(); |
| 600 | + assert!(retrieved.is_none()); |
| 601 | + tx3.commit().await.unwrap(); |
| 602 | +} |
0 commit comments