Răsfoiți Sursa

feat(cdk): add Bolt12 mint quote subscription support (#976)

* feat(cdk): add Bolt12 mint quote subscription support

Extends subscription to handle Bolt12 payment method alongside existing Bolt11 support across wallet, mint, and CLI components.
thesimplekid 10 luni în urmă
părinte
comite
d3a3c30d99

+ 1 - 1
Cargo.toml

@@ -68,7 +68,7 @@ lightning = { version = "0.1.2", default-features = false, features = ["std"]}
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 thiserror = { version = "2" }
-tokio = { version = "1", default-features = false, features = ["rt", "macros", "test-util"] }
+tokio = { version = "1", default-features = false, features = ["rt", "macros", "test-util", "sync"] }
 tokio-util = { version = "0.7.11", default-features = false }
 tower-http = { version = "0.6.1", features = ["compression-full", "decompression-full", "cors", "trace"] }
 tokio-tungstenite = { version = "0.26.0", default-features = false }

+ 2 - 0
crates/cashu/src/nuts/nut17/mod.rs

@@ -174,6 +174,8 @@ pub enum Kind {
     Bolt11MintQuote,
     /// Proof State
     ProofState,
+    /// Bolt 12 Mint Quote
+    Bolt12MintQuote,
 }
 
 impl<I> AsRef<I> for Params<I> {

+ 2 - 2
crates/cdk-cli/src/main.rs

@@ -102,9 +102,9 @@ async fn main() -> Result<()> {
     let args: Cli = Cli::parse();
     let default_filter = args.log_level;
 
-    let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn";
+    let filter = "rustls=warn,hyper_util=warn,reqwest=warn";
 
-    let env_filter = EnvFilter::new(format!("{default_filter},{sqlx_filter}"));
+    let env_filter = EnvFilter::new(format!("{default_filter},{filter}"));
 
     // Parse input
     tracing_subscriber::fmt().with_env_filter(env_filter).init();

+ 38 - 69
crates/cdk-cli/src/sub_commands/mint.rs

@@ -1,11 +1,12 @@
 use std::str::FromStr;
+use std::time::Duration;
 
 use anyhow::{anyhow, Result};
 use cdk::amount::SplitTarget;
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut00::ProofsMethods;
-use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload, PaymentMethod};
-use cdk::wallet::{MultiMintWallet, WalletSubscription};
+use cdk::nuts::{CurrencyUnit, PaymentMethod};
+use cdk::wallet::MultiMintWallet;
 use cdk::Amount;
 use clap::Args;
 use serde::{Deserialize, Serialize};
@@ -36,6 +37,9 @@ pub struct MintSubCommand {
     /// Expiry
     #[arg(short, long)]
     single_use: Option<bool>,
+    /// Wait duration in seconds for mint quote polling
+    #[arg(long, default_value = "30")]
+    wait_duration: u64,
 }
 
 pub async fn mint(
@@ -48,9 +52,9 @@ pub async fn mint(
 
     let wallet = get_or_create_wallet(multi_mint_wallet, &mint_url, unit).await?;
 
-    let mut payment_method = PaymentMethod::from_str(&sub_command_args.method)?;
+    let payment_method = PaymentMethod::from_str(&sub_command_args.method)?;
 
-    let quote_id = match &sub_command_args.quote_id {
+    let quote = match &sub_command_args.quote_id {
         None => match payment_method {
             PaymentMethod::Bolt11 => {
                 let amount = sub_command_args
@@ -62,20 +66,7 @@ pub async fn mint(
 
                 println!("Please pay: {}", quote.request);
 
-                let mut subscription = wallet
-                    .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote
-                        .id
-                        .clone()]))
-                    .await;
-
-                while let Some(msg) = subscription.recv().await {
-                    if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
-                        if response.state == MintQuoteState::Paid {
-                            break;
-                        }
-                    }
-                }
-                quote.id
+                quote
             }
             PaymentMethod::Bolt12 => {
                 let amount = sub_command_args.amount;
@@ -88,68 +79,46 @@ pub async fn mint(
 
                 println!("Please pay: {}", quote.request);
 
-                let mut subscription = wallet
-                    .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote
-                        .id
-                        .clone()]))
-                    .await;
-
-                while let Some(msg) = subscription.recv().await {
-                    if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
-                        if response.state == MintQuoteState::Paid {
-                            break;
-                        }
-                    }
-                }
-                quote.id
+                quote
             }
             _ => {
                 todo!()
             }
         },
-        Some(quote_id) => {
-            let quote = wallet
-                .localstore
-                .get_mint_quote(quote_id)
-                .await?
-                .ok_or(anyhow!("Unknown quote"))?;
-
-            payment_method = quote.payment_method;
-            quote_id.to_string()
-        }
+        Some(quote_id) => wallet
+            .localstore
+            .get_mint_quote(quote_id)
+            .await?
+            .ok_or(anyhow!("Unknown quote"))?,
     };
 
     tracing::debug!("Attempting mint for: {}", payment_method);
 
-    let proofs = match payment_method {
-        PaymentMethod::Bolt11 => wallet.mint(&quote_id, SplitTarget::default(), None).await?,
-        PaymentMethod::Bolt12 => {
-            let response = wallet.mint_bolt12_quote_state(&quote_id).await?;
-
-            let amount_mintable = response.amount_paid - response.amount_issued;
-
-            if amount_mintable == Amount::ZERO {
-                println!("Mint quote does not have amount that can be minted.");
-                return Ok(());
-            }
-
-            wallet
-                .mint_bolt12(
-                    &quote_id,
-                    Some(amount_mintable),
-                    SplitTarget::default(),
-                    None,
-                )
-                .await?
-        }
-        _ => {
-            todo!()
+    let mut amount_minted = Amount::ZERO;
+
+    loop {
+        let proofs = wallet
+            .wait_and_mint_quote(
+                quote.clone(),
+                SplitTarget::default(),
+                None,
+                Duration::from_secs(sub_command_args.wait_duration),
+            )
+            .await?;
+
+        amount_minted += proofs.total_amount()?;
+
+        if sub_command_args.quote_id.is_none() || quote.payment_method == PaymentMethod::Bolt11 {
+            break;
+        } else {
+            println!(
+                "Minted {} waiting for next payment.",
+                proofs.total_amount()?
+            );
         }
-    };
-
-    let receive_amount = proofs.total_amount()?;
+    }
 
-    println!("Received {receive_amount} from mint {mint_url}");
+    println!("Received {amount_minted} from mint {mint_url}");
 
     Ok(())
 }

+ 3 - 0
crates/cdk-common/src/subscription.rs

@@ -51,6 +51,9 @@ impl TryFrom<IndexableParams> for Vec<Index<Notification>> {
                         Notification::MintQuoteBolt11(Uuid::from_str(&filter)?)
                     }
                     Kind::ProofState => Notification::ProofState(PublicKey::from_str(&filter)?),
+                    Kind::Bolt12MintQuote => {
+                        Notification::MintQuoteBolt12(Uuid::from_str(&filter)?)
+                    }
                 };
 
                 Ok(Index::from((idx, params.id.clone(), sub_id)))

+ 1 - 1
crates/cdk-fake-wallet/src/lib.rs

@@ -108,7 +108,7 @@ impl SecondaryRepaymentQueue {
 
             loop {
                 // Wait for a random interval between 30 seconds and 3 minutes (180 seconds)
-                let delay_secs = rng.gen_range(30..=180);
+                let delay_secs = rng.gen_range(1..=3);
                 time::sleep(time::Duration::from_secs(delay_secs)).await;
 
                 // Try to process a random payment from the queue without removing it

+ 8 - 4
crates/cdk-integration-tests/tests/fake_auth.rs

@@ -330,13 +330,17 @@ async fn test_mint_with_auth() {
 
     let mint_amount: Amount = 100.into();
 
-    let (_, proofs) = wallet
-        .mint_once_paid(mint_amount, None, Duration::from_secs(10))
+    let quote = wallet.mint_quote(mint_amount, None).await.unwrap();
+    let proofs = wallet
+        .wait_and_mint_quote(
+            quote,
+            Default::default(),
+            Default::default(),
+            Duration::from_secs(10),
+        )
         .await
         .unwrap();
 
-    let proofs = proofs.await.expect("could not mint");
-
     assert!(proofs.total_amount().expect("Could not get proofs amount") == mint_amount);
 }
 

+ 7 - 7
crates/cdk/examples/auth_wallet.rs

@@ -5,6 +5,7 @@ use cdk::error::Error;
 use cdk::nuts::CurrencyUnit;
 use cdk::wallet::{SendOptions, Wallet};
 use cdk::{Amount, OidcClient};
+use cdk_common::amount::SplitTarget;
 use cdk_common::{MintInfo, ProofsMethods};
 use cdk_sqlite::wallet::memory;
 use rand::Rng;
@@ -57,14 +58,13 @@ async fn main() -> Result<(), Error> {
         .await
         .expect("Could not mint blind auth");
 
-    let (_invoice_to_pay, proofs) = wallet
-        .mint_once_paid(amount, None, Duration::from_secs(10))
-        .await?;
-
-    // Mint the received amount
-    let receive_amount = proofs.await?;
+    let quote = wallet.mint_quote(amount, None).await.unwrap();
+    let proofs = wallet
+        .wait_and_mint_quote(quote, SplitTarget::default(), None, Duration::from_secs(10))
+        .await
+        .unwrap();
 
-    println!("Received: {}", receive_amount.total_amount()?);
+    println!("Received: {}", proofs.total_amount()?);
 
     // Get the total balance of the wallet
     let balance = wallet.total_balance().await?;

+ 8 - 4
crates/cdk/examples/melt-token.rs

@@ -29,12 +29,16 @@ async fn main() -> Result<(), Error> {
     // Create a new wallet
     let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None)?;
 
-    let (_invoice_to_pay, proofs) = wallet
-        .mint_once_paid(amount, None, Duration::from_secs(10))
+    let quote = wallet.mint_quote(amount, None).await?;
+    let proofs = wallet
+        .wait_and_mint_quote(
+            quote,
+            Default::default(),
+            Default::default(),
+            Duration::from_secs(10),
+        )
         .await?;
 
-    // Mint the received amount
-    let proofs = proofs.await?;
     let receive_amount = proofs.total_amount()?;
     println!("Received {} from mint {}", receive_amount, mint_url);
 

+ 8 - 3
crates/cdk/examples/mint-token.rs

@@ -35,12 +35,17 @@ async fn main() -> Result<(), Error> {
     // Create a new wallet
     let wallet = Wallet::new(mint_url, unit, localstore, seed, None)?;
 
-    let (_invoice_to_pay, proofs) = wallet
-        .mint_once_paid(amount, None, Duration::from_secs(10))
+    let quote = wallet.mint_quote(amount, None).await?;
+    let proofs = wallet
+        .wait_and_mint_quote(
+            quote,
+            Default::default(),
+            Default::default(),
+            Duration::from_secs(10),
+        )
         .await?;
 
     // Mint the received amount
-    let proofs = proofs.await?;
     let receive_amount = proofs.total_amount()?;
     println!("Received {} from mint {}", receive_amount, mint_url);
 

+ 9 - 7
crates/cdk/examples/p2pk.rs

@@ -34,18 +34,20 @@ async fn main() -> Result<(), Error> {
     // Create a new wallet
     let wallet = Wallet::new(mint_url, unit, localstore, seed, None).unwrap();
 
-    let (_invoice_to_pay, proofs) = wallet
-        .mint_once_paid(amount, None, Duration::from_secs(10))
+    let quote = wallet.mint_quote(amount, None).await?;
+    let proofs = wallet
+        .wait_and_mint_quote(
+            quote,
+            Default::default(),
+            Default::default(),
+            Duration::from_secs(10),
+        )
         .await?;
 
     // Mint the received amount
-    let received_proofs = proofs.await?;
     println!(
         "Minted nuts: {:?}",
-        received_proofs
-            .into_iter()
-            .map(|p| p.amount)
-            .collect::<Vec<_>>()
+        proofs.into_iter().map(|p| p.amount).collect::<Vec<_>>()
     );
 
     // Generate a secret key for spending conditions

+ 8 - 3
crates/cdk/examples/proof-selection.rs

@@ -31,12 +31,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     for amount in [64] {
         let amount = Amount::from(amount);
 
-        let (_invoice_to_pay, proofs) = wallet
-            .mint_once_paid(amount, None, Duration::from_secs(10))
+        let quote = wallet.mint_quote(amount, None).await?;
+        let proofs = wallet
+            .wait_and_mint_quote(
+                quote,
+                Default::default(),
+                Default::default(),
+                Duration::from_secs(10),
+            )
             .await?;
 
         // Mint the received amount
-        let proofs = proofs.await?;
         let receive_amount = proofs.total_amount()?;
         println!("Minted {}", receive_amount);
     }

+ 8 - 3
crates/cdk/examples/wallet.rs

@@ -24,12 +24,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     // Create a new wallet
     let wallet = Wallet::new(mint_url, unit, localstore, seed, None)?;
 
-    let (_invoice_to_pay, proofs) = wallet
-        .mint_once_paid(amount, None, Duration::from_secs(10))
+    let quote = wallet.mint_quote(amount, None).await?;
+    let proofs = wallet
+        .wait_and_mint_quote(
+            quote,
+            Default::default(),
+            Default::default(),
+            Duration::from_secs(10),
+        )
         .await?;
 
     // Mint the received amount
-    let proofs = proofs.await?;
     let receive_amount = proofs.total_amount()?;
     println!("Minted {}", receive_amount);
 

+ 17 - 3
crates/cdk/src/mint/subscription/on_subscription.rs

@@ -6,7 +6,7 @@ use std::sync::Arc;
 use cdk_common::database::{self, MintDatabase};
 use cdk_common::nut17::Notification;
 use cdk_common::pub_sub::OnNewSubscription;
-use cdk_common::NotificationPayload;
+use cdk_common::{MintQuoteBolt12Response, NotificationPayload, PaymentMethod};
 use uuid::Uuid;
 
 use crate::nuts::{MeltQuoteBolt11Response, MintQuoteBolt11Response, ProofState, PublicKey};
@@ -79,8 +79,22 @@ impl OnNewSubscription for OnSubscription {
                     .map(|quotes| {
                         quotes
                             .into_iter()
-                            .filter_map(|quote| quote.map(|x| x.into()))
-                            .map(|x: MintQuoteBolt11Response<Uuid>| x.into())
+                            .filter_map(|quote| {
+                                quote.and_then(|x| match x.payment_method {
+                                    PaymentMethod::Bolt11 => {
+                                        let response: MintQuoteBolt11Response<Uuid> = x.into();
+                                        Some(response.into())
+                                    }
+                                    PaymentMethod::Bolt12 => match x.try_into() {
+                                        Ok(response) => {
+                                            let response: MintQuoteBolt12Response<Uuid> = response;
+                                            Some(response.into())
+                                        }
+                                        Err(_) => None,
+                                    },
+                                    PaymentMethod::Custom(_) => None,
+                                })
+                            })
                             .collect::<Vec<_>>()
                     })
                     .map_err(|e| e.to_string())?,

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

@@ -94,6 +94,8 @@ pub enum WalletSubscription {
     Bolt11MintQuoteState(Vec<String>),
     /// Melt quote subscription
     Bolt11MeltQuoteState(Vec<String>),
+    /// Mint bolt12 quote subscription
+    Bolt12MintQuoteState(Vec<String>),
 }
 
 impl From<WalletSubscription> for Params {
@@ -126,6 +128,11 @@ impl From<WalletSubscription> for Params {
                 kind: Kind::Bolt11MeltQuote,
                 id: id.into(),
             },
+            WalletSubscription::Bolt12MintQuoteState(filters) => Params {
+                filters,
+                kind: Kind::Bolt12MintQuote,
+                id: id.into(),
+            },
         }
     }
 }

+ 5 - 0
crates/cdk/src/wallet/subscription/http.rs

@@ -66,6 +66,11 @@ async fn convert_subscription(
                 }
             }
         }
+        Kind::Bolt12MintQuote => {
+            for id in sub.1.filters.iter().map(|id| UrlType::Mint(id.clone())) {
+                subscribed_to.insert(id, (sub.0.clone(), sub.1.id.clone(), AnyState::Empty));
+            }
+        }
     }
 
     Some(())

+ 32 - 51
crates/cdk/src/wallet/wait.rs

@@ -1,9 +1,8 @@
-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,
+    Amount, Error, MeltQuoteState, MintQuoteState, NotificationPayload, PaymentMethod, Proofs,
+    SpendingConditions,
 };
 use futures::future::BoxFuture;
 use tokio::time::{timeout, Duration};
@@ -11,9 +10,11 @@ use tokio::time::{timeout, Duration};
 use super::{Wallet, WalletSubscription};
 
 #[allow(private_bounds)]
+#[allow(clippy::enum_variant_names)]
 enum WaitableEvent {
     MeltQuote(String),
     MintQuote(String),
+    Bolt12MintQuote(String),
 }
 
 impl From<&MeltQuote> for WaitableEvent {
@@ -24,7 +25,11 @@ impl From<&MeltQuote> for WaitableEvent {
 
 impl From<&MintQuote> for WaitableEvent {
     fn from(event: &MintQuote) -> Self {
-        WaitableEvent::MintQuote(event.id.to_owned())
+        match event.payment_method {
+            PaymentMethod::Bolt11 => WaitableEvent::MintQuote(event.id.to_owned()),
+            PaymentMethod::Bolt12 => WaitableEvent::Bolt12MintQuote(event.id.to_owned()),
+            PaymentMethod::Custom(_) => WaitableEvent::MintQuote(event.id.to_owned()),
+        }
     }
 }
 
@@ -37,62 +42,38 @@ impl From<WaitableEvent> for WalletSubscription {
             WaitableEvent::MintQuote(quote_id) => {
                 WalletSubscription::Bolt11MintQuoteState(vec![quote_id])
             }
+            WaitableEvent::Bolt12MintQuote(quote_id) => {
+                WalletSubscription::Bolt12MintQuoteState(vec![quote_id])
+            }
         }
     }
 }
 
 impl Wallet {
     #[inline(always)]
-    async fn wait_and_mint_quote(
+    /// Mints a mint quote once it is paid
+    pub 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>,
-        timeout_duration: Duration,
-    ) -> Result<(String, impl Future<Output = Result<Proofs, Error>> + '_), Error> {
-        self.mint_once_paid_ex(
-            amount,
-            description,
-            Default::default(),
-            None,
-            timeout_duration,
-        )
-        .await
-    }
+        let amount = self.wait_for_payment(&quote, 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<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?;
+        tracing::debug!("Received payment notification for {}. Minting...", quote.id);
 
-        Ok((
-            quote.request.clone(),
-            self.wait_and_mint_quote(
-                quote,
-                amount_split_target,
-                spending_conditions,
-                timeout_duration,
-            ),
-        ))
+        match quote.payment_method {
+            PaymentMethod::Bolt11 => {
+                self.mint(&quote.id, amount_split_target, spending_conditions)
+                    .await
+            }
+            PaymentMethod::Bolt12 => {
+                self.mint_bolt12(&quote.id, amount, amount_split_target, spending_conditions)
+                    .await
+            }
+            _ => Err(Error::UnsupportedPaymentMethod),
+        }
     }
 
     /// Returns a BoxFuture that will wait for payment on the given event with a timeout check
@@ -101,7 +82,7 @@ impl Wallet {
         &self,
         event: T,
         timeout_duration: Duration,
-    ) -> BoxFuture<'_, Result<(), Error>>
+    ) -> BoxFuture<'_, Result<Option<Amount>, Error>>
     where
         T: Into<WaitableEvent>,
     {
@@ -114,17 +95,17 @@ impl Wallet {
                     match subscription.recv().await.ok_or(Error::Internal)? {
                         NotificationPayload::MintQuoteBolt11Response(info) => {
                             if info.state == MintQuoteState::Paid {
-                                return Ok(());
+                                return Ok(None);
                             }
                         }
                         NotificationPayload::MintQuoteBolt12Response(info) => {
-                            if info.amount_paid > Amount::ZERO {
-                                return Ok(());
+                            if info.amount_paid - info.amount_issued > Amount::ZERO {
+                                return Ok(Some(info.amount_paid - info.amount_issued));
                             }
                         }
                         NotificationPayload::MeltQuoteBolt11Response(info) => {
                             if info.state == MeltQuoteState::Paid {
-                                return Ok(());
+                                return Ok(None);
                             }
                         }
                         _ => {}