feat: replace 3s polling with CDK WebSocket subscription (NUT-17) for mint quote

This commit is contained in:
Forte11Cuba
2026-04-01 12:24:12 -06:00
parent 66119f6b56
commit a5cd50af13
+55 -56
View File
@@ -12,7 +12,7 @@ use cdk::{
wallet::{ wallet::{
MeltQuote as CdkMeltQuote, MintQuote as CdkMintQuote, PreparedSend as CdkPreparedSend, MeltQuote as CdkMeltQuote, MintQuote as CdkMintQuote, PreparedSend as CdkPreparedSend,
ReceiveOptions as CdkReceiveOptions, SendMemo, SendOptions as CdkSendOptions, ReceiveOptions as CdkReceiveOptions, SendMemo, SendOptions as CdkSendOptions,
Wallet as CdkWallet, Wallet as CdkWallet, WalletSubscription,
}, },
}; };
use cdk_common::{ use cdk_common::{
@@ -20,11 +20,12 @@ use cdk_common::{
wallet::{ wallet::{
Transaction as CdkTransaction, TransactionDirection as CdkTransactionDirection, Transaction as CdkTransaction, TransactionDirection as CdkTransactionDirection,
}, },
NotificationPayload,
}; };
use cdk_sqlite::WalletSqliteDatabase; use cdk_sqlite::WalletSqliteDatabase;
use flutter_rust_bridge::frb; use flutter_rust_bridge::frb;
use log::info; use log::info;
use tokio::{sync::broadcast, time::sleep}; use tokio::sync::broadcast;
use uuid::Uuid; use uuid::Uuid;
use crate::frb_generated::StreamSink; use crate::frb_generated::StreamSink;
@@ -184,50 +185,38 @@ impl Wallet {
return Ok(()); return Ok(());
} }
// Subscribe to quote state changes via WebSocket (NUT-17) with HTTP polling fallback
let mut subscription = self
.inner
.subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote.id.clone()]))
.await
.map_err(|e| Error::Cdk(e.to_string()))?;
let _self = self.clone(); let _self = self.clone();
flutter_rust_bridge::spawn(async move { flutter_rust_bridge::spawn(async move {
loop { // Timeout: time until quote expires + 30s buffer, or 1 hour if no expiry
sleep(Duration::from_secs(3)).await; let remaining = quote.expiry.saturating_sub(unix_time());
let timeout_dur = if remaining > 0 {
Duration::from_secs(remaining + 30)
} else {
Duration::from_secs(3600)
};
// Check if the Dart stream is still alive before polling // Clone for the timeout error path (originals are moved into the async block)
if sink let expired_id = quote.id.clone();
.add(MintQuote { let expired_request = quote.request.clone();
id: quote.id.clone(), let expired_amount = quote.amount;
request: quote.request.clone(), let expired_expiry = quote.expiry;
amount: quote.amount.map(|a| a.into()),
expiry: Some(quote.expiry),
state: MintQuoteState::Unpaid,
token: None,
error: None,
})
.is_err()
{
info!("Mint polling stopped: Dart stream closed for {}", quote.id);
break;
}
info!("Checking mint quote state for {}", quote.id); let result = tokio::time::timeout(timeout_dur, async {
match _self.inner.check_mint_quote_status(&quote.id).await { while let Some(event) = subscription.recv().await {
Ok(state_res) => match state_res.state { match event.into_inner() {
CdkMintQuoteState::Unpaid => { NotificationPayload::MintQuoteBolt11Response(info)
if state_res.expiry < unix_time() { if info.state == CdkMintQuoteState::Paid =>
let _ = sink.add(MintQuote { {
id: quote.id, info!("Mint quote {} paid via subscription", quote.id);
request: quote.request,
amount: quote.amount.map(|a| a.into()), // Notify Dart: payment detected
expiry: Some(state_res.expiry),
state: MintQuoteState::Error,
token: None,
error: Some("Quote expired".to_string()),
});
break;
}
continue;
}
CdkMintQuoteState::Issued => {
break;
}
CdkMintQuoteState::Paid => {
let _ = sink.add(MintQuote { let _ = sink.add(MintQuote {
id: quote.id.clone(), id: quote.id.clone(),
request: quote.request.clone(), request: quote.request.clone(),
@@ -237,6 +226,8 @@ impl Wallet {
token: None, token: None,
error: None, error: None,
}); });
// Mint the ecash tokens
match _self match _self
.inner .inner
.mint(&quote.id, SplitTarget::None, None) .mint(&quote.id, SplitTarget::None, None)
@@ -261,7 +252,6 @@ impl Wallet {
error: None, error: None,
}); });
_self.update_balance_streams().await; _self.update_balance_streams().await;
break;
} }
Err(e) => { Err(e) => {
let _ = sink.add(MintQuote { let _ = sink.add(MintQuote {
@@ -273,24 +263,33 @@ impl Wallet {
token: None, token: None,
error: Some(e.to_string()), error: Some(e.to_string()),
}); });
break;
} }
} }
return;
} }
}, NotificationPayload::MintQuoteBolt11Response(info)
Err(e) => { if info.state == CdkMintQuoteState::Issued =>
let _ = sink.add(MintQuote { {
id: quote.id, // Already issued (recovered from previous session)
request: quote.request, return;
amount: quote.amount.map(|a| a.into()), }
expiry: Some(quote.expiry), _ => continue,
state: MintQuoteState::Error,
token: None,
error: Some(e.to_string()),
});
break;
} }
} }
})
.await;
if result.is_err() {
// Timeout: quote expired
let _ = sink.add(MintQuote {
id: expired_id,
request: expired_request,
amount: expired_amount.map(|a| a.into()),
expiry: Some(expired_expiry),
state: MintQuoteState::Error,
token: None,
error: Some("Quote expired".to_string()),
});
} }
}); });
Ok(()) Ok(())