1use base64::Engine;
2use bitcoin::{
3 consensus::serialize,
4 hashes::{Hash, sha256},
5 hex::DisplayHex,
6 secp256k1::{PublicKey, ecdsa::Signature},
7};
8use breez_sdk_common::{
9 fiat::FiatService,
10 lnurl::{self, withdraw::execute_lnurl_withdraw},
11};
12use breez_sdk_common::{
13 lnurl::{
14 error::LnurlError,
15 pay::{
16 AesSuccessActionDataResult, SuccessAction, SuccessActionProcessed, validate_lnurl_pay,
17 },
18 },
19 rest::RestClient,
20};
21use lnurl_models::sanitize_username;
22use spark_wallet::{
23 ExitSpeed, InvoiceDescription, SparkAddress, SparkWallet, TransferTokenOutput, WalletEvent,
24 WalletTransfer,
25};
26use std::{str::FromStr, sync::Arc};
27use tracing::{error, info, trace, warn};
28use web_time::{Duration, SystemTime};
29
30use tokio::{
31 select,
32 sync::{Mutex, OnceCell, mpsc, oneshot, watch},
33 time::timeout,
34};
35use tokio_with_wasm::alias as tokio;
36use web_time::Instant;
37use x509_parser::parse_x509_certificate;
38
39use crate::{
40 BitcoinAddressDetails, BitcoinChainService, Bolt11InvoiceDetails, CheckLightningAddressRequest,
41 CheckMessageRequest, CheckMessageResponse, ClaimDepositRequest, ClaimDepositResponse,
42 DepositInfo, ExternalInputParser, Fee, GetPaymentRequest, GetPaymentResponse,
43 GetTokensMetadataRequest, GetTokensMetadataResponse, InputType, LightningAddressInfo,
44 ListFiatCurrenciesResponse, ListFiatRatesResponse, ListUnclaimedDepositsRequest,
45 ListUnclaimedDepositsResponse, LnurlPayInfo, LnurlPayRequest, LnurlPayResponse,
46 LnurlWithdrawRequest, LnurlWithdrawResponse, Logger, Network, PaymentDetails, PaymentStatus,
47 PrepareLnurlPayRequest, PrepareLnurlPayResponse, RefundDepositRequest, RefundDepositResponse,
48 RegisterLightningAddressRequest, SendOnchainFeeQuote, SendPaymentOptions, SignMessageRequest,
49 SignMessageResponse, UpdateUserSettingsRequest, UserSettings, WaitForPaymentIdentifier,
50 WaitForPaymentRequest, WaitForPaymentResponse,
51 error::SdkError,
52 events::{EventEmitter, EventListener, SdkEvent},
53 issuer::TokenIssuer,
54 lnurl::LnurlServerClient,
55 logger,
56 models::{
57 Config, GetInfoRequest, GetInfoResponse, ListPaymentsRequest, ListPaymentsResponse,
58 Payment, PrepareSendPaymentRequest, PrepareSendPaymentResponse, ReceivePaymentMethod,
59 ReceivePaymentRequest, ReceivePaymentResponse, SendPaymentMethod, SendPaymentRequest,
60 SendPaymentResponse, SyncWalletRequest, SyncWalletResponse,
61 },
62 persist::{
63 CachedAccountInfo, ObjectCacheRepository, PaymentMetadata, PaymentRequestMetadata,
64 StaticDepositAddress, Storage, UpdateDepositPayload,
65 },
66 sync::SparkSyncService,
67 utils::{
68 deposit_chain_syncer::DepositChainSyncer,
69 run_with_shutdown,
70 send_payment_validation::validate_prepare_send_payment_request,
71 token::{get_tokens_metadata_cached_or_query, map_and_persist_token_transaction},
72 utxo_fetcher::{CachedUtxoFetcher, DetailedUtxo},
73 },
74};
75
76pub async fn parse_input(
77 input: &str,
78 external_input_parsers: Option<Vec<ExternalInputParser>>,
79) -> Result<InputType, SdkError> {
80 Ok(breez_sdk_common::input::parse(
81 input,
82 external_input_parsers.map(|parsers| parsers.into_iter().map(From::from).collect()),
83 )
84 .await?
85 .into())
86}
87
88#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
89const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
90
91#[cfg(all(target_family = "wasm", target_os = "unknown"))]
92const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology:442";
93
94#[derive(Clone, Debug)]
95enum SyncType {
96 Full,
97 PaymentsOnly,
98}
99
100#[derive(Clone, Debug)]
101struct SyncRequest {
102 sync_type: SyncType,
103 #[allow(clippy::type_complexity)]
104 reply: Arc<Mutex<Option<oneshot::Sender<Result<(), SdkError>>>>>,
105}
106
107impl SyncRequest {
108 fn full(reply: Option<oneshot::Sender<Result<(), SdkError>>>) -> Self {
109 Self {
110 sync_type: SyncType::Full,
111 reply: Arc::new(Mutex::new(reply)),
112 }
113 }
114
115 fn payments_only(reply: Option<oneshot::Sender<Result<(), SdkError>>>) -> Self {
116 Self {
117 sync_type: SyncType::PaymentsOnly,
118 reply: Arc::new(Mutex::new(reply)),
119 }
120 }
121
122 async fn reply(&self, error: Option<SdkError>) {
123 if let Some(reply) = self.reply.lock().await.take() {
124 let _ = match error {
125 Some(e) => reply.send(Err(e)),
126 None => reply.send(Ok(())),
127 };
128 }
129 }
130}
131
132#[derive(Clone)]
135#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
136pub struct BreezSdk {
137 config: Config,
138 spark_wallet: Arc<SparkWallet>,
139 storage: Arc<dyn Storage>,
140 chain_service: Arc<dyn BitcoinChainService>,
141 fiat_service: Arc<dyn FiatService>,
142 lnurl_client: Arc<dyn RestClient>,
143 lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
144 event_emitter: Arc<EventEmitter>,
145 shutdown_sender: watch::Sender<()>,
146 sync_trigger: tokio::sync::broadcast::Sender<SyncRequest>,
147 initial_synced_watcher: watch::Receiver<bool>,
148 external_input_parsers: Vec<ExternalInputParser>,
149 spark_private_mode_initialized: Arc<OnceCell<()>>,
150}
151
152#[cfg_attr(feature = "uniffi", uniffi::export)]
153pub fn init_logging(
154 log_dir: Option<String>,
155 app_logger: Option<Box<dyn Logger>>,
156 log_filter: Option<String>,
157) -> Result<(), SdkError> {
158 logger::init_logging(log_dir, app_logger, log_filter)
159}
160
161#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
171#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
172pub async fn connect(request: crate::ConnectRequest) -> Result<BreezSdk, SdkError> {
173 let builder = super::sdk_builder::SdkBuilder::new(request.config, request.seed)
174 .with_default_storage(request.storage_dir);
175 let sdk = builder.build().await?;
176 Ok(sdk)
177}
178
179#[cfg_attr(feature = "uniffi", uniffi::export)]
180pub fn default_config(network: Network) -> Config {
181 Config {
182 api_key: None,
183 network,
184 sync_interval_secs: 60, max_deposit_claim_fee: Some(Fee::Rate { sat_per_vbyte: 1 }),
186 lnurl_domain: Some("breez.tips".to_string()),
187 prefer_spark_over_lightning: false,
188 external_input_parsers: None,
189 use_default_external_input_parsers: true,
190 real_time_sync_server_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
191 private_enabled_default: true,
192 }
193}
194
195pub(crate) struct BreezSdkParams {
196 pub config: Config,
197 pub storage: Arc<dyn Storage>,
198 pub chain_service: Arc<dyn BitcoinChainService>,
199 pub fiat_service: Arc<dyn FiatService>,
200 pub lnurl_client: Arc<dyn RestClient>,
201 pub lnurl_server_client: Option<Arc<dyn LnurlServerClient>>,
202 pub shutdown_sender: watch::Sender<()>,
203 pub spark_wallet: Arc<SparkWallet>,
204 pub event_emitter: Arc<EventEmitter>,
205}
206
207impl BreezSdk {
208 pub(crate) fn init_and_start(params: BreezSdkParams) -> Result<Self, SdkError> {
210 if !matches!(params.config.network, Network::Regtest) {
213 match ¶ms.config.api_key {
214 Some(api_key) => validate_breez_api_key(api_key)?,
215 None => return Err(SdkError::Generic("Missing Breez API key".to_string())),
216 }
217 }
218 let (initial_synced_sender, initial_synced_watcher) = watch::channel(false);
219 let external_input_parsers = params.config.get_all_external_input_parsers();
220 let sdk = Self {
221 config: params.config,
222 spark_wallet: params.spark_wallet,
223 storage: params.storage,
224 chain_service: params.chain_service,
225 fiat_service: params.fiat_service,
226 lnurl_client: params.lnurl_client,
227 lnurl_server_client: params.lnurl_server_client,
228 event_emitter: params.event_emitter,
229 shutdown_sender: params.shutdown_sender,
230 sync_trigger: tokio::sync::broadcast::channel(10).0,
231 initial_synced_watcher,
232 external_input_parsers,
233 spark_private_mode_initialized: Arc::new(OnceCell::new()),
234 };
235
236 sdk.start(initial_synced_sender);
237 Ok(sdk)
238 }
239
240 fn start(&self, initial_synced_sender: watch::Sender<bool>) {
247 self.spawn_spark_private_mode_initialization();
248 self.periodic_sync(initial_synced_sender);
249 self.try_recover_lightning_address();
250 }
251
252 fn spawn_spark_private_mode_initialization(&self) {
253 let sdk = self.clone();
254 tokio::spawn(async move {
255 if let Err(e) = sdk.ensure_spark_private_mode_initialized().await {
256 error!("Failed to initialize spark private mode: {e:?}");
257 }
258 });
259 }
260
261 fn try_recover_lightning_address(&self) {
263 let sdk = self.clone();
264 tokio::spawn(async move {
265 if sdk.config.lnurl_domain.is_none() {
266 return;
267 }
268
269 match sdk.recover_lightning_address().await {
270 Ok(None) => info!("no lightning address to recover on startup"),
271 Ok(Some(value)) => info!(
272 "recovered lightning address on startup: lnurl: {}, address: {}",
273 value.lnurl, value.lightning_address
274 ),
275 Err(e) => error!("Failed to recover lightning address on startup: {e:?}"),
276 }
277 });
278 }
279
280 fn periodic_sync(&self, initial_synced_sender: watch::Sender<bool>) {
281 let sdk = self.clone();
282 let mut shutdown_receiver = sdk.shutdown_sender.subscribe();
283 let mut subscription = sdk.spark_wallet.subscribe_events();
284 let sync_trigger_sender = sdk.sync_trigger.clone();
285 let mut sync_trigger_receiver = sdk.sync_trigger.clone().subscribe();
286 let mut last_sync_time = SystemTime::now();
287 let sync_interval = u64::from(self.config.sync_interval_secs);
288 tokio::spawn(async move {
289 let balance_watcher =
290 BalanceWatcher::new(sdk.spark_wallet.clone(), sdk.storage.clone());
291 let balance_watcher_id = sdk.add_event_listener(Box::new(balance_watcher)).await;
292 loop {
293 tokio::select! {
294 _ = shutdown_receiver.changed() => {
295 if !sdk.remove_event_listener(&balance_watcher_id).await {
296 error!("Failed to remove balance watcher listener");
297 }
298 info!("Deposit tracking loop shutdown signal received");
299 return;
300 }
301 event = subscription.recv() => {
302 match event {
303 Ok(event) => {
304 info!("Received event: {event}");
305 trace!("Received event: {:?}", event);
306 sdk.handle_wallet_event(event).await;
307 }
308 Err(e) => {
309 error!("Failed to receive event: {e:?}");
310 }
311 }
312 }
313 sync_type_res = sync_trigger_receiver.recv() => {
314 if let Ok(sync_request) = sync_type_res {
315 info!("Sync trigger changed: {:?}", &sync_request);
316 let cloned_sdk = sdk.clone();
317 let initial_synced_sender = initial_synced_sender.clone();
318 if let Some(true) = run_with_shutdown(shutdown_receiver.clone(), "Sync trigger changed", async move {
319 if let Err(e) = cloned_sdk.sync_wallet_internal(sync_request.sync_type.clone()).await {
320 error!("Failed to sync wallet: {e:?}");
321 let () = sync_request.reply(Some(e)).await;
322 return false
323 }
324 if matches!(sync_request.sync_type, SyncType::Full) {
325 let () = sync_request.reply(None).await;
326 if let Err(e) = initial_synced_sender.send(true) {
327 error!("Failed to send initial synced signal: {e:?}");
328 }
329 return true
330 }
331 false
332 }).await {
333 last_sync_time = SystemTime::now();
334 }
335 }
336 }
337 () = tokio::time::sleep(Duration::from_secs(10)) => {
339 let now = SystemTime::now();
340 if let Ok(elapsed) = now.duration_since(last_sync_time) && elapsed.as_secs() >= sync_interval
341 && let Err(e) = sync_trigger_sender.send(SyncRequest::full(None)) {
342 error!("Failed to trigger periodic sync: {e:?}");
343 }
344 }
345 }
346 }
347 });
348 }
349
350 async fn handle_wallet_event(&self, event: WalletEvent) {
351 match event {
352 WalletEvent::DepositConfirmed(_) => {
353 info!("Deposit confirmed");
354 }
355 WalletEvent::StreamConnected => {
356 info!("Stream connected");
357 }
358 WalletEvent::StreamDisconnected => {
359 info!("Stream disconnected");
360 }
361 WalletEvent::Synced => {
362 info!("Synced");
363 if let Err(e) = self.sync_trigger.send(SyncRequest::full(None)) {
364 error!("Failed to sync wallet: {e:?}");
365 }
366 }
367 WalletEvent::TransferClaimed(transfer) => {
368 info!("Transfer claimed");
369 if let Ok(payment) = Payment::try_from(transfer) {
370 if let Err(e) = self.storage.insert_payment(payment.clone()).await {
372 error!("Failed to insert succeeded payment: {e:?}");
373 }
374 self.event_emitter
375 .emit(&SdkEvent::PaymentSucceeded { payment })
376 .await;
377 }
378 if let Err(e) = self.sync_trigger.send(SyncRequest::payments_only(None)) {
379 error!("Failed to sync wallet: {e:?}");
380 }
381 }
382 WalletEvent::TransferClaimStarting(transfer) => {
383 info!("Transfer claim starting");
384 if let Ok(payment) = Payment::try_from(transfer) {
385 if let Err(e) = self.storage.insert_payment(payment.clone()).await {
387 error!("Failed to insert pending payment: {e:?}");
388 }
389 self.event_emitter
390 .emit(&SdkEvent::PaymentPending { payment })
391 .await;
392 }
393 if let Err(e) = self.sync_trigger.send(SyncRequest::payments_only(None)) {
394 error!("Failed to sync wallet: {e:?}");
395 }
396 }
397 }
398 }
399
400 async fn sync_wallet_internal(&self, sync_type: SyncType) -> Result<(), SdkError> {
401 let start_time = Instant::now();
402 if let SyncType::Full = sync_type {
403 if let Err(e) = self.spark_wallet.sync().await {
405 error!("sync_wallet_internal: Failed to sync with Spark network: {e:?}");
406 }
407 }
408 if let Err(e) = self.sync_wallet_state_to_storage().await {
409 error!("sync_wallet_internal: Failed to sync wallet state to storage: {e:?}");
410 }
411 if let Err(e) = self.check_and_claim_static_deposits().await {
412 error!("sync_wallet_internal: Failed to check and claim static deposits: {e:?}");
413 }
414 let elapsed = start_time.elapsed();
415 info!("sync_wallet_internal: Wallet sync completed in {elapsed:?}");
416 self.event_emitter.emit(&SdkEvent::Synced {}).await;
417 Ok(())
418 }
419
420 async fn sync_wallet_state_to_storage(&self) -> Result<(), SdkError> {
422 update_balances(self.spark_wallet.clone(), self.storage.clone()).await?;
423
424 let sync_service = SparkSyncService::new(self.spark_wallet.clone(), self.storage.clone());
425 sync_service.sync_payments().await?;
426
427 Ok(())
428 }
429
430 async fn check_and_claim_static_deposits(&self) -> Result<(), SdkError> {
431 self.ensure_spark_private_mode_initialized().await?;
432 let to_claim = DepositChainSyncer::new(
433 self.chain_service.clone(),
434 self.storage.clone(),
435 self.spark_wallet.clone(),
436 )
437 .sync()
438 .await?;
439
440 let mut claimed_deposits: Vec<DepositInfo> = Vec::new();
441 let mut unclaimed_deposits: Vec<DepositInfo> = Vec::new();
442 for detailed_utxo in to_claim {
443 match self
444 .claim_utxo(&detailed_utxo, self.config.max_deposit_claim_fee.clone())
445 .await
446 {
447 Ok(_) => {
448 info!("Claimed utxo {}:{}", detailed_utxo.txid, detailed_utxo.vout);
449 self.storage
450 .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
451 .await?;
452 claimed_deposits.push(detailed_utxo.into());
453 }
454 Err(e) => {
455 warn!(
456 "Failed to claim utxo {}:{}: {e}",
457 detailed_utxo.txid, detailed_utxo.vout
458 );
459 self.storage
460 .update_deposit(
461 detailed_utxo.txid.to_string(),
462 detailed_utxo.vout,
463 UpdateDepositPayload::ClaimError {
464 error: e.clone().into(),
465 },
466 )
467 .await?;
468 let mut unclaimed_deposit: DepositInfo = detailed_utxo.clone().into();
469 unclaimed_deposit.claim_error = Some(e.into());
470 unclaimed_deposits.push(unclaimed_deposit);
471 }
472 }
473 }
474
475 info!("background claim completed, unclaimed deposits: {unclaimed_deposits:?}");
476
477 if !unclaimed_deposits.is_empty() {
478 self.event_emitter
479 .emit(&SdkEvent::UnclaimedDeposits { unclaimed_deposits })
480 .await;
481 }
482 if !claimed_deposits.is_empty() {
483 self.event_emitter
484 .emit(&SdkEvent::ClaimedDeposits { claimed_deposits })
485 .await;
486 }
487 Ok(())
488 }
489
490 async fn claim_utxo(
491 &self,
492 detailed_utxo: &DetailedUtxo,
493 max_claim_fee: Option<Fee>,
494 ) -> Result<WalletTransfer, SdkError> {
495 info!(
496 "Fetching static deposit claim quote for deposit tx {}:{} and amount: {}",
497 detailed_utxo.txid, detailed_utxo.vout, detailed_utxo.value
498 );
499 let quote = self
500 .spark_wallet
501 .fetch_static_deposit_claim_quote(detailed_utxo.tx.clone(), Some(detailed_utxo.vout))
502 .await?;
503 let spark_requested_fee = detailed_utxo.value.saturating_sub(quote.credit_amount_sats);
504 let Some(max_deposit_claim_fee) = max_claim_fee else {
505 return Err(SdkError::DepositClaimFeeExceeded {
506 tx: detailed_utxo.txid.to_string(),
507 vout: detailed_utxo.vout,
508 max_fee: None,
509 actual_fee: spark_requested_fee,
510 });
511 };
512 match max_deposit_claim_fee {
513 Fee::Fixed { amount } => {
514 info!(
515 "User max fee: {} spark requested fee: {}",
516 amount, spark_requested_fee
517 );
518 if spark_requested_fee > amount {
519 return Err(SdkError::DepositClaimFeeExceeded {
520 tx: detailed_utxo.txid.to_string(),
521 vout: detailed_utxo.vout,
522 max_fee: Some(max_deposit_claim_fee),
523 actual_fee: spark_requested_fee,
524 });
525 }
526 }
527 Fee::Rate { sat_per_vbyte } => {
528 const CLAIM_TX_SIZE: u64 = 99;
530 let user_max_fee = CLAIM_TX_SIZE.saturating_mul(sat_per_vbyte);
531 info!(
532 "User max fee: {} spark requested fee: {}",
533 user_max_fee, spark_requested_fee
534 );
535 if spark_requested_fee > user_max_fee {
536 return Err(SdkError::DepositClaimFeeExceeded {
537 tx: detailed_utxo.txid.to_string(),
538 vout: detailed_utxo.vout,
539 max_fee: Some(max_deposit_claim_fee),
540 actual_fee: spark_requested_fee,
541 });
542 }
543 }
544 }
545 info!(
546 "Claiming static deposit for utxo {}:{}",
547 detailed_utxo.txid, detailed_utxo.vout
548 );
549 let transfer = self.spark_wallet.claim_static_deposit(quote).await?;
550 info!(
551 "Claimed static deposit transfer: {}",
552 serde_json::to_string_pretty(&transfer)?
553 );
554 Ok(transfer)
555 }
556
557 async fn ensure_spark_private_mode_initialized(&self) -> Result<(), SdkError> {
558 self.spark_private_mode_initialized
559 .get_or_try_init(|| async {
560 let object_repository = ObjectCacheRepository::new(self.storage.clone());
562 let is_initialized = object_repository
563 .fetch_spark_private_mode_initialized()
564 .await?;
565
566 if !is_initialized {
567 self.initialize_spark_private_mode().await?;
569 }
570 Ok::<_, SdkError>(())
571 })
572 .await?;
573 Ok(())
574 }
575
576 async fn initialize_spark_private_mode(&self) -> Result<(), SdkError> {
577 if !self.config.private_enabled_default {
578 ObjectCacheRepository::new(self.storage.clone())
579 .save_spark_private_mode_initialized()
580 .await?;
581 info!("Spark private mode initialized: no changes needed");
582 return Ok(());
583 }
584
585 self.update_user_settings(UpdateUserSettingsRequest {
587 spark_private_mode_enabled: Some(true),
588 })
589 .await?;
590 ObjectCacheRepository::new(self.storage.clone())
591 .save_spark_private_mode_initialized()
592 .await?;
593 info!("Spark private mode initialized: enabled");
594 Ok(())
595 }
596}
597
598#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
599#[allow(clippy::needless_pass_by_value)]
600impl BreezSdk {
601 pub async fn add_event_listener(&self, listener: Box<dyn EventListener>) -> String {
611 self.event_emitter.add_listener(listener).await
612 }
613
614 pub async fn remove_event_listener(&self, id: &str) -> bool {
624 self.event_emitter.remove_listener(id).await
625 }
626
627 pub async fn disconnect(&self) -> Result<(), SdkError> {
636 info!("Disconnecting Breez SDK");
637 self.shutdown_sender
638 .send(())
639 .map_err(|_| SdkError::Generic("Failed to send shutdown signal".to_string()))?;
640
641 self.shutdown_sender.closed().await;
642 info!("Breez SDK disconnected");
643 Ok(())
644 }
645
646 pub async fn parse(&self, input: &str) -> Result<InputType, SdkError> {
647 parse_input(input, Some(self.external_input_parsers.clone())).await
648 }
649
650 #[allow(unused_variables)]
652 pub async fn get_info(&self, request: GetInfoRequest) -> Result<GetInfoResponse, SdkError> {
653 if request.ensure_synced.unwrap_or_default() {
654 self.initial_synced_watcher
655 .clone()
656 .changed()
657 .await
658 .map_err(|_| {
659 SdkError::Generic("Failed to receive initial synced signal".to_string())
660 })?;
661 }
662 let object_repository = ObjectCacheRepository::new(self.storage.clone());
663 let account_info = object_repository
664 .fetch_account_info()
665 .await?
666 .unwrap_or_default();
667 Ok(GetInfoResponse {
668 balance_sats: account_info.balance_sats,
669 token_balances: account_info.token_balances,
670 })
671 }
672
673 pub async fn receive_payment(
674 &self,
675 request: ReceivePaymentRequest,
676 ) -> Result<ReceivePaymentResponse, SdkError> {
677 self.ensure_spark_private_mode_initialized().await?;
678 match request.payment_method {
679 ReceivePaymentMethod::SparkAddress => Ok(ReceivePaymentResponse {
680 fee: 0,
681 payment_request: self
682 .spark_wallet
683 .get_spark_address()?
684 .to_address_string()
685 .map_err(|e| {
686 SdkError::Generic(format!("Failed to convert Spark address to string: {e}"))
687 })?,
688 }),
689 ReceivePaymentMethod::SparkInvoice {
690 amount,
691 token_identifier,
692 expiry_time,
693 description,
694 sender_public_key,
695 } => {
696 let invoice = self.spark_wallet.create_spark_invoice(
697 amount,
698 token_identifier.clone(),
699 expiry_time
700 .map(|time| {
701 SystemTime::UNIX_EPOCH
702 .checked_add(Duration::from_secs(time))
703 .ok_or(SdkError::Generic("Invalid expiry time".to_string()))
704 })
705 .transpose()?,
706 description,
707 sender_public_key.map(|key| PublicKey::from_str(&key).unwrap()),
708 )?;
709 Ok(ReceivePaymentResponse {
710 fee: 0,
711 payment_request: invoice,
712 })
713 }
714 ReceivePaymentMethod::BitcoinAddress => {
715 let object_repository = ObjectCacheRepository::new(self.storage.clone());
718
719 let static_deposit_address =
721 object_repository.fetch_static_deposit_address().await?;
722 if let Some(static_deposit_address) = static_deposit_address {
723 return Ok(ReceivePaymentResponse {
724 payment_request: static_deposit_address.address.clone(),
725 fee: 0,
726 });
727 }
728
729 let deposit_addresses = self
731 .spark_wallet
732 .list_static_deposit_addresses(None)
733 .await?;
734
735 let address = match deposit_addresses.items.last() {
737 Some(address) => address.to_string(),
738 None => self
739 .spark_wallet
740 .generate_deposit_address(true)
741 .await?
742 .to_string(),
743 };
744
745 object_repository
746 .save_static_deposit_address(&StaticDepositAddress {
747 address: address.clone(),
748 })
749 .await?;
750
751 Ok(ReceivePaymentResponse {
752 payment_request: address,
753 fee: 0,
754 })
755 }
756 ReceivePaymentMethod::Bolt11Invoice {
757 description,
758 amount_sats,
759 } => Ok(ReceivePaymentResponse {
760 payment_request: self
761 .spark_wallet
762 .create_lightning_invoice(
763 amount_sats.unwrap_or_default(),
764 Some(InvoiceDescription::Memo(description.clone())),
765 None,
766 self.config.prefer_spark_over_lightning,
767 )
768 .await?
769 .invoice,
770 fee: 0,
771 }),
772 }
773 }
774
775 pub async fn prepare_lnurl_pay(
776 &self,
777 request: PrepareLnurlPayRequest,
778 ) -> Result<PrepareLnurlPayResponse, SdkError> {
779 let success_data = match validate_lnurl_pay(
780 self.lnurl_client.as_ref(),
781 request.amount_sats.saturating_mul(1_000),
782 &None,
783 &request.pay_request.clone().into(),
784 self.config.network.into(),
785 request.validate_success_action_url,
786 )
787 .await?
788 {
789 lnurl::pay::ValidatedCallbackResponse::EndpointError { data } => {
790 return Err(LnurlError::EndpointError(data.reason).into());
791 }
792 lnurl::pay::ValidatedCallbackResponse::EndpointSuccess { data } => data,
793 };
794
795 let prepare_response = self
796 .prepare_send_payment(PrepareSendPaymentRequest {
797 payment_request: success_data.pr,
798 amount: Some(request.amount_sats.into()),
799 token_identifier: None,
800 })
801 .await?;
802
803 let SendPaymentMethod::Bolt11Invoice {
804 invoice_details,
805 lightning_fee_sats,
806 ..
807 } = prepare_response.payment_method
808 else {
809 return Err(SdkError::Generic(
810 "Expected Bolt11Invoice payment method".to_string(),
811 ));
812 };
813
814 Ok(PrepareLnurlPayResponse {
815 amount_sats: request.amount_sats,
816 comment: request.comment,
817 pay_request: request.pay_request,
818 invoice_details,
819 fee_sats: lightning_fee_sats,
820 success_action: success_data.success_action.map(From::from),
821 })
822 }
823
824 pub async fn lnurl_pay(&self, request: LnurlPayRequest) -> Result<LnurlPayResponse, SdkError> {
825 self.ensure_spark_private_mode_initialized().await?;
826 let mut payment = Box::pin(self.send_payment_internal(
827 SendPaymentRequest {
828 prepare_response: PrepareSendPaymentResponse {
829 payment_method: SendPaymentMethod::Bolt11Invoice {
830 invoice_details: request.prepare_response.invoice_details,
831 spark_transfer_fee_sats: None,
832 lightning_fee_sats: request.prepare_response.fee_sats,
833 },
834 amount: request.prepare_response.amount_sats.into(),
835 token_identifier: None,
836 },
837 options: None,
838 },
839 true,
840 ))
841 .await?
842 .payment;
843
844 let success_action = process_success_action(
845 &payment,
846 request
847 .prepare_response
848 .success_action
849 .clone()
850 .map(Into::into)
851 .as_ref(),
852 )?;
853
854 let lnurl_info = LnurlPayInfo {
855 ln_address: request.prepare_response.pay_request.address,
856 comment: request.prepare_response.comment,
857 domain: Some(request.prepare_response.pay_request.domain),
858 metadata: Some(request.prepare_response.pay_request.metadata_str),
859 processed_success_action: success_action.clone().map(From::from),
860 raw_success_action: request.prepare_response.success_action,
861 };
862 let Some(PaymentDetails::Lightning {
863 lnurl_pay_info,
864 description,
865 ..
866 }) = &mut payment.details
867 else {
868 return Err(SdkError::Generic(
869 "Expected Lightning payment details".to_string(),
870 ));
871 };
872 *lnurl_pay_info = Some(lnurl_info.clone());
873
874 let lnurl_description = lnurl_info.extract_description();
875 description.clone_from(&lnurl_description);
876
877 self.storage
878 .set_payment_metadata(
879 payment.id.clone(),
880 PaymentMetadata {
881 lnurl_pay_info: Some(lnurl_info),
882 lnurl_description,
883 ..Default::default()
884 },
885 )
886 .await?;
887
888 emit_payment_status(&self.event_emitter, payment.clone()).await;
889 Ok(LnurlPayResponse {
890 payment,
891 success_action: success_action.map(From::from),
892 })
893 }
894
895 pub async fn lnurl_withdraw(
921 &self,
922 request: LnurlWithdrawRequest,
923 ) -> Result<LnurlWithdrawResponse, SdkError> {
924 self.ensure_spark_private_mode_initialized().await?;
925 let LnurlWithdrawRequest {
926 amount_sats,
927 withdraw_request,
928 completion_timeout_secs,
929 } = request;
930 let withdraw_request: breez_sdk_common::lnurl::withdraw::LnurlWithdrawRequestDetails =
931 withdraw_request.into();
932 if !withdraw_request.is_amount_valid(amount_sats) {
933 return Err(SdkError::InvalidInput(
934 "Amount must be within min/max LNURL withdrawable limits".to_string(),
935 ));
936 }
937
938 let payment_request = self
940 .receive_payment(ReceivePaymentRequest {
941 payment_method: ReceivePaymentMethod::Bolt11Invoice {
942 description: withdraw_request.default_description.clone(),
943 amount_sats: Some(amount_sats),
944 },
945 })
946 .await?
947 .payment_request;
948
949 let cache = ObjectCacheRepository::new(self.storage.clone());
951 cache
952 .save_payment_request_metadata(&PaymentRequestMetadata {
953 payment_request: payment_request.clone(),
954 lnurl_withdraw_request_details: withdraw_request.clone(),
955 })
956 .await?;
957
958 let withdraw_response = execute_lnurl_withdraw(
960 self.lnurl_client.as_ref(),
961 &withdraw_request,
962 &payment_request,
963 )
964 .await?;
965 if let lnurl::withdraw::ValidatedCallbackResponse::EndpointError { data } =
966 withdraw_response
967 {
968 return Err(LnurlError::EndpointError(data.reason).into());
969 }
970
971 let completion_timeout_secs = match completion_timeout_secs {
972 Some(secs) if secs > 0 => secs,
973 _ => {
974 return Ok(LnurlWithdrawResponse {
975 payment_request,
976 payment: None,
977 });
978 }
979 };
980
981 let fut = self.wait_for_payment(WaitForPaymentRequest {
983 identifier: WaitForPaymentIdentifier::PaymentRequest(payment_request.clone()),
984 });
985
986 let payment = match timeout(Duration::from_secs(completion_timeout_secs.into()), fut).await
987 {
988 Ok(Ok(res)) => Some(res.payment),
990 Ok(Err(e)) => return Err(SdkError::Generic(format!("Error waiting for payment: {e}"))),
992 Err(_) => None,
994 };
995
996 Ok(LnurlWithdrawResponse {
997 payment_request,
998 payment,
999 })
1000 }
1001
1002 #[allow(clippy::too_many_lines)]
1003 pub async fn prepare_send_payment(
1004 &self,
1005 request: PrepareSendPaymentRequest,
1006 ) -> Result<PrepareSendPaymentResponse, SdkError> {
1007 let parsed_input = self.parse(&request.payment_request).await?;
1008
1009 validate_prepare_send_payment_request(
1010 &parsed_input,
1011 &request,
1012 &self.spark_wallet.get_identity_public_key().to_string(),
1013 )?;
1014
1015 match &parsed_input {
1016 InputType::SparkAddress(spark_address_details) => Ok(PrepareSendPaymentResponse {
1017 payment_method: SendPaymentMethod::SparkAddress {
1018 address: spark_address_details.address.clone(),
1019 fee: 0,
1020 token_identifier: request.token_identifier.clone(),
1021 },
1022 amount: request
1023 .amount
1024 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
1025 token_identifier: request.token_identifier,
1026 }),
1027 InputType::SparkInvoice(spark_invoice_details) => Ok(PrepareSendPaymentResponse {
1028 payment_method: SendPaymentMethod::SparkInvoice {
1029 spark_invoice_details: spark_invoice_details.clone(),
1030 fee: 0,
1031 token_identifier: request.token_identifier.clone(),
1032 },
1033 amount: spark_invoice_details
1034 .amount
1035 .or(request.amount)
1036 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
1037 token_identifier: request.token_identifier,
1038 }),
1039 InputType::Bolt11Invoice(detailed_bolt11_invoice) => {
1040 let spark_address = self
1041 .spark_wallet
1042 .extract_spark_address(&request.payment_request)?;
1043
1044 let spark_transfer_fee_sats = if spark_address.is_some() {
1045 Some(0)
1046 } else {
1047 None
1048 };
1049
1050 let lightning_fee_sats = self
1051 .spark_wallet
1052 .fetch_lightning_send_fee_estimate(
1053 &request.payment_request,
1054 request
1055 .amount
1056 .map(|a| Ok::<u64, SdkError>(a.try_into()?))
1057 .transpose()?,
1058 )
1059 .await?;
1060
1061 Ok(PrepareSendPaymentResponse {
1062 payment_method: SendPaymentMethod::Bolt11Invoice {
1063 invoice_details: detailed_bolt11_invoice.clone(),
1064 spark_transfer_fee_sats,
1065 lightning_fee_sats,
1066 },
1067 amount: request
1068 .amount
1069 .or(detailed_bolt11_invoice
1070 .amount_msat
1071 .map(|msat| u128::from(msat) / 1000))
1072 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
1073 token_identifier: None,
1074 })
1075 }
1076 InputType::BitcoinAddress(withdrawal_address) => {
1077 let fee_quote = self
1078 .spark_wallet
1079 .fetch_coop_exit_fee_quote(
1080 &withdrawal_address.address,
1081 Some(
1082 request
1083 .amount
1084 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?
1085 .try_into()?,
1086 ),
1087 )
1088 .await?;
1089 Ok(PrepareSendPaymentResponse {
1090 payment_method: SendPaymentMethod::BitcoinAddress {
1091 address: withdrawal_address.clone(),
1092 fee_quote: fee_quote.into(),
1093 },
1094 amount: request
1095 .amount
1096 .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?,
1097 token_identifier: None,
1098 })
1099 }
1100 _ => Err(SdkError::InvalidInput(
1101 "Unsupported payment method".to_string(),
1102 )),
1103 }
1104 }
1105
1106 pub async fn send_payment(
1107 &self,
1108 request: SendPaymentRequest,
1109 ) -> Result<SendPaymentResponse, SdkError> {
1110 self.ensure_spark_private_mode_initialized().await?;
1111 Box::pin(self.send_payment_internal(request, false)).await
1112 }
1113
1114 #[allow(unused_variables)]
1116 pub async fn sync_wallet(
1117 &self,
1118 request: SyncWalletRequest,
1119 ) -> Result<SyncWalletResponse, SdkError> {
1120 let (tx, rx) = oneshot::channel();
1121
1122 if let Err(e) = self.sync_trigger.send(SyncRequest::full(Some(tx))) {
1123 error!("Failed to send sync trigger: {e:?}");
1124 }
1125 let _ = rx.await.map_err(|e| {
1126 error!("Failed to receive sync trigger: {e:?}");
1127 SdkError::Generic(format!("sync trigger failed: {e:?}"))
1128 })?;
1129 Ok(SyncWalletResponse {})
1130 }
1131
1132 pub async fn list_payments(
1147 &self,
1148 request: ListPaymentsRequest,
1149 ) -> Result<ListPaymentsResponse, SdkError> {
1150 let payments = self.storage.list_payments(request).await?;
1151 Ok(ListPaymentsResponse { payments })
1152 }
1153
1154 pub async fn get_payment(
1155 &self,
1156 request: GetPaymentRequest,
1157 ) -> Result<GetPaymentResponse, SdkError> {
1158 let payment = self.storage.get_payment_by_id(request.payment_id).await?;
1159 Ok(GetPaymentResponse { payment })
1160 }
1161
1162 pub async fn claim_deposit(
1163 &self,
1164 request: ClaimDepositRequest,
1165 ) -> Result<ClaimDepositResponse, SdkError> {
1166 self.ensure_spark_private_mode_initialized().await?;
1167 let detailed_utxo =
1168 CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
1169 .fetch_detailed_utxo(&request.txid, request.vout)
1170 .await?;
1171
1172 let max_fee = request
1173 .max_fee
1174 .or(self.config.max_deposit_claim_fee.clone());
1175 match self.claim_utxo(&detailed_utxo, max_fee).await {
1176 Ok(transfer) => {
1177 self.storage
1178 .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
1179 .await?;
1180 if let Err(e) = self.sync_trigger.send(SyncRequest::payments_only(None)) {
1181 error!("Failed to execute sync after deposit claim: {e:?}");
1182 }
1183 Ok(ClaimDepositResponse {
1184 payment: transfer.try_into()?,
1185 })
1186 }
1187 Err(e) => {
1188 error!("Failed to claim deposit: {e:?}");
1189 self.storage
1190 .update_deposit(
1191 detailed_utxo.txid.to_string(),
1192 detailed_utxo.vout,
1193 UpdateDepositPayload::ClaimError {
1194 error: e.clone().into(),
1195 },
1196 )
1197 .await?;
1198 Err(e)
1199 }
1200 }
1201 }
1202
1203 pub async fn refund_deposit(
1204 &self,
1205 request: RefundDepositRequest,
1206 ) -> Result<RefundDepositResponse, SdkError> {
1207 let detailed_utxo =
1208 CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
1209 .fetch_detailed_utxo(&request.txid, request.vout)
1210 .await?;
1211 let tx = self
1212 .spark_wallet
1213 .refund_static_deposit(
1214 detailed_utxo.clone().tx,
1215 Some(detailed_utxo.vout),
1216 &request.destination_address,
1217 request.fee.into(),
1218 )
1219 .await?;
1220 let deposit: DepositInfo = detailed_utxo.into();
1221 let tx_hex = serialize(&tx).as_hex().to_string();
1222 let tx_id = tx.compute_txid().as_raw_hash().to_string();
1223
1224 self.storage
1226 .update_deposit(
1227 deposit.txid.clone(),
1228 deposit.vout,
1229 UpdateDepositPayload::Refund {
1230 refund_tx: tx_hex.clone(),
1231 refund_txid: tx_id.clone(),
1232 },
1233 )
1234 .await?;
1235
1236 self.chain_service
1237 .broadcast_transaction(tx_hex.clone())
1238 .await?;
1239 Ok(RefundDepositResponse { tx_id, tx_hex })
1240 }
1241
1242 #[allow(unused_variables)]
1243 pub async fn list_unclaimed_deposits(
1244 &self,
1245 request: ListUnclaimedDepositsRequest,
1246 ) -> Result<ListUnclaimedDepositsResponse, SdkError> {
1247 let deposits = self.storage.list_deposits().await?;
1248 Ok(ListUnclaimedDepositsResponse { deposits })
1249 }
1250
1251 pub async fn check_lightning_address_available(
1252 &self,
1253 req: CheckLightningAddressRequest,
1254 ) -> Result<bool, SdkError> {
1255 let Some(client) = &self.lnurl_server_client else {
1256 return Err(SdkError::Generic(
1257 "LNURL server is not configured".to_string(),
1258 ));
1259 };
1260
1261 let username = sanitize_username(&req.username);
1262 let available = client.check_username_available(&username).await?;
1263 Ok(available)
1264 }
1265
1266 pub async fn get_lightning_address(&self) -> Result<Option<LightningAddressInfo>, SdkError> {
1267 let cache = ObjectCacheRepository::new(self.storage.clone());
1268 Ok(cache.fetch_lightning_address().await?)
1269 }
1270
1271 pub async fn register_lightning_address(
1272 &self,
1273 request: RegisterLightningAddressRequest,
1274 ) -> Result<LightningAddressInfo, SdkError> {
1275 let cache = ObjectCacheRepository::new(self.storage.clone());
1276 let Some(client) = &self.lnurl_server_client else {
1277 return Err(SdkError::Generic(
1278 "LNURL server is not configured".to_string(),
1279 ));
1280 };
1281
1282 let username = sanitize_username(&request.username);
1283
1284 let description = match request.description {
1285 Some(description) => description,
1286 None => format!("Pay to {}@{}", username, client.domain()),
1287 };
1288 let params = crate::lnurl::RegisterLightningAddressRequest {
1289 username: username.clone(),
1290 description: description.clone(),
1291 };
1292
1293 let response = client.register_lightning_address(¶ms).await?;
1294 let address_info = LightningAddressInfo {
1295 lightning_address: response.lightning_address,
1296 description,
1297 lnurl: response.lnurl,
1298 username,
1299 };
1300 cache.save_lightning_address(&address_info).await?;
1301 Ok(address_info)
1302 }
1303
1304 pub async fn delete_lightning_address(&self) -> Result<(), SdkError> {
1305 let cache = ObjectCacheRepository::new(self.storage.clone());
1306 let Some(address_info) = cache.fetch_lightning_address().await? else {
1307 return Ok(());
1308 };
1309
1310 let Some(client) = &self.lnurl_server_client else {
1311 return Err(SdkError::Generic(
1312 "LNURL server is not configured".to_string(),
1313 ));
1314 };
1315
1316 let params = crate::lnurl::UnregisterLightningAddressRequest {
1317 username: address_info.username,
1318 };
1319
1320 client.unregister_lightning_address(¶ms).await?;
1321 cache.delete_lightning_address().await?;
1322 Ok(())
1323 }
1324
1325 pub async fn list_fiat_currencies(&self) -> Result<ListFiatCurrenciesResponse, SdkError> {
1328 let currencies = self
1329 .fiat_service
1330 .fetch_fiat_currencies()
1331 .await?
1332 .into_iter()
1333 .map(From::from)
1334 .collect();
1335 Ok(ListFiatCurrenciesResponse { currencies })
1336 }
1337
1338 pub async fn list_fiat_rates(&self) -> Result<ListFiatRatesResponse, SdkError> {
1340 let rates = self
1341 .fiat_service
1342 .fetch_fiat_rates()
1343 .await?
1344 .into_iter()
1345 .map(From::from)
1346 .collect();
1347 Ok(ListFiatRatesResponse { rates })
1348 }
1349
1350 pub async fn wait_for_payment(
1351 &self,
1352 request: WaitForPaymentRequest,
1353 ) -> Result<WaitForPaymentResponse, SdkError> {
1354 let (tx, mut rx) = mpsc::channel(20);
1355 let id = self
1356 .add_event_listener(Box::new(InternalEventListener::new(tx)))
1357 .await;
1358
1359 if let WaitForPaymentIdentifier::PaymentRequest(payment_request) = &request.identifier
1361 && let Some(payment) = self
1362 .storage
1363 .get_payment_by_invoice(payment_request.clone())
1364 .await?
1365 {
1366 self.remove_event_listener(&id).await;
1367 return Ok(WaitForPaymentResponse { payment });
1368 }
1369
1370 let payment_result = loop {
1372 let Some(event) = rx.recv().await else {
1373 break Err(SdkError::Generic("Event channel closed".to_string()));
1374 };
1375
1376 let SdkEvent::PaymentSucceeded { payment } = event else {
1377 continue;
1378 };
1379
1380 if is_payment_match(&payment, &request) {
1381 break Ok(payment);
1382 }
1383 };
1384
1385 self.remove_event_listener(&id).await;
1386 Ok(WaitForPaymentResponse {
1387 payment: payment_result?,
1388 })
1389 }
1390
1391 pub async fn get_tokens_metadata(
1398 &self,
1399 request: GetTokensMetadataRequest,
1400 ) -> Result<GetTokensMetadataResponse, SdkError> {
1401 let metadata = get_tokens_metadata_cached_or_query(
1402 &self.spark_wallet,
1403 &ObjectCacheRepository::new(self.storage.clone()),
1404 &request
1405 .token_identifiers
1406 .iter()
1407 .map(String::as_str)
1408 .collect::<Vec<_>>(),
1409 )
1410 .await?;
1411 Ok(GetTokensMetadataResponse {
1412 tokens_metadata: metadata,
1413 })
1414 }
1415
1416 pub async fn sign_message(
1420 &self,
1421 request: SignMessageRequest,
1422 ) -> Result<SignMessageResponse, SdkError> {
1423 let pubkey = self.spark_wallet.get_identity_public_key().to_string();
1424 let signature = self.spark_wallet.sign_message(&request.message).await?;
1425 let signature_hex = if request.compact {
1426 signature.serialize_compact().to_lower_hex_string()
1427 } else {
1428 signature.serialize_der().to_lower_hex_string()
1429 };
1430
1431 Ok(SignMessageResponse {
1432 pubkey,
1433 signature: signature_hex,
1434 })
1435 }
1436
1437 pub async fn check_message(
1441 &self,
1442 request: CheckMessageRequest,
1443 ) -> Result<CheckMessageResponse, SdkError> {
1444 let pubkey = PublicKey::from_str(&request.pubkey)
1445 .map_err(|_| SdkError::InvalidInput("Invalid public key".to_string()))?;
1446 let signature_bytes = hex::decode(&request.signature)
1447 .map_err(|_| SdkError::InvalidInput("Not a valid hex encoded signature".to_string()))?;
1448 let signature = Signature::from_der(&signature_bytes)
1449 .or_else(|_| Signature::from_compact(&signature_bytes))
1450 .map_err(|_| {
1451 SdkError::InvalidInput("Not a valid DER or compact encoded signature".to_string())
1452 })?;
1453
1454 let is_valid = self
1455 .spark_wallet
1456 .verify_message(&request.message, &signature, &pubkey)
1457 .await
1458 .is_ok();
1459 Ok(CheckMessageResponse { is_valid })
1460 }
1461
1462 pub async fn get_user_settings(&self) -> Result<UserSettings, SdkError> {
1466 self.ensure_spark_private_mode_initialized().await?;
1468
1469 let spark_user_settings = self.spark_wallet.query_wallet_settings().await?;
1470
1471 Ok(UserSettings {
1474 spark_private_mode_enabled: spark_user_settings.private_enabled,
1475 })
1476 }
1477
1478 pub async fn update_user_settings(
1482 &self,
1483 request: UpdateUserSettingsRequest,
1484 ) -> Result<(), SdkError> {
1485 if let Some(spark_private_mode_enabled) = request.spark_private_mode_enabled {
1486 self.spark_wallet
1487 .update_wallet_settings(spark_private_mode_enabled)
1488 .await?;
1489 }
1490 Ok(())
1491 }
1492
1493 pub fn get_token_issuer(&self) -> TokenIssuer {
1495 TokenIssuer::new(self.spark_wallet.clone(), self.storage.clone())
1496 }
1497}
1498
1499impl BreezSdk {
1501 async fn send_payment_internal(
1502 &self,
1503 request: SendPaymentRequest,
1504 suppress_payment_event: bool,
1505 ) -> Result<SendPaymentResponse, SdkError> {
1506 let res = match &request.prepare_response.payment_method {
1507 SendPaymentMethod::SparkAddress {
1508 address,
1509 token_identifier,
1510 ..
1511 } => {
1512 self.send_spark_address(address, token_identifier.clone(), &request)
1513 .await
1514 }
1515 SendPaymentMethod::SparkInvoice {
1516 spark_invoice_details,
1517 ..
1518 } => {
1519 self.send_spark_invoice(&spark_invoice_details.invoice, &request)
1520 .await
1521 }
1522 SendPaymentMethod::Bolt11Invoice {
1523 invoice_details,
1524 spark_transfer_fee_sats,
1525 lightning_fee_sats,
1526 } => {
1527 self.send_bolt11_invoice(
1528 invoice_details,
1529 *spark_transfer_fee_sats,
1530 *lightning_fee_sats,
1531 &request,
1532 )
1533 .await
1534 }
1535 SendPaymentMethod::BitcoinAddress { address, fee_quote } => {
1536 self.send_bitcoin_address(address, fee_quote, &request)
1537 .await
1538 }
1539 };
1540 if let Ok(response) = &res {
1541 if !suppress_payment_event {
1545 emit_payment_status(&self.event_emitter, response.payment.clone()).await;
1546 }
1547 if let Err(e) = self.sync_trigger.send(SyncRequest::payments_only(None)) {
1548 error!("Failed to send sync trigger: {e:?}");
1549 }
1550 }
1551 res
1552 }
1553
1554 async fn send_spark_address(
1555 &self,
1556 address: &str,
1557 token_identifier: Option<String>,
1558 request: &SendPaymentRequest,
1559 ) -> Result<SendPaymentResponse, SdkError> {
1560 let spark_address = address
1561 .parse::<SparkAddress>()
1562 .map_err(|_| SdkError::InvalidInput("Invalid spark address".to_string()))?;
1563
1564 let payment = if let Some(identifier) = token_identifier {
1565 self.send_spark_token_address(
1566 identifier,
1567 request.prepare_response.amount,
1568 spark_address,
1569 )
1570 .await?
1571 } else {
1572 let transfer = self
1573 .spark_wallet
1574 .transfer(request.prepare_response.amount.try_into()?, &spark_address)
1575 .await?;
1576 transfer.try_into()?
1577 };
1578
1579 Ok(SendPaymentResponse { payment })
1580 }
1581
1582 async fn send_spark_token_address(
1583 &self,
1584 token_identifier: String,
1585 amount: u128,
1586 receiver_address: SparkAddress,
1587 ) -> Result<Payment, SdkError> {
1588 let token_transaction = self
1589 .spark_wallet
1590 .transfer_tokens(
1591 vec![TransferTokenOutput {
1592 token_id: token_identifier,
1593 amount,
1594 receiver_address: receiver_address.clone(),
1595 spark_invoice: None,
1596 }],
1597 None,
1598 )
1599 .await?;
1600
1601 map_and_persist_token_transaction(&self.spark_wallet, &self.storage, &token_transaction)
1602 .await
1603 }
1604
1605 async fn send_spark_invoice(
1606 &self,
1607 invoice: &str,
1608 request: &SendPaymentRequest,
1609 ) -> Result<SendPaymentResponse, SdkError> {
1610 let payment = match self
1611 .spark_wallet
1612 .fulfill_spark_invoice(invoice, Some(request.prepare_response.amount))
1613 .await?
1614 {
1615 spark_wallet::FulfillSparkInvoiceResult::Transfer(wallet_transfer) => {
1616 (*wallet_transfer).try_into()?
1617 }
1618 spark_wallet::FulfillSparkInvoiceResult::TokenTransaction(token_transaction) => {
1619 map_and_persist_token_transaction(
1620 &self.spark_wallet,
1621 &self.storage,
1622 &token_transaction,
1623 )
1624 .await?
1625 }
1626 };
1627
1628 Ok(SendPaymentResponse { payment })
1629 }
1630
1631 async fn send_bolt11_invoice(
1632 &self,
1633 invoice_details: &Bolt11InvoiceDetails,
1634 spark_transfer_fee_sats: Option<u64>,
1635 lightning_fee_sats: u64,
1636 request: &SendPaymentRequest,
1637 ) -> Result<SendPaymentResponse, SdkError> {
1638 let amount_to_send = match invoice_details.amount_msat {
1639 Some(_) => None,
1641 None => Some(request.prepare_response.amount),
1643 };
1644 let (prefer_spark, completion_timeout_secs) = match request.options {
1645 Some(SendPaymentOptions::Bolt11Invoice {
1646 prefer_spark,
1647 completion_timeout_secs,
1648 }) => (prefer_spark, completion_timeout_secs),
1649 _ => (self.config.prefer_spark_over_lightning, None),
1650 };
1651 let fee_sats = match (prefer_spark, spark_transfer_fee_sats, lightning_fee_sats) {
1652 (true, Some(fee), _) => fee,
1653 _ => lightning_fee_sats,
1654 };
1655
1656 let payment_response = self
1657 .spark_wallet
1658 .pay_lightning_invoice(
1659 &invoice_details.invoice.bolt11,
1660 amount_to_send
1661 .map(|a| Ok::<u64, SdkError>(a.try_into()?))
1662 .transpose()?,
1663 Some(fee_sats),
1664 prefer_spark,
1665 )
1666 .await?;
1667 let payment = match payment_response.lightning_payment {
1668 Some(lightning_payment) => {
1669 let ssp_id = lightning_payment.id.clone();
1670 let payment = Payment::from_lightning(
1671 lightning_payment,
1672 request.prepare_response.amount,
1673 payment_response.transfer.id.to_string(),
1674 )?;
1675 self.poll_lightning_send_payment(&payment, ssp_id);
1676 payment
1677 }
1678 None => payment_response.transfer.try_into()?,
1679 };
1680
1681 let Some(completion_timeout_secs) = completion_timeout_secs else {
1682 return Ok(SendPaymentResponse { payment });
1683 };
1684
1685 if completion_timeout_secs == 0 {
1686 return Ok(SendPaymentResponse { payment });
1687 }
1688
1689 let fut = self.wait_for_payment(WaitForPaymentRequest {
1690 identifier: WaitForPaymentIdentifier::PaymentId(payment.id.clone()),
1691 });
1692 let payment = match timeout(Duration::from_secs(completion_timeout_secs.into()), fut).await
1693 {
1694 Ok(res) => res?.payment,
1695 Err(_) => payment,
1697 };
1698
1699 Ok(SendPaymentResponse { payment })
1700 }
1701
1702 async fn send_bitcoin_address(
1703 &self,
1704 address: &BitcoinAddressDetails,
1705 fee_quote: &SendOnchainFeeQuote,
1706 request: &SendPaymentRequest,
1707 ) -> Result<SendPaymentResponse, SdkError> {
1708 let exit_speed: ExitSpeed = match &request.options {
1709 Some(SendPaymentOptions::BitcoinAddress { confirmation_speed }) => {
1710 confirmation_speed.clone().into()
1711 }
1712 None => ExitSpeed::Fast,
1713 _ => {
1714 return Err(SdkError::InvalidInput("Invalid options".to_string()));
1715 }
1716 };
1717 let response = self
1718 .spark_wallet
1719 .withdraw(
1720 &address.address,
1721 Some(request.prepare_response.amount.try_into()?),
1722 exit_speed,
1723 fee_quote.clone().into(),
1724 )
1725 .await?;
1726 Ok(SendPaymentResponse {
1727 payment: response.try_into()?,
1728 })
1729 }
1730
1731 fn poll_lightning_send_payment(&self, payment: &Payment, ssp_id: String) {
1733 const MAX_POLL_ATTEMPTS: u32 = 20;
1734 let payment_id = payment.id.clone();
1735 info!("Polling lightning send payment {}", payment_id);
1736
1737 let spark_wallet = self.spark_wallet.clone();
1738 let sync_trigger = self.sync_trigger.clone();
1739 let event_emitter = self.event_emitter.clone();
1740 let payment = payment.clone();
1741 let payment_id = payment_id.clone();
1742 let mut shutdown = self.shutdown_sender.subscribe();
1743
1744 tokio::spawn(async move {
1745 for i in 0..MAX_POLL_ATTEMPTS {
1746 info!(
1747 "Polling lightning send payment {} attempt {}",
1748 payment_id, i
1749 );
1750 select! {
1751 _ = shutdown.changed() => {
1752 info!("Shutdown signal received");
1753 return;
1754 },
1755 p = spark_wallet.fetch_lightning_send_payment(&ssp_id) => {
1756 if let Ok(Some(p)) = p && let Ok(payment) = Payment::from_lightning(p.clone(), payment.amount, payment.id.clone()) {
1757 info!("Polling payment status = {} {:?}", payment.status, p.status);
1758 if payment.status != PaymentStatus::Pending {
1759 info!("Polling payment completed status = {}", payment.status);
1760 emit_payment_status(&event_emitter, payment.clone()).await;
1761 if let Err(e) = sync_trigger.send(SyncRequest::payments_only(None)) {
1762 error!("Failed to send sync trigger: {e:?}");
1763 }
1764 return;
1765 }
1766 }
1767
1768 let sleep_time = if i < 5 {
1769 Duration::from_secs(1)
1770 } else {
1771 Duration::from_secs(i.into())
1772 };
1773 tokio::time::sleep(sleep_time).await;
1774 }
1775 }
1776 }
1777 });
1778 }
1779
1780 async fn recover_lightning_address(&self) -> Result<Option<LightningAddressInfo>, SdkError> {
1782 let cache = ObjectCacheRepository::new(self.storage.clone());
1783
1784 let Some(client) = &self.lnurl_server_client else {
1785 return Err(SdkError::Generic(
1786 "LNURL server is not configured".to_string(),
1787 ));
1788 };
1789 let resp = client.recover_lightning_address().await?;
1790
1791 let result = if let Some(resp) = resp {
1792 let address_info = resp.into();
1793 cache.save_lightning_address(&address_info).await?;
1794 Some(address_info)
1795 } else {
1796 cache.delete_lightning_address().await?;
1797 None
1798 };
1799
1800 Ok(result)
1801 }
1802}
1803
1804fn is_payment_match(payment: &Payment, request: &WaitForPaymentRequest) -> bool {
1805 match &request.identifier {
1806 WaitForPaymentIdentifier::PaymentId(payment_id) => payment.id == *payment_id,
1807 WaitForPaymentIdentifier::PaymentRequest(payment_request) => {
1808 if let Some(details) = &payment.details {
1809 match details {
1810 PaymentDetails::Lightning { invoice, .. } => {
1811 invoice.to_lowercase() == payment_request.to_lowercase()
1812 }
1813 PaymentDetails::Spark {
1814 invoice_details: invoice,
1815 }
1816 | PaymentDetails::Token {
1817 invoice_details: invoice,
1818 ..
1819 } => {
1820 if let Some(invoice) = invoice {
1821 invoice.invoice.to_lowercase() == payment_request.to_lowercase()
1822 } else {
1823 false
1824 }
1825 }
1826 PaymentDetails::Withdraw { tx_id: _ }
1827 | PaymentDetails::Deposit { tx_id: _ } => false,
1828 }
1829 } else {
1830 false
1831 }
1832 }
1833 }
1834}
1835
1836struct BalanceWatcher {
1837 spark_wallet: Arc<SparkWallet>,
1838 storage: Arc<dyn Storage>,
1839}
1840
1841impl BalanceWatcher {
1842 fn new(spark_wallet: Arc<SparkWallet>, storage: Arc<dyn Storage>) -> Self {
1843 Self {
1844 spark_wallet,
1845 storage,
1846 }
1847 }
1848}
1849
1850#[macros::async_trait]
1851impl EventListener for BalanceWatcher {
1852 async fn on_event(&self, event: SdkEvent) {
1853 match event {
1854 SdkEvent::PaymentSucceeded { .. } | SdkEvent::ClaimedDeposits { .. } => {
1855 match update_balances(self.spark_wallet.clone(), self.storage.clone()).await {
1856 Ok(()) => info!("Balance updated successfully"),
1857 Err(e) => error!("Failed to update balance: {e:?}"),
1858 }
1859 }
1860 _ => {}
1861 }
1862 }
1863}
1864
1865async fn update_balances(
1866 spark_wallet: Arc<SparkWallet>,
1867 storage: Arc<dyn Storage>,
1868) -> Result<(), SdkError> {
1869 let balance_sats = spark_wallet.get_balance().await?;
1870 let token_balances = spark_wallet
1871 .get_token_balances()
1872 .await?
1873 .into_iter()
1874 .map(|(k, v)| (k, v.into()))
1875 .collect();
1876 let object_repository = ObjectCacheRepository::new(storage.clone());
1877
1878 object_repository
1879 .save_account_info(&CachedAccountInfo {
1880 balance_sats,
1881 token_balances,
1882 })
1883 .await?;
1884 let identity_public_key = spark_wallet.get_identity_public_key();
1885 info!(
1886 "Balance updated successfully {} for identity {}",
1887 balance_sats, identity_public_key
1888 );
1889 Ok(())
1890}
1891
1892struct InternalEventListener {
1893 tx: mpsc::Sender<SdkEvent>,
1894}
1895
1896impl InternalEventListener {
1897 #[allow(unused)]
1898 pub fn new(tx: mpsc::Sender<SdkEvent>) -> Self {
1899 Self { tx }
1900 }
1901}
1902
1903#[macros::async_trait]
1904impl EventListener for InternalEventListener {
1905 async fn on_event(&self, event: SdkEvent) {
1906 let _ = self.tx.send(event).await;
1907 }
1908}
1909
1910fn process_success_action(
1911 payment: &Payment,
1912 success_action: Option<&SuccessAction>,
1913) -> Result<Option<SuccessActionProcessed>, LnurlError> {
1914 let Some(success_action) = success_action else {
1915 return Ok(None);
1916 };
1917
1918 let data = match success_action {
1919 SuccessAction::Aes { data } => data,
1920 SuccessAction::Message { data } => {
1921 return Ok(Some(SuccessActionProcessed::Message { data: data.clone() }));
1922 }
1923 SuccessAction::Url { data } => {
1924 return Ok(Some(SuccessActionProcessed::Url { data: data.clone() }));
1925 }
1926 };
1927
1928 let Some(PaymentDetails::Lightning { preimage, .. }) = &payment.details else {
1929 return Err(LnurlError::general(format!(
1930 "Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.",
1931 payment.details
1932 )));
1933 };
1934
1935 let Some(preimage) = preimage else {
1936 return Ok(None);
1937 };
1938
1939 let preimage =
1940 sha256::Hash::from_str(preimage).map_err(|_| LnurlError::general("Invalid preimage"))?;
1941 let preimage = preimage.as_byte_array();
1942 let result: AesSuccessActionDataResult = match (data, preimage).try_into() {
1943 Ok(data) => AesSuccessActionDataResult::Decrypted { data },
1944 Err(e) => AesSuccessActionDataResult::ErrorStatus {
1945 reason: e.to_string(),
1946 },
1947 };
1948
1949 Ok(Some(SuccessActionProcessed::Aes { result }))
1950}
1951
1952async fn emit_payment_status(event_emitter: &EventEmitter, payment: Payment) {
1953 match payment.status {
1954 PaymentStatus::Completed => {
1955 event_emitter
1956 .emit(&SdkEvent::PaymentSucceeded { payment })
1957 .await;
1958 }
1959 PaymentStatus::Failed => {
1960 event_emitter
1961 .emit(&SdkEvent::PaymentFailed { payment })
1962 .await;
1963 }
1964 PaymentStatus::Pending => {
1965 event_emitter
1966 .emit(&SdkEvent::PaymentPending { payment })
1967 .await;
1968 }
1969 }
1970}
1971
1972fn validate_breez_api_key(api_key: &str) -> Result<(), SdkError> {
1973 let api_key_decoded = base64::engine::general_purpose::STANDARD
1974 .decode(api_key.as_bytes())
1975 .map_err(|err| {
1976 SdkError::Generic(format!(
1977 "Could not base64 decode the Breez API key: {err:?}"
1978 ))
1979 })?;
1980 let (_rem, cert) = parse_x509_certificate(&api_key_decoded).map_err(|err| {
1981 SdkError::Generic(format!("Invalid certificate for Breez API key: {err:?}"))
1982 })?;
1983
1984 let issuer = cert
1985 .issuer()
1986 .iter_common_name()
1987 .next()
1988 .and_then(|cn| cn.as_str().ok());
1989 match issuer {
1990 Some(common_name) => {
1991 if !common_name.starts_with("Breez") {
1992 return Err(SdkError::Generic(
1993 "Invalid certificate found for Breez API key: issuer mismatch. Please confirm that the certificate's origin is trusted"
1994 .to_string()
1995 ));
1996 }
1997 }
1998 _ => {
1999 return Err(SdkError::Generic(
2000 "Could not parse Breez API key certificate: issuer is invalid or not found."
2001 .to_string(),
2002 ));
2003 }
2004 }
2005
2006 Ok(())
2007}