1pub(crate) mod backend;
2#[cfg(any(feature = "mysql", feature = "db-storage-api"))]
3pub mod mysql;
4pub(crate) mod path;
5#[cfg(any(feature = "postgres", feature = "db-storage-api"))]
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";
40const LAST_SYNCED_AT_KEY: &str = "last_synced_at";
41pub(crate) const LIGHTNING_ADDRESS_KEY: &str = "lightning_address";
42const LNURL_METADATA_UPDATED_AFTER_KEY: &str = "lnurl_metadata_updated_after";
43const SYNC_OFFSET_KEY: &str = "sync_offset";
44const TX_CACHE_KEY: &str = "tx_cache";
45const TOKEN_METADATA_KEY_PREFIX: &str = "token_metadata_";
47const PAYMENT_METADATA_KEY_PREFIX: &str = "payment_metadata";
48const PUBLISHED_PACKAGE_KEY_PREFIX: &str = "published_package_";
49const SPARK_PRIVATE_MODE_INITIALIZED_KEY: &str = "spark_private_mode_initialized";
50const FLASHNET_REWARDS_DELEGATION_KEY: &str = "flashnet_rewards_delegation";
51pub(crate) const STABLE_BALANCE_ACTIVE_LABEL_KEY: &str = "stable_balance_active_label";
52const PENDING_CONVERSIONS_KEY: &str = "pending_conversions";
53const PENDING_LIGHTNING_SENDS_KEY: &str = "pending_lightning_sends";
54const CONVERSION_BACKOFF_KEY: &str = "conversion_backoff";
55const RECOVERABLE_FUNDS_KEY: &str = "recoverable_funds";
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
60pub(crate) struct CachedLightningAddress {
61 pub address: Option<LightningAddressInfo>,
62 pub recovered: bool,
63}
64
65pub(crate) fn parse_cached_lightning_address(
67 value: &str,
68) -> Result<CachedLightningAddress, StorageError> {
69 serde_json::from_str(value).map_err(|e| StorageError::Serialization(e.to_string()))
70}
71
72#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
73pub enum UpdateDepositPayload {
74 ClaimError {
75 error: DepositClaimError,
76 },
77 Refund {
78 refund_txid: String,
79 refund_tx: String,
80 state: RefundState,
81 },
82 InstantClaim {
83 status: InstantClaimStatus,
84 },
85 RefundBroadcastState {
90 refund_txid: String,
91 state: RefundState,
92 },
93 MaxClaimFee {
97 max_fee: Option<MaxFee>,
98 },
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
104pub struct WatchedDepositAddress {
105 pub address: String,
106 pub issued_at: u64,
109 pub seen: bool,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
115pub enum UpdateWatchedAddressPayload {
116 Watch { issued_at: u64 },
119 Seen,
121 Unwatch { issued_at: u64 },
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
130pub struct ChainTransaction {
131 pub txid: String,
132 pub block_height: u32,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
138pub struct StoredWatchtowerExitOutput {
139 pub txid: String,
140 pub vout: u32,
141 pub amount_sats: u64,
142 pub script_pubkey: String,
143 pub block_height: u32,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
150#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
151pub struct WatchtowerExitRecovery {
152 pub txid: String,
153 pub transaction_hex: String,
154 pub output_amount_sats: u64,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
161pub struct LeafRecovery {
162 pub leaf_id: String,
163 pub chain_checked_at: Option<u64>,
167 pub watchtower_exit_output: Option<StoredWatchtowerExitOutput>,
168 pub watchtower_exit_recoveries: Vec<WatchtowerExitRecovery>,
170 pub watchtower_exit_spend: Option<ChainTransaction>,
172 pub unilateral_exit_sweep: Option<ChainTransaction>,
174}
175
176#[derive(Debug, Clone, Default, PartialEq, Eq)]
179#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
180pub struct UpdateLeafRecovery {
181 pub leaf_id: String,
182 pub chain_checked_at: Option<u64>,
184 pub watchtower_exit_output: Option<StoredWatchtowerExitOutput>,
188 pub watchtower_exit_recovery: Option<WatchtowerExitRecovery>,
190 pub watchtower_exit_spend: Option<ChainTransaction>,
192 pub unilateral_exit_sweep: Option<ChainTransaction>,
194}
195
196#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
197pub struct SetLnurlMetadataItem {
198 pub payment_hash: String,
199 pub sender_comment: Option<String>,
200 pub nostr_zap_request: Option<String>,
201 pub nostr_zap_receipt: Option<String>,
202 pub lightning_address: Option<String>,
203}
204
205impl From<lnurl_models::ListMetadataMetadata> for SetLnurlMetadataItem {
206 fn from(value: lnurl_models::ListMetadataMetadata) -> Self {
207 SetLnurlMetadataItem {
208 payment_hash: value.payment_hash,
209 sender_comment: value.sender_comment,
210 nostr_zap_request: value.nostr_zap_request,
211 nostr_zap_receipt: value.nostr_zap_receipt,
212 lightning_address: value.lightning_address,
213 }
214 }
215}
216
217#[derive(Debug, Error, Clone)]
219#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
220pub enum StorageError {
221 #[error("Connection error: {0}")]
224 Connection(String),
225
226 #[error("Underlying implementation error: {0}")]
227 Implementation(String),
228
229 #[error("Failed to initialize database: {0}")]
231 InitializationError(String),
232
233 #[error("Failed to serialize/deserialize data: {0}")]
234 Serialization(String),
235
236 #[error("Not found")]
237 NotFound,
238}
239
240impl From<serde_json::Error> for StorageError {
241 fn from(e: serde_json::Error) -> Self {
242 StorageError::Serialization(e.to_string())
243 }
244}
245
246impl From<std::num::TryFromIntError> for StorageError {
247 fn from(e: std::num::TryFromIntError) -> Self {
248 StorageError::Implementation(format!("integer overflow: {e}"))
249 }
250}
251
252#[derive(Debug, Clone)]
254#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
255pub enum ConversionFilter {
256 AmmRefundNeeded,
258 OrchestraPending,
260 BoltzPending,
264}
265
266#[derive(Debug, Clone)]
268#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
269pub enum StoragePaymentDetailsFilter {
270 Spark {
271 htlc_status: Option<Vec<SparkHtlcStatus>>,
272 conversion_filter: Option<ConversionFilter>,
273 },
274 Token {
275 conversion_filter: Option<ConversionFilter>,
276 tx_hash: Option<String>,
277 tx_type: Option<TokenTransactionType>,
278 },
279 Lightning {
280 htlc_status: Option<Vec<SparkHtlcStatus>>,
281 conversion_filter: Option<ConversionFilter>,
282 },
283}
284
285impl From<PaymentDetailsFilter> for StoragePaymentDetailsFilter {
286 fn from(filter: PaymentDetailsFilter) -> Self {
287 match filter {
288 PaymentDetailsFilter::Spark {
289 htlc_status,
290 conversion_refund_needed,
291 } => StoragePaymentDetailsFilter::Spark {
292 htlc_status,
293 conversion_filter: conversion_refund_needed
294 .and_then(|v| v.then_some(ConversionFilter::AmmRefundNeeded)),
295 },
296 PaymentDetailsFilter::Token {
297 conversion_refund_needed,
298 tx_hash,
299 tx_type,
300 } => StoragePaymentDetailsFilter::Token {
301 conversion_filter: conversion_refund_needed
302 .and_then(|v| v.then_some(ConversionFilter::AmmRefundNeeded)),
303 tx_hash,
304 tx_type,
305 },
306 PaymentDetailsFilter::Lightning { htlc_status } => {
307 StoragePaymentDetailsFilter::Lightning {
308 htlc_status,
309 conversion_filter: None,
310 }
311 }
312 }
313 }
314}
315
316impl From<StoragePaymentDetailsFilter> for PaymentDetailsFilter {
317 fn from(filter: StoragePaymentDetailsFilter) -> Self {
318 match filter {
319 StoragePaymentDetailsFilter::Spark {
320 htlc_status,
321 conversion_filter,
322 } => PaymentDetailsFilter::Spark {
323 htlc_status,
324 conversion_refund_needed: conversion_filter
325 .map(|f| matches!(f, ConversionFilter::AmmRefundNeeded)),
326 },
327 StoragePaymentDetailsFilter::Token {
328 conversion_filter,
329 tx_hash,
330 tx_type,
331 } => PaymentDetailsFilter::Token {
332 conversion_refund_needed: conversion_filter
333 .map(|f| matches!(f, ConversionFilter::AmmRefundNeeded)),
334 tx_hash,
335 tx_type,
336 },
337 StoragePaymentDetailsFilter::Lightning { htlc_status, .. } => {
338 PaymentDetailsFilter::Lightning { htlc_status }
339 }
340 }
341 }
342}
343
344#[derive(Debug, Clone, Default)]
347#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
348pub struct StorageListPaymentsRequest {
349 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
350 pub type_filter: Option<Vec<PaymentType>>,
351 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
352 pub status_filter: Option<Vec<PaymentStatus>>,
353 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
354 pub asset_filter: Option<AssetFilter>,
355 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
356 pub payment_details_filter: Option<Vec<StoragePaymentDetailsFilter>>,
357 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
358 pub from_timestamp: Option<u64>,
359 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
360 pub to_timestamp: Option<u64>,
361 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
362 pub offset: Option<u32>,
363 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
364 pub limit: Option<u32>,
365 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
366 pub sort_ascending: Option<bool>,
367}
368
369impl From<ListPaymentsRequest> for StorageListPaymentsRequest {
370 fn from(request: ListPaymentsRequest) -> Self {
371 StorageListPaymentsRequest {
372 type_filter: request.type_filter,
373 status_filter: request.status_filter,
374 asset_filter: request.asset_filter,
375 payment_details_filter: request
376 .payment_details_filter
377 .map(|filters| filters.into_iter().map(Into::into).collect()),
378 from_timestamp: request.from_timestamp,
379 to_timestamp: request.to_timestamp,
380 offset: request.offset,
381 limit: request.limit,
382 sort_ascending: request.sort_ascending,
383 }
384 }
385}
386
387impl From<StorageListPaymentsRequest> for ListPaymentsRequest {
388 fn from(request: StorageListPaymentsRequest) -> Self {
389 ListPaymentsRequest {
390 type_filter: request.type_filter,
391 status_filter: request.status_filter,
392 asset_filter: request.asset_filter,
393 payment_details_filter: request
394 .payment_details_filter
395 .map(|filters| filters.into_iter().map(Into::into).collect()),
396 from_timestamp: request.from_timestamp,
397 to_timestamp: request.to_timestamp,
398 offset: request.offset,
399 limit: request.limit,
400 sort_ascending: request.sort_ascending,
401 }
402 }
403}
404
405#[derive(Clone, Default, Deserialize, Serialize)]
407#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
408pub struct PaymentMetadata {
409 #[serde(skip_serializing_if = "Option::is_none")]
410 pub parent_payment_id: Option<String>,
411 #[serde(skip_serializing_if = "Option::is_none")]
412 pub lnurl_pay_info: Option<LnurlPayInfo>,
413 #[serde(skip_serializing_if = "Option::is_none")]
414 pub lnurl_withdraw_info: Option<LnurlWithdrawInfo>,
415 #[serde(skip_serializing_if = "Option::is_none")]
416 pub lnurl_description: Option<String>,
417 #[serde(
420 skip_serializing_if = "Option::is_none",
421 deserialize_with = "deserialize_conversion_info_with_default_type",
422 default
423 )]
424 pub conversion_info: Option<ConversionInfo>,
425 #[serde(skip_serializing_if = "Option::is_none")]
426 pub conversion_status: Option<ConversionStatus>,
427}
428
429#[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
430pub(crate) fn parse_payment_status(value: &str) -> Result<PaymentStatus, StorageError> {
431 value
432 .parse()
433 .map_err(|e| StorageError::Implementation(format!("invalid payment status: {e}")))
434}
435
436fn deserialize_conversion_info_with_default_type<'de, D>(
441 deserializer: D,
442) -> Result<Option<ConversionInfo>, D::Error>
443where
444 D: serde::Deserializer<'de>,
445{
446 use serde::de::Deserialize;
447
448 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
449 match value {
450 None => Ok(None),
451 Some(mut v) => {
452 if let Some(obj) = v.as_object_mut() {
453 obj.entry("type")
454 .or_insert_with(|| serde_json::Value::String("amm".to_string()));
455 }
456 match serde_json::from_value::<ConversionInfo>(v) {
457 Ok(info) => Ok(Some(info)),
458 Err(_) => Ok(None),
459 }
460 }
461 }
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize)]
473#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
474#[serde(rename_all = "camelCase")]
475pub struct StoredCrossChainSwap {
476 pub provider: String,
478 pub id: String,
480 pub is_terminal: bool,
483 pub updated_at: u64,
486 pub data: String,
488 pub secrets: String,
491}
492
493#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
495#[async_trait]
496pub trait Storage: Send + Sync {
497 async fn delete_cached_item(&self, key: String) -> Result<(), StorageError>;
498 async fn get_cached_item(&self, key: String) -> Result<Option<String>, StorageError>;
499 async fn set_cached_item(&self, key: String, value: String) -> Result<(), StorageError>;
500 async fn list_payments(
510 &self,
511 request: StorageListPaymentsRequest,
512 ) -> Result<Vec<Payment>, StorageError>;
513
514 async fn apply_payment_update(&self, payment: Payment) -> Result<bool, StorageError>;
523
524 async fn insert_payment_metadata(
535 &self,
536 payment_id: String,
537 metadata: PaymentMetadata,
538 ) -> Result<(), StorageError>;
539
540 async fn get_payment_by_id(&self, id: String) -> Result<Payment, StorageError>;
549
550 async fn get_payment_by_invoice(
558 &self,
559 invoice: String,
560 ) -> Result<Option<Payment>, StorageError>;
561
562 async fn get_payments_by_parent_ids(
573 &self,
574 parent_payment_ids: Vec<String>,
575 ) -> Result<HashMap<String, Vec<Payment>>, StorageError>;
576
577 async fn add_deposit(
589 &self,
590 txid: String,
591 vout: u32,
592 amount_sats: u64,
593 is_mature: bool,
594 ) -> Result<(), StorageError>;
595
596 async fn delete_deposit(&self, txid: String, vout: u32) -> Result<(), StorageError>;
606
607 async fn list_deposits(&self) -> Result<Vec<DepositInfo>, StorageError>;
612
613 async fn update_deposit(
624 &self,
625 txid: String,
626 vout: u32,
627 payload: UpdateDepositPayload,
628 ) -> Result<(), StorageError>;
629
630 async fn list_watched_deposit_addresses(
633 &self,
634 ) -> Result<Vec<WatchedDepositAddress>, StorageError>;
635
636 async fn update_watched_deposit_address(
639 &self,
640 address: String,
641 payload: UpdateWatchedAddressPayload,
642 ) -> Result<(), StorageError>;
643
644 async fn list_leaf_recoveries(&self) -> Result<Vec<LeafRecovery>, StorageError>;
646
647 async fn update_leaf_recovery(&self, update: UpdateLeafRecovery) -> Result<(), StorageError>;
651
652 async fn set_lnurl_metadata(
653 &self,
654 metadata: Vec<SetLnurlMetadataItem>,
655 ) -> Result<(), StorageError>;
656
657 async fn list_contacts(
659 &self,
660 request: ListContactsRequest,
661 ) -> Result<Vec<Contact>, StorageError>;
662
663 async fn get_contact(&self, id: String) -> Result<Contact, StorageError>;
665
666 async fn insert_contact(&self, contact: Contact) -> Result<(), StorageError>;
669
670 async fn delete_contact(&self, id: String) -> Result<(), StorageError>;
672
673 async fn set_cross_chain_swap(&self, swap: StoredCrossChainSwap) -> Result<(), StorageError>;
675
676 async fn get_cross_chain_swap(
678 &self,
679 provider: String,
680 id: String,
681 ) -> Result<Option<StoredCrossChainSwap>, StorageError>;
682
683 async fn list_active_cross_chain_swaps(
686 &self,
687 provider: String,
688 ) -> Result<Vec<StoredCrossChainSwap>, StorageError>;
689
690 async fn add_outgoing_change(
692 &self,
693 record: UnversionedRecordChange,
694 ) -> Result<u64, StorageError>;
695 async fn complete_outgoing_sync(
696 &self,
697 record: Record,
698 local_revision: u64,
699 ) -> Result<(), StorageError>;
700 async fn get_pending_outgoing_changes(
701 &self,
702 limit: u32,
703 ) -> Result<Vec<OutgoingChange>, StorageError>;
704
705 async fn get_last_revision(&self) -> Result<u64, StorageError>;
712
713 async fn insert_incoming_records(&self, records: Vec<Record>) -> Result<(), StorageError>;
715
716 async fn delete_incoming_record(&self, record: Record) -> Result<(), StorageError>;
718
719 async fn get_incoming_records(&self, limit: u32) -> Result<Vec<IncomingChange>, StorageError>;
721
722 async fn get_latest_outgoing_change(&self) -> Result<Option<OutgoingChange>, StorageError>;
724
725 async fn update_record_from_incoming(&self, record: Record) -> Result<(), StorageError>;
727}
728
729pub(crate) struct ObjectCacheRepository {
730 storage: Arc<dyn Storage>,
731}
732
733impl ObjectCacheRepository {
734 pub(crate) fn new(storage: Arc<dyn Storage>) -> Self {
735 ObjectCacheRepository { storage }
736 }
737
738 pub(crate) async fn save_account_info(
739 &self,
740 value: &CachedAccountInfo,
741 ) -> Result<(), StorageError> {
742 self.storage
743 .set_cached_item(ACCOUNT_INFO_KEY.to_string(), serde_json::to_string(value)?)
744 .await?;
745 Ok(())
746 }
747
748 pub(crate) async fn fetch_account_info(
749 &self,
750 ) -> Result<Option<CachedAccountInfo>, StorageError> {
751 let value = self
752 .storage
753 .get_cached_item(ACCOUNT_INFO_KEY.to_string())
754 .await?;
755 match value {
756 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
757 None => Ok(None),
758 }
759 }
760
761 pub(crate) async fn save_sync_info(&self, value: &CachedSyncInfo) -> Result<(), StorageError> {
762 self.storage
763 .set_cached_item(SYNC_OFFSET_KEY.to_string(), serde_json::to_string(value)?)
764 .await?;
765 Ok(())
766 }
767
768 pub(crate) async fn fetch_sync_info(&self) -> Result<Option<CachedSyncInfo>, StorageError> {
769 let value = self
770 .storage
771 .get_cached_item(SYNC_OFFSET_KEY.to_string())
772 .await?;
773 match value {
774 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
775 None => Ok(None),
776 }
777 }
778
779 pub(crate) async fn save_published_package(
784 &self,
785 package_id: &str,
786 payment_id: &str,
787 ) -> Result<(), StorageError> {
788 self.storage
789 .set_cached_item(
790 format!("{PUBLISHED_PACKAGE_KEY_PREFIX}{package_id}"),
791 payment_id.to_string(),
792 )
793 .await?;
794 Ok(())
795 }
796
797 pub(crate) async fn fetch_published_package(
798 &self,
799 package_id: &str,
800 ) -> Result<Option<String>, StorageError> {
801 self.storage
802 .get_cached_item(format!("{PUBLISHED_PACKAGE_KEY_PREFIX}{package_id}"))
803 .await
804 }
805
806 pub(crate) async fn save_recoverable_funds(&self, sats: u64) -> Result<(), StorageError> {
807 self.storage
808 .set_cached_item(RECOVERABLE_FUNDS_KEY.to_string(), sats.to_string())
809 .await
810 }
811
812 pub(crate) async fn fetch_recoverable_funds(&self) -> Result<Option<u64>, StorageError> {
813 let value = self
814 .storage
815 .get_cached_item(RECOVERABLE_FUNDS_KEY.to_string())
816 .await?;
817 match value {
818 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
819 None => Ok(None),
820 }
821 }
822
823 pub(crate) async fn save_tx(&self, txid: &str, value: &CachedTx) -> Result<(), StorageError> {
824 self.storage
825 .set_cached_item(
826 format!("{TX_CACHE_KEY}-{txid}"),
827 serde_json::to_string(value)?,
828 )
829 .await?;
830 Ok(())
831 }
832
833 pub(crate) async fn delete_tx(&self, txid: &str) -> Result<(), StorageError> {
834 self.storage
835 .delete_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
836 .await
837 }
838
839 pub(crate) async fn fetch_tx(&self, txid: &str) -> Result<Option<CachedTx>, StorageError> {
840 let value = self
841 .storage
842 .get_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
843 .await?;
844 match value {
845 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
846 None => Ok(None),
847 }
848 }
849
850 pub(crate) async fn save_lightning_address(
851 &self,
852 value: &LightningAddressInfo,
853 recovered: bool,
854 ) -> Result<(), StorageError> {
855 let cached = CachedLightningAddress {
856 address: Some(value.clone()),
857 recovered,
858 };
859 self.storage
860 .set_cached_item(
861 LIGHTNING_ADDRESS_KEY.to_string(),
862 serde_json::to_string(&cached)?,
863 )
864 .await?;
865 Ok(())
866 }
867
868 pub(crate) async fn delete_lightning_address(
870 &self,
871 recovered: bool,
872 ) -> Result<(), StorageError> {
873 let cached = CachedLightningAddress {
874 address: None,
875 recovered,
876 };
877 self.storage
878 .set_cached_item(
879 LIGHTNING_ADDRESS_KEY.to_string(),
880 serde_json::to_string(&cached)?,
881 )
882 .await?;
883 Ok(())
884 }
885
886 pub(crate) async fn fetch_lightning_address(
891 &self,
892 ) -> Result<Option<Option<LightningAddressInfo>>, StorageError> {
893 let value = self
894 .storage
895 .get_cached_item(LIGHTNING_ADDRESS_KEY.to_string())
896 .await?;
897 match value {
898 Some(value) => {
899 let cached = parse_cached_lightning_address(&value)?;
900 Ok(Some(cached.address))
901 }
902 None => Ok(None),
903 }
904 }
905
906 pub(crate) async fn save_token_metadata(
907 &self,
908 value: &TokenMetadata,
909 ) -> Result<(), StorageError> {
910 self.storage
911 .set_cached_item(
912 format!("{TOKEN_METADATA_KEY_PREFIX}{}", value.identifier),
913 serde_json::to_string(value)?,
914 )
915 .await?;
916 Ok(())
917 }
918
919 pub(crate) async fn fetch_token_metadata(
920 &self,
921 identifier: &str,
922 ) -> Result<Option<TokenMetadata>, StorageError> {
923 let value = self
924 .storage
925 .get_cached_item(format!("{TOKEN_METADATA_KEY_PREFIX}{identifier}"))
926 .await?;
927 match value {
928 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
929 None => Ok(None),
930 }
931 }
932
933 pub(crate) async fn save_payment_metadata(
934 &self,
935 identifier: &str,
936 value: &PaymentMetadata,
937 ) -> Result<(), StorageError> {
938 self.storage
939 .set_cached_item(
940 format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"),
941 serde_json::to_string(value)?,
942 )
943 .await?;
944 Ok(())
945 }
946
947 pub(crate) async fn fetch_payment_metadata(
948 &self,
949 identifier: &str,
950 ) -> Result<Option<PaymentMetadata>, StorageError> {
951 let value = self
952 .storage
953 .get_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"))
954 .await?;
955 match value {
956 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
957 None => Ok(None),
958 }
959 }
960
961 pub(crate) async fn delete_payment_metadata(
962 &self,
963 identifier: &str,
964 ) -> Result<(), StorageError> {
965 self.storage
966 .delete_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"))
967 .await?;
968 Ok(())
969 }
970
971 pub(crate) async fn save_spark_private_mode_initialized(&self) -> Result<(), StorageError> {
972 self.storage
973 .set_cached_item(
974 SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string(),
975 "true".to_string(),
976 )
977 .await?;
978 Ok(())
979 }
980
981 pub(crate) async fn fetch_spark_private_mode_initialized(&self) -> Result<bool, StorageError> {
982 let value = self
983 .storage
984 .get_cached_item(SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string())
985 .await?;
986 match value {
987 Some(value) => Ok(value == "true"),
988 None => Ok(false),
989 }
990 }
991
992 pub(crate) async fn save_rewards_delegation(
993 &self,
994 value: &CachedRewardsDelegation,
995 ) -> Result<(), StorageError> {
996 self.storage
997 .set_cached_item(
998 FLASHNET_REWARDS_DELEGATION_KEY.to_string(),
999 serde_json::to_string(value)?,
1000 )
1001 .await?;
1002 Ok(())
1003 }
1004
1005 pub(crate) async fn fetch_rewards_delegation(
1006 &self,
1007 ) -> Result<Option<CachedRewardsDelegation>, StorageError> {
1008 let value = self
1009 .storage
1010 .get_cached_item(FLASHNET_REWARDS_DELEGATION_KEY.to_string())
1011 .await?;
1012 match value {
1013 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
1014 None => Ok(None),
1015 }
1016 }
1017
1018 pub(crate) async fn save_stable_balance_active_label(
1019 &self,
1020 label: &str,
1021 ) -> Result<(), StorageError> {
1022 self.storage
1023 .set_cached_item(
1024 STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string(),
1025 label.to_string(),
1026 )
1027 .await
1028 }
1029
1030 pub(crate) async fn fetch_stable_balance_active_label(
1031 &self,
1032 ) -> Result<Option<String>, StorageError> {
1033 self.storage
1034 .get_cached_item(STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string())
1035 .await
1036 }
1037
1038 pub(crate) async fn save_stable_balance_deactivated(&self) -> Result<(), StorageError> {
1042 self.save_stable_balance_active_label("").await
1043 }
1044
1045 pub(crate) async fn save_pending_lightning_sends(
1050 &self,
1051 pending: &[super::sdk::PendingLightningSend],
1052 ) -> Result<(), StorageError> {
1053 self.storage
1054 .set_cached_item(
1055 PENDING_LIGHTNING_SENDS_KEY.to_string(),
1056 serde_json::to_string(pending)?,
1057 )
1058 .await?;
1059 Ok(())
1060 }
1061
1062 pub(crate) async fn fetch_pending_lightning_sends(
1063 &self,
1064 ) -> Result<Vec<super::sdk::PendingLightningSend>, StorageError> {
1065 let value = self
1066 .storage
1067 .get_cached_item(PENDING_LIGHTNING_SENDS_KEY.to_string())
1068 .await?;
1069 match value {
1070 Some(value) => Ok(serde_json::from_str(&value)?),
1071 None => Ok(Vec::new()),
1072 }
1073 }
1074
1075 pub(crate) async fn save_pending_conversions(
1076 &self,
1077 pending: &super::stable_balance::PendingQueue,
1078 ) -> Result<(), StorageError> {
1079 self.storage
1080 .set_cached_item(
1081 PENDING_CONVERSIONS_KEY.to_string(),
1082 serde_json::to_string(pending)?,
1083 )
1084 .await?;
1085 Ok(())
1086 }
1087
1088 pub(crate) async fn fetch_pending_conversions(
1089 &self,
1090 ) -> Result<Option<super::stable_balance::PendingQueue>, StorageError> {
1091 let value = self
1092 .storage
1093 .get_cached_item(PENDING_CONVERSIONS_KEY.to_string())
1094 .await?;
1095 match value {
1096 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
1097 None => Ok(None),
1098 }
1099 }
1100
1101 pub(crate) async fn delete_pending_conversions(&self) -> Result<(), StorageError> {
1102 self.storage
1103 .delete_cached_item(PENDING_CONVERSIONS_KEY.to_string())
1104 .await
1105 }
1106
1107 pub(crate) async fn save_conversion_backoff(
1108 &self,
1109 backoff: &super::stable_balance::ConversionBackoff,
1110 ) -> Result<(), StorageError> {
1111 self.storage
1112 .set_cached_item(
1113 CONVERSION_BACKOFF_KEY.to_string(),
1114 serde_json::to_string(backoff)?,
1115 )
1116 .await?;
1117 Ok(())
1118 }
1119
1120 pub(crate) async fn fetch_conversion_backoff(
1121 &self,
1122 ) -> Result<Option<super::stable_balance::ConversionBackoff>, StorageError> {
1123 let value = self
1124 .storage
1125 .get_cached_item(CONVERSION_BACKOFF_KEY.to_string())
1126 .await?;
1127 match value {
1128 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
1129 None => Ok(None),
1130 }
1131 }
1132
1133 pub(crate) async fn delete_conversion_backoff(&self) -> Result<(), StorageError> {
1134 self.storage
1135 .delete_cached_item(CONVERSION_BACKOFF_KEY.to_string())
1136 .await
1137 }
1138
1139 pub(crate) async fn save_lnurl_metadata_updated_after(
1140 &self,
1141 offset: i64,
1142 ) -> Result<(), StorageError> {
1143 self.storage
1144 .set_cached_item(
1145 LNURL_METADATA_UPDATED_AFTER_KEY.to_string(),
1146 offset.to_string(),
1147 )
1148 .await?;
1149 Ok(())
1150 }
1151
1152 pub(crate) async fn fetch_lnurl_metadata_updated_after(&self) -> Result<i64, StorageError> {
1153 let value = self
1154 .storage
1155 .get_cached_item(LNURL_METADATA_UPDATED_AFTER_KEY.to_string())
1156 .await?;
1157 match value {
1158 Some(value) => Ok(value.parse().map_err(|_| {
1159 StorageError::Serialization("invalid lnurl_metadata_updated_after".to_string())
1160 })?),
1161 None => Ok(0),
1162 }
1163 }
1164
1165 pub(crate) async fn get_last_sync_time(&self) -> Result<Option<u64>, StorageError> {
1166 let value = self
1167 .storage
1168 .get_cached_item(LAST_SYNC_TIME_KEY.to_string())
1169 .await?;
1170 match value {
1171 Some(v) => Ok(Some(v.parse().map_err(|_| {
1172 StorageError::Serialization("invalid last_sync_time".to_string())
1173 })?)),
1174 None => Ok(None),
1175 }
1176 }
1177
1178 pub(crate) async fn set_last_sync_time(&self, time: u64) -> Result<(), StorageError> {
1179 self.storage
1180 .set_cached_item(LAST_SYNC_TIME_KEY.to_string(), time.to_string())
1181 .await
1182 }
1183
1184 pub(crate) async fn get_last_synced_at(&self) -> Result<Option<u64>, StorageError> {
1185 let value = self
1186 .storage
1187 .get_cached_item(LAST_SYNCED_AT_KEY.to_string())
1188 .await?;
1189 match value {
1190 Some(v) => Ok(Some(v.parse().map_err(|_| {
1191 StorageError::Serialization("invalid last_synced_at".to_string())
1192 })?)),
1193 None => Ok(None),
1194 }
1195 }
1196
1197 pub(crate) async fn set_last_synced_at(&self, time: u64) -> Result<(), StorageError> {
1198 self.storage
1199 .set_cached_item(LAST_SYNCED_AT_KEY.to_string(), time.to_string())
1200 .await
1201 }
1202}
1203
1204#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1206pub(crate) enum CachedRewardsDelegation {
1207 Done,
1209 RetryAfter(u64),
1211}
1212
1213#[derive(Serialize, Deserialize, Default)]
1214pub(crate) struct CachedAccountInfo {
1215 pub(crate) balance_sats: u64,
1216 #[serde(default)]
1217 pub(crate) token_balances: HashMap<String, TokenBalance>,
1218}
1219
1220#[derive(Serialize, Deserialize, Default)]
1221pub(crate) struct CachedSyncInfo {
1222 pub(crate) offset: u64,
1223 pub(crate) last_synced_final_token_payment_id: Option<String>,
1224}
1225
1226#[derive(Serialize, Deserialize, Default)]
1227pub(crate) struct CachedTx {
1228 pub(crate) raw_tx: String,
1229}
1230
1231#[cfg(feature = "test-utils")]
1232pub mod tests;