1#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2pub(crate) mod sqlite;
3
4use std::sync::Arc;
5
6use macros::async_trait;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::{DepositClaimError, DepositInfo, LnurlPayInfo, models::Payment};
11
12const ACCOUNT_INFO_KEY: &str = "account_info";
13const SYNC_OFFSET_KEY: &str = "sync_offset";
14const TX_CACHE_KEY: &str = "tx_cache";
15const STATIC_DEPOSIT_ADDRESS_CACHE_KEY: &str = "static_deposit_address";
16
17#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
18pub enum UpdateDepositPayload {
19 ClaimError {
20 error: DepositClaimError,
21 },
22 Refund {
23 refund_txid: String,
24 refund_tx: String,
25 },
26}
27
28#[derive(Debug, Error, Clone)]
30#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
31pub enum StorageError {
32 #[error("Underline implementation error: {0}")]
33 Implementation(String),
34
35 #[error("Failed to initialize database: {0}")]
37 InitializationError(String),
38
39 #[error("Failed to serialize/deserialize data: {0}")]
40 Serialization(String),
41}
42
43impl From<serde_json::Error> for StorageError {
44 fn from(e: serde_json::Error) -> Self {
45 StorageError::Serialization(e.to_string())
46 }
47}
48
49#[derive(Clone)]
51#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
52pub struct PaymentMetadata {
53 pub lnurl_pay_info: Option<LnurlPayInfo>,
54}
55
56#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
58#[async_trait]
59pub trait Storage: Send + Sync {
60 async fn get_cached_item(&self, key: String) -> Result<Option<String>, StorageError>;
61 async fn set_cached_item(&self, key: String, value: String) -> Result<(), StorageError>;
62 async fn list_payments(
73 &self,
74 offset: Option<u32>,
75 limit: Option<u32>,
76 ) -> Result<Vec<Payment>, StorageError>;
77
78 async fn insert_payment(&self, payment: Payment) -> Result<(), StorageError>;
88
89 async fn set_payment_metadata(
100 &self,
101 payment_id: String,
102 metadata: PaymentMetadata,
103 ) -> Result<(), StorageError>;
104
105 async fn get_payment_by_id(&self, id: String) -> Result<Payment, StorageError>;
114
115 async fn add_deposit(
126 &self,
127 txid: String,
128 vout: u32,
129 amount_sats: u64,
130 ) -> Result<(), StorageError>;
131
132 async fn delete_deposit(&self, txid: String, vout: u32) -> Result<(), StorageError>;
142
143 async fn list_deposits(&self) -> Result<Vec<DepositInfo>, StorageError>;
148
149 async fn update_deposit(
160 &self,
161 txid: String,
162 vout: u32,
163 payload: UpdateDepositPayload,
164 ) -> Result<(), StorageError>;
165}
166
167pub(crate) struct ObjectCacheRepository {
168 storage: Arc<dyn Storage>,
169}
170
171impl ObjectCacheRepository {
172 pub(crate) fn new(storage: Arc<dyn Storage>) -> Self {
173 ObjectCacheRepository { storage }
174 }
175
176 pub(crate) async fn save_account_info(
177 &self,
178 value: &CachedAccountInfo,
179 ) -> Result<(), StorageError> {
180 self.storage
181 .set_cached_item(ACCOUNT_INFO_KEY.to_string(), serde_json::to_string(value)?)
182 .await?;
183 Ok(())
184 }
185
186 pub(crate) async fn fetch_account_info(
187 &self,
188 ) -> Result<Option<CachedAccountInfo>, StorageError> {
189 let value = self
190 .storage
191 .get_cached_item(ACCOUNT_INFO_KEY.to_string())
192 .await?;
193 match value {
194 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
195 None => Ok(None),
196 }
197 }
198
199 pub(crate) async fn save_sync_info(&self, value: &CachedSyncInfo) -> Result<(), StorageError> {
200 self.storage
201 .set_cached_item(SYNC_OFFSET_KEY.to_string(), serde_json::to_string(value)?)
202 .await?;
203 Ok(())
204 }
205
206 pub(crate) async fn fetch_sync_info(&self) -> Result<Option<CachedSyncInfo>, StorageError> {
207 let value = self
208 .storage
209 .get_cached_item(SYNC_OFFSET_KEY.to_string())
210 .await?;
211 match value {
212 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
213 None => Ok(None),
214 }
215 }
216
217 pub(crate) async fn save_tx(&self, txid: &str, value: &CachedTx) -> Result<(), StorageError> {
218 self.storage
219 .set_cached_item(
220 format!("{TX_CACHE_KEY}-{txid}"),
221 serde_json::to_string(value)?,
222 )
223 .await?;
224 Ok(())
225 }
226
227 pub(crate) async fn fetch_tx(&self, txid: &str) -> Result<Option<CachedTx>, StorageError> {
228 let value = self
229 .storage
230 .get_cached_item(format!("{TX_CACHE_KEY}-{txid}"))
231 .await?;
232 match value {
233 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
234 None => Ok(None),
235 }
236 }
237
238 pub(crate) async fn save_static_deposit_address(
239 &self,
240 value: &StaticDepositAddress,
241 ) -> Result<(), StorageError> {
242 self.storage
243 .set_cached_item(
244 STATIC_DEPOSIT_ADDRESS_CACHE_KEY.to_string(),
245 serde_json::to_string(value)?,
246 )
247 .await?;
248 Ok(())
249 }
250
251 pub(crate) async fn fetch_static_deposit_address(
252 &self,
253 ) -> Result<Option<StaticDepositAddress>, StorageError> {
254 let value = self
255 .storage
256 .get_cached_item(STATIC_DEPOSIT_ADDRESS_CACHE_KEY.to_string())
257 .await?;
258 match value {
259 Some(value) => Ok(Some(serde_json::from_str(&value)?)),
260 None => Ok(None),
261 }
262 }
263}
264
265#[derive(Serialize, Deserialize, Default)]
266pub(crate) struct CachedAccountInfo {
267 pub(crate) balance_sats: u64,
268}
269
270#[derive(Serialize, Deserialize, Default)]
271pub(crate) struct CachedSyncInfo {
272 pub(crate) offset: u64,
273}
274
275#[derive(Serialize, Deserialize, Default)]
276pub(crate) struct CachedTx {
277 pub(crate) raw_tx: String,
278}
279
280#[derive(Serialize, Deserialize, Default)]
281pub(crate) struct StaticDepositAddress {
282 pub(crate) address: String,
283}
284
285#[cfg(feature = "test-utils")]
286pub mod tests {
287 use crate::{
288 DepositClaimError, Payment, PaymentDetails, PaymentMethod, PaymentStatus, PaymentType,
289 Storage, UpdateDepositPayload,
290 };
291 use chrono::Utc;
292
293 pub async fn test_sqlite_storage(storage: Box<dyn Storage>) {
294 let payment = Payment {
296 id: "pmt123".to_string(),
297 payment_type: PaymentType::Send,
298 status: PaymentStatus::Completed,
299 amount: 100_000,
300 fees: 1000,
301 timestamp: Utc::now().timestamp().try_into().unwrap(),
302 method: PaymentMethod::Spark,
303 details: Some(PaymentDetails::Spark),
304 };
305
306 storage.insert_payment(payment.clone()).await.unwrap();
308
309 let payments = storage.list_payments(Some(0), Some(10)).await.unwrap();
311 assert_eq!(payments.len(), 1);
312 assert_eq!(payments[0].id, payment.id);
313 assert_eq!(payments[0].payment_type, payment.payment_type);
314 assert_eq!(payments[0].status, payment.status);
315 assert_eq!(payments[0].amount, payment.amount);
316 assert_eq!(payments[0].fees, payment.fees);
317 assert!(matches!(payments[0].details, Some(PaymentDetails::Spark)));
318
319 let retrieved_payment = storage.get_payment_by_id(payment.id.clone()).await.unwrap();
321 assert_eq!(retrieved_payment.id, payment.id);
322 assert_eq!(retrieved_payment.payment_type, payment.payment_type);
323 assert_eq!(retrieved_payment.status, payment.status);
324 assert_eq!(retrieved_payment.amount, payment.amount);
325 assert_eq!(retrieved_payment.fees, payment.fees);
326 assert!(matches!(
327 retrieved_payment.details,
328 Some(PaymentDetails::Spark)
329 ));
330 }
331
332 pub async fn test_unclaimed_deposits_crud(storage: Box<dyn Storage>) {
333 let deposits = storage.list_deposits().await.unwrap();
335 assert_eq!(deposits.len(), 0);
336
337 storage
339 .add_deposit("tx123".to_string(), 0, 50000)
340 .await
341 .unwrap();
342 let deposits = storage.list_deposits().await.unwrap();
343 assert_eq!(deposits.len(), 1);
344 assert_eq!(deposits[0].txid, "tx123");
345 assert_eq!(deposits[0].vout, 0);
346 assert_eq!(deposits[0].amount_sats, 50000);
347 assert!(deposits[0].claim_error.is_none());
348
349 storage
351 .add_deposit("tx456".to_string(), 1, 75000)
352 .await
353 .unwrap();
354 storage
355 .update_deposit(
356 "tx456".to_string(),
357 1,
358 UpdateDepositPayload::ClaimError {
359 error: DepositClaimError::Generic {
360 message: "Test error".to_string(),
361 },
362 },
363 )
364 .await
365 .unwrap();
366 let deposits = storage.list_deposits().await.unwrap();
367 assert_eq!(deposits.len(), 2);
368
369 let deposit2_found = deposits.iter().find(|d| d.txid == "tx456").unwrap();
371 assert_eq!(deposit2_found.vout, 1);
372 assert_eq!(deposit2_found.amount_sats, 75000);
373 assert!(deposit2_found.claim_error.is_some());
374
375 storage
377 .delete_deposit("tx123".to_string(), 0)
378 .await
379 .unwrap();
380 let deposits = storage.list_deposits().await.unwrap();
381 assert_eq!(deposits.len(), 1);
382 assert_eq!(deposits[0].txid, "tx456");
383
384 storage
386 .delete_deposit("tx456".to_string(), 1)
387 .await
388 .unwrap();
389 let deposits = storage.list_deposits().await.unwrap();
390 assert_eq!(deposits.len(), 0);
391 }
392
393 pub async fn test_deposit_refunds(storage: Box<dyn Storage>) {
394 storage
396 .add_deposit("test_tx_123".to_string(), 0, 100_000)
397 .await
398 .unwrap();
399 let deposits = storage.list_deposits().await.unwrap();
400 assert_eq!(deposits.len(), 1);
401 assert_eq!(deposits[0].txid, "test_tx_123");
402 assert_eq!(deposits[0].vout, 0);
403 assert_eq!(deposits[0].amount_sats, 100_000);
404 assert!(deposits[0].claim_error.is_none());
405
406 storage
408 .update_deposit(
409 "test_tx_123".to_string(),
410 0,
411 UpdateDepositPayload::Refund {
412 refund_txid: "refund_tx_id_456".to_string(),
413 refund_tx: "0200000001abcd1234...".to_string(),
414 },
415 )
416 .await
417 .unwrap();
418
419 let deposits = storage.list_deposits().await.unwrap();
421 assert_eq!(deposits.len(), 1);
422 assert_eq!(deposits[0].txid, "test_tx_123");
423 assert_eq!(deposits[0].vout, 0);
424 assert_eq!(deposits[0].amount_sats, 100_000);
425 assert!(deposits[0].claim_error.is_none());
426 assert_eq!(
427 deposits[0].refund_tx_id,
428 Some("refund_tx_id_456".to_string())
429 );
430 assert_eq!(
431 deposits[0].refund_tx,
432 Some("0200000001abcd1234...".to_string())
433 );
434 }
435}