breez_sdk_spark/persist/
mod.rs

1#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2pub(crate) mod sqlite;
3
4use std::sync::Arc;
5
6use macros::async_trait;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::{DepositClaimError, DepositInfo, LnurlPayInfo, models::Payment};
11
12const ACCOUNT_INFO_KEY: &str = "account_info";
13const SYNC_OFFSET_KEY: &str = "sync_offset";
14const TX_CACHE_KEY: &str = "tx_cache";
15const STATIC_DEPOSIT_ADDRESS_CACHE_KEY: &str = "static_deposit_address";
16
17#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
18pub enum UpdateDepositPayload {
19    ClaimError {
20        error: DepositClaimError,
21    },
22    Refund {
23        refund_txid: String,
24        refund_tx: String,
25    },
26}
27
28/// Errors that can occur during storage operations
29#[derive(Debug, Error, Clone)]
30#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
31pub enum StorageError {
32    #[error("Underline implementation error: {0}")]
33    Implementation(String),
34
35    /// Database initialization error
36    #[error("Failed to initialize database: {0}")]
37    InitializationError(String),
38
39    #[error("Failed to serialize/deserialize data: {0}")]
40    Serialization(String),
41}
42
43impl From<serde_json::Error> for StorageError {
44    fn from(e: serde_json::Error) -> Self {
45        StorageError::Serialization(e.to_string())
46    }
47}
48
49/// Metadata associated with a payment that cannot be extracted from the Spark operator.
50#[derive(Clone)]
51#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
52pub struct PaymentMetadata {
53    pub lnurl_pay_info: Option<LnurlPayInfo>,
54}
55
56/// Trait for persistent storage
57#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
58#[async_trait]
59pub trait Storage: Send + Sync {
60    async fn get_cached_item(&self, key: String) -> Result<Option<String>, StorageError>;
61    async fn set_cached_item(&self, key: String, value: String) -> Result<(), StorageError>;
62    /// Lists payments with pagination
63    ///
64    /// # Arguments
65    ///
66    /// * `offset` - Number of records to skip
67    /// * `limit` - Maximum number of records to return
68    ///
69    /// # Returns
70    ///
71    /// A vector of payments or a `StorageError`
72    async fn list_payments(
73        &self,
74        offset: Option<u32>,
75        limit: Option<u32>,
76    ) -> Result<Vec<Payment>, StorageError>;
77
78    /// Inserts a payment into storage
79    ///
80    /// # Arguments
81    ///
82    /// * `payment` - The payment to insert
83    ///
84    /// # Returns
85    ///
86    /// Success or a `StorageError`
87    async fn insert_payment(&self, payment: Payment) -> Result<(), StorageError>;
88
89    /// Inserts payment metadata into storage
90    ///
91    /// # Arguments
92    ///
93    /// * `payment_id` - The ID of the payment
94    /// * `metadata` - The metadata to insert
95    ///
96    /// # Returns
97    ///
98    /// Success or a `StorageError`
99    async fn set_payment_metadata(
100        &self,
101        payment_id: String,
102        metadata: PaymentMetadata,
103    ) -> Result<(), StorageError>;
104
105    /// Gets a payment by its ID
106    /// # Arguments
107    ///
108    /// * `id` - The ID of the payment to retrieve
109    ///
110    /// # Returns
111    ///
112    /// The payment if found or None if not found
113    async fn get_payment_by_id(&self, id: String) -> Result<Payment, StorageError>;
114
115    /// Add a deposit to storage
116    /// # Arguments
117    ///
118    /// * `txid` - The transaction ID of the deposit
119    /// * `vout` - The output index of the deposit
120    /// * `amount_sats` - The amount of the deposit in sats
121    ///
122    /// # Returns
123    ///
124    /// Success or a `StorageError`
125    async fn add_deposit(
126        &self,
127        txid: String,
128        vout: u32,
129        amount_sats: u64,
130    ) -> Result<(), StorageError>;
131
132    /// Removes an unclaimed deposit from storage
133    /// # Arguments
134    ///
135    /// * `txid` - The transaction ID of the deposit
136    /// * `vout` - The output index of the deposit
137    ///
138    /// # Returns
139    ///
140    /// Success or a `StorageError`
141    async fn delete_deposit(&self, txid: String, vout: u32) -> Result<(), StorageError>;
142
143    /// Lists all unclaimed deposits from storage
144    /// # Returns
145    ///
146    /// A vector of `DepositInfo` or a `StorageError`
147    async fn list_deposits(&self) -> Result<Vec<DepositInfo>, StorageError>;
148
149    /// Updates or inserts unclaimed deposit details
150    /// # Arguments
151    ///
152    /// * `txid` - The transaction ID of the deposit
153    /// * `vout` - The output index of the deposit
154    /// * `payload` - The payload for the update
155    ///
156    /// # Returns
157    ///
158    /// Success or a `StorageError`
159    async fn update_deposit(
160        &self,
161        txid: String,
162        vout: u32,
163        payload: UpdateDepositPayload,
164    ) -> Result<(), StorageError>;
165}
166
167pub(crate) struct ObjectCacheRepository {
168    storage: Arc<dyn Storage>,
169}
170
171impl ObjectCacheRepository {
172    pub(crate) fn new(storage: Arc<dyn Storage>) -> Self {
173        ObjectCacheRepository { storage }
174    }
175
176    pub(crate) async fn save_account_info(
177        &self,
178        value: &CachedAccountInfo,
179    ) -> Result<(), StorageError> {
180        self.storage
181            .set_cached_item(ACCOUNT_INFO_KEY.to_string(), serde_json::to_string(value)?)
182            .await?;
183        Ok(())
184    }
185
186    pub(crate) async fn fetch_account_info(
187        &self,
188    ) -> Result<Option<CachedAccountInfo>, StorageError> {
189        let value = self
190            .storage
191            .get_cached_item(ACCOUNT_INFO_KEY.to_string())
192            .await?;
193        match value {
194            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
195            None => Ok(None),
196        }
197    }
198
199    pub(crate) async fn save_sync_info(&self, value: &CachedSyncInfo) -> Result<(), StorageError> {
200        self.storage
201            .set_cached_item(SYNC_OFFSET_KEY.to_string(), serde_json::to_string(value)?)
202            .await?;
203        Ok(())
204    }
205
206    pub(crate) async fn fetch_sync_info(&self) -> Result<Option<CachedSyncInfo>, StorageError> {
207        let value = self
208            .storage
209            .get_cached_item(SYNC_OFFSET_KEY.to_string())
210            .await?;
211        match value {
212            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
213            None => Ok(None),
214        }
215    }
216
217    pub(crate) async fn save_tx(&self, txid: &str, value: &CachedTx) -> Result<(), StorageError> {
218        self.storage
219            .set_cached_item(
220                format!("{TX_CACHE_KEY}-{txid}"),
221                serde_json::to_string(value)?,
222            )
223            .await?;
224        Ok(())
225    }
226
227    pub(crate) async fn fetch_tx(&self, txid: &str) -> Result<Option<CachedTx>, StorageError> {
228        let value = self
229            .storage
230            .get_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
231            .await?;
232        match value {
233            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
234            None => Ok(None),
235        }
236    }
237
238    pub(crate) async fn save_static_deposit_address(
239        &self,
240        value: &StaticDepositAddress,
241    ) -> Result<(), StorageError> {
242        self.storage
243            .set_cached_item(
244                STATIC_DEPOSIT_ADDRESS_CACHE_KEY.to_string(),
245                serde_json::to_string(value)?,
246            )
247            .await?;
248        Ok(())
249    }
250
251    pub(crate) async fn fetch_static_deposit_address(
252        &self,
253    ) -> Result<Option<StaticDepositAddress>, StorageError> {
254        let value = self
255            .storage
256            .get_cached_item(STATIC_DEPOSIT_ADDRESS_CACHE_KEY.to_string())
257            .await?;
258        match value {
259            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
260            None => Ok(None),
261        }
262    }
263}
264
265#[derive(Serialize, Deserialize, Default)]
266pub(crate) struct CachedAccountInfo {
267    pub(crate) balance_sats: u64,
268}
269
270#[derive(Serialize, Deserialize, Default)]
271pub(crate) struct CachedSyncInfo {
272    pub(crate) offset: u64,
273}
274
275#[derive(Serialize, Deserialize, Default)]
276pub(crate) struct CachedTx {
277    pub(crate) raw_tx: String,
278}
279
280#[derive(Serialize, Deserialize, Default)]
281pub(crate) struct StaticDepositAddress {
282    pub(crate) address: String,
283}
284
285#[cfg(feature = "test-utils")]
286pub mod tests {
287    use crate::{
288        DepositClaimError, Payment, PaymentDetails, PaymentMethod, PaymentStatus, PaymentType,
289        Storage, UpdateDepositPayload,
290    };
291    use chrono::Utc;
292
293    pub async fn test_sqlite_storage(storage: Box<dyn Storage>) {
294        // Create test payment
295        let payment = Payment {
296            id: "pmt123".to_string(),
297            payment_type: PaymentType::Send,
298            status: PaymentStatus::Completed,
299            amount: 100_000,
300            fees: 1000,
301            timestamp: Utc::now().timestamp().try_into().unwrap(),
302            method: PaymentMethod::Spark,
303            details: Some(PaymentDetails::Spark),
304        };
305
306        // Insert payment
307        storage.insert_payment(payment.clone()).await.unwrap();
308
309        // List payments
310        let payments = storage.list_payments(Some(0), Some(10)).await.unwrap();
311        assert_eq!(payments.len(), 1);
312        assert_eq!(payments[0].id, payment.id);
313        assert_eq!(payments[0].payment_type, payment.payment_type);
314        assert_eq!(payments[0].status, payment.status);
315        assert_eq!(payments[0].amount, payment.amount);
316        assert_eq!(payments[0].fees, payment.fees);
317        assert!(matches!(payments[0].details, Some(PaymentDetails::Spark)));
318
319        // Get payment by ID
320        let retrieved_payment = storage.get_payment_by_id(payment.id.clone()).await.unwrap();
321        assert_eq!(retrieved_payment.id, payment.id);
322        assert_eq!(retrieved_payment.payment_type, payment.payment_type);
323        assert_eq!(retrieved_payment.status, payment.status);
324        assert_eq!(retrieved_payment.amount, payment.amount);
325        assert_eq!(retrieved_payment.fees, payment.fees);
326        assert!(matches!(
327            retrieved_payment.details,
328            Some(PaymentDetails::Spark)
329        ));
330    }
331
332    pub async fn test_unclaimed_deposits_crud(storage: Box<dyn Storage>) {
333        // Initially, list should be empty
334        let deposits = storage.list_deposits().await.unwrap();
335        assert_eq!(deposits.len(), 0);
336
337        // Add first deposit
338        storage
339            .add_deposit("tx123".to_string(), 0, 50000)
340            .await
341            .unwrap();
342        let deposits = storage.list_deposits().await.unwrap();
343        assert_eq!(deposits.len(), 1);
344        assert_eq!(deposits[0].txid, "tx123");
345        assert_eq!(deposits[0].vout, 0);
346        assert_eq!(deposits[0].amount_sats, 50000);
347        assert!(deposits[0].claim_error.is_none());
348
349        // Add second deposit
350        storage
351            .add_deposit("tx456".to_string(), 1, 75000)
352            .await
353            .unwrap();
354        storage
355            .update_deposit(
356                "tx456".to_string(),
357                1,
358                UpdateDepositPayload::ClaimError {
359                    error: DepositClaimError::Generic {
360                        message: "Test error".to_string(),
361                    },
362                },
363            )
364            .await
365            .unwrap();
366        let deposits = storage.list_deposits().await.unwrap();
367        assert_eq!(deposits.len(), 2);
368
369        // Find deposit2 in the list
370        let deposit2_found = deposits.iter().find(|d| d.txid == "tx456").unwrap();
371        assert_eq!(deposit2_found.vout, 1);
372        assert_eq!(deposit2_found.amount_sats, 75000);
373        assert!(deposit2_found.claim_error.is_some());
374
375        // Remove first deposit
376        storage
377            .delete_deposit("tx123".to_string(), 0)
378            .await
379            .unwrap();
380        let deposits = storage.list_deposits().await.unwrap();
381        assert_eq!(deposits.len(), 1);
382        assert_eq!(deposits[0].txid, "tx456");
383
384        // Remove second deposit
385        storage
386            .delete_deposit("tx456".to_string(), 1)
387            .await
388            .unwrap();
389        let deposits = storage.list_deposits().await.unwrap();
390        assert_eq!(deposits.len(), 0);
391    }
392
393    pub async fn test_deposit_refunds(storage: Box<dyn Storage>) {
394        // Add the initial deposit
395        storage
396            .add_deposit("test_tx_123".to_string(), 0, 100_000)
397            .await
398            .unwrap();
399        let deposits = storage.list_deposits().await.unwrap();
400        assert_eq!(deposits.len(), 1);
401        assert_eq!(deposits[0].txid, "test_tx_123");
402        assert_eq!(deposits[0].vout, 0);
403        assert_eq!(deposits[0].amount_sats, 100_000);
404        assert!(deposits[0].claim_error.is_none());
405
406        // Update the deposit refund information
407        storage
408            .update_deposit(
409                "test_tx_123".to_string(),
410                0,
411                UpdateDepositPayload::Refund {
412                    refund_txid: "refund_tx_id_456".to_string(),
413                    refund_tx: "0200000001abcd1234...".to_string(),
414                },
415            )
416            .await
417            .unwrap();
418
419        // Verify that the deposit information remains unchanged
420        let deposits = storage.list_deposits().await.unwrap();
421        assert_eq!(deposits.len(), 1);
422        assert_eq!(deposits[0].txid, "test_tx_123");
423        assert_eq!(deposits[0].vout, 0);
424        assert_eq!(deposits[0].amount_sats, 100_000);
425        assert!(deposits[0].claim_error.is_none());
426        assert_eq!(
427            deposits[0].refund_tx_id,
428            Some("refund_tx_id_456".to_string())
429        );
430        assert_eq!(
431            deposits[0].refund_tx,
432            Some("0200000001abcd1234...".to_string())
433        );
434    }
435}