Skip to main content

breez_sdk_spark/persist/
mod.rs

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