1pub(crate) mod backend;
2#[cfg(feature = "mysql")]
3pub mod mysql;
4pub(crate) mod path;
5#[cfg(feature = "postgres")]
6pub mod postgres;
7#[cfg(feature = "sqlite")]
8pub(crate) mod sqlite;
9
10#[cfg(all(
14 any(feature = "sqlite", feature = "postgres", feature = "mysql"),
15 target_family = "wasm",
16 target_os = "unknown"
17))]
18compile_error!(
19 "the `sqlite`, `postgres` and `mysql` storage features are native-only and \
20 cannot be enabled for the wasm32 target"
21);
22
23use std::{collections::HashMap, sync::Arc};
24
25use macros::async_trait;
26use serde::{Deserialize, Serialize};
27use thiserror::Error;
28
29use crate::{
30 AssetFilter, Contact, ConversionInfo, ConversionStatus, DepositClaimError, DepositInfo,
31 InstantClaimStatus, LightningAddressInfo, ListContactsRequest, ListPaymentsRequest,
32 LnurlPayInfo, LnurlWithdrawInfo, PaymentDetailsFilter, PaymentStatus, PaymentType, RefundState,
33 SparkHtlcStatus, TokenBalance, TokenMetadata, TokenTransactionType,
34 models::Payment,
35 sync_storage::{IncomingChange, OutgoingChange, Record, UnversionedRecordChange},
36};
37
38const ACCOUNT_INFO_KEY: &str = "account_info";
39const LAST_SYNC_TIME_KEY: &str = "last_sync_time";
40pub(crate) const LIGHTNING_ADDRESS_KEY: &str = "lightning_address";
41const LNURL_METADATA_UPDATED_AFTER_KEY: &str = "lnurl_metadata_updated_after";
42const SYNC_OFFSET_KEY: &str = "sync_offset";
43const TX_CACHE_KEY: &str = "tx_cache";
44const TOKEN_METADATA_KEY_PREFIX: &str = "token_metadata_";
46const PAYMENT_METADATA_KEY_PREFIX: &str = "payment_metadata";
47const PUBLISHED_PACKAGE_KEY_PREFIX: &str = "published_package_";
48const SPARK_PRIVATE_MODE_INITIALIZED_KEY: &str = "spark_private_mode_initialized";
49pub(crate) const STABLE_BALANCE_ACTIVE_LABEL_KEY: &str = "stable_balance_active_label";
50const PENDING_CONVERSIONS_KEY: &str = "pending_conversions";
51const PENDING_LIGHTNING_SENDS_KEY: &str = "pending_lightning_sends";
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
56pub(crate) struct CachedLightningAddress {
57 pub address: Option<LightningAddressInfo>,
58 pub recovered: bool,
59}
60
61pub(crate) fn parse_cached_lightning_address(
63 value: &str,
64) -> Result<CachedLightningAddress, StorageError> {
65 serde_json::from_str(value).map_err(|e| StorageError::Serialization(e.to_string()))
66}
67
68#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
69pub enum UpdateDepositPayload {
70 ClaimError {
71 error: DepositClaimError,
72 },
73 Refund {
74 refund_txid: String,
75 refund_tx: String,
76 state: RefundState,
77 },
78 InstantClaim {
79 status: InstantClaimStatus,
80 },
81 RefundBroadcastState {
86 refund_txid: String,
87 state: RefundState,
88 },
89}
90
91#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
92pub struct SetLnurlMetadataItem {
93 pub payment_hash: String,
94 pub sender_comment: Option<String>,
95 pub nostr_zap_request: Option<String>,
96 pub nostr_zap_receipt: Option<String>,
97}
98
99impl From<lnurl_models::ListMetadataMetadata> for SetLnurlMetadataItem {
100 fn from(value: lnurl_models::ListMetadataMetadata) -> Self {
101 SetLnurlMetadataItem {
102 payment_hash: value.payment_hash,
103 sender_comment: value.sender_comment,
104 nostr_zap_request: value.nostr_zap_request,
105 nostr_zap_receipt: value.nostr_zap_receipt,
106 }
107 }
108}
109
110#[derive(Debug, Error, Clone)]
112#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
113pub enum StorageError {
114 #[error("Connection error: {0}")]
117 Connection(String),
118
119 #[error("Underlying implementation error: {0}")]
120 Implementation(String),
121
122 #[error("Failed to initialize database: {0}")]
124 InitializationError(String),
125
126 #[error("Failed to serialize/deserialize data: {0}")]
127 Serialization(String),
128
129 #[error("Not found")]
130 NotFound,
131}
132
133impl From<serde_json::Error> for StorageError {
134 fn from(e: serde_json::Error) -> Self {
135 StorageError::Serialization(e.to_string())
136 }
137}
138
139impl From<std::num::TryFromIntError> for StorageError {
140 fn from(e: std::num::TryFromIntError) -> Self {
141 StorageError::Implementation(format!("integer overflow: {e}"))
142 }
143}
144
145#[derive(Debug, Clone)]
147#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
148pub enum ConversionFilter {
149 AmmRefundNeeded,
151 OrchestraPending,
153 BoltzPending,
157}
158
159#[derive(Debug, Clone)]
161#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
162pub enum StoragePaymentDetailsFilter {
163 Spark {
164 htlc_status: Option<Vec<SparkHtlcStatus>>,
165 conversion_filter: Option<ConversionFilter>,
166 },
167 Token {
168 conversion_filter: Option<ConversionFilter>,
169 tx_hash: Option<String>,
170 tx_type: Option<TokenTransactionType>,
171 },
172 Lightning {
173 htlc_status: Option<Vec<SparkHtlcStatus>>,
174 conversion_filter: Option<ConversionFilter>,
175 },
176}
177
178impl From<PaymentDetailsFilter> for StoragePaymentDetailsFilter {
179 fn from(filter: PaymentDetailsFilter) -> Self {
180 match filter {
181 PaymentDetailsFilter::Spark {
182 htlc_status,
183 conversion_refund_needed,
184 } => StoragePaymentDetailsFilter::Spark {
185 htlc_status,
186 conversion_filter: conversion_refund_needed
187 .and_then(|v| v.then_some(ConversionFilter::AmmRefundNeeded)),
188 },
189 PaymentDetailsFilter::Token {
190 conversion_refund_needed,
191 tx_hash,
192 tx_type,
193 } => StoragePaymentDetailsFilter::Token {
194 conversion_filter: conversion_refund_needed
195 .and_then(|v| v.then_some(ConversionFilter::AmmRefundNeeded)),
196 tx_hash,
197 tx_type,
198 },
199 PaymentDetailsFilter::Lightning { htlc_status } => {
200 StoragePaymentDetailsFilter::Lightning {
201 htlc_status,
202 conversion_filter: None,
203 }
204 }
205 }
206 }
207}
208
209impl From<StoragePaymentDetailsFilter> for PaymentDetailsFilter {
210 fn from(filter: StoragePaymentDetailsFilter) -> Self {
211 match filter {
212 StoragePaymentDetailsFilter::Spark {
213 htlc_status,
214 conversion_filter,
215 } => PaymentDetailsFilter::Spark {
216 htlc_status,
217 conversion_refund_needed: conversion_filter
218 .map(|f| matches!(f, ConversionFilter::AmmRefundNeeded)),
219 },
220 StoragePaymentDetailsFilter::Token {
221 conversion_filter,
222 tx_hash,
223 tx_type,
224 } => PaymentDetailsFilter::Token {
225 conversion_refund_needed: conversion_filter
226 .map(|f| matches!(f, ConversionFilter::AmmRefundNeeded)),
227 tx_hash,
228 tx_type,
229 },
230 StoragePaymentDetailsFilter::Lightning { htlc_status, .. } => {
231 PaymentDetailsFilter::Lightning { htlc_status }
232 }
233 }
234 }
235}
236
237#[derive(Debug, Clone, Default)]
240#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
241pub struct StorageListPaymentsRequest {
242 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
243 pub type_filter: Option<Vec<PaymentType>>,
244 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
245 pub status_filter: Option<Vec<PaymentStatus>>,
246 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
247 pub asset_filter: Option<AssetFilter>,
248 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
249 pub payment_details_filter: Option<Vec<StoragePaymentDetailsFilter>>,
250 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
251 pub from_timestamp: Option<u64>,
252 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
253 pub to_timestamp: Option<u64>,
254 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
255 pub offset: Option<u32>,
256 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
257 pub limit: Option<u32>,
258 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
259 pub sort_ascending: Option<bool>,
260}
261
262impl From<ListPaymentsRequest> for StorageListPaymentsRequest {
263 fn from(request: ListPaymentsRequest) -> Self {
264 StorageListPaymentsRequest {
265 type_filter: request.type_filter,
266 status_filter: request.status_filter,
267 asset_filter: request.asset_filter,
268 payment_details_filter: request
269 .payment_details_filter
270 .map(|filters| filters.into_iter().map(Into::into).collect()),
271 from_timestamp: request.from_timestamp,
272 to_timestamp: request.to_timestamp,
273 offset: request.offset,
274 limit: request.limit,
275 sort_ascending: request.sort_ascending,
276 }
277 }
278}
279
280impl From<StorageListPaymentsRequest> for ListPaymentsRequest {
281 fn from(request: StorageListPaymentsRequest) -> Self {
282 ListPaymentsRequest {
283 type_filter: request.type_filter,
284 status_filter: request.status_filter,
285 asset_filter: request.asset_filter,
286 payment_details_filter: request
287 .payment_details_filter
288 .map(|filters| filters.into_iter().map(Into::into).collect()),
289 from_timestamp: request.from_timestamp,
290 to_timestamp: request.to_timestamp,
291 offset: request.offset,
292 limit: request.limit,
293 sort_ascending: request.sort_ascending,
294 }
295 }
296}
297
298#[derive(Clone, Default, Deserialize, Serialize)]
300#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
301pub struct PaymentMetadata {
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub parent_payment_id: Option<String>,
304 #[serde(skip_serializing_if = "Option::is_none")]
305 pub lnurl_pay_info: Option<LnurlPayInfo>,
306 #[serde(skip_serializing_if = "Option::is_none")]
307 pub lnurl_withdraw_info: Option<LnurlWithdrawInfo>,
308 #[serde(skip_serializing_if = "Option::is_none")]
309 pub lnurl_description: Option<String>,
310 #[serde(
313 skip_serializing_if = "Option::is_none",
314 deserialize_with = "deserialize_conversion_info_with_default_type",
315 default
316 )]
317 pub conversion_info: Option<ConversionInfo>,
318 #[serde(skip_serializing_if = "Option::is_none")]
319 pub conversion_status: Option<ConversionStatus>,
320}
321
322#[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
323pub(crate) fn parse_payment_status(value: &str) -> Result<PaymentStatus, StorageError> {
324 value
325 .parse()
326 .map_err(|e| StorageError::Implementation(format!("invalid payment status: {e}")))
327}
328
329fn deserialize_conversion_info_with_default_type<'de, D>(
334 deserializer: D,
335) -> Result<Option<ConversionInfo>, D::Error>
336where
337 D: serde::Deserializer<'de>,
338{
339 use serde::de::Deserialize;
340
341 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
342 match value {
343 None => Ok(None),
344 Some(mut v) => {
345 if let Some(obj) = v.as_object_mut() {
346 obj.entry("type")
347 .or_insert_with(|| serde_json::Value::String("amm".to_string()));
348 }
349 match serde_json::from_value::<ConversionInfo>(v) {
350 Ok(info) => Ok(Some(info)),
351 Err(_) => Ok(None),
352 }
353 }
354 }
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
366#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
367#[serde(rename_all = "camelCase")]
368pub struct StoredCrossChainSwap {
369 pub provider: String,
371 pub id: String,
373 pub is_terminal: bool,
376 pub updated_at: u64,
379 pub data: String,
381 pub secrets: String,
384}
385
386#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
388#[async_trait]
389pub trait Storage: Send + Sync {
390 async fn delete_cached_item(&self, key: String) -> Result<(), StorageError>;
391 async fn get_cached_item(&self, key: String) -> Result<Option<String>, StorageError>;
392 async fn set_cached_item(&self, key: String, value: String) -> Result<(), StorageError>;
393 async fn list_payments(
403 &self,
404 request: StorageListPaymentsRequest,
405 ) -> Result<Vec<Payment>, StorageError>;
406
407 async fn apply_payment_update(&self, payment: Payment) -> Result<bool, StorageError>;
416
417 async fn insert_payment_metadata(
428 &self,
429 payment_id: String,
430 metadata: PaymentMetadata,
431 ) -> Result<(), StorageError>;
432
433 async fn get_payment_by_id(&self, id: String) -> Result<Payment, StorageError>;
442
443 async fn get_payment_by_invoice(
451 &self,
452 invoice: String,
453 ) -> Result<Option<Payment>, StorageError>;
454
455 async fn get_payments_by_parent_ids(
466 &self,
467 parent_payment_ids: Vec<String>,
468 ) -> Result<HashMap<String, Vec<Payment>>, StorageError>;
469
470 async fn add_deposit(
482 &self,
483 txid: String,
484 vout: u32,
485 amount_sats: u64,
486 is_mature: bool,
487 ) -> Result<(), StorageError>;
488
489 async fn delete_deposit(&self, txid: String, vout: u32) -> Result<(), StorageError>;
499
500 async fn list_deposits(&self) -> Result<Vec<DepositInfo>, StorageError>;
505
506 async fn update_deposit(
517 &self,
518 txid: String,
519 vout: u32,
520 payload: UpdateDepositPayload,
521 ) -> Result<(), StorageError>;
522
523 async fn set_lnurl_metadata(
524 &self,
525 metadata: Vec<SetLnurlMetadataItem>,
526 ) -> Result<(), StorageError>;
527
528 async fn list_contacts(
530 &self,
531 request: ListContactsRequest,
532 ) -> Result<Vec<Contact>, StorageError>;
533
534 async fn get_contact(&self, id: String) -> Result<Contact, StorageError>;
536
537 async fn insert_contact(&self, contact: Contact) -> Result<(), StorageError>;
540
541 async fn delete_contact(&self, id: String) -> Result<(), StorageError>;
543
544 async fn set_cross_chain_swap(&self, swap: StoredCrossChainSwap) -> Result<(), StorageError>;
546
547 async fn get_cross_chain_swap(
549 &self,
550 provider: String,
551 id: String,
552 ) -> Result<Option<StoredCrossChainSwap>, StorageError>;
553
554 async fn list_active_cross_chain_swaps(
557 &self,
558 provider: String,
559 ) -> Result<Vec<StoredCrossChainSwap>, StorageError>;
560
561 async fn add_outgoing_change(
563 &self,
564 record: UnversionedRecordChange,
565 ) -> Result<u64, StorageError>;
566 async fn complete_outgoing_sync(
567 &self,
568 record: Record,
569 local_revision: u64,
570 ) -> Result<(), StorageError>;
571 async fn get_pending_outgoing_changes(
572 &self,
573 limit: u32,
574 ) -> Result<Vec<OutgoingChange>, StorageError>;
575
576 async fn get_last_revision(&self) -> Result<u64, StorageError>;
583
584 async fn insert_incoming_records(&self, records: Vec<Record>) -> Result<(), StorageError>;
586
587 async fn delete_incoming_record(&self, record: Record) -> Result<(), StorageError>;
589
590 async fn get_incoming_records(&self, limit: u32) -> Result<Vec<IncomingChange>, StorageError>;
592
593 async fn get_latest_outgoing_change(&self) -> Result<Option<OutgoingChange>, StorageError>;
595
596 async fn update_record_from_incoming(&self, record: Record) -> Result<(), StorageError>;
598}
599
600pub(crate) struct ObjectCacheRepository {
601 storage: Arc<dyn Storage>,
602}
603
604impl ObjectCacheRepository {
605 pub(crate) fn new(storage: Arc<dyn Storage>) -> Self {
606 ObjectCacheRepository { storage }
607 }
608
609 pub(crate) async fn save_account_info(
610 &self,
611 value: &CachedAccountInfo,
612 ) -> Result<(), StorageError> {
613 self.storage
614 .set_cached_item(ACCOUNT_INFO_KEY.to_string(), serde_json::to_string(value)?)
615 .await?;
616 Ok(())
617 }
618
619 pub(crate) async fn fetch_account_info(
620 &self,
621 ) -> Result<Option<CachedAccountInfo>, StorageError> {
622 let value = self
623 .storage
624 .get_cached_item(ACCOUNT_INFO_KEY.to_string())
625 .await?;
626 match value {
627 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
628 None => Ok(None),
629 }
630 }
631
632 pub(crate) async fn save_sync_info(&self, value: &CachedSyncInfo) -> Result<(), StorageError> {
633 self.storage
634 .set_cached_item(SYNC_OFFSET_KEY.to_string(), serde_json::to_string(value)?)
635 .await?;
636 Ok(())
637 }
638
639 pub(crate) async fn fetch_sync_info(&self) -> Result<Option<CachedSyncInfo>, StorageError> {
640 let value = self
641 .storage
642 .get_cached_item(SYNC_OFFSET_KEY.to_string())
643 .await?;
644 match value {
645 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
646 None => Ok(None),
647 }
648 }
649
650 pub(crate) async fn save_published_package(
655 &self,
656 package_id: &str,
657 payment_id: &str,
658 ) -> Result<(), StorageError> {
659 self.storage
660 .set_cached_item(
661 format!("{PUBLISHED_PACKAGE_KEY_PREFIX}{package_id}"),
662 payment_id.to_string(),
663 )
664 .await?;
665 Ok(())
666 }
667
668 pub(crate) async fn fetch_published_package(
669 &self,
670 package_id: &str,
671 ) -> Result<Option<String>, StorageError> {
672 self.storage
673 .get_cached_item(format!("{PUBLISHED_PACKAGE_KEY_PREFIX}{package_id}"))
674 .await
675 }
676
677 pub(crate) async fn save_tx(&self, txid: &str, value: &CachedTx) -> Result<(), StorageError> {
678 self.storage
679 .set_cached_item(
680 format!("{TX_CACHE_KEY}-{txid}"),
681 serde_json::to_string(value)?,
682 )
683 .await?;
684 Ok(())
685 }
686
687 pub(crate) async fn delete_tx(&self, txid: &str) -> Result<(), StorageError> {
688 self.storage
689 .delete_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
690 .await
691 }
692
693 pub(crate) async fn fetch_tx(&self, txid: &str) -> Result<Option<CachedTx>, StorageError> {
694 let value = self
695 .storage
696 .get_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
697 .await?;
698 match value {
699 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
700 None => Ok(None),
701 }
702 }
703
704 pub(crate) async fn save_lightning_address(
705 &self,
706 value: &LightningAddressInfo,
707 recovered: bool,
708 ) -> Result<(), StorageError> {
709 let cached = CachedLightningAddress {
710 address: Some(value.clone()),
711 recovered,
712 };
713 self.storage
714 .set_cached_item(
715 LIGHTNING_ADDRESS_KEY.to_string(),
716 serde_json::to_string(&cached)?,
717 )
718 .await?;
719 Ok(())
720 }
721
722 pub(crate) async fn delete_lightning_address(
724 &self,
725 recovered: bool,
726 ) -> Result<(), StorageError> {
727 let cached = CachedLightningAddress {
728 address: None,
729 recovered,
730 };
731 self.storage
732 .set_cached_item(
733 LIGHTNING_ADDRESS_KEY.to_string(),
734 serde_json::to_string(&cached)?,
735 )
736 .await?;
737 Ok(())
738 }
739
740 pub(crate) async fn fetch_lightning_address(
745 &self,
746 ) -> Result<Option<Option<LightningAddressInfo>>, StorageError> {
747 let value = self
748 .storage
749 .get_cached_item(LIGHTNING_ADDRESS_KEY.to_string())
750 .await?;
751 match value {
752 Some(value) => {
753 let cached = parse_cached_lightning_address(&value)?;
754 Ok(Some(cached.address))
755 }
756 None => Ok(None),
757 }
758 }
759
760 pub(crate) async fn save_token_metadata(
761 &self,
762 value: &TokenMetadata,
763 ) -> Result<(), StorageError> {
764 self.storage
765 .set_cached_item(
766 format!("{TOKEN_METADATA_KEY_PREFIX}{}", value.identifier),
767 serde_json::to_string(value)?,
768 )
769 .await?;
770 Ok(())
771 }
772
773 pub(crate) async fn fetch_token_metadata(
774 &self,
775 identifier: &str,
776 ) -> Result<Option<TokenMetadata>, StorageError> {
777 let value = self
778 .storage
779 .get_cached_item(format!("{TOKEN_METADATA_KEY_PREFIX}{identifier}"))
780 .await?;
781 match value {
782 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
783 None => Ok(None),
784 }
785 }
786
787 pub(crate) async fn save_payment_metadata(
788 &self,
789 identifier: &str,
790 value: &PaymentMetadata,
791 ) -> Result<(), StorageError> {
792 self.storage
793 .set_cached_item(
794 format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"),
795 serde_json::to_string(value)?,
796 )
797 .await?;
798 Ok(())
799 }
800
801 pub(crate) async fn fetch_payment_metadata(
802 &self,
803 identifier: &str,
804 ) -> Result<Option<PaymentMetadata>, StorageError> {
805 let value = self
806 .storage
807 .get_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"))
808 .await?;
809 match value {
810 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
811 None => Ok(None),
812 }
813 }
814
815 pub(crate) async fn delete_payment_metadata(
816 &self,
817 identifier: &str,
818 ) -> Result<(), StorageError> {
819 self.storage
820 .delete_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"))
821 .await?;
822 Ok(())
823 }
824
825 pub(crate) async fn save_spark_private_mode_initialized(&self) -> Result<(), StorageError> {
826 self.storage
827 .set_cached_item(
828 SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string(),
829 "true".to_string(),
830 )
831 .await?;
832 Ok(())
833 }
834
835 pub(crate) async fn fetch_spark_private_mode_initialized(&self) -> Result<bool, StorageError> {
836 let value = self
837 .storage
838 .get_cached_item(SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string())
839 .await?;
840 match value {
841 Some(value) => Ok(value == "true"),
842 None => Ok(false),
843 }
844 }
845
846 pub(crate) async fn save_stable_balance_active_label(
847 &self,
848 label: &str,
849 ) -> Result<(), StorageError> {
850 self.storage
851 .set_cached_item(
852 STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string(),
853 label.to_string(),
854 )
855 .await
856 }
857
858 pub(crate) async fn fetch_stable_balance_active_label(
859 &self,
860 ) -> Result<Option<String>, StorageError> {
861 self.storage
862 .get_cached_item(STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string())
863 .await
864 }
865
866 pub(crate) async fn delete_stable_balance_active_label(&self) -> Result<(), StorageError> {
867 self.storage
868 .delete_cached_item(STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string())
869 .await
870 }
871
872 pub(crate) async fn save_pending_lightning_sends(
877 &self,
878 pending: &[super::sdk::PendingLightningSend],
879 ) -> Result<(), StorageError> {
880 self.storage
881 .set_cached_item(
882 PENDING_LIGHTNING_SENDS_KEY.to_string(),
883 serde_json::to_string(pending)?,
884 )
885 .await?;
886 Ok(())
887 }
888
889 pub(crate) async fn fetch_pending_lightning_sends(
890 &self,
891 ) -> Result<Vec<super::sdk::PendingLightningSend>, StorageError> {
892 let value = self
893 .storage
894 .get_cached_item(PENDING_LIGHTNING_SENDS_KEY.to_string())
895 .await?;
896 match value {
897 Some(value) => Ok(serde_json::from_str(&value)?),
898 None => Ok(Vec::new()),
899 }
900 }
901
902 pub(crate) async fn save_pending_conversions(
903 &self,
904 pending: &[super::stable_balance::PendingConversion],
905 ) -> Result<(), StorageError> {
906 self.storage
907 .set_cached_item(
908 PENDING_CONVERSIONS_KEY.to_string(),
909 serde_json::to_string(pending)?,
910 )
911 .await?;
912 Ok(())
913 }
914
915 pub(crate) async fn fetch_pending_conversions(
916 &self,
917 ) -> Result<Option<Vec<super::stable_balance::PendingConversion>>, StorageError> {
918 let value = self
919 .storage
920 .get_cached_item(PENDING_CONVERSIONS_KEY.to_string())
921 .await?;
922 match value {
923 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
924 None => Ok(None),
925 }
926 }
927
928 pub(crate) async fn delete_pending_conversions(&self) -> Result<(), StorageError> {
929 self.storage
930 .delete_cached_item(PENDING_CONVERSIONS_KEY.to_string())
931 .await
932 }
933
934 pub(crate) async fn save_lnurl_metadata_updated_after(
935 &self,
936 offset: i64,
937 ) -> Result<(), StorageError> {
938 self.storage
939 .set_cached_item(
940 LNURL_METADATA_UPDATED_AFTER_KEY.to_string(),
941 offset.to_string(),
942 )
943 .await?;
944 Ok(())
945 }
946
947 pub(crate) async fn fetch_lnurl_metadata_updated_after(&self) -> Result<i64, StorageError> {
948 let value = self
949 .storage
950 .get_cached_item(LNURL_METADATA_UPDATED_AFTER_KEY.to_string())
951 .await?;
952 match value {
953 Some(value) => Ok(value.parse().map_err(|_| {
954 StorageError::Serialization("invalid lnurl_metadata_updated_after".to_string())
955 })?),
956 None => Ok(0),
957 }
958 }
959
960 pub(crate) async fn get_last_sync_time(&self) -> Result<Option<u64>, StorageError> {
961 let value = self
962 .storage
963 .get_cached_item(LAST_SYNC_TIME_KEY.to_string())
964 .await?;
965 match value {
966 Some(v) => Ok(Some(v.parse().map_err(|_| {
967 StorageError::Serialization("invalid last_sync_time".to_string())
968 })?)),
969 None => Ok(None),
970 }
971 }
972
973 pub(crate) async fn set_last_sync_time(&self, time: u64) -> Result<(), StorageError> {
974 self.storage
975 .set_cached_item(LAST_SYNC_TIME_KEY.to_string(), time.to_string())
976 .await
977 }
978}
979
980#[derive(Serialize, Deserialize, Default)]
981pub(crate) struct CachedAccountInfo {
982 pub(crate) balance_sats: u64,
983 #[serde(default)]
984 pub(crate) token_balances: HashMap<String, TokenBalance>,
985}
986
987#[derive(Serialize, Deserialize, Default)]
988pub(crate) struct CachedSyncInfo {
989 pub(crate) offset: u64,
990 pub(crate) last_synced_final_token_payment_id: Option<String>,
991}
992
993#[derive(Serialize, Deserialize, Default)]
994pub(crate) struct CachedTx {
995 pub(crate) raw_tx: String,
996}
997
998#[cfg(feature = "test-utils")]
999pub mod tests;