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