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, MaxFee, PaymentDetailsFilter, PaymentStatus, PaymentType,
33 RefundState, 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 MaxClaimFee {
93 max_fee: Option<MaxFee>,
94 },
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
100pub struct WatchedDepositAddress {
101 pub address: String,
102 pub issued_at: u64,
105 pub seen: bool,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
111pub enum UpdateWatchedAddressPayload {
112 Watch { issued_at: u64 },
115 Seen,
117 Unwatch { issued_at: u64 },
121}
122
123#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
124pub struct SetLnurlMetadataItem {
125 pub payment_hash: String,
126 pub sender_comment: Option<String>,
127 pub nostr_zap_request: Option<String>,
128 pub nostr_zap_receipt: Option<String>,
129}
130
131impl From<lnurl_models::ListMetadataMetadata> for SetLnurlMetadataItem {
132 fn from(value: lnurl_models::ListMetadataMetadata) -> Self {
133 SetLnurlMetadataItem {
134 payment_hash: value.payment_hash,
135 sender_comment: value.sender_comment,
136 nostr_zap_request: value.nostr_zap_request,
137 nostr_zap_receipt: value.nostr_zap_receipt,
138 }
139 }
140}
141
142#[derive(Debug, Error, Clone)]
144#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
145pub enum StorageError {
146 #[error("Connection error: {0}")]
149 Connection(String),
150
151 #[error("Underlying implementation error: {0}")]
152 Implementation(String),
153
154 #[error("Failed to initialize database: {0}")]
156 InitializationError(String),
157
158 #[error("Failed to serialize/deserialize data: {0}")]
159 Serialization(String),
160
161 #[error("Not found")]
162 NotFound,
163}
164
165impl From<serde_json::Error> for StorageError {
166 fn from(e: serde_json::Error) -> Self {
167 StorageError::Serialization(e.to_string())
168 }
169}
170
171impl From<std::num::TryFromIntError> for StorageError {
172 fn from(e: std::num::TryFromIntError) -> Self {
173 StorageError::Implementation(format!("integer overflow: {e}"))
174 }
175}
176
177#[derive(Debug, Clone)]
179#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
180pub enum ConversionFilter {
181 AmmRefundNeeded,
183 OrchestraPending,
185 BoltzPending,
189}
190
191#[derive(Debug, Clone)]
193#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
194pub enum StoragePaymentDetailsFilter {
195 Spark {
196 htlc_status: Option<Vec<SparkHtlcStatus>>,
197 conversion_filter: Option<ConversionFilter>,
198 },
199 Token {
200 conversion_filter: Option<ConversionFilter>,
201 tx_hash: Option<String>,
202 tx_type: Option<TokenTransactionType>,
203 },
204 Lightning {
205 htlc_status: Option<Vec<SparkHtlcStatus>>,
206 conversion_filter: Option<ConversionFilter>,
207 },
208}
209
210impl From<PaymentDetailsFilter> for StoragePaymentDetailsFilter {
211 fn from(filter: PaymentDetailsFilter) -> Self {
212 match filter {
213 PaymentDetailsFilter::Spark {
214 htlc_status,
215 conversion_refund_needed,
216 } => StoragePaymentDetailsFilter::Spark {
217 htlc_status,
218 conversion_filter: conversion_refund_needed
219 .and_then(|v| v.then_some(ConversionFilter::AmmRefundNeeded)),
220 },
221 PaymentDetailsFilter::Token {
222 conversion_refund_needed,
223 tx_hash,
224 tx_type,
225 } => StoragePaymentDetailsFilter::Token {
226 conversion_filter: conversion_refund_needed
227 .and_then(|v| v.then_some(ConversionFilter::AmmRefundNeeded)),
228 tx_hash,
229 tx_type,
230 },
231 PaymentDetailsFilter::Lightning { htlc_status } => {
232 StoragePaymentDetailsFilter::Lightning {
233 htlc_status,
234 conversion_filter: None,
235 }
236 }
237 }
238 }
239}
240
241impl From<StoragePaymentDetailsFilter> for PaymentDetailsFilter {
242 fn from(filter: StoragePaymentDetailsFilter) -> Self {
243 match filter {
244 StoragePaymentDetailsFilter::Spark {
245 htlc_status,
246 conversion_filter,
247 } => PaymentDetailsFilter::Spark {
248 htlc_status,
249 conversion_refund_needed: conversion_filter
250 .map(|f| matches!(f, ConversionFilter::AmmRefundNeeded)),
251 },
252 StoragePaymentDetailsFilter::Token {
253 conversion_filter,
254 tx_hash,
255 tx_type,
256 } => PaymentDetailsFilter::Token {
257 conversion_refund_needed: conversion_filter
258 .map(|f| matches!(f, ConversionFilter::AmmRefundNeeded)),
259 tx_hash,
260 tx_type,
261 },
262 StoragePaymentDetailsFilter::Lightning { htlc_status, .. } => {
263 PaymentDetailsFilter::Lightning { htlc_status }
264 }
265 }
266 }
267}
268
269#[derive(Debug, Clone, Default)]
272#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
273pub struct StorageListPaymentsRequest {
274 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
275 pub type_filter: Option<Vec<PaymentType>>,
276 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
277 pub status_filter: Option<Vec<PaymentStatus>>,
278 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
279 pub asset_filter: Option<AssetFilter>,
280 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
281 pub payment_details_filter: Option<Vec<StoragePaymentDetailsFilter>>,
282 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
283 pub from_timestamp: Option<u64>,
284 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
285 pub to_timestamp: Option<u64>,
286 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
287 pub offset: Option<u32>,
288 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
289 pub limit: Option<u32>,
290 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
291 pub sort_ascending: Option<bool>,
292}
293
294impl From<ListPaymentsRequest> for StorageListPaymentsRequest {
295 fn from(request: ListPaymentsRequest) -> Self {
296 StorageListPaymentsRequest {
297 type_filter: request.type_filter,
298 status_filter: request.status_filter,
299 asset_filter: request.asset_filter,
300 payment_details_filter: request
301 .payment_details_filter
302 .map(|filters| filters.into_iter().map(Into::into).collect()),
303 from_timestamp: request.from_timestamp,
304 to_timestamp: request.to_timestamp,
305 offset: request.offset,
306 limit: request.limit,
307 sort_ascending: request.sort_ascending,
308 }
309 }
310}
311
312impl From<StorageListPaymentsRequest> for ListPaymentsRequest {
313 fn from(request: StorageListPaymentsRequest) -> Self {
314 ListPaymentsRequest {
315 type_filter: request.type_filter,
316 status_filter: request.status_filter,
317 asset_filter: request.asset_filter,
318 payment_details_filter: request
319 .payment_details_filter
320 .map(|filters| filters.into_iter().map(Into::into).collect()),
321 from_timestamp: request.from_timestamp,
322 to_timestamp: request.to_timestamp,
323 offset: request.offset,
324 limit: request.limit,
325 sort_ascending: request.sort_ascending,
326 }
327 }
328}
329
330#[derive(Clone, Default, Deserialize, Serialize)]
332#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
333pub struct PaymentMetadata {
334 #[serde(skip_serializing_if = "Option::is_none")]
335 pub parent_payment_id: Option<String>,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub lnurl_pay_info: Option<LnurlPayInfo>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub lnurl_withdraw_info: Option<LnurlWithdrawInfo>,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 pub lnurl_description: Option<String>,
342 #[serde(
345 skip_serializing_if = "Option::is_none",
346 deserialize_with = "deserialize_conversion_info_with_default_type",
347 default
348 )]
349 pub conversion_info: Option<ConversionInfo>,
350 #[serde(skip_serializing_if = "Option::is_none")]
351 pub conversion_status: Option<ConversionStatus>,
352}
353
354#[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
355pub(crate) fn parse_payment_status(value: &str) -> Result<PaymentStatus, StorageError> {
356 value
357 .parse()
358 .map_err(|e| StorageError::Implementation(format!("invalid payment status: {e}")))
359}
360
361fn deserialize_conversion_info_with_default_type<'de, D>(
366 deserializer: D,
367) -> Result<Option<ConversionInfo>, D::Error>
368where
369 D: serde::Deserializer<'de>,
370{
371 use serde::de::Deserialize;
372
373 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
374 match value {
375 None => Ok(None),
376 Some(mut v) => {
377 if let Some(obj) = v.as_object_mut() {
378 obj.entry("type")
379 .or_insert_with(|| serde_json::Value::String("amm".to_string()));
380 }
381 match serde_json::from_value::<ConversionInfo>(v) {
382 Ok(info) => Ok(Some(info)),
383 Err(_) => Ok(None),
384 }
385 }
386 }
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
398#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
399#[serde(rename_all = "camelCase")]
400pub struct StoredCrossChainSwap {
401 pub provider: String,
403 pub id: String,
405 pub is_terminal: bool,
408 pub updated_at: u64,
411 pub data: String,
413 pub secrets: String,
416}
417
418#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
420#[async_trait]
421pub trait Storage: Send + Sync {
422 async fn delete_cached_item(&self, key: String) -> Result<(), StorageError>;
423 async fn get_cached_item(&self, key: String) -> Result<Option<String>, StorageError>;
424 async fn set_cached_item(&self, key: String, value: String) -> Result<(), StorageError>;
425 async fn list_payments(
435 &self,
436 request: StorageListPaymentsRequest,
437 ) -> Result<Vec<Payment>, StorageError>;
438
439 async fn apply_payment_update(&self, payment: Payment) -> Result<bool, StorageError>;
448
449 async fn insert_payment_metadata(
460 &self,
461 payment_id: String,
462 metadata: PaymentMetadata,
463 ) -> Result<(), StorageError>;
464
465 async fn get_payment_by_id(&self, id: String) -> Result<Payment, StorageError>;
474
475 async fn get_payment_by_invoice(
483 &self,
484 invoice: String,
485 ) -> Result<Option<Payment>, StorageError>;
486
487 async fn get_payments_by_parent_ids(
498 &self,
499 parent_payment_ids: Vec<String>,
500 ) -> Result<HashMap<String, Vec<Payment>>, StorageError>;
501
502 async fn add_deposit(
514 &self,
515 txid: String,
516 vout: u32,
517 amount_sats: u64,
518 is_mature: bool,
519 ) -> Result<(), StorageError>;
520
521 async fn delete_deposit(&self, txid: String, vout: u32) -> Result<(), StorageError>;
531
532 async fn list_deposits(&self) -> Result<Vec<DepositInfo>, StorageError>;
537
538 async fn update_deposit(
549 &self,
550 txid: String,
551 vout: u32,
552 payload: UpdateDepositPayload,
553 ) -> Result<(), StorageError>;
554
555 async fn list_watched_deposit_addresses(
558 &self,
559 ) -> Result<Vec<WatchedDepositAddress>, StorageError>;
560
561 async fn update_watched_deposit_address(
564 &self,
565 address: String,
566 payload: UpdateWatchedAddressPayload,
567 ) -> Result<(), StorageError>;
568
569 async fn set_lnurl_metadata(
570 &self,
571 metadata: Vec<SetLnurlMetadataItem>,
572 ) -> Result<(), StorageError>;
573
574 async fn list_contacts(
576 &self,
577 request: ListContactsRequest,
578 ) -> Result<Vec<Contact>, StorageError>;
579
580 async fn get_contact(&self, id: String) -> Result<Contact, StorageError>;
582
583 async fn insert_contact(&self, contact: Contact) -> Result<(), StorageError>;
586
587 async fn delete_contact(&self, id: String) -> Result<(), StorageError>;
589
590 async fn set_cross_chain_swap(&self, swap: StoredCrossChainSwap) -> Result<(), StorageError>;
592
593 async fn get_cross_chain_swap(
595 &self,
596 provider: String,
597 id: String,
598 ) -> Result<Option<StoredCrossChainSwap>, StorageError>;
599
600 async fn list_active_cross_chain_swaps(
603 &self,
604 provider: String,
605 ) -> Result<Vec<StoredCrossChainSwap>, StorageError>;
606
607 async fn add_outgoing_change(
609 &self,
610 record: UnversionedRecordChange,
611 ) -> Result<u64, StorageError>;
612 async fn complete_outgoing_sync(
613 &self,
614 record: Record,
615 local_revision: u64,
616 ) -> Result<(), StorageError>;
617 async fn get_pending_outgoing_changes(
618 &self,
619 limit: u32,
620 ) -> Result<Vec<OutgoingChange>, StorageError>;
621
622 async fn get_last_revision(&self) -> Result<u64, StorageError>;
629
630 async fn insert_incoming_records(&self, records: Vec<Record>) -> Result<(), StorageError>;
632
633 async fn delete_incoming_record(&self, record: Record) -> Result<(), StorageError>;
635
636 async fn get_incoming_records(&self, limit: u32) -> Result<Vec<IncomingChange>, StorageError>;
638
639 async fn get_latest_outgoing_change(&self) -> Result<Option<OutgoingChange>, StorageError>;
641
642 async fn update_record_from_incoming(&self, record: Record) -> Result<(), StorageError>;
644}
645
646pub(crate) struct ObjectCacheRepository {
647 storage: Arc<dyn Storage>,
648}
649
650impl ObjectCacheRepository {
651 pub(crate) fn new(storage: Arc<dyn Storage>) -> Self {
652 ObjectCacheRepository { storage }
653 }
654
655 pub(crate) async fn save_account_info(
656 &self,
657 value: &CachedAccountInfo,
658 ) -> Result<(), StorageError> {
659 self.storage
660 .set_cached_item(ACCOUNT_INFO_KEY.to_string(), serde_json::to_string(value)?)
661 .await?;
662 Ok(())
663 }
664
665 pub(crate) async fn fetch_account_info(
666 &self,
667 ) -> Result<Option<CachedAccountInfo>, StorageError> {
668 let value = self
669 .storage
670 .get_cached_item(ACCOUNT_INFO_KEY.to_string())
671 .await?;
672 match value {
673 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
674 None => Ok(None),
675 }
676 }
677
678 pub(crate) async fn save_sync_info(&self, value: &CachedSyncInfo) -> Result<(), StorageError> {
679 self.storage
680 .set_cached_item(SYNC_OFFSET_KEY.to_string(), serde_json::to_string(value)?)
681 .await?;
682 Ok(())
683 }
684
685 pub(crate) async fn fetch_sync_info(&self) -> Result<Option<CachedSyncInfo>, StorageError> {
686 let value = self
687 .storage
688 .get_cached_item(SYNC_OFFSET_KEY.to_string())
689 .await?;
690 match value {
691 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
692 None => Ok(None),
693 }
694 }
695
696 pub(crate) async fn save_published_package(
701 &self,
702 package_id: &str,
703 payment_id: &str,
704 ) -> Result<(), StorageError> {
705 self.storage
706 .set_cached_item(
707 format!("{PUBLISHED_PACKAGE_KEY_PREFIX}{package_id}"),
708 payment_id.to_string(),
709 )
710 .await?;
711 Ok(())
712 }
713
714 pub(crate) async fn fetch_published_package(
715 &self,
716 package_id: &str,
717 ) -> Result<Option<String>, StorageError> {
718 self.storage
719 .get_cached_item(format!("{PUBLISHED_PACKAGE_KEY_PREFIX}{package_id}"))
720 .await
721 }
722
723 pub(crate) async fn save_tx(&self, txid: &str, value: &CachedTx) -> Result<(), StorageError> {
724 self.storage
725 .set_cached_item(
726 format!("{TX_CACHE_KEY}-{txid}"),
727 serde_json::to_string(value)?,
728 )
729 .await?;
730 Ok(())
731 }
732
733 pub(crate) async fn delete_tx(&self, txid: &str) -> Result<(), StorageError> {
734 self.storage
735 .delete_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
736 .await
737 }
738
739 pub(crate) async fn fetch_tx(&self, txid: &str) -> Result<Option<CachedTx>, StorageError> {
740 let value = self
741 .storage
742 .get_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
743 .await?;
744 match value {
745 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
746 None => Ok(None),
747 }
748 }
749
750 pub(crate) async fn save_lightning_address(
751 &self,
752 value: &LightningAddressInfo,
753 recovered: bool,
754 ) -> Result<(), StorageError> {
755 let cached = CachedLightningAddress {
756 address: Some(value.clone()),
757 recovered,
758 };
759 self.storage
760 .set_cached_item(
761 LIGHTNING_ADDRESS_KEY.to_string(),
762 serde_json::to_string(&cached)?,
763 )
764 .await?;
765 Ok(())
766 }
767
768 pub(crate) async fn delete_lightning_address(
770 &self,
771 recovered: bool,
772 ) -> Result<(), StorageError> {
773 let cached = CachedLightningAddress {
774 address: None,
775 recovered,
776 };
777 self.storage
778 .set_cached_item(
779 LIGHTNING_ADDRESS_KEY.to_string(),
780 serde_json::to_string(&cached)?,
781 )
782 .await?;
783 Ok(())
784 }
785
786 pub(crate) async fn fetch_lightning_address(
791 &self,
792 ) -> Result<Option<Option<LightningAddressInfo>>, StorageError> {
793 let value = self
794 .storage
795 .get_cached_item(LIGHTNING_ADDRESS_KEY.to_string())
796 .await?;
797 match value {
798 Some(value) => {
799 let cached = parse_cached_lightning_address(&value)?;
800 Ok(Some(cached.address))
801 }
802 None => Ok(None),
803 }
804 }
805
806 pub(crate) async fn save_token_metadata(
807 &self,
808 value: &TokenMetadata,
809 ) -> Result<(), StorageError> {
810 self.storage
811 .set_cached_item(
812 format!("{TOKEN_METADATA_KEY_PREFIX}{}", value.identifier),
813 serde_json::to_string(value)?,
814 )
815 .await?;
816 Ok(())
817 }
818
819 pub(crate) async fn fetch_token_metadata(
820 &self,
821 identifier: &str,
822 ) -> Result<Option<TokenMetadata>, StorageError> {
823 let value = self
824 .storage
825 .get_cached_item(format!("{TOKEN_METADATA_KEY_PREFIX}{identifier}"))
826 .await?;
827 match value {
828 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
829 None => Ok(None),
830 }
831 }
832
833 pub(crate) async fn save_payment_metadata(
834 &self,
835 identifier: &str,
836 value: &PaymentMetadata,
837 ) -> Result<(), StorageError> {
838 self.storage
839 .set_cached_item(
840 format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"),
841 serde_json::to_string(value)?,
842 )
843 .await?;
844 Ok(())
845 }
846
847 pub(crate) async fn fetch_payment_metadata(
848 &self,
849 identifier: &str,
850 ) -> Result<Option<PaymentMetadata>, StorageError> {
851 let value = self
852 .storage
853 .get_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"))
854 .await?;
855 match value {
856 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
857 None => Ok(None),
858 }
859 }
860
861 pub(crate) async fn delete_payment_metadata(
862 &self,
863 identifier: &str,
864 ) -> Result<(), StorageError> {
865 self.storage
866 .delete_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"))
867 .await?;
868 Ok(())
869 }
870
871 pub(crate) async fn save_spark_private_mode_initialized(&self) -> Result<(), StorageError> {
872 self.storage
873 .set_cached_item(
874 SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string(),
875 "true".to_string(),
876 )
877 .await?;
878 Ok(())
879 }
880
881 pub(crate) async fn fetch_spark_private_mode_initialized(&self) -> Result<bool, StorageError> {
882 let value = self
883 .storage
884 .get_cached_item(SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string())
885 .await?;
886 match value {
887 Some(value) => Ok(value == "true"),
888 None => Ok(false),
889 }
890 }
891
892 pub(crate) async fn save_stable_balance_active_label(
893 &self,
894 label: &str,
895 ) -> Result<(), StorageError> {
896 self.storage
897 .set_cached_item(
898 STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string(),
899 label.to_string(),
900 )
901 .await
902 }
903
904 pub(crate) async fn fetch_stable_balance_active_label(
905 &self,
906 ) -> Result<Option<String>, StorageError> {
907 self.storage
908 .get_cached_item(STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string())
909 .await
910 }
911
912 pub(crate) async fn delete_stable_balance_active_label(&self) -> Result<(), StorageError> {
913 self.storage
914 .delete_cached_item(STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string())
915 .await
916 }
917
918 pub(crate) async fn save_pending_lightning_sends(
923 &self,
924 pending: &[super::sdk::PendingLightningSend],
925 ) -> Result<(), StorageError> {
926 self.storage
927 .set_cached_item(
928 PENDING_LIGHTNING_SENDS_KEY.to_string(),
929 serde_json::to_string(pending)?,
930 )
931 .await?;
932 Ok(())
933 }
934
935 pub(crate) async fn fetch_pending_lightning_sends(
936 &self,
937 ) -> Result<Vec<super::sdk::PendingLightningSend>, StorageError> {
938 let value = self
939 .storage
940 .get_cached_item(PENDING_LIGHTNING_SENDS_KEY.to_string())
941 .await?;
942 match value {
943 Some(value) => Ok(serde_json::from_str(&value)?),
944 None => Ok(Vec::new()),
945 }
946 }
947
948 pub(crate) async fn save_pending_conversions(
949 &self,
950 pending: &[super::stable_balance::PendingConversion],
951 ) -> Result<(), StorageError> {
952 self.storage
953 .set_cached_item(
954 PENDING_CONVERSIONS_KEY.to_string(),
955 serde_json::to_string(pending)?,
956 )
957 .await?;
958 Ok(())
959 }
960
961 pub(crate) async fn fetch_pending_conversions(
962 &self,
963 ) -> Result<Option<Vec<super::stable_balance::PendingConversion>>, StorageError> {
964 let value = self
965 .storage
966 .get_cached_item(PENDING_CONVERSIONS_KEY.to_string())
967 .await?;
968 match value {
969 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
970 None => Ok(None),
971 }
972 }
973
974 pub(crate) async fn delete_pending_conversions(&self) -> Result<(), StorageError> {
975 self.storage
976 .delete_cached_item(PENDING_CONVERSIONS_KEY.to_string())
977 .await
978 }
979
980 pub(crate) async fn save_lnurl_metadata_updated_after(
981 &self,
982 offset: i64,
983 ) -> Result<(), StorageError> {
984 self.storage
985 .set_cached_item(
986 LNURL_METADATA_UPDATED_AFTER_KEY.to_string(),
987 offset.to_string(),
988 )
989 .await?;
990 Ok(())
991 }
992
993 pub(crate) async fn fetch_lnurl_metadata_updated_after(&self) -> Result<i64, StorageError> {
994 let value = self
995 .storage
996 .get_cached_item(LNURL_METADATA_UPDATED_AFTER_KEY.to_string())
997 .await?;
998 match value {
999 Some(value) => Ok(value.parse().map_err(|_| {
1000 StorageError::Serialization("invalid lnurl_metadata_updated_after".to_string())
1001 })?),
1002 None => Ok(0),
1003 }
1004 }
1005
1006 pub(crate) async fn get_last_sync_time(&self) -> Result<Option<u64>, StorageError> {
1007 let value = self
1008 .storage
1009 .get_cached_item(LAST_SYNC_TIME_KEY.to_string())
1010 .await?;
1011 match value {
1012 Some(v) => Ok(Some(v.parse().map_err(|_| {
1013 StorageError::Serialization("invalid last_sync_time".to_string())
1014 })?)),
1015 None => Ok(None),
1016 }
1017 }
1018
1019 pub(crate) async fn set_last_sync_time(&self, time: u64) -> Result<(), StorageError> {
1020 self.storage
1021 .set_cached_item(LAST_SYNC_TIME_KEY.to_string(), time.to_string())
1022 .await
1023 }
1024}
1025
1026#[derive(Serialize, Deserialize, Default)]
1027pub(crate) struct CachedAccountInfo {
1028 pub(crate) balance_sats: u64,
1029 #[serde(default)]
1030 pub(crate) token_balances: HashMap<String, TokenBalance>,
1031}
1032
1033#[derive(Serialize, Deserialize, Default)]
1034pub(crate) struct CachedSyncInfo {
1035 pub(crate) offset: u64,
1036 pub(crate) last_synced_final_token_payment_id: Option<String>,
1037}
1038
1039#[derive(Serialize, Deserialize, Default)]
1040pub(crate) struct CachedTx {
1041 pub(crate) raw_tx: String,
1042}
1043
1044#[cfg(feature = "test-utils")]
1045pub mod tests;