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 macros::async_trait;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use crate::{
12 ConversionInfo, DepositClaimError, DepositInfo, LightningAddressInfo, ListPaymentsRequest,
13 LnurlPayInfo, LnurlWithdrawInfo, TokenBalance, TokenMetadata, models::Payment,
14};
15
16const ACCOUNT_INFO_KEY: &str = "account_info";
17const LIGHTNING_ADDRESS_KEY: &str = "lightning_address";
18const LNURL_METADATA_UPDATED_AFTER_KEY: &str = "lnurl_metadata_updated_after";
19const SYNC_OFFSET_KEY: &str = "sync_offset";
20const TX_CACHE_KEY: &str = "tx_cache";
21const STATIC_DEPOSIT_ADDRESS_CACHE_KEY: &str = "static_deposit_address";
22const TOKEN_METADATA_KEY_PREFIX: &str = "token_metadata_";
23const PAYMENT_METADATA_KEY_PREFIX: &str = "payment_metadata";
24const SPARK_PRIVATE_MODE_INITIALIZED_KEY: &str = "spark_private_mode_initialized";
25
26#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
27pub enum UpdateDepositPayload {
28 ClaimError {
29 error: DepositClaimError,
30 },
31 Refund {
32 refund_txid: String,
33 refund_tx: String,
34 },
35}
36
37#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
38pub struct SetLnurlMetadataItem {
39 pub payment_hash: String,
40 pub sender_comment: Option<String>,
41 pub nostr_zap_request: Option<String>,
42 pub nostr_zap_receipt: Option<String>,
43}
44
45impl From<lnurl_models::ListMetadataMetadata> for SetLnurlMetadataItem {
46 fn from(value: lnurl_models::ListMetadataMetadata) -> Self {
47 SetLnurlMetadataItem {
48 payment_hash: value.payment_hash,
49 sender_comment: value.sender_comment,
50 nostr_zap_request: value.nostr_zap_request,
51 nostr_zap_receipt: value.nostr_zap_receipt,
52 }
53 }
54}
55
56#[derive(Debug, Error, Clone)]
58#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
59pub enum StorageError {
60 #[error("Underline implementation error: {0}")]
61 Implementation(String),
62
63 #[error("Failed to initialize database: {0}")]
65 InitializationError(String),
66
67 #[error("Failed to serialize/deserialize data: {0}")]
68 Serialization(String),
69}
70
71impl From<serde_json::Error> for StorageError {
72 fn from(e: serde_json::Error) -> Self {
73 StorageError::Serialization(e.to_string())
74 }
75}
76
77#[derive(Clone, Default, Deserialize, Serialize)]
79#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
80pub struct PaymentMetadata {
81 pub parent_payment_id: Option<String>,
82 pub lnurl_pay_info: Option<LnurlPayInfo>,
83 pub lnurl_withdraw_info: Option<LnurlWithdrawInfo>,
84 pub lnurl_description: Option<String>,
85 pub conversion_info: Option<ConversionInfo>,
86}
87
88#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
90#[async_trait]
91pub trait Storage: Send + Sync {
92 async fn delete_cached_item(&self, key: String) -> Result<(), StorageError>;
93 async fn get_cached_item(&self, key: String) -> Result<Option<String>, StorageError>;
94 async fn set_cached_item(&self, key: String, value: String) -> Result<(), StorageError>;
95 async fn list_payments(
105 &self,
106 request: ListPaymentsRequest,
107 ) -> Result<Vec<Payment>, StorageError>;
108
109 async fn insert_payment(&self, payment: Payment) -> Result<(), StorageError>;
119
120 async fn set_payment_metadata(
131 &self,
132 payment_id: String,
133 metadata: PaymentMetadata,
134 ) -> Result<(), StorageError>;
135
136 async fn get_payment_by_id(&self, id: String) -> Result<Payment, StorageError>;
145
146 async fn get_payment_by_invoice(
154 &self,
155 invoice: String,
156 ) -> Result<Option<Payment>, StorageError>;
157
158 async fn add_deposit(
169 &self,
170 txid: String,
171 vout: u32,
172 amount_sats: u64,
173 ) -> Result<(), StorageError>;
174
175 async fn delete_deposit(&self, txid: String, vout: u32) -> Result<(), StorageError>;
185
186 async fn list_deposits(&self) -> Result<Vec<DepositInfo>, StorageError>;
191
192 async fn update_deposit(
203 &self,
204 txid: String,
205 vout: u32,
206 payload: UpdateDepositPayload,
207 ) -> Result<(), StorageError>;
208
209 async fn set_lnurl_metadata(
210 &self,
211 metadata: Vec<SetLnurlMetadataItem>,
212 ) -> Result<(), StorageError>;
213}
214
215pub(crate) struct ObjectCacheRepository {
216 storage: Arc<dyn Storage>,
217}
218
219impl ObjectCacheRepository {
220 pub(crate) fn new(storage: Arc<dyn Storage>) -> Self {
221 ObjectCacheRepository { storage }
222 }
223
224 pub(crate) async fn save_account_info(
225 &self,
226 value: &CachedAccountInfo,
227 ) -> Result<(), StorageError> {
228 self.storage
229 .set_cached_item(ACCOUNT_INFO_KEY.to_string(), serde_json::to_string(value)?)
230 .await?;
231 Ok(())
232 }
233
234 pub(crate) async fn fetch_account_info(
235 &self,
236 ) -> Result<Option<CachedAccountInfo>, StorageError> {
237 let value = self
238 .storage
239 .get_cached_item(ACCOUNT_INFO_KEY.to_string())
240 .await?;
241 match value {
242 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
243 None => Ok(None),
244 }
245 }
246
247 pub(crate) async fn save_sync_info(&self, value: &CachedSyncInfo) -> Result<(), StorageError> {
248 self.storage
249 .set_cached_item(SYNC_OFFSET_KEY.to_string(), serde_json::to_string(value)?)
250 .await?;
251 Ok(())
252 }
253
254 pub(crate) async fn fetch_sync_info(&self) -> Result<Option<CachedSyncInfo>, StorageError> {
255 let value = self
256 .storage
257 .get_cached_item(SYNC_OFFSET_KEY.to_string())
258 .await?;
259 match value {
260 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
261 None => Ok(None),
262 }
263 }
264
265 pub(crate) async fn save_tx(&self, txid: &str, value: &CachedTx) -> Result<(), StorageError> {
266 self.storage
267 .set_cached_item(
268 format!("{TX_CACHE_KEY}-{txid}"),
269 serde_json::to_string(value)?,
270 )
271 .await?;
272 Ok(())
273 }
274
275 pub(crate) async fn fetch_tx(&self, txid: &str) -> Result<Option<CachedTx>, StorageError> {
276 let value = self
277 .storage
278 .get_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
279 .await?;
280 match value {
281 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
282 None => Ok(None),
283 }
284 }
285
286 pub(crate) async fn save_static_deposit_address(
287 &self,
288 value: &StaticDepositAddress,
289 ) -> Result<(), StorageError> {
290 self.storage
291 .set_cached_item(
292 STATIC_DEPOSIT_ADDRESS_CACHE_KEY.to_string(),
293 serde_json::to_string(value)?,
294 )
295 .await?;
296 Ok(())
297 }
298
299 pub(crate) async fn fetch_static_deposit_address(
300 &self,
301 ) -> Result<Option<StaticDepositAddress>, StorageError> {
302 let value = self
303 .storage
304 .get_cached_item(STATIC_DEPOSIT_ADDRESS_CACHE_KEY.to_string())
305 .await?;
306 match value {
307 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
308 None => Ok(None),
309 }
310 }
311
312 pub(crate) async fn save_lightning_address(
313 &self,
314 value: &LightningAddressInfo,
315 ) -> Result<(), StorageError> {
316 self.storage
317 .set_cached_item(
318 LIGHTNING_ADDRESS_KEY.to_string(),
319 serde_json::to_string(value)?,
320 )
321 .await?;
322 Ok(())
323 }
324
325 pub(crate) async fn delete_lightning_address(&self) -> Result<(), StorageError> {
326 self.storage
327 .delete_cached_item(LIGHTNING_ADDRESS_KEY.to_string())
328 .await?;
329 Ok(())
330 }
331
332 pub(crate) async fn fetch_lightning_address(
333 &self,
334 ) -> Result<Option<LightningAddressInfo>, StorageError> {
335 let value = self
336 .storage
337 .get_cached_item(LIGHTNING_ADDRESS_KEY.to_string())
338 .await?;
339 match value {
340 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
341 None => Ok(None),
342 }
343 }
344
345 pub(crate) async fn save_token_metadata(
346 &self,
347 value: &TokenMetadata,
348 ) -> Result<(), StorageError> {
349 self.storage
350 .set_cached_item(
351 format!("{TOKEN_METADATA_KEY_PREFIX}{}", value.identifier),
352 serde_json::to_string(value)?,
353 )
354 .await?;
355 Ok(())
356 }
357
358 pub(crate) async fn fetch_token_metadata(
359 &self,
360 identifier: &str,
361 ) -> Result<Option<TokenMetadata>, StorageError> {
362 let value = self
363 .storage
364 .get_cached_item(format!("{TOKEN_METADATA_KEY_PREFIX}{identifier}"))
365 .await?;
366 match value {
367 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
368 None => Ok(None),
369 }
370 }
371
372 pub(crate) async fn save_payment_metadata(
373 &self,
374 identifier: &str,
375 value: &PaymentMetadata,
376 ) -> Result<(), StorageError> {
377 self.storage
378 .set_cached_item(
379 format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"),
380 serde_json::to_string(value)?,
381 )
382 .await?;
383 Ok(())
384 }
385
386 pub(crate) async fn fetch_payment_metadata(
387 &self,
388 identifier: &str,
389 ) -> Result<Option<PaymentMetadata>, StorageError> {
390 let value = self
391 .storage
392 .get_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}",))
393 .await?;
394 match value {
395 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
396 None => Ok(None),
397 }
398 }
399
400 pub(crate) async fn delete_payment_metadata(
401 &self,
402 identifier: &str,
403 ) -> Result<(), StorageError> {
404 self.storage
405 .delete_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}",))
406 .await?;
407 Ok(())
408 }
409
410 pub(crate) async fn save_spark_private_mode_initialized(&self) -> Result<(), StorageError> {
411 self.storage
412 .set_cached_item(
413 SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string(),
414 "true".to_string(),
415 )
416 .await?;
417 Ok(())
418 }
419
420 pub(crate) async fn fetch_spark_private_mode_initialized(&self) -> Result<bool, StorageError> {
421 let value = self
422 .storage
423 .get_cached_item(SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string())
424 .await?;
425 match value {
426 Some(value) => Ok(value == "true"),
427 None => Ok(false),
428 }
429 }
430
431 pub(crate) async fn save_lnurl_metadata_updated_after(
432 &self,
433 offset: i64,
434 ) -> Result<(), StorageError> {
435 self.storage
436 .set_cached_item(
437 LNURL_METADATA_UPDATED_AFTER_KEY.to_string(),
438 offset.to_string(),
439 )
440 .await?;
441 Ok(())
442 }
443
444 pub(crate) async fn fetch_lnurl_metadata_updated_after(&self) -> Result<i64, StorageError> {
445 let value = self
446 .storage
447 .get_cached_item(LNURL_METADATA_UPDATED_AFTER_KEY.to_string())
448 .await?;
449 match value {
450 Some(value) => Ok(value.parse().map_err(|_| {
451 StorageError::Serialization("invalid lnurl_metadata_updated_after".to_string())
452 })?),
453 None => Ok(0),
454 }
455 }
456}
457
458#[derive(Serialize, Deserialize, Default)]
459pub(crate) struct CachedAccountInfo {
460 pub(crate) balance_sats: u64,
461 #[serde(default)]
462 pub(crate) token_balances: HashMap<String, TokenBalance>,
463}
464
465#[derive(Serialize, Deserialize, Default)]
466pub(crate) struct CachedSyncInfo {
467 pub(crate) offset: u64,
468 pub(crate) last_synced_final_token_payment_id: Option<String>,
469}
470
471#[derive(Serialize, Deserialize, Default)]
472pub(crate) struct CachedTx {
473 pub(crate) raw_tx: String,
474}
475
476#[derive(Serialize, Deserialize, Default)]
477pub(crate) struct StaticDepositAddress {
478 pub(crate) address: String,
479}
480
481#[cfg(feature = "test-utils")]
482pub mod tests {
483 use chrono::Utc;
484
485 use crate::{
486 DepositClaimError, ListPaymentsRequest, LnurlWithdrawInfo, Payment, PaymentDetails,
487 PaymentMetadata, PaymentMethod, PaymentStatus, PaymentType, SparkHtlcDetails,
488 SparkHtlcStatus, Storage, UpdateDepositPayload,
489 persist::ObjectCacheRepository,
490 sync_storage::{Record, RecordId, SyncStorage, UnversionedRecordChange},
491 };
492
493 #[allow(clippy::too_many_lines)]
494 pub async fn test_sqlite_sync_storage(storage: Box<dyn SyncStorage>) {
495 use std::collections::HashMap;
496
497 let last_revision = storage.get_last_revision().await.unwrap();
499 assert_eq!(last_revision, 0, "Initial last revision should be 0");
500
501 let pending = storage.get_pending_outgoing_changes(10).await.unwrap();
503 assert_eq!(pending.len(), 0, "Should have no pending outgoing changes");
504
505 let incoming = storage.get_incoming_records(10).await.unwrap();
507 assert_eq!(incoming.len(), 0, "Should have no incoming records");
508
509 let latest = storage.get_latest_outgoing_change().await.unwrap();
511 assert!(latest.is_none(), "Should have no latest outgoing change");
512
513 let mut updated_fields = HashMap::new();
515 updated_fields.insert("name".to_string(), "\"Alice\"".to_string());
516 updated_fields.insert("age".to_string(), "30".to_string());
517
518 let change1 = UnversionedRecordChange {
519 id: RecordId::new("user".to_string(), "user1".to_string()),
520 schema_version: "1.0.0".to_string(),
521 updated_fields: updated_fields.clone(),
522 };
523
524 let revision1 = storage.add_outgoing_change(change1).await.unwrap();
525 assert!(revision1 > 0, "First revision should be greater than 0");
526
527 let pending = storage.get_pending_outgoing_changes(10).await.unwrap();
529 assert_eq!(pending.len(), 1, "Should have 1 pending outgoing change");
530 assert_eq!(pending[0].change.id.r#type, "user");
531 assert_eq!(pending[0].change.id.data_id, "user1");
532 assert_eq!(pending[0].change.revision, revision1);
533 assert_eq!(pending[0].change.schema_version, "1.0.0");
534 assert!(
535 pending[0].parent.is_none(),
536 "First change should have no parent"
537 );
538
539 let latest = storage.get_latest_outgoing_change().await.unwrap();
541 assert!(latest.is_some());
542 let latest = latest.unwrap();
543 assert_eq!(latest.change.id.r#type, "user");
544 assert_eq!(latest.change.revision, revision1);
545
546 let mut complete_data = HashMap::new();
548 complete_data.insert("name".to_string(), "\"Alice\"".to_string());
549 complete_data.insert("age".to_string(), "30".to_string());
550
551 let completed_record = Record {
552 id: RecordId::new("user".to_string(), "user1".to_string()),
553 revision: revision1,
554 schema_version: "1.0.0".to_string(),
555 data: complete_data,
556 };
557
558 storage
559 .complete_outgoing_sync(completed_record.clone())
560 .await
561 .unwrap();
562
563 let pending = storage.get_pending_outgoing_changes(10).await.unwrap();
565 assert_eq!(
566 pending.len(),
567 0,
568 "Should have no pending changes after completion"
569 );
570
571 let last_revision = storage.get_last_revision().await.unwrap();
573 assert_eq!(
574 last_revision, revision1,
575 "Last revision should match completed revision"
576 );
577
578 let mut updated_fields2 = HashMap::new();
580 updated_fields2.insert("age".to_string(), "31".to_string());
581
582 let change2 = UnversionedRecordChange {
583 id: RecordId::new("user".to_string(), "user1".to_string()),
584 schema_version: "1.0.0".to_string(),
585 updated_fields: updated_fields2,
586 };
587
588 let revision2 = storage.add_outgoing_change(change2).await.unwrap();
589 assert!(
590 revision2 > revision1,
591 "Second revision should be greater than first"
592 );
593
594 let pending = storage.get_pending_outgoing_changes(10).await.unwrap();
596 assert_eq!(pending.len(), 1, "Should have 1 pending change");
597 assert!(
598 pending[0].parent.is_some(),
599 "Update should have parent record"
600 );
601 let parent = pending[0].parent.as_ref().unwrap();
602 assert_eq!(parent.revision, revision1);
603 assert_eq!(parent.id.r#type, "user");
604
605 let mut incoming_data1 = HashMap::new();
607 incoming_data1.insert("title".to_string(), "\"Post 1\"".to_string());
608 incoming_data1.insert("content".to_string(), "\"Hello World\"".to_string());
609
610 let incoming_record1 = Record {
611 id: RecordId::new("post".to_string(), "post1".to_string()),
612 revision: 100,
613 schema_version: "1.0.0".to_string(),
614 data: incoming_data1,
615 };
616
617 let mut incoming_data2 = HashMap::new();
618 incoming_data2.insert("title".to_string(), "\"Post 2\"".to_string());
619
620 let incoming_record2 = Record {
621 id: RecordId::new("post".to_string(), "post2".to_string()),
622 revision: 101,
623 schema_version: "1.0.0".to_string(),
624 data: incoming_data2,
625 };
626
627 storage
628 .insert_incoming_records(vec![incoming_record1.clone(), incoming_record2.clone()])
629 .await
630 .unwrap();
631
632 let incoming = storage.get_incoming_records(10).await.unwrap();
634 assert_eq!(incoming.len(), 2, "Should have 2 incoming records");
635 assert_eq!(incoming[0].new_state.id.r#type, "post");
636 assert_eq!(incoming[0].new_state.revision, 100);
637 assert!(
638 incoming[0].old_state.is_none(),
639 "New incoming record should have no old state"
640 );
641
642 storage
644 .update_record_from_incoming(incoming_record1.clone())
645 .await
646 .unwrap();
647
648 storage
650 .delete_incoming_record(incoming_record1.clone())
651 .await
652 .unwrap();
653
654 let incoming = storage.get_incoming_records(10).await.unwrap();
656 assert_eq!(incoming.len(), 1, "Should have 1 incoming record remaining");
657 assert_eq!(incoming[0].new_state.id.data_id, "post2");
658
659 let mut updated_incoming_data = HashMap::new();
661 updated_incoming_data.insert("title".to_string(), "\"Post 1 Updated\"".to_string());
662 updated_incoming_data.insert("content".to_string(), "\"Updated content\"".to_string());
663
664 let updated_incoming_record = Record {
665 id: RecordId::new("post".to_string(), "post1".to_string()),
666 revision: 102,
667 schema_version: "1.0.0".to_string(),
668 data: updated_incoming_data,
669 };
670
671 storage
672 .insert_incoming_records(vec![updated_incoming_record.clone()])
673 .await
674 .unwrap();
675
676 let incoming = storage.get_incoming_records(10).await.unwrap();
678 let post1_update = incoming.iter().find(|r| r.new_state.id.data_id == "post1");
679 assert!(post1_update.is_some(), "Should find post1 update");
680 let post1_update = post1_update.unwrap();
681 assert!(
682 post1_update.old_state.is_some(),
683 "Update should have old state"
684 );
685 assert_eq!(
686 post1_update.old_state.as_ref().unwrap().revision,
687 100,
688 "Old state should be original revision"
689 );
690
691 storage.rebase_pending_outgoing_records(150).await.unwrap();
693
694 let pending = storage.get_pending_outgoing_changes(10).await.unwrap();
696 assert!(
697 pending[0].change.revision > revision2,
698 "Revision should be rebased"
699 );
700
701 for i in 0..5 {
704 let mut fields = HashMap::new();
705 fields.insert("value".to_string(), format!("\"{i}\""));
706
707 let change = UnversionedRecordChange {
708 id: RecordId::new("test".to_string(), format!("test{i}")),
709 schema_version: "1.0.0".to_string(),
710 updated_fields: fields,
711 };
712 storage.add_outgoing_change(change).await.unwrap();
713 }
714
715 let pending_limited = storage.get_pending_outgoing_changes(3).await.unwrap();
716 assert_eq!(
717 pending_limited.len(),
718 3,
719 "Should respect limit on pending changes"
720 );
721
722 let incoming_limited = storage.get_incoming_records(1).await.unwrap();
724 assert_eq!(
725 incoming_limited.len(),
726 1,
727 "Should respect limit on incoming records"
728 );
729
730 let all_pending = storage.get_pending_outgoing_changes(100).await.unwrap();
732 for i in 1..all_pending.len() {
733 assert!(
734 all_pending[i].change.revision >= all_pending[i.saturating_sub(1)].change.revision,
735 "Pending changes should be ordered by revision ascending"
736 );
737 }
738
739 let all_incoming = storage.get_incoming_records(100).await.unwrap();
741 for i in 1..all_incoming.len() {
742 assert!(
743 all_incoming[i].new_state.revision
744 >= all_incoming[i.saturating_sub(1)].new_state.revision,
745 "Incoming records should be ordered by revision ascending"
746 );
747 }
748
749 storage.insert_incoming_records(vec![]).await.unwrap();
751
752 let mut settings_fields = HashMap::new();
754 settings_fields.insert("theme".to_string(), "\"dark\"".to_string());
755
756 let settings_change = UnversionedRecordChange {
757 id: RecordId::new("settings".to_string(), "global".to_string()),
758 schema_version: "2.0.0".to_string(),
759 updated_fields: settings_fields,
760 };
761
762 let settings_revision = storage.add_outgoing_change(settings_change).await.unwrap();
763
764 let pending = storage.get_pending_outgoing_changes(100).await.unwrap();
765 let settings_pending = pending.iter().find(|p| p.change.id.r#type == "settings");
766 assert!(settings_pending.is_some(), "Should find settings change");
767 assert_eq!(
768 settings_pending.unwrap().change.schema_version,
769 "2.0.0",
770 "Should preserve schema version"
771 );
772
773 let mut complete_settings_data = HashMap::new();
775 complete_settings_data.insert("theme".to_string(), "\"dark\"".to_string());
776
777 let completed_settings = Record {
778 id: RecordId::new("settings".to_string(), "global".to_string()),
779 revision: settings_revision,
780 schema_version: "2.0.0".to_string(),
781 data: complete_settings_data,
782 };
783
784 storage
785 .complete_outgoing_sync(completed_settings)
786 .await
787 .unwrap();
788
789 let last_revision = storage.get_last_revision().await.unwrap();
790 assert!(
791 last_revision >= settings_revision,
792 "Last revision should be at least settings revision"
793 );
794 }
795
796 #[allow(clippy::too_many_lines)]
797 pub async fn test_sqlite_storage(storage: Box<dyn Storage>) {
798 use crate::SetLnurlMetadataItem;
799 use crate::models::{LnurlPayInfo, TokenMetadata};
800
801 let spark_payment = Payment {
803 id: "spark_pmt123".to_string(),
804 payment_type: PaymentType::Send,
805 status: PaymentStatus::Completed,
806 amount: u128::from(u64::MAX).checked_add(100_000).unwrap(),
807 fees: 1_000,
808 timestamp: 5_000,
809 method: PaymentMethod::Spark,
810 details: Some(PaymentDetails::Spark {
811 invoice_details: Some(crate::SparkInvoicePaymentDetails {
812 description: Some("description".to_string()),
813 invoice: "invoice_string".to_string(),
814 }),
815 htlc_details: None,
816 conversion_info: None,
817 }),
818 };
819
820 let spark_htlc_payment = Payment {
822 id: "spark_htlc_pmt123".to_string(),
823 payment_type: PaymentType::Receive,
824 status: PaymentStatus::Completed,
825 amount: 20_000,
826 fees: 2_000,
827 timestamp: 10_000,
828 method: PaymentMethod::Spark,
829 details: Some(PaymentDetails::Spark {
830 invoice_details: None,
831 htlc_details: Some(SparkHtlcDetails {
832 payment_hash: "payment_hash123".to_string(),
833 preimage: Some("preimage123".to_string()),
834 expiry_time: 15_000,
835 status: SparkHtlcStatus::PreimageShared,
836 }),
837 conversion_info: None,
838 }),
839 };
840
841 let token_metadata = TokenMetadata {
843 identifier: "token123".to_string(),
844 issuer_public_key:
845 "02abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab".to_string(),
846 name: "Test Token".to_string(),
847 ticker: "TTK".to_string(),
848 decimals: 8,
849 max_supply: 21_000_000,
850 is_freezable: false,
851 };
852 let token_payment = Payment {
853 id: "token_pmt456".to_string(),
854 payment_type: PaymentType::Receive,
855 status: PaymentStatus::Pending,
856 amount: 50_000,
857 fees: 500,
858 timestamp: Utc::now().timestamp().try_into().unwrap(),
859 method: PaymentMethod::Token,
860 details: Some(PaymentDetails::Token {
861 metadata: token_metadata.clone(),
862 tx_hash: "tx_hash".to_string(),
863 invoice_details: Some(crate::SparkInvoicePaymentDetails {
864 description: Some("description_2".to_string()),
865 invoice: "invoice_string_2".to_string(),
866 }),
867 conversion_info: None,
868 }),
869 };
870
871 let pay_metadata = PaymentMetadata {
873 lnurl_pay_info: Some(LnurlPayInfo {
874 ln_address: Some("test@example.com".to_string()),
875 comment: Some("Test comment".to_string()),
876 domain: Some("example.com".to_string()),
877 metadata: Some("[[\"text/plain\", \"Test metadata\"]]".to_string()),
878 processed_success_action: None,
879 raw_success_action: None,
880 }),
881 ..Default::default()
882 };
883
884 let lightning_lnurl_pay_payment = Payment {
885 id: "lightning_pmt789".to_string(),
886 payment_type: PaymentType::Send,
887 status: PaymentStatus::Completed,
888 amount: 25_000,
889 fees: 250,
890 timestamp: Utc::now().timestamp().try_into().unwrap(),
891 method: PaymentMethod::Lightning,
892 details: Some(PaymentDetails::Lightning {
893 description: Some("Test lightning payment".to_string()),
894 preimage: Some("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab".to_string()),
895 invoice: "lnbc250n1pjqxyz9pp5abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890abcdefghijklmnopqrstuvwxyz".to_string(),
896 payment_hash: "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321".to_string(),
897 destination_pubkey: "03123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01".to_string(),
898 lnurl_pay_info: pay_metadata.lnurl_pay_info.clone(),
899 lnurl_withdraw_info: pay_metadata.lnurl_withdraw_info.clone(),
900 lnurl_receive_metadata: None,
901 }),
902 };
903
904 let withdraw_metadata = PaymentMetadata {
906 lnurl_withdraw_info: Some(LnurlWithdrawInfo {
907 withdraw_url: "http://example.com/withdraw".to_string(),
908 }),
909 ..Default::default()
910 };
911 let lightning_lnurl_withdraw_payment = Payment {
912 id: "lightning_pmtabc".to_string(),
913 payment_type: PaymentType::Receive,
914 status: PaymentStatus::Completed,
915 amount: 75_000,
916 fees: 750,
917 timestamp: Utc::now().timestamp().try_into().unwrap(),
918 method: PaymentMethod::Lightning,
919 details: Some(PaymentDetails::Lightning {
920 description: Some("Test lightning payment".to_string()),
921 preimage: Some("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab".to_string()),
922 invoice: "lnbc250n1pjqxyz9pp5abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890abcdefghijklmnopqrstuvwxyz".to_string(),
923 payment_hash: "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321".to_string(),
924 destination_pubkey: "03123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01".to_string(),
925 lnurl_pay_info: withdraw_metadata.lnurl_pay_info.clone(),
926 lnurl_withdraw_info: withdraw_metadata.lnurl_withdraw_info.clone(),
927 lnurl_receive_metadata: None,
928 }),
929 };
930
931 let lightning_minimal_payment = Payment {
933 id: "lightning_minimal_pmt012".to_string(),
934 payment_type: PaymentType::Receive,
935 status: PaymentStatus::Failed,
936 amount: 10_000,
937 fees: 100,
938 timestamp: Utc::now().timestamp().try_into().unwrap(),
939 method: PaymentMethod::Lightning,
940 details: Some(PaymentDetails::Lightning {
941 description: None,
942 preimage: None,
943 invoice: "lnbc100n1pjqxyz9pp5def456ghi789jkl012mno345pqr678stu901vwx234yz567890abcdefghijklmnopqrstuvwxyz".to_string(),
944 payment_hash: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(),
945 destination_pubkey: "02987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba09".to_string(),
946 lnurl_pay_info: None,
947 lnurl_withdraw_info: None,
948 lnurl_receive_metadata: None,
949 }),
950 };
951
952 let lnurl_receive_payment_hash =
954 "receivehash1234567890abcdef1234567890abcdef1234567890abcdef1234".to_string();
955 let lnurl_receive_metadata = crate::LnurlReceiveMetadata {
956 sender_comment: Some("Test sender comment".to_string()),
957 nostr_zap_request: Some(r#"{"kind":9734,"content":"test zap"}"#.to_string()),
958 nostr_zap_receipt: Some(r#"{"kind":9735,"content":"test receipt"}"#.to_string()),
959 };
960 let lightning_lnurl_receive_payment = Payment {
961 id: "lightning_lnurl_receive_pmt".to_string(),
962 payment_type: PaymentType::Receive,
963 status: PaymentStatus::Completed,
964 amount: 100_000,
965 fees: 1000,
966 timestamp: Utc::now().timestamp().try_into().unwrap(),
967 method: PaymentMethod::Lightning,
968 details: Some(PaymentDetails::Lightning {
969 description: Some("LNURL receive test".to_string()),
970 preimage: Some("receivepreimage1234567890abcdef1234567890abcdef1234567890abcdef12".to_string()),
971 invoice: "lnbc1000n1pjqxyz9pp5receive123def456ghi789jkl012mno345pqr678stu901vwx234yz567890abcdefghijklmnopqrstuvwxyz".to_string(),
972 payment_hash: lnurl_receive_payment_hash.clone(),
973 destination_pubkey: "03receivepubkey123456789abcdef0123456789abcdef0123456789abcdef01234".to_string(),
974 lnurl_pay_info: None,
975 lnurl_withdraw_info: None,
976 lnurl_receive_metadata: Some(lnurl_receive_metadata.clone()),
977 }),
978 };
979
980 let withdraw_payment = Payment {
982 id: "withdraw_pmt345".to_string(),
983 payment_type: PaymentType::Send,
984 status: PaymentStatus::Completed,
985 amount: 200_000,
986 fees: 2000,
987 timestamp: Utc::now().timestamp().try_into().unwrap(),
988 method: PaymentMethod::Withdraw,
989 details: Some(PaymentDetails::Withdraw {
990 tx_id: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
991 .to_string(),
992 }),
993 };
994
995 let deposit_payment = Payment {
997 id: "deposit_pmt678".to_string(),
998 payment_type: PaymentType::Receive,
999 status: PaymentStatus::Completed,
1000 amount: 150_000,
1001 fees: 1500,
1002 timestamp: Utc::now().timestamp().try_into().unwrap(),
1003 method: PaymentMethod::Deposit,
1004 details: Some(PaymentDetails::Deposit {
1005 tx_id: "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321fe"
1006 .to_string(),
1007 }),
1008 };
1009
1010 let no_details_payment = Payment {
1012 id: "no_details_pmt901".to_string(),
1013 payment_type: PaymentType::Send,
1014 status: PaymentStatus::Pending,
1015 amount: 75_000,
1016 fees: 750,
1017 timestamp: Utc::now().timestamp().try_into().unwrap(),
1018 method: PaymentMethod::Unknown,
1019 details: None,
1020 };
1021
1022 let successful_sent_conversion_payment_metadata = PaymentMetadata {
1024 parent_payment_id: Some("after_conversion_pmt124".to_string()),
1025 conversion_info: Some(crate::ConversionInfo {
1026 pool_id: "pool_abc".to_string(),
1027 conversion_id: "conversion_sent_pmt123".to_string(),
1028 status: crate::ConversionStatus::Completed,
1029 fee: Some(21),
1030 purpose: None,
1031 }),
1032 ..Default::default()
1033 };
1034 let successful_sent_conversion_payment = Payment {
1035 id: "conversion_sent_pmt123".to_string(),
1036 payment_type: PaymentType::Send,
1037 status: PaymentStatus::Completed,
1038 amount: 10_000,
1039 fees: 0,
1040 timestamp: Utc::now().timestamp().try_into().unwrap(),
1041 method: PaymentMethod::Spark,
1042 details: Some(PaymentDetails::Spark {
1043 invoice_details: None,
1044 htlc_details: None,
1045 conversion_info: successful_sent_conversion_payment_metadata
1046 .conversion_info
1047 .clone(),
1048 }),
1049 };
1050 let successful_received_conversion_payment = Payment {
1051 id: "conversion_received_pmt123".to_string(),
1052 payment_type: PaymentType::Receive,
1053 status: PaymentStatus::Completed,
1054 amount: 10_000_000,
1055 fees: 0,
1056 timestamp: Utc::now().timestamp().try_into().unwrap(),
1057 method: PaymentMethod::Token,
1058 details: Some(PaymentDetails::Token {
1059 metadata: token_metadata.clone(),
1060 tx_hash: "conversion_received_pmt123_tx_hash".to_string(),
1061 invoice_details: None,
1062 conversion_info: None,
1063 }),
1064 };
1065 let after_conversion_payment = Payment {
1066 id: "after_conversion_pmt124".to_string(),
1067 payment_type: PaymentType::Send,
1068 status: PaymentStatus::Completed,
1069 amount: 10_000_000,
1070 fees: 0,
1071 timestamp: Utc::now().timestamp().try_into().unwrap(),
1072 method: PaymentMethod::Token,
1073 details: Some(PaymentDetails::Token {
1074 metadata: token_metadata.clone(),
1075 tx_hash: "after_conversion_pmt124_tx_hash".to_string(),
1076 invoice_details: None,
1077 conversion_info: None,
1078 }),
1079 };
1080
1081 let failed_with_refund_conversion_payment_metadata = PaymentMetadata {
1083 conversion_info: Some(crate::ConversionInfo {
1084 pool_id: "pool_xyz".to_string(),
1085 conversion_id: "conversion_pmt789".to_string(),
1086 status: crate::ConversionStatus::Refunded,
1087 fee: None,
1088 purpose: None,
1089 }),
1090 ..Default::default()
1091 };
1092 let failed_with_refund_conversion_payment = Payment {
1093 id: "conversion_pmt789".to_string(),
1094 payment_type: PaymentType::Send,
1095 status: PaymentStatus::Completed,
1096 amount: 10_000,
1097 fees: 0,
1098 timestamp: Utc::now().timestamp().try_into().unwrap(),
1099 method: PaymentMethod::Spark,
1100 details: Some(PaymentDetails::Spark {
1101 invoice_details: None,
1102 htlc_details: None,
1103 conversion_info: failed_with_refund_conversion_payment_metadata
1104 .conversion_info
1105 .clone(),
1106 }),
1107 };
1108
1109 let failed_no_refund_conversion_payment_metadata = PaymentMetadata {
1111 conversion_info: Some(crate::ConversionInfo {
1112 pool_id: "pool_xyz".to_string(),
1113 conversion_id: "conversion_pmt000".to_string(),
1114 status: crate::ConversionStatus::RefundNeeded,
1115 fee: None,
1116 purpose: None,
1117 }),
1118 ..Default::default()
1119 };
1120 let failed_no_refund_conversion_payment = Payment {
1121 id: "conversion_pmt000".to_string(),
1122 payment_type: PaymentType::Send,
1123 status: PaymentStatus::Completed,
1124 amount: 20_000,
1125 fees: 0,
1126 timestamp: Utc::now().timestamp().try_into().unwrap(),
1127 method: PaymentMethod::Spark,
1128 details: Some(PaymentDetails::Spark {
1129 invoice_details: None,
1130 htlc_details: None,
1131 conversion_info: failed_no_refund_conversion_payment_metadata
1132 .conversion_info
1133 .clone(),
1134 }),
1135 };
1136
1137 let test_payments = vec![
1138 spark_payment.clone(),
1139 spark_htlc_payment.clone(),
1140 token_payment.clone(),
1141 lightning_lnurl_pay_payment.clone(),
1142 lightning_lnurl_withdraw_payment.clone(),
1143 lightning_minimal_payment.clone(),
1144 lightning_lnurl_receive_payment.clone(),
1145 withdraw_payment.clone(),
1146 deposit_payment.clone(),
1147 no_details_payment.clone(),
1148 successful_sent_conversion_payment.clone(),
1149 successful_received_conversion_payment.clone(),
1150 after_conversion_payment.clone(),
1151 failed_with_refund_conversion_payment.clone(),
1152 failed_no_refund_conversion_payment.clone(),
1153 ];
1154
1155 for payment in &test_payments {
1157 storage.insert_payment(payment.clone()).await.unwrap();
1158 }
1159 storage
1160 .set_payment_metadata(lightning_lnurl_pay_payment.id.clone(), pay_metadata)
1161 .await
1162 .unwrap();
1163 storage
1164 .set_payment_metadata(
1165 lightning_lnurl_withdraw_payment.id.clone(),
1166 withdraw_metadata,
1167 )
1168 .await
1169 .unwrap();
1170 storage
1171 .set_lnurl_metadata(vec![SetLnurlMetadataItem {
1172 nostr_zap_receipt: lnurl_receive_metadata.nostr_zap_receipt.clone(),
1173 nostr_zap_request: lnurl_receive_metadata.nostr_zap_request.clone(),
1174 payment_hash: lnurl_receive_payment_hash.clone(),
1175 sender_comment: lnurl_receive_metadata.sender_comment.clone(),
1176 }])
1177 .await
1178 .unwrap();
1179 storage
1180 .set_payment_metadata(
1181 successful_sent_conversion_payment.id.clone(),
1182 successful_sent_conversion_payment_metadata,
1183 )
1184 .await
1185 .unwrap();
1186 storage
1187 .set_payment_metadata(
1188 failed_with_refund_conversion_payment.id.clone(),
1189 failed_with_refund_conversion_payment_metadata,
1190 )
1191 .await
1192 .unwrap();
1193 storage
1194 .set_payment_metadata(
1195 failed_no_refund_conversion_payment.id.clone(),
1196 failed_no_refund_conversion_payment_metadata,
1197 )
1198 .await
1199 .unwrap();
1200 let payments = storage
1202 .list_payments(ListPaymentsRequest {
1203 offset: Some(0),
1204 limit: Some(16),
1205 ..Default::default()
1206 })
1207 .await
1208 .unwrap();
1209 assert_eq!(payments.len(), 15);
1210
1211 for (i, expected_payment) in test_payments.iter().enumerate() {
1213 let retrieved_payment = storage
1214 .get_payment_by_id(expected_payment.id.clone())
1215 .await
1216 .unwrap();
1217
1218 assert_eq!(retrieved_payment.id, expected_payment.id);
1220 assert_eq!(
1221 retrieved_payment.payment_type,
1222 expected_payment.payment_type
1223 );
1224 assert_eq!(retrieved_payment.status, expected_payment.status);
1225 assert_eq!(retrieved_payment.amount, expected_payment.amount);
1226 assert_eq!(retrieved_payment.fees, expected_payment.fees);
1227 assert_eq!(retrieved_payment.method, expected_payment.method);
1228
1229 match (&retrieved_payment.details, &expected_payment.details) {
1231 (None, None) => {}
1232 (
1233 Some(PaymentDetails::Spark {
1234 invoice_details: r_invoice,
1235 htlc_details: r_htlc,
1236 conversion_info: r_conversion_info,
1237 }),
1238 Some(PaymentDetails::Spark {
1239 invoice_details: e_invoice,
1240 htlc_details: e_htlc,
1241 conversion_info: e_conversion_info,
1242 }),
1243 ) => {
1244 assert_eq!(r_invoice, e_invoice);
1245 assert_eq!(r_htlc, e_htlc);
1246 assert_eq!(r_conversion_info, e_conversion_info);
1247 }
1248 (
1249 Some(PaymentDetails::Token {
1250 metadata: r_metadata,
1251 tx_hash: r_tx_hash,
1252 invoice_details: r_invoice,
1253 conversion_info: r_conversion_info,
1254 }),
1255 Some(PaymentDetails::Token {
1256 metadata: e_metadata,
1257 tx_hash: e_tx_hash,
1258 invoice_details: e_invoice,
1259 conversion_info: e_conversion_info,
1260 }),
1261 ) => {
1262 assert_eq!(r_metadata.identifier, e_metadata.identifier);
1263 assert_eq!(r_metadata.issuer_public_key, e_metadata.issuer_public_key);
1264 assert_eq!(r_metadata.name, e_metadata.name);
1265 assert_eq!(r_metadata.ticker, e_metadata.ticker);
1266 assert_eq!(r_metadata.decimals, e_metadata.decimals);
1267 assert_eq!(r_metadata.max_supply, e_metadata.max_supply);
1268 assert_eq!(r_metadata.is_freezable, e_metadata.is_freezable);
1269 assert_eq!(r_tx_hash, e_tx_hash);
1270 assert_eq!(r_invoice, e_invoice);
1271 assert_eq!(r_conversion_info, e_conversion_info);
1272 }
1273 (
1274 Some(PaymentDetails::Lightning {
1275 description: r_description,
1276 preimage: r_preimage,
1277 invoice: r_invoice,
1278 payment_hash: r_hash,
1279 destination_pubkey: r_dest_pubkey,
1280 lnurl_pay_info: r_pay_lnurl,
1281 lnurl_withdraw_info: r_withdraw_lnurl,
1282 lnurl_receive_metadata: r_receive_metadata,
1283 }),
1284 Some(PaymentDetails::Lightning {
1285 description: e_description,
1286 preimage: e_preimage,
1287 invoice: e_invoice,
1288 payment_hash: e_hash,
1289 destination_pubkey: e_dest_pubkey,
1290 lnurl_pay_info: e_pay_lnurl,
1291 lnurl_withdraw_info: e_withdraw_lnurl,
1292 lnurl_receive_metadata: e_receive_metadata,
1293 }),
1294 ) => {
1295 assert_eq!(r_description, e_description);
1296 assert_eq!(r_preimage, e_preimage);
1297 assert_eq!(r_invoice, e_invoice);
1298 assert_eq!(r_hash, e_hash);
1299 assert_eq!(r_dest_pubkey, e_dest_pubkey);
1300
1301 match (r_pay_lnurl, e_pay_lnurl) {
1303 (Some(r_info), Some(e_info)) => {
1304 assert_eq!(r_info.ln_address, e_info.ln_address);
1305 assert_eq!(r_info.comment, e_info.comment);
1306 assert_eq!(r_info.domain, e_info.domain);
1307 assert_eq!(r_info.metadata, e_info.metadata);
1308 }
1309 (None, None) => {}
1310 _ => panic!(
1311 "LNURL pay info mismatch for payment {}",
1312 expected_payment.id
1313 ),
1314 }
1315
1316 match (r_withdraw_lnurl, e_withdraw_lnurl) {
1318 (Some(r_info), Some(e_info)) => {
1319 assert_eq!(r_info.withdraw_url, e_info.withdraw_url);
1320 }
1321 (None, None) => {}
1322 _ => panic!(
1323 "LNURL withdraw info mismatch for payment {}",
1324 expected_payment.id
1325 ),
1326 }
1327
1328 match (r_receive_metadata, e_receive_metadata) {
1330 (Some(r_info), Some(e_info)) => {
1331 assert_eq!(r_info.nostr_zap_request, e_info.nostr_zap_request);
1332 assert_eq!(r_info.sender_comment, e_info.sender_comment);
1333 }
1334 (None, None) => {}
1335 _ => panic!(
1336 "LNURL receive metadata mismatch for payment {}",
1337 expected_payment.id
1338 ),
1339 }
1340 }
1341 (
1342 Some(PaymentDetails::Withdraw { tx_id: r_tx_id }),
1343 Some(PaymentDetails::Withdraw { tx_id: e_tx_id }),
1344 )
1345 | (
1346 Some(PaymentDetails::Deposit { tx_id: r_tx_id }),
1347 Some(PaymentDetails::Deposit { tx_id: e_tx_id }),
1348 ) => {
1349 assert_eq!(r_tx_id, e_tx_id);
1350 }
1351 _ => panic!(
1352 "Payment details mismatch for payment {} (index {})",
1353 expected_payment.id, i
1354 ),
1355 }
1356 }
1357
1358 let send_payments = payments
1360 .iter()
1361 .filter(|p| p.payment_type == PaymentType::Send)
1362 .count();
1363 let receive_payments = payments
1364 .iter()
1365 .filter(|p| p.payment_type == PaymentType::Receive)
1366 .count();
1367 assert_eq!(send_payments, 8); assert_eq!(receive_payments, 7); let completed_payments = payments
1372 .iter()
1373 .filter(|p| p.status == PaymentStatus::Completed)
1374 .count();
1375 let pending_payments = payments
1376 .iter()
1377 .filter(|p| p.status == PaymentStatus::Pending)
1378 .count();
1379 let failed_payments = payments
1380 .iter()
1381 .filter(|p| p.status == PaymentStatus::Failed)
1382 .count();
1383 assert_eq!(completed_payments, 12); assert_eq!(pending_payments, 2); assert_eq!(failed_payments, 1); let lightning_count = payments
1389 .iter()
1390 .filter(|p| p.method == PaymentMethod::Lightning)
1391 .count();
1392 assert_eq!(lightning_count, 4); let lightning_zap_payment = Payment {
1396 id: "lightning_zap_pmt".to_string(),
1397 payment_type: PaymentType::Receive,
1398 status: PaymentStatus::Completed,
1399 amount: 100_000,
1400 fees: 1000,
1401 timestamp: Utc::now().timestamp().try_into().unwrap(),
1402 method: PaymentMethod::Lightning,
1403 details: Some(PaymentDetails::Lightning {
1404 description: Some("Zap payment".to_string()),
1405 preimage: Some("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01".to_string()),
1406 invoice: "lnbc1000n1pjqxyz9pp5zap123def456ghi789jkl012mno345pqr678stu901vwx234yz567890abcdefghijklmnopqrstuvwxyz".to_string(),
1407 payment_hash: "zaphash1234567890abcdef1234567890abcdef1234567890abcdef12345678".to_string(),
1408 destination_pubkey: "03zappubkey123456789abcdef0123456789abcdef0123456789abcdef0123456701".to_string(),
1409 lnurl_pay_info: None,
1410 lnurl_withdraw_info: None,
1411 lnurl_receive_metadata: None,
1412 }),
1413 };
1414
1415 storage
1416 .insert_payment(lightning_zap_payment.clone())
1417 .await
1418 .unwrap();
1419
1420 storage
1422 .set_lnurl_metadata(vec![SetLnurlMetadataItem {
1423 payment_hash: "zaphash1234567890abcdef1234567890abcdef1234567890abcdef12345678"
1424 .to_string(),
1425 sender_comment: Some("Great content!".to_string()),
1426 nostr_zap_request: Some(
1427 r#"{"kind":9734,"content":"zap request","tags":[]}"#.to_string(),
1428 ),
1429 nostr_zap_receipt: Some(
1430 r#"{"kind":9735,"content":"zap receipt","tags":[]}"#.to_string(),
1431 ),
1432 }])
1433 .await
1434 .unwrap();
1435
1436 let retrieved_zap_payment = storage
1438 .get_payment_by_id(lightning_zap_payment.id.clone())
1439 .await
1440 .unwrap();
1441
1442 match retrieved_zap_payment.details {
1443 Some(PaymentDetails::Lightning {
1444 lnurl_receive_metadata: Some(metadata),
1445 ..
1446 }) => {
1447 assert_eq!(
1448 metadata.sender_comment,
1449 Some("Great content!".to_string()),
1450 "Sender comment should match"
1451 );
1452 assert_eq!(
1453 metadata.nostr_zap_request,
1454 Some(r#"{"kind":9734,"content":"zap request","tags":[]}"#.to_string()),
1455 "Nostr zap request should match"
1456 );
1457 assert_eq!(
1458 metadata.nostr_zap_receipt,
1459 Some(r#"{"kind":9735,"content":"zap receipt","tags":[]}"#.to_string()),
1460 "Nostr zap receipt should match"
1461 );
1462 }
1463 _ => panic!("Expected Lightning payment with lnurl receive metadata"),
1464 }
1465
1466 let lightning_zap_payment2 = Payment {
1468 id: "lightning_zap_pmt2".to_string(),
1469 payment_type: PaymentType::Receive,
1470 status: PaymentStatus::Completed,
1471 amount: 50_000,
1472 fees: 500,
1473 timestamp: Utc::now().timestamp().try_into().unwrap(),
1474 method: PaymentMethod::Lightning,
1475 details: Some(PaymentDetails::Lightning {
1476 description: Some("Another zap".to_string()),
1477 preimage: None,
1478 invoice: "lnbc500n1pjqxyz9pp5zap2".to_string(),
1479 payment_hash: "zaphash2".to_string(),
1480 destination_pubkey: "03zappubkey2".to_string(),
1481 lnurl_pay_info: None,
1482 lnurl_withdraw_info: None,
1483 lnurl_receive_metadata: None,
1484 }),
1485 };
1486
1487 let lightning_zap_payment3 = Payment {
1488 id: "lightning_zap_pmt3".to_string(),
1489 payment_type: PaymentType::Receive,
1490 status: PaymentStatus::Completed,
1491 amount: 25_000,
1492 fees: 250,
1493 timestamp: Utc::now().timestamp().try_into().unwrap(),
1494 method: PaymentMethod::Lightning,
1495 details: Some(PaymentDetails::Lightning {
1496 description: Some("Third zap".to_string()),
1497 preimage: None,
1498 invoice: "lnbc250n1pjqxyz9pp5zap3".to_string(),
1499 payment_hash: "zaphash3".to_string(),
1500 destination_pubkey: "03zappubkey3".to_string(),
1501 lnurl_pay_info: None,
1502 lnurl_withdraw_info: None,
1503 lnurl_receive_metadata: None,
1504 }),
1505 };
1506
1507 storage
1508 .insert_payment(lightning_zap_payment2.clone())
1509 .await
1510 .unwrap();
1511 storage
1512 .insert_payment(lightning_zap_payment3.clone())
1513 .await
1514 .unwrap();
1515
1516 storage
1518 .set_lnurl_metadata(vec![
1519 SetLnurlMetadataItem {
1520 payment_hash: "zaphash2".to_string(),
1521 sender_comment: Some("Nice work!".to_string()),
1522 nostr_zap_request: None,
1523 nostr_zap_receipt: None,
1524 },
1525 SetLnurlMetadataItem {
1526 payment_hash: "zaphash3".to_string(),
1527 sender_comment: None,
1528 nostr_zap_request: Some(r#"{"kind":9734,"content":"zap3"}"#.to_string()),
1529 nostr_zap_receipt: None,
1530 },
1531 ])
1532 .await
1533 .unwrap();
1534
1535 let retrieved_zap2 = storage
1537 .get_payment_by_id(lightning_zap_payment2.id.clone())
1538 .await
1539 .unwrap();
1540
1541 match retrieved_zap2.details {
1542 Some(PaymentDetails::Lightning {
1543 lnurl_receive_metadata: Some(metadata),
1544 ..
1545 }) => {
1546 assert_eq!(
1547 metadata.sender_comment,
1548 Some("Nice work!".to_string()),
1549 "Second payment should have sender comment"
1550 );
1551 assert_eq!(
1552 metadata.nostr_zap_request, None,
1553 "Second payment should not have zap request"
1554 );
1555 }
1556 _ => panic!("Expected Lightning payment with lnurl receive metadata"),
1557 }
1558
1559 let retrieved_zap3 = storage
1560 .get_payment_by_id(lightning_zap_payment3.id.clone())
1561 .await
1562 .unwrap();
1563
1564 match retrieved_zap3.details {
1565 Some(PaymentDetails::Lightning {
1566 lnurl_receive_metadata: Some(metadata),
1567 ..
1568 }) => {
1569 assert_eq!(
1570 metadata.sender_comment, None,
1571 "Third payment should not have sender comment"
1572 );
1573 assert_eq!(
1574 metadata.nostr_zap_request,
1575 Some(r#"{"kind":9734,"content":"zap3"}"#.to_string()),
1576 "Third payment should have zap request"
1577 );
1578 }
1579 _ => panic!("Expected Lightning payment with lnurl receive metadata"),
1580 }
1581
1582 let retrieved_minimal = storage
1584 .get_payment_by_id(lightning_minimal_payment.id.clone())
1585 .await
1586 .unwrap();
1587
1588 match retrieved_minimal.details {
1589 Some(PaymentDetails::Lightning {
1590 lnurl_receive_metadata,
1591 ..
1592 }) => {
1593 assert!(
1594 lnurl_receive_metadata.is_none(),
1595 "Payment without metadata should have None"
1596 );
1597 }
1598 _ => panic!("Expected Lightning payment"),
1599 }
1600 }
1601
1602 pub async fn test_unclaimed_deposits_crud(storage: Box<dyn Storage>) {
1603 let deposits = storage.list_deposits().await.unwrap();
1605 assert_eq!(deposits.len(), 0);
1606
1607 storage
1609 .add_deposit("tx123".to_string(), 0, 50000)
1610 .await
1611 .unwrap();
1612 let deposits = storage.list_deposits().await.unwrap();
1613 assert_eq!(deposits.len(), 1);
1614 assert_eq!(deposits[0].txid, "tx123");
1615 assert_eq!(deposits[0].vout, 0);
1616 assert_eq!(deposits[0].amount_sats, 50000);
1617 assert!(deposits[0].claim_error.is_none());
1618
1619 storage
1621 .add_deposit("tx456".to_string(), 1, 75000)
1622 .await
1623 .unwrap();
1624 storage
1625 .update_deposit(
1626 "tx456".to_string(),
1627 1,
1628 UpdateDepositPayload::ClaimError {
1629 error: DepositClaimError::Generic {
1630 message: "Test error".to_string(),
1631 },
1632 },
1633 )
1634 .await
1635 .unwrap();
1636 let deposits = storage.list_deposits().await.unwrap();
1637 assert_eq!(deposits.len(), 2);
1638
1639 let deposit2_found = deposits.iter().find(|d| d.txid == "tx456").unwrap();
1641 assert_eq!(deposit2_found.vout, 1);
1642 assert_eq!(deposit2_found.amount_sats, 75000);
1643 assert!(deposit2_found.claim_error.is_some());
1644
1645 storage
1647 .delete_deposit("tx123".to_string(), 0)
1648 .await
1649 .unwrap();
1650 let deposits = storage.list_deposits().await.unwrap();
1651 assert_eq!(deposits.len(), 1);
1652 assert_eq!(deposits[0].txid, "tx456");
1653
1654 storage
1656 .delete_deposit("tx456".to_string(), 1)
1657 .await
1658 .unwrap();
1659 let deposits = storage.list_deposits().await.unwrap();
1660 assert_eq!(deposits.len(), 0);
1661 }
1662
1663 pub async fn test_deposit_refunds(storage: Box<dyn Storage>) {
1664 storage
1666 .add_deposit("test_tx_123".to_string(), 0, 100_000)
1667 .await
1668 .unwrap();
1669 let deposits = storage.list_deposits().await.unwrap();
1670 assert_eq!(deposits.len(), 1);
1671 assert_eq!(deposits[0].txid, "test_tx_123");
1672 assert_eq!(deposits[0].vout, 0);
1673 assert_eq!(deposits[0].amount_sats, 100_000);
1674 assert!(deposits[0].claim_error.is_none());
1675
1676 storage
1678 .update_deposit(
1679 "test_tx_123".to_string(),
1680 0,
1681 UpdateDepositPayload::Refund {
1682 refund_txid: "refund_tx_id_456".to_string(),
1683 refund_tx: "0200000001abcd1234...".to_string(),
1684 },
1685 )
1686 .await
1687 .unwrap();
1688
1689 let deposits = storage.list_deposits().await.unwrap();
1691 assert_eq!(deposits.len(), 1);
1692 assert_eq!(deposits[0].txid, "test_tx_123");
1693 assert_eq!(deposits[0].vout, 0);
1694 assert_eq!(deposits[0].amount_sats, 100_000);
1695 assert!(deposits[0].claim_error.is_none());
1696 assert_eq!(
1697 deposits[0].refund_tx_id,
1698 Some("refund_tx_id_456".to_string())
1699 );
1700 assert_eq!(
1701 deposits[0].refund_tx,
1702 Some("0200000001abcd1234...".to_string())
1703 );
1704 }
1705
1706 pub async fn test_payment_type_filtering(storage: Box<dyn Storage>) {
1707 let send_payment = Payment {
1709 id: "send_1".to_string(),
1710 payment_type: PaymentType::Send,
1711 status: PaymentStatus::Completed,
1712 amount: 10_000,
1713 fees: 100,
1714 timestamp: 1000,
1715 method: PaymentMethod::Lightning,
1716 details: Some(PaymentDetails::Lightning {
1717 invoice: "lnbc1".to_string(),
1718 payment_hash: "hash1".to_string(),
1719 destination_pubkey: "pubkey1".to_string(),
1720 description: None,
1721 preimage: None,
1722 lnurl_pay_info: None,
1723 lnurl_withdraw_info: None,
1724 lnurl_receive_metadata: None,
1725 }),
1726 };
1727
1728 let receive_payment = Payment {
1729 id: "receive_1".to_string(),
1730 payment_type: PaymentType::Receive,
1731 status: PaymentStatus::Completed,
1732 amount: 20_000,
1733 fees: 200,
1734 timestamp: 2000,
1735 method: PaymentMethod::Lightning,
1736 details: Some(PaymentDetails::Lightning {
1737 invoice: "lnbc2".to_string(),
1738 payment_hash: "hash2".to_string(),
1739 destination_pubkey: "pubkey2".to_string(),
1740 description: None,
1741 preimage: None,
1742 lnurl_pay_info: None,
1743 lnurl_withdraw_info: None,
1744 lnurl_receive_metadata: None,
1745 }),
1746 };
1747
1748 storage.insert_payment(send_payment).await.unwrap();
1749 storage.insert_payment(receive_payment).await.unwrap();
1750
1751 let send_only = storage
1753 .list_payments(ListPaymentsRequest {
1754 type_filter: Some(vec![PaymentType::Send]),
1755 ..Default::default()
1756 })
1757 .await
1758 .unwrap();
1759 assert_eq!(send_only.len(), 1);
1760 assert_eq!(send_only[0].id, "send_1");
1761
1762 let receive_only = storage
1764 .list_payments(ListPaymentsRequest {
1765 type_filter: Some(vec![PaymentType::Receive]),
1766 ..Default::default()
1767 })
1768 .await
1769 .unwrap();
1770 assert_eq!(receive_only.len(), 1);
1771 assert_eq!(receive_only[0].id, "receive_1");
1772
1773 let both_types = storage
1775 .list_payments(ListPaymentsRequest {
1776 type_filter: Some(vec![PaymentType::Send, PaymentType::Receive]),
1777 ..Default::default()
1778 })
1779 .await
1780 .unwrap();
1781 assert_eq!(both_types.len(), 2);
1782
1783 let all_payments = storage
1785 .list_payments(ListPaymentsRequest::default())
1786 .await
1787 .unwrap();
1788 assert_eq!(all_payments.len(), 2);
1789 }
1790
1791 pub async fn test_payment_status_filtering(storage: Box<dyn Storage>) {
1792 let completed_payment = Payment {
1794 id: "completed_1".to_string(),
1795 payment_type: PaymentType::Send,
1796 status: PaymentStatus::Completed,
1797 amount: 10_000,
1798 fees: 100,
1799 timestamp: 1000,
1800 method: PaymentMethod::Spark,
1801 details: Some(PaymentDetails::Spark {
1802 invoice_details: None,
1803 htlc_details: None,
1804 conversion_info: None,
1805 }),
1806 };
1807
1808 let pending_payment = Payment {
1809 id: "pending_1".to_string(),
1810 payment_type: PaymentType::Send,
1811 status: PaymentStatus::Pending,
1812 amount: 20_000,
1813 fees: 200,
1814 timestamp: 2000,
1815 method: PaymentMethod::Spark,
1816 details: Some(PaymentDetails::Spark {
1817 invoice_details: None,
1818 htlc_details: None,
1819 conversion_info: None,
1820 }),
1821 };
1822
1823 let failed_payment = Payment {
1824 id: "failed_1".to_string(),
1825 payment_type: PaymentType::Send,
1826 status: PaymentStatus::Failed,
1827 amount: 30_000,
1828 fees: 300,
1829 timestamp: 3000,
1830 method: PaymentMethod::Spark,
1831 details: Some(PaymentDetails::Spark {
1832 invoice_details: None,
1833 htlc_details: None,
1834 conversion_info: None,
1835 }),
1836 };
1837
1838 storage.insert_payment(completed_payment).await.unwrap();
1839 storage.insert_payment(pending_payment).await.unwrap();
1840 storage.insert_payment(failed_payment).await.unwrap();
1841
1842 let completed_only = storage
1844 .list_payments(ListPaymentsRequest {
1845 status_filter: Some(vec![PaymentStatus::Completed]),
1846 ..Default::default()
1847 })
1848 .await
1849 .unwrap();
1850 assert_eq!(completed_only.len(), 1);
1851 assert_eq!(completed_only[0].id, "completed_1");
1852
1853 let pending_only = storage
1855 .list_payments(ListPaymentsRequest {
1856 status_filter: Some(vec![PaymentStatus::Pending]),
1857 ..Default::default()
1858 })
1859 .await
1860 .unwrap();
1861 assert_eq!(pending_only.len(), 1);
1862 assert_eq!(pending_only[0].id, "pending_1");
1863
1864 let completed_or_failed = storage
1866 .list_payments(ListPaymentsRequest {
1867 status_filter: Some(vec![PaymentStatus::Completed, PaymentStatus::Failed]),
1868 ..Default::default()
1869 })
1870 .await
1871 .unwrap();
1872 assert_eq!(completed_or_failed.len(), 2);
1873 }
1874
1875 #[allow(clippy::too_many_lines)]
1876 pub async fn test_asset_filtering(storage: Box<dyn Storage>) {
1877 use crate::models::TokenMetadata;
1878
1879 let spark_payment = Payment {
1881 id: "spark_1".to_string(),
1882 payment_type: PaymentType::Send,
1883 status: PaymentStatus::Completed,
1884 amount: 10_000,
1885 fees: 100,
1886 timestamp: 1000,
1887 method: PaymentMethod::Spark,
1888 details: Some(PaymentDetails::Spark {
1889 invoice_details: None,
1890 htlc_details: None,
1891 conversion_info: None,
1892 }),
1893 };
1894
1895 let lightning_payment = Payment {
1896 id: "lightning_1".to_string(),
1897 payment_type: PaymentType::Send,
1898 status: PaymentStatus::Completed,
1899 amount: 20_000,
1900 fees: 200,
1901 timestamp: 2000,
1902 method: PaymentMethod::Lightning,
1903 details: Some(PaymentDetails::Lightning {
1904 invoice: "lnbc1".to_string(),
1905 payment_hash: "hash1".to_string(),
1906 destination_pubkey: "pubkey1".to_string(),
1907 description: None,
1908 preimage: None,
1909 lnurl_pay_info: None,
1910 lnurl_withdraw_info: None,
1911 lnurl_receive_metadata: None,
1912 }),
1913 };
1914
1915 let token_payment = Payment {
1916 id: "token_1".to_string(),
1917 payment_type: PaymentType::Receive,
1918 status: PaymentStatus::Completed,
1919 amount: 30_000,
1920 fees: 300,
1921 timestamp: 3000,
1922 method: PaymentMethod::Token,
1923 details: Some(PaymentDetails::Token {
1924 metadata: TokenMetadata {
1925 identifier: "token_id_1".to_string(),
1926 issuer_public_key: "pubkey".to_string(),
1927 name: "Token 1".to_string(),
1928 ticker: "TK1".to_string(),
1929 decimals: 8,
1930 max_supply: 1_000_000,
1931 is_freezable: false,
1932 },
1933 tx_hash: "tx_hash_1".to_string(),
1934 invoice_details: None,
1935 conversion_info: None,
1936 }),
1937 };
1938
1939 let withdraw_payment = Payment {
1940 id: "withdraw_1".to_string(),
1941 payment_type: PaymentType::Send,
1942 status: PaymentStatus::Completed,
1943 amount: 40_000,
1944 fees: 400,
1945 timestamp: 4000,
1946 method: PaymentMethod::Withdraw,
1947 details: Some(PaymentDetails::Withdraw {
1948 tx_id: "withdraw_tx_1".to_string(),
1949 }),
1950 };
1951
1952 let deposit_payment = Payment {
1953 id: "deposit_1".to_string(),
1954 payment_type: PaymentType::Receive,
1955 status: PaymentStatus::Completed,
1956 amount: 50_000,
1957 fees: 500,
1958 timestamp: 5000,
1959 method: PaymentMethod::Deposit,
1960 details: Some(PaymentDetails::Deposit {
1961 tx_id: "deposit_tx_1".to_string(),
1962 }),
1963 };
1964
1965 storage.insert_payment(spark_payment).await.unwrap();
1966 storage.insert_payment(lightning_payment).await.unwrap();
1967 storage.insert_payment(token_payment).await.unwrap();
1968 storage.insert_payment(withdraw_payment).await.unwrap();
1969 storage.insert_payment(deposit_payment).await.unwrap();
1970
1971 let spark_only = storage
1973 .list_payments(ListPaymentsRequest {
1974 asset_filter: Some(crate::AssetFilter::Bitcoin),
1975 ..Default::default()
1976 })
1977 .await
1978 .unwrap();
1979 assert_eq!(spark_only.len(), 4);
1980
1981 let token_only = storage
1983 .list_payments(ListPaymentsRequest {
1984 asset_filter: Some(crate::AssetFilter::Token {
1985 token_identifier: None,
1986 }),
1987 ..Default::default()
1988 })
1989 .await
1990 .unwrap();
1991 assert_eq!(token_only.len(), 1);
1992 assert_eq!(token_only[0].id, "token_1");
1993
1994 let token_specific = storage
1996 .list_payments(ListPaymentsRequest {
1997 asset_filter: Some(crate::AssetFilter::Token {
1998 token_identifier: Some("token_id_1".to_string()),
1999 }),
2000 ..Default::default()
2001 })
2002 .await
2003 .unwrap();
2004 assert_eq!(token_specific.len(), 1);
2005 assert_eq!(token_specific[0].id, "token_1");
2006
2007 let token_no_match = storage
2009 .list_payments(ListPaymentsRequest {
2010 asset_filter: Some(crate::AssetFilter::Token {
2011 token_identifier: Some("nonexistent".to_string()),
2012 }),
2013 ..Default::default()
2014 })
2015 .await
2016 .unwrap();
2017 assert_eq!(token_no_match.len(), 0);
2018 }
2019
2020 #[allow(clippy::too_many_lines)]
2021 pub async fn test_spark_htlc_status_filtering(storage: Box<dyn Storage>) {
2022 let htlc_waiting = Payment {
2024 id: "htlc_waiting".to_string(),
2025 payment_type: PaymentType::Receive,
2026 status: PaymentStatus::Pending,
2027 amount: 10_000,
2028 fees: 0,
2029 timestamp: 1000,
2030 method: PaymentMethod::Spark,
2031 details: Some(PaymentDetails::Spark {
2032 invoice_details: None,
2033 htlc_details: Some(SparkHtlcDetails {
2034 payment_hash: "hash1".to_string(),
2035 preimage: None,
2036 expiry_time: 2000,
2037 status: SparkHtlcStatus::WaitingForPreimage,
2038 }),
2039 conversion_info: None,
2040 }),
2041 };
2042
2043 let htlc_shared = Payment {
2044 id: "htlc_shared".to_string(),
2045 payment_type: PaymentType::Receive,
2046 status: PaymentStatus::Completed,
2047 amount: 20_000,
2048 fees: 0,
2049 timestamp: 2000,
2050 method: PaymentMethod::Spark,
2051 details: Some(PaymentDetails::Spark {
2052 invoice_details: None,
2053 htlc_details: Some(SparkHtlcDetails {
2054 payment_hash: "hash2".to_string(),
2055 preimage: Some("preimage123".to_string()),
2056 expiry_time: 3000,
2057 status: SparkHtlcStatus::PreimageShared,
2058 }),
2059 conversion_info: None,
2060 }),
2061 };
2062
2063 let htlc_returned = Payment {
2064 id: "htlc_returned".to_string(),
2065 payment_type: PaymentType::Receive,
2066 status: PaymentStatus::Failed,
2067 amount: 30_000,
2068 fees: 0,
2069 timestamp: 3000,
2070 method: PaymentMethod::Spark,
2071 details: Some(PaymentDetails::Spark {
2072 invoice_details: None,
2073 htlc_details: Some(SparkHtlcDetails {
2074 payment_hash: "hash3".to_string(),
2075 preimage: None,
2076 expiry_time: 4000,
2077 status: SparkHtlcStatus::Returned,
2078 }),
2079 conversion_info: None,
2080 }),
2081 };
2082
2083 let non_htlc_payment = Payment {
2085 id: "non_htlc".to_string(),
2086 payment_type: PaymentType::Send,
2087 status: PaymentStatus::Completed,
2088 amount: 40_000,
2089 fees: 100,
2090 timestamp: 4000,
2091 method: PaymentMethod::Spark,
2092 details: Some(PaymentDetails::Spark {
2093 invoice_details: Some(crate::SparkInvoicePaymentDetails {
2094 description: Some("Test invoice".to_string()),
2095 invoice: "spark_invoice".to_string(),
2096 }),
2097 htlc_details: None,
2098 conversion_info: None,
2099 }),
2100 };
2101
2102 storage.insert_payment(htlc_waiting).await.unwrap();
2104 storage.insert_payment(htlc_shared).await.unwrap();
2105 storage.insert_payment(htlc_returned).await.unwrap();
2106 storage.insert_payment(non_htlc_payment).await.unwrap();
2107
2108 let waiting_filter = storage
2110 .list_payments(ListPaymentsRequest {
2111 payment_details_filter: Some(vec![crate::PaymentDetailsFilter::Spark {
2112 htlc_status: Some(vec![SparkHtlcStatus::WaitingForPreimage]),
2113 conversion_refund_needed: None,
2114 }]),
2115 ..Default::default()
2116 })
2117 .await
2118 .unwrap();
2119 assert_eq!(waiting_filter.len(), 1);
2120 assert_eq!(waiting_filter[0].id, "htlc_waiting");
2121
2122 let shared_filter = storage
2124 .list_payments(ListPaymentsRequest {
2125 payment_details_filter: Some(vec![crate::PaymentDetailsFilter::Spark {
2126 htlc_status: Some(vec![SparkHtlcStatus::PreimageShared]),
2127 conversion_refund_needed: None,
2128 }]),
2129 ..Default::default()
2130 })
2131 .await
2132 .unwrap();
2133 assert_eq!(shared_filter.len(), 1);
2134 assert_eq!(shared_filter[0].id, "htlc_shared");
2135
2136 let returned_filter = storage
2138 .list_payments(ListPaymentsRequest {
2139 payment_details_filter: Some(vec![crate::PaymentDetailsFilter::Spark {
2140 htlc_status: Some(vec![SparkHtlcStatus::Returned]),
2141 conversion_refund_needed: None,
2142 }]),
2143 ..Default::default()
2144 })
2145 .await
2146 .unwrap();
2147 assert_eq!(returned_filter.len(), 1);
2148 assert_eq!(returned_filter[0].id, "htlc_returned");
2149
2150 let multiple_filter = storage
2152 .list_payments(ListPaymentsRequest {
2153 payment_details_filter: Some(vec![crate::PaymentDetailsFilter::Spark {
2154 htlc_status: Some(vec![
2155 SparkHtlcStatus::WaitingForPreimage,
2156 SparkHtlcStatus::PreimageShared,
2157 ]),
2158 conversion_refund_needed: None,
2159 }]),
2160 ..Default::default()
2161 })
2162 .await
2163 .unwrap();
2164 assert_eq!(multiple_filter.len(), 2);
2165 assert!(multiple_filter.iter().any(|p| p.id == "htlc_waiting"));
2166 assert!(multiple_filter.iter().any(|p| p.id == "htlc_shared"));
2167
2168 let all_htlc_filter = storage
2170 .list_payments(ListPaymentsRequest {
2171 payment_details_filter: Some(vec![crate::PaymentDetailsFilter::Spark {
2172 htlc_status: Some(vec![
2173 SparkHtlcStatus::WaitingForPreimage,
2174 SparkHtlcStatus::PreimageShared,
2175 SparkHtlcStatus::Returned,
2176 ]),
2177 conversion_refund_needed: None,
2178 }]),
2179 ..Default::default()
2180 })
2181 .await
2182 .unwrap();
2183 assert_eq!(all_htlc_filter.len(), 3);
2184 assert!(all_htlc_filter.iter().all(|p| p.id != "non_htlc"));
2185 }
2186
2187 #[allow(clippy::too_many_lines)]
2188 pub async fn test_conversion_refund_needed_filtering(storage: Box<dyn Storage>) {
2189 let payment_with_refund_metadata = PaymentMetadata {
2191 conversion_info: Some(crate::ConversionInfo {
2192 pool_id: "pool1".to_string(),
2193 conversion_id: "with_refund".to_string(),
2194 status: crate::ConversionStatus::Refunded,
2195 fee: None,
2196 purpose: None,
2197 }),
2198 ..Default::default()
2199 };
2200 let payment_with_refund = Payment {
2201 id: "with_refund".to_string(),
2202 payment_type: PaymentType::Send,
2203 status: PaymentStatus::Completed,
2204 amount: 10_000_000,
2205 fees: 0,
2206 timestamp: 1000,
2207 method: PaymentMethod::Token,
2208 details: Some(PaymentDetails::Token {
2209 metadata: crate::TokenMetadata {
2210 identifier: "token1".to_string(),
2211 issuer_public_key: "pubkey1".to_string(),
2212 name: "Test Token".to_string(),
2213 ticker: "TTK".to_string(),
2214 decimals: 8,
2215 max_supply: 1_000_000_000,
2216 is_freezable: false,
2217 },
2218 tx_hash: "txhash1".to_string(),
2219 invoice_details: None,
2220 conversion_info: None,
2221 }),
2222 };
2223
2224 let successful_conversion_metadata = PaymentMetadata {
2225 conversion_info: Some(crate::ConversionInfo {
2226 pool_id: "pool1".to_string(),
2227 conversion_id: "successful_conversion".to_string(),
2228 status: crate::ConversionStatus::Completed,
2229 fee: Some(100),
2230 purpose: None,
2231 }),
2232 ..Default::default()
2233 };
2234 let successful_conversion = Payment {
2235 id: "successful_conversion".to_string(),
2236 payment_type: PaymentType::Send,
2237 status: PaymentStatus::Completed,
2238 amount: 20_000,
2239 fees: 0,
2240 timestamp: 2000,
2241 method: PaymentMethod::Spark,
2242 details: Some(PaymentDetails::Spark {
2243 invoice_details: None,
2244 htlc_details: None,
2245 conversion_info: None,
2246 }),
2247 };
2248
2249 let payment_without_refund_metadata = PaymentMetadata {
2250 conversion_info: Some(crate::ConversionInfo {
2251 pool_id: "pool1".to_string(),
2252 conversion_id: "without_refund".to_string(),
2253 status: crate::ConversionStatus::RefundNeeded,
2254 fee: None,
2255 purpose: None,
2256 }),
2257 ..Default::default()
2258 };
2259 let payment_without_refund = Payment {
2260 id: "without_refund".to_string(),
2261 payment_type: PaymentType::Send,
2262 status: PaymentStatus::Completed,
2263 amount: 10_000,
2264 fees: 0,
2265 timestamp: 3000,
2266 method: PaymentMethod::Spark,
2267 details: Some(PaymentDetails::Spark {
2268 invoice_details: None,
2269 htlc_details: None,
2270 conversion_info: None,
2271 }),
2272 };
2273
2274 storage.insert_payment(payment_with_refund).await.unwrap();
2275 storage.insert_payment(successful_conversion).await.unwrap();
2276 storage
2277 .insert_payment(payment_without_refund)
2278 .await
2279 .unwrap();
2280 storage
2281 .set_payment_metadata("with_refund".to_string(), payment_with_refund_metadata)
2282 .await
2283 .unwrap();
2284 storage
2285 .set_payment_metadata(
2286 "successful_conversion".to_string(),
2287 successful_conversion_metadata,
2288 )
2289 .await
2290 .unwrap();
2291 storage
2292 .set_payment_metadata(
2293 "without_refund".to_string(),
2294 payment_without_refund_metadata,
2295 )
2296 .await
2297 .unwrap();
2298
2299 let payments = storage
2300 .list_payments(ListPaymentsRequest::default())
2301 .await
2302 .unwrap();
2303 assert_eq!(payments.len(), 3);
2304
2305 let missing_refund_filter = storage
2307 .list_payments(ListPaymentsRequest {
2308 payment_details_filter: Some(vec![crate::PaymentDetailsFilter::Spark {
2309 htlc_status: None,
2310 conversion_refund_needed: Some(true),
2311 }]),
2312 ..Default::default()
2313 })
2314 .await
2315 .unwrap();
2316 assert_eq!(missing_refund_filter.len(), 1);
2317 assert_eq!(missing_refund_filter[0].id, "without_refund");
2318
2319 let present_refund_filter = storage
2321 .list_payments(ListPaymentsRequest {
2322 payment_details_filter: Some(vec![crate::PaymentDetailsFilter::Token {
2323 conversion_refund_needed: Some(false),
2324 tx_hash: None,
2325 }]),
2326 ..Default::default()
2327 })
2328 .await
2329 .unwrap();
2330 assert_eq!(present_refund_filter.len(), 1);
2331 assert_eq!(present_refund_filter[0].id, "with_refund");
2332
2333 let multiple_filters = storage
2335 .list_payments(ListPaymentsRequest {
2336 payment_details_filter: Some(vec![
2337 crate::PaymentDetailsFilter::Spark {
2338 htlc_status: None,
2339 conversion_refund_needed: Some(true),
2340 },
2341 crate::PaymentDetailsFilter::Token {
2342 conversion_refund_needed: Some(false),
2343 tx_hash: None,
2344 },
2345 ]),
2346 ..Default::default()
2347 })
2348 .await
2349 .unwrap();
2350 assert_eq!(multiple_filters.len(), 2);
2351
2352 let token_no_refund_filter = storage
2354 .list_payments(ListPaymentsRequest {
2355 payment_details_filter: Some(vec![crate::PaymentDetailsFilter::Token {
2356 conversion_refund_needed: Some(true),
2357 tx_hash: None,
2358 }]),
2359 ..Default::default()
2360 })
2361 .await
2362 .unwrap();
2363 assert_eq!(token_no_refund_filter.len(), 0);
2364
2365 let spark_with_refund_filter = storage
2367 .list_payments(ListPaymentsRequest {
2368 payment_details_filter: Some(vec![crate::PaymentDetailsFilter::Spark {
2369 htlc_status: None,
2370 conversion_refund_needed: Some(false),
2371 }]),
2372 ..Default::default()
2373 })
2374 .await
2375 .unwrap();
2376 assert_eq!(spark_with_refund_filter.len(), 1);
2377
2378 let all_payments_filter = storage
2380 .list_payments(ListPaymentsRequest {
2381 payment_details_filter: Some(vec![crate::PaymentDetailsFilter::Spark {
2382 htlc_status: None,
2383 conversion_refund_needed: None,
2384 }]),
2385 ..Default::default()
2386 })
2387 .await
2388 .unwrap();
2389 assert_eq!(all_payments_filter.len(), 3);
2390 }
2391
2392 pub async fn test_timestamp_filtering(storage: Box<dyn Storage>) {
2393 let payment1 = Payment {
2395 id: "ts_1000".to_string(),
2396 payment_type: PaymentType::Send,
2397 status: PaymentStatus::Completed,
2398 amount: 10_000,
2399 fees: 100,
2400 timestamp: 1000,
2401 method: PaymentMethod::Spark,
2402 details: Some(PaymentDetails::Spark {
2403 invoice_details: None,
2404 htlc_details: None,
2405 conversion_info: None,
2406 }),
2407 };
2408
2409 let payment2 = Payment {
2410 id: "ts_2000".to_string(),
2411 payment_type: PaymentType::Send,
2412 status: PaymentStatus::Completed,
2413 amount: 20_000,
2414 fees: 200,
2415 timestamp: 2000,
2416 method: PaymentMethod::Spark,
2417 details: Some(PaymentDetails::Spark {
2418 invoice_details: None,
2419 htlc_details: None,
2420 conversion_info: None,
2421 }),
2422 };
2423
2424 let payment3 = Payment {
2425 id: "ts_3000".to_string(),
2426 payment_type: PaymentType::Send,
2427 status: PaymentStatus::Completed,
2428 amount: 30_000,
2429 fees: 300,
2430 timestamp: 3000,
2431 method: PaymentMethod::Spark,
2432 details: Some(PaymentDetails::Spark {
2433 invoice_details: None,
2434 htlc_details: None,
2435 conversion_info: None,
2436 }),
2437 };
2438
2439 storage.insert_payment(payment1).await.unwrap();
2440 storage.insert_payment(payment2).await.unwrap();
2441 storage.insert_payment(payment3).await.unwrap();
2442
2443 let from_2000 = storage
2445 .list_payments(ListPaymentsRequest {
2446 from_timestamp: Some(2000),
2447 ..Default::default()
2448 })
2449 .await
2450 .unwrap();
2451 assert_eq!(from_2000.len(), 2);
2452 assert!(from_2000.iter().any(|p| p.id == "ts_2000"));
2453 assert!(from_2000.iter().any(|p| p.id == "ts_3000"));
2454
2455 let to_2000 = storage
2457 .list_payments(ListPaymentsRequest {
2458 to_timestamp: Some(2000),
2459 ..Default::default()
2460 })
2461 .await
2462 .unwrap();
2463 assert_eq!(to_2000.len(), 1);
2464 assert!(to_2000.iter().any(|p| p.id == "ts_1000"));
2465
2466 let range = storage
2468 .list_payments(ListPaymentsRequest {
2469 from_timestamp: Some(1500),
2470 to_timestamp: Some(2500),
2471 ..Default::default()
2472 })
2473 .await
2474 .unwrap();
2475 assert_eq!(range.len(), 1);
2476 assert_eq!(range[0].id, "ts_2000");
2477 }
2478
2479 pub async fn test_combined_filters(storage: Box<dyn Storage>) {
2480 let payment1 = Payment {
2482 id: "combined_1".to_string(),
2483 payment_type: PaymentType::Send,
2484 status: PaymentStatus::Completed,
2485 amount: 10_000,
2486 fees: 100,
2487 timestamp: 1000,
2488 method: PaymentMethod::Spark,
2489 details: Some(PaymentDetails::Spark {
2490 invoice_details: None,
2491 htlc_details: None,
2492 conversion_info: None,
2493 }),
2494 };
2495
2496 let payment2 = Payment {
2497 id: "combined_2".to_string(),
2498 payment_type: PaymentType::Send,
2499 status: PaymentStatus::Pending,
2500 amount: 20_000,
2501 fees: 200,
2502 timestamp: 2000,
2503 method: PaymentMethod::Lightning,
2504 details: Some(PaymentDetails::Lightning {
2505 invoice: "lnbc1".to_string(),
2506 payment_hash: "hash1".to_string(),
2507 destination_pubkey: "pubkey1".to_string(),
2508 description: None,
2509 preimage: None,
2510 lnurl_pay_info: None,
2511 lnurl_withdraw_info: None,
2512 lnurl_receive_metadata: None,
2513 }),
2514 };
2515
2516 let payment3 = Payment {
2517 id: "combined_3".to_string(),
2518 payment_type: PaymentType::Receive,
2519 status: PaymentStatus::Completed,
2520 amount: 30_000,
2521 fees: 300,
2522 timestamp: 3000,
2523 method: PaymentMethod::Lightning,
2524 details: Some(PaymentDetails::Lightning {
2525 invoice: "lnbc2".to_string(),
2526 payment_hash: "hash2".to_string(),
2527 destination_pubkey: "pubkey2".to_string(),
2528 description: None,
2529 preimage: None,
2530 lnurl_pay_info: None,
2531 lnurl_withdraw_info: None,
2532 lnurl_receive_metadata: None,
2533 }),
2534 };
2535
2536 storage.insert_payment(payment1).await.unwrap();
2537 storage.insert_payment(payment2).await.unwrap();
2538 storage.insert_payment(payment3).await.unwrap();
2539
2540 let send_completed = storage
2542 .list_payments(ListPaymentsRequest {
2543 type_filter: Some(vec![PaymentType::Send]),
2544 status_filter: Some(vec![PaymentStatus::Completed]),
2545 ..Default::default()
2546 })
2547 .await
2548 .unwrap();
2549 assert_eq!(send_completed.len(), 1);
2550 assert_eq!(send_completed[0].id, "combined_1");
2551
2552 let bitcoin_recent = storage
2554 .list_payments(ListPaymentsRequest {
2555 asset_filter: Some(crate::AssetFilter::Bitcoin),
2556 from_timestamp: Some(2500),
2557 ..Default::default()
2558 })
2559 .await
2560 .unwrap();
2561 assert_eq!(bitcoin_recent.len(), 1);
2562 assert_eq!(bitcoin_recent[0].id, "combined_3");
2563
2564 let send_pending_bitcoin = storage
2566 .list_payments(ListPaymentsRequest {
2567 type_filter: Some(vec![PaymentType::Send]),
2568 status_filter: Some(vec![PaymentStatus::Pending]),
2569 asset_filter: Some(crate::AssetFilter::Bitcoin),
2570 ..Default::default()
2571 })
2572 .await
2573 .unwrap();
2574 assert_eq!(send_pending_bitcoin.len(), 1);
2575 assert_eq!(send_pending_bitcoin[0].id, "combined_2");
2576 }
2577
2578 pub async fn test_sort_order(storage: Box<dyn Storage>) {
2579 let payment1 = Payment {
2581 id: "sort_1".to_string(),
2582 payment_type: PaymentType::Send,
2583 status: PaymentStatus::Completed,
2584 amount: 10_000,
2585 fees: 100,
2586 timestamp: 1000,
2587 method: PaymentMethod::Spark,
2588 details: Some(PaymentDetails::Spark {
2589 invoice_details: None,
2590 htlc_details: None,
2591 conversion_info: None,
2592 }),
2593 };
2594
2595 let payment2 = Payment {
2596 id: "sort_2".to_string(),
2597 payment_type: PaymentType::Send,
2598 status: PaymentStatus::Completed,
2599 amount: 20_000,
2600 fees: 200,
2601 timestamp: 2000,
2602 method: PaymentMethod::Spark,
2603 details: Some(PaymentDetails::Spark {
2604 invoice_details: None,
2605 htlc_details: None,
2606 conversion_info: None,
2607 }),
2608 };
2609
2610 let payment3 = Payment {
2611 id: "sort_3".to_string(),
2612 payment_type: PaymentType::Send,
2613 status: PaymentStatus::Completed,
2614 amount: 30_000,
2615 fees: 300,
2616 timestamp: 3000,
2617 method: PaymentMethod::Spark,
2618 details: Some(PaymentDetails::Spark {
2619 invoice_details: None,
2620 htlc_details: None,
2621 conversion_info: None,
2622 }),
2623 };
2624
2625 storage.insert_payment(payment1).await.unwrap();
2626 storage.insert_payment(payment2).await.unwrap();
2627 storage.insert_payment(payment3).await.unwrap();
2628
2629 let desc_payments = storage
2631 .list_payments(ListPaymentsRequest::default())
2632 .await
2633 .unwrap();
2634 assert_eq!(desc_payments.len(), 3);
2635 assert_eq!(desc_payments[0].id, "sort_3"); assert_eq!(desc_payments[1].id, "sort_2");
2637 assert_eq!(desc_payments[2].id, "sort_1");
2638
2639 let asc_payments = storage
2641 .list_payments(ListPaymentsRequest {
2642 sort_ascending: Some(true),
2643 ..Default::default()
2644 })
2645 .await
2646 .unwrap();
2647 assert_eq!(asc_payments.len(), 3);
2648 assert_eq!(asc_payments[0].id, "sort_1"); assert_eq!(asc_payments[1].id, "sort_2");
2650 assert_eq!(asc_payments[2].id, "sort_3");
2651
2652 let desc_explicit = storage
2654 .list_payments(ListPaymentsRequest {
2655 sort_ascending: Some(false),
2656 ..Default::default()
2657 })
2658 .await
2659 .unwrap();
2660 assert_eq!(desc_explicit.len(), 3);
2661 assert_eq!(desc_explicit[0].id, "sort_3");
2662 assert_eq!(desc_explicit[1].id, "sort_2");
2663 assert_eq!(desc_explicit[2].id, "sort_1");
2664 }
2665
2666 pub async fn test_payment_metadata(storage: Box<dyn Storage>) {
2667 let cache = ObjectCacheRepository::new(storage.into());
2668
2669 let payment_request1 = "pr1".to_string();
2671 let metadata1 = PaymentMetadata {
2672 lnurl_description: Some("desc1".to_string()),
2673 lnurl_withdraw_info: Some(LnurlWithdrawInfo {
2674 withdraw_url: "https://callback.url".to_string(),
2675 }),
2676 ..Default::default()
2677 };
2678
2679 let payment_request2 = "pr2".to_string();
2680 let metadata2 = PaymentMetadata {
2681 lnurl_description: Some("desc2".to_string()),
2682 lnurl_withdraw_info: Some(LnurlWithdrawInfo {
2683 withdraw_url: "https://callback2.url".to_string(),
2684 }),
2685 ..Default::default()
2686 };
2687
2688 cache
2690 .save_payment_metadata(&payment_request1, &metadata1)
2691 .await
2692 .unwrap();
2693 cache
2694 .save_payment_metadata(&payment_request2, &metadata2)
2695 .await
2696 .unwrap();
2697
2698 let fetched1 = cache
2700 .fetch_payment_metadata(&payment_request1)
2701 .await
2702 .unwrap();
2703 assert!(fetched1.is_some());
2704 let fetched1 = fetched1.unwrap();
2705 assert_eq!(fetched1.lnurl_description.unwrap(), "desc1");
2706 assert_eq!(
2707 fetched1.lnurl_withdraw_info.unwrap().withdraw_url,
2708 "https://callback.url"
2709 );
2710
2711 let fetched2 = cache
2712 .fetch_payment_metadata(&payment_request2)
2713 .await
2714 .unwrap();
2715 assert!(fetched2.is_some());
2716
2717 cache
2719 .delete_payment_metadata(&payment_request1)
2720 .await
2721 .unwrap();
2722 let deleted = cache
2723 .fetch_payment_metadata(&payment_request1)
2724 .await
2725 .unwrap();
2726 assert!(deleted.is_none());
2727 }
2728
2729 pub async fn test_payment_details_update_persistence(storage: Box<dyn Storage>) {
2730 let mut payment = Payment {
2732 id: "payment_1".to_string(),
2733 payment_type: PaymentType::Send,
2734 status: PaymentStatus::Pending,
2735 amount: 15_000,
2736 fees: 150,
2737 timestamp: 1_234_567_890,
2738 method: PaymentMethod::Lightning,
2739 details: Some(PaymentDetails::Spark {
2740 invoice_details: None,
2741 htlc_details: Some(SparkHtlcDetails {
2742 payment_hash: "hash_123".to_string(),
2743 preimage: None,
2744 expiry_time: 1_234_567_990,
2745 status: SparkHtlcStatus::WaitingForPreimage,
2746 }),
2747 conversion_info: None,
2748 }),
2749 };
2750
2751 storage.insert_payment(payment.clone()).await.unwrap();
2753
2754 payment.status = PaymentStatus::Completed;
2756 storage.insert_payment(payment.clone()).await.unwrap();
2757
2758 let updated_payment = storage
2760 .get_payment_by_id("payment_1".to_string())
2761 .await
2762 .unwrap();
2763 assert_eq!(updated_payment.status, PaymentStatus::Completed);
2764 let Some(PaymentDetails::Spark { htlc_details, .. }) = &updated_payment.details else {
2765 panic!("Payment details are not of Spark type");
2766 };
2767 assert_eq!(
2768 htlc_details.as_ref().unwrap().status,
2769 SparkHtlcStatus::WaitingForPreimage
2770 );
2771
2772 payment.details = Some(PaymentDetails::Spark {
2774 invoice_details: None,
2775 htlc_details: Some(SparkHtlcDetails {
2776 payment_hash: "hash_123".to_string(),
2777 preimage: Some("preimage_123".to_string()),
2778 expiry_time: 1_234_567_990,
2779 status: SparkHtlcStatus::PreimageShared,
2780 }),
2781 conversion_info: None,
2782 });
2783 storage.insert_payment(payment.clone()).await.unwrap();
2784
2785 let updated_payment = storage
2787 .get_payment_by_id("payment_1".to_string())
2788 .await
2789 .unwrap();
2790 let Some(PaymentDetails::Spark { htlc_details, .. }) = &updated_payment.details else {
2791 panic!("Payment details are not of Spark type");
2792 };
2793 assert_eq!(
2794 htlc_details.as_ref().unwrap().status,
2795 SparkHtlcStatus::PreimageShared
2796 );
2797 assert_eq!(
2798 htlc_details.as_ref().unwrap().preimage.as_ref().unwrap(),
2799 "preimage_123"
2800 );
2801 }
2802}