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