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