1#[cfg(all(
2 feature = "mysql",
3 not(all(target_family = "wasm", target_os = "unknown"))
4))]
5pub mod mysql;
6pub(crate) mod path;
7#[cfg(all(
8 feature = "postgres",
9 not(all(target_family = "wasm", target_os = "unknown"))
10))]
11pub mod postgres;
12#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
13pub(crate) mod sqlite;
14
15use std::{collections::HashMap, sync::Arc};
16
17use macros::async_trait;
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20
21use crate::{
22 AssetFilter, Contact, ConversionInfo, ConversionStatus, DepositClaimError, DepositInfo,
23 LightningAddressInfo, ListContactsRequest, ListPaymentsRequest, LnurlPayInfo,
24 LnurlWithdrawInfo, PaymentDetailsFilter, PaymentStatus, PaymentType, SparkHtlcStatus,
25 TokenBalance, TokenMetadata, TokenTransactionType,
26 models::Payment,
27 sync_storage::{IncomingChange, OutgoingChange, Record, UnversionedRecordChange},
28};
29
30const ACCOUNT_INFO_KEY: &str = "account_info";
31const LAST_SYNC_TIME_KEY: &str = "last_sync_time";
32pub(crate) const LIGHTNING_ADDRESS_KEY: &str = "lightning_address";
33const LNURL_METADATA_UPDATED_AFTER_KEY: &str = "lnurl_metadata_updated_after";
34const SYNC_OFFSET_KEY: &str = "sync_offset";
35const TX_CACHE_KEY: &str = "tx_cache";
36const TOKEN_METADATA_KEY_PREFIX: &str = "token_metadata_";
38const PAYMENT_METADATA_KEY_PREFIX: &str = "payment_metadata";
39const SPARK_PRIVATE_MODE_INITIALIZED_KEY: &str = "spark_private_mode_initialized";
40pub(crate) const STABLE_BALANCE_ACTIVE_LABEL_KEY: &str = "stable_balance_active_label";
41const PENDING_CONVERSIONS_KEY: &str = "pending_conversions";
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
46pub(crate) struct CachedLightningAddress {
47 pub address: Option<LightningAddressInfo>,
48 pub recovered: bool,
49}
50
51pub(crate) fn parse_cached_lightning_address(
53 value: &str,
54) -> Result<CachedLightningAddress, StorageError> {
55 serde_json::from_str(value).map_err(|e| StorageError::Serialization(e.to_string()))
56}
57
58#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
59pub enum UpdateDepositPayload {
60 ClaimError {
61 error: DepositClaimError,
62 },
63 Refund {
64 refund_txid: String,
65 refund_tx: String,
66 },
67}
68
69#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
70pub struct SetLnurlMetadataItem {
71 pub payment_hash: String,
72 pub sender_comment: Option<String>,
73 pub nostr_zap_request: Option<String>,
74 pub nostr_zap_receipt: Option<String>,
75}
76
77impl From<lnurl_models::ListMetadataMetadata> for SetLnurlMetadataItem {
78 fn from(value: lnurl_models::ListMetadataMetadata) -> Self {
79 SetLnurlMetadataItem {
80 payment_hash: value.payment_hash,
81 sender_comment: value.sender_comment,
82 nostr_zap_request: value.nostr_zap_request,
83 nostr_zap_receipt: value.nostr_zap_receipt,
84 }
85 }
86}
87
88#[derive(Debug, Error, Clone)]
90#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
91pub enum StorageError {
92 #[error("Connection error: {0}")]
95 Connection(String),
96
97 #[error("Underlying implementation error: {0}")]
98 Implementation(String),
99
100 #[error("Failed to initialize database: {0}")]
102 InitializationError(String),
103
104 #[error("Failed to serialize/deserialize data: {0}")]
105 Serialization(String),
106
107 #[error("Not found")]
108 NotFound,
109}
110
111impl From<serde_json::Error> for StorageError {
112 fn from(e: serde_json::Error) -> Self {
113 StorageError::Serialization(e.to_string())
114 }
115}
116
117impl From<std::num::TryFromIntError> for StorageError {
118 fn from(e: std::num::TryFromIntError) -> Self {
119 StorageError::Implementation(format!("integer overflow: {e}"))
120 }
121}
122
123#[derive(Debug, Clone)]
125#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
126pub enum StoragePaymentDetailsFilter {
127 Spark {
128 htlc_status: Option<Vec<SparkHtlcStatus>>,
129 conversion_refund_needed: Option<bool>,
130 },
131 Token {
132 conversion_refund_needed: Option<bool>,
133 tx_hash: Option<String>,
134 tx_type: Option<TokenTransactionType>,
135 },
136 Lightning {
137 htlc_status: Option<Vec<SparkHtlcStatus>>,
138 },
139}
140
141impl From<PaymentDetailsFilter> for StoragePaymentDetailsFilter {
142 fn from(filter: PaymentDetailsFilter) -> Self {
143 match filter {
144 PaymentDetailsFilter::Spark {
145 htlc_status,
146 conversion_refund_needed,
147 } => StoragePaymentDetailsFilter::Spark {
148 htlc_status,
149 conversion_refund_needed,
150 },
151 PaymentDetailsFilter::Token {
152 conversion_refund_needed,
153 tx_hash,
154 tx_type,
155 } => StoragePaymentDetailsFilter::Token {
156 conversion_refund_needed,
157 tx_hash,
158 tx_type,
159 },
160 PaymentDetailsFilter::Lightning { htlc_status } => {
161 StoragePaymentDetailsFilter::Lightning { htlc_status }
162 }
163 }
164 }
165}
166
167impl From<StoragePaymentDetailsFilter> for PaymentDetailsFilter {
168 fn from(filter: StoragePaymentDetailsFilter) -> Self {
169 match filter {
170 StoragePaymentDetailsFilter::Spark {
171 htlc_status,
172 conversion_refund_needed,
173 } => PaymentDetailsFilter::Spark {
174 htlc_status,
175 conversion_refund_needed,
176 },
177 StoragePaymentDetailsFilter::Token {
178 conversion_refund_needed,
179 tx_hash,
180 tx_type,
181 } => PaymentDetailsFilter::Token {
182 conversion_refund_needed,
183 tx_hash,
184 tx_type,
185 },
186 StoragePaymentDetailsFilter::Lightning { htlc_status } => {
187 PaymentDetailsFilter::Lightning { htlc_status }
188 }
189 }
190 }
191}
192
193#[derive(Debug, Clone, Default)]
196#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
197pub struct StorageListPaymentsRequest {
198 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
199 pub type_filter: Option<Vec<PaymentType>>,
200 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
201 pub status_filter: Option<Vec<PaymentStatus>>,
202 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
203 pub asset_filter: Option<AssetFilter>,
204 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
205 pub payment_details_filter: Option<Vec<StoragePaymentDetailsFilter>>,
206 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
207 pub from_timestamp: Option<u64>,
208 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
209 pub to_timestamp: Option<u64>,
210 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
211 pub offset: Option<u32>,
212 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
213 pub limit: Option<u32>,
214 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
215 pub sort_ascending: Option<bool>,
216}
217
218impl From<ListPaymentsRequest> for StorageListPaymentsRequest {
219 fn from(request: ListPaymentsRequest) -> Self {
220 StorageListPaymentsRequest {
221 type_filter: request.type_filter,
222 status_filter: request.status_filter,
223 asset_filter: request.asset_filter,
224 payment_details_filter: request
225 .payment_details_filter
226 .map(|filters| filters.into_iter().map(Into::into).collect()),
227 from_timestamp: request.from_timestamp,
228 to_timestamp: request.to_timestamp,
229 offset: request.offset,
230 limit: request.limit,
231 sort_ascending: request.sort_ascending,
232 }
233 }
234}
235
236impl From<StorageListPaymentsRequest> for ListPaymentsRequest {
237 fn from(request: StorageListPaymentsRequest) -> Self {
238 ListPaymentsRequest {
239 type_filter: request.type_filter,
240 status_filter: request.status_filter,
241 asset_filter: request.asset_filter,
242 payment_details_filter: request
243 .payment_details_filter
244 .map(|filters| filters.into_iter().map(Into::into).collect()),
245 from_timestamp: request.from_timestamp,
246 to_timestamp: request.to_timestamp,
247 offset: request.offset,
248 limit: request.limit,
249 sort_ascending: request.sort_ascending,
250 }
251 }
252}
253
254#[derive(Clone, Default, Deserialize, Serialize)]
256#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
257pub struct PaymentMetadata {
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub parent_payment_id: Option<String>,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub lnurl_pay_info: Option<LnurlPayInfo>,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub lnurl_withdraw_info: Option<LnurlWithdrawInfo>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub lnurl_description: Option<String>,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub conversion_info: Option<ConversionInfo>,
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub conversion_status: Option<ConversionStatus>,
270}
271
272#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
274#[async_trait]
275pub trait Storage: Send + Sync {
276 async fn delete_cached_item(&self, key: String) -> Result<(), StorageError>;
277 async fn get_cached_item(&self, key: String) -> Result<Option<String>, StorageError>;
278 async fn set_cached_item(&self, key: String, value: String) -> Result<(), StorageError>;
279 async fn list_payments(
289 &self,
290 request: StorageListPaymentsRequest,
291 ) -> Result<Vec<Payment>, StorageError>;
292
293 async fn insert_payment(&self, payment: Payment) -> Result<(), StorageError>;
303
304 async fn insert_payment_metadata(
315 &self,
316 payment_id: String,
317 metadata: PaymentMetadata,
318 ) -> Result<(), StorageError>;
319
320 async fn get_payment_by_id(&self, id: String) -> Result<Payment, StorageError>;
329
330 async fn get_payment_by_invoice(
338 &self,
339 invoice: String,
340 ) -> Result<Option<Payment>, StorageError>;
341
342 async fn get_payments_by_parent_ids(
353 &self,
354 parent_payment_ids: Vec<String>,
355 ) -> Result<HashMap<String, Vec<Payment>>, StorageError>;
356
357 async fn add_deposit(
369 &self,
370 txid: String,
371 vout: u32,
372 amount_sats: u64,
373 is_mature: bool,
374 ) -> Result<(), StorageError>;
375
376 async fn delete_deposit(&self, txid: String, vout: u32) -> Result<(), StorageError>;
386
387 async fn list_deposits(&self) -> Result<Vec<DepositInfo>, StorageError>;
392
393 async fn update_deposit(
404 &self,
405 txid: String,
406 vout: u32,
407 payload: UpdateDepositPayload,
408 ) -> Result<(), StorageError>;
409
410 async fn set_lnurl_metadata(
411 &self,
412 metadata: Vec<SetLnurlMetadataItem>,
413 ) -> Result<(), StorageError>;
414
415 async fn list_contacts(
417 &self,
418 request: ListContactsRequest,
419 ) -> Result<Vec<Contact>, StorageError>;
420
421 async fn get_contact(&self, id: String) -> Result<Contact, StorageError>;
423
424 async fn insert_contact(&self, contact: Contact) -> Result<(), StorageError>;
427
428 async fn delete_contact(&self, id: String) -> Result<(), StorageError>;
430
431 async fn add_outgoing_change(
433 &self,
434 record: UnversionedRecordChange,
435 ) -> Result<u64, StorageError>;
436 async fn complete_outgoing_sync(
437 &self,
438 record: Record,
439 local_revision: u64,
440 ) -> Result<(), StorageError>;
441 async fn get_pending_outgoing_changes(
442 &self,
443 limit: u32,
444 ) -> Result<Vec<OutgoingChange>, StorageError>;
445
446 async fn get_last_revision(&self) -> Result<u64, StorageError>;
453
454 async fn insert_incoming_records(&self, records: Vec<Record>) -> Result<(), StorageError>;
456
457 async fn delete_incoming_record(&self, record: Record) -> Result<(), StorageError>;
459
460 async fn get_incoming_records(&self, limit: u32) -> Result<Vec<IncomingChange>, StorageError>;
462
463 async fn get_latest_outgoing_change(&self) -> Result<Option<OutgoingChange>, StorageError>;
465
466 async fn update_record_from_incoming(&self, record: Record) -> Result<(), StorageError>;
468}
469
470pub(crate) struct ObjectCacheRepository {
471 storage: Arc<dyn Storage>,
472}
473
474impl ObjectCacheRepository {
475 pub(crate) fn new(storage: Arc<dyn Storage>) -> Self {
476 ObjectCacheRepository { storage }
477 }
478
479 pub(crate) async fn save_account_info(
480 &self,
481 value: &CachedAccountInfo,
482 ) -> Result<(), StorageError> {
483 self.storage
484 .set_cached_item(ACCOUNT_INFO_KEY.to_string(), serde_json::to_string(value)?)
485 .await?;
486 Ok(())
487 }
488
489 pub(crate) async fn fetch_account_info(
490 &self,
491 ) -> Result<Option<CachedAccountInfo>, StorageError> {
492 let value = self
493 .storage
494 .get_cached_item(ACCOUNT_INFO_KEY.to_string())
495 .await?;
496 match value {
497 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
498 None => Ok(None),
499 }
500 }
501
502 pub(crate) async fn save_sync_info(&self, value: &CachedSyncInfo) -> Result<(), StorageError> {
503 self.storage
504 .set_cached_item(SYNC_OFFSET_KEY.to_string(), serde_json::to_string(value)?)
505 .await?;
506 Ok(())
507 }
508
509 pub(crate) async fn fetch_sync_info(&self) -> Result<Option<CachedSyncInfo>, StorageError> {
510 let value = self
511 .storage
512 .get_cached_item(SYNC_OFFSET_KEY.to_string())
513 .await?;
514 match value {
515 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
516 None => Ok(None),
517 }
518 }
519
520 pub(crate) async fn save_tx(&self, txid: &str, value: &CachedTx) -> Result<(), StorageError> {
521 self.storage
522 .set_cached_item(
523 format!("{TX_CACHE_KEY}-{txid}"),
524 serde_json::to_string(value)?,
525 )
526 .await?;
527 Ok(())
528 }
529
530 pub(crate) async fn fetch_tx(&self, txid: &str) -> Result<Option<CachedTx>, StorageError> {
531 let value = self
532 .storage
533 .get_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
534 .await?;
535 match value {
536 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
537 None => Ok(None),
538 }
539 }
540
541 pub(crate) async fn save_lightning_address(
542 &self,
543 value: &LightningAddressInfo,
544 recovered: bool,
545 ) -> Result<(), StorageError> {
546 let cached = CachedLightningAddress {
547 address: Some(value.clone()),
548 recovered,
549 };
550 self.storage
551 .set_cached_item(
552 LIGHTNING_ADDRESS_KEY.to_string(),
553 serde_json::to_string(&cached)?,
554 )
555 .await?;
556 Ok(())
557 }
558
559 pub(crate) async fn delete_lightning_address(
561 &self,
562 recovered: bool,
563 ) -> Result<(), StorageError> {
564 let cached = CachedLightningAddress {
565 address: None,
566 recovered,
567 };
568 self.storage
569 .set_cached_item(
570 LIGHTNING_ADDRESS_KEY.to_string(),
571 serde_json::to_string(&cached)?,
572 )
573 .await?;
574 Ok(())
575 }
576
577 pub(crate) async fn fetch_lightning_address(
582 &self,
583 ) -> Result<Option<Option<LightningAddressInfo>>, StorageError> {
584 let value = self
585 .storage
586 .get_cached_item(LIGHTNING_ADDRESS_KEY.to_string())
587 .await?;
588 match value {
589 Some(value) => {
590 let cached = parse_cached_lightning_address(&value)?;
591 Ok(Some(cached.address))
592 }
593 None => Ok(None),
594 }
595 }
596
597 pub(crate) async fn save_token_metadata(
598 &self,
599 value: &TokenMetadata,
600 ) -> Result<(), StorageError> {
601 self.storage
602 .set_cached_item(
603 format!("{TOKEN_METADATA_KEY_PREFIX}{}", value.identifier),
604 serde_json::to_string(value)?,
605 )
606 .await?;
607 Ok(())
608 }
609
610 pub(crate) async fn fetch_token_metadata(
611 &self,
612 identifier: &str,
613 ) -> Result<Option<TokenMetadata>, StorageError> {
614 let value = self
615 .storage
616 .get_cached_item(format!("{TOKEN_METADATA_KEY_PREFIX}{identifier}"))
617 .await?;
618 match value {
619 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
620 None => Ok(None),
621 }
622 }
623
624 pub(crate) async fn save_payment_metadata(
625 &self,
626 identifier: &str,
627 value: &PaymentMetadata,
628 ) -> Result<(), StorageError> {
629 self.storage
630 .set_cached_item(
631 format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"),
632 serde_json::to_string(value)?,
633 )
634 .await?;
635 Ok(())
636 }
637
638 pub(crate) async fn fetch_payment_metadata(
639 &self,
640 identifier: &str,
641 ) -> Result<Option<PaymentMetadata>, StorageError> {
642 let value = self
643 .storage
644 .get_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"))
645 .await?;
646 match value {
647 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
648 None => Ok(None),
649 }
650 }
651
652 pub(crate) async fn delete_payment_metadata(
653 &self,
654 identifier: &str,
655 ) -> Result<(), StorageError> {
656 self.storage
657 .delete_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"))
658 .await?;
659 Ok(())
660 }
661
662 pub(crate) async fn save_spark_private_mode_initialized(&self) -> Result<(), StorageError> {
663 self.storage
664 .set_cached_item(
665 SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string(),
666 "true".to_string(),
667 )
668 .await?;
669 Ok(())
670 }
671
672 pub(crate) async fn fetch_spark_private_mode_initialized(&self) -> Result<bool, StorageError> {
673 let value = self
674 .storage
675 .get_cached_item(SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string())
676 .await?;
677 match value {
678 Some(value) => Ok(value == "true"),
679 None => Ok(false),
680 }
681 }
682
683 pub(crate) async fn save_stable_balance_active_label(
684 &self,
685 label: &str,
686 ) -> Result<(), StorageError> {
687 self.storage
688 .set_cached_item(
689 STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string(),
690 label.to_string(),
691 )
692 .await
693 }
694
695 pub(crate) async fn fetch_stable_balance_active_label(
696 &self,
697 ) -> Result<Option<String>, StorageError> {
698 self.storage
699 .get_cached_item(STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string())
700 .await
701 }
702
703 pub(crate) async fn delete_stable_balance_active_label(&self) -> Result<(), StorageError> {
704 self.storage
705 .delete_cached_item(STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string())
706 .await
707 }
708
709 pub(crate) async fn save_pending_conversions(
710 &self,
711 pending: &[super::stable_balance::PendingConversion],
712 ) -> Result<(), StorageError> {
713 self.storage
714 .set_cached_item(
715 PENDING_CONVERSIONS_KEY.to_string(),
716 serde_json::to_string(pending)?,
717 )
718 .await?;
719 Ok(())
720 }
721
722 pub(crate) async fn fetch_pending_conversions(
723 &self,
724 ) -> Result<Option<Vec<super::stable_balance::PendingConversion>>, StorageError> {
725 let value = self
726 .storage
727 .get_cached_item(PENDING_CONVERSIONS_KEY.to_string())
728 .await?;
729 match value {
730 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
731 None => Ok(None),
732 }
733 }
734
735 pub(crate) async fn delete_pending_conversions(&self) -> Result<(), StorageError> {
736 self.storage
737 .delete_cached_item(PENDING_CONVERSIONS_KEY.to_string())
738 .await
739 }
740
741 pub(crate) async fn save_lnurl_metadata_updated_after(
742 &self,
743 offset: i64,
744 ) -> Result<(), StorageError> {
745 self.storage
746 .set_cached_item(
747 LNURL_METADATA_UPDATED_AFTER_KEY.to_string(),
748 offset.to_string(),
749 )
750 .await?;
751 Ok(())
752 }
753
754 pub(crate) async fn fetch_lnurl_metadata_updated_after(&self) -> Result<i64, StorageError> {
755 let value = self
756 .storage
757 .get_cached_item(LNURL_METADATA_UPDATED_AFTER_KEY.to_string())
758 .await?;
759 match value {
760 Some(value) => Ok(value.parse().map_err(|_| {
761 StorageError::Serialization("invalid lnurl_metadata_updated_after".to_string())
762 })?),
763 None => Ok(0),
764 }
765 }
766
767 pub(crate) async fn get_last_sync_time(&self) -> Result<Option<u64>, StorageError> {
768 let value = self
769 .storage
770 .get_cached_item(LAST_SYNC_TIME_KEY.to_string())
771 .await?;
772 match value {
773 Some(v) => Ok(Some(v.parse().map_err(|_| {
774 StorageError::Serialization("invalid last_sync_time".to_string())
775 })?)),
776 None => Ok(None),
777 }
778 }
779
780 pub(crate) async fn set_last_sync_time(&self, time: u64) -> Result<(), StorageError> {
781 self.storage
782 .set_cached_item(LAST_SYNC_TIME_KEY.to_string(), time.to_string())
783 .await
784 }
785}
786
787#[derive(Serialize, Deserialize, Default)]
788pub(crate) struct CachedAccountInfo {
789 pub(crate) balance_sats: u64,
790 #[serde(default)]
791 pub(crate) token_balances: HashMap<String, TokenBalance>,
792}
793
794#[derive(Serialize, Deserialize, Default)]
795pub(crate) struct CachedSyncInfo {
796 pub(crate) offset: u64,
797 pub(crate) last_synced_final_token_payment_id: Option<String>,
798}
799
800#[derive(Serialize, Deserialize, Default)]
801pub(crate) struct CachedTx {
802 pub(crate) raw_tx: String,
803}
804
805#[cfg(feature = "test-utils")]
806pub mod tests;