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