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#[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 #[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#[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#[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 async fn list_payments(
104 &self,
105 request: ListPaymentsRequest,
106 ) -> Result<Vec<Payment>, StorageError>;
107
108 async fn insert_payment(&self, payment: Payment) -> Result<(), StorageError>;
118
119 async fn set_payment_metadata(
130 &self,
131 payment_id: String,
132 metadata: PaymentMetadata,
133 ) -> Result<(), StorageError>;
134
135 async fn get_payment_by_id(&self, id: String) -> Result<Payment, StorageError>;
144
145 async fn get_payment_by_invoice(
153 &self,
154 invoice: String,
155 ) -> Result<Option<Payment>, StorageError>;
156
157 async fn add_deposit(
168 &self,
169 txid: String,
170 vout: u32,
171 amount_sats: u64,
172 ) -> Result<(), StorageError>;
173
174 async fn delete_deposit(&self, txid: String, vout: u32) -> Result<(), StorageError>;
184
185 async fn list_deposits(&self) -> Result<Vec<DepositInfo>, StorageError>;
190
191 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 let last_revision = storage.get_last_revision().await.unwrap();
512 assert_eq!(last_revision, 0, "Initial last revision should be 0");
513
514 let pending = storage.get_pending_outgoing_changes(10).await.unwrap();
516 assert_eq!(pending.len(), 0, "Should have no pending outgoing changes");
517
518 let incoming = storage.get_incoming_records(10).await.unwrap();
520 assert_eq!(incoming.len(), 0, "Should have no incoming records");
521
522 let latest = storage.get_latest_outgoing_change().await.unwrap();
524 assert!(latest.is_none(), "Should have no latest outgoing change");
525
526 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 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 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 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 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 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 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 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 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 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 storage
657 .update_record_from_incoming(incoming_record1.clone())
658 .await
659 .unwrap();
660
661 storage
663 .delete_incoming_record(incoming_record1.clone())
664 .await
665 .unwrap();
666
667 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 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 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 storage.rebase_pending_outgoing_records(150).await.unwrap();
706
707 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 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 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 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 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 storage.insert_incoming_records(vec![]).await.unwrap();
764
765 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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); assert_eq!(receive_payments, 6); 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); assert_eq!(pending_payments, 2); assert_eq!(failed_payments, 1); let lightning_count = payments
1254 .iter()
1255 .filter(|p| p.method == PaymentMethod::Lightning)
1256 .count();
1257 assert_eq!(lightning_count, 4); 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 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 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 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 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 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 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 let deposits = storage.list_deposits().await.unwrap();
1470 assert_eq!(deposits.len(), 0);
1471
1472 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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"); assert_eq!(desc_payments[1].id, "sort_2");
2266 assert_eq!(desc_payments[2].id, "sort_1");
2267
2268 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"); assert_eq!(asc_payments[1].id, "sort_2");
2279 assert_eq!(asc_payments[2].id, "sort_3");
2280
2281 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 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 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 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 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 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}