1pub(crate) mod path;
2#[cfg(all(
3 feature = "postgres",
4 not(all(target_family = "wasm", target_os = "unknown"))
5))]
6pub mod postgres;
7#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
8pub(crate) mod sqlite;
9
10use std::{collections::HashMap, sync::Arc};
11
12use macros::async_trait;
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16use crate::{
17 AssetFilter, ConversionInfo, DepositClaimError, DepositInfo, LightningAddressInfo,
18 ListPaymentsRequest, LnurlPayInfo, LnurlWithdrawInfo, PaymentDetailsFilter, PaymentStatus,
19 PaymentType, SparkHtlcStatus, TokenBalance, TokenMetadata, TokenTransactionType,
20 models::Payment,
21 sync_storage::{IncomingChange, OutgoingChange, Record, UnversionedRecordChange},
22};
23
24const ACCOUNT_INFO_KEY: &str = "account_info";
25const LAST_SYNC_TIME_KEY: &str = "last_sync_time";
26const LIGHTNING_ADDRESS_KEY: &str = "lightning_address";
27const LNURL_METADATA_UPDATED_AFTER_KEY: &str = "lnurl_metadata_updated_after";
28const SYNC_OFFSET_KEY: &str = "sync_offset";
29const TX_CACHE_KEY: &str = "tx_cache";
30const STATIC_DEPOSIT_ADDRESS_CACHE_KEY: &str = "static_deposit_address";
31const TOKEN_METADATA_KEY_PREFIX: &str = "token_metadata_";
32const PAYMENT_METADATA_KEY_PREFIX: &str = "payment_metadata";
33const SPARK_PRIVATE_MODE_INITIALIZED_KEY: &str = "spark_private_mode_initialized";
34
35#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
36pub enum UpdateDepositPayload {
37 ClaimError {
38 error: DepositClaimError,
39 },
40 Refund {
41 refund_txid: String,
42 refund_tx: String,
43 },
44}
45
46#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
47pub struct SetLnurlMetadataItem {
48 pub payment_hash: String,
49 pub sender_comment: Option<String>,
50 pub nostr_zap_request: Option<String>,
51 pub nostr_zap_receipt: Option<String>,
52 pub preimage: Option<String>,
53}
54
55impl From<lnurl_models::ListMetadataMetadata> for SetLnurlMetadataItem {
56 fn from(value: lnurl_models::ListMetadataMetadata) -> Self {
57 SetLnurlMetadataItem {
58 payment_hash: value.payment_hash,
59 sender_comment: value.sender_comment,
60 nostr_zap_request: value.nostr_zap_request,
61 nostr_zap_receipt: value.nostr_zap_receipt,
62 preimage: value.preimage,
63 }
64 }
65}
66
67#[derive(Debug, Error, Clone)]
69#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
70pub enum StorageError {
71 #[error("Connection error: {0}")]
74 Connection(String),
75
76 #[error("Underlying implementation error: {0}")]
77 Implementation(String),
78
79 #[error("Failed to initialize database: {0}")]
81 InitializationError(String),
82
83 #[error("Failed to serialize/deserialize data: {0}")]
84 Serialization(String),
85}
86
87impl From<serde_json::Error> for StorageError {
88 fn from(e: serde_json::Error) -> Self {
89 StorageError::Serialization(e.to_string())
90 }
91}
92
93impl From<std::num::TryFromIntError> for StorageError {
94 fn from(e: std::num::TryFromIntError) -> Self {
95 StorageError::Implementation(format!("integer overflow: {e}"))
96 }
97}
98
99#[derive(Debug, Clone)]
103#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
104pub enum StoragePaymentDetailsFilter {
105 Spark {
106 htlc_status: Option<Vec<SparkHtlcStatus>>,
107 conversion_refund_needed: Option<bool>,
108 },
109 Token {
110 conversion_refund_needed: Option<bool>,
111 tx_hash: Option<String>,
112 tx_type: Option<TokenTransactionType>,
113 },
114 Lightning {
115 htlc_status: Option<Vec<SparkHtlcStatus>>,
116 has_lnurl_preimage: Option<bool>,
117 },
118}
119
120impl From<PaymentDetailsFilter> for StoragePaymentDetailsFilter {
121 fn from(filter: PaymentDetailsFilter) -> Self {
122 match filter {
123 PaymentDetailsFilter::Spark {
124 htlc_status,
125 conversion_refund_needed,
126 } => StoragePaymentDetailsFilter::Spark {
127 htlc_status,
128 conversion_refund_needed,
129 },
130 PaymentDetailsFilter::Token {
131 conversion_refund_needed,
132 tx_hash,
133 tx_type,
134 } => StoragePaymentDetailsFilter::Token {
135 conversion_refund_needed,
136 tx_hash,
137 tx_type,
138 },
139 PaymentDetailsFilter::Lightning { htlc_status } => {
140 StoragePaymentDetailsFilter::Lightning {
141 htlc_status,
142 has_lnurl_preimage: None,
143 }
144 }
145 }
146 }
147}
148
149impl From<StoragePaymentDetailsFilter> for PaymentDetailsFilter {
150 fn from(filter: StoragePaymentDetailsFilter) -> Self {
151 match filter {
152 StoragePaymentDetailsFilter::Spark {
153 htlc_status,
154 conversion_refund_needed,
155 } => PaymentDetailsFilter::Spark {
156 htlc_status,
157 conversion_refund_needed,
158 },
159 StoragePaymentDetailsFilter::Token {
160 conversion_refund_needed,
161 tx_hash,
162 tx_type,
163 } => PaymentDetailsFilter::Token {
164 conversion_refund_needed,
165 tx_hash,
166 tx_type,
167 },
168 StoragePaymentDetailsFilter::Lightning { htlc_status, .. } => {
169 PaymentDetailsFilter::Lightning { htlc_status }
170 }
171 }
172 }
173}
174
175#[derive(Debug, Clone, Default)]
178#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
179pub struct StorageListPaymentsRequest {
180 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
181 pub type_filter: Option<Vec<PaymentType>>,
182 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
183 pub status_filter: Option<Vec<PaymentStatus>>,
184 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
185 pub asset_filter: Option<AssetFilter>,
186 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
187 pub payment_details_filter: Option<Vec<StoragePaymentDetailsFilter>>,
188 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
189 pub from_timestamp: Option<u64>,
190 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
191 pub to_timestamp: Option<u64>,
192 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
193 pub offset: Option<u32>,
194 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
195 pub limit: Option<u32>,
196 #[cfg_attr(feature = "uniffi", uniffi(default=None))]
197 pub sort_ascending: Option<bool>,
198}
199
200impl From<ListPaymentsRequest> for StorageListPaymentsRequest {
201 fn from(request: ListPaymentsRequest) -> Self {
202 StorageListPaymentsRequest {
203 type_filter: request.type_filter,
204 status_filter: request.status_filter,
205 asset_filter: request.asset_filter,
206 payment_details_filter: request
207 .payment_details_filter
208 .map(|filters| filters.into_iter().map(Into::into).collect()),
209 from_timestamp: request.from_timestamp,
210 to_timestamp: request.to_timestamp,
211 offset: request.offset,
212 limit: request.limit,
213 sort_ascending: request.sort_ascending,
214 }
215 }
216}
217
218impl From<StorageListPaymentsRequest> for ListPaymentsRequest {
219 fn from(request: StorageListPaymentsRequest) -> Self {
220 ListPaymentsRequest {
221 type_filter: request.type_filter,
222 status_filter: request.status_filter,
223 asset_filter: request.asset_filter,
224 payment_details_filter: request
225 .payment_details_filter
226 .map(|filters| filters.into_iter().map(Into::into).collect()),
227 from_timestamp: request.from_timestamp,
228 to_timestamp: request.to_timestamp,
229 offset: request.offset,
230 limit: request.limit,
231 sort_ascending: request.sort_ascending,
232 }
233 }
234}
235
236#[derive(Clone, Default, Deserialize, Serialize)]
238#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
239pub struct PaymentMetadata {
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub parent_payment_id: Option<String>,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub lnurl_pay_info: Option<LnurlPayInfo>,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub lnurl_withdraw_info: Option<LnurlWithdrawInfo>,
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub lnurl_description: Option<String>,
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub conversion_info: Option<ConversionInfo>,
250}
251
252#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
254#[async_trait]
255pub trait Storage: Send + Sync {
256 async fn delete_cached_item(&self, key: String) -> Result<(), StorageError>;
257 async fn get_cached_item(&self, key: String) -> Result<Option<String>, StorageError>;
258 async fn set_cached_item(&self, key: String, value: String) -> Result<(), StorageError>;
259 async fn list_payments(
269 &self,
270 request: StorageListPaymentsRequest,
271 ) -> Result<Vec<Payment>, StorageError>;
272
273 async fn insert_payment(&self, payment: Payment) -> Result<(), StorageError>;
283
284 async fn insert_payment_metadata(
295 &self,
296 payment_id: String,
297 metadata: PaymentMetadata,
298 ) -> Result<(), StorageError>;
299
300 async fn get_payment_by_id(&self, id: String) -> Result<Payment, StorageError>;
309
310 async fn get_payment_by_invoice(
318 &self,
319 invoice: String,
320 ) -> Result<Option<Payment>, StorageError>;
321
322 async fn get_payments_by_parent_ids(
333 &self,
334 parent_payment_ids: Vec<String>,
335 ) -> Result<HashMap<String, Vec<Payment>>, StorageError>;
336
337 async fn add_deposit(
348 &self,
349 txid: String,
350 vout: u32,
351 amount_sats: u64,
352 ) -> Result<(), StorageError>;
353
354 async fn delete_deposit(&self, txid: String, vout: u32) -> Result<(), StorageError>;
364
365 async fn list_deposits(&self) -> Result<Vec<DepositInfo>, StorageError>;
370
371 async fn update_deposit(
382 &self,
383 txid: String,
384 vout: u32,
385 payload: UpdateDepositPayload,
386 ) -> Result<(), StorageError>;
387
388 async fn set_lnurl_metadata(
389 &self,
390 metadata: Vec<SetLnurlMetadataItem>,
391 ) -> Result<(), StorageError>;
392
393 async fn add_outgoing_change(
395 &self,
396 record: UnversionedRecordChange,
397 ) -> Result<u64, StorageError>;
398 async fn complete_outgoing_sync(
399 &self,
400 record: Record,
401 local_revision: u64,
402 ) -> Result<(), StorageError>;
403 async fn get_pending_outgoing_changes(
404 &self,
405 limit: u32,
406 ) -> Result<Vec<OutgoingChange>, StorageError>;
407
408 async fn get_last_revision(&self) -> Result<u64, StorageError>;
415
416 async fn insert_incoming_records(&self, records: Vec<Record>) -> Result<(), StorageError>;
418
419 async fn delete_incoming_record(&self, record: Record) -> Result<(), StorageError>;
421
422 async fn get_incoming_records(&self, limit: u32) -> Result<Vec<IncomingChange>, StorageError>;
424
425 async fn get_latest_outgoing_change(&self) -> Result<Option<OutgoingChange>, StorageError>;
427
428 async fn update_record_from_incoming(&self, record: Record) -> Result<(), StorageError>;
430}
431
432pub(crate) struct ObjectCacheRepository {
433 storage: Arc<dyn Storage>,
434}
435
436impl ObjectCacheRepository {
437 pub(crate) fn new(storage: Arc<dyn Storage>) -> Self {
438 ObjectCacheRepository { storage }
439 }
440
441 pub(crate) async fn save_account_info(
442 &self,
443 value: &CachedAccountInfo,
444 ) -> Result<(), StorageError> {
445 self.storage
446 .set_cached_item(ACCOUNT_INFO_KEY.to_string(), serde_json::to_string(value)?)
447 .await?;
448 Ok(())
449 }
450
451 pub(crate) async fn fetch_account_info(
452 &self,
453 ) -> Result<Option<CachedAccountInfo>, StorageError> {
454 let value = self
455 .storage
456 .get_cached_item(ACCOUNT_INFO_KEY.to_string())
457 .await?;
458 match value {
459 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
460 None => Ok(None),
461 }
462 }
463
464 pub(crate) async fn save_sync_info(&self, value: &CachedSyncInfo) -> Result<(), StorageError> {
465 self.storage
466 .set_cached_item(SYNC_OFFSET_KEY.to_string(), serde_json::to_string(value)?)
467 .await?;
468 Ok(())
469 }
470
471 pub(crate) async fn fetch_sync_info(&self) -> Result<Option<CachedSyncInfo>, StorageError> {
472 let value = self
473 .storage
474 .get_cached_item(SYNC_OFFSET_KEY.to_string())
475 .await?;
476 match value {
477 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
478 None => Ok(None),
479 }
480 }
481
482 pub(crate) async fn save_tx(&self, txid: &str, value: &CachedTx) -> Result<(), StorageError> {
483 self.storage
484 .set_cached_item(
485 format!("{TX_CACHE_KEY}-{txid}"),
486 serde_json::to_string(value)?,
487 )
488 .await?;
489 Ok(())
490 }
491
492 pub(crate) async fn fetch_tx(&self, txid: &str) -> Result<Option<CachedTx>, StorageError> {
493 let value = self
494 .storage
495 .get_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
496 .await?;
497 match value {
498 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
499 None => Ok(None),
500 }
501 }
502
503 pub(crate) async fn save_static_deposit_address(
504 &self,
505 value: &StaticDepositAddress,
506 ) -> Result<(), StorageError> {
507 self.storage
508 .set_cached_item(
509 STATIC_DEPOSIT_ADDRESS_CACHE_KEY.to_string(),
510 serde_json::to_string(value)?,
511 )
512 .await?;
513 Ok(())
514 }
515
516 pub(crate) async fn fetch_static_deposit_address(
517 &self,
518 ) -> Result<Option<StaticDepositAddress>, StorageError> {
519 let value = self
520 .storage
521 .get_cached_item(STATIC_DEPOSIT_ADDRESS_CACHE_KEY.to_string())
522 .await?;
523 match value {
524 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
525 None => Ok(None),
526 }
527 }
528
529 pub(crate) async fn save_lightning_address(
530 &self,
531 value: &LightningAddressInfo,
532 ) -> Result<(), StorageError> {
533 self.storage
534 .set_cached_item(
535 LIGHTNING_ADDRESS_KEY.to_string(),
536 serde_json::to_string(&Some(value))?,
537 )
538 .await?;
539 Ok(())
540 }
541
542 pub(crate) async fn delete_lightning_address(&self) -> Result<(), StorageError> {
544 self.storage
545 .set_cached_item(
546 LIGHTNING_ADDRESS_KEY.to_string(),
547 serde_json::to_string(&None::<LightningAddressInfo>)?,
548 )
549 .await?;
550 Ok(())
551 }
552
553 pub(crate) async fn fetch_lightning_address(
558 &self,
559 ) -> Result<Option<Option<LightningAddressInfo>>, StorageError> {
560 let value = self
561 .storage
562 .get_cached_item(LIGHTNING_ADDRESS_KEY.to_string())
563 .await?;
564 match value {
565 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
566 None => Ok(None),
567 }
568 }
569
570 pub(crate) async fn save_token_metadata(
571 &self,
572 value: &TokenMetadata,
573 ) -> Result<(), StorageError> {
574 self.storage
575 .set_cached_item(
576 format!("{TOKEN_METADATA_KEY_PREFIX}{}", value.identifier),
577 serde_json::to_string(value)?,
578 )
579 .await?;
580 Ok(())
581 }
582
583 pub(crate) async fn fetch_token_metadata(
584 &self,
585 identifier: &str,
586 ) -> Result<Option<TokenMetadata>, StorageError> {
587 let value = self
588 .storage
589 .get_cached_item(format!("{TOKEN_METADATA_KEY_PREFIX}{identifier}"))
590 .await?;
591 match value {
592 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
593 None => Ok(None),
594 }
595 }
596
597 pub(crate) async fn save_payment_metadata(
598 &self,
599 identifier: &str,
600 value: &PaymentMetadata,
601 ) -> Result<(), StorageError> {
602 self.storage
603 .set_cached_item(
604 format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}"),
605 serde_json::to_string(value)?,
606 )
607 .await?;
608 Ok(())
609 }
610
611 pub(crate) async fn fetch_payment_metadata(
612 &self,
613 identifier: &str,
614 ) -> Result<Option<PaymentMetadata>, StorageError> {
615 let value = self
616 .storage
617 .get_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}",))
618 .await?;
619 match value {
620 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
621 None => Ok(None),
622 }
623 }
624
625 pub(crate) async fn delete_payment_metadata(
626 &self,
627 identifier: &str,
628 ) -> Result<(), StorageError> {
629 self.storage
630 .delete_cached_item(format!("{PAYMENT_METADATA_KEY_PREFIX}-{identifier}",))
631 .await?;
632 Ok(())
633 }
634
635 pub(crate) async fn save_spark_private_mode_initialized(&self) -> Result<(), StorageError> {
636 self.storage
637 .set_cached_item(
638 SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string(),
639 "true".to_string(),
640 )
641 .await?;
642 Ok(())
643 }
644
645 pub(crate) async fn fetch_spark_private_mode_initialized(&self) -> Result<bool, StorageError> {
646 let value = self
647 .storage
648 .get_cached_item(SPARK_PRIVATE_MODE_INITIALIZED_KEY.to_string())
649 .await?;
650 match value {
651 Some(value) => Ok(value == "true"),
652 None => Ok(false),
653 }
654 }
655
656 pub(crate) async fn save_lnurl_metadata_updated_after(
657 &self,
658 offset: i64,
659 ) -> Result<(), StorageError> {
660 self.storage
661 .set_cached_item(
662 LNURL_METADATA_UPDATED_AFTER_KEY.to_string(),
663 offset.to_string(),
664 )
665 .await?;
666 Ok(())
667 }
668
669 pub(crate) async fn fetch_lnurl_metadata_updated_after(&self) -> Result<i64, StorageError> {
670 let value = self
671 .storage
672 .get_cached_item(LNURL_METADATA_UPDATED_AFTER_KEY.to_string())
673 .await?;
674 match value {
675 Some(value) => Ok(value.parse().map_err(|_| {
676 StorageError::Serialization("invalid lnurl_metadata_updated_after".to_string())
677 })?),
678 None => Ok(0),
679 }
680 }
681
682 pub(crate) async fn get_last_sync_time(&self) -> Result<Option<u64>, StorageError> {
683 let value = self
684 .storage
685 .get_cached_item(LAST_SYNC_TIME_KEY.to_string())
686 .await?;
687 match value {
688 Some(v) => Ok(Some(v.parse().map_err(|_| {
689 StorageError::Serialization("invalid last_sync_time".to_string())
690 })?)),
691 None => Ok(None),
692 }
693 }
694
695 pub(crate) async fn set_last_sync_time(&self, time: u64) -> Result<(), StorageError> {
696 self.storage
697 .set_cached_item(LAST_SYNC_TIME_KEY.to_string(), time.to_string())
698 .await
699 }
700}
701
702#[derive(Serialize, Deserialize, Default)]
703pub(crate) struct CachedAccountInfo {
704 pub(crate) balance_sats: u64,
705 #[serde(default)]
706 pub(crate) token_balances: HashMap<String, TokenBalance>,
707}
708
709#[derive(Serialize, Deserialize, Default)]
710pub(crate) struct CachedSyncInfo {
711 pub(crate) offset: u64,
712 pub(crate) last_synced_final_token_payment_id: Option<String>,
713}
714
715#[derive(Serialize, Deserialize, Default)]
716pub(crate) struct CachedTx {
717 pub(crate) raw_tx: String,
718}
719
720#[derive(Serialize, Deserialize, Default)]
721pub(crate) struct StaticDepositAddress {
722 pub(crate) address: String,
723}
724
725#[cfg(feature = "test-utils")]
726pub mod tests;