diff --git a/crates/cdk-common/src/error.rs b/crates/cdk-common/src/error.rs index c21b2988..23aaeb5e 100644 --- a/crates/cdk-common/src/error.rs +++ b/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), diff --git a/crates/cdk-integration-tests/src/init_pure_tests.rs b/crates/cdk-integration-tests/src/init_pure_tests.rs index 73417233..91101834 100644 --- a/crates/cdk-integration-tests/src/init_pure_tests.rs +++ b/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>>, @@ -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, "e.id, 60).await?; + wallet + .wait_for_payment("e, Duration::from_secs(60)) + .await?; Ok(wallet .mint("e.id, split_target.unwrap_or_default(), None) diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index 0643cae6..2f4dc70d 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/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, amount: Amount) { .await .expect("Could not get mint quote"); - wait_for_mint_to_be_paid(&wallet, "e.id, 60) + wallet + .wait_for_payment("e, Duration::from_secs(60)) .await - .expect("Waiting for mint failed"); + .expect("wait for mint failed"); let _proofs = wallet .mint("e.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"); diff --git a/crates/cdk-integration-tests/tests/bolt12.rs b/crates/cdk-integration-tests/tests/bolt12.rs index 973a1894..ca25a4c9 100644 --- a/crates/cdk-integration-tests/tests/bolt12.rs +++ b/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, "e_one.id, 60).await?; + + wallet_one + .wait_for_payment("e_one, Duration::from_secs(60)) + .await?; + let proofs_one = wallet_one .mint_bolt12("e_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, "e_two.id, 60).await?; + + wallet_two + .wait_for_payment("e_two, Duration::from_secs(60)) + .await?; let proofs_two = wallet_two .mint_bolt12("e_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?; diff --git a/crates/cdk-integration-tests/tests/fake_auth.rs b/crates/cdk-integration-tests/tests/fake_auth.rs index 16c3123c..6a934254 100644 --- a/crates/cdk-integration-tests/tests/fake_auth.rs +++ b/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,12 @@ async fn test_mint_with_auth() { let mint_amount: Amount = 100.into(); - let mint_quote = wallet - .mint_quote(mint_amount, None) + let (_, proofs) = wallet + .mint_once_paid(mint_amount, None, Duration::from_secs(10)) .await - .expect("failed to get mint quote"); + .unwrap(); - wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60) - .await - .expect("failed to wait for payment"); - - 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); } diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs index c4066c5b..6d3cb3f2 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/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(); diff --git a/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs b/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs index c1dcd4f0..63046092 100644 --- a/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs +++ b/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(); diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index fabc7c32..fad09655 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/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, "e.id, 60) + wallet1 + .wait_for_payment("e, 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, "e.id, 60) + wallet2 + .wait_for_payment("e, 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, "e.id, 60) + wallet + .wait_for_payment("e, Duration::from_secs(60)) .await .unwrap(); diff --git a/crates/cdk-integration-tests/tests/test_fees.rs b/crates/cdk-integration-tests/tests/test_fees.rs index bfbcae9f..a2352f6c 100644 --- a/crates/cdk-integration-tests/tests/test_fees.rs +++ b/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(); diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index 73d3a1e7..b00373e9 100644 --- a/crates/cdk/Cargo.toml +++ b/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 diff --git a/crates/cdk/examples/auth_wallet.rs b/crates/cdk/examples/auth_wallet.rs index 73b614a2..5b23a4f2 100644 --- a/crates/cdk/examples/auth_wallet.rs +++ b/crates/cdk/examples/auth_wallet.rs @@ -1,9 +1,9 @@ use std::sync::Arc; +use std::time::Duration; -use cdk::amount::SplitTarget; use cdk::error::Error; -use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload}; -use cdk::wallet::{SendOptions, Wallet, WalletSubscription}; +use cdk::nuts::CurrencyUnit; +use cdk::wallet::{SendOptions, Wallet}; use cdk::{Amount, OidcClient}; use cdk_common::{MintInfo, ProofsMethods}; use cdk_sqlite::wallet::memory; @@ -57,27 +57,12 @@ async fn main() -> Result<(), Error> { .await .expect("Could not mint blind auth"); - // Request a mint quote from the wallet - let quote = wallet.mint_quote(amount, None).await?; - - // Subscribe to updates on the mint quote state - let mut subscription = wallet - .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote - .id - .clone()])) - .await; - - // Wait for the mint quote to be paid - while let Some(msg) = subscription.recv().await { - if let NotificationPayload::MintQuoteBolt11Response(response) = msg { - if response.state == MintQuoteState::Paid { - break; - } - } - } + let (_invoice_to_pay, proofs) = wallet + .mint_once_paid(amount, None, Duration::from_secs(10)) + .await?; // Mint the received amount - let receive_amount = wallet.mint("e.id, SplitTarget::default(), None).await?; + let receive_amount = proofs.await?; println!("Received: {}", receive_amount.total_amount()?); diff --git a/crates/cdk/examples/melt-token.rs b/crates/cdk/examples/melt-token.rs index b3aeb402..c97106fe 100644 --- a/crates/cdk/examples/melt-token.rs +++ b/crates/cdk/examples/melt-token.rs @@ -1,13 +1,13 @@ use std::sync::Arc; +use std::time::Duration; use bitcoin::hashes::{sha256, Hash}; use bitcoin::hex::prelude::FromHex; use bitcoin::secp256k1::Secp256k1; -use cdk::amount::SplitTarget; use cdk::error::Error; use cdk::nuts::nut00::ProofsMethods; -use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload, SecretKey}; -use cdk::wallet::{Wallet, WalletSubscription}; +use cdk::nuts::{CurrencyUnit, SecretKey}; +use cdk::wallet::Wallet; use cdk::Amount; use cdk_sqlite::wallet::memory; use lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret}; @@ -29,28 +29,12 @@ async fn main() -> Result<(), Error> { // Create a new wallet let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None)?; - // Request a mint quote from the wallet - let quote = wallet.mint_quote(amount, None).await?; - println!("Quote: {:#?}", quote); - - // Subscribe to updates on the mint quote state - let mut subscription = wallet - .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote - .id - .clone()])) - .await; - - // Wait for the mint quote to be paid - while let Some(msg) = subscription.recv().await { - if let NotificationPayload::MintQuoteBolt11Response(response) = msg { - if response.state == MintQuoteState::Paid { - break; - } - } - } + let (_invoice_to_pay, proofs) = wallet + .mint_once_paid(amount, None, Duration::from_secs(10)) + .await?; // Mint the received amount - let proofs = wallet.mint("e.id, SplitTarget::default(), None).await?; + let proofs = proofs.await?; let receive_amount = proofs.total_amount()?; println!("Received {} from mint {}", receive_amount, mint_url); diff --git a/crates/cdk/examples/mint-token.rs b/crates/cdk/examples/mint-token.rs index 92a3cc95..64fe3d92 100644 --- a/crates/cdk/examples/mint-token.rs +++ b/crates/cdk/examples/mint-token.rs @@ -1,10 +1,10 @@ use std::sync::Arc; +use std::time::Duration; -use cdk::amount::SplitTarget; use cdk::error::Error; use cdk::nuts::nut00::ProofsMethods; -use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload}; -use cdk::wallet::{SendOptions, Wallet, WalletSubscription}; +use cdk::nuts::CurrencyUnit; +use cdk::wallet::{SendOptions, Wallet}; use cdk::Amount; use cdk_sqlite::wallet::memory; use rand::random; @@ -35,28 +35,12 @@ async fn main() -> Result<(), Error> { // Create a new wallet let wallet = Wallet::new(mint_url, unit, localstore, seed, None)?; - // Request a mint quote from the wallet - let quote = wallet.mint_quote(amount, None).await?; - println!("Quote: {:#?}", quote); - - // Subscribe to updates on the mint quote state - let mut subscription = wallet - .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote - .id - .clone()])) - .await; - - // Wait for the mint quote to be paid - while let Some(msg) = subscription.recv().await { - if let NotificationPayload::MintQuoteBolt11Response(response) = msg { - if response.state == MintQuoteState::Paid { - break; - } - } - } + let (_invoice_to_pay, proofs) = wallet + .mint_once_paid(amount, None, Duration::from_secs(10)) + .await?; // Mint the received amount - let proofs = wallet.mint("e.id, SplitTarget::default(), None).await?; + let proofs = proofs.await?; let receive_amount = proofs.total_amount()?; println!("Received {} from mint {}", receive_amount, mint_url); diff --git a/crates/cdk/examples/p2pk.rs b/crates/cdk/examples/p2pk.rs index 7b7150a0..eaf56fbb 100644 --- a/crates/cdk/examples/p2pk.rs +++ b/crates/cdk/examples/p2pk.rs @@ -1,9 +1,9 @@ use std::sync::Arc; +use std::time::Duration; -use cdk::amount::SplitTarget; use cdk::error::Error; -use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload, SecretKey, SpendingConditions}; -use cdk::wallet::{ReceiveOptions, SendOptions, Wallet, WalletSubscription}; +use cdk::nuts::{CurrencyUnit, SecretKey, SpendingConditions}; +use cdk::wallet::{ReceiveOptions, SendOptions, Wallet}; use cdk::Amount; use cdk_sqlite::wallet::memory; use rand::random; @@ -34,29 +34,12 @@ async fn main() -> Result<(), Error> { // Create a new wallet let wallet = Wallet::new(mint_url, unit, localstore, seed, None).unwrap(); - // Request a mint quote from the wallet - let quote = wallet.mint_quote(amount, None).await?; - - println!("Minting nuts ..."); - - // Subscribe to updates on the mint quote state - let mut subscription = wallet - .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote - .id - .clone()])) - .await; - - // Wait for the mint quote to be paid - while let Some(msg) = subscription.recv().await { - if let NotificationPayload::MintQuoteBolt11Response(response) = msg { - if response.state == MintQuoteState::Paid { - break; - } - } - } + let (_invoice_to_pay, proofs) = wallet + .mint_once_paid(amount, None, Duration::from_secs(10)) + .await?; // Mint the received amount - let received_proofs = wallet.mint("e.id, SplitTarget::default(), None).await?; + let received_proofs = proofs.await?; println!( "Minted nuts: {:?}", received_proofs diff --git a/crates/cdk/examples/proof-selection.rs b/crates/cdk/examples/proof-selection.rs index 73ae18ce..05369291 100644 --- a/crates/cdk/examples/proof-selection.rs +++ b/crates/cdk/examples/proof-selection.rs @@ -2,11 +2,11 @@ use std::collections::HashMap; use std::sync::Arc; +use std::time::Duration; -use cdk::amount::SplitTarget; use cdk::nuts::nut00::ProofsMethods; -use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload}; -use cdk::wallet::{Wallet, WalletSubscription}; +use cdk::nuts::CurrencyUnit; +use cdk::wallet::Wallet; use cdk::Amount; use cdk_common::nut02::KeySetInfosMethods; use cdk_sqlite::wallet::memory; @@ -31,28 +31,12 @@ async fn main() -> Result<(), Box> { for amount in [64] { let amount = Amount::from(amount); - // Request a mint quote from the wallet - let quote = wallet.mint_quote(amount, None).await?; - println!("Pay request: {}", quote.request); - - // Subscribe to the wallet for updates on the mint quote state - let mut subscription = wallet - .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote - .id - .clone()])) - .await; - - // Wait for the mint quote to be paid - while let Some(msg) = subscription.recv().await { - if let NotificationPayload::MintQuoteBolt11Response(response) = msg { - if response.state == MintQuoteState::Paid { - break; - } - } - } + let (_invoice_to_pay, proofs) = wallet + .mint_once_paid(amount, None, Duration::from_secs(10)) + .await?; // Mint the received amount - let proofs = wallet.mint("e.id, SplitTarget::default(), None).await?; + let proofs = proofs.await?; let receive_amount = proofs.total_amount()?; println!("Minted {}", receive_amount); } diff --git a/crates/cdk/examples/wallet.rs b/crates/cdk/examples/wallet.rs index 37ea2e3e..3d036c6c 100644 --- a/crates/cdk/examples/wallet.rs +++ b/crates/cdk/examples/wallet.rs @@ -1,14 +1,12 @@ use std::sync::Arc; use std::time::Duration; -use cdk::amount::SplitTarget; use cdk::nuts::nut00::ProofsMethods; -use cdk::nuts::{CurrencyUnit, MintQuoteState}; +use cdk::nuts::CurrencyUnit; use cdk::wallet::{SendOptions, Wallet}; use cdk::Amount; use cdk_sqlite::wallet::memory; use rand::random; -use tokio::time::sleep; #[tokio::main] async fn main() -> Result<(), Box> { @@ -26,34 +24,12 @@ async fn main() -> Result<(), Box> { // Create a new wallet let wallet = Wallet::new(mint_url, unit, localstore, seed, None)?; - // Request a mint quote from the wallet - let quote = wallet.mint_quote(amount, None).await?; - - println!("Pay request: {}", quote.request); - - // Check the quote state in a loop with a timeout - let timeout = Duration::from_secs(60); // Set a timeout duration - let start = std::time::Instant::now(); - - loop { - let status = wallet.mint_quote_state("e.id).await?; - - if status.state == MintQuoteState::Paid { - break; - } - - if start.elapsed() >= timeout { - eprintln!("Timeout while waiting for mint quote to be paid"); - return Err("Timeout while waiting for mint quote to be paid".into()); - } - - println!("Quote state: {}", status.state); - - sleep(Duration::from_secs(5)).await; - } + let (_invoice_to_pay, proofs) = wallet + .mint_once_paid(amount, None, Duration::from_secs(10)) + .await?; // Mint the received amount - let proofs = wallet.mint("e.id, SplitTarget::default(), None).await?; + let proofs = proofs.await?; let receive_amount = proofs.total_amount()?; println!("Minted {}", receive_amount); diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index e683eaa7..f0207901 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/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}; diff --git a/crates/cdk/src/wallet/wait.rs b/crates/cdk/src/wallet/wait.rs new file mode 100644 index 00000000..0774e169 --- /dev/null +++ b/crates/cdk/src/wallet/wait.rs @@ -0,0 +1,138 @@ +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 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 { + #[inline(always)] + async fn wait_and_mint_quote( + &self, + quote: MintQuote, + amount_split_target: SplitTarget, + spending_conditions: Option, + timeout_duration: Duration, + ) -> Result { + self.wait_for_payment("e, timeout_duration).await?; + self.mint("e.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, + timeout_duration: Duration, + ) -> Result<(String, impl Future> + '_), Error> { + self.mint_once_paid_ex( + amount, + description, + Default::default(), + None, + timeout_duration, + ) + .await + } + + /// Similar function to mint_once_paid but with no default options + pub async fn mint_once_paid_ex( + &self, + amount: Amount, + description: Option, + amount_split_target: SplitTarget, + spending_conditions: Option, + timeout_duration: Duration, + ) -> Result<(String, impl Future> + '_), 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( + &self, + event: T, + timeout_duration: Duration, + ) -> BoxFuture<'_, Result<(), Error>> + where + T: Into, + { + let subs = self.subscribe::(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)? + }) + } +}