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