breez_sdk_spark/persist/
mod.rs

1pub(crate) mod path;
2#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
3pub(crate) mod sqlite;
4
5use std::{collections::HashMap, sync::Arc};
6
7use breez_sdk_common::lnurl::withdraw::LnurlWithdrawRequestDetails;
8use macros::async_trait;
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::{
13    DepositClaimError, DepositInfo, LightningAddressInfo, ListPaymentsRequest, LnurlPayInfo,
14    LnurlWithdrawInfo, TokenBalance, TokenMetadata, models::Payment,
15};
16
17const ACCOUNT_INFO_KEY: &str = "account_info";
18const LIGHTNING_ADDRESS_KEY: &str = "lightning_address";
19const SYNC_OFFSET_KEY: &str = "sync_offset";
20const TX_CACHE_KEY: &str = "tx_cache";
21const STATIC_DEPOSIT_ADDRESS_CACHE_KEY: &str = "static_deposit_address";
22const TOKEN_METADATA_KEY_PREFIX: &str = "token_metadata_";
23const PAYMENT_REQUEST_METADATA_KEY_PREFIX: &str = "payment_request_metadata";
24const SPARK_PRIVATE_MODE_INITIALIZED_KEY: &str = "spark_private_mode_initialized";
25
26#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
27pub enum UpdateDepositPayload {
28    ClaimError {
29        error: DepositClaimError,
30    },
31    Refund {
32        refund_txid: String,
33        refund_tx: String,
34    },
35}
36
37/// Errors that can occur during storage operations
38#[derive(Debug, Error, Clone)]
39#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
40pub enum StorageError {
41    #[error("Underline implementation error: {0}")]
42    Implementation(String),
43
44    /// Database initialization error
45    #[error("Failed to initialize database: {0}")]
46    InitializationError(String),
47
48    #[error("Failed to serialize/deserialize data: {0}")]
49    Serialization(String),
50}
51
52impl From<serde_json::Error> for StorageError {
53    fn from(e: serde_json::Error) -> Self {
54        StorageError::Serialization(e.to_string())
55    }
56}
57
58/// Metadata associated with a payment that cannot be extracted from the Spark operator.
59#[derive(Clone, Default, Deserialize, Serialize)]
60#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
61pub struct PaymentMetadata {
62    pub lnurl_pay_info: Option<LnurlPayInfo>,
63    pub lnurl_withdraw_info: Option<LnurlWithdrawInfo>,
64    pub lnurl_description: Option<String>,
65}
66
67/// Trait for persistent storage
68#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
69#[async_trait]
70pub trait Storage: Send + Sync {
71    async fn delete_cached_item(&self, key: String) -> Result<(), StorageError>;
72    async fn get_cached_item(&self, key: String) -> Result<Option<String>, StorageError>;
73    async fn set_cached_item(&self, key: String, value: String) -> Result<(), StorageError>;
74    /// Lists payments with optional filters and pagination
75    ///
76    /// # Arguments
77    ///
78    /// * `list_payments_request` - The request to list payments
79    ///
80    /// # Returns
81    ///
82    /// A vector of payments or a `StorageError`
83    async fn list_payments(
84        &self,
85        request: ListPaymentsRequest,
86    ) -> Result<Vec<Payment>, StorageError>;
87
88    /// Inserts a payment into storage
89    ///
90    /// # Arguments
91    ///
92    /// * `payment` - The payment to insert
93    ///
94    /// # Returns
95    ///
96    /// Success or a `StorageError`
97    async fn insert_payment(&self, payment: Payment) -> Result<(), StorageError>;
98
99    /// Inserts payment metadata into storage
100    ///
101    /// # Arguments
102    ///
103    /// * `payment_id` - The ID of the payment
104    /// * `metadata` - The metadata to insert
105    ///
106    /// # Returns
107    ///
108    /// Success or a `StorageError`
109    async fn set_payment_metadata(
110        &self,
111        payment_id: String,
112        metadata: PaymentMetadata,
113    ) -> Result<(), StorageError>;
114
115    /// Gets a payment by its ID
116    /// # Arguments
117    ///
118    /// * `id` - The ID of the payment to retrieve
119    ///
120    /// # Returns
121    ///
122    /// The payment if found or None if not found
123    async fn get_payment_by_id(&self, id: String) -> Result<Payment, StorageError>;
124
125    /// Gets a payment by its invoice
126    /// # Arguments
127    ///
128    /// * `invoice` - The invoice of the payment to retrieve
129    /// # Returns
130    ///
131    /// The payment if found or None if not found
132    async fn get_payment_by_invoice(
133        &self,
134        invoice: String,
135    ) -> Result<Option<Payment>, StorageError>;
136
137    /// Add a deposit to storage
138    /// # Arguments
139    ///
140    /// * `txid` - The transaction ID of the deposit
141    /// * `vout` - The output index of the deposit
142    /// * `amount_sats` - The amount of the deposit in sats
143    ///
144    /// # Returns
145    ///
146    /// Success or a `StorageError`
147    async fn add_deposit(
148        &self,
149        txid: String,
150        vout: u32,
151        amount_sats: u64,
152    ) -> Result<(), StorageError>;
153
154    /// Removes an unclaimed deposit from storage
155    /// # Arguments
156    ///
157    /// * `txid` - The transaction ID of the deposit
158    /// * `vout` - The output index of the deposit
159    ///
160    /// # Returns
161    ///
162    /// Success or a `StorageError`
163    async fn delete_deposit(&self, txid: String, vout: u32) -> Result<(), StorageError>;
164
165    /// Lists all unclaimed deposits from storage
166    /// # Returns
167    ///
168    /// A vector of `DepositInfo` or a `StorageError`
169    async fn list_deposits(&self) -> Result<Vec<DepositInfo>, StorageError>;
170
171    /// Updates or inserts unclaimed deposit details
172    /// # Arguments
173    ///
174    /// * `txid` - The transaction ID of the deposit
175    /// * `vout` - The output index of the deposit
176    /// * `payload` - The payload for the update
177    ///
178    /// # Returns
179    ///
180    /// Success or a `StorageError`
181    async fn update_deposit(
182        &self,
183        txid: String,
184        vout: u32,
185        payload: UpdateDepositPayload,
186    ) -> Result<(), StorageError>;
187}
188
189pub(crate) struct ObjectCacheRepository {
190    storage: Arc<dyn Storage>,
191}
192
193impl ObjectCacheRepository {
194    pub(crate) fn new(storage: Arc<dyn Storage>) -> Self {
195        ObjectCacheRepository { storage }
196    }
197
198    pub(crate) async fn save_account_info(
199        &self,
200        value: &CachedAccountInfo,
201    ) -> Result<(), StorageError> {
202        self.storage
203            .set_cached_item(ACCOUNT_INFO_KEY.to_string(), serde_json::to_string(value)?)
204            .await?;
205        Ok(())
206    }
207
208    pub(crate) async fn fetch_account_info(
209        &self,
210    ) -> Result<Option<CachedAccountInfo>, StorageError> {
211        let value = self
212            .storage
213            .get_cached_item(ACCOUNT_INFO_KEY.to_string())
214            .await?;
215        match value {
216            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
217            None => Ok(None),
218        }
219    }
220
221    pub(crate) async fn save_sync_info(&self, value: &CachedSyncInfo) -> Result<(), StorageError> {
222        self.storage
223            .set_cached_item(SYNC_OFFSET_KEY.to_string(), serde_json::to_string(value)?)
224            .await?;
225        Ok(())
226    }
227
228    pub(crate) async fn fetch_sync_info(&self) -> Result<Option<CachedSyncInfo>, StorageError> {
229        let value = self
230            .storage
231            .get_cached_item(SYNC_OFFSET_KEY.to_string())
232            .await?;
233        match value {
234            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
235            None => Ok(None),
236        }
237    }
238
239    pub(crate) async fn save_tx(&self, txid: &str, value: &CachedTx) -> Result<(), StorageError> {
240        self.storage
241            .set_cached_item(
242                format!("{TX_CACHE_KEY}-{txid}"),
243                serde_json::to_string(value)?,
244            )
245            .await?;
246        Ok(())
247    }
248
249    pub(crate) async fn fetch_tx(&self, txid: &str) -> Result<Option<CachedTx>, StorageError> {
250        let value = self
251            .storage
252            .get_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
253            .await?;
254        match value {
255            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
256            None => Ok(None),
257        }
258    }
259
260    pub(crate) async fn save_static_deposit_address(
261        &self,
262        value: &StaticDepositAddress,
263    ) -> Result<(), StorageError> {
264        self.storage
265            .set_cached_item(
266                STATIC_DEPOSIT_ADDRESS_CACHE_KEY.to_string(),
267                serde_json::to_string(value)?,
268            )
269            .await?;
270        Ok(())
271    }
272
273    pub(crate) async fn fetch_static_deposit_address(
274        &self,
275    ) -> Result<Option<StaticDepositAddress>, StorageError> {
276        let value = self
277            .storage
278            .get_cached_item(STATIC_DEPOSIT_ADDRESS_CACHE_KEY.to_string())
279            .await?;
280        match value {
281            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
282            None => Ok(None),
283        }
284    }
285
286    pub(crate) async fn save_lightning_address(
287        &self,
288        value: &LightningAddressInfo,
289    ) -> Result<(), StorageError> {
290        self.storage
291            .set_cached_item(
292                LIGHTNING_ADDRESS_KEY.to_string(),
293                serde_json::to_string(value)?,
294            )
295            .await?;
296        Ok(())
297    }
298
299    pub(crate) async fn delete_lightning_address(&self) -> Result<(), StorageError> {
300        self.storage
301            .delete_cached_item(LIGHTNING_ADDRESS_KEY.to_string())
302            .await?;
303        Ok(())
304    }
305
306    pub(crate) async fn fetch_lightning_address(
307        &self,
308    ) -> Result<Option<LightningAddressInfo>, StorageError> {
309        let value = self
310            .storage
311            .get_cached_item(LIGHTNING_ADDRESS_KEY.to_string())
312            .await?;
313        match value {
314            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
315            None => Ok(None),
316        }
317    }
318
319    pub(crate) async fn save_token_metadata(
320        &self,
321        value: &TokenMetadata,
322    ) -> Result<(), StorageError> {
323        self.storage
324            .set_cached_item(
325                format!("{TOKEN_METADATA_KEY_PREFIX}{}", value.identifier),
326                serde_json::to_string(value)?,
327            )
328            .await?;
329        Ok(())
330    }
331
332    pub(crate) async fn fetch_token_metadata(
333        &self,
334        identifier: &str,
335    ) -> Result<Option<TokenMetadata>, StorageError> {
336        let value = self
337            .storage
338            .get_cached_item(format!("{TOKEN_METADATA_KEY_PREFIX}{identifier}"))
339            .await?;
340        match value {
341            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
342            None => Ok(None),
343        }
344    }
345
346    pub(crate) async fn save_payment_request_metadata(
347        &self,
348        value: &PaymentRequestMetadata,
349    ) -> Result<(), StorageError> {
350        self.storage
351            .set_cached_item(
352                format!(
353                    "{PAYMENT_REQUEST_METADATA_KEY_PREFIX}-{}",
354                    value.payment_request
355                ),
356                serde_json::to_string(value)?,
357            )
358            .await?;
359        Ok(())
360    }
361
362    pub(crate) async fn fetch_payment_request_metadata(
363        &self,
364        payment_request: &str,
365    ) -> Result<Option<PaymentRequestMetadata>, StorageError> {
366        let value = self
367            .storage
368            .get_cached_item(format!(
369                "{PAYMENT_REQUEST_METADATA_KEY_PREFIX}-{payment_request}",
370            ))
371            .await?;
372        match value {
373            Some(value) => Ok(Some(serde_json::from_str(&value)?)),
374            None => Ok(None),
375        }
376    }
377
378    pub(crate) async fn delete_payment_request_metadata(
379        &self,
380        payment_request: &str,
381    ) -> Result<(), StorageError> {
382        self.storage
383            .delete_cached_item(format!(
384                "{PAYMENT_REQUEST_METADATA_KEY_PREFIX}-{payment_request}",
385            ))
386            .await?;
387        Ok(())
388    }
389
390    pub(crate) async fn save_spark_private_mode_initialized(&self) -> Result<(), StorageError> {
391        self.storage
392            .set_cached_item(
393                SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string(),
394                "true".to_string(),
395            )
396            .await?;
397        Ok(())
398    }
399
400    pub(crate) async fn fetch_spark_private_mode_initialized(&self) -> Result<bool, StorageError> {
401        let value = self
402            .storage
403            .get_cached_item(SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string())
404            .await?;
405        match value {
406            Some(value) => Ok(value == "true"),
407            None => Ok(false),
408        }
409    }
410}
411
412#[derive(Serialize, Deserialize, Default)]
413pub(crate) struct CachedAccountInfo {
414    pub(crate) balance_sats: u64,
415    #[serde(default)]
416    pub(crate) token_balances: HashMap<String, TokenBalance>,
417}
418
419#[derive(Serialize, Deserialize, Default)]
420pub(crate) struct CachedSyncInfo {
421    pub(crate) offset: u64,
422    pub(crate) last_synced_final_token_payment_id: Option<String>,
423}
424
425#[derive(Serialize, Deserialize, Default)]
426pub(crate) struct CachedTx {
427    pub(crate) raw_tx: String,
428}
429
430#[derive(Clone, Deserialize, Serialize)]
431pub(crate) struct PaymentRequestMetadata {
432    pub payment_request: String,
433    pub lnurl_withdraw_request_details: LnurlWithdrawRequestDetails,
434}
435
436#[derive(Serialize, Deserialize, Default)]
437pub(crate) struct StaticDepositAddress {
438    pub(crate) address: String,
439}
440
441#[cfg(feature = "test-utils")]
442pub mod tests {
443    use breez_sdk_common::lnurl::withdraw::LnurlWithdrawRequestDetails;
444
445    use chrono::Utc;
446
447    use crate::{
448        DepositClaimError, ListPaymentsRequest, LnurlWithdrawInfo, Payment, PaymentDetails,
449        PaymentMetadata, PaymentMethod, PaymentStatus, PaymentType, Storage, UpdateDepositPayload,
450        persist::{ObjectCacheRepository, PaymentRequestMetadata},
451        sync_storage::{Record, RecordId, SyncStorage, UnversionedRecordChange},
452    };
453
454    #[allow(clippy::too_many_lines)]
455    pub async fn test_sqlite_sync_storage(storage: Box<dyn SyncStorage>) {
456        use std::collections::HashMap;
457
458        // Test 1: Initial state - get_last_revision should return 0
459        let last_revision = storage.get_last_revision().await.unwrap();
460        assert_eq!(last_revision, 0, "Initial last revision should be 0");
461
462        // Test 2: No pending outgoing changes initially
463        let pending = storage.get_pending_outgoing_changes(10).await.unwrap();
464        assert_eq!(pending.len(), 0, "Should have no pending outgoing changes");
465
466        // Test 3: No incoming records initially
467        let incoming = storage.get_incoming_records(10).await.unwrap();
468        assert_eq!(incoming.len(), 0, "Should have no incoming records");
469
470        // Test 4: No latest outgoing change initially
471        let latest = storage.get_latest_outgoing_change().await.unwrap();
472        assert!(latest.is_none(), "Should have no latest outgoing change");
473
474        // Test 5: Add outgoing change (create new record)
475        let mut updated_fields = HashMap::new();
476        updated_fields.insert("name".to_string(), "\"Alice\"".to_string());
477        updated_fields.insert("age".to_string(), "30".to_string());
478
479        let change1 = UnversionedRecordChange {
480            id: RecordId::new("user".to_string(), "user1".to_string()),
481            schema_version: "1.0.0".to_string(),
482            updated_fields: updated_fields.clone(),
483        };
484
485        let revision1 = storage.add_outgoing_change(change1).await.unwrap();
486        assert!(revision1 > 0, "First revision should be greater than 0");
487
488        // Test 6: Check pending outgoing changes
489        let pending = storage.get_pending_outgoing_changes(10).await.unwrap();
490        assert_eq!(pending.len(), 1, "Should have 1 pending outgoing change");
491        assert_eq!(pending[0].change.id.r#type, "user");
492        assert_eq!(pending[0].change.id.data_id, "user1");
493        assert_eq!(pending[0].change.revision, revision1);
494        assert_eq!(pending[0].change.schema_version, "1.0.0");
495        assert!(
496            pending[0].parent.is_none(),
497            "First change should have no parent"
498        );
499
500        // Test 7: Get latest outgoing change
501        let latest = storage.get_latest_outgoing_change().await.unwrap();
502        assert!(latest.is_some());
503        let latest = latest.unwrap();
504        assert_eq!(latest.change.id.r#type, "user");
505        assert_eq!(latest.change.revision, revision1);
506
507        // Test 8: Complete outgoing sync (moves to sync_state)
508        let mut complete_data = HashMap::new();
509        complete_data.insert("name".to_string(), "\"Alice\"".to_string());
510        complete_data.insert("age".to_string(), "30".to_string());
511
512        let completed_record = Record {
513            id: RecordId::new("user".to_string(), "user1".to_string()),
514            revision: revision1,
515            schema_version: "1.0.0".to_string(),
516            data: complete_data,
517        };
518
519        storage
520            .complete_outgoing_sync(completed_record.clone())
521            .await
522            .unwrap();
523
524        // Test 9: Pending changes should now be empty
525        let pending = storage.get_pending_outgoing_changes(10).await.unwrap();
526        assert_eq!(
527            pending.len(),
528            0,
529            "Should have no pending changes after completion"
530        );
531
532        // Test 10: Last revision should be updated
533        let last_revision = storage.get_last_revision().await.unwrap();
534        assert_eq!(
535            last_revision, revision1,
536            "Last revision should match completed revision"
537        );
538
539        // Test 11: Add another outgoing change (update existing record)
540        let mut updated_fields2 = HashMap::new();
541        updated_fields2.insert("age".to_string(), "31".to_string());
542
543        let change2 = UnversionedRecordChange {
544            id: RecordId::new("user".to_string(), "user1".to_string()),
545            schema_version: "1.0.0".to_string(),
546            updated_fields: updated_fields2,
547        };
548
549        let revision2 = storage.add_outgoing_change(change2).await.unwrap();
550        assert!(
551            revision2 > revision1,
552            "Second revision should be greater than first"
553        );
554
555        // Test 12: Check pending changes now includes parent
556        let pending = storage.get_pending_outgoing_changes(10).await.unwrap();
557        assert_eq!(pending.len(), 1, "Should have 1 pending change");
558        assert!(
559            pending[0].parent.is_some(),
560            "Update should have parent record"
561        );
562        let parent = pending[0].parent.as_ref().unwrap();
563        assert_eq!(parent.revision, revision1);
564        assert_eq!(parent.id.r#type, "user");
565
566        // Test 13: Insert incoming records
567        let mut incoming_data1 = HashMap::new();
568        incoming_data1.insert("title".to_string(), "\"Post 1\"".to_string());
569        incoming_data1.insert("content".to_string(), "\"Hello World\"".to_string());
570
571        let incoming_record1 = Record {
572            id: RecordId::new("post".to_string(), "post1".to_string()),
573            revision: 100,
574            schema_version: "1.0.0".to_string(),
575            data: incoming_data1,
576        };
577
578        let mut incoming_data2 = HashMap::new();
579        incoming_data2.insert("title".to_string(), "\"Post 2\"".to_string());
580
581        let incoming_record2 = Record {
582            id: RecordId::new("post".to_string(), "post2".to_string()),
583            revision: 101,
584            schema_version: "1.0.0".to_string(),
585            data: incoming_data2,
586        };
587
588        storage
589            .insert_incoming_records(vec![incoming_record1.clone(), incoming_record2.clone()])
590            .await
591            .unwrap();
592
593        // Test 14: Get incoming records
594        let incoming = storage.get_incoming_records(10).await.unwrap();
595        assert_eq!(incoming.len(), 2, "Should have 2 incoming records");
596        assert_eq!(incoming[0].new_state.id.r#type, "post");
597        assert_eq!(incoming[0].new_state.revision, 100);
598        assert!(
599            incoming[0].old_state.is_none(),
600            "New incoming record should have no old state"
601        );
602
603        // Test 15: Update record from incoming (moves to sync_state)
604        storage
605            .update_record_from_incoming(incoming_record1.clone())
606            .await
607            .unwrap();
608
609        // Test 16: Delete incoming record
610        storage
611            .delete_incoming_record(incoming_record1.clone())
612            .await
613            .unwrap();
614
615        // Test 17: Check incoming records after deletion
616        let incoming = storage.get_incoming_records(10).await.unwrap();
617        assert_eq!(incoming.len(), 1, "Should have 1 incoming record remaining");
618        assert_eq!(incoming[0].new_state.id.data_id, "post2");
619
620        // Test 18: Insert incoming record that updates existing state
621        let mut updated_incoming_data = HashMap::new();
622        updated_incoming_data.insert("title".to_string(), "\"Post 1 Updated\"".to_string());
623        updated_incoming_data.insert("content".to_string(), "\"Updated content\"".to_string());
624
625        let updated_incoming_record = Record {
626            id: RecordId::new("post".to_string(), "post1".to_string()),
627            revision: 102,
628            schema_version: "1.0.0".to_string(),
629            data: updated_incoming_data,
630        };
631
632        storage
633            .insert_incoming_records(vec![updated_incoming_record.clone()])
634            .await
635            .unwrap();
636
637        // Test 19: Get incoming records with old_state
638        let incoming = storage.get_incoming_records(10).await.unwrap();
639        let post1_update = incoming.iter().find(|r| r.new_state.id.data_id == "post1");
640        assert!(post1_update.is_some(), "Should find post1 update");
641        let post1_update = post1_update.unwrap();
642        assert!(
643            post1_update.old_state.is_some(),
644            "Update should have old state"
645        );
646        assert_eq!(
647            post1_update.old_state.as_ref().unwrap().revision,
648            100,
649            "Old state should be original revision"
650        );
651
652        // Test 20: Rebase pending outgoing records
653        storage.rebase_pending_outgoing_records(150).await.unwrap();
654
655        // Test 21: Check that pending outgoing change revision was updated
656        let pending = storage.get_pending_outgoing_changes(10).await.unwrap();
657        assert!(
658            pending[0].change.revision > revision2,
659            "Revision should be rebased"
660        );
661
662        // Test 22: Test limit on pending outgoing changes
663        // Add multiple changes
664        for i in 0..5 {
665            let mut fields = HashMap::new();
666            fields.insert("value".to_string(), format!("\"{i}\""));
667
668            let change = UnversionedRecordChange {
669                id: RecordId::new("test".to_string(), format!("test{i}")),
670                schema_version: "1.0.0".to_string(),
671                updated_fields: fields,
672            };
673            storage.add_outgoing_change(change).await.unwrap();
674        }
675
676        let pending_limited = storage.get_pending_outgoing_changes(3).await.unwrap();
677        assert_eq!(
678            pending_limited.len(),
679            3,
680            "Should respect limit on pending changes"
681        );
682
683        // Test 23: Test limit on incoming records
684        let incoming_limited = storage.get_incoming_records(1).await.unwrap();
685        assert_eq!(
686            incoming_limited.len(),
687            1,
688            "Should respect limit on incoming records"
689        );
690
691        // Test 24: Test ordering - pending outgoing should be ordered by revision ASC
692        let all_pending = storage.get_pending_outgoing_changes(100).await.unwrap();
693        for i in 1..all_pending.len() {
694            assert!(
695                all_pending[i].change.revision >= all_pending[i.saturating_sub(1)].change.revision,
696                "Pending changes should be ordered by revision ascending"
697            );
698        }
699
700        // Test 25: Test ordering - incoming should be ordered by revision ASC
701        let all_incoming = storage.get_incoming_records(100).await.unwrap();
702        for i in 1..all_incoming.len() {
703            assert!(
704                all_incoming[i].new_state.revision
705                    >= all_incoming[i.saturating_sub(1)].new_state.revision,
706                "Incoming records should be ordered by revision ascending"
707            );
708        }
709
710        // Test 26: Test empty insert_incoming_records
711        storage.insert_incoming_records(vec![]).await.unwrap();
712
713        // Test 27: Test different record types
714        let mut settings_fields = HashMap::new();
715        settings_fields.insert("theme".to_string(), "\"dark\"".to_string());
716
717        let settings_change = UnversionedRecordChange {
718            id: RecordId::new("settings".to_string(), "global".to_string()),
719            schema_version: "2.0.0".to_string(),
720            updated_fields: settings_fields,
721        };
722
723        let settings_revision = storage.add_outgoing_change(settings_change).await.unwrap();
724
725        let pending = storage.get_pending_outgoing_changes(100).await.unwrap();
726        let settings_pending = pending.iter().find(|p| p.change.id.r#type == "settings");
727        assert!(settings_pending.is_some(), "Should find settings change");
728        assert_eq!(
729            settings_pending.unwrap().change.schema_version,
730            "2.0.0",
731            "Should preserve schema version"
732        );
733
734        // Test 28: Complete multiple types
735        let mut complete_settings_data = HashMap::new();
736        complete_settings_data.insert("theme".to_string(), "\"dark\"".to_string());
737
738        let completed_settings = Record {
739            id: RecordId::new("settings".to_string(), "global".to_string()),
740            revision: settings_revision,
741            schema_version: "2.0.0".to_string(),
742            data: complete_settings_data,
743        };
744
745        storage
746            .complete_outgoing_sync(completed_settings)
747            .await
748            .unwrap();
749
750        let last_revision = storage.get_last_revision().await.unwrap();
751        assert!(
752            last_revision >= settings_revision,
753            "Last revision should be at least settings revision"
754        );
755    }
756
757    #[allow(clippy::too_many_lines)]
758    pub async fn test_sqlite_storage(storage: Box<dyn Storage>) {
759        use crate::models::{LnurlPayInfo, TokenMetadata};
760
761        // Test 1: Spark payment
762        let spark_payment = Payment {
763            id: "spark_pmt123".to_string(),
764            payment_type: PaymentType::Send,
765            status: PaymentStatus::Completed,
766            amount: u128::from(u64::MAX).checked_add(100_000).unwrap(),
767            fees: 1000,
768            timestamp: 5000,
769            method: PaymentMethod::Spark,
770            details: Some(PaymentDetails::Spark {
771                invoice_details: Some(crate::SparkInvoicePaymentDetails {
772                    description: Some("description".to_string()),
773                    invoice: "invoice_string".to_string(),
774                }),
775            }),
776        };
777
778        // Test 2: Token payment
779        let token_metadata = TokenMetadata {
780            identifier: "token123".to_string(),
781            issuer_public_key:
782                "02abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab".to_string(),
783            name: "Test Token".to_string(),
784            ticker: "TTK".to_string(),
785            decimals: 8,
786            max_supply: 21_000_000,
787            is_freezable: false,
788        };
789        let token_payment = Payment {
790            id: "token_pmt456".to_string(),
791            payment_type: PaymentType::Receive,
792            status: PaymentStatus::Pending,
793            amount: 50_000,
794            fees: 500,
795            timestamp: Utc::now().timestamp().try_into().unwrap(),
796            method: PaymentMethod::Token,
797            details: Some(PaymentDetails::Token {
798                metadata: token_metadata.clone(),
799                tx_hash: "tx_hash".to_string(),
800                invoice_details: Some(crate::SparkInvoicePaymentDetails {
801                    description: Some("description_2".to_string()),
802                    invoice: "invoice_string_2".to_string(),
803                }),
804            }),
805        };
806
807        // Test 3: Lightning payment with full details
808        let pay_metadata = PaymentMetadata {
809            lnurl_pay_info: Some(LnurlPayInfo {
810                ln_address: Some("test@example.com".to_string()),
811                comment: Some("Test comment".to_string()),
812                domain: Some("example.com".to_string()),
813                metadata: Some("[[\"text/plain\", \"Test metadata\"]]".to_string()),
814                processed_success_action: None,
815                raw_success_action: None,
816            }),
817            lnurl_withdraw_info: None,
818            lnurl_description: None,
819        };
820        let lightning_lnurl_pay_payment = Payment {
821            id: "lightning_pmt789".to_string(),
822            payment_type: PaymentType::Send,
823            status: PaymentStatus::Completed,
824            amount: 25_000,
825            fees: 250,
826            timestamp: Utc::now().timestamp().try_into().unwrap(),
827            method: PaymentMethod::Lightning,
828            details: Some(PaymentDetails::Lightning {
829                description: Some("Test lightning payment".to_string()),
830                preimage: Some("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab".to_string()),
831                invoice: "lnbc250n1pjqxyz9pp5abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890abcdefghijklmnopqrstuvwxyz".to_string(),
832                payment_hash: "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321".to_string(),
833                destination_pubkey: "03123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01".to_string(),
834                lnurl_pay_info: pay_metadata.lnurl_pay_info.clone(),
835                lnurl_withdraw_info: pay_metadata.lnurl_withdraw_info.clone(),
836            }),
837        };
838
839        // Test 4: Lightning payment with full details
840        let withdraw_metadata = PaymentMetadata {
841            lnurl_pay_info: None,
842            lnurl_withdraw_info: Some(LnurlWithdrawInfo {
843                withdraw_url: "http://example.com/withdraw".to_string(),
844            }),
845            lnurl_description: None,
846        };
847        let lightning_lnurl_withdraw_payment = Payment {
848            id: "lightning_pmtabc".to_string(),
849            payment_type: PaymentType::Receive,
850            status: PaymentStatus::Completed,
851            amount: 75_000,
852            fees: 750,
853            timestamp: Utc::now().timestamp().try_into().unwrap(),
854            method: PaymentMethod::Lightning,
855            details: Some(PaymentDetails::Lightning {
856                description: Some("Test lightning payment".to_string()),
857                preimage: Some("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab".to_string()),
858                invoice: "lnbc250n1pjqxyz9pp5abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890abcdefghijklmnopqrstuvwxyz".to_string(),
859                payment_hash: "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321".to_string(),
860                destination_pubkey: "03123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01".to_string(),
861                lnurl_pay_info: withdraw_metadata.lnurl_pay_info.clone(),
862                lnurl_withdraw_info: withdraw_metadata.lnurl_withdraw_info.clone(),
863            }),
864        };
865
866        // Test 5: Lightning payment with minimal details
867        let lightning_minimal_payment = Payment {
868            id: "lightning_minimal_pmt012".to_string(),
869            payment_type: PaymentType::Receive,
870            status: PaymentStatus::Failed,
871            amount: 10_000,
872            fees: 100,
873            timestamp: Utc::now().timestamp().try_into().unwrap(),
874            method: PaymentMethod::Lightning,
875            details: Some(PaymentDetails::Lightning {
876                description: None,
877                preimage: None,
878                invoice: "lnbc100n1pjqxyz9pp5def456ghi789jkl012mno345pqr678stu901vwx234yz567890abcdefghijklmnopqrstuvwxyz".to_string(),
879                payment_hash: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(),
880                destination_pubkey: "02987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba09".to_string(),
881                lnurl_pay_info: None,
882                lnurl_withdraw_info: None,
883            }),
884        };
885
886        // Test 6: Withdraw payment
887        let withdraw_payment = Payment {
888            id: "withdraw_pmt345".to_string(),
889            payment_type: PaymentType::Send,
890            status: PaymentStatus::Completed,
891            amount: 200_000,
892            fees: 2000,
893            timestamp: Utc::now().timestamp().try_into().unwrap(),
894            method: PaymentMethod::Withdraw,
895            details: Some(PaymentDetails::Withdraw {
896                tx_id: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
897                    .to_string(),
898            }),
899        };
900
901        // Test 7: Deposit payment
902        let deposit_payment = Payment {
903            id: "deposit_pmt678".to_string(),
904            payment_type: PaymentType::Receive,
905            status: PaymentStatus::Completed,
906            amount: 150_000,
907            fees: 1500,
908            timestamp: Utc::now().timestamp().try_into().unwrap(),
909            method: PaymentMethod::Deposit,
910            details: Some(PaymentDetails::Deposit {
911                tx_id: "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321fe"
912                    .to_string(),
913            }),
914        };
915
916        // Test 8: Payment with no details
917        let no_details_payment = Payment {
918            id: "no_details_pmt901".to_string(),
919            payment_type: PaymentType::Send,
920            status: PaymentStatus::Pending,
921            amount: 75_000,
922            fees: 750,
923            timestamp: Utc::now().timestamp().try_into().unwrap(),
924            method: PaymentMethod::Unknown,
925            details: None,
926        };
927
928        let test_payments = vec![
929            spark_payment.clone(),
930            token_payment.clone(),
931            lightning_lnurl_pay_payment.clone(),
932            lightning_lnurl_withdraw_payment.clone(),
933            lightning_minimal_payment.clone(),
934            withdraw_payment.clone(),
935            deposit_payment.clone(),
936            no_details_payment.clone(),
937        ];
938
939        // Insert all payments
940        for payment in &test_payments {
941            storage.insert_payment(payment.clone()).await.unwrap();
942        }
943        storage
944            .set_payment_metadata(lightning_lnurl_pay_payment.id.clone(), pay_metadata)
945            .await
946            .unwrap();
947        storage
948            .set_payment_metadata(
949                lightning_lnurl_withdraw_payment.id.clone(),
950                withdraw_metadata,
951            )
952            .await
953            .unwrap();
954
955        // List all payments
956        let payments = storage
957            .list_payments(ListPaymentsRequest {
958                offset: Some(0),
959                limit: Some(10),
960                ..Default::default()
961            })
962            .await
963            .unwrap();
964        assert_eq!(payments.len(), 8);
965
966        // Test each payment type individually
967        for (i, expected_payment) in test_payments.iter().enumerate() {
968            let retrieved_payment = storage
969                .get_payment_by_id(expected_payment.id.clone())
970                .await
971                .unwrap();
972
973            // Basic fields
974            assert_eq!(retrieved_payment.id, expected_payment.id);
975            assert_eq!(
976                retrieved_payment.payment_type,
977                expected_payment.payment_type
978            );
979            assert_eq!(retrieved_payment.status, expected_payment.status);
980            assert_eq!(retrieved_payment.amount, expected_payment.amount);
981            assert_eq!(retrieved_payment.fees, expected_payment.fees);
982            assert_eq!(retrieved_payment.method, expected_payment.method);
983
984            // Test payment details persistence
985            match (&retrieved_payment.details, &expected_payment.details) {
986                (None, None) => {}
987                (
988                    Some(PaymentDetails::Spark {
989                        invoice_details: r_invoice,
990                    }),
991                    Some(PaymentDetails::Spark {
992                        invoice_details: e_invoice,
993                    }),
994                ) => {
995                    assert_eq!(r_invoice, e_invoice);
996                }
997                (
998                    Some(PaymentDetails::Token {
999                        metadata: r_metadata,
1000                        tx_hash: r_tx_hash,
1001                        invoice_details: r_invoice,
1002                    }),
1003                    Some(PaymentDetails::Token {
1004                        metadata: e_metadata,
1005                        tx_hash: e_tx_hash,
1006                        invoice_details: e_invoice,
1007                    }),
1008                ) => {
1009                    assert_eq!(r_metadata.identifier, e_metadata.identifier);
1010                    assert_eq!(r_metadata.issuer_public_key, e_metadata.issuer_public_key);
1011                    assert_eq!(r_metadata.name, e_metadata.name);
1012                    assert_eq!(r_metadata.ticker, e_metadata.ticker);
1013                    assert_eq!(r_metadata.decimals, e_metadata.decimals);
1014                    assert_eq!(r_metadata.max_supply, e_metadata.max_supply);
1015                    assert_eq!(r_metadata.is_freezable, e_metadata.is_freezable);
1016                    assert_eq!(r_tx_hash, e_tx_hash);
1017                    assert_eq!(r_invoice, e_invoice);
1018                }
1019                (
1020                    Some(PaymentDetails::Lightning {
1021                        description: r_description,
1022                        preimage: r_preimage,
1023                        invoice: r_invoice,
1024                        payment_hash: r_hash,
1025                        destination_pubkey: r_dest_pubkey,
1026                        lnurl_pay_info: r_pay_lnurl,
1027                        lnurl_withdraw_info: r_withdraw_lnurl,
1028                    }),
1029                    Some(PaymentDetails::Lightning {
1030                        description: e_description,
1031                        preimage: e_preimage,
1032                        invoice: e_invoice,
1033                        payment_hash: e_hash,
1034                        destination_pubkey: e_dest_pubkey,
1035                        lnurl_pay_info: e_pay_lnurl,
1036                        lnurl_withdraw_info: e_withdraw_lnurl,
1037                    }),
1038                ) => {
1039                    assert_eq!(r_description, e_description);
1040                    assert_eq!(r_preimage, e_preimage);
1041                    assert_eq!(r_invoice, e_invoice);
1042                    assert_eq!(r_hash, e_hash);
1043                    assert_eq!(r_dest_pubkey, e_dest_pubkey);
1044
1045                    // Test LNURL pay info if present
1046                    match (r_pay_lnurl, e_pay_lnurl) {
1047                        (Some(r_info), Some(e_info)) => {
1048                            assert_eq!(r_info.ln_address, e_info.ln_address);
1049                            assert_eq!(r_info.comment, e_info.comment);
1050                            assert_eq!(r_info.domain, e_info.domain);
1051                            assert_eq!(r_info.metadata, e_info.metadata);
1052                        }
1053                        (None, None) => {}
1054                        _ => panic!(
1055                            "LNURL pay info mismatch for payment {}",
1056                            expected_payment.id
1057                        ),
1058                    }
1059
1060                    // Test LNURL withdraw info if present
1061                    match (r_withdraw_lnurl, e_withdraw_lnurl) {
1062                        (Some(r_info), Some(e_info)) => {
1063                            assert_eq!(r_info.withdraw_url, e_info.withdraw_url);
1064                        }
1065                        (None, None) => {}
1066                        _ => panic!(
1067                            "LNURL withdraw info mismatch for payment {}",
1068                            expected_payment.id
1069                        ),
1070                    }
1071                }
1072                (
1073                    Some(PaymentDetails::Withdraw { tx_id: r_tx_id }),
1074                    Some(PaymentDetails::Withdraw { tx_id: e_tx_id }),
1075                )
1076                | (
1077                    Some(PaymentDetails::Deposit { tx_id: r_tx_id }),
1078                    Some(PaymentDetails::Deposit { tx_id: e_tx_id }),
1079                ) => {
1080                    assert_eq!(r_tx_id, e_tx_id);
1081                }
1082                _ => panic!(
1083                    "Payment details mismatch for payment {} (index {})",
1084                    expected_payment.id, i
1085                ),
1086            }
1087        }
1088
1089        // Test filtering by payment type
1090        let send_payments = payments
1091            .iter()
1092            .filter(|p| p.payment_type == PaymentType::Send)
1093            .count();
1094        let receive_payments = payments
1095            .iter()
1096            .filter(|p| p.payment_type == PaymentType::Receive)
1097            .count();
1098        assert_eq!(send_payments, 4); // spark, lightning_lnurl_pay, withdraw, no_details
1099        assert_eq!(receive_payments, 4); // token, lightning_lnurl_withdraw, lightning_minimal, deposit
1100
1101        // Test filtering by status
1102        let completed_payments = payments
1103            .iter()
1104            .filter(|p| p.status == PaymentStatus::Completed)
1105            .count();
1106        let pending_payments = payments
1107            .iter()
1108            .filter(|p| p.status == PaymentStatus::Pending)
1109            .count();
1110        let failed_payments = payments
1111            .iter()
1112            .filter(|p| p.status == PaymentStatus::Failed)
1113            .count();
1114        assert_eq!(completed_payments, 5); // spark, lightning_lnurl_pay, lightning_lnurl_withdraw, withdraw, deposit
1115        assert_eq!(pending_payments, 2); // token, no_details
1116        assert_eq!(failed_payments, 1); // lightning_minimal
1117
1118        // Test filtering by method
1119        let lightning_count = payments
1120            .iter()
1121            .filter(|p| p.method == PaymentMethod::Lightning)
1122            .count();
1123        assert_eq!(lightning_count, 3); // lightning_lnurl_pay, lightning_lnurl_withdraw and lightning_minimal
1124    }
1125
1126    pub async fn test_unclaimed_deposits_crud(storage: Box<dyn Storage>) {
1127        // Initially, list should be empty
1128        let deposits = storage.list_deposits().await.unwrap();
1129        assert_eq!(deposits.len(), 0);
1130
1131        // Add first deposit
1132        storage
1133            .add_deposit("tx123".to_string(), 0, 50000)
1134            .await
1135            .unwrap();
1136        let deposits = storage.list_deposits().await.unwrap();
1137        assert_eq!(deposits.len(), 1);
1138        assert_eq!(deposits[0].txid, "tx123");
1139        assert_eq!(deposits[0].vout, 0);
1140        assert_eq!(deposits[0].amount_sats, 50000);
1141        assert!(deposits[0].claim_error.is_none());
1142
1143        // Add second deposit
1144        storage
1145            .add_deposit("tx456".to_string(), 1, 75000)
1146            .await
1147            .unwrap();
1148        storage
1149            .update_deposit(
1150                "tx456".to_string(),
1151                1,
1152                UpdateDepositPayload::ClaimError {
1153                    error: DepositClaimError::Generic {
1154                        message: "Test error".to_string(),
1155                    },
1156                },
1157            )
1158            .await
1159            .unwrap();
1160        let deposits = storage.list_deposits().await.unwrap();
1161        assert_eq!(deposits.len(), 2);
1162
1163        // Find deposit2 in the list
1164        let deposit2_found = deposits.iter().find(|d| d.txid == "tx456").unwrap();
1165        assert_eq!(deposit2_found.vout, 1);
1166        assert_eq!(deposit2_found.amount_sats, 75000);
1167        assert!(deposit2_found.claim_error.is_some());
1168
1169        // Remove first deposit
1170        storage
1171            .delete_deposit("tx123".to_string(), 0)
1172            .await
1173            .unwrap();
1174        let deposits = storage.list_deposits().await.unwrap();
1175        assert_eq!(deposits.len(), 1);
1176        assert_eq!(deposits[0].txid, "tx456");
1177
1178        // Remove second deposit
1179        storage
1180            .delete_deposit("tx456".to_string(), 1)
1181            .await
1182            .unwrap();
1183        let deposits = storage.list_deposits().await.unwrap();
1184        assert_eq!(deposits.len(), 0);
1185    }
1186
1187    pub async fn test_deposit_refunds(storage: Box<dyn Storage>) {
1188        // Add the initial deposit
1189        storage
1190            .add_deposit("test_tx_123".to_string(), 0, 100_000)
1191            .await
1192            .unwrap();
1193        let deposits = storage.list_deposits().await.unwrap();
1194        assert_eq!(deposits.len(), 1);
1195        assert_eq!(deposits[0].txid, "test_tx_123");
1196        assert_eq!(deposits[0].vout, 0);
1197        assert_eq!(deposits[0].amount_sats, 100_000);
1198        assert!(deposits[0].claim_error.is_none());
1199
1200        // Update the deposit refund information
1201        storage
1202            .update_deposit(
1203                "test_tx_123".to_string(),
1204                0,
1205                UpdateDepositPayload::Refund {
1206                    refund_txid: "refund_tx_id_456".to_string(),
1207                    refund_tx: "0200000001abcd1234...".to_string(),
1208                },
1209            )
1210            .await
1211            .unwrap();
1212
1213        // Verify that the deposit information remains unchanged
1214        let deposits = storage.list_deposits().await.unwrap();
1215        assert_eq!(deposits.len(), 1);
1216        assert_eq!(deposits[0].txid, "test_tx_123");
1217        assert_eq!(deposits[0].vout, 0);
1218        assert_eq!(deposits[0].amount_sats, 100_000);
1219        assert!(deposits[0].claim_error.is_none());
1220        assert_eq!(
1221            deposits[0].refund_tx_id,
1222            Some("refund_tx_id_456".to_string())
1223        );
1224        assert_eq!(
1225            deposits[0].refund_tx,
1226            Some("0200000001abcd1234...".to_string())
1227        );
1228    }
1229
1230    pub async fn test_payment_type_filtering(storage: Box<dyn Storage>) {
1231        // Create test payments with different types
1232        let send_payment = Payment {
1233            id: "send_1".to_string(),
1234            payment_type: PaymentType::Send,
1235            status: PaymentStatus::Completed,
1236            amount: 10_000,
1237            fees: 100,
1238            timestamp: 1000,
1239            method: PaymentMethod::Lightning,
1240            details: Some(PaymentDetails::Lightning {
1241                invoice: "lnbc1".to_string(),
1242                payment_hash: "hash1".to_string(),
1243                destination_pubkey: "pubkey1".to_string(),
1244                description: None,
1245                preimage: None,
1246                lnurl_pay_info: None,
1247                lnurl_withdraw_info: None,
1248            }),
1249        };
1250
1251        let receive_payment = Payment {
1252            id: "receive_1".to_string(),
1253            payment_type: PaymentType::Receive,
1254            status: PaymentStatus::Completed,
1255            amount: 20_000,
1256            fees: 200,
1257            timestamp: 2000,
1258            method: PaymentMethod::Lightning,
1259            details: Some(PaymentDetails::Lightning {
1260                invoice: "lnbc2".to_string(),
1261                payment_hash: "hash2".to_string(),
1262                destination_pubkey: "pubkey2".to_string(),
1263                description: None,
1264                preimage: None,
1265                lnurl_pay_info: None,
1266                lnurl_withdraw_info: None,
1267            }),
1268        };
1269
1270        storage.insert_payment(send_payment).await.unwrap();
1271        storage.insert_payment(receive_payment).await.unwrap();
1272
1273        // Test filter by Send type only
1274        let send_only = storage
1275            .list_payments(ListPaymentsRequest {
1276                type_filter: Some(vec![PaymentType::Send]),
1277                ..Default::default()
1278            })
1279            .await
1280            .unwrap();
1281        assert_eq!(send_only.len(), 1);
1282        assert_eq!(send_only[0].id, "send_1");
1283
1284        // Test filter by Receive type only
1285        let receive_only = storage
1286            .list_payments(ListPaymentsRequest {
1287                type_filter: Some(vec![PaymentType::Receive]),
1288                ..Default::default()
1289            })
1290            .await
1291            .unwrap();
1292        assert_eq!(receive_only.len(), 1);
1293        assert_eq!(receive_only[0].id, "receive_1");
1294
1295        // Test filter by both types
1296        let both_types = storage
1297            .list_payments(ListPaymentsRequest {
1298                type_filter: Some(vec![PaymentType::Send, PaymentType::Receive]),
1299                ..Default::default()
1300            })
1301            .await
1302            .unwrap();
1303        assert_eq!(both_types.len(), 2);
1304
1305        // Test with no filter (should return all)
1306        let all_payments = storage
1307            .list_payments(ListPaymentsRequest::default())
1308            .await
1309            .unwrap();
1310        assert_eq!(all_payments.len(), 2);
1311    }
1312
1313    pub async fn test_payment_status_filtering(storage: Box<dyn Storage>) {
1314        // Create test payments with different statuses
1315        let completed_payment = Payment {
1316            id: "completed_1".to_string(),
1317            payment_type: PaymentType::Send,
1318            status: PaymentStatus::Completed,
1319            amount: 10_000,
1320            fees: 100,
1321            timestamp: 1000,
1322            method: PaymentMethod::Spark,
1323            details: Some(PaymentDetails::Spark {
1324                invoice_details: None,
1325            }),
1326        };
1327
1328        let pending_payment = Payment {
1329            id: "pending_1".to_string(),
1330            payment_type: PaymentType::Send,
1331            status: PaymentStatus::Pending,
1332            amount: 20_000,
1333            fees: 200,
1334            timestamp: 2000,
1335            method: PaymentMethod::Spark,
1336            details: Some(PaymentDetails::Spark {
1337                invoice_details: None,
1338            }),
1339        };
1340
1341        let failed_payment = Payment {
1342            id: "failed_1".to_string(),
1343            payment_type: PaymentType::Send,
1344            status: PaymentStatus::Failed,
1345            amount: 30_000,
1346            fees: 300,
1347            timestamp: 3000,
1348            method: PaymentMethod::Spark,
1349            details: Some(PaymentDetails::Spark {
1350                invoice_details: None,
1351            }),
1352        };
1353
1354        storage.insert_payment(completed_payment).await.unwrap();
1355        storage.insert_payment(pending_payment).await.unwrap();
1356        storage.insert_payment(failed_payment).await.unwrap();
1357
1358        // Test filter by Completed status only
1359        let completed_only = storage
1360            .list_payments(ListPaymentsRequest {
1361                status_filter: Some(vec![PaymentStatus::Completed]),
1362                ..Default::default()
1363            })
1364            .await
1365            .unwrap();
1366        assert_eq!(completed_only.len(), 1);
1367        assert_eq!(completed_only[0].id, "completed_1");
1368
1369        // Test filter by Pending status only
1370        let pending_only = storage
1371            .list_payments(ListPaymentsRequest {
1372                status_filter: Some(vec![PaymentStatus::Pending]),
1373                ..Default::default()
1374            })
1375            .await
1376            .unwrap();
1377        assert_eq!(pending_only.len(), 1);
1378        assert_eq!(pending_only[0].id, "pending_1");
1379
1380        // Test filter by multiple statuses
1381        let completed_or_failed = storage
1382            .list_payments(ListPaymentsRequest {
1383                status_filter: Some(vec![PaymentStatus::Completed, PaymentStatus::Failed]),
1384                ..Default::default()
1385            })
1386            .await
1387            .unwrap();
1388        assert_eq!(completed_or_failed.len(), 2);
1389    }
1390
1391    #[allow(clippy::too_many_lines)]
1392    pub async fn test_asset_filtering(storage: Box<dyn Storage>) {
1393        use crate::models::TokenMetadata;
1394
1395        // Create payments with different asset types
1396        let spark_payment = Payment {
1397            id: "spark_1".to_string(),
1398            payment_type: PaymentType::Send,
1399            status: PaymentStatus::Completed,
1400            amount: 10_000,
1401            fees: 100,
1402            timestamp: 1000,
1403            method: PaymentMethod::Spark,
1404            details: Some(PaymentDetails::Spark {
1405                invoice_details: None,
1406            }),
1407        };
1408
1409        let lightning_payment = Payment {
1410            id: "lightning_1".to_string(),
1411            payment_type: PaymentType::Send,
1412            status: PaymentStatus::Completed,
1413            amount: 20_000,
1414            fees: 200,
1415            timestamp: 2000,
1416            method: PaymentMethod::Lightning,
1417            details: Some(PaymentDetails::Lightning {
1418                invoice: "lnbc1".to_string(),
1419                payment_hash: "hash1".to_string(),
1420                destination_pubkey: "pubkey1".to_string(),
1421                description: None,
1422                preimage: None,
1423                lnurl_pay_info: None,
1424                lnurl_withdraw_info: None,
1425            }),
1426        };
1427
1428        let token_payment = Payment {
1429            id: "token_1".to_string(),
1430            payment_type: PaymentType::Receive,
1431            status: PaymentStatus::Completed,
1432            amount: 30_000,
1433            fees: 300,
1434            timestamp: 3000,
1435            method: PaymentMethod::Token,
1436            details: Some(PaymentDetails::Token {
1437                metadata: TokenMetadata {
1438                    identifier: "token_id_1".to_string(),
1439                    issuer_public_key: "pubkey".to_string(),
1440                    name: "Token 1".to_string(),
1441                    ticker: "TK1".to_string(),
1442                    decimals: 8,
1443                    max_supply: 1_000_000,
1444                    is_freezable: false,
1445                },
1446                tx_hash: "tx_hash_1".to_string(),
1447                invoice_details: None,
1448            }),
1449        };
1450
1451        let withdraw_payment = Payment {
1452            id: "withdraw_1".to_string(),
1453            payment_type: PaymentType::Send,
1454            status: PaymentStatus::Completed,
1455            amount: 40_000,
1456            fees: 400,
1457            timestamp: 4000,
1458            method: PaymentMethod::Withdraw,
1459            details: Some(PaymentDetails::Withdraw {
1460                tx_id: "withdraw_tx_1".to_string(),
1461            }),
1462        };
1463
1464        let deposit_payment = Payment {
1465            id: "deposit_1".to_string(),
1466            payment_type: PaymentType::Receive,
1467            status: PaymentStatus::Completed,
1468            amount: 50_000,
1469            fees: 500,
1470            timestamp: 5000,
1471            method: PaymentMethod::Deposit,
1472            details: Some(PaymentDetails::Deposit {
1473                tx_id: "deposit_tx_1".to_string(),
1474            }),
1475        };
1476
1477        storage.insert_payment(spark_payment).await.unwrap();
1478        storage.insert_payment(lightning_payment).await.unwrap();
1479        storage.insert_payment(token_payment).await.unwrap();
1480        storage.insert_payment(withdraw_payment).await.unwrap();
1481        storage.insert_payment(deposit_payment).await.unwrap();
1482
1483        // Test filter by Bitcoin
1484        let spark_only = storage
1485            .list_payments(ListPaymentsRequest {
1486                asset_filter: Some(crate::AssetFilter::Bitcoin),
1487                ..Default::default()
1488            })
1489            .await
1490            .unwrap();
1491        assert_eq!(spark_only.len(), 4);
1492
1493        // Test filter by Token (no identifier)
1494        let token_only = storage
1495            .list_payments(ListPaymentsRequest {
1496                asset_filter: Some(crate::AssetFilter::Token {
1497                    token_identifier: None,
1498                }),
1499                ..Default::default()
1500            })
1501            .await
1502            .unwrap();
1503        assert_eq!(token_only.len(), 1);
1504        assert_eq!(token_only[0].id, "token_1");
1505
1506        // Test filter by Token with specific identifier
1507        let token_specific = storage
1508            .list_payments(ListPaymentsRequest {
1509                asset_filter: Some(crate::AssetFilter::Token {
1510                    token_identifier: Some("token_id_1".to_string()),
1511                }),
1512                ..Default::default()
1513            })
1514            .await
1515            .unwrap();
1516        assert_eq!(token_specific.len(), 1);
1517        assert_eq!(token_specific[0].id, "token_1");
1518
1519        // Test filter by Token with non-existent identifier
1520        let token_no_match = storage
1521            .list_payments(ListPaymentsRequest {
1522                asset_filter: Some(crate::AssetFilter::Token {
1523                    token_identifier: Some("nonexistent".to_string()),
1524                }),
1525                ..Default::default()
1526            })
1527            .await
1528            .unwrap();
1529        assert_eq!(token_no_match.len(), 0);
1530    }
1531
1532    pub async fn test_timestamp_filtering(storage: Box<dyn Storage>) {
1533        // Create payments at different timestamps
1534        let payment1 = Payment {
1535            id: "ts_1000".to_string(),
1536            payment_type: PaymentType::Send,
1537            status: PaymentStatus::Completed,
1538            amount: 10_000,
1539            fees: 100,
1540            timestamp: 1000,
1541            method: PaymentMethod::Spark,
1542            details: Some(PaymentDetails::Spark {
1543                invoice_details: None,
1544            }),
1545        };
1546
1547        let payment2 = Payment {
1548            id: "ts_2000".to_string(),
1549            payment_type: PaymentType::Send,
1550            status: PaymentStatus::Completed,
1551            amount: 20_000,
1552            fees: 200,
1553            timestamp: 2000,
1554            method: PaymentMethod::Spark,
1555            details: Some(PaymentDetails::Spark {
1556                invoice_details: None,
1557            }),
1558        };
1559
1560        let payment3 = Payment {
1561            id: "ts_3000".to_string(),
1562            payment_type: PaymentType::Send,
1563            status: PaymentStatus::Completed,
1564            amount: 30_000,
1565            fees: 300,
1566            timestamp: 3000,
1567            method: PaymentMethod::Spark,
1568            details: Some(PaymentDetails::Spark {
1569                invoice_details: None,
1570            }),
1571        };
1572
1573        storage.insert_payment(payment1).await.unwrap();
1574        storage.insert_payment(payment2).await.unwrap();
1575        storage.insert_payment(payment3).await.unwrap();
1576
1577        // Test filter by from_timestamp
1578        let from_2000 = storage
1579            .list_payments(ListPaymentsRequest {
1580                from_timestamp: Some(2000),
1581                ..Default::default()
1582            })
1583            .await
1584            .unwrap();
1585        assert_eq!(from_2000.len(), 2);
1586        assert!(from_2000.iter().any(|p| p.id == "ts_2000"));
1587        assert!(from_2000.iter().any(|p| p.id == "ts_3000"));
1588
1589        // Test filter by to_timestamp
1590        let to_2000 = storage
1591            .list_payments(ListPaymentsRequest {
1592                to_timestamp: Some(2000),
1593                ..Default::default()
1594            })
1595            .await
1596            .unwrap();
1597        assert_eq!(to_2000.len(), 1);
1598        assert!(to_2000.iter().any(|p| p.id == "ts_1000"));
1599
1600        // Test filter by both from_timestamp and to_timestamp
1601        let range = storage
1602            .list_payments(ListPaymentsRequest {
1603                from_timestamp: Some(1500),
1604                to_timestamp: Some(2500),
1605                ..Default::default()
1606            })
1607            .await
1608            .unwrap();
1609        assert_eq!(range.len(), 1);
1610        assert_eq!(range[0].id, "ts_2000");
1611    }
1612
1613    pub async fn test_combined_filters(storage: Box<dyn Storage>) {
1614        // Create diverse test payments
1615        let payment1 = Payment {
1616            id: "combined_1".to_string(),
1617            payment_type: PaymentType::Send,
1618            status: PaymentStatus::Completed,
1619            amount: 10_000,
1620            fees: 100,
1621            timestamp: 1000,
1622            method: PaymentMethod::Spark,
1623            details: Some(PaymentDetails::Spark {
1624                invoice_details: None,
1625            }),
1626        };
1627
1628        let payment2 = Payment {
1629            id: "combined_2".to_string(),
1630            payment_type: PaymentType::Send,
1631            status: PaymentStatus::Pending,
1632            amount: 20_000,
1633            fees: 200,
1634            timestamp: 2000,
1635            method: PaymentMethod::Lightning,
1636            details: Some(PaymentDetails::Lightning {
1637                invoice: "lnbc1".to_string(),
1638                payment_hash: "hash1".to_string(),
1639                destination_pubkey: "pubkey1".to_string(),
1640                description: None,
1641                preimage: None,
1642                lnurl_pay_info: None,
1643                lnurl_withdraw_info: None,
1644            }),
1645        };
1646
1647        let payment3 = Payment {
1648            id: "combined_3".to_string(),
1649            payment_type: PaymentType::Receive,
1650            status: PaymentStatus::Completed,
1651            amount: 30_000,
1652            fees: 300,
1653            timestamp: 3000,
1654            method: PaymentMethod::Lightning,
1655            details: Some(PaymentDetails::Lightning {
1656                invoice: "lnbc2".to_string(),
1657                payment_hash: "hash2".to_string(),
1658                destination_pubkey: "pubkey2".to_string(),
1659                description: None,
1660                preimage: None,
1661                lnurl_pay_info: None,
1662                lnurl_withdraw_info: None,
1663            }),
1664        };
1665
1666        storage.insert_payment(payment1).await.unwrap();
1667        storage.insert_payment(payment2).await.unwrap();
1668        storage.insert_payment(payment3).await.unwrap();
1669
1670        // Test: Send + Completed
1671        let send_completed = storage
1672            .list_payments(ListPaymentsRequest {
1673                type_filter: Some(vec![PaymentType::Send]),
1674                status_filter: Some(vec![PaymentStatus::Completed]),
1675                ..Default::default()
1676            })
1677            .await
1678            .unwrap();
1679        assert_eq!(send_completed.len(), 1);
1680        assert_eq!(send_completed[0].id, "combined_1");
1681
1682        // Test: Bitcoin + timestamp range
1683        let bitcoin_recent = storage
1684            .list_payments(ListPaymentsRequest {
1685                asset_filter: Some(crate::AssetFilter::Bitcoin),
1686                from_timestamp: Some(2500),
1687                ..Default::default()
1688            })
1689            .await
1690            .unwrap();
1691        assert_eq!(bitcoin_recent.len(), 1);
1692        assert_eq!(bitcoin_recent[0].id, "combined_3");
1693
1694        // Test: Type + Status + Asset
1695        let send_pending_bitcoin = storage
1696            .list_payments(ListPaymentsRequest {
1697                type_filter: Some(vec![PaymentType::Send]),
1698                status_filter: Some(vec![PaymentStatus::Pending]),
1699                asset_filter: Some(crate::AssetFilter::Bitcoin),
1700                ..Default::default()
1701            })
1702            .await
1703            .unwrap();
1704        assert_eq!(send_pending_bitcoin.len(), 1);
1705        assert_eq!(send_pending_bitcoin[0].id, "combined_2");
1706    }
1707
1708    pub async fn test_sort_order(storage: Box<dyn Storage>) {
1709        // Create payments at different timestamps
1710        let payment1 = Payment {
1711            id: "sort_1".to_string(),
1712            payment_type: PaymentType::Send,
1713            status: PaymentStatus::Completed,
1714            amount: 10_000,
1715            fees: 100,
1716            timestamp: 1000,
1717            method: PaymentMethod::Spark,
1718            details: Some(PaymentDetails::Spark {
1719                invoice_details: None,
1720            }),
1721        };
1722
1723        let payment2 = Payment {
1724            id: "sort_2".to_string(),
1725            payment_type: PaymentType::Send,
1726            status: PaymentStatus::Completed,
1727            amount: 20_000,
1728            fees: 200,
1729            timestamp: 2000,
1730            method: PaymentMethod::Spark,
1731            details: Some(PaymentDetails::Spark {
1732                invoice_details: None,
1733            }),
1734        };
1735
1736        let payment3 = Payment {
1737            id: "sort_3".to_string(),
1738            payment_type: PaymentType::Send,
1739            status: PaymentStatus::Completed,
1740            amount: 30_000,
1741            fees: 300,
1742            timestamp: 3000,
1743            method: PaymentMethod::Spark,
1744            details: Some(PaymentDetails::Spark {
1745                invoice_details: None,
1746            }),
1747        };
1748
1749        storage.insert_payment(payment1).await.unwrap();
1750        storage.insert_payment(payment2).await.unwrap();
1751        storage.insert_payment(payment3).await.unwrap();
1752
1753        // Test default sort (descending by timestamp)
1754        let desc_payments = storage
1755            .list_payments(ListPaymentsRequest::default())
1756            .await
1757            .unwrap();
1758        assert_eq!(desc_payments.len(), 3);
1759        assert_eq!(desc_payments[0].id, "sort_3"); // Most recent first
1760        assert_eq!(desc_payments[1].id, "sort_2");
1761        assert_eq!(desc_payments[2].id, "sort_1");
1762
1763        // Test ascending sort
1764        let asc_payments = storage
1765            .list_payments(ListPaymentsRequest {
1766                sort_ascending: Some(true),
1767                ..Default::default()
1768            })
1769            .await
1770            .unwrap();
1771        assert_eq!(asc_payments.len(), 3);
1772        assert_eq!(asc_payments[0].id, "sort_1"); // Oldest first
1773        assert_eq!(asc_payments[1].id, "sort_2");
1774        assert_eq!(asc_payments[2].id, "sort_3");
1775
1776        // Test explicit descending sort
1777        let desc_explicit = storage
1778            .list_payments(ListPaymentsRequest {
1779                sort_ascending: Some(false),
1780                ..Default::default()
1781            })
1782            .await
1783            .unwrap();
1784        assert_eq!(desc_explicit.len(), 3);
1785        assert_eq!(desc_explicit[0].id, "sort_3");
1786        assert_eq!(desc_explicit[1].id, "sort_2");
1787        assert_eq!(desc_explicit[2].id, "sort_1");
1788    }
1789
1790    pub async fn test_payment_request_metadata(storage: Box<dyn Storage>) {
1791        let cache = ObjectCacheRepository::new(storage.into());
1792
1793        // Prepare test data
1794        let payment_request1 = "pr1".to_string();
1795        let metadata1 = PaymentRequestMetadata {
1796            payment_request: payment_request1.clone(),
1797            lnurl_withdraw_request_details: LnurlWithdrawRequestDetails {
1798                callback: "https://callback.url".to_string(),
1799                k1: "k1value".to_string(),
1800                default_description: "desc1".to_string(),
1801                min_withdrawable: 1000,
1802                max_withdrawable: 2000,
1803            },
1804        };
1805
1806        let payment_request2 = "pr2".to_string();
1807        let metadata2 = PaymentRequestMetadata {
1808            payment_request: payment_request2.clone(),
1809            lnurl_withdraw_request_details: LnurlWithdrawRequestDetails {
1810                callback: "https://callback2.url".to_string(),
1811                k1: "k1value2".to_string(),
1812                default_description: "desc2".to_string(),
1813                min_withdrawable: 10000,
1814                max_withdrawable: 20000,
1815            },
1816        };
1817
1818        // set_payment_request_metadata
1819        cache
1820            .save_payment_request_metadata(&metadata1)
1821            .await
1822            .unwrap();
1823        cache
1824            .save_payment_request_metadata(&metadata2)
1825            .await
1826            .unwrap();
1827
1828        // get_payment_request_metadata
1829        let fetched1 = cache
1830            .fetch_payment_request_metadata(&payment_request1)
1831            .await
1832            .unwrap();
1833        assert!(fetched1.is_some());
1834        let fetched1 = fetched1.unwrap();
1835        assert_eq!(fetched1.payment_request, payment_request1);
1836        // Check lnurl_withdraw_request_details is present and correct
1837        let details = fetched1.lnurl_withdraw_request_details;
1838        assert_eq!(details.k1, "k1value");
1839        assert_eq!(details.default_description, "desc1");
1840        assert_eq!(details.min_withdrawable, 1000);
1841        assert_eq!(details.max_withdrawable, 2000);
1842        assert_eq!(details.callback, "https://callback.url");
1843
1844        let fetched2 = cache
1845            .fetch_payment_request_metadata(&payment_request2)
1846            .await
1847            .unwrap();
1848        assert!(fetched2.is_some());
1849        assert_eq!(fetched2.as_ref().unwrap().payment_request, payment_request2);
1850
1851        // delete_payment_request_metadata
1852        cache
1853            .delete_payment_request_metadata(&payment_request1)
1854            .await
1855            .unwrap();
1856        let deleted = cache
1857            .fetch_payment_request_metadata(&payment_request1)
1858            .await
1859            .unwrap();
1860        assert!(deleted.is_none());
1861    }
1862}