feat: replace 3s polling with CDK WebSocket subscription (NUT-17) for mint quote
This commit is contained in:
+55
-56
@@ -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("e.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("e.id, SplitTarget::None, None)
|
.mint("e.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(())
|
||||||
|
|||||||
Reference in New Issue
Block a user