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