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