breez_sdk_spark/persist/
mod.rs

1pub(crate) mod path;
2#[cfg(all(
3    feature = "postgres",
4    not(all(target_family = "wasm", target_os = "unknown"))
5))]
6pub mod postgres;
7#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
8pub(crate) mod sqlite;
9
10use std::{collections::HashMap, sync::Arc};
11
12use macros::async_trait;
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16use crate::{
17    AssetFilter, Contact, ConversionInfo, ConversionStatus, DepositClaimError, DepositInfo,
18    LightningAddressInfo, ListContactsRequest, ListPaymentsRequest, LnurlPayInfo,
19    LnurlWithdrawInfo, PaymentDetailsFilter, PaymentStatus, PaymentType, SparkHtlcStatus,
20    TokenBalance, TokenMetadata, TokenTransactionType,
21    models::Payment,
22    sync_storage::{IncomingChange, OutgoingChange, Record, UnversionedRecordChange},
23};
24
25const ACCOUNT_INFO_KEY: &str = "account_info";
26const LAST_SYNC_TIME_KEY: &str = "last_sync_time";
27pub(crate) const LIGHTNING_ADDRESS_KEY: &str = "lightning_address";
28const LNURL_METADATA_UPDATED_AFTER_KEY: &str = "lnurl_metadata_updated_after";
29const SYNC_OFFSET_KEY: &str = "sync_offset";
30const TX_CACHE_KEY: &str = "tx_cache";
31// Note: the key "static_deposit_address" may still exist in storage from older versions.
32const TOKEN_METADATA_KEY_PREFIX: &str = "token_metadata_";
33const PAYMENT_METADATA_KEY_PREFIX: &str = "payment_metadata";
34const SPARK_PRIVATE_MODE_INITIALIZED_KEY: &str = "spark_private_mode_initialized";
35pub(crate) const STABLE_BALANCE_ACTIVE_LABEL_KEY: &str = "stable_balance_active_label";
36const PENDING_CONVERSIONS_KEY: &str = "pending_conversions";
37
38/// Wrapper stored in the cache that carries context about whether the value
39/// was written as part of a recovery or a client-initiated change.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub(crate) struct CachedLightningAddress {
42    pub address: Option<LightningAddressInfo>,
43    pub recovered: bool,
44}
45
46/// Parses a cached lightning address value.
47pub(crate) fn parse_cached_lightning_address(
48    value: &str,
49) -> Result<CachedLightningAddress, StorageError> {
50    serde_json::from_str(value).map_err(|e| StorageError::Serialization(e.to_string()))
51}
52
53#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
54pub enum UpdateDepositPayload {
55    ClaimError {
56        error: DepositClaimError,
57    },
58    Refund {
59        refund_txid: String,
60        refund_tx: String,
61    },
62}
63
64#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
65pub struct SetLnurlMetadataItem {
66    pub payment_hash: String,
67    pub sender_comment: Option<String>,
68    pub nostr_zap_request: Option<String>,
69    pub nostr_zap_receipt: Option<String>,
70}
71
72impl From<lnurl_models::ListMetadataMetadata> for SetLnurlMetadataItem {
73    fn from(value: lnurl_models::ListMetadataMetadata) -> Self {
74        SetLnurlMetadataItem {
75            payment_hash: value.payment_hash,
76            sender_comment: value.sender_comment,
77            nostr_zap_request: value.nostr_zap_request,
78            nostr_zap_receipt: value.nostr_zap_receipt,
79        }
80    }
81}
82
83/// Errors that can occur during storage operations
84#[derive(Debug, Error, Clone)]
85#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
86pub enum StorageError {
87    /// Connection-related errors (pool exhaustion, timeouts, connection refused).
88    /// These are often transient and may be retried.
89    #[error("Connection error: {0}")]
90    Connection(String),
91
92    #[error("Underlying implementation error: {0}")]
93    Implementation(String),
94
95    /// Database initialization error
96    #[error("Failed to initialize database: {0}")]
97    InitializationError(String),
98
99    #[error("Failed to serialize/deserialize data: {0}")]
100    Serialization(String),
101
102    #[error("Not found")]
103    NotFound,
104}
105
106impl From<serde_json::Error> for StorageError {
107    fn from(e: serde_json::Error) -> Self {
108        StorageError::Serialization(e.to_string())
109    }
110}
111
112impl From<std::num::TryFromIntError> for StorageError {
113    fn from(e: std::num::TryFromIntError) -> Self {
114        StorageError::Implementation(format!("integer overflow: {e}"))
115    }
116}
117
118/// Storage-internal variant of [`PaymentDetailsFilter`].
119#[derive(Debug, Clone)]
120#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
121pub enum StoragePaymentDetailsFilter {
122    Spark {
123        htlc_status: Option<Vec<SparkHtlcStatus>>,
124        conversion_refund_needed: Option<bool>,
125    },
126    Token {
127        conversion_refund_needed: Option<bool>,
128        tx_hash: Option<String>,
129        tx_type: Option<TokenTransactionType>,
130    },
131    Lightning {
132        htlc_status: Option<Vec<SparkHtlcStatus>>,
133    },
134}
135
136impl From<PaymentDetailsFilter> for StoragePaymentDetailsFilter {
137    fn from(filter: PaymentDetailsFilter) -> Self {
138        match filter {
139            PaymentDetailsFilter::Spark {
140                htlc_status,
141                conversion_refund_needed,
142            } => StoragePaymentDetailsFilter::Spark {
143                htlc_status,
144                conversion_refund_needed,
145            },
146            PaymentDetailsFilter::Token {
147                conversion_refund_needed,
148                tx_hash,
149                tx_type,
150            } => StoragePaymentDetailsFilter::Token {
151                conversion_refund_needed,
152                tx_hash,
153                tx_type,
154            },
155            PaymentDetailsFilter::Lightning { htlc_status } => {
156                StoragePaymentDetailsFilter::Lightning { htlc_status }
157            }
158        }
159    }
160}
161
162impl From<StoragePaymentDetailsFilter> for PaymentDetailsFilter {
163    fn from(filter: StoragePaymentDetailsFilter) -> Self {
164        match filter {
165            StoragePaymentDetailsFilter::Spark {
166                htlc_status,
167                conversion_refund_needed,
168            } => PaymentDetailsFilter::Spark {
169                htlc_status,
170                conversion_refund_needed,
171            },
172            StoragePaymentDetailsFilter::Token {
173                conversion_refund_needed,
174                tx_hash,
175                tx_type,
176            } => PaymentDetailsFilter::Token {
177                conversion_refund_needed,
178                tx_hash,
179                tx_type,
180            },
181            StoragePaymentDetailsFilter::Lightning { htlc_status } => {
182                PaymentDetailsFilter::Lightning { htlc_status }
183            }
184        }
185    }
186}
187
188/// Storage-internal variant of [`ListPaymentsRequest`] that uses
189/// [`StoragePaymentDetailsFilter`] instead of the public [`PaymentDetailsFilter`].
190#[derive(Debug, Clone, Default)]
191#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
192pub struct StorageListPaymentsRequest {
193    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
194    pub type_filter: Option<Vec<PaymentType>>,
195    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
196    pub status_filter: Option<Vec<PaymentStatus>>,
197    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
198    pub asset_filter: Option<AssetFilter>,
199    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
200    pub payment_details_filter: Option<Vec<StoragePaymentDetailsFilter>>,
201    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
202    pub from_timestamp: Option<u64>,
203    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
204    pub to_timestamp: Option<u64>,
205    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
206    pub offset: Option<u32>,
207    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
208    pub limit: Option<u32>,
209    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
210    pub sort_ascending: Option<bool>,
211}
212
213impl From<ListPaymentsRequest> for StorageListPaymentsRequest {
214    fn from(request: ListPaymentsRequest) -> Self {
215        StorageListPaymentsRequest {
216            type_filter: request.type_filter,
217            status_filter: request.status_filter,
218            asset_filter: request.asset_filter,
219            payment_details_filter: request
220                .payment_details_filter
221                .map(|filters| filters.into_iter().map(Into::into).collect()),
222            from_timestamp: request.from_timestamp,
223            to_timestamp: request.to_timestamp,
224            offset: request.offset,
225            limit: request.limit,
226            sort_ascending: request.sort_ascending,
227        }
228    }
229}
230
231impl From<StorageListPaymentsRequest> for ListPaymentsRequest {
232    fn from(request: StorageListPaymentsRequest) -> Self {
233        ListPaymentsRequest {
234            type_filter: request.type_filter,
235            status_filter: request.status_filter,
236            asset_filter: request.asset_filter,
237            payment_details_filter: request
238                .payment_details_filter
239                .map(|filters| filters.into_iter().map(Into::into).collect()),
240            from_timestamp: request.from_timestamp,
241            to_timestamp: request.to_timestamp,
242            offset: request.offset,
243            limit: request.limit,
244            sort_ascending: request.sort_ascending,
245        }
246    }
247}
248
249/// Metadata associated with a payment that cannot be extracted from the Spark operator.
250#[derive(Clone, Default, Deserialize, Serialize)]
251#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
252pub struct PaymentMetadata {
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub parent_payment_id: Option<String>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub lnurl_pay_info: Option<LnurlPayInfo>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub lnurl_withdraw_info: Option<LnurlWithdrawInfo>,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub lnurl_description: Option<String>,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub conversion_info: Option<ConversionInfo>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub conversion_status: Option<ConversionStatus>,
265}
266
267/// Trait for persistent storage
268#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
269#[async_trait]
270pub trait Storage: Send + Sync {
271    async fn delete_cached_item(&self, key: String) -> Result<(), StorageError>;
272    async fn get_cached_item(&self, key: String) -> Result<Option<String>, StorageError>;
273    async fn set_cached_item(&self, key: String, value: String) -> Result<(), StorageError>;
274    /// Lists payments with optional filters and pagination
275    ///
276    /// # Arguments
277    ///
278    /// * `list_payments_request` - The request to list payments
279    ///
280    /// # Returns
281    ///
282    /// A vector of payments or a `StorageError`
283    async fn list_payments(
284        &self,
285        request: StorageListPaymentsRequest,
286    ) -> Result<Vec<Payment>, StorageError>;
287
288    /// Inserts a payment into storage
289    ///
290    /// # Arguments
291    ///
292    /// * `payment` - The payment to insert
293    ///
294    /// # Returns
295    ///
296    /// Success or a `StorageError`
297    async fn insert_payment(&self, payment: Payment) -> Result<(), StorageError>;
298
299    /// Inserts payment metadata into storage
300    ///
301    /// # Arguments
302    ///
303    /// * `payment_id` - The ID of the payment
304    /// * `metadata` - The metadata to insert
305    ///
306    /// # Returns
307    ///
308    /// Success or a `StorageError`
309    async fn insert_payment_metadata(
310        &self,
311        payment_id: String,
312        metadata: PaymentMetadata,
313    ) -> Result<(), StorageError>;
314
315    /// Gets a payment by its ID
316    /// # Arguments
317    ///
318    /// * `id` - The ID of the payment to retrieve
319    ///
320    /// # Returns
321    ///
322    /// The payment if found or None if not found
323    async fn get_payment_by_id(&self, id: String) -> Result<Payment, StorageError>;
324
325    /// Gets a payment by its invoice
326    /// # Arguments
327    ///
328    /// * `invoice` - The invoice of the payment to retrieve
329    /// # Returns
330    ///
331    /// The payment if found or None if not found
332    async fn get_payment_by_invoice(
333        &self,
334        invoice: String,
335    ) -> Result<Option<Payment>, StorageError>;
336
337    /// Gets payments that have any of the specified parent payment IDs.
338    /// Used to load related payments for a set of parent payments.
339    ///
340    /// # Arguments
341    ///
342    /// * `parent_payment_ids` - The IDs of the parent payments
343    ///
344    /// # Returns
345    ///
346    /// A map of `parent_payment_id` -> Vec<Payment> or a `StorageError`
347    async fn get_payments_by_parent_ids(
348        &self,
349        parent_payment_ids: Vec<String>,
350    ) -> Result<HashMap<String, Vec<Payment>>, StorageError>;
351
352    /// Add a deposit to storage (upsert: updates `is_mature` and `amount_sats` on conflict)
353    /// # Arguments
354    ///
355    /// * `txid` - The transaction ID of the deposit
356    /// * `vout` - The output index of the deposit
357    /// * `amount_sats` - The amount of the deposit in sats
358    /// * `is_mature` - Whether the deposit UTXO has enough confirmations to be claimable
359    ///
360    /// # Returns
361    ///
362    /// Success or a `StorageError`
363    async fn add_deposit(
364        &self,
365        txid: String,
366        vout: u32,
367        amount_sats: u64,
368        is_mature: bool,
369    ) -> Result<(), StorageError>;
370
371    /// Removes an unclaimed deposit from storage
372    /// # Arguments
373    ///
374    /// * `txid` - The transaction ID of the deposit
375    /// * `vout` - The output index of the deposit
376    ///
377    /// # Returns
378    ///
379    /// Success or a `StorageError`
380    async fn delete_deposit(&self, txid: String, vout: u32) -> Result<(), StorageError>;
381
382    /// Lists all unclaimed deposits from storage
383    /// # Returns
384    ///
385    /// A vector of `DepositInfo` or a `StorageError`
386    async fn list_deposits(&self) -> Result<Vec<DepositInfo>, StorageError>;
387
388    /// Updates or inserts unclaimed deposit details
389    /// # Arguments
390    ///
391    /// * `txid` - The transaction ID of the deposit
392    /// * `vout` - The output index of the deposit
393    /// * `payload` - The payload for the update
394    ///
395    /// # Returns
396    ///
397    /// Success or a `StorageError`
398    async fn update_deposit(
399        &self,
400        txid: String,
401        vout: u32,
402        payload: UpdateDepositPayload,
403    ) -> Result<(), StorageError>;
404
405    async fn set_lnurl_metadata(
406        &self,
407        metadata: Vec<SetLnurlMetadataItem>,
408    ) -> Result<(), StorageError>;
409
410    /// Lists contacts from storage with optional pagination
411    async fn list_contacts(
412        &self,
413        request: ListContactsRequest,
414    ) -> Result<Vec<Contact>, StorageError>;
415
416    /// Gets a single contact by its ID
417    async fn get_contact(&self, id: String) -> Result<Contact, StorageError>;
418
419    /// Inserts or updates a contact in storage (upsert by id).
420    /// Preserves `created_at` on update.
421    async fn insert_contact(&self, contact: Contact) -> Result<(), StorageError>;
422
423    /// Deletes a contact by its ID
424    async fn delete_contact(&self, id: String) -> Result<(), StorageError>;
425
426    // Sync storage methods
427    async fn add_outgoing_change(
428        &self,
429        record: UnversionedRecordChange,
430    ) -> Result<u64, StorageError>;
431    async fn complete_outgoing_sync(
432        &self,
433        record: Record,
434        local_revision: u64,
435    ) -> Result<(), StorageError>;
436    async fn get_pending_outgoing_changes(
437        &self,
438        limit: u32,
439    ) -> Result<Vec<OutgoingChange>, StorageError>;
440
441    /// Get the last committed sync revision.
442    ///
443    /// The `sync_revision` table tracks the highest revision that has been committed
444    /// (i.e. acknowledged by the server or received from it). It does NOT include
445    /// pending outgoing queue ids. This value is used by the sync protocol to
446    /// request changes from the server.
447    async fn get_last_revision(&self) -> Result<u64, StorageError>;
448
449    /// Insert incoming records from remote sync
450    async fn insert_incoming_records(&self, records: Vec<Record>) -> Result<(), StorageError>;
451
452    /// Delete an incoming record after it has been processed
453    async fn delete_incoming_record(&self, record: Record) -> Result<(), StorageError>;
454
455    /// Get incoming records that need to be processed, up to the specified limit
456    async fn get_incoming_records(&self, limit: u32) -> Result<Vec<IncomingChange>, StorageError>;
457
458    /// Get the latest outgoing record if any exists
459    async fn get_latest_outgoing_change(&self) -> Result<Option<OutgoingChange>, StorageError>;
460
461    /// Update the sync state record from an incoming record
462    async fn update_record_from_incoming(&self, record: Record) -> Result<(), StorageError>;
463}
464
465pub(crate) struct ObjectCacheRepository {
466    storage: Arc<dyn Storage>,
467}
468
469impl ObjectCacheRepository {
470    pub(crate) fn new(storage: Arc<dyn Storage>) -> Self {
471        ObjectCacheRepository { storage }
472    }
473
474    pub(crate) async fn save_account_info(
475        &self,
476        value: &CachedAccountInfo,
477    ) -> Result<(), StorageError> {
478        self.storage
479            .set_cached_item(ACCOUNT_INFO_KEY.to_string(), serde_json::to_string(value)?)
480            .await?;
481        Ok(())
482    }
483
484    pub(crate) async fn fetch_account_info(
485        &self,
486    ) -> Result<Option<CachedAccountInfo>, StorageError> {
487        let value = self
488            .storage
489            .get_cached_item(ACCOUNT_INFO_KEY.to_string())
490            .await?;
491        match value {
492            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
493            None => Ok(None),
494        }
495    }
496
497    pub(crate) async fn save_sync_info(&self, value: &CachedSyncInfo) -> Result<(), StorageError> {
498        self.storage
499            .set_cached_item(SYNC_OFFSET_KEY.to_string(), serde_json::to_string(value)?)
500            .await?;
501        Ok(())
502    }
503
504    pub(crate) async fn fetch_sync_info(&self) -> Result<Option<CachedSyncInfo>, StorageError> {
505        let value = self
506            .storage
507            .get_cached_item(SYNC_OFFSET_KEY.to_string())
508            .await?;
509        match value {
510            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
511            None => Ok(None),
512        }
513    }
514
515    pub(crate) async fn save_tx(&self, txid: &str, value: &CachedTx) -> Result<(), StorageError> {
516        self.storage
517            .set_cached_item(
518                format!("{TX_CACHE_KEY}-{txid}"),
519                serde_json::to_string(value)?,
520            )
521            .await?;
522        Ok(())
523    }
524
525    pub(crate) async fn fetch_tx(&self, txid: &str) -> Result<Option<CachedTx>, StorageError> {
526        let value = self
527            .storage
528            .get_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
529            .await?;
530        match value {
531            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
532            None => Ok(None),
533        }
534    }
535
536    pub(crate) async fn save_lightning_address(
537        &self,
538        value: &LightningAddressInfo,
539        recovered: bool,
540    ) -> Result<(), StorageError> {
541        let cached = CachedLightningAddress {
542            address: Some(value.clone()),
543            recovered,
544        };
545        self.storage
546            .set_cached_item(
547                LIGHTNING_ADDRESS_KEY.to_string(),
548                serde_json::to_string(&cached)?,
549            )
550            .await?;
551        Ok(())
552    }
553
554    /// Marks the lightning address as "no address registered" by storing `None`.
555    pub(crate) async fn delete_lightning_address(
556        &self,
557        recovered: bool,
558    ) -> Result<(), StorageError> {
559        let cached = CachedLightningAddress {
560            address: None,
561            recovered,
562        };
563        self.storage
564            .set_cached_item(
565                LIGHTNING_ADDRESS_KEY.to_string(),
566                serde_json::to_string(&cached)?,
567            )
568            .await?;
569        Ok(())
570    }
571
572    /// Returns:
573    /// - `Ok(None)` — key absent, never recovered
574    /// - `Ok(Some(None))` — recovered, no address registered
575    /// - `Ok(Some(Some(info)))` — recovered, has address
576    pub(crate) async fn fetch_lightning_address(
577        &self,
578    ) -> Result<Option<Option<LightningAddressInfo>>, StorageError> {
579        let value = self
580            .storage
581            .get_cached_item(LIGHTNING_ADDRESS_KEY.to_string())
582            .await?;
583        match value {
584            Some(value) => {
585                let cached = parse_cached_lightning_address(&value)?;
586                Ok(Some(cached.address))
587            }
588            None => Ok(None),
589        }
590    }
591
592    pub(crate) async fn save_token_metadata(
593        &self,
594        value: &TokenMetadata,
595    ) -> Result<(), StorageError> {
596        self.storage
597            .set_cached_item(
598                format!("{TOKEN_METADATA_KEY_PREFIX}{}", value.identifier),
599                serde_json::to_string(value)?,
600            )
601            .await?;
602        Ok(())
603    }
604
605    pub(crate) async fn fetch_token_metadata(
606        &self,
607        identifier: &str,
608    ) -> Result<Option<TokenMetadata>, StorageError> {
609        let value = self
610            .storage
611            .get_cached_item(format!("{TOKEN_METADATA_KEY_PREFIX}{identifier}"))
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_payment_metadata(
620        &self,
621        identifier: &str,
622        value: &PaymentMetadata,
623    ) -> Result<(), StorageError> {
624        self.storage
625            .set_cached_item(
626                format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"),
627                serde_json::to_string(value)?,
628            )
629            .await?;
630        Ok(())
631    }
632
633    pub(crate) async fn fetch_payment_metadata(
634        &self,
635        identifier: &str,
636    ) -> Result<Option<PaymentMetadata>, StorageError> {
637        let value = self
638            .storage
639            .get_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}",))
640            .await?;
641        match value {
642            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
643            None => Ok(None),
644        }
645    }
646
647    pub(crate) async fn delete_payment_metadata(
648        &self,
649        identifier: &str,
650    ) -> Result<(), StorageError> {
651        self.storage
652            .delete_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}",))
653            .await?;
654        Ok(())
655    }
656
657    pub(crate) async fn save_spark_private_mode_initialized(&self) -> Result<(), StorageError> {
658        self.storage
659            .set_cached_item(
660                SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string(),
661                "true".to_string(),
662            )
663            .await?;
664        Ok(())
665    }
666
667    pub(crate) async fn fetch_spark_private_mode_initialized(&self) -> Result<bool, StorageError> {
668        let value = self
669            .storage
670            .get_cached_item(SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string())
671            .await?;
672        match value {
673            Some(value) => Ok(value == "true"),
674            None => Ok(false),
675        }
676    }
677
678    pub(crate) async fn save_stable_balance_active_label(
679        &self,
680        label: &str,
681    ) -> Result<(), StorageError> {
682        self.storage
683            .set_cached_item(
684                STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string(),
685                label.to_string(),
686            )
687            .await
688    }
689
690    pub(crate) async fn fetch_stable_balance_active_label(
691        &self,
692    ) -> Result<Option<String>, StorageError> {
693        self.storage
694            .get_cached_item(STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string())
695            .await
696    }
697
698    pub(crate) async fn delete_stable_balance_active_label(&self) -> Result<(), StorageError> {
699        self.storage
700            .delete_cached_item(STABLE_BALANCE_ACTIVE_LABEL_KEY.to_string())
701            .await
702    }
703
704    pub(crate) async fn save_pending_conversions(
705        &self,
706        pending: &[super::stable_balance::PendingConversion],
707    ) -> Result<(), StorageError> {
708        self.storage
709            .set_cached_item(
710                PENDING_CONVERSIONS_KEY.to_string(),
711                serde_json::to_string(pending)?,
712            )
713            .await?;
714        Ok(())
715    }
716
717    pub(crate) async fn fetch_pending_conversions(
718        &self,
719    ) -> Result<Option<Vec<super::stable_balance::PendingConversion>>, StorageError> {
720        let value = self
721            .storage
722            .get_cached_item(PENDING_CONVERSIONS_KEY.to_string())
723            .await?;
724        match value {
725            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
726            None => Ok(None),
727        }
728    }
729
730    pub(crate) async fn delete_pending_conversions(&self) -> Result<(), StorageError> {
731        self.storage
732            .delete_cached_item(PENDING_CONVERSIONS_KEY.to_string())
733            .await
734    }
735
736    pub(crate) async fn save_lnurl_metadata_updated_after(
737        &self,
738        offset: i64,
739    ) -> Result<(), StorageError> {
740        self.storage
741            .set_cached_item(
742                LNURL_METADATA_UPDATED_AFTER_KEY.to_string(),
743                offset.to_string(),
744            )
745            .await?;
746        Ok(())
747    }
748
749    pub(crate) async fn fetch_lnurl_metadata_updated_after(&self) -> Result<i64, StorageError> {
750        let value = self
751            .storage
752            .get_cached_item(LNURL_METADATA_UPDATED_AFTER_KEY.to_string())
753            .await?;
754        match value {
755            Some(value) => Ok(value.parse().map_err(|_| {
756                StorageError::Serialization("invalid lnurl_metadata_updated_after".to_string())
757            })?),
758            None => Ok(0),
759        }
760    }
761
762    pub(crate) async fn get_last_sync_time(&self) -> Result<Option<u64>, StorageError> {
763        let value = self
764            .storage
765            .get_cached_item(LAST_SYNC_TIME_KEY.to_string())
766            .await?;
767        match value {
768            Some(v) => Ok(Some(v.parse().map_err(|_| {
769                StorageError::Serialization("invalid last_sync_time".to_string())
770            })?)),
771            None => Ok(None),
772        }
773    }
774
775    pub(crate) async fn set_last_sync_time(&self, time: u64) -> Result<(), StorageError> {
776        self.storage
777            .set_cached_item(LAST_SYNC_TIME_KEY.to_string(), time.to_string())
778            .await
779    }
780}
781
782#[derive(Serialize, Deserialize, Default)]
783pub(crate) struct CachedAccountInfo {
784    pub(crate) balance_sats: u64,
785    #[serde(default)]
786    pub(crate) token_balances: HashMap<String, TokenBalance>,
787}
788
789#[derive(Serialize, Deserialize, Default)]
790pub(crate) struct CachedSyncInfo {
791    pub(crate) offset: u64,
792    pub(crate) last_synced_final_token_payment_id: Option<String>,
793}
794
795#[derive(Serialize, Deserialize, Default)]
796pub(crate) struct CachedTx {
797    pub(crate) raw_tx: String,
798}
799
800#[cfg(feature = "test-utils")]
801pub mod tests;