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,
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";
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
55pub(crate) struct CachedLightningAddress {
56 pub address: Option<LightningAddressInfo>,
57 pub recovered: bool,
58}
59
60pub(crate) fn parse_cached_lightning_address(
62 value: &str,
63) -> Result<CachedLightningAddress, StorageError> {
64 serde_json::from_str(value).map_err(|e| StorageError::Serialization(e.to_string()))
65}
66
67#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
68pub enum UpdateDepositPayload {
69 ClaimError {
70 error: DepositClaimError,
71 },
72 Refund {
73 refund_txid: String,
74 refund_tx: String,
75 },
76 InstantClaim {
77 status: InstantClaimStatus,
78 },
79}
80
81#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
82pub struct SetLnurlMetadataItem {
83 pub payment_hash: String,
84 pub sender_comment: Option<String>,
85 pub nostr_zap_request: Option<String>,
86 pub nostr_zap_receipt: Option<String>,
87}
88
89impl From<lnurl_models::ListMetadataMetadata> for SetLnurlMetadataItem {
90 fn from(value: lnurl_models::ListMetadataMetadata) -> Self {
91 SetLnurlMetadataItem {
92 payment_hash: value.payment_hash,
93 sender_comment: value.sender_comment,
94 nostr_zap_request: value.nostr_zap_request,
95 nostr_zap_receipt: value.nostr_zap_receipt,
96 }
97 }
98}
99
100#[derive(Debug, Error, Clone)]
102#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
103pub enum StorageError {
104 #[error("Connection error: {0}")]
107 Connection(String),
108
109 #[error("Underlying implementation error: {0}")]
110 Implementation(String),
111
112 #[error("Failed to initialize database: {0}")]
114 InitializationError(String),
115
116 #[error("Failed to serialize/deserialize data: {0}")]
117 Serialization(String),
118
119 #[error("Not found")]
120 NotFound,
121}
122
123impl From<serde_json::Error> for StorageError {
124 fn from(e: serde_json::Error) -> Self {
125 StorageError::Serialization(e.to_string())
126 }
127}
128
129impl From<std::num::TryFromIntError> for StorageError {
130 fn from(e: std::num::TryFromIntError) -> Self {
131 StorageError::Implementation(format!("integer overflow: {e}"))
132 }
133}
134
135#[derive(Debug, Clone)]
137#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
138pub enum ConversionFilter {
139 AmmRefundNeeded,
141 OrchestraPending,
143 BoltzPending,
147}
148
149#[derive(Debug, Clone)]
151#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
152pub enum StoragePaymentDetailsFilter {
153 Spark {
154 htlc_status: Option<Vec<SparkHtlcStatus>>,
155 conversion_filter: Option<ConversionFilter>,
156 },
157 Token {
158 conversion_filter: Option<ConversionFilter>,
159 tx_hash: Option<String>,
160 tx_type: Option<TokenTransactionType>,
161 },
162 Lightning {
163 htlc_status: Option<Vec<SparkHtlcStatus>>,
164 conversion_filter: Option<ConversionFilter>,
165 },
166}
167
168impl From<PaymentDetailsFilter> for StoragePaymentDetailsFilter {
169 fn from(filter: PaymentDetailsFilter) -> Self {
170 match filter {
171 PaymentDetailsFilter::Spark {
172 htlc_status,
173 conversion_refund_needed,
174 } => StoragePaymentDetailsFilter::Spark {
175 htlc_status,
176 conversion_filter: conversion_refund_needed
177 .and_then(|v| v.then_some(ConversionFilter::AmmRefundNeeded)),
178 },
179 PaymentDetailsFilter::Token {
180 conversion_refund_needed,
181 tx_hash,
182 tx_type,
183 } => StoragePaymentDetailsFilter::Token {
184 conversion_filter: conversion_refund_needed
185 .and_then(|v| v.then_some(ConversionFilter::AmmRefundNeeded)),
186 tx_hash,
187 tx_type,
188 },
189 PaymentDetailsFilter::Lightning { htlc_status } => {
190 StoragePaymentDetailsFilter::Lightning {
191 htlc_status,
192 conversion_filter: None,
193 }
194 }
195 }
196 }
197}
198
199impl From<StoragePaymentDetailsFilter> for PaymentDetailsFilter {
200 fn from(filter: StoragePaymentDetailsFilter) -> Self {
201 match filter {
202 StoragePaymentDetailsFilter::Spark {
203 htlc_status,
204 conversion_filter,
205 } => PaymentDetailsFilter::Spark {
206 htlc_status,
207 conversion_refund_needed: conversion_filter
208 .map(|f| matches!(f, ConversionFilter::AmmRefundNeeded)),
209 },
210 StoragePaymentDetailsFilter::Token {
211 conversion_filter,
212 tx_hash,
213 tx_type,
214 } => PaymentDetailsFilter::Token {
215 conversion_refund_needed: conversion_filter
216 .map(|f| matches!(f, ConversionFilter::AmmRefundNeeded)),
217 tx_hash,
218 tx_type,
219 },
220 StoragePaymentDetailsFilter::Lightning { htlc_status, .. } => {
221 PaymentDetailsFilter::Lightning { htlc_status }
222 }
223 }
224 }
225}
226
227#[derive(Debug, Clone, Default)]
230#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
231pub struct StorageListPaymentsRequest {
232 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
233 pub type_filter: Option<Vec<PaymentType>>,
234 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
235 pub status_filter: Option<Vec<PaymentStatus>>,
236 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
237 pub asset_filter: Option<AssetFilter>,
238 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
239 pub payment_details_filter: Option<Vec<StoragePaymentDetailsFilter>>,
240 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
241 pub from_timestamp: Option<u64>,
242 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
243 pub to_timestamp: Option<u64>,
244 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
245 pub offset: Option<u32>,
246 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
247 pub limit: Option<u32>,
248 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
249 pub sort_ascending: Option<bool>,
250}
251
252impl From<ListPaymentsRequest> for StorageListPaymentsRequest {
253 fn from(request: ListPaymentsRequest) -> Self {
254 StorageListPaymentsRequest {
255 type_filter: request.type_filter,
256 status_filter: request.status_filter,
257 asset_filter: request.asset_filter,
258 payment_details_filter: request
259 .payment_details_filter
260 .map(|filters| filters.into_iter().map(Into::into).collect()),
261 from_timestamp: request.from_timestamp,
262 to_timestamp: request.to_timestamp,
263 offset: request.offset,
264 limit: request.limit,
265 sort_ascending: request.sort_ascending,
266 }
267 }
268}
269
270impl From<StorageListPaymentsRequest> for ListPaymentsRequest {
271 fn from(request: StorageListPaymentsRequest) -> Self {
272 ListPaymentsRequest {
273 type_filter: request.type_filter,
274 status_filter: request.status_filter,
275 asset_filter: request.asset_filter,
276 payment_details_filter: request
277 .payment_details_filter
278 .map(|filters| filters.into_iter().map(Into::into).collect()),
279 from_timestamp: request.from_timestamp,
280 to_timestamp: request.to_timestamp,
281 offset: request.offset,
282 limit: request.limit,
283 sort_ascending: request.sort_ascending,
284 }
285 }
286}
287
288#[derive(Clone, Default, Deserialize, Serialize)]
290#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
291pub struct PaymentMetadata {
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub parent_payment_id: Option<String>,
294 #[serde(skip_serializing_if = "Option::is_none")]
295 pub lnurl_pay_info: Option<LnurlPayInfo>,
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub lnurl_withdraw_info: Option<LnurlWithdrawInfo>,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub lnurl_description: Option<String>,
300 #[serde(
303 skip_serializing_if = "Option::is_none",
304 deserialize_with = "deserialize_conversion_info_with_default_type",
305 default
306 )]
307 pub conversion_info: Option<ConversionInfo>,
308 #[serde(skip_serializing_if = "Option::is_none")]
309 pub conversion_status: Option<ConversionStatus>,
310}
311
312#[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
313pub(crate) fn parse_payment_status(value: &str) -> Result<PaymentStatus, StorageError> {
314 value
315 .parse()
316 .map_err(|e| StorageError::Implementation(format!("invalid payment status: {e}")))
317}
318
319fn deserialize_conversion_info_with_default_type<'de, D>(
324 deserializer: D,
325) -> Result<Option<ConversionInfo>, D::Error>
326where
327 D: serde::Deserializer<'de>,
328{
329 use serde::de::Deserialize;
330
331 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
332 match value {
333 None => Ok(None),
334 Some(mut v) => {
335 if let Some(obj) = v.as_object_mut() {
336 obj.entry("type")
337 .or_insert_with(|| serde_json::Value::String("amm".to_string()));
338 }
339 match serde_json::from_value::<ConversionInfo>(v) {
340 Ok(info) => Ok(Some(info)),
341 Err(_) => Ok(None),
342 }
343 }
344 }
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
356#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
357#[serde(rename_all = "camelCase")]
358pub struct StoredCrossChainSwap {
359 pub provider: String,
361 pub id: String,
363 pub is_terminal: bool,
366 pub updated_at: u64,
369 pub data: String,
371 pub secrets: String,
374}
375
376#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
378#[async_trait]
379pub trait Storage: Send + Sync {
380 async fn delete_cached_item(&self, key: String) -> Result<(), StorageError>;
381 async fn get_cached_item(&self, key: String) -> Result<Option<String>, StorageError>;
382 async fn set_cached_item(&self, key: String, value: String) -> Result<(), StorageError>;
383 async fn list_payments(
393 &self,
394 request: StorageListPaymentsRequest,
395 ) -> Result<Vec<Payment>, StorageError>;
396
397 async fn apply_payment_update(&self, payment: Payment) -> Result<bool, StorageError>;
406
407 async fn insert_payment_metadata(
418 &self,
419 payment_id: String,
420 metadata: PaymentMetadata,
421 ) -> Result<(), StorageError>;
422
423 async fn get_payment_by_id(&self, id: String) -> Result<Payment, StorageError>;
432
433 async fn get_payment_by_invoice(
441 &self,
442 invoice: String,
443 ) -> Result<Option<Payment>, StorageError>;
444
445 async fn get_payments_by_parent_ids(
456 &self,
457 parent_payment_ids: Vec<String>,
458 ) -> Result<HashMap<String, Vec<Payment>>, StorageError>;
459
460 async fn add_deposit(
472 &self,
473 txid: String,
474 vout: u32,
475 amount_sats: u64,
476 is_mature: bool,
477 ) -> Result<(), StorageError>;
478
479 async fn delete_deposit(&self, txid: String, vout: u32) -> Result<(), StorageError>;
489
490 async fn list_deposits(&self) -> Result<Vec<DepositInfo>, StorageError>;
495
496 async fn update_deposit(
507 &self,
508 txid: String,
509 vout: u32,
510 payload: UpdateDepositPayload,
511 ) -> Result<(), StorageError>;
512
513 async fn set_lnurl_metadata(
514 &self,
515 metadata: Vec<SetLnurlMetadataItem>,
516 ) -> Result<(), StorageError>;
517
518 async fn list_contacts(
520 &self,
521 request: ListContactsRequest,
522 ) -> Result<Vec<Contact>, StorageError>;
523
524 async fn get_contact(&self, id: String) -> Result<Contact, StorageError>;
526
527 async fn insert_contact(&self, contact: Contact) -> Result<(), StorageError>;
530
531 async fn delete_contact(&self, id: String) -> Result<(), StorageError>;
533
534 async fn set_cross_chain_swap(&self, swap: StoredCrossChainSwap) -> Result<(), StorageError>;
536
537 async fn get_cross_chain_swap(
539 &self,
540 provider: String,
541 id: String,
542 ) -> Result<Option<StoredCrossChainSwap>, StorageError>;
543
544 async fn list_active_cross_chain_swaps(
547 &self,
548 provider: String,
549 ) -> Result<Vec<StoredCrossChainSwap>, StorageError>;
550
551 async fn add_outgoing_change(
553 &self,
554 record: UnversionedRecordChange,
555 ) -> Result<u64, StorageError>;
556 async fn complete_outgoing_sync(
557 &self,
558 record: Record,
559 local_revision: u64,
560 ) -> Result<(), StorageError>;
561 async fn get_pending_outgoing_changes(
562 &self,
563 limit: u32,
564 ) -> Result<Vec<OutgoingChange>, StorageError>;
565
566 async fn get_last_revision(&self) -> Result<u64, StorageError>;
573
574 async fn insert_incoming_records(&self, records: Vec<Record>) -> Result<(), StorageError>;
576
577 async fn delete_incoming_record(&self, record: Record) -> Result<(), StorageError>;
579
580 async fn get_incoming_records(&self, limit: u32) -> Result<Vec<IncomingChange>, StorageError>;
582
583 async fn get_latest_outgoing_change(&self) -> Result<Option<OutgoingChange>, StorageError>;
585
586 async fn update_record_from_incoming(&self, record: Record) -> Result<(), StorageError>;
588}
589
590pub(crate) struct ObjectCacheRepository {
591 storage: Arc<dyn Storage>,
592}
593
594impl ObjectCacheRepository {
595 pub(crate) fn new(storage: Arc<dyn Storage>) -> Self {
596 ObjectCacheRepository { storage }
597 }
598
599 pub(crate) async fn save_account_info(
600 &self,
601 value: &CachedAccountInfo,
602 ) -> Result<(), StorageError> {
603 self.storage
604 .set_cached_item(ACCOUNT_INFO_KEY.to_string(), serde_json::to_string(value)?)
605 .await?;
606 Ok(())
607 }
608
609 pub(crate) async fn fetch_account_info(
610 &self,
611 ) -> Result<Option<CachedAccountInfo>, StorageError> {
612 let value = self
613 .storage
614 .get_cached_item(ACCOUNT_INFO_KEY.to_string())
615 .await?;
616 match value {
617 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
618 None => Ok(None),
619 }
620 }
621
622 pub(crate) async fn save_sync_info(&self, value: &CachedSyncInfo) -> Result<(), StorageError> {
623 self.storage
624 .set_cached_item(SYNC_OFFSET_KEY.to_string(), serde_json::to_string(value)?)
625 .await?;
626 Ok(())
627 }
628
629 pub(crate) async fn fetch_sync_info(&self) -> Result<Option<CachedSyncInfo>, StorageError> {
630 let value = self
631 .storage
632 .get_cached_item(SYNC_OFFSET_KEY.to_string())
633 .await?;
634 match value {
635 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
636 None => Ok(None),
637 }
638 }
639
640 pub(crate) async fn save_published_package(
645 &self,
646 package_id: &str,
647 payment_id: &str,
648 ) -> Result<(), StorageError> {
649 self.storage
650 .set_cached_item(
651 format!("{PUBLISHED_PACKAGE_KEY_PREFIX}{package_id}"),
652 payment_id.to_string(),
653 )
654 .await?;
655 Ok(())
656 }
657
658 pub(crate) async fn fetch_published_package(
659 &self,
660 package_id: &str,
661 ) -> Result<Option<String>, StorageError> {
662 self.storage
663 .get_cached_item(format!("{PUBLISHED_PACKAGE_KEY_PREFIX}{package_id}"))
664 .await
665 }
666
667 pub(crate) async fn save_tx(&self, txid: &str, value: &CachedTx) -> Result<(), StorageError> {
668 self.storage
669 .set_cached_item(
670 format!("{TX_CACHE_KEY}-{txid}"),
671 serde_json::to_string(value)?,
672 )
673 .await?;
674 Ok(())
675 }
676
677 pub(crate) async fn fetch_tx(&self, txid: &str) -> Result<Option<CachedTx>, StorageError> {
678 let value = self
679 .storage
680 .get_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
681 .await?;
682 match value {
683 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
684 None => Ok(None),
685 }
686 }
687
688 pub(crate) async fn save_lightning_address(
689 &self,
690 value: &LightningAddressInfo,
691 recovered: bool,
692 ) -> Result<(), StorageError> {
693 let cached = CachedLightningAddress {
694 address: Some(value.clone()),
695 recovered,
696 };
697 self.storage
698 .set_cached_item(
699 LIGHTNING_ADDRESS_KEY.to_string(),
700 serde_json::to_string(&cached)?,
701 )
702 .await?;
703 Ok(())
704 }
705
706 pub(crate) async fn delete_lightning_address(
708 &self,
709 recovered: bool,
710 ) -> Result<(), StorageError> {
711 let cached = CachedLightningAddress {
712 address: None,
713 recovered,
714 };
715 self.storage
716 .set_cached_item(
717 LIGHTNING_ADDRESS_KEY.to_string(),
718 serde_json::to_string(&cached)?,
719 )
720 .await?;
721 Ok(())
722 }
723
724 pub(crate) async fn fetch_lightning_address(
729 &self,
730 ) -> Result<Option<Option<LightningAddressInfo>>, StorageError> {
731 let value = self
732 .storage
733 .get_cached_item(LIGHTNING_ADDRESS_KEY.to_string())
734 .await?;
735 match value {
736 Some(value) => {
737 let cached = parse_cached_lightning_address(&value)?;
738 Ok(Some(cached.address))
739 }
740 None => Ok(None),
741 }
742 }
743
744 pub(crate) async fn save_token_metadata(
745 &self,
746 value: &TokenMetadata,
747 ) -> Result<(), StorageError> {
748 self.storage
749 .set_cached_item(
750 format!("{TOKEN_METADATA_KEY_PREFIX}{}", value.identifier),
751 serde_json::to_string(value)?,
752 )
753 .await?;
754 Ok(())
755 }
756
757 pub(crate) async fn fetch_token_metadata(
758 &self,
759 identifier: &str,
760 ) -> Result<Option<TokenMetadata>, StorageError> {
761 let value = self
762 .storage
763 .get_cached_item(format!("{TOKEN_METADATA_KEY_PREFIX}{identifier}"))
764 .await?;
765 match value {
766 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
767 None => Ok(None),
768 }
769 }
770
771 pub(crate) async fn save_payment_metadata(
772 &self,
773 identifier: &str,
774 value: &PaymentMetadata,
775 ) -> Result<(), StorageError> {
776 self.storage
777 .set_cached_item(
778 format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"),
779 serde_json::to_string(value)?,
780 )
781 .await?;
782 Ok(())
783 }
784
785 pub(crate) async fn fetch_payment_metadata(
786 &self,
787 identifier: &str,
788 ) -> Result<Option<PaymentMetadata>, StorageError> {
789 let value = self
790 .storage
791 .get_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"))
792 .await?;
793 match value {
794 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
795 None => Ok(None),
796 }
797 }
798
799 pub(crate) async fn delete_payment_metadata(
800 &self,
801 identifier: &str,
802 ) -> Result<(), StorageError> {
803 self.storage
804 .delete_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"))
805 .await?;
806 Ok(())
807 }
808
809 pub(crate) async fn save_spark_private_mode_initialized(&self) -> Result<(), StorageError> {
810 self.storage
811 .set_cached_item(
812 SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string(),
813 "true".to_string(),
814 )
815 .await?;
816 Ok(())
817 }
818
819 pub(crate) async fn fetch_spark_private_mode_initialized(&self) -> Result<bool, StorageError> {
820 let value = self
821 .storage
822 .get_cached_item(SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string())
823 .await?;
824 match value {
825 Some(value) => Ok(value == "true"),
826 None => Ok(false),
827 }
828 }
829
830 pub(crate) async fn save_stable_balance_active_label(
831 &self,
832 label: &str,
833 ) -> Result<(), StorageError> {
834 self.storage
835 .set_cached_item(
836 STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string(),
837 label.to_string(),
838 )
839 .await
840 }
841
842 pub(crate) async fn fetch_stable_balance_active_label(
843 &self,
844 ) -> Result<Option<String>, StorageError> {
845 self.storage
846 .get_cached_item(STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string())
847 .await
848 }
849
850 pub(crate) async fn delete_stable_balance_active_label(&self) -> Result<(), StorageError> {
851 self.storage
852 .delete_cached_item(STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string())
853 .await
854 }
855
856 pub(crate) async fn save_pending_conversions(
857 &self,
858 pending: &[super::stable_balance::PendingConversion],
859 ) -> Result<(), StorageError> {
860 self.storage
861 .set_cached_item(
862 PENDING_CONVERSIONS_KEY.to_string(),
863 serde_json::to_string(pending)?,
864 )
865 .await?;
866 Ok(())
867 }
868
869 pub(crate) async fn fetch_pending_conversions(
870 &self,
871 ) -> Result<Option<Vec<super::stable_balance::PendingConversion>>, StorageError> {
872 let value = self
873 .storage
874 .get_cached_item(PENDING_CONVERSIONS_KEY.to_string())
875 .await?;
876 match value {
877 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
878 None => Ok(None),
879 }
880 }
881
882 pub(crate) async fn delete_pending_conversions(&self) -> Result<(), StorageError> {
883 self.storage
884 .delete_cached_item(PENDING_CONVERSIONS_KEY.to_string())
885 .await
886 }
887
888 pub(crate) async fn save_lnurl_metadata_updated_after(
889 &self,
890 offset: i64,
891 ) -> Result<(), StorageError> {
892 self.storage
893 .set_cached_item(
894 LNURL_METADATA_UPDATED_AFTER_KEY.to_string(),
895 offset.to_string(),
896 )
897 .await?;
898 Ok(())
899 }
900
901 pub(crate) async fn fetch_lnurl_metadata_updated_after(&self) -> Result<i64, StorageError> {
902 let value = self
903 .storage
904 .get_cached_item(LNURL_METADATA_UPDATED_AFTER_KEY.to_string())
905 .await?;
906 match value {
907 Some(value) => Ok(value.parse().map_err(|_| {
908 StorageError::Serialization("invalid lnurl_metadata_updated_after".to_string())
909 })?),
910 None => Ok(0),
911 }
912 }
913
914 pub(crate) async fn get_last_sync_time(&self) -> Result<Option<u64>, StorageError> {
915 let value = self
916 .storage
917 .get_cached_item(LAST_SYNC_TIME_KEY.to_string())
918 .await?;
919 match value {
920 Some(v) => Ok(Some(v.parse().map_err(|_| {
921 StorageError::Serialization("invalid last_sync_time".to_string())
922 })?)),
923 None => Ok(None),
924 }
925 }
926
927 pub(crate) async fn set_last_sync_time(&self, time: u64) -> Result<(), StorageError> {
928 self.storage
929 .set_cached_item(LAST_SYNC_TIME_KEY.to_string(), time.to_string())
930 .await
931 }
932}
933
934#[derive(Serialize, Deserialize, Default)]
935pub(crate) struct CachedAccountInfo {
936 pub(crate) balance_sats: u64,
937 #[serde(default)]
938 pub(crate) token_balances: HashMap<String, TokenBalance>,
939}
940
941#[derive(Serialize, Deserialize, Default)]
942pub(crate) struct CachedSyncInfo {
943 pub(crate) offset: u64,
944 pub(crate) last_synced_final_token_payment_id: Option<String>,
945}
946
947#[derive(Serialize, Deserialize, Default)]
948pub(crate) struct CachedTx {
949 pub(crate) raw_tx: String,
950}
951
952#[cfg(feature = "test-utils")]
953pub mod tests;