1use base64::Engine;
2use bitcoin::{
3 consensus::serialize,
4 hashes::{Hash, sha256},
5 hex::DisplayHex,
6 secp256k1::{PublicKey, ecdsa::Signature},
7};
8use bitflags::bitflags;
9use breez_sdk_common::{
10 fiat::FiatService,
11 lnurl::{self, withdraw::execute_lnurl_withdraw},
12};
13use breez_sdk_common::{
14 lnurl::{
15 error::LnurlError,
16 pay::{
17 AesSuccessActionDataResult, SuccessAction, SuccessActionProcessed, validate_lnurl_pay,
18 },
19 },
20 rest::RestClient,
21};
22use flashnet::{
23 ClawbackRequest, ClawbackResponse, ExecuteSwapRequest, FlashnetClient, FlashnetError,
24 GetMinAmountsRequest, ListPoolsRequest, PoolSortOrder, SimulateSwapRequest,
25};
26use lnurl_models::sanitize_username;
27use spark_wallet::{
28 ExitSpeed, InvoiceDescription, ListTokenTransactionsRequest, ListTransfersRequest, Preimage,
29 SparkAddress, SparkWallet, TransferId, TransferTokenOutput, WalletEvent, WalletTransfer,
30};
31use std::{collections::HashMap, str::FromStr, sync::Arc};
32use tracing::{debug, error, info, trace, warn};
33use web_time::{Duration, SystemTime};
34
35use tokio::{
36 select,
37 sync::{Mutex, OnceCell, mpsc, oneshot, watch},
38 time::timeout,
39};
40use tokio_with_wasm::alias as tokio;
41use web_time::Instant;
42use x509_parser::parse_x509_certificate;
43
44use crate::{
45 AssetFilter, BitcoinAddressDetails, BitcoinChainService, Bolt11InvoiceDetails,
46 CheckLightningAddressRequest, CheckMessageRequest, CheckMessageResponse, ClaimDepositRequest,
47 ClaimDepositResponse, ClaimHtlcPaymentRequest, ClaimHtlcPaymentResponse, ConversionEstimate,
48 ConversionInfo, ConversionOptions, ConversionPurpose, ConversionStatus, ConversionType,
49 DepositInfo, ExternalInputParser, FetchConversionLimitsRequest, FetchConversionLimitsResponse,
50 GetPaymentRequest, GetPaymentResponse, GetTokensMetadataRequest, GetTokensMetadataResponse,
51 InputType, LightningAddressInfo, ListFiatCurrenciesResponse, ListFiatRatesResponse,
52 ListUnclaimedDepositsRequest, ListUnclaimedDepositsResponse, LnurlAuthRequestDetails,
53 LnurlCallbackStatus, LnurlPayInfo, LnurlPayRequest, LnurlPayResponse, LnurlWithdrawInfo,
54 LnurlWithdrawRequest, LnurlWithdrawResponse, Logger, MaxFee, Network, OnchainConfirmationSpeed,
55 OptimizationConfig, OptimizationProgress, PaymentDetails, PaymentDetailsFilter, PaymentStatus,
56 PaymentType, PrepareLnurlPayRequest, PrepareLnurlPayResponse, RefundDepositRequest,
57 RefundDepositResponse, RegisterLightningAddressRequest, SendOnchainFeeQuote,
58 SendPaymentOptions, SetLnurlMetadataItem, SignMessageRequest, SignMessageResponse,
59 SparkHtlcOptions, SparkInvoiceDetails, TokenConversionPool, TokenConversionResponse,
60 UpdateUserSettingsRequest, UserSettings, WaitForPaymentIdentifier,
61 chain::RecommendedFees,
62 error::SdkError,
63 events::{EventEmitter, EventListener, InternalSyncedEvent, SdkEvent},
64 issuer::TokenIssuer,
65 lnurl::{ListMetadataRequest, LnurlServerClient, PublishZapReceiptRequest},
66 logger,
67 models::{
68 Config, GetInfoRequest, GetInfoResponse, ListPaymentsRequest, ListPaymentsResponse,
69 Payment, PrepareSendPaymentRequest, PrepareSendPaymentResponse, ReceivePaymentMethod,
70 ReceivePaymentRequest, ReceivePaymentResponse, SendPaymentMethod, SendPaymentRequest,
71 SendPaymentResponse, SyncWalletRequest, SyncWalletResponse,
72 },
73 nostr::NostrClient,
74 persist::{
75 CachedAccountInfo, ObjectCacheRepository, PaymentMetadata, StaticDepositAddress, Storage,
76 UpdateDepositPayload,
77 },
78 sync::SparkSyncService,
79 utils::{
80 deposit_chain_syncer::DepositChainSyncer,
81 run_with_shutdown,
82 send_payment_validation::validate_prepare_send_payment_request,
83 token::{
84 get_tokens_metadata_cached_or_query, map_and_persist_token_transaction,
85 token_transaction_to_payments,
86 },
87 utxo_fetcher::{CachedUtxoFetcher, DetailedUtxo},
88 },
89};
90
91pub async fn parse_input(
92 input: &str,
93 external_input_parsers: Option<Vec<ExternalInputParser>>,
94) -> Result<InputType, SdkError> {
95 Ok(breez_sdk_common::input::parse(
96 input,
97 external_input_parsers.map(|parsers| parsers.into_iter().map(From::from).collect()),
98 )
99 .await?
100 .into())
101}
102
103#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
104const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
105
106#[cfg(all(target_family = "wasm", target_os = "unknown"))]
107const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology:442";
108
109const CLAIM_TX_SIZE_VBYTES: u64 = 99;
110const SYNC_PAGING_LIMIT: u32 = 100;
111const DEFAULT_TOKEN_CONVERSION_MAX_SLIPPAGE_BPS: u32 = 50;
113const DEFAULT_TOKEN_CONVERSION_TIMEOUT_SECS: u32 = 30;
115
116bitflags! {
117 #[derive(Clone, Debug)]
118 struct SyncType: u32 {
119 const Wallet = 1 << 0;
120 const WalletState = 1 << 1;
121 const Deposits = 1 << 2;
122 const LnurlMetadata = 1 << 3;
123 const Full = Self::Wallet.0.0
124 | Self::WalletState.0.0
125 | Self::Deposits.0.0
126 | Self::LnurlMetadata.0.0;
127 }
128}
129
130#[derive(Clone, Debug)]
131struct SyncRequest {
132 sync_type: SyncType,
133 #[allow(clippy::type_complexity)]
134 reply: Arc<Mutex<Option<oneshot::Sender<Result<(), SdkError>>>>>,
135}
136
137impl SyncRequest {
138 fn new(reply: oneshot::Sender<Result<(), SdkError>>, sync_type: SyncType) -> Self {
139 Self {
140 sync_type,
141 reply: Arc::new(Mutex::new(Some(reply))),
142 }
143 }
144
145 fn full(reply: Option<oneshot::Sender<Result<(), SdkError>>>) -> Self {
146 Self {
147 sync_type: SyncType::Full,
148 reply: Arc::new(Mutex::new(reply)),
149 }
150 }
151
152 fn no_reply(sync_type: SyncType) -> Self {
153 Self {
154 sync_type,
155 reply: Arc::new(Mutex::new(None)),
156 }
157 }
158
159 async fn reply(&self, error: Option<SdkError>) {
160 if let Some(reply) = self.reply.lock().await.take() {
161 let _ = match error {
162 Some(e) => reply.send(Err(e)),
163 None => reply.send(Ok(())),
164 };
165 }
166 }
167}
168
169#[derive(Clone)]
172#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
173pub struct BreezSdk {
174 config: Config,
175 spark_wallet: Arc<SparkWallet>,
176 storage: Arc<dyn Storage>,
177 chain_service: Arc<dyn BitcoinChainService>,
178 fiat_service: Arc<dyn FiatService>,
179 lnurl_client: Arc<dyn RestClient>,
180 lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
181 lnurl_auth_signer: Arc<crate::signer::lnurl_auth::LnurlAuthSignerAdapter>,
182 event_emitter: Arc<EventEmitter>,
183 shutdown_sender: watch::Sender<()>,
184 sync_trigger: tokio::sync::broadcast::Sender<SyncRequest>,
185 zap_receipt_trigger: tokio::sync::broadcast::Sender<()>,
186 conversion_refund_trigger: tokio::sync::broadcast::Sender<()>,
187 initial_synced_watcher: watch::Receiver<bool>,
188 external_input_parsers: Vec<ExternalInputParser>,
189 spark_private_mode_initialized: Arc<OnceCell<()>>,
190 nostr_client: Arc<NostrClient>,
191 flashnet_client: Arc<FlashnetClient>,
192}
193
194#[cfg_attr(feature = "uniffi", uniffi::export)]
195pub fn init_logging(
196 log_dir: Option<String>,
197 app_logger: Option<Box<dyn Logger>>,
198 log_filter: Option<String>,
199) -> Result<(), SdkError> {
200 logger::init_logging(log_dir, app_logger, log_filter)
201}
202
203#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
213#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
214pub async fn connect(request: crate::ConnectRequest) -> Result<BreezSdk, SdkError> {
215 let builder = super::sdk_builder::SdkBuilder::new(request.config, request.seed)
216 .with_default_storage(request.storage_dir);
217 let sdk = builder.build().await?;
218 Ok(sdk)
219}
220
221#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
234#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
235pub async fn connect_with_signer(
236 request: crate::ConnectWithSignerRequest,
237) -> Result<BreezSdk, SdkError> {
238 let builder = super::sdk_builder::SdkBuilder::new_with_signer(request.config, request.signer)
239 .with_default_storage(request.storage_dir);
240 let sdk = builder.build().await?;
241 Ok(sdk)
242}
243
244#[cfg_attr(feature = "uniffi", uniffi::export)]
245pub fn default_config(network: Network) -> Config {
246 let lnurl_domain = match network {
247 Network::Mainnet => Some("breez.tips".to_string()),
248 Network::Regtest => None,
249 };
250 Config {
251 api_key: None,
252 network,
253 sync_interval_secs: 60, max_deposit_claim_fee: Some(MaxFee::Rate { sat_per_vbyte: 1 }),
255 lnurl_domain,
256 prefer_spark_over_lightning: false,
257 external_input_parsers: None,
258 use_default_external_input_parsers: true,
259 real_time_sync_server_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
260 private_enabled_default: true,
261 optimization_config: OptimizationConfig {
262 auto_enabled: true,
263 multiplicity: 1,
264 },
265 }
266}
267
268#[cfg_attr(feature = "uniffi", uniffi::export)]
284pub fn default_external_signer(
285 mnemonic: String,
286 passphrase: Option<String>,
287 network: Network,
288 key_set_config: Option<crate::models::KeySetConfig>,
289) -> Result<Arc<dyn crate::signer::ExternalSigner>, SdkError> {
290 use crate::signer::DefaultExternalSigner;
291
292 let config = key_set_config.unwrap_or_default();
293 let signer = DefaultExternalSigner::new(
294 mnemonic,
295 passphrase,
296 network,
297 config.key_set_type,
298 config.use_address_index,
299 config.account_number,
300 )?;
301
302 Ok(Arc::new(signer))
303}
304
305pub(crate) struct BreezSdkParams {
306 pub config: Config,
307 pub storage: Arc<dyn Storage>,
308 pub chain_service: Arc<dyn BitcoinChainService>,
309 pub fiat_service: Arc<dyn FiatService>,
310 pub lnurl_client: Arc<dyn RestClient>,
311 pub lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
312 pub lnurl_auth_signer: Arc<crate::signer::lnurl_auth::LnurlAuthSignerAdapter>,
313 pub shutdown_sender: watch::Sender<()>,
314 pub spark_wallet: Arc<SparkWallet>,
315 pub event_emitter: Arc<EventEmitter>,
316 pub nostr_client: Arc<NostrClient>,
317 pub flashnet_client: Arc<FlashnetClient>,
318}
319
320impl BreezSdk {
321 pub(crate) fn init_and_start(params: BreezSdkParams) -> Result<Self, SdkError> {
323 if !matches!(params.config.network, Network::Regtest) {
326 match ¶ms.config.api_key {
327 Some(api_key) => validate_breez_api_key(api_key)?,
328 None => return Err(SdkError::Generic("Missing Breez API key".to_string())),
329 }
330 }
331 let (initial_synced_sender, initial_synced_watcher) = watch::channel(false);
332 let external_input_parsers = params.config.get_all_external_input_parsers();
333 let sdk = Self {
334 config: params.config,
335 spark_wallet: params.spark_wallet,
336 storage: params.storage,
337 chain_service: params.chain_service,
338 fiat_service: params.fiat_service,
339 lnurl_client: params.lnurl_client,
340 lnurl_server_client: params.lnurl_server_client,
341 lnurl_auth_signer: params.lnurl_auth_signer,
342 event_emitter: params.event_emitter,
343 shutdown_sender: params.shutdown_sender,
344 sync_trigger: tokio::sync::broadcast::channel(10).0,
345 zap_receipt_trigger: tokio::sync::broadcast::channel(10).0,
346 conversion_refund_trigger: tokio::sync::broadcast::channel(10).0,
347 initial_synced_watcher,
348 external_input_parsers,
349 spark_private_mode_initialized: Arc::new(OnceCell::new()),
350 nostr_client: params.nostr_client,
351 flashnet_client: params.flashnet_client,
352 };
353
354 sdk.start(initial_synced_sender);
355 Ok(sdk)
356 }
357
358 fn start(&self, initial_synced_sender: watch::Sender<bool>) {
367 self.spawn_spark_private_mode_initialization();
368 self.periodic_sync(initial_synced_sender);
369 self.try_recover_lightning_address();
370 self.spawn_zap_receipt_publisher();
371 self.spawn_conversion_refunder();
372 }
373
374 fn spawn_spark_private_mode_initialization(&self) {
375 let sdk = self.clone();
376 tokio::spawn(async move {
377 if let Err(e) = sdk.ensure_spark_private_mode_initialized().await {
378 error!("Failed to initialize spark private mode: {e:?}");
379 }
380 });
381 }
382
383 fn try_recover_lightning_address(&self) {
385 let sdk = self.clone();
386 tokio::spawn(async move {
387 if sdk.config.lnurl_domain.is_none() {
388 return;
389 }
390
391 match sdk.recover_lightning_address().await {
392 Ok(None) => info!("no lightning address to recover on startup"),
393 Ok(Some(value)) => info!(
394 "recovered lightning address on startup: lnurl: {}, address: {}",
395 value.lnurl, value.lightning_address
396 ),
397 Err(e) => error!("Failed to recover lightning address on startup: {e:?}"),
398 }
399 });
400 }
401
402 fn spawn_zap_receipt_publisher(&self) {
405 let sdk = self.clone();
406 let mut shutdown_receiver = sdk.shutdown_sender.subscribe();
407 let mut trigger_receiver = sdk.zap_receipt_trigger.clone().subscribe();
408
409 tokio::spawn(async move {
410 if let Err(e) = Self::process_pending_zap_receipts(&sdk).await {
411 error!("Failed to process pending zap receipts on startup: {e:?}");
412 }
413
414 loop {
415 tokio::select! {
416 _ = shutdown_receiver.changed() => {
417 info!("Zap receipt publisher shutdown signal received");
418 return;
419 }
420 _ = trigger_receiver.recv() => {
421 if let Err(e) = Self::process_pending_zap_receipts(&sdk).await {
422 error!("Failed to process pending zap receipts: {e:?}");
423 }
424 }
425 }
426 }
427 });
428 }
429
430 fn spawn_conversion_refunder(&self) {
433 let sdk = self.clone();
434 let mut shutdown_receiver = sdk.shutdown_sender.subscribe();
435 let mut trigger_receiver = sdk.conversion_refund_trigger.clone().subscribe();
436
437 tokio::spawn(async move {
438 loop {
439 if let Err(e) = sdk.refund_failed_conversions().await {
440 error!("Failed to refund failed conversions: {e:?}");
441 }
442
443 select! {
444 _ = shutdown_receiver.changed() => {
445 info!("Conversion refunder shutdown signal received");
446 return;
447 }
448 _ = trigger_receiver.recv() => {
449 debug!("Conversion refunder triggered");
450 }
451 () = tokio::time::sleep(Duration::from_secs(150)) => {}
452 }
453 }
454 });
455 }
456
457 async fn process_pending_zap_receipts(&self) -> Result<(), SdkError> {
458 let Some(lnurl_server_client) = self.lnurl_server_client.clone() else {
459 return Ok(());
460 };
461
462 let mut offset = 0;
463 let limit = 100;
464 loop {
465 let payments = self
466 .storage
467 .list_payments(ListPaymentsRequest {
468 offset: Some(offset),
469 limit: Some(limit),
470 status_filter: Some(vec![PaymentStatus::Completed]),
471 type_filter: Some(vec![PaymentType::Receive]),
472 asset_filter: Some(AssetFilter::Bitcoin),
473 ..Default::default()
474 })
475 .await?;
476 if payments.is_empty() {
477 break;
478 }
479
480 let len = u32::try_from(payments.len())?;
481 for payment in payments {
482 let Some(PaymentDetails::Lightning {
483 ref lnurl_receive_metadata,
484 ref payment_hash,
485 ..
486 }) = payment.details
487 else {
488 continue;
489 };
490
491 let Some(lnurl_receive_metadata) = lnurl_receive_metadata else {
492 continue;
493 };
494
495 let Some(zap_request) = &lnurl_receive_metadata.nostr_zap_request else {
496 continue;
497 };
498
499 if lnurl_receive_metadata.nostr_zap_receipt.is_some() {
500 continue;
501 }
502
503 let zap_receipt = match self
505 .nostr_client
506 .create_zap_receipt(zap_request, &payment)
507 .await
508 {
509 Ok(receipt) => receipt,
510 Err(e) => {
511 error!(
512 "Failed to create zap receipt for payment {}: {e:?}",
513 payment.id
514 );
515 continue;
516 }
517 };
518
519 let zap_receipt = match lnurl_server_client
521 .publish_zap_receipt(&PublishZapReceiptRequest {
522 payment_hash: payment_hash.clone(),
523 zap_receipt: zap_receipt.clone(),
524 })
525 .await
526 {
527 Ok(zap_receipt) => zap_receipt,
528 Err(e) => {
529 error!(
530 "Failed to publish zap receipt for payment {}: {}",
531 payment.id, e
532 );
533 continue;
534 }
535 };
536
537 if let Err(e) = self
538 .storage
539 .set_lnurl_metadata(vec![SetLnurlMetadataItem {
540 sender_comment: lnurl_receive_metadata.sender_comment.clone(),
541 nostr_zap_request: Some(zap_request.clone()),
542 nostr_zap_receipt: Some(zap_receipt),
543 payment_hash: payment_hash.clone(),
544 }])
545 .await
546 {
547 error!(
548 "Failed to store zap receipt for payment {}: {}",
549 payment.id, e
550 );
551 }
552 }
553
554 if len < limit {
555 break;
556 }
557
558 offset = offset.saturating_add(len);
559 }
560
561 Ok(())
562 }
563
564 fn periodic_sync(&self, initial_synced_sender: watch::Sender<bool>) {
565 let sdk = self.clone();
566 let mut shutdown_receiver = sdk.shutdown_sender.subscribe();
567 let mut subscription = sdk.spark_wallet.subscribe_events();
568 let sync_trigger_sender = sdk.sync_trigger.clone();
569 let mut sync_trigger_receiver = sdk.sync_trigger.clone().subscribe();
570 let mut last_sync_time = SystemTime::now();
571 let sync_interval = u64::from(self.config.sync_interval_secs);
572 tokio::spawn(async move {
573 let balance_watcher =
574 BalanceWatcher::new(sdk.spark_wallet.clone(), sdk.storage.clone());
575 let balance_watcher_id = sdk.add_event_listener(Box::new(balance_watcher)).await;
576 loop {
577 tokio::select! {
578 _ = shutdown_receiver.changed() => {
579 if !sdk.remove_event_listener(&balance_watcher_id).await {
580 error!("Failed to remove balance watcher listener");
581 }
582 info!("Deposit tracking loop shutdown signal received");
583 return;
584 }
585 event = subscription.recv() => {
586 match event {
587 Ok(event) => {
588 info!("Received event: {event}");
589 trace!("Received event: {:?}", event);
590 sdk.handle_wallet_event(event).await;
591 }
592 Err(e) => {
593 error!("Failed to receive event: {e:?}");
594 }
595 }
596 }
597 sync_type_res = sync_trigger_receiver.recv() => {
598 let Ok(sync_request) = sync_type_res else {
599 continue;
600 };
601 info!("Sync trigger changed: {:?}", &sync_request);
602 let cloned_sdk = sdk.clone();
603 let initial_synced_sender = initial_synced_sender.clone();
604 if let Some(true) = Box::pin(run_with_shutdown(shutdown_receiver.clone(), "Sync trigger changed", async move {
605 if let Err(e) = cloned_sdk.sync_wallet_internal(sync_request.sync_type.clone()).await {
606 error!("Failed to sync wallet: {e:?}");
607 let () = sync_request.reply(Some(e)).await;
608 return false;
609 }
610 let () = sync_request.reply(None).await;
612 if sync_request.sync_type.contains(SyncType::Full) {
614 if let Err(e) = initial_synced_sender.send(true) {
615 error!("Failed to send initial synced signal: {e:?}");
616 }
617 return true;
618 }
619
620 false
621 })).await {
622 last_sync_time = SystemTime::now();
623 }
624 }
625 () = tokio::time::sleep(Duration::from_secs(10)) => {
627 let now = SystemTime::now();
628 if let Ok(elapsed) = now.duration_since(last_sync_time) && elapsed.as_secs() >= sync_interval
629 && let Err(e) = sync_trigger_sender.send(SyncRequest::full(None)) {
630 error!("Failed to trigger periodic sync: {e:?}");
631 }
632 }
633 }
634 }
635 });
636 }
637
638 async fn handle_wallet_event(&self, event: WalletEvent) {
639 match event {
640 WalletEvent::DepositConfirmed(_) => {
641 info!("Deposit confirmed");
642 }
643 WalletEvent::StreamConnected => {
644 info!("Stream connected");
645 }
646 WalletEvent::StreamDisconnected => {
647 info!("Stream disconnected");
648 }
649 WalletEvent::Synced => {
650 info!("Synced");
651 if let Err(e) = self.sync_trigger.send(SyncRequest::full(None)) {
652 error!("Failed to sync wallet: {e:?}");
653 }
654 }
655 WalletEvent::TransferClaimed(transfer) => {
656 info!("Transfer claimed");
657 if let Ok(mut payment) = Payment::try_from(transfer) {
658 if let Err(e) = self.storage.insert_payment(payment.clone()).await {
660 error!("Failed to insert succeeded payment: {e:?}");
661 }
662
663 self.sync_single_lnurl_metadata(&mut payment).await;
666
667 self.event_emitter
668 .emit(&SdkEvent::PaymentSucceeded { payment })
669 .await;
670 }
671 if let Err(e) = self
672 .sync_trigger
673 .send(SyncRequest::no_reply(SyncType::WalletState))
674 {
675 error!("Failed to sync wallet: {e:?}");
676 }
677 }
678 WalletEvent::TransferClaimStarting(transfer) => {
679 info!("Transfer claim starting");
680 if let Ok(mut payment) = Payment::try_from(transfer) {
681 if let Err(e) = self.storage.insert_payment(payment.clone()).await {
683 error!("Failed to insert pending payment: {e:?}");
684 }
685
686 self.sync_single_lnurl_metadata(&mut payment).await;
688
689 self.event_emitter
690 .emit(&SdkEvent::PaymentPending { payment })
691 .await;
692 }
693 if let Err(e) = self
694 .sync_trigger
695 .send(SyncRequest::no_reply(SyncType::WalletState))
696 {
697 error!("Failed to sync wallet: {e:?}");
698 }
699 }
700 WalletEvent::Optimization(event) => {
701 info!("Optimization event: {:?}", event);
702 }
703 }
704 }
705
706 async fn sync_single_lnurl_metadata(&self, payment: &mut Payment) {
707 if payment.payment_type != PaymentType::Receive {
708 return;
709 }
710
711 let Some(PaymentDetails::Lightning {
712 invoice,
713 lnurl_receive_metadata,
714 ..
715 }) = &mut payment.details
716 else {
717 return;
718 };
719
720 if lnurl_receive_metadata.is_some() {
721 return;
723 }
724
725 let Ok(input) = parse_input(invoice, None).await else {
726 error!(
727 "Failed to parse invoice for lnurl metadata sync: {}",
728 invoice
729 );
730 return;
731 };
732
733 let InputType::Bolt11Invoice(details) = input else {
734 error!(
735 "Input is not a Bolt11 invoice for lnurl metadata sync: {}",
736 invoice
737 );
738 return;
739 };
740
741 if details.description_hash.is_none() {
743 return;
744 }
745
746 if let Ok(db_payment) = self.storage.get_payment_by_id(payment.id.clone()).await
748 && let Some(PaymentDetails::Lightning {
749 lnurl_receive_metadata: db_lnurl_receive_metadata,
750 ..
751 }) = db_payment.details
752 {
753 *lnurl_receive_metadata = db_lnurl_receive_metadata;
754 return;
755 }
756
757 let (tx, rx) = oneshot::channel();
759 if let Err(e) = self
760 .sync_trigger
761 .send(SyncRequest::new(tx, SyncType::LnurlMetadata))
762 {
763 error!("Failed to trigger lnurl metadata sync: {e}");
764 return;
765 }
766
767 if let Err(e) = rx.await {
768 error!("Failed to sync lnurl metadata for invoice {}: {e}", invoice);
769 return;
770 }
771
772 let db_payment = match self.storage.get_payment_by_id(payment.id.clone()).await {
773 Ok(p) => p,
774 Err(e) => {
775 debug!("Payment not found in storage for invoice {}: {e}", invoice);
776 return;
777 }
778 };
779
780 let Some(PaymentDetails::Lightning {
781 lnurl_receive_metadata: db_lnurl_receive_metadata,
782 ..
783 }) = db_payment.details
784 else {
785 debug!(
786 "No lnurl receive metadata in storage for invoice {}",
787 invoice
788 );
789 return;
790 };
791 *lnurl_receive_metadata = db_lnurl_receive_metadata;
792 }
793
794 #[allow(clippy::too_many_lines)]
795 async fn sync_wallet_internal(&self, sync_type: SyncType) -> Result<(), SdkError> {
796 let start_time = Instant::now();
797
798 let sync_wallet = async {
799 let wallet_synced = if sync_type.contains(SyncType::Wallet) {
800 debug!("sync_wallet_internal: Starting Wallet sync");
801 let wallet_start = Instant::now();
802 match self.spark_wallet.sync().await {
803 Ok(()) => {
804 debug!(
805 "sync_wallet_internal: Wallet sync completed in {:?}",
806 wallet_start.elapsed()
807 );
808 true
809 }
810 Err(e) => {
811 error!(
812 "sync_wallet_internal: Spark wallet sync failed in {:?}: {e:?}",
813 wallet_start.elapsed()
814 );
815 false
816 }
817 }
818 } else {
819 trace!("sync_wallet_internal: Skipping Wallet sync");
820 false
821 };
822
823 let wallet_state_synced = if sync_type.contains(SyncType::WalletState) {
824 debug!("sync_wallet_internal: Starting WalletState sync");
825 let wallet_state_start = Instant::now();
826 match self.sync_wallet_state_to_storage().await {
827 Ok(()) => {
828 debug!(
829 "sync_wallet_internal: WalletState sync completed in {:?}",
830 wallet_state_start.elapsed()
831 );
832 true
833 }
834 Err(e) => {
835 error!(
836 "sync_wallet_internal: Failed to sync wallet state to storage in {:?}: {e:?}",
837 wallet_state_start.elapsed()
838 );
839 false
840 }
841 }
842 } else {
843 trace!("sync_wallet_internal: Skipping WalletState sync");
844 false
845 };
846
847 (wallet_synced, wallet_state_synced)
848 };
849
850 let sync_lnurl = async {
851 if sync_type.contains(SyncType::LnurlMetadata) {
852 debug!("sync_wallet_internal: Starting LnurlMetadata sync");
853 let lnurl_start = Instant::now();
854 match self.sync_lnurl_metadata().await {
855 Ok(()) => {
856 debug!(
857 "sync_wallet_internal: LnurlMetadata sync completed in {:?}",
858 lnurl_start.elapsed()
859 );
860 true
861 }
862 Err(e) => {
863 error!(
864 "sync_wallet_internal: Failed to sync lnurl metadata in {:?}: {e:?}",
865 lnurl_start.elapsed()
866 );
867 false
868 }
869 }
870 } else {
871 trace!("sync_wallet_internal: Skipping LnurlMetadata sync");
872 false
873 }
874 };
875
876 let sync_deposits = async {
877 if sync_type.contains(SyncType::Deposits) {
878 debug!("sync_wallet_internal: Starting Deposits sync");
879 let deposits_start = Instant::now();
880 match self.check_and_claim_static_deposits().await {
881 Ok(()) => {
882 debug!(
883 "sync_wallet_internal: Deposits sync completed in {:?}",
884 deposits_start.elapsed()
885 );
886 true
887 }
888 Err(e) => {
889 error!(
890 "sync_wallet_internal: Failed to check and claim static deposits in {:?}: {e:?}",
891 deposits_start.elapsed()
892 );
893 false
894 }
895 }
896 } else {
897 trace!("sync_wallet_internal: Skipping Deposits sync");
898 false
899 }
900 };
901
902 let ((wallet, wallet_state), lnurl_metadata, deposits) =
903 tokio::join!(sync_wallet, sync_lnurl, sync_deposits);
904
905 let elapsed = start_time.elapsed();
906 let event = InternalSyncedEvent {
907 wallet,
908 wallet_state,
909 lnurl_metadata,
910 deposits,
911 storage_incoming: None,
912 };
913 info!("sync_wallet_internal: Wallet sync completed in {elapsed:?}: {event:?}");
914 self.event_emitter.emit_synced(&event).await;
915 Ok(())
916 }
917
918 async fn sync_wallet_state_to_storage(&self) -> Result<(), SdkError> {
920 update_balances(self.spark_wallet.clone(), self.storage.clone()).await?;
921
922 let initial_sync_complete = *self.initial_synced_watcher.borrow();
923 let sync_service = SparkSyncService::new(
924 self.spark_wallet.clone(),
925 self.storage.clone(),
926 self.event_emitter.clone(),
927 );
928 sync_service.sync_payments(initial_sync_complete).await?;
929
930 Ok(())
931 }
932
933 async fn check_and_claim_static_deposits(&self) -> Result<(), SdkError> {
934 self.ensure_spark_private_mode_initialized().await?;
935 let to_claim = DepositChainSyncer::new(
936 self.chain_service.clone(),
937 self.storage.clone(),
938 self.spark_wallet.clone(),
939 )
940 .sync()
941 .await?;
942
943 let mut claimed_deposits: Vec<DepositInfo> = Vec::new();
944 let mut unclaimed_deposits: Vec<DepositInfo> = Vec::new();
945 for detailed_utxo in to_claim {
946 match self
947 .claim_utxo(&detailed_utxo, self.config.max_deposit_claim_fee.clone())
948 .await
949 {
950 Ok(_) => {
951 info!("Claimed utxo {}:{}", detailed_utxo.txid, detailed_utxo.vout);
952 self.storage
953 .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
954 .await?;
955 claimed_deposits.push(detailed_utxo.into());
956 }
957 Err(e) => {
958 warn!(
959 "Failed to claim utxo {}:{}: {e}",
960 detailed_utxo.txid, detailed_utxo.vout
961 );
962 self.storage
963 .update_deposit(
964 detailed_utxo.txid.to_string(),
965 detailed_utxo.vout,
966 UpdateDepositPayload::ClaimError {
967 error: e.clone().into(),
968 },
969 )
970 .await?;
971 let mut unclaimed_deposit: DepositInfo = detailed_utxo.clone().into();
972 unclaimed_deposit.claim_error = Some(e.into());
973 unclaimed_deposits.push(unclaimed_deposit);
974 }
975 }
976 }
977
978 info!("background claim completed, unclaimed deposits: {unclaimed_deposits:?}");
979
980 if !unclaimed_deposits.is_empty() {
981 self.event_emitter
982 .emit(&SdkEvent::UnclaimedDeposits { unclaimed_deposits })
983 .await;
984 }
985 if !claimed_deposits.is_empty() {
986 self.event_emitter
987 .emit(&SdkEvent::ClaimedDeposits { claimed_deposits })
988 .await;
989 }
990 Ok(())
991 }
992
993 async fn sync_lnurl_metadata(&self) -> Result<(), SdkError> {
994 let Some(lnurl_server_client) = self.lnurl_server_client.clone() else {
995 return Ok(());
996 };
997
998 let cache = ObjectCacheRepository::new(Arc::clone(&self.storage));
999 let mut updated_after = cache.fetch_lnurl_metadata_updated_after().await?;
1000
1001 loop {
1002 debug!("Syncing lnurl metadata from updated_after {updated_after}");
1003 let metadata = lnurl_server_client
1004 .list_metadata(&ListMetadataRequest {
1005 offset: None,
1006 limit: Some(SYNC_PAGING_LIMIT),
1007 updated_after: Some(updated_after),
1008 })
1009 .await?;
1010
1011 if metadata.metadata.is_empty() {
1012 debug!("No more lnurl metadata on offset {updated_after}");
1013 break;
1014 }
1015
1016 let len = u32::try_from(metadata.metadata.len())?;
1017 let last_updated_at = metadata.metadata.last().map(|m| m.updated_at);
1018 self.storage
1019 .set_lnurl_metadata(metadata.metadata.into_iter().map(From::from).collect())
1020 .await?;
1021
1022 debug!(
1023 "Synchronized {} lnurl metadata at updated_after {updated_after}",
1024 len
1025 );
1026 updated_after = last_updated_at.unwrap_or(updated_after);
1027 cache
1028 .save_lnurl_metadata_updated_after(updated_after)
1029 .await?;
1030
1031 let _ = self.zap_receipt_trigger.send(());
1032 if len < SYNC_PAGING_LIMIT {
1033 break;
1035 }
1036 }
1037
1038 Ok(())
1039 }
1040
1041 async fn refund_failed_conversions(&self) -> Result<(), SdkError> {
1045 debug!("Checking for failed conversions needing refunds");
1046 let payments = self
1047 .storage
1048 .list_payments(ListPaymentsRequest {
1049 payment_details_filter: Some(vec![
1050 PaymentDetailsFilter::Spark {
1051 htlc_status: None,
1052 conversion_refund_needed: Some(true),
1053 },
1054 PaymentDetailsFilter::Token {
1055 conversion_refund_needed: Some(true),
1056 tx_hash: None,
1057 },
1058 ]),
1059 ..Default::default()
1060 })
1061 .await?;
1062 debug!(
1063 "Found {} payments needing conversion refunds",
1064 payments.len()
1065 );
1066 for payment in payments {
1067 if let Err(e) = self.refund_conversion(&payment).await {
1068 error!(
1069 "Failed to refund conversion for payment {}: {e:?}",
1070 payment.id
1071 );
1072 }
1073 }
1074
1075 Ok(())
1076 }
1077
1078 async fn refund_conversion(&self, payment: &Payment) -> Result<(), SdkError> {
1080 let (clawback_id, conversion_info) = match &payment.details {
1081 Some(PaymentDetails::Spark {
1082 conversion_info, ..
1083 }) => (payment.id.clone(), conversion_info),
1084 Some(PaymentDetails::Token {
1085 tx_hash,
1086 conversion_info,
1087 ..
1088 }) => (tx_hash.clone(), conversion_info),
1089 _ => {
1090 return Err(SdkError::Generic(
1091 "Payment is not a Spark or Conversion".to_string(),
1092 ));
1093 }
1094 };
1095 let Some(ConversionInfo {
1096 pool_id,
1097 conversion_id,
1098 status: ConversionStatus::RefundNeeded,
1099 fee,
1100 purpose,
1101 }) = conversion_info
1102 else {
1103 return Err(SdkError::Generic(
1104 "Conversion does not have a refund pending status".to_string(),
1105 ));
1106 };
1107 debug!(
1108 "Conversion refund needed for payment {}: pool_id {pool_id}, conversion_id {conversion_id}",
1109 payment.id
1110 );
1111 let Ok(pool_id) = PublicKey::from_str(pool_id) else {
1112 return Err(SdkError::Generic(format!("Invalid pool_id: {pool_id}")));
1113 };
1114 match self
1115 .flashnet_client
1116 .clawback(ClawbackRequest {
1117 pool_id,
1118 transfer_id: clawback_id,
1119 })
1120 .await
1121 {
1122 Ok(ClawbackResponse {
1123 accepted: true,
1124 spark_status_tracking_id,
1125 ..
1126 }) => {
1127 debug!(
1128 "Clawback initiated for payment {}: tracking_id: {}",
1129 payment.id, spark_status_tracking_id
1130 );
1131 self.merge_payment_metadata(
1133 payment.id.clone(),
1134 PaymentMetadata {
1135 conversion_info: Some(ConversionInfo {
1136 pool_id: pool_id.to_string(),
1137 conversion_id: conversion_id.clone(),
1138 status: ConversionStatus::Refunded,
1139 fee: *fee,
1140 purpose: purpose.clone(),
1141 }),
1142 ..Default::default()
1143 },
1144 )
1145 .await?;
1146 let cache = ObjectCacheRepository::new(self.storage.clone());
1148 cache
1149 .save_payment_metadata(
1150 &spark_status_tracking_id,
1151 &PaymentMetadata {
1152 conversion_info: Some(ConversionInfo {
1153 pool_id: pool_id.to_string(),
1154 conversion_id: conversion_id.clone(),
1155 status: ConversionStatus::Refunded,
1156 fee: Some(0),
1157 purpose: None,
1158 }),
1159 ..Default::default()
1160 },
1161 )
1162 .await?;
1163 Ok(())
1164 }
1165 Ok(ClawbackResponse {
1166 accepted: false,
1167 request_id,
1168 error,
1169 ..
1170 }) => Err(SdkError::Generic(format!(
1171 "Clawback not accepted: request_id: {request_id:?}, error: {error:?}"
1172 ))),
1173 Err(e) => Err(SdkError::Generic(format!(
1174 "Failed to initiate clawback: {e}"
1175 ))),
1176 }
1177 }
1178
1179 async fn claim_utxo(
1180 &self,
1181 detailed_utxo: &DetailedUtxo,
1182 max_claim_fee: Option<MaxFee>,
1183 ) -> Result<WalletTransfer, SdkError> {
1184 info!(
1185 "Fetching static deposit claim quote for deposit tx {}:{} and amount: {}",
1186 detailed_utxo.txid, detailed_utxo.vout, detailed_utxo.value
1187 );
1188 let quote = self
1189 .spark_wallet
1190 .fetch_static_deposit_claim_quote(detailed_utxo.tx.clone(), Some(detailed_utxo.vout))
1191 .await?;
1192
1193 let spark_requested_fee_sats = detailed_utxo.value.saturating_sub(quote.credit_amount_sats);
1194
1195 let spark_requested_fee_rate = spark_requested_fee_sats.div_ceil(CLAIM_TX_SIZE_VBYTES);
1196
1197 let Some(max_deposit_claim_fee) = max_claim_fee else {
1198 return Err(SdkError::MaxDepositClaimFeeExceeded {
1199 tx: detailed_utxo.txid.to_string(),
1200 vout: detailed_utxo.vout,
1201 max_fee: None,
1202 required_fee_sats: spark_requested_fee_sats,
1203 required_fee_rate_sat_per_vbyte: spark_requested_fee_rate,
1204 });
1205 };
1206 let max_fee = max_deposit_claim_fee
1207 .to_fee(self.chain_service.as_ref())
1208 .await?;
1209 let max_fee_sats = max_fee.to_sats(CLAIM_TX_SIZE_VBYTES);
1210 info!(
1211 "User max fee: {} spark requested fee: {}",
1212 max_fee_sats, spark_requested_fee_sats
1213 );
1214 if spark_requested_fee_sats > max_fee_sats {
1215 return Err(SdkError::MaxDepositClaimFeeExceeded {
1216 tx: detailed_utxo.txid.to_string(),
1217 vout: detailed_utxo.vout,
1218 max_fee: Some(max_fee),
1219 required_fee_sats: spark_requested_fee_sats,
1220 required_fee_rate_sat_per_vbyte: spark_requested_fee_rate,
1221 });
1222 }
1223
1224 info!(
1225 "Claiming static deposit for utxo {}:{}",
1226 detailed_utxo.txid, detailed_utxo.vout
1227 );
1228 let transfer = self.spark_wallet.claim_static_deposit(quote).await?;
1229 info!(
1230 "Claimed static deposit transfer for utxo {}:{}, value {}",
1231 detailed_utxo.txid, detailed_utxo.vout, transfer.total_value_sat,
1232 );
1233 Ok(transfer)
1234 }
1235
1236 async fn ensure_spark_private_mode_initialized(&self) -> Result<(), SdkError> {
1237 self.spark_private_mode_initialized
1238 .get_or_try_init(|| async {
1239 let object_repository = ObjectCacheRepository::new(self.storage.clone());
1241 let is_initialized = object_repository
1242 .fetch_spark_private_mode_initialized()
1243 .await?;
1244
1245 if !is_initialized {
1246 self.initialize_spark_private_mode().await?;
1248 }
1249 Ok::<_, SdkError>(())
1250 })
1251 .await?;
1252 Ok(())
1253 }
1254
1255 async fn initialize_spark_private_mode(&self) -> Result<(), SdkError> {
1256 if !self.config.private_enabled_default {
1257 ObjectCacheRepository::new(self.storage.clone())
1258 .save_spark_private_mode_initialized()
1259 .await?;
1260 info!("Spark private mode initialized: no changes needed");
1261 return Ok(());
1262 }
1263
1264 self.update_user_settings(UpdateUserSettingsRequest {
1266 spark_private_mode_enabled: Some(true),
1267 })
1268 .await?;
1269 ObjectCacheRepository::new(self.storage.clone())
1270 .save_spark_private_mode_initialized()
1271 .await?;
1272 info!("Spark private mode initialized: enabled");
1273 Ok(())
1274 }
1275}
1276
1277#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
1278#[allow(clippy::needless_pass_by_value)]
1279impl BreezSdk {
1280 pub async fn add_event_listener(&self, listener: Box<dyn EventListener>) -> String {
1290 self.event_emitter.add_listener(listener).await
1291 }
1292
1293 pub async fn remove_event_listener(&self, id: &str) -> bool {
1303 self.event_emitter.remove_listener(id).await
1304 }
1305
1306 pub async fn disconnect(&self) -> Result<(), SdkError> {
1315 info!("Disconnecting Breez SDK");
1316 self.shutdown_sender
1317 .send(())
1318 .map_err(|_| SdkError::Generic("Failed to send shutdown signal".to_string()))?;
1319
1320 self.shutdown_sender.closed().await;
1321 info!("Breez SDK disconnected");
1322 Ok(())
1323 }
1324
1325 pub async fn parse(&self, input: &str) -> Result<InputType, SdkError> {
1326 parse_input(input, Some(self.external_input_parsers.clone())).await
1327 }
1328
1329 #[allow(unused_variables)]
1331 pub async fn get_info(&self, request: GetInfoRequest) -> Result<GetInfoResponse, SdkError> {
1332 if request.ensure_synced.unwrap_or_default() {
1333 self.initial_synced_watcher
1334 .clone()
1335 .changed()
1336 .await
1337 .map_err(|_| {
1338 SdkError::Generic("Failed to receive initial synced signal".to_string())
1339 })?;
1340 }
1341 let object_repository = ObjectCacheRepository::new(self.storage.clone());
1342 let account_info = object_repository
1343 .fetch_account_info()
1344 .await?
1345 .unwrap_or_default();
1346 Ok(GetInfoResponse {
1347 balance_sats: account_info.balance_sats,
1348 token_balances: account_info.token_balances,
1349 })
1350 }
1351
1352 pub async fn receive_payment(
1353 &self,
1354 request: ReceivePaymentRequest,
1355 ) -> Result<ReceivePaymentResponse, SdkError> {
1356 self.ensure_spark_private_mode_initialized().await?;
1357 match request.payment_method {
1358 ReceivePaymentMethod::SparkAddress => Ok(ReceivePaymentResponse {
1359 fee: 0,
1360 payment_request: self
1361 .spark_wallet
1362 .get_spark_address()?
1363 .to_address_string()
1364 .map_err(|e| {
1365 SdkError::Generic(format!("Failed to convert Spark address to string: {e}"))
1366 })?,
1367 }),
1368 ReceivePaymentMethod::SparkInvoice {
1369 amount,
1370 token_identifier,
1371 expiry_time,
1372 description,
1373 sender_public_key,
1374 } => {
1375 let invoice = self
1376 .spark_wallet
1377 .create_spark_invoice(
1378 amount,
1379 token_identifier.clone(),
1380 expiry_time
1381 .map(|time| {
1382 SystemTime::UNIX_EPOCH
1383 .checked_add(Duration::from_secs(time))
1384 .ok_or(SdkError::Generic("Invalid expiry time".to_string()))
1385 })
1386 .transpose()?,
1387 description,
1388 sender_public_key.map(|key| PublicKey::from_str(&key).unwrap()),
1389 )
1390 .await?;
1391 Ok(ReceivePaymentResponse {
1392 fee: 0,
1393 payment_request: invoice,
1394 })
1395 }
1396 ReceivePaymentMethod::BitcoinAddress => {
1397 let object_repository = ObjectCacheRepository::new(self.storage.clone());
1400
1401 let static_deposit_address =
1403 object_repository.fetch_static_deposit_address().await?;
1404 if let Some(static_deposit_address) = static_deposit_address {
1405 return Ok(ReceivePaymentResponse {
1406 payment_request: static_deposit_address.address.clone(),
1407 fee: 0,
1408 });
1409 }
1410
1411 let deposit_addresses = self
1413 .spark_wallet
1414 .list_static_deposit_addresses(None)
1415 .await?;
1416
1417 let address = match deposit_addresses.items.last() {
1419 Some(address) => address.to_string(),
1420 None => self
1421 .spark_wallet
1422 .generate_deposit_address(true)
1423 .await?
1424 .to_string(),
1425 };
1426
1427 object_repository
1428 .save_static_deposit_address(&StaticDepositAddress {
1429 address: address.clone(),
1430 })
1431 .await?;
1432
1433 Ok(ReceivePaymentResponse {
1434 payment_request: address,
1435 fee: 0,
1436 })
1437 }
1438 ReceivePaymentMethod::Bolt11Invoice {
1439 description,
1440 amount_sats,
1441 expiry_secs,
1442 } => Ok(ReceivePaymentResponse {
1443 payment_request: self
1444 .spark_wallet
1445 .create_lightning_invoice(
1446 amount_sats.unwrap_or_default(),
1447 Some(InvoiceDescription::Memo(description.clone())),
1448 None,
1449 expiry_secs,
1450 self.config.prefer_spark_over_lightning,
1451 )
1452 .await?
1453 .invoice,
1454 fee: 0,
1455 }),
1456 }
1457 }
1458
1459 pub async fn claim_htlc_payment(
1460 &self,
1461 request: ClaimHtlcPaymentRequest,
1462 ) -> Result<ClaimHtlcPaymentResponse, SdkError> {
1463 let preimage = Preimage::from_hex(&request.preimage)
1464 .map_err(|_| SdkError::InvalidInput("Invalid preimage".to_string()))?;
1465 let payment_hash = preimage.compute_hash();
1466
1467 let claimable_htlc_transfers = self
1469 .spark_wallet
1470 .list_claimable_htlc_transfers(None)
1471 .await?;
1472 if !claimable_htlc_transfers
1473 .iter()
1474 .filter_map(|t| t.htlc_preimage_request.as_ref())
1475 .any(|p| p.payment_hash == payment_hash)
1476 {
1477 return Err(SdkError::InvalidInput(
1478 "No claimable HTLC with the given payment hash".to_string(),
1479 ));
1480 }
1481
1482 let transfer = self.spark_wallet.claim_htlc(&preimage).await?;
1483 let payment: Payment = transfer.try_into()?;
1484
1485 self.storage.insert_payment(payment.clone()).await?;
1487
1488 Ok(ClaimHtlcPaymentResponse { payment })
1489 }
1490
1491 pub async fn prepare_lnurl_pay(
1492 &self,
1493 request: PrepareLnurlPayRequest,
1494 ) -> Result<PrepareLnurlPayResponse, SdkError> {
1495 let success_data = match validate_lnurl_pay(
1496 self.lnurl_client.as_ref(),
1497 request.amount_sats.saturating_mul(1_000),
1498 &None,
1499 &request.pay_request.clone().into(),
1500 self.config.network.into(),
1501 request.validate_success_action_url,
1502 )
1503 .await?
1504 {
1505 lnurl::pay::ValidatedCallbackResponse::EndpointError { data } => {
1506 return Err(LnurlError::EndpointError(data.reason).into());
1507 }
1508 lnurl::pay::ValidatedCallbackResponse::EndpointSuccess { data } => data,
1509 };
1510
1511 let prepare_response = self
1512 .prepare_send_payment(PrepareSendPaymentRequest {
1513 payment_request: success_data.pr,
1514 amount: Some(request.amount_sats.into()),
1515 token_identifier: None,
1516 conversion_options: None,
1517 })
1518 .await?;
1519
1520 let SendPaymentMethod::Bolt11Invoice {
1521 invoice_details,
1522 lightning_fee_sats,
1523 ..
1524 } = prepare_response.payment_method
1525 else {
1526 return Err(SdkError::Generic(
1527 "Expected Bolt11Invoice payment method".to_string(),
1528 ));
1529 };
1530
1531 Ok(PrepareLnurlPayResponse {
1532 amount_sats: request.amount_sats,
1533 comment: request.comment,
1534 pay_request: request.pay_request,
1535 invoice_details,
1536 fee_sats: lightning_fee_sats,
1537 success_action: success_data.success_action.map(From::from),
1538 })
1539 }
1540
1541 pub async fn lnurl_pay(&self, request: LnurlPayRequest) -> Result<LnurlPayResponse, SdkError> {
1542 self.ensure_spark_private_mode_initialized().await?;
1543 let mut payment = Box::pin(self.maybe_convert_token_send_payment(
1544 SendPaymentRequest {
1545 prepare_response: PrepareSendPaymentResponse {
1546 payment_method: SendPaymentMethod::Bolt11Invoice {
1547 invoice_details: request.prepare_response.invoice_details,
1548 spark_transfer_fee_sats: None,
1549 lightning_fee_sats: request.prepare_response.fee_sats,
1550 },
1551 amount: request.prepare_response.amount_sats.into(),
1552 token_identifier: None,
1553 conversion_estimate: None,
1554 },
1555 options: None,
1556 idempotency_key: request.idempotency_key,
1557 },
1558 true,
1559 ))
1560 .await?
1561 .payment;
1562
1563 let success_action = process_success_action(
1564 &payment,
1565 request
1566 .prepare_response
1567 .success_action
1568 .clone()
1569 .map(Into::into)
1570 .as_ref(),
1571 )?;
1572
1573 let lnurl_info = LnurlPayInfo {
1574 ln_address: request.prepare_response.pay_request.address,
1575 comment: request.prepare_response.comment,
1576 domain: Some(request.prepare_response.pay_request.domain),
1577 metadata: Some(request.prepare_response.pay_request.metadata_str),
1578 processed_success_action: success_action.clone().map(From::from),
1579 raw_success_action: request.prepare_response.success_action,
1580 };
1581 let Some(PaymentDetails::Lightning {
1582 lnurl_pay_info,
1583 description,
1584 ..
1585 }) = &mut payment.details
1586 else {
1587 return Err(SdkError::Generic(
1588 "Expected Lightning payment details".to_string(),
1589 ));
1590 };
1591 *lnurl_pay_info = Some(lnurl_info.clone());
1592
1593 let lnurl_description = lnurl_info.extract_description();
1594 description.clone_from(&lnurl_description);
1595
1596 self.storage
1597 .set_payment_metadata(
1598 payment.id.clone(),
1599 PaymentMetadata {
1600 lnurl_pay_info: Some(lnurl_info),
1601 lnurl_description,
1602 ..Default::default()
1603 },
1604 )
1605 .await?;
1606
1607 self.event_emitter
1608 .emit(&SdkEvent::from_payment(payment.clone()))
1609 .await;
1610 Ok(LnurlPayResponse {
1611 payment,
1612 success_action: success_action.map(From::from),
1613 })
1614 }
1615
1616 pub async fn lnurl_withdraw(
1642 &self,
1643 request: LnurlWithdrawRequest,
1644 ) -> Result<LnurlWithdrawResponse, SdkError> {
1645 self.ensure_spark_private_mode_initialized().await?;
1646 let LnurlWithdrawRequest {
1647 amount_sats,
1648 withdraw_request,
1649 completion_timeout_secs,
1650 } = request;
1651 let withdraw_request: breez_sdk_common::lnurl::withdraw::LnurlWithdrawRequestDetails =
1652 withdraw_request.into();
1653 if !withdraw_request.is_amount_valid(amount_sats) {
1654 return Err(SdkError::InvalidInput(
1655 "Amount must be within min/max LNURL withdrawable limits".to_string(),
1656 ));
1657 }
1658
1659 let payment_request = self
1661 .receive_payment(ReceivePaymentRequest {
1662 payment_method: ReceivePaymentMethod::Bolt11Invoice {
1663 description: withdraw_request.default_description.clone(),
1664 amount_sats: Some(amount_sats),
1665 expiry_secs: None,
1666 },
1667 })
1668 .await?
1669 .payment_request;
1670
1671 let cache = ObjectCacheRepository::new(self.storage.clone());
1673 cache
1674 .save_payment_metadata(
1675 &payment_request,
1676 &PaymentMetadata {
1677 lnurl_withdraw_info: Some(LnurlWithdrawInfo {
1678 withdraw_url: withdraw_request.callback.clone(),
1679 }),
1680 lnurl_description: Some(withdraw_request.default_description.clone()),
1681 ..Default::default()
1682 },
1683 )
1684 .await?;
1685
1686 let withdraw_response = execute_lnurl_withdraw(
1688 self.lnurl_client.as_ref(),
1689 &withdraw_request,
1690 &payment_request,
1691 )
1692 .await?;
1693 if let lnurl::withdraw::ValidatedCallbackResponse::EndpointError { data } =
1694 withdraw_response
1695 {
1696 return Err(LnurlError::EndpointError(data.reason).into());
1697 }
1698
1699 let completion_timeout_secs = match completion_timeout_secs {
1700 Some(secs) if secs > 0 => secs,
1701 _ => {
1702 return Ok(LnurlWithdrawResponse {
1703 payment_request,
1704 payment: None,
1705 });
1706 }
1707 };
1708
1709 let payment = self
1711 .wait_for_payment(
1712 WaitForPaymentIdentifier::PaymentRequest(payment_request.clone()),
1713 completion_timeout_secs,
1714 )
1715 .await
1716 .ok();
1717 Ok(LnurlWithdrawResponse {
1718 payment_request,
1719 payment,
1720 })
1721 }
1722
1723 pub async fn lnurl_auth(
1771 &self,
1772 request_data: LnurlAuthRequestDetails,
1773 ) -> Result<LnurlCallbackStatus, SdkError> {
1774 let request: breez_sdk_common::lnurl::auth::LnurlAuthRequestDetails = request_data.into();
1775 let status = breez_sdk_common::lnurl::auth::perform_lnurl_auth(
1776 self.lnurl_client.as_ref(),
1777 &request,
1778 self.lnurl_auth_signer.as_ref(),
1779 )
1780 .await
1781 .map_err(|e| match e {
1782 LnurlError::ServiceConnectivity(msg) => SdkError::NetworkError(msg.to_string()),
1783 LnurlError::InvalidUri(msg) => SdkError::InvalidInput(msg),
1784 _ => SdkError::Generic(e.to_string()),
1785 })?;
1786 Ok(status.into())
1787 }
1788
1789 #[allow(clippy::too_many_lines)]
1790 pub async fn prepare_send_payment(
1791 &self,
1792 request: PrepareSendPaymentRequest,
1793 ) -> Result<PrepareSendPaymentResponse, SdkError> {
1794 let parsed_input = self.parse(&request.payment_request).await?;
1795
1796 validate_prepare_send_payment_request(
1797 &parsed_input,
1798 &request,
1799 &self.spark_wallet.get_identity_public_key().to_string(),
1800 )?;
1801
1802 match &parsed_input {
1803 InputType::SparkAddress(spark_address_details) => {
1804 let amount = request
1805 .amount
1806 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?;
1807 let conversion_estimate = self
1808 .estimate_conversion(
1809 request.conversion_options.as_ref(),
1810 request.token_identifier.as_ref(),
1811 amount,
1812 )
1813 .await?;
1814
1815 Ok(PrepareSendPaymentResponse {
1816 payment_method: SendPaymentMethod::SparkAddress {
1817 address: spark_address_details.address.clone(),
1818 fee: 0,
1819 token_identifier: request.token_identifier.clone(),
1820 },
1821 amount,
1822 token_identifier: request.token_identifier,
1823 conversion_estimate,
1824 })
1825 }
1826 InputType::SparkInvoice(spark_invoice_details) => {
1827 let amount = spark_invoice_details
1828 .amount
1829 .or(request.amount)
1830 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?;
1831 let conversion_estimate = self
1832 .estimate_conversion(
1833 request.conversion_options.as_ref(),
1834 request.token_identifier.as_ref(),
1835 amount,
1836 )
1837 .await?;
1838
1839 Ok(PrepareSendPaymentResponse {
1840 payment_method: SendPaymentMethod::SparkInvoice {
1841 spark_invoice_details: spark_invoice_details.clone(),
1842 fee: 0,
1843 token_identifier: request.token_identifier.clone(),
1844 },
1845 amount,
1846 token_identifier: request.token_identifier,
1847 conversion_estimate,
1848 })
1849 }
1850 InputType::Bolt11Invoice(detailed_bolt11_invoice) => {
1851 let spark_address: Option<SparkAddress> = self
1852 .spark_wallet
1853 .extract_spark_address(&request.payment_request)?;
1854
1855 let spark_transfer_fee_sats = if spark_address.is_some() {
1856 Some(0)
1857 } else {
1858 None
1859 };
1860
1861 let amount = request
1862 .amount
1863 .or(detailed_bolt11_invoice
1864 .amount_msat
1865 .map(|msat| u128::from(msat).saturating_div(1000)))
1866 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?;
1867 let lightning_fee_sats = self
1868 .spark_wallet
1869 .fetch_lightning_send_fee_estimate(
1870 &request.payment_request,
1871 request
1872 .amount
1873 .map(|a| Ok::<u64, SdkError>(a.try_into()?))
1874 .transpose()?,
1875 )
1876 .await?;
1877 let conversion_estimate = self
1878 .estimate_conversion(
1879 request.conversion_options.as_ref(),
1880 request.token_identifier.as_ref(),
1881 amount.saturating_add(u128::from(lightning_fee_sats)),
1882 )
1883 .await?;
1884
1885 Ok(PrepareSendPaymentResponse {
1886 payment_method: SendPaymentMethod::Bolt11Invoice {
1887 invoice_details: detailed_bolt11_invoice.clone(),
1888 spark_transfer_fee_sats,
1889 lightning_fee_sats,
1890 },
1891 amount,
1892 token_identifier: request.token_identifier,
1893 conversion_estimate,
1894 })
1895 }
1896 InputType::BitcoinAddress(withdrawal_address) => {
1897 let amount = request
1898 .amount
1899 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?;
1900 let fee_quote: SendOnchainFeeQuote = self
1901 .spark_wallet
1902 .fetch_coop_exit_fee_quote(
1903 &withdrawal_address.address,
1904 Some(amount.try_into()?),
1905 )
1906 .await?
1907 .into();
1908 let conversion_estimate = self
1909 .estimate_conversion(
1910 request.conversion_options.as_ref(),
1911 request.token_identifier.as_ref(),
1912 amount.saturating_add(u128::from(fee_quote.speed_fast.total_fee_sat())),
1913 )
1914 .await?;
1915 Ok(PrepareSendPaymentResponse {
1916 payment_method: SendPaymentMethod::BitcoinAddress {
1917 address: withdrawal_address.clone(),
1918 fee_quote,
1919 },
1920 amount,
1921 token_identifier: None,
1922 conversion_estimate,
1923 })
1924 }
1925 _ => Err(SdkError::InvalidInput(
1926 "Unsupported payment method".to_string(),
1927 )),
1928 }
1929 }
1930
1931 pub async fn send_payment(
1932 &self,
1933 request: SendPaymentRequest,
1934 ) -> Result<SendPaymentResponse, SdkError> {
1935 self.ensure_spark_private_mode_initialized().await?;
1936 Box::pin(self.maybe_convert_token_send_payment(request, false)).await
1937 }
1938
1939 pub async fn fetch_conversion_limits(
1940 &self,
1941 request: FetchConversionLimitsRequest,
1942 ) -> Result<FetchConversionLimitsResponse, SdkError> {
1943 let (asset_in_address, asset_out_address) = request
1944 .conversion_type
1945 .as_asset_addresses(request.token_identifier.as_ref())?;
1946 let min_amounts = self
1947 .flashnet_client
1948 .get_min_amounts(GetMinAmountsRequest {
1949 asset_in_address,
1950 asset_out_address,
1951 })
1952 .await?;
1953 Ok(FetchConversionLimitsResponse {
1954 min_from_amount: min_amounts.asset_in_min,
1955 min_to_amount: min_amounts.asset_out_min,
1956 })
1957 }
1958
1959 #[allow(unused_variables)]
1961 pub async fn sync_wallet(
1962 &self,
1963 request: SyncWalletRequest,
1964 ) -> Result<SyncWalletResponse, SdkError> {
1965 let (tx, rx) = oneshot::channel();
1966
1967 if let Err(e) = self.sync_trigger.send(SyncRequest::full(Some(tx))) {
1968 error!("Failed to send sync trigger: {e:?}");
1969 }
1970 let _ = rx.await.map_err(|e| {
1971 error!("Failed to receive sync trigger: {e:?}");
1972 SdkError::Generic(format!("sync trigger failed: {e:?}"))
1973 })?;
1974 Ok(SyncWalletResponse {})
1975 }
1976
1977 pub async fn list_payments(
1992 &self,
1993 request: ListPaymentsRequest,
1994 ) -> Result<ListPaymentsResponse, SdkError> {
1995 let payments = self.storage.list_payments(request).await?;
1996 Ok(ListPaymentsResponse { payments })
1997 }
1998
1999 pub async fn get_payment(
2000 &self,
2001 request: GetPaymentRequest,
2002 ) -> Result<GetPaymentResponse, SdkError> {
2003 let payment = self.storage.get_payment_by_id(request.payment_id).await?;
2004 Ok(GetPaymentResponse { payment })
2005 }
2006
2007 pub async fn claim_deposit(
2008 &self,
2009 request: ClaimDepositRequest,
2010 ) -> Result<ClaimDepositResponse, SdkError> {
2011 self.ensure_spark_private_mode_initialized().await?;
2012 let detailed_utxo =
2013 CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
2014 .fetch_detailed_utxo(&request.txid, request.vout)
2015 .await?;
2016
2017 let max_fee = request
2018 .max_fee
2019 .or(self.config.max_deposit_claim_fee.clone());
2020 match self.claim_utxo(&detailed_utxo, max_fee).await {
2021 Ok(transfer) => {
2022 self.storage
2023 .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
2024 .await?;
2025 if let Err(e) = self
2026 .sync_trigger
2027 .send(SyncRequest::no_reply(SyncType::WalletState))
2028 {
2029 error!("Failed to execute sync after deposit claim: {e:?}");
2030 }
2031 Ok(ClaimDepositResponse {
2032 payment: transfer.try_into()?,
2033 })
2034 }
2035 Err(e) => {
2036 error!("Failed to claim deposit: {e:?}");
2037 self.storage
2038 .update_deposit(
2039 detailed_utxo.txid.to_string(),
2040 detailed_utxo.vout,
2041 UpdateDepositPayload::ClaimError {
2042 error: e.clone().into(),
2043 },
2044 )
2045 .await?;
2046 Err(e)
2047 }
2048 }
2049 }
2050
2051 pub async fn refund_deposit(
2052 &self,
2053 request: RefundDepositRequest,
2054 ) -> Result<RefundDepositResponse, SdkError> {
2055 let detailed_utxo =
2056 CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
2057 .fetch_detailed_utxo(&request.txid, request.vout)
2058 .await?;
2059 let tx = self
2060 .spark_wallet
2061 .refund_static_deposit(
2062 detailed_utxo.clone().tx,
2063 Some(detailed_utxo.vout),
2064 &request.destination_address,
2065 request.fee.into(),
2066 )
2067 .await?;
2068 let deposit: DepositInfo = detailed_utxo.into();
2069 let tx_hex = serialize(&tx).as_hex().to_string();
2070 let tx_id = tx.compute_txid().as_raw_hash().to_string();
2071
2072 self.storage
2074 .update_deposit(
2075 deposit.txid.clone(),
2076 deposit.vout,
2077 UpdateDepositPayload::Refund {
2078 refund_tx: tx_hex.clone(),
2079 refund_txid: tx_id.clone(),
2080 },
2081 )
2082 .await?;
2083
2084 self.chain_service
2085 .broadcast_transaction(tx_hex.clone())
2086 .await?;
2087 Ok(RefundDepositResponse { tx_id, tx_hex })
2088 }
2089
2090 #[allow(unused_variables)]
2091 pub async fn list_unclaimed_deposits(
2092 &self,
2093 request: ListUnclaimedDepositsRequest,
2094 ) -> Result<ListUnclaimedDepositsResponse, SdkError> {
2095 let deposits = self.storage.list_deposits().await?;
2096 Ok(ListUnclaimedDepositsResponse { deposits })
2097 }
2098
2099 pub async fn check_lightning_address_available(
2100 &self,
2101 req: CheckLightningAddressRequest,
2102 ) -> Result<bool, SdkError> {
2103 let Some(client) = &self.lnurl_server_client else {
2104 return Err(SdkError::Generic(
2105 "LNURL server is not configured".to_string(),
2106 ));
2107 };
2108
2109 let username = sanitize_username(&req.username);
2110 let available = client.check_username_available(&username).await?;
2111 Ok(available)
2112 }
2113
2114 pub async fn get_lightning_address(&self) -> Result<Option<LightningAddressInfo>, SdkError> {
2115 let cache = ObjectCacheRepository::new(self.storage.clone());
2116 Ok(cache.fetch_lightning_address().await?)
2117 }
2118
2119 pub async fn register_lightning_address(
2120 &self,
2121 request: RegisterLightningAddressRequest,
2122 ) -> Result<LightningAddressInfo, SdkError> {
2123 self.ensure_spark_private_mode_initialized().await?;
2125
2126 self.register_lightning_address_internal(request).await
2127 }
2128
2129 pub async fn delete_lightning_address(&self) -> Result<(), SdkError> {
2130 let cache = ObjectCacheRepository::new(self.storage.clone());
2131 let Some(address_info) = cache.fetch_lightning_address().await? else {
2132 return Ok(());
2133 };
2134
2135 let Some(client) = &self.lnurl_server_client else {
2136 return Err(SdkError::Generic(
2137 "LNURL server is not configured".to_string(),
2138 ));
2139 };
2140
2141 let params = crate::lnurl::UnregisterLightningAddressRequest {
2142 username: address_info.username,
2143 };
2144
2145 client.unregister_lightning_address(¶ms).await?;
2146 cache.delete_lightning_address().await?;
2147 Ok(())
2148 }
2149
2150 pub async fn list_fiat_currencies(&self) -> Result<ListFiatCurrenciesResponse, SdkError> {
2153 let currencies = self
2154 .fiat_service
2155 .fetch_fiat_currencies()
2156 .await?
2157 .into_iter()
2158 .map(From::from)
2159 .collect();
2160 Ok(ListFiatCurrenciesResponse { currencies })
2161 }
2162
2163 pub async fn list_fiat_rates(&self) -> Result<ListFiatRatesResponse, SdkError> {
2165 let rates = self
2166 .fiat_service
2167 .fetch_fiat_rates()
2168 .await?
2169 .into_iter()
2170 .map(From::from)
2171 .collect();
2172 Ok(ListFiatRatesResponse { rates })
2173 }
2174
2175 pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
2177 Ok(self.chain_service.recommended_fees().await?)
2178 }
2179
2180 pub async fn get_tokens_metadata(
2187 &self,
2188 request: GetTokensMetadataRequest,
2189 ) -> Result<GetTokensMetadataResponse, SdkError> {
2190 let metadata = get_tokens_metadata_cached_or_query(
2191 &self.spark_wallet,
2192 &ObjectCacheRepository::new(self.storage.clone()),
2193 &request
2194 .token_identifiers
2195 .iter()
2196 .map(String::as_str)
2197 .collect::<Vec<_>>(),
2198 )
2199 .await?;
2200 Ok(GetTokensMetadataResponse {
2201 tokens_metadata: metadata,
2202 })
2203 }
2204
2205 pub async fn sign_message(
2209 &self,
2210 request: SignMessageRequest,
2211 ) -> Result<SignMessageResponse, SdkError> {
2212 let pubkey = self.spark_wallet.get_identity_public_key().to_string();
2213 let signature = self.spark_wallet.sign_message(&request.message).await?;
2214 let signature_hex = if request.compact {
2215 signature.serialize_compact().to_lower_hex_string()
2216 } else {
2217 signature.serialize_der().to_lower_hex_string()
2218 };
2219
2220 Ok(SignMessageResponse {
2221 pubkey,
2222 signature: signature_hex,
2223 })
2224 }
2225
2226 pub async fn check_message(
2230 &self,
2231 request: CheckMessageRequest,
2232 ) -> Result<CheckMessageResponse, SdkError> {
2233 let pubkey = PublicKey::from_str(&request.pubkey)
2234 .map_err(|_| SdkError::InvalidInput("Invalid public key".to_string()))?;
2235 let signature_bytes = hex::decode(&request.signature)
2236 .map_err(|_| SdkError::InvalidInput("Not a valid hex encoded signature".to_string()))?;
2237 let signature = Signature::from_der(&signature_bytes)
2238 .or_else(|_| Signature::from_compact(&signature_bytes))
2239 .map_err(|_| {
2240 SdkError::InvalidInput("Not a valid DER or compact encoded signature".to_string())
2241 })?;
2242
2243 let is_valid = self
2244 .spark_wallet
2245 .verify_message(&request.message, &signature, &pubkey)
2246 .await
2247 .is_ok();
2248 Ok(CheckMessageResponse { is_valid })
2249 }
2250
2251 pub async fn get_user_settings(&self) -> Result<UserSettings, SdkError> {
2255 self.ensure_spark_private_mode_initialized().await?;
2257
2258 let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
2259
2260 Ok(UserSettings {
2263 spark_private_mode_enabled: spark_user_settings.private_enabled,
2264 })
2265 }
2266
2267 pub async fn update_user_settings(
2271 &self,
2272 request: UpdateUserSettingsRequest,
2273 ) -> Result<(), SdkError> {
2274 if let Some(spark_private_mode_enabled) = request.spark_private_mode_enabled {
2275 self.spark_wallet
2276 .update_wallet_settings(spark_private_mode_enabled)
2277 .await?;
2278
2279 let lightning_address = match self.get_lightning_address().await {
2281 Ok(lightning_address) => lightning_address,
2282 Err(e) => {
2283 error!("Failed to get lightning address during user settings update: {e:?}");
2284 return Ok(());
2285 }
2286 };
2287 let Some(lightning_address) = lightning_address else {
2288 return Ok(());
2289 };
2290 if let Err(e) = self
2291 .register_lightning_address_internal(RegisterLightningAddressRequest {
2292 username: lightning_address.username,
2293 description: Some(lightning_address.description),
2294 })
2295 .await
2296 {
2297 error!("Failed to reregister lightning address during user settings update: {e:?}");
2298 }
2299 }
2300 Ok(())
2301 }
2302
2303 pub fn get_token_issuer(&self) -> TokenIssuer {
2305 TokenIssuer::new(self.spark_wallet.clone(), self.storage.clone())
2306 }
2307
2308 pub fn start_leaf_optimization(&self) {
2314 self.spark_wallet.start_leaf_optimization();
2315 }
2316
2317 pub async fn cancel_leaf_optimization(&self) -> Result<(), SdkError> {
2326 self.spark_wallet.cancel_leaf_optimization().await?;
2327 Ok(())
2328 }
2329
2330 pub fn get_leaf_optimization_progress(&self) -> OptimizationProgress {
2332 self.spark_wallet.get_leaf_optimization_progress().into()
2333 }
2334}
2335
2336impl BreezSdk {
2338 async fn maybe_convert_token_send_payment(
2339 &self,
2340 request: SendPaymentRequest,
2341 mut suppress_payment_event: bool,
2342 ) -> Result<SendPaymentResponse, SdkError> {
2343 if request.idempotency_key.is_some() && request.prepare_response.token_identifier.is_some()
2345 {
2346 return Err(SdkError::InvalidInput(
2347 "Idempotency key is not supported for token payments".to_string(),
2348 ));
2349 }
2350 if let Some(idempotency_key) = &request.idempotency_key {
2351 if let Ok(payment) = self
2353 .storage
2354 .get_payment_by_id(idempotency_key.clone())
2355 .await
2356 {
2357 return Ok(SendPaymentResponse { payment });
2358 }
2359 }
2360 let res = if let Some(ConversionEstimate {
2362 options: conversion_options,
2363 ..
2364 }) = &request.prepare_response.conversion_estimate
2365 {
2366 Box::pin(self.convert_token_send_payment_internal(
2367 conversion_options,
2368 &request,
2369 &mut suppress_payment_event,
2370 ))
2371 .await
2372 } else {
2373 Box::pin(self.send_payment_internal(&request)).await
2374 };
2375 if let Ok(response) = &res {
2377 if !suppress_payment_event {
2378 self.event_emitter
2379 .emit(&SdkEvent::from_payment(response.payment.clone()))
2380 .await;
2381 }
2382 if let Err(e) = self
2383 .sync_trigger
2384 .send(SyncRequest::no_reply(SyncType::WalletState))
2385 {
2386 error!("Failed to send sync trigger: {e:?}");
2387 }
2388 }
2389 res
2390 }
2391
2392 #[allow(clippy::too_many_lines)]
2393 async fn convert_token_send_payment_internal(
2394 &self,
2395 conversion_options: &ConversionOptions,
2396 request: &SendPaymentRequest,
2397 suppress_payment_event: &mut bool,
2398 ) -> Result<SendPaymentResponse, SdkError> {
2399 let (conversion_response, conversion_purpose) =
2401 match &request.prepare_response.payment_method {
2402 SendPaymentMethod::SparkAddress { address, .. } => {
2403 let spark_address = address
2404 .parse::<SparkAddress>()
2405 .map_err(|_| SdkError::InvalidInput("Invalid spark address".to_string()))?;
2406 let conversion_purpose = if spark_address.identity_public_key
2407 == self.spark_wallet.get_identity_public_key()
2408 {
2409 ConversionPurpose::SelfTransfer
2410 } else {
2411 ConversionPurpose::OngoingPayment {
2412 payment_request: address.clone(),
2413 }
2414 };
2415 let res = self
2416 .convert_token(
2417 conversion_options,
2418 &conversion_purpose,
2419 request.prepare_response.token_identifier.as_ref(),
2420 request.prepare_response.amount,
2421 )
2422 .await?;
2423 (res, conversion_purpose)
2424 }
2425 SendPaymentMethod::SparkInvoice {
2426 spark_invoice_details:
2427 SparkInvoiceDetails {
2428 identity_public_key,
2429 invoice,
2430 ..
2431 },
2432 ..
2433 } => {
2434 let own_identity_public_key =
2435 self.spark_wallet.get_identity_public_key().to_string();
2436 let conversion_purpose = if identity_public_key == &own_identity_public_key {
2437 ConversionPurpose::SelfTransfer
2438 } else {
2439 ConversionPurpose::OngoingPayment {
2440 payment_request: invoice.clone(),
2441 }
2442 };
2443 let res = self
2444 .convert_token(
2445 conversion_options,
2446 &conversion_purpose,
2447 request.prepare_response.token_identifier.as_ref(),
2448 request.prepare_response.amount,
2449 )
2450 .await?;
2451 (res, conversion_purpose)
2452 }
2453 SendPaymentMethod::Bolt11Invoice {
2454 spark_transfer_fee_sats,
2455 lightning_fee_sats,
2456 invoice_details,
2457 ..
2458 } => {
2459 let conversion_purpose = ConversionPurpose::OngoingPayment {
2460 payment_request: invoice_details.invoice.bolt11.clone(),
2461 };
2462 let res = self
2463 .convert_token_for_bolt11_invoice(
2464 conversion_options,
2465 *spark_transfer_fee_sats,
2466 *lightning_fee_sats,
2467 request,
2468 &conversion_purpose,
2469 )
2470 .await?;
2471 (res, conversion_purpose)
2472 }
2473 SendPaymentMethod::BitcoinAddress { address, fee_quote } => {
2474 let conversion_purpose = ConversionPurpose::OngoingPayment {
2475 payment_request: address.address.clone(),
2476 };
2477 let res = self
2478 .convert_token_for_bitcoin_address(
2479 conversion_options,
2480 fee_quote,
2481 request,
2482 &conversion_purpose,
2483 )
2484 .await?;
2485 (res, conversion_purpose)
2486 }
2487 };
2488 if matches!(
2490 conversion_options.conversion_type,
2491 ConversionType::FromBitcoin
2492 ) {
2493 let _ = self
2494 .sync_trigger
2495 .send(SyncRequest::no_reply(SyncType::WalletState));
2496 }
2497 let payment = self
2499 .wait_for_payment(
2500 WaitForPaymentIdentifier::PaymentId(
2501 conversion_response.received_payment_id.clone(),
2502 ),
2503 conversion_options
2504 .completion_timeout_secs
2505 .unwrap_or(DEFAULT_TOKEN_CONVERSION_TIMEOUT_SECS),
2506 )
2507 .await
2508 .map_err(|e| {
2509 SdkError::Generic(format!("Timeout waiting for conversion to complete: {e}"))
2510 })?;
2511 if conversion_purpose == ConversionPurpose::SelfTransfer {
2513 *suppress_payment_event = true;
2514 return Ok(SendPaymentResponse { payment });
2515 }
2516 let response = Box::pin(self.send_payment_internal(request)).await?;
2518 self.merge_payment_metadata(
2520 conversion_response.sent_payment_id,
2521 PaymentMetadata {
2522 parent_payment_id: Some(response.payment.id.clone()),
2523 ..Default::default()
2524 },
2525 )
2526 .await?;
2527 self.merge_payment_metadata(
2528 conversion_response.received_payment_id,
2529 PaymentMetadata {
2530 parent_payment_id: Some(response.payment.id.clone()),
2531 ..Default::default()
2532 },
2533 )
2534 .await?;
2535
2536 Ok(response)
2537 }
2538
2539 async fn send_payment_internal(
2540 &self,
2541 request: &SendPaymentRequest,
2542 ) -> Result<SendPaymentResponse, SdkError> {
2543 match &request.prepare_response.payment_method {
2544 SendPaymentMethod::SparkAddress {
2545 address,
2546 token_identifier,
2547 ..
2548 } => {
2549 self.send_spark_address(
2550 address,
2551 token_identifier.clone(),
2552 request.prepare_response.amount,
2553 request.options.as_ref(),
2554 request.idempotency_key.clone(),
2555 )
2556 .await
2557 }
2558 SendPaymentMethod::SparkInvoice {
2559 spark_invoice_details,
2560 ..
2561 } => {
2562 self.send_spark_invoice(&spark_invoice_details.invoice, request)
2563 .await
2564 }
2565 SendPaymentMethod::Bolt11Invoice {
2566 invoice_details,
2567 spark_transfer_fee_sats,
2568 lightning_fee_sats,
2569 ..
2570 } => {
2571 Box::pin(self.send_bolt11_invoice(
2572 invoice_details,
2573 *spark_transfer_fee_sats,
2574 *lightning_fee_sats,
2575 request,
2576 ))
2577 .await
2578 }
2579 SendPaymentMethod::BitcoinAddress { address, fee_quote } => {
2580 self.send_bitcoin_address(address, fee_quote, request).await
2581 }
2582 }
2583 }
2584
2585 async fn send_spark_address(
2586 &self,
2587 address: &str,
2588 token_identifier: Option<String>,
2589 amount: u128,
2590 options: Option<&SendPaymentOptions>,
2591 idempotency_key: Option<String>,
2592 ) -> Result<SendPaymentResponse, SdkError> {
2593 let spark_address = address
2594 .parse::<SparkAddress>()
2595 .map_err(|_| SdkError::InvalidInput("Invalid spark address".to_string()))?;
2596
2597 if let Some(SendPaymentOptions::SparkAddress { htlc_options }) = options
2599 && let Some(htlc_options) = htlc_options
2600 {
2601 if token_identifier.is_some() {
2602 return Err(SdkError::InvalidInput(
2603 "Can't provide both token identifier and HTLC options".to_string(),
2604 ));
2605 }
2606
2607 return self
2608 .send_spark_htlc(
2609 &spark_address,
2610 amount.try_into()?,
2611 htlc_options,
2612 idempotency_key,
2613 )
2614 .await;
2615 }
2616
2617 let payment = if let Some(identifier) = token_identifier {
2618 self.send_spark_token_address(identifier, amount, spark_address)
2619 .await?
2620 } else {
2621 let transfer_id = idempotency_key
2622 .as_ref()
2623 .map(|key| TransferId::from_str(key))
2624 .transpose()?;
2625 let transfer = self
2626 .spark_wallet
2627 .transfer(amount.try_into()?, &spark_address, transfer_id)
2628 .await?;
2629 transfer.try_into()?
2630 };
2631
2632 self.storage.insert_payment(payment.clone()).await?;
2634
2635 Ok(SendPaymentResponse { payment })
2636 }
2637
2638 async fn send_spark_htlc(
2639 &self,
2640 address: &SparkAddress,
2641 amount_sat: u64,
2642 htlc_options: &SparkHtlcOptions,
2643 idempotency_key: Option<String>,
2644 ) -> Result<SendPaymentResponse, SdkError> {
2645 let payment_hash = sha256::Hash::from_str(&htlc_options.payment_hash)
2646 .map_err(|_| SdkError::InvalidInput("Invalid payment hash".to_string()))?;
2647
2648 if htlc_options.expiry_duration_secs == 0 {
2649 return Err(SdkError::InvalidInput(
2650 "Expiry duration must be greater than 0".to_string(),
2651 ));
2652 }
2653 let expiry_duration = Duration::from_secs(htlc_options.expiry_duration_secs);
2654
2655 let transfer_id = idempotency_key
2656 .as_ref()
2657 .map(|key| TransferId::from_str(key))
2658 .transpose()?;
2659 let transfer = self
2660 .spark_wallet
2661 .create_htlc(
2662 amount_sat,
2663 address,
2664 &payment_hash,
2665 expiry_duration,
2666 transfer_id,
2667 )
2668 .await?;
2669
2670 let payment: Payment = transfer.try_into()?;
2671
2672 self.storage.insert_payment(payment.clone()).await?;
2674
2675 Ok(SendPaymentResponse { payment })
2676 }
2677
2678 async fn send_spark_token_address(
2679 &self,
2680 token_identifier: String,
2681 amount: u128,
2682 receiver_address: SparkAddress,
2683 ) -> Result<Payment, SdkError> {
2684 let token_transaction = self
2685 .spark_wallet
2686 .transfer_tokens(
2687 vec![TransferTokenOutput {
2688 token_id: token_identifier,
2689 amount,
2690 receiver_address: receiver_address.clone(),
2691 spark_invoice: None,
2692 }],
2693 None,
2694 None,
2695 )
2696 .await?;
2697
2698 map_and_persist_token_transaction(&self.spark_wallet, &self.storage, &token_transaction)
2699 .await
2700 }
2701
2702 async fn send_spark_invoice(
2703 &self,
2704 invoice: &str,
2705 request: &SendPaymentRequest,
2706 ) -> Result<SendPaymentResponse, SdkError> {
2707 let transfer_id = request
2708 .idempotency_key
2709 .as_ref()
2710 .map(|key| TransferId::from_str(key))
2711 .transpose()?;
2712
2713 let payment = match self
2714 .spark_wallet
2715 .fulfill_spark_invoice(invoice, Some(request.prepare_response.amount), transfer_id)
2716 .await?
2717 {
2718 spark_wallet::FulfillSparkInvoiceResult::Transfer(wallet_transfer) => {
2719 (*wallet_transfer).try_into()?
2720 }
2721 spark_wallet::FulfillSparkInvoiceResult::TokenTransaction(token_transaction) => {
2722 map_and_persist_token_transaction(
2723 &self.spark_wallet,
2724 &self.storage,
2725 &token_transaction,
2726 )
2727 .await?
2728 }
2729 };
2730
2731 self.storage.insert_payment(payment.clone()).await?;
2733
2734 Ok(SendPaymentResponse { payment })
2735 }
2736
2737 async fn send_bolt11_invoice(
2738 &self,
2739 invoice_details: &Bolt11InvoiceDetails,
2740 spark_transfer_fee_sats: Option<u64>,
2741 lightning_fee_sats: u64,
2742 request: &SendPaymentRequest,
2743 ) -> Result<SendPaymentResponse, SdkError> {
2744 let amount_to_send = match invoice_details.amount_msat {
2745 Some(_) => None,
2747 None => Some(request.prepare_response.amount),
2749 };
2750 let (prefer_spark, completion_timeout_secs) = match request.options {
2751 Some(SendPaymentOptions::Bolt11Invoice {
2752 prefer_spark,
2753 completion_timeout_secs,
2754 }) => (prefer_spark, completion_timeout_secs),
2755 _ => (self.config.prefer_spark_over_lightning, None),
2756 };
2757 let fee_sats = match (prefer_spark, spark_transfer_fee_sats, lightning_fee_sats) {
2758 (true, Some(fee), _) => fee,
2759 _ => lightning_fee_sats,
2760 };
2761 let transfer_id = request
2762 .idempotency_key
2763 .as_ref()
2764 .map(|idempotency_key| TransferId::from_str(idempotency_key))
2765 .transpose()?;
2766
2767 let payment_response = self
2768 .spark_wallet
2769 .pay_lightning_invoice(
2770 &invoice_details.invoice.bolt11,
2771 amount_to_send
2772 .map(|a| Ok::<u64, SdkError>(a.try_into()?))
2773 .transpose()?,
2774 Some(fee_sats),
2775 prefer_spark,
2776 transfer_id,
2777 )
2778 .await?;
2779 let payment = match payment_response.lightning_payment {
2780 Some(lightning_payment) => {
2781 let ssp_id = lightning_payment.id.clone();
2782 let payment = Payment::from_lightning(
2783 lightning_payment,
2784 request.prepare_response.amount,
2785 payment_response.transfer.id.to_string(),
2786 )?;
2787 self.poll_lightning_send_payment(&payment, ssp_id);
2788 payment
2789 }
2790 None => payment_response.transfer.try_into()?,
2791 };
2792
2793 let Some(completion_timeout_secs) = completion_timeout_secs else {
2794 return Ok(SendPaymentResponse { payment });
2795 };
2796
2797 if completion_timeout_secs == 0 {
2798 return Ok(SendPaymentResponse { payment });
2799 }
2800
2801 let payment = self
2802 .wait_for_payment(
2803 WaitForPaymentIdentifier::PaymentId(payment.id.clone()),
2804 completion_timeout_secs,
2805 )
2806 .await
2807 .unwrap_or(payment);
2808
2809 self.storage.insert_payment(payment.clone()).await?;
2811
2812 Ok(SendPaymentResponse { payment })
2813 }
2814
2815 async fn send_bitcoin_address(
2816 &self,
2817 address: &BitcoinAddressDetails,
2818 fee_quote: &SendOnchainFeeQuote,
2819 request: &SendPaymentRequest,
2820 ) -> Result<SendPaymentResponse, SdkError> {
2821 let exit_speed = match &request.options {
2822 Some(SendPaymentOptions::BitcoinAddress { confirmation_speed }) => {
2823 confirmation_speed.clone().into()
2824 }
2825 None => ExitSpeed::Fast,
2826 _ => {
2827 return Err(SdkError::InvalidInput("Invalid options".to_string()));
2828 }
2829 };
2830 let transfer_id = request
2831 .idempotency_key
2832 .as_ref()
2833 .map(|idempotency_key| TransferId::from_str(idempotency_key))
2834 .transpose()?;
2835 let response = self
2836 .spark_wallet
2837 .withdraw(
2838 &address.address,
2839 Some(request.prepare_response.amount.try_into()?),
2840 exit_speed,
2841 fee_quote.clone().into(),
2842 transfer_id,
2843 )
2844 .await?;
2845
2846 let payment: Payment = response.try_into()?;
2847
2848 self.storage.insert_payment(payment.clone()).await?;
2849
2850 Ok(SendPaymentResponse { payment })
2851 }
2852
2853 async fn wait_for_payment(
2854 &self,
2855 identifier: WaitForPaymentIdentifier,
2856 completion_timeout_secs: u32,
2857 ) -> Result<Payment, SdkError> {
2858 let (tx, mut rx) = mpsc::channel(20);
2859 let id = self
2860 .add_event_listener(Box::new(InternalEventListener::new(tx)))
2861 .await;
2862
2863 let payment = match &identifier {
2865 WaitForPaymentIdentifier::PaymentId(payment_id) => self
2866 .storage
2867 .get_payment_by_id(payment_id.clone())
2868 .await
2869 .ok(),
2870 WaitForPaymentIdentifier::PaymentRequest(payment_request) => {
2871 self.storage
2872 .get_payment_by_invoice(payment_request.clone())
2873 .await?
2874 }
2875 };
2876 if let Some(payment) = payment
2877 && payment.status == PaymentStatus::Completed
2878 {
2879 self.remove_event_listener(&id).await;
2880 return Ok(payment);
2881 }
2882
2883 let timeout_res = timeout(Duration::from_secs(completion_timeout_secs.into()), async {
2884 loop {
2885 let Some(event) = rx.recv().await else {
2886 return Err(SdkError::Generic("Event channel closed".to_string()));
2887 };
2888
2889 let SdkEvent::PaymentSucceeded { payment } = event else {
2890 continue;
2891 };
2892
2893 if is_payment_match(&payment, &identifier) {
2894 return Ok(payment);
2895 }
2896 }
2897 })
2898 .await
2899 .map_err(|_| SdkError::Generic("Timeout waiting for payment".to_string()));
2900
2901 self.remove_event_listener(&id).await;
2902 timeout_res?
2903 }
2904
2905 async fn merge_payment_metadata(
2906 &self,
2907 payment_id: String,
2908 mut metadata: PaymentMetadata,
2909 ) -> Result<(), SdkError> {
2910 if let Some(details) = self
2911 .storage
2912 .get_payment_by_id(payment_id.clone())
2913 .await
2914 .ok()
2915 .and_then(|p| p.details)
2916 {
2917 match details {
2918 PaymentDetails::Lightning {
2919 lnurl_pay_info,
2920 lnurl_withdraw_info,
2921 ..
2922 } => {
2923 metadata.lnurl_pay_info = metadata.lnurl_pay_info.or(lnurl_pay_info);
2924 metadata.lnurl_withdraw_info =
2925 metadata.lnurl_withdraw_info.or(lnurl_withdraw_info);
2926 }
2927 PaymentDetails::Spark {
2928 conversion_info, ..
2929 }
2930 | PaymentDetails::Token {
2931 conversion_info, ..
2932 } => {
2933 metadata.conversion_info = metadata.conversion_info.or(conversion_info);
2934 }
2935 _ => {}
2936 }
2937 }
2938 self.storage
2939 .set_payment_metadata(payment_id, metadata)
2940 .await?;
2941 Ok(())
2942 }
2943
2944 fn poll_lightning_send_payment(&self, payment: &Payment, ssp_id: String) {
2946 const MAX_POLL_ATTEMPTS: u32 = 20;
2947 let payment_id = payment.id.clone();
2948 info!("Polling lightning send payment {}", payment_id);
2949
2950 let spark_wallet = self.spark_wallet.clone();
2951 let sync_trigger = self.sync_trigger.clone();
2952 let event_emitter = self.event_emitter.clone();
2953 let payment = payment.clone();
2954 let payment_id = payment_id.clone();
2955 let mut shutdown = self.shutdown_sender.subscribe();
2956
2957 tokio::spawn(async move {
2958 for i in 0..MAX_POLL_ATTEMPTS {
2959 info!(
2960 "Polling lightning send payment {} attempt {}",
2961 payment_id, i
2962 );
2963 select! {
2964 _ = shutdown.changed() => {
2965 info!("Shutdown signal received");
2966 return;
2967 },
2968 p = spark_wallet.fetch_lightning_send_payment(&ssp_id) => {
2969 if let Ok(Some(p)) = p && let Ok(payment) = Payment::from_lightning(p.clone(), payment.amount, payment.id.clone()) {
2970 info!("Polling payment status = {} {:?}", payment.status, p.status);
2971 if payment.status != PaymentStatus::Pending {
2972 info!("Polling payment completed status = {}", payment.status);
2973 event_emitter.emit(&SdkEvent::from_payment(payment.clone())).await;
2974 if let Err(e) = sync_trigger.send(SyncRequest::no_reply(SyncType::WalletState)) {
2975 error!("Failed to send sync trigger: {e:?}");
2976 }
2977 return;
2978 }
2979 }
2980
2981 let sleep_time = if i < 5 {
2982 Duration::from_secs(1)
2983 } else {
2984 Duration::from_secs(i.into())
2985 };
2986 tokio::time::sleep(sleep_time).await;
2987 }
2988 }
2989 }
2990 });
2991 }
2992
2993 async fn recover_lightning_address(&self) -> Result<Option<LightningAddressInfo>, SdkError> {
2995 let cache = ObjectCacheRepository::new(self.storage.clone());
2996
2997 let Some(client) = &self.lnurl_server_client else {
2998 return Err(SdkError::Generic(
2999 "LNURL server is not configured".to_string(),
3000 ));
3001 };
3002 let resp = client.recover_lightning_address().await?;
3003
3004 let result = if let Some(resp) = resp {
3005 let address_info = resp.into();
3006 cache.save_lightning_address(&address_info).await?;
3007 Some(address_info)
3008 } else {
3009 cache.delete_lightning_address().await?;
3010 None
3011 };
3012
3013 Ok(result)
3014 }
3015
3016 async fn register_lightning_address_internal(
3017 &self,
3018 request: RegisterLightningAddressRequest,
3019 ) -> Result<LightningAddressInfo, SdkError> {
3020 let cache = ObjectCacheRepository::new(self.storage.clone());
3021 let Some(client) = &self.lnurl_server_client else {
3022 return Err(SdkError::Generic(
3023 "LNURL server is not configured".to_string(),
3024 ));
3025 };
3026
3027 let username = sanitize_username(&request.username);
3028
3029 let description = match request.description {
3030 Some(description) => description,
3031 None => format!("Pay to {}@{}", username, client.domain()),
3032 };
3033
3034 let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
3036 let nostr_pubkey = if spark_user_settings.private_enabled {
3037 Some(self.nostr_client.nostr_pubkey())
3038 } else {
3039 None
3040 };
3041
3042 let params = crate::lnurl::RegisterLightningAddressRequest {
3043 username: username.clone(),
3044 description: description.clone(),
3045 nostr_pubkey,
3046 };
3047
3048 let response = client.register_lightning_address(¶ms).await?;
3049 let address_info = LightningAddressInfo {
3050 lightning_address: response.lightning_address,
3051 description,
3052 lnurl: response.lnurl,
3053 username,
3054 };
3055 cache.save_lightning_address(&address_info).await?;
3056 Ok(address_info)
3057 }
3058
3059 async fn convert_token_for_bolt11_invoice(
3060 &self,
3061 conversion_options: &ConversionOptions,
3062 spark_transfer_fee_sats: Option<u64>,
3063 lightning_fee_sats: u64,
3064 request: &SendPaymentRequest,
3065 conversion_purpose: &ConversionPurpose,
3066 ) -> Result<TokenConversionResponse, SdkError> {
3067 let fee_sats = match request.options {
3069 Some(SendPaymentOptions::Bolt11Invoice { prefer_spark, .. }) => {
3070 match (prefer_spark, spark_transfer_fee_sats) {
3071 (true, Some(fee)) => fee,
3072 _ => lightning_fee_sats,
3073 }
3074 }
3075 _ => lightning_fee_sats,
3076 };
3077 let min_amount_out = request
3079 .prepare_response
3080 .amount
3081 .saturating_add(u128::from(fee_sats));
3082
3083 self.convert_token(
3084 conversion_options,
3085 conversion_purpose,
3086 request.prepare_response.token_identifier.as_ref(),
3087 min_amount_out,
3088 )
3089 .await
3090 }
3091
3092 async fn convert_token_for_bitcoin_address(
3093 &self,
3094 conversion_options: &ConversionOptions,
3095 fee_quote: &SendOnchainFeeQuote,
3096 request: &SendPaymentRequest,
3097 conversion_purpose: &ConversionPurpose,
3098 ) -> Result<TokenConversionResponse, SdkError> {
3099 let fee_sats = if let Some(SendPaymentOptions::BitcoinAddress { confirmation_speed }) =
3101 &request.options
3102 {
3103 match confirmation_speed {
3104 OnchainConfirmationSpeed::Slow => fee_quote.speed_slow.total_fee_sat(),
3105 OnchainConfirmationSpeed::Medium => fee_quote.speed_medium.total_fee_sat(),
3106 OnchainConfirmationSpeed::Fast => fee_quote.speed_fast.total_fee_sat(),
3107 }
3108 } else {
3109 fee_quote.speed_fast.total_fee_sat()
3110 };
3111 let min_amount_out = request
3113 .prepare_response
3114 .amount
3115 .saturating_add(u128::from(fee_sats));
3116
3117 self.convert_token(
3118 conversion_options,
3119 conversion_purpose,
3120 request.prepare_response.token_identifier.as_ref(),
3121 min_amount_out,
3122 )
3123 .await
3124 }
3125
3126 #[allow(clippy::too_many_lines)]
3127 async fn convert_token(
3128 &self,
3129 conversion_options: &ConversionOptions,
3130 conversion_purpose: &ConversionPurpose,
3131 token_identifier: Option<&String>,
3132 min_amount_out: u128,
3133 ) -> Result<TokenConversionResponse, SdkError> {
3134 let conversion_pool = self
3135 .get_conversion_pool(conversion_options, token_identifier, min_amount_out)
3136 .await?;
3137 let conversion_estimate = self
3138 .estimate_conversion_internal(&conversion_pool, conversion_options, min_amount_out)
3139 .await?
3140 .ok_or(SdkError::Generic(
3141 "No conversion estimate available".to_string(),
3142 ))?;
3143 let pool_id = conversion_pool.pool.lp_public_key;
3145 let response_res = self
3146 .flashnet_client
3147 .execute_swap(ExecuteSwapRequest {
3148 asset_in_address: conversion_pool.asset_in_address.clone(),
3149 asset_out_address: conversion_pool.asset_out_address.clone(),
3150 pool_id,
3151 amount_in: conversion_estimate.amount,
3152 max_slippage_bps: conversion_options
3153 .max_slippage_bps
3154 .unwrap_or(DEFAULT_TOKEN_CONVERSION_MAX_SLIPPAGE_BPS),
3155 min_amount_out,
3156 integrator_fee_rate_bps: None,
3157 integrator_public_key: None,
3158 })
3159 .await;
3160 match response_res {
3161 Ok(response) => {
3162 info!(
3163 "Conversion executed: accepted {}, error {:?}",
3164 response.accepted, response.error
3165 );
3166 let (sent_payment_id, received_payment_id) = self
3167 .update_payment_conversion_info(
3168 &pool_id,
3169 response.transfer_id,
3170 response.outbound_transfer_id,
3171 response.refund_transfer_id,
3172 response.fee_amount,
3173 conversion_purpose,
3174 )
3175 .await?;
3176 if let Some(received_payment_id) = received_payment_id
3177 && response.accepted
3178 {
3179 Ok(TokenConversionResponse {
3180 sent_payment_id,
3181 received_payment_id,
3182 })
3183 } else {
3184 let error_message = response
3185 .error
3186 .unwrap_or("Conversion not accepted".to_string());
3187 Err(SdkError::Generic(format!(
3188 "Convert token failed, refund in progress: {error_message}",
3189 )))
3190 }
3191 }
3192 Err(e) => {
3193 error!("Convert token failed: {e:?}");
3194 if let FlashnetError::Execution {
3195 transaction_identifier: Some(transaction_identifier),
3196 source,
3197 } = &e
3198 {
3199 let _ = self
3200 .update_payment_conversion_info(
3201 &pool_id,
3202 transaction_identifier.clone(),
3203 None,
3204 None,
3205 None,
3206 conversion_purpose,
3207 )
3208 .await;
3209 let _ = self.conversion_refund_trigger.send(());
3210 Err(SdkError::Generic(format!(
3211 "Convert token failed, refund pending: {}",
3212 *source.clone()
3213 )))
3214 } else {
3215 Err(e.into())
3216 }
3217 }
3218 }
3219 }
3220
3221 async fn get_conversion_pool(
3222 &self,
3223 conversion_options: &ConversionOptions,
3224 token_identifier: Option<&String>,
3225 amount_out: u128,
3226 ) -> Result<TokenConversionPool, SdkError> {
3227 let conversion_type = &conversion_options.conversion_type;
3228 let (asset_in_address, asset_out_address) =
3229 conversion_type.as_asset_addresses(token_identifier)?;
3230
3231 let a_in_pools_fut = self.flashnet_client.list_pools(ListPoolsRequest {
3233 asset_a_address: Some(asset_in_address.clone()),
3234 asset_b_address: Some(asset_out_address.clone()),
3235 sort: Some(PoolSortOrder::Volume24hDesc),
3236 ..Default::default()
3237 });
3238 let b_in_pools_fut = self.flashnet_client.list_pools(ListPoolsRequest {
3239 asset_a_address: Some(asset_out_address.clone()),
3240 asset_b_address: Some(asset_in_address.clone()),
3241 sort: Some(PoolSortOrder::Volume24hDesc),
3242 ..Default::default()
3243 });
3244 let (a_in_pools_res, b_in_pools_res) = tokio::join!(a_in_pools_fut, b_in_pools_fut);
3245 let mut pools = a_in_pools_res.map_or(HashMap::new(), |res| {
3246 res.pools
3247 .into_iter()
3248 .map(|pool| (pool.lp_public_key, pool))
3249 .collect::<HashMap<_, _>>()
3250 });
3251 if let Ok(res) = b_in_pools_res {
3252 pools.extend(res.pools.into_iter().map(|pool| (pool.lp_public_key, pool)));
3253 }
3254 let pools = pools.into_values().collect::<Vec<_>>();
3255 if pools.is_empty() {
3256 warn!(
3257 "No conversion pools available: in address {asset_in_address}, out address {asset_out_address}",
3258 );
3259 return Err(SdkError::Generic(
3260 "No conversion pools available".to_string(),
3261 ));
3262 }
3263
3264 let max_slippage_bps = conversion_options
3266 .max_slippage_bps
3267 .unwrap_or(DEFAULT_TOKEN_CONVERSION_MAX_SLIPPAGE_BPS);
3268
3269 let pool = flashnet::select_best_pool(
3271 &pools,
3272 &asset_in_address,
3273 amount_out,
3274 max_slippage_bps,
3275 self.config.network.into(),
3276 )?;
3277
3278 Ok(TokenConversionPool {
3279 asset_in_address,
3280 asset_out_address,
3281 pool,
3282 })
3283 }
3284
3285 async fn estimate_conversion(
3286 &self,
3287 conversion_options: Option<&ConversionOptions>,
3288 token_identifier: Option<&String>,
3289 amount_out: u128,
3290 ) -> Result<Option<ConversionEstimate>, SdkError> {
3291 let Some(conversion_options) = conversion_options else {
3292 return Ok(None);
3293 };
3294 let conversion_pool = self
3295 .get_conversion_pool(conversion_options, token_identifier, amount_out)
3296 .await?;
3297
3298 self.estimate_conversion_internal(&conversion_pool, conversion_options, amount_out)
3299 .await
3300 }
3301
3302 async fn estimate_conversion_internal(
3303 &self,
3304 conversion_pool: &TokenConversionPool,
3305 conversion_options: &ConversionOptions,
3306 amount_out: u128,
3307 ) -> Result<Option<ConversionEstimate>, SdkError> {
3308 let TokenConversionPool {
3309 asset_in_address,
3310 asset_out_address,
3311 pool,
3312 } = conversion_pool;
3313 let amount_in = pool.calculate_amount_in(
3315 asset_in_address,
3316 amount_out,
3317 conversion_options
3318 .max_slippage_bps
3319 .unwrap_or(DEFAULT_TOKEN_CONVERSION_MAX_SLIPPAGE_BPS),
3320 self.config.network.into(),
3321 )?;
3322 let response = self
3324 .flashnet_client
3325 .simulate_swap(SimulateSwapRequest {
3326 asset_in_address: asset_in_address.clone(),
3327 asset_out_address: asset_out_address.clone(),
3328 pool_id: pool.lp_public_key,
3329 amount_in,
3330 integrator_bps: None,
3331 })
3332 .await?;
3333 if response.amount_out < amount_out {
3334 return Err(SdkError::Generic(format!(
3335 "Validation returned {} but expected at least {amount_out}",
3336 response.amount_out
3337 )));
3338 }
3339 Ok(response.fee_paid_asset_in.map(|fee| ConversionEstimate {
3340 options: conversion_options.clone(),
3341 amount: amount_in,
3342 fee,
3343 }))
3344 }
3345
3346 async fn fetch_payment_by_conversion_identifier(
3349 &self,
3350 identifier: &str,
3351 tx_inputs_are_ours: bool,
3352 ) -> Result<Payment, SdkError> {
3353 debug!("Fetching conversion payment for identifier: {}", identifier);
3354 let payment = if let Ok(transfer_id) = TransferId::from_str(identifier) {
3355 let transfers = self
3356 .spark_wallet
3357 .list_transfers(ListTransfersRequest {
3358 transfer_ids: vec![transfer_id],
3359 ..Default::default()
3360 })
3361 .await?;
3362 let transfer = transfers
3363 .items
3364 .first()
3365 .cloned()
3366 .ok_or_else(|| SdkError::Generic("Transfer not found".to_string()))?;
3367 transfer.try_into()
3368 } else {
3369 let token_transactions = self
3370 .spark_wallet
3371 .list_token_transactions(ListTokenTransactionsRequest {
3372 token_transaction_hashes: vec![identifier.to_string()],
3373 ..Default::default()
3374 })
3375 .await?;
3376 let token_transaction = token_transactions
3377 .items
3378 .first()
3379 .ok_or_else(|| SdkError::Generic("Token transaction not found".to_string()))?;
3380 let object_repository = ObjectCacheRepository::new(self.storage.clone());
3381 let payments = token_transaction_to_payments(
3382 &self.spark_wallet,
3383 &object_repository,
3384 token_transaction,
3385 tx_inputs_are_ours,
3386 )
3387 .await?;
3388 payments.first().cloned().ok_or_else(|| {
3389 SdkError::Generic("Payment not found for token transaction".to_string())
3390 })
3391 };
3392 payment
3393 .inspect(|p| debug!("Found payment: {p:?}"))
3394 .inspect_err(|e| debug!("No payment found: {e}"))
3395 }
3396
3397 async fn update_payment_conversion_info(
3410 &self,
3411 pool_id: &PublicKey,
3412 outbound_identifier: String,
3413 inbound_identifier: Option<String>,
3414 refund_identifier: Option<String>,
3415 fee: Option<u128>,
3416 purpose: &ConversionPurpose,
3417 ) -> Result<(String, Option<String>), SdkError> {
3418 debug!(
3419 "Updating payment conversion info for pool_id: {pool_id}, outbound_identifier: {outbound_identifier}, inbound_identifier: {inbound_identifier:?}, refund_identifier: {refund_identifier:?}"
3420 );
3421 let cache = ObjectCacheRepository::new(self.storage.clone());
3422 let status = match (&inbound_identifier, &refund_identifier) {
3423 (Some(_), _) => ConversionStatus::Completed,
3424 (None, Some(_)) => ConversionStatus::Refunded,
3425 _ => ConversionStatus::RefundNeeded,
3426 };
3427 let pool_id_str = pool_id.to_string();
3428 let conversion_id = uuid::Uuid::now_v7().to_string();
3429
3430 let sent_payment = self
3432 .fetch_payment_by_conversion_identifier(&outbound_identifier, true)
3433 .await?;
3434 let sent_payment_id = sent_payment.id.clone();
3435 self.storage
3436 .set_payment_metadata(
3437 sent_payment_id.clone(),
3438 PaymentMetadata {
3439 conversion_info: Some(ConversionInfo {
3440 pool_id: pool_id_str.clone(),
3441 conversion_id: conversion_id.clone(),
3442 status: status.clone(),
3443 fee,
3444 purpose: None,
3445 }),
3446 ..Default::default()
3447 },
3448 )
3449 .await?;
3450
3451 let received_payment_id = if let Some(identifier) = &inbound_identifier {
3453 let metadata = PaymentMetadata {
3454 conversion_info: Some(ConversionInfo {
3455 pool_id: pool_id_str.clone(),
3456 conversion_id: conversion_id.clone(),
3457 status: status.clone(),
3458 fee: None,
3459 purpose: Some(purpose.clone()),
3460 }),
3461 ..Default::default()
3462 };
3463 if let Ok(payment) = self
3464 .fetch_payment_by_conversion_identifier(identifier, false)
3465 .await
3466 {
3467 self.storage
3468 .set_payment_metadata(payment.id.clone(), metadata)
3469 .await?;
3470 Some(payment.id)
3471 } else {
3472 cache.save_payment_metadata(identifier, &metadata).await?;
3473 Some(identifier.clone())
3474 }
3475 } else {
3476 None
3477 };
3478
3479 if let Some(identifier) = &refund_identifier {
3481 let metadata = PaymentMetadata {
3482 conversion_info: Some(ConversionInfo {
3483 pool_id: pool_id_str,
3484 conversion_id,
3485 status,
3486 fee: None,
3487 purpose: None,
3488 }),
3489 ..Default::default()
3490 };
3491 if let Ok(payment) = self
3492 .fetch_payment_by_conversion_identifier(identifier, false)
3493 .await
3494 {
3495 self.storage
3496 .set_payment_metadata(payment.id.clone(), metadata)
3497 .await?;
3498 } else {
3499 cache.save_payment_metadata(identifier, &metadata).await?;
3500 }
3501 }
3502
3503 self.storage.insert_payment(sent_payment).await?;
3504
3505 Ok((sent_payment_id, received_payment_id))
3506 }
3507}
3508
3509fn is_payment_match(payment: &Payment, identifier: &WaitForPaymentIdentifier) -> bool {
3510 match identifier {
3511 WaitForPaymentIdentifier::PaymentId(payment_id) => payment.id == *payment_id,
3512 WaitForPaymentIdentifier::PaymentRequest(payment_request) => {
3513 if let Some(details) = &payment.details {
3514 match details {
3515 PaymentDetails::Lightning { invoice, .. } => {
3516 invoice.to_lowercase() == payment_request.to_lowercase()
3517 }
3518 PaymentDetails::Spark {
3519 invoice_details: invoice,
3520 ..
3521 }
3522 | PaymentDetails::Token {
3523 invoice_details: invoice,
3524 ..
3525 } => {
3526 if let Some(invoice) = invoice {
3527 invoice.invoice.to_lowercase() == payment_request.to_lowercase()
3528 } else {
3529 false
3530 }
3531 }
3532 PaymentDetails::Withdraw { tx_id: _ }
3533 | PaymentDetails::Deposit { tx_id: _ } => false,
3534 }
3535 } else {
3536 false
3537 }
3538 }
3539 }
3540}
3541
3542struct BalanceWatcher {
3543 spark_wallet: Arc<SparkWallet>,
3544 storage: Arc<dyn Storage>,
3545}
3546
3547impl BalanceWatcher {
3548 fn new(spark_wallet: Arc<SparkWallet>, storage: Arc<dyn Storage>) -> Self {
3549 Self {
3550 spark_wallet,
3551 storage,
3552 }
3553 }
3554}
3555
3556#[macros::async_trait]
3557impl EventListener for BalanceWatcher {
3558 async fn on_event(&self, event: SdkEvent) {
3559 match event {
3560 SdkEvent::PaymentSucceeded { .. } | SdkEvent::ClaimedDeposits { .. } => {
3561 match update_balances(self.spark_wallet.clone(), self.storage.clone()).await {
3562 Ok(()) => info!("Balance updated successfully"),
3563 Err(e) => error!("Failed to update balance: {e:?}"),
3564 }
3565 }
3566 _ => {}
3567 }
3568 }
3569}
3570
3571async fn update_balances(
3572 spark_wallet: Arc<SparkWallet>,
3573 storage: Arc<dyn Storage>,
3574) -> Result<(), SdkError> {
3575 let balance_sats = spark_wallet.get_balance().await?;
3576 let token_balances = spark_wallet
3577 .get_token_balances()
3578 .await?
3579 .into_iter()
3580 .map(|(k, v)| (k, v.into()))
3581 .collect();
3582 let object_repository = ObjectCacheRepository::new(storage.clone());
3583
3584 object_repository
3585 .save_account_info(&CachedAccountInfo {
3586 balance_sats,
3587 token_balances,
3588 })
3589 .await?;
3590 let identity_public_key = spark_wallet.get_identity_public_key();
3591 info!(
3592 "Balance updated successfully {} for identity {}",
3593 balance_sats, identity_public_key
3594 );
3595 Ok(())
3596}
3597
3598struct InternalEventListener {
3599 tx: mpsc::Sender<SdkEvent>,
3600}
3601
3602impl InternalEventListener {
3603 #[allow(unused)]
3604 pub fn new(tx: mpsc::Sender<SdkEvent>) -> Self {
3605 Self { tx }
3606 }
3607}
3608
3609#[macros::async_trait]
3610impl EventListener for InternalEventListener {
3611 async fn on_event(&self, event: SdkEvent) {
3612 let _ = self.tx.send(event).await;
3613 }
3614}
3615
3616fn process_success_action(
3617 payment: &Payment,
3618 success_action: Option<&SuccessAction>,
3619) -> Result<Option<SuccessActionProcessed>, LnurlError> {
3620 let Some(success_action) = success_action else {
3621 return Ok(None);
3622 };
3623
3624 let data = match success_action {
3625 SuccessAction::Aes { data } => data,
3626 SuccessAction::Message { data } => {
3627 return Ok(Some(SuccessActionProcessed::Message { data: data.clone() }));
3628 }
3629 SuccessAction::Url { data } => {
3630 return Ok(Some(SuccessActionProcessed::Url { data: data.clone() }));
3631 }
3632 };
3633
3634 let Some(PaymentDetails::Lightning { preimage, .. }) = &payment.details else {
3635 return Err(LnurlError::general(format!(
3636 "Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.",
3637 payment.details
3638 )));
3639 };
3640
3641 let Some(preimage) = preimage else {
3642 return Ok(None);
3643 };
3644
3645 let preimage =
3646 sha256::Hash::from_str(preimage).map_err(|_| LnurlError::general("Invalid preimage"))?;
3647 let preimage = preimage.as_byte_array();
3648 let result: AesSuccessActionDataResult = match (data, preimage).try_into() {
3649 Ok(data) => AesSuccessActionDataResult::Decrypted { data },
3650 Err(e) => AesSuccessActionDataResult::ErrorStatus {
3651 reason: e.to_string(),
3652 },
3653 };
3654
3655 Ok(Some(SuccessActionProcessed::Aes { result }))
3656}
3657
3658fn validate_breez_api_key(api_key: &str) -> Result<(), SdkError> {
3659 let api_key_decoded = base64::engine::general_purpose::STANDARD
3660 .decode(api_key.as_bytes())
3661 .map_err(|err| {
3662 SdkError::Generic(format!(
3663 "Could not base64 decode the Breez API key: {err:?}"
3664 ))
3665 })?;
3666 let (_rem, cert) = parse_x509_certificate(&api_key_decoded).map_err(|err| {
3667 SdkError::Generic(format!("Invalid certificate for Breez API key: {err:?}"))
3668 })?;
3669
3670 let issuer = cert
3671 .issuer()
3672 .iter_common_name()
3673 .next()
3674 .and_then(|cn| cn.as_str().ok());
3675 match issuer {
3676 Some(common_name) => {
3677 if !common_name.starts_with("Breez") {
3678 return Err(SdkError::Generic(
3679 "Invalid certificate found for Breez API key: issuer mismatch. Please confirm that the certificate's origin is trusted"
3680 .to_string()
3681 ));
3682 }
3683 }
3684 _ => {
3685 return Err(SdkError::Generic(
3686 "Could not parse Breez API key certificate: issuer is invalid or not found."
3687 .to_string(),
3688 ));
3689 }
3690 }
3691
3692 Ok(())
3693}