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