Quellcode durchsuchen

Add `wait_for_payment` function in the wallet

Fixes #941
Cesar Rodas vor 2 Monaten
Ursprung
Commit
32ca4da7aa

+ 4 - 0
crates/cdk-common/src/error.rs

@@ -111,6 +111,10 @@ pub enum Error {
     #[error("Could not parse bolt12")]
     Bolt12parse,
 
+    /// Operation timeout
+    #[error("Operation timeout")]
+    Timeout,
+
     /// Internal Error - Send error
     #[error("Internal send error: {0}")]
     SendError(String),

+ 4 - 3
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -3,6 +3,7 @@ use std::fmt::{Debug, Formatter};
 use std::path::PathBuf;
 use std::str::FromStr;
 use std::sync::Arc;
+use std::time::Duration;
 use std::{env, fs};
 
 use anyhow::{anyhow, bail, Result};
@@ -28,8 +29,6 @@ use tokio::sync::RwLock;
 use tracing_subscriber::EnvFilter;
 use uuid::Uuid;
 
-use crate::wait_for_mint_to_be_paid;
-
 pub struct DirectMintConnection {
     pub mint: Mint,
     auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
@@ -362,7 +361,9 @@ pub async fn fund_wallet(
     let desired_amount = Amount::from(amount);
     let quote = wallet.mint_quote(desired_amount, None).await?;
 
-    wait_for_mint_to_be_paid(&wallet, &quote.id, 60).await?;
+    wallet
+        .wait_for_payment(&quote, Duration::from_secs(60))
+        .await?;
 
     Ok(wallet
         .mint(&quote.id, split_target.unwrap_or_default(), None)

+ 6 - 88
crates/cdk-integration-tests/src/lib.rs

@@ -21,15 +21,14 @@ use std::path::Path;
 use std::sync::Arc;
 
 use anyhow::{anyhow, bail, Result};
-use cashu::{Bolt11Invoice, PaymentMethod};
+use cashu::Bolt11Invoice;
 use cdk::amount::{Amount, SplitTarget};
-use cdk::nuts::{MintQuoteState, NotificationPayload, State};
-use cdk::wallet::WalletSubscription;
+use cdk::nuts::State;
 use cdk::Wallet;
 use cdk_fake_wallet::create_fake_invoice;
 use init_regtest::{get_lnd_dir, LND_RPC_ADDR};
 use ln_regtest_rs::ln_client::{LightningClient, LndClient};
-use tokio::time::{sleep, timeout, Duration};
+use tokio::time::Duration;
 
 pub mod cli;
 pub mod init_auth_mint;
@@ -43,9 +42,10 @@ pub async fn fund_wallet(wallet: Arc<Wallet>, amount: Amount) {
         .await
         .expect("Could not get mint quote");
 
-    wait_for_mint_to_be_paid(&wallet, &quote.id, 60)
+    wallet
+        .wait_for_payment(&quote, Duration::from_secs(60))
         .await
-        .expect("Waiting for mint failed");
+        .expect("wait for mint failed");
 
     let _proofs = wallet
         .mint(&quote.id, SplitTarget::default(), None)
@@ -104,88 +104,6 @@ pub async fn attempt_to_swap_pending(wallet: &Wallet) -> Result<()> {
     Ok(())
 }
 
-pub async fn wait_for_mint_to_be_paid(
-    wallet: &Wallet,
-    mint_quote_id: &str,
-    timeout_secs: u64,
-) -> Result<()> {
-    let mut subscription = wallet
-        .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![
-            mint_quote_id.to_owned(),
-        ]))
-        .await;
-    // Create the timeout future
-    let wait_future = async {
-        while let Some(msg) = subscription.recv().await {
-            if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
-                if response.state == MintQuoteState::Paid {
-                    return Ok(());
-                }
-            } else if let NotificationPayload::MintQuoteBolt12Response(response) = msg {
-                if response.amount_paid > Amount::ZERO {
-                    return Ok(());
-                }
-            }
-        }
-        Err(anyhow!("Subscription ended without quote being paid"))
-    };
-
-    let timeout_future = timeout(Duration::from_secs(timeout_secs), wait_future);
-
-    let check_interval = Duration::from_secs(5);
-
-    let method = wallet
-        .localstore
-        .get_mint_quote(mint_quote_id)
-        .await?
-        .map(|q| q.payment_method)
-        .unwrap_or_default();
-
-    let periodic_task = async {
-        loop {
-            match method {
-                PaymentMethod::Bolt11 => match wallet.mint_quote_state(mint_quote_id).await {
-                    Ok(result) => {
-                        if result.state == MintQuoteState::Paid {
-                            tracing::info!("mint quote paid via poll");
-                            return Ok(());
-                        }
-                    }
-                    Err(e) => {
-                        tracing::error!("Could not check mint quote status: {:?}", e);
-                    }
-                },
-                PaymentMethod::Bolt12 => {
-                    match wallet.mint_bolt12_quote_state(mint_quote_id).await {
-                        Ok(result) => {
-                            if result.amount_paid > Amount::ZERO {
-                                return Ok(());
-                            }
-                        }
-                        Err(e) => {
-                            tracing::error!("Could not check mint quote status: {:?}", e);
-                        }
-                    }
-                }
-                PaymentMethod::Custom(_) => (),
-            }
-            sleep(check_interval).await;
-        }
-    };
-
-    tokio::select! {
-        result = timeout_future => {
-            match result {
-                Ok(payment_result) => payment_result,
-                Err(_) => Err(anyhow!("Timeout waiting for mint quote ({}) to be paid", mint_quote_id)),
-            }
-        }
-        result = periodic_task => {
-            result // Now propagates the result from periodic checks
-        }
-    }
-}
-
 // This is the ln wallet we use to send/receive ln payements as the wallet
 pub async fn init_lnd_client(work_dir: &Path) -> LndClient {
     let lnd_dir = get_lnd_dir(work_dir, "one");

+ 24 - 8
crates/cdk-integration-tests/tests/bolt12.rs

@@ -1,6 +1,7 @@
 use std::env;
 use std::path::PathBuf;
 use std::sync::Arc;
+use std::time::Duration;
 
 use anyhow::{bail, Result};
 use bip39::Mnemonic;
@@ -8,8 +9,8 @@ use cashu::amount::SplitTarget;
 use cashu::nut23::Amountless;
 use cashu::{Amount, CurrencyUnit, MintRequest, PreMintSecrets, ProofsMethods};
 use cdk::wallet::{HttpClient, MintConnector, Wallet};
+use cdk_integration_tests::get_mint_url_from_env;
 use cdk_integration_tests::init_regtest::{get_cln_dir, get_temp_dir};
-use cdk_integration_tests::{get_mint_url_from_env, wait_for_mint_to_be_paid};
 use cdk_sqlite::wallet::memory;
 use ln_regtest_rs::ln_client::ClnClient;
 
@@ -115,7 +116,9 @@ async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
         .await
         .unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
+        .await?;
 
     wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
 
@@ -127,11 +130,13 @@ async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
     assert_eq!(proofs.total_amount().unwrap(), 10.into());
 
     cln_client
-        .pay_bolt12_offer(Some(11_000), mint_quote.request)
+        .pay_bolt12_offer(Some(11_000), mint_quote.request.clone())
         .await
         .unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
+        .await?;
 
     wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
 
@@ -181,7 +186,11 @@ async fn test_regtest_bolt12_multiple_wallets() -> Result<()> {
     cln_client
         .pay_bolt12_offer(None, quote_one.request.clone())
         .await?;
-    wait_for_mint_to_be_paid(&wallet_one, &quote_one.id, 60).await?;
+
+    wallet_one
+        .wait_for_payment(&quote_one, Duration::from_secs(60))
+        .await?;
+
     let proofs_one = wallet_one
         .mint_bolt12(&quote_one.id, None, SplitTarget::default(), None)
         .await?;
@@ -195,7 +204,10 @@ async fn test_regtest_bolt12_multiple_wallets() -> Result<()> {
     cln_client
         .pay_bolt12_offer(None, quote_two.request.clone())
         .await?;
-    wait_for_mint_to_be_paid(&wallet_two, &quote_two.id, 60).await?;
+
+    wallet_two
+        .wait_for_payment(&quote_two, Duration::from_secs(60))
+        .await?;
 
     let proofs_two = wallet_two
         .mint_bolt12(&quote_two.id, None, SplitTarget::default(), None)
@@ -271,7 +283,9 @@ async fn test_regtest_bolt12_melt() -> Result<()> {
         .await?;
 
     // Wait for payment to be processed
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
+        .await?;
 
     let offer = cln_client
         .get_bolt12_offer(Some(10_000), true, "hhhhhhhh".to_string())
@@ -330,7 +344,9 @@ async fn test_regtest_bolt12_mint_extra() -> Result<()> {
         .pay_bolt12_offer(Some(pay_amount_msats), mint_quote.request.clone())
         .await?;
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 10).await?;
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(10))
+        .await?;
 
     let state = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
 

+ 12 - 12
crates/cdk-integration-tests/tests/fake_auth.rs

@@ -1,6 +1,7 @@
 use std::env;
 use std::str::FromStr;
 use std::sync::Arc;
+use std::time::Duration;
 
 use bip39::Mnemonic;
 use cashu::{MintAuthRequest, MintInfo};
@@ -15,7 +16,7 @@ use cdk::nuts::{
 use cdk::wallet::{AuthHttpClient, AuthMintConnector, HttpClient, MintConnector, WalletBuilder};
 use cdk::{Error, OidcClient};
 use cdk_fake_wallet::create_fake_invoice;
-use cdk_integration_tests::{fund_wallet, wait_for_mint_to_be_paid};
+use cdk_integration_tests::fund_wallet;
 use cdk_sqlite::wallet::memory;
 
 const MINT_URL: &str = "http://127.0.0.1:8087";
@@ -329,19 +330,18 @@ async fn test_mint_with_auth() {
 
     let mint_amount: Amount = 100.into();
 
-    let mint_quote = wallet
-        .mint_quote(mint_amount, None)
-        .await
-        .expect("failed to get mint quote");
-
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    let (_, proofs) = wallet
+        .mint_once_paid(
+            mint_amount,
+            None,
+            SplitTarget::default(),
+            None,
+            Duration::from_secs(10),
+        )
         .await
-        .expect("failed to wait for payment");
+        .unwrap();
 
-    let proofs = wallet
-        .mint(&mint_quote.id, SplitTarget::default(), None)
-        .await
-        .expect("could not mint");
+    let proofs = proofs.await.expect("could not mint");
 
     assert!(proofs.total_amount().expect("Could not get proofs amount") == mint_amount);
 }

+ 46 - 23
crates/cdk-integration-tests/tests/fake_wallet.rs

@@ -15,6 +15,7 @@
 //! - Duplicate proof detection
 
 use std::sync::Arc;
+use std::time::Duration;
 
 use bip39::Mnemonic;
 use cashu::Amount;
@@ -27,7 +28,7 @@ use cdk::nuts::{
 use cdk::wallet::types::TransactionDirection;
 use cdk::wallet::{HttpClient, MintConnector, Wallet};
 use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
-use cdk_integration_tests::{attempt_to_swap_pending, wait_for_mint_to_be_paid};
+use cdk_integration_tests::attempt_to_swap_pending;
 use cdk_sqlite::wallet::memory;
 
 const MINT_URL: &str = "http://127.0.0.1:8086";
@@ -46,7 +47,8 @@ async fn test_fake_tokens_pending() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -88,7 +90,8 @@ async fn test_fake_melt_payment_fail() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -153,7 +156,8 @@ async fn test_fake_melt_payment_fail_and_check() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -201,7 +205,8 @@ async fn test_fake_melt_payment_return_fail_status() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -264,7 +269,8 @@ async fn test_fake_melt_payment_error_unknown() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -327,7 +333,8 @@ async fn test_fake_melt_payment_err_paid() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -368,7 +375,8 @@ async fn test_fake_melt_change_in_quote() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -437,7 +445,8 @@ async fn test_fake_mint_with_witness() {
     .expect("failed to create new wallet");
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -465,7 +474,8 @@ async fn test_fake_mint_without_witness() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -505,7 +515,8 @@ async fn test_fake_mint_with_wrong_witness() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -551,7 +562,8 @@ async fn test_fake_mint_inflated() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -609,7 +621,8 @@ async fn test_fake_mint_multiple_units() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -688,7 +701,8 @@ async fn test_fake_mint_multiple_unit_swap() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -709,7 +723,8 @@ async fn test_fake_mint_multiple_unit_swap() {
 
     let mint_quote = wallet_usd.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet_usd, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -802,7 +817,8 @@ async fn test_fake_mint_multiple_unit_melt() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -825,7 +841,8 @@ async fn test_fake_mint_multiple_unit_melt() {
     let mint_quote = wallet_usd.mint_quote(100.into(), None).await.unwrap();
     println!("Minted quote usd");
 
-    wait_for_mint_to_be_paid(&wallet_usd, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -920,7 +937,8 @@ async fn test_fake_mint_input_output_mismatch() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -978,7 +996,8 @@ async fn test_fake_mint_swap_inflated() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -1022,7 +1041,8 @@ async fn test_fake_mint_swap_spend_after_fail() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -1093,7 +1113,8 @@ async fn test_fake_mint_melt_spend_after_fail() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -1165,7 +1186,8 @@ async fn test_fake_mint_duplicate_proofs_swap() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -1245,7 +1267,8 @@ async fn test_fake_mint_duplicate_proofs_melt() {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 

+ 11 - 8
crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs

@@ -22,9 +22,7 @@ use cdk::amount::{Amount, SplitTarget};
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, MeltQuoteState, NotificationPayload, State};
 use cdk::wallet::{HttpClient, MintConnector, Wallet};
-use cdk_integration_tests::{
-    create_invoice_for_env, get_mint_url_from_env, pay_if_regtest, wait_for_mint_to_be_paid,
-};
+use cdk_integration_tests::{create_invoice_for_env, get_mint_url_from_env, pay_if_regtest};
 use cdk_sqlite::wallet::memory;
 use futures::{SinkExt, StreamExt};
 use lightning_invoice::Bolt11Invoice;
@@ -111,7 +109,8 @@ async fn test_happy_mint_melt_round_trip() {
         .await
         .unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 10)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -234,7 +233,8 @@ async fn test_happy_mint() {
         .await
         .unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -281,7 +281,8 @@ async fn test_restore() {
         .await
         .unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -360,7 +361,8 @@ async fn test_fake_melt_change_in_quote() {
 
     pay_if_regtest(&get_test_temp_dir(), &bolt11).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -431,7 +433,8 @@ async fn test_pay_invoice_twice() {
         .await
         .unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 

+ 12 - 9
crates/cdk-integration-tests/tests/regtest.rs

@@ -27,9 +27,7 @@ use cdk::nuts::{
 };
 use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription};
 use cdk_integration_tests::init_regtest::{get_lnd_dir, LND_RPC_ADDR};
-use cdk_integration_tests::{
-    get_mint_url_from_env, get_second_mint_url_from_env, wait_for_mint_to_be_paid,
-};
+use cdk_integration_tests::{get_mint_url_from_env, get_second_mint_url_from_env};
 use cdk_sqlite::wallet::{self, memory};
 use futures::join;
 use ln_regtest_rs::ln_client::{LightningClient, LndClient};
@@ -84,11 +82,12 @@ async fn test_internal_payment() {
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
     lnd_client
-        .pay_invoice(mint_quote.request)
+        .pay_invoice(mint_quote.request.clone())
         .await
         .expect("failed to pay invoice");
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -119,7 +118,8 @@ async fn test_internal_payment() {
 
     let _melted = wallet.melt(&melt.id).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet_2, &mint_quote.id, 60)
+    wallet_2
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -261,7 +261,8 @@ async fn test_multimint_melt() {
         .pay_invoice(quote.request.clone())
         .await
         .expect("failed to pay invoice");
-    wait_for_mint_to_be_paid(&wallet1, &quote.id, 60)
+    wallet1
+        .wait_for_payment(&quote, Duration::from_secs(60))
         .await
         .unwrap();
     wallet1
@@ -274,7 +275,8 @@ async fn test_multimint_melt() {
         .pay_invoice(quote.request.clone())
         .await
         .expect("failed to pay invoice");
-    wait_for_mint_to_be_paid(&wallet2, &quote.id, 60)
+    wallet2
+        .wait_for_payment(&quote, Duration::from_secs(60))
         .await
         .unwrap();
     wallet2
@@ -334,7 +336,8 @@ async fn test_cached_mint() {
         .await
         .expect("failed to pay invoice");
 
-    wait_for_mint_to_be_paid(&wallet, &quote.id, 60)
+    wallet
+        .wait_for_payment(&quote, Duration::from_secs(60))
         .await
         .unwrap();
 

+ 6 - 5
crates/cdk-integration-tests/tests/test_fees.rs

@@ -1,5 +1,6 @@
 use std::str::FromStr;
 use std::sync::Arc;
+use std::time::Duration;
 
 use bip39::Mnemonic;
 use cashu::{Bolt11Invoice, ProofsMethods};
@@ -7,9 +8,7 @@ use cdk::amount::{Amount, SplitTarget};
 use cdk::nuts::CurrencyUnit;
 use cdk::wallet::{ReceiveOptions, SendKind, SendOptions, Wallet};
 use cdk_integration_tests::init_regtest::get_temp_dir;
-use cdk_integration_tests::{
-    create_invoice_for_env, get_mint_url_from_env, pay_if_regtest, wait_for_mint_to_be_paid,
-};
+use cdk_integration_tests::{create_invoice_for_env, get_mint_url_from_env, pay_if_regtest};
 use cdk_sqlite::wallet::memory;
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -29,7 +28,8 @@ async fn test_swap() {
     let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
     pay_if_regtest(&get_temp_dir(), &invoice).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 10)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 
@@ -96,7 +96,8 @@ async fn test_fake_melt_change_in_quote() {
 
     pay_if_regtest(&get_temp_dir(), &bolt11).await.unwrap();
 
-    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+    wallet
+        .wait_for_payment(&mint_quote, Duration::from_secs(60))
         .await
         .unwrap();
 

+ 1 - 1
crates/cdk/Cargo.toml

@@ -12,7 +12,7 @@ license.workspace = true
 
 [features]
 default = ["mint", "wallet", "auth"]
-wallet = ["dep:reqwest", "cdk-common/wallet", "dep:rustls"]
+wallet = ["dep:futures", "dep:reqwest", "cdk-common/wallet", "dep:rustls"]
 mint = ["dep:futures", "dep:reqwest", "cdk-common/mint", "cdk-signatory"]
 auth = ["dep:jsonwebtoken", "cdk-common/auth", "cdk-common/auth"]
 # We do not commit to a MSRV with swagger enabled

+ 1 - 0
crates/cdk/src/wallet/mod.rs

@@ -45,6 +45,7 @@ pub mod subscription;
 mod swap;
 mod transactions;
 pub mod util;
+mod wait;
 
 #[cfg(feature = "auth")]
 pub use auth::{AuthMintConnector, AuthWallet};

+ 120 - 0
crates/cdk/src/wallet/wait.rs

@@ -0,0 +1,120 @@
+use std::future::Future;
+
+use cdk_common::amount::SplitTarget;
+use cdk_common::wallet::{MeltQuote, MintQuote};
+use cdk_common::{
+    Amount, Error, MeltQuoteState, MintQuoteState, NotificationPayload, Proofs, SpendingConditions,
+};
+use futures::future::BoxFuture;
+use tokio::time::{timeout, Duration};
+
+use super::{Wallet, WalletSubscription};
+
+#[allow(private_bounds)]
+enum WaitableEvent {
+    MeltQuote(String),
+    MintQuote(String),
+}
+
+impl From<&MeltQuote> for WaitableEvent {
+    fn from(event: &MeltQuote) -> Self {
+        WaitableEvent::MeltQuote(event.id.to_owned())
+    }
+}
+
+impl From<&MintQuote> for WaitableEvent {
+    fn from(event: &MintQuote) -> Self {
+        WaitableEvent::MintQuote(event.id.to_owned())
+    }
+}
+
+impl From<WaitableEvent> for WalletSubscription {
+    fn from(val: WaitableEvent) -> Self {
+        match val {
+            WaitableEvent::MeltQuote(quote_id) => {
+                WalletSubscription::Bolt11MeltQuoteState(vec![quote_id])
+            }
+            WaitableEvent::MintQuote(quote_id) => {
+                WalletSubscription::Bolt11MintQuoteState(vec![quote_id])
+            }
+        }
+    }
+}
+
+impl Wallet {
+    async fn wait_and_mint_quote(
+        &self,
+        quote: MintQuote,
+        amount_split_target: SplitTarget,
+        spending_conditions: Option<SpendingConditions>,
+        timeout_duration: Duration,
+    ) -> Result<Proofs, Error> {
+        self.wait_for_payment(&quote, timeout_duration).await?;
+        self.mint(&quote.id, amount_split_target, spending_conditions)
+            .await
+    }
+
+    /// Mints an amount and returns the invoice to be paid, and a BoxFuture that will finalize the
+    /// mint once the invoice has been paid
+    pub async fn mint_once_paid(
+        &self,
+        amount: Amount,
+        description: Option<String>,
+        amount_split_target: SplitTarget,
+        spending_conditions: Option<SpendingConditions>,
+        timeout_duration: Duration,
+    ) -> Result<(String, impl Future<Output = Result<Proofs, Error>> + '_), Error> {
+        let quote = self.mint_quote(amount, description).await?;
+
+        Ok((
+            quote.request.clone(),
+            self.wait_and_mint_quote(
+                quote,
+                amount_split_target,
+                spending_conditions,
+                timeout_duration,
+            ),
+        ))
+    }
+
+    /// Returns a BoxFuture that will wait for payment on the given event with a timeout check
+    #[allow(private_bounds)]
+    pub fn wait_for_payment<T>(
+        &self,
+        event: T,
+        timeout_duration: Duration,
+    ) -> BoxFuture<'_, Result<(), Error>>
+    where
+        T: Into<WaitableEvent>,
+    {
+        let subs = self.subscribe::<WalletSubscription>(event.into().into());
+
+        Box::pin(async move {
+            timeout(timeout_duration, async {
+                let mut subscription = subs.await;
+                loop {
+                    match subscription.recv().await.ok_or(Error::Internal)? {
+                        NotificationPayload::MintQuoteBolt11Response(info) => {
+                            if info.state == MintQuoteState::Paid {
+                                return Ok(());
+                            }
+                        }
+                        NotificationPayload::MintQuoteBolt12Response(info) => {
+                            if info.amount_paid > Amount::ZERO {
+                                return Ok(());
+                            }
+                        }
+                        NotificationPayload::MeltQuoteBolt11Response(info) => {
+                            if info.state == MeltQuoteState::Paid {
+                                return Ok(());
+                            }
+                        }
+                        _ => {}
+                    }
+                }
+            })
+            .await
+            .map_err(|_| Error::Timeout)?
+        })
+    }
+}