1mod address;
2pub(crate) mod asset_metadata;
3mod backup;
4pub(crate) mod bolt12_offer;
5pub(crate) mod cache;
6pub(crate) mod chain;
7mod migrations;
8pub(crate) mod model;
9pub(crate) mod receive;
10pub(crate) mod send;
11pub(crate) mod sync;
12pub(crate) mod wallet_updates;
13
14use std::collections::{HashMap, HashSet};
15use std::ops::Not;
16use std::{path::PathBuf, str::FromStr};
17
18use crate::elements::AssetId;
19use crate::model::*;
20use crate::sync::model::RecordType;
21use crate::utils::{
22 self, from_optional_u64_to_row, from_row_to_optional_u64, from_row_to_u64, from_u64_to_row,
23};
24use anyhow::{anyhow, Result};
25use boltz_client::boltz::{ChainPair, ReversePair, SubmarinePair};
26use log::{error, warn};
27use lwk_wollet::WalletTx;
28use migrations::current_migrations;
29use model::{PaymentTxBalance, PaymentTxDetails};
30use rusqlite::backup::Backup;
31use rusqlite::{
32 params, params_from_iter, Connection, OptionalExtension, Row, ToSql, TransactionBehavior,
33};
34use rusqlite_migration::{Migrations, M};
35use tokio::sync::broadcast::{self, Sender};
36
37const DEFAULT_DB_FILENAME: &str = "storage.sql";
38
39pub struct Persister {
40 main_db_dir: PathBuf,
41 network: LiquidNetwork,
42 pub(crate) sync_trigger: Option<Sender<()>>,
43}
44
45fn get_where_clause_state_in(allowed_states: &[PaymentState]) -> String {
47 format!(
48 "state in ({})",
49 allowed_states
50 .iter()
51 .map(|t| format!("'{}'", *t as i8))
52 .collect::<Vec<_>>()
53 .join(", ")
54 )
55}
56
57fn where_clauses_to_string(where_clauses: Vec<String>) -> String {
58 let mut where_clause_str = String::new();
59 if !where_clauses.is_empty() {
60 where_clause_str = String::from("WHERE ");
61 where_clause_str.push_str(where_clauses.join(" AND ").as_str());
62 }
63 where_clause_str
64}
65
66impl Persister {
67 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
69 pub fn new_using_fs(
70 working_dir: &str,
71 network: LiquidNetwork,
72 sync_enabled: bool,
73 asset_metadata: Option<Vec<AssetMetadata>>,
74 ) -> Result<Self> {
75 let main_db_dir = PathBuf::from_str(working_dir)?;
76 if !main_db_dir.exists() {
77 std::fs::create_dir_all(&main_db_dir)?;
78 }
79 Self::new_inner(main_db_dir, network, sync_enabled, asset_metadata, None)
80 }
81
82 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
87 pub fn new_in_memory(
88 database_id: &str,
89 network: LiquidNetwork,
90 sync_enabled: bool,
91 asset_metadata: Option<Vec<AssetMetadata>>,
92 backup_bytes: Option<Vec<u8>>,
93 ) -> Result<Self> {
94 let main_db_dir = PathBuf::from_str(database_id)?;
95 let backup_con = backup_bytes
96 .map(|data| {
97 let size = data.len();
98 let cursor = std::io::Cursor::new(data);
99 let mut conn = Connection::open_in_memory()?;
100 conn.deserialize_read_exact(rusqlite::MAIN_DB, cursor, size, false)?;
101 Ok::<Connection, anyhow::Error>(conn)
102 })
103 .transpose()
104 .unwrap_or_else(|e| {
105 error!("Failed to deserialize backup data: {e} - proceeding without it");
106 None
107 });
108 Self::new_inner(
109 main_db_dir,
110 network,
111 sync_enabled,
112 asset_metadata,
113 backup_con,
114 )
115 }
116
117 fn new_inner(
118 main_db_dir: PathBuf,
119 network: LiquidNetwork,
120 sync_enabled: bool,
121 asset_metadata: Option<Vec<AssetMetadata>>,
122 backup_con: Option<Connection>,
123 ) -> Result<Self> {
124 let mut sync_trigger = None;
125 if sync_enabled {
126 let (events_notifier, _) = broadcast::channel::<()>(1);
127 sync_trigger = Some(events_notifier);
128 }
129
130 let persister = Persister {
131 main_db_dir,
132 network,
133 sync_trigger,
134 };
135
136 if let Some(backup_con) = backup_con {
137 if let Err(e) = (|| {
138 let mut dst_con = persister.get_connection()?;
139 let backup = Backup::new(&backup_con, &mut dst_con)?;
140 backup.step(-1)?;
141 Ok::<(), anyhow::Error>(())
142 })() {
143 error!("Failed to restore from backup: {e} - proceeding without it");
144 }
145 }
146
147 persister.init()?;
148 persister.replace_asset_metadata(asset_metadata)?;
149
150 Ok(persister)
151 }
152
153 fn get_db_path(&self) -> PathBuf {
154 self.main_db_dir.join(DEFAULT_DB_FILENAME)
155 }
156
157 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
162 pub fn clear_in_memory_db(&self) -> Result<()> {
163 sqlite_wasm_rs::MemVfsUtil::<sqlite_wasm_rs::WasmOsCallback>::new().delete_db(
164 self.get_db_path()
165 .to_str()
166 .ok_or(anyhow!("Failed to get db path str"))?,
167 );
168 Ok(())
169 }
170
171 pub(crate) fn get_connection(&self) -> Result<Connection> {
172 Ok(Connection::open(self.get_db_path())?)
173 }
174
175 pub fn init(&self) -> Result<()> {
176 self.migrate_main_db()?;
177 Ok(())
178 }
179
180 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
181 pub fn serialize(&self) -> Result<Vec<u8>> {
182 let con = self.get_connection()?;
183 let db_bytes = con.serialize(rusqlite::MAIN_DB)?;
184 Ok(db_bytes.to_vec())
185 }
186
187 #[cfg(any(test, feature = "test-utils"))]
188 pub(crate) fn get_database_dir(&self) -> &PathBuf {
189 &self.main_db_dir
190 }
191
192 fn migrate_main_db(&self) -> Result<()> {
193 let migrations = Migrations::new(
194 current_migrations(self.network)
195 .into_iter()
196 .map(M::up)
197 .collect(),
198 );
199 let mut conn = self.get_connection()?;
200 migrations.to_latest(&mut conn)?;
201 Ok(())
202 }
203
204 pub(crate) fn fetch_swap_by_id(&self, id: &str) -> Result<Swap> {
205 match self.fetch_send_swap_by_id(id) {
206 Ok(Some(send_swap)) => Ok(Swap::Send(send_swap)),
207 _ => match self.fetch_receive_swap_by_id(id) {
208 Ok(Some(receive_swap)) => Ok(Swap::Receive(receive_swap)),
209 _ => match self.fetch_chain_swap_by_id(id) {
210 Ok(Some(chain_swap)) => Ok(Swap::Chain(chain_swap)),
211 _ => Err(anyhow!("Could not find Swap {id}")),
212 },
213 },
214 }
215 }
216
217 pub(crate) fn insert_or_update_payment_with_wallet_tx(&self, tx: &WalletTx) -> Result<()> {
218 let tx_id = tx.txid.to_string();
219 let is_tx_confirmed = tx.height.is_some();
220
221 let mut tx_balances: HashMap<AssetId, i64> = HashMap::new();
222 for input in &tx.inputs {
223 let Some(input) = input else {
224 continue;
225 };
226 let value = input.unblinded.value as i64;
227 tx_balances
228 .entry(input.unblinded.asset)
229 .and_modify(|v| *v -= value)
230 .or_insert_with(|| -value);
231 }
232 for output in &tx.outputs {
233 let Some(output) = output else {
234 continue;
235 };
236 let value = output.unblinded.value as i64;
237 tx_balances
238 .entry(output.unblinded.asset)
239 .and_modify(|v| *v += value)
240 .or_insert_with(|| value);
241 }
242
243 if tx_balances.is_empty() {
244 warn!("Attempted to persist a payment with no balance: tx_id {tx_id} balances {tx_balances:?}");
245 return Ok(());
246 }
247
248 let lbtc_asset_id = utils::lbtc_asset_id(self.network);
249 let payment_balances: Vec<PaymentTxBalance> = tx_balances
250 .into_iter()
251 .map(|(asset_id, balance)| {
252 let payment_type = match balance >= 0 {
253 true => PaymentType::Receive,
254 false => PaymentType::Send,
255 };
256 let mut amount = balance.unsigned_abs();
257 if payment_type == PaymentType::Send && asset_id == lbtc_asset_id {
258 amount = amount.saturating_sub(tx.fee);
259 }
260 let asset_id = asset_id.to_string();
261 PaymentTxBalance {
262 payment_type,
263 asset_id,
264 amount,
265 }
266 })
267 .collect();
268
269 let maybe_address = tx
270 .outputs
271 .iter()
272 .find(|output| output.is_some())
273 .and_then(|output| {
274 output.clone().and_then(|o| {
275 o.address.blinding_pubkey.map(|blinding_pubkey| {
276 o.address.to_confidential(blinding_pubkey).to_string()
277 })
278 })
279 });
280 let unblinding_data = tx
281 .unblinded_url("")
282 .replace(&format!("tx/{tx_id}#blinded="), "");
283 self.insert_or_update_payment(
284 PaymentTxData {
285 tx_id: tx_id.clone(),
286 timestamp: tx.timestamp,
287 fees_sat: tx.fee,
288 is_confirmed: is_tx_confirmed,
289 unblinding_data: Some(unblinding_data),
290 },
291 &payment_balances,
292 maybe_address.map(|destination| PaymentTxDetails {
293 tx_id,
294 destination,
295 ..Default::default()
296 }),
297 true,
298 )
299 }
300
301 pub(crate) fn list_unconfirmed_payment_txs_data(&self) -> Result<Vec<PaymentTxData>> {
302 let con = self.get_connection()?;
303 let mut stmt = con.prepare(
304 "SELECT tx_id,
305 timestamp,
306 fees_sat,
307 is_confirmed,
308 unblinding_data
309 FROM payment_tx_data
310 WHERE is_confirmed = 0",
311 )?;
312 let payments: Vec<PaymentTxData> = stmt
313 .query_map([], |row| {
314 Ok(PaymentTxData {
315 tx_id: row.get(0)?,
316 timestamp: row.get(1)?,
317 fees_sat: from_row_to_u64(row, 2)?,
318 is_confirmed: row.get(3)?,
319 unblinding_data: row.get(4)?,
320 })
321 })?
322 .map(|i| i.unwrap())
323 .collect();
324 Ok(payments)
325 }
326
327 fn insert_or_update_payment_balance(
328 con: &Connection,
329 tx_id: &str,
330 balance: &PaymentTxBalance,
331 ) -> Result<()> {
332 con.execute(
333 "INSERT OR REPLACE INTO payment_balance (
334 tx_id,
335 asset_id,
336 payment_type,
337 amount
338 )
339 VALUES (?, ?, ?, ?)",
340 (
341 tx_id,
342 &balance.asset_id,
343 balance.payment_type,
344 from_u64_to_row(balance.amount)?,
345 ),
346 )?;
347 Ok(())
348 }
349
350 pub(crate) fn insert_or_update_payment(
351 &self,
352 ptx: PaymentTxData,
353 balances: &[PaymentTxBalance],
354 payment_tx_details: Option<PaymentTxDetails>,
355 from_wallet_tx_data: bool,
356 ) -> Result<()> {
357 let mut con = self.get_connection()?;
358 let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?;
359 tx.execute(
360 "INSERT INTO payment_tx_data (
361 tx_id,
362 timestamp,
363 fees_sat,
364 is_confirmed,
365 unblinding_data
366 )
367 VALUES (?, ?, ?, ?, ?)
368 ON CONFLICT (tx_id)
369 DO UPDATE SET timestamp = CASE WHEN excluded.is_confirmed = 1 THEN excluded.timestamp ELSE timestamp END,
370 fees_sat = excluded.fees_sat,
371 is_confirmed = excluded.is_confirmed,
372 unblinding_data = excluded.unblinding_data
373 ",
374 (
375 &ptx.tx_id,
376 ptx.timestamp.or(Some(utils::now())),
377 from_u64_to_row(ptx.fees_sat)?,
378 ptx.is_confirmed,
379 ptx.unblinding_data,
380 ),
381 )?;
382
383 for balance in balances {
384 Self::insert_or_update_payment_balance(&tx, &ptx.tx_id, balance)?;
385 }
386
387 let mut trigger_sync = false;
388 if let Some(ref payment_tx_details) = payment_tx_details {
389 Self::insert_or_update_payment_details_inner(
393 &tx,
394 payment_tx_details,
395 from_wallet_tx_data,
396 )?;
397 if !from_wallet_tx_data {
398 self.commit_outgoing(
399 &tx,
400 &payment_tx_details.tx_id,
401 RecordType::PaymentDetails,
402 None,
403 )?;
404 trigger_sync = true;
405 }
406 }
407
408 tx.commit()?;
409 if trigger_sync {
410 self.trigger_sync();
411 }
412
413 Ok(())
414 }
415
416 pub(crate) fn get_payment_tx_timestamp(&self, tx_id: &str) -> Result<Option<u32>> {
417 let con = self.get_connection()?;
418 let res = con.query_row(
419 "SELECT timestamp FROM payment_tx_data WHERE tx_id = ?",
420 [tx_id],
421 |row| row.get(0),
422 );
423 Ok(res.ok().flatten())
424 }
425
426 pub(crate) fn delete_payment_tx_data(&self, tx_id: &str) -> Result<()> {
427 let con = self.get_connection()?;
428
429 con.execute("DELETE FROM payment_tx_data WHERE tx_id = ?", [tx_id])?;
430
431 Ok(())
432 }
433
434 fn insert_or_update_payment_details_inner(
435 con: &Connection,
436 payment_tx_details: &PaymentTxDetails,
437 skip_destination_update: bool,
438 ) -> Result<()> {
439 let destination_update = if skip_destination_update.not() {
440 "destination = excluded.destination,"
441 } else {
442 Default::default()
443 };
444 con.execute(
445 &format!(
446 "INSERT INTO payment_details (
447 tx_id,
448 destination,
449 description,
450 lnurl_info_json,
451 bip353_address,
452 payer_note,
453 asset_fees,
454 settled_at
455 )
456 VALUES (?, ?, ?, ?, ?, ?, ?, ?)
457 ON CONFLICT (tx_id)
458 DO UPDATE SET
459 {destination_update}
460 description = COALESCE(excluded.description, description),
461 lnurl_info_json = COALESCE(excluded.lnurl_info_json, lnurl_info_json),
462 bip353_address = COALESCE(excluded.bip353_address, bip353_address),
463 payer_note = COALESCE(excluded.payer_note, payer_note),
464 asset_fees = COALESCE(excluded.asset_fees, asset_fees),
465 settled_at = COALESCE(excluded.settled_at, settled_at)
466 "
467 ),
468 (
469 &payment_tx_details.tx_id,
470 &payment_tx_details.destination,
471 &payment_tx_details.description,
472 payment_tx_details
473 .lnurl_info
474 .as_ref()
475 .map(|info| serde_json::to_string(&info).ok()),
476 &payment_tx_details.bip353_address,
477 &payment_tx_details.payer_note,
478 from_optional_u64_to_row(&payment_tx_details.asset_fees)?,
479 &payment_tx_details.settled_at,
480 ),
481 )?;
482 Ok(())
483 }
484
485 pub(crate) fn insert_or_update_payment_details(
486 &self,
487 payment_tx_details: PaymentTxDetails,
488 ) -> Result<()> {
489 let mut con = self.get_connection()?;
490 let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?;
491
492 Self::insert_or_update_payment_details_inner(&tx, &payment_tx_details, false)?;
493 self.commit_outgoing(
494 &tx,
495 &payment_tx_details.tx_id,
496 RecordType::PaymentDetails,
497 None,
498 )?;
499 tx.commit()?;
500 self.trigger_sync();
501
502 Ok(())
503 }
504
505 pub(crate) fn get_payment_details(&self, tx_id: &str) -> Result<Option<PaymentTxDetails>> {
506 let con = self.get_connection()?;
507 let mut stmt = con.prepare(
508 "SELECT destination, description, lnurl_info_json, bip353_address, payer_note, asset_fees, settled_at
509 FROM payment_details
510 WHERE tx_id = ?",
511 )?;
512 let res = stmt.query_row([tx_id], |row| {
513 let destination = row.get(0)?;
514 let description = row.get(1)?;
515 let maybe_lnurl_info_json: Option<String> = row.get(2)?;
516 let maybe_bip353_address = row.get(3)?;
517 let maybe_payer_note = row.get(4)?;
518 let maybe_asset_fees = from_row_to_optional_u64(row, 5)?;
519 let maybe_settled_at = row.get(6)?;
520 Ok(PaymentTxDetails {
521 tx_id: tx_id.to_string(),
522 destination,
523 description,
524 lnurl_info: maybe_lnurl_info_json
525 .and_then(|info| serde_json::from_str::<LnUrlInfo>(&info).ok()),
526 bip353_address: maybe_bip353_address,
527 payer_note: maybe_payer_note,
528 asset_fees: maybe_asset_fees,
529 settled_at: maybe_settled_at,
530 })
531 });
532 Ok(res.ok())
533 }
534
535 pub(crate) fn list_ongoing_swaps(&self) -> Result<Vec<Swap>> {
536 let ongoing_send_swaps: Vec<Swap> = self
537 .list_ongoing_send_swaps()?
538 .into_iter()
539 .map(Swap::Send)
540 .collect();
541 let ongoing_receive_swaps: Vec<Swap> = self
542 .list_ongoing_receive_swaps()?
543 .into_iter()
544 .map(Swap::Receive)
545 .collect();
546 let ongoing_chain_swaps: Vec<Swap> = self
547 .list_ongoing_chain_swaps()?
548 .into_iter()
549 .map(Swap::Chain)
550 .collect();
551 Ok([
552 ongoing_send_swaps,
553 ongoing_receive_swaps,
554 ongoing_chain_swaps,
555 ]
556 .concat())
557 }
558
559 fn select_payment_query(
560 &self,
561 where_clause: Option<&str>,
562 offset: Option<u32>,
563 limit: Option<u32>,
564 sort_ascending: Option<bool>,
565 include_all_states: Option<bool>,
566 ) -> String {
567 let (where_receive_swap_clause, where_chain_swap_clause) = if include_all_states
568 .unwrap_or_default()
569 {
570 ("true", "true")
571 } else {
572 (
573 "COALESCE(claim_tx_id, lockup_tx_id, mrh_tx_id) IS NOT NULL AND state NOT IN (0, 3, 4)",
575 "COALESCE(user_lockup_tx_id, claim_tx_id) IS NOT NULL AND state NOT IN (0, 4)",
577 )
578 };
579
580 format!(
581 "
582 SELECT
583 ptx.tx_id,
584 ptx.timestamp,
585 ptx.fees_sat,
586 ptx.is_confirmed,
587 ptx.unblinding_data,
588 pb.amount,
589 pb.asset_id,
590 pb.payment_type,
591 rs.id,
592 rs.created_at,
593 rs.timeout_block_height,
594 rs.invoice,
595 rs.bolt12_offer,
596 rs.payment_hash,
597 rs.destination_pubkey,
598 rs.description,
599 rs.payer_note,
600 rs.preimage,
601 rs.payer_amount_sat,
602 rs.receiver_amount_sat,
603 rs.state,
604 rs.pair_fees_json,
605 rs.claim_tx_id,
606 ss.id,
607 ss.created_at,
608 ss.timeout_block_height,
609 ss.invoice,
610 ss.bolt12_offer,
611 ss.payment_hash,
612 ss.destination_pubkey,
613 ss.description,
614 ss.preimage,
615 ss.refund_tx_id,
616 ss.payer_amount_sat,
617 ss.receiver_amount_sat,
618 ss.state,
619 ss.pair_fees_json,
620 cs.id,
621 cs.created_at,
622 cs.timeout_block_height,
623 cs.claim_timeout_block_height,
624 cs.direction,
625 cs.preimage,
626 cs.description,
627 cs.refund_tx_id,
628 cs.payer_amount_sat,
629 cs.receiver_amount_sat,
630 cs.claim_address,
631 cs.lockup_address,
632 cs.state,
633 cs.pair_fees_json,
634 cs.actual_payer_amount_sat,
635 cs.accepted_receiver_amount_sat,
636 cs.auto_accepted_fees,
637 cs.user_lockup_tx_id,
638 cs.claim_tx_id,
639 rb.amount,
640 pd.destination,
641 pd.description,
642 pd.lnurl_info_json,
643 pd.bip353_address,
644 pd.payer_note,
645 pd.asset_fees,
646 pd.settled_at,
647 am.name,
648 am.ticker,
649 am.precision
650 FROM payment_tx_data AS ptx -- Payment tx (each tx results in a Payment)
651 LEFT JOIN payment_balance AS pb
652 ON pb.tx_id = ptx.tx_id -- Payment tx balances, split by asset
653 FULL JOIN (
654 SELECT * FROM receive_swaps WHERE {}
655 ) rs -- Receive Swap data
656 ON ptx.tx_id in (rs.claim_tx_id, rs.mrh_tx_id)
657 FULL JOIN (
658 SELECT * FROM chain_swaps WHERE {}
659 ) cs -- Chain Swap data
660 ON ptx.tx_id in (cs.user_lockup_tx_id, cs.claim_tx_id)
661 LEFT JOIN send_swaps AS ss -- Send Swap data
662 ON ptx.tx_id = ss.lockup_tx_id
663 LEFT JOIN payment_balance AS rb -- Refund tx balance
664 ON rb.tx_id in (ss.refund_tx_id, cs.refund_tx_id)
665 LEFT JOIN payment_details AS pd -- Payment details
666 ON pd.tx_id = ptx.tx_id
667 LEFT JOIN asset_metadata AS am -- Asset metadata
668 ON am.asset_id = pb.asset_id
669 WHERE
670 (ptx.tx_id IS NULL -- Filter out refund txs from Chain/Send Swaps
671 OR ptx.tx_id NOT IN (SELECT refund_tx_id FROM send_swaps WHERE refund_tx_id NOT NULL)
672 AND ptx.tx_id NOT IN (SELECT refund_tx_id FROM chain_swaps WHERE refund_tx_id NOT NULL))
673 AND {}
674 ORDER BY -- Order by swap creation time or tx timestamp (in case of direct tx)
675 COALESCE(rs.created_at, ss.created_at, cs.created_at, ptx.timestamp) {}
676 LIMIT {}
677 OFFSET {}
678 ",
679 where_receive_swap_clause,
680 where_chain_swap_clause,
681 where_clause.unwrap_or("true"),
682 match sort_ascending.unwrap_or(false) {
683 true => "ASC",
684 false => "DESC",
685 },
686 limit.unwrap_or(u32::MAX),
687 offset.unwrap_or(0),
688 )
689 }
690
691 fn sql_row_to_payment(&self, row: &Row) -> Result<Payment, rusqlite::Error> {
692 let maybe_tx_tx_id: Result<String, rusqlite::Error> = row.get(0);
693 let tx_with_balance = match maybe_tx_tx_id {
694 Ok(ref tx_id) => Some((
695 PaymentTxData {
696 tx_id: tx_id.to_string(),
697 timestamp: row.get(1)?,
698 fees_sat: from_row_to_u64(row, 2)?,
699 is_confirmed: row.get(3)?,
700 unblinding_data: row.get(4)?,
701 },
702 PaymentTxBalance {
703 amount: from_row_to_u64(row, 5)?,
704 asset_id: row.get(6)?,
705 payment_type: row.get(7)?,
706 },
707 )),
708 _ => None,
709 };
710
711 let maybe_receive_swap_id: Option<String> = row.get(8)?;
712 let maybe_receive_swap_created_at: Option<u32> = row.get(9)?;
713 let maybe_receive_swap_timeout_block_height: Option<u32> = row.get(10)?;
714 let maybe_receive_swap_invoice: Option<String> = row.get(11)?;
715 let maybe_receive_swap_bolt12_offer: Option<String> = row.get(12)?;
716 let maybe_receive_swap_payment_hash: Option<String> = row.get(13)?;
717 let maybe_receive_swap_destination_pubkey: Option<String> = row.get(14)?;
718 let maybe_receive_swap_description: Option<String> = row.get(15)?;
719 let maybe_receive_swap_payer_note: Option<String> = row.get(16)?;
720 let maybe_receive_swap_preimage: Option<String> = row.get(17)?;
721 let maybe_receive_swap_payer_amount_sat = from_row_to_optional_u64(row, 18)?;
722 let maybe_receive_swap_receiver_amount_sat = from_row_to_optional_u64(row, 19)?;
723 let maybe_receive_swap_receiver_state: Option<PaymentState> = row.get(20)?;
724 let maybe_receive_swap_pair_fees_json: Option<String> = row.get(21)?;
725 let maybe_receive_swap_pair_fees: Option<ReversePair> =
726 maybe_receive_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok());
727 let maybe_receive_swap_claim_tx_id: Option<String> = row.get(22)?;
728
729 let maybe_send_swap_id: Option<String> = row.get(23)?;
730 let maybe_send_swap_created_at: Option<u32> = row.get(24)?;
731 let maybe_send_swap_timeout_block_height: Option<u32> = row.get(25)?;
732 let maybe_send_swap_invoice: Option<String> = row.get(26)?;
733 let maybe_send_swap_bolt12_offer: Option<String> = row.get(27)?;
734 let maybe_send_swap_payment_hash: Option<String> = row.get(28)?;
735 let maybe_send_swap_destination_pubkey: Option<String> = row.get(29)?;
736 let maybe_send_swap_description: Option<String> = row.get(30)?;
737 let maybe_send_swap_preimage: Option<String> = row.get(31)?;
738 let maybe_send_swap_refund_tx_id: Option<String> = row.get(32)?;
739 let maybe_send_swap_payer_amount_sat = from_row_to_optional_u64(row, 33)?;
740 let maybe_send_swap_receiver_amount_sat = from_row_to_optional_u64(row, 34)?;
741 let maybe_send_swap_state: Option<PaymentState> = row.get(35)?;
742 let maybe_send_swap_pair_fees_json: Option<String> = row.get(36)?;
743 let maybe_send_swap_pair_fees: Option<SubmarinePair> =
744 maybe_send_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok());
745
746 let maybe_chain_swap_id: Option<String> = row.get(37)?;
747 let maybe_chain_swap_created_at: Option<u32> = row.get(38)?;
748 let maybe_chain_swap_timeout_block_height: Option<u32> = row.get(39)?;
749 let maybe_chain_swap_claim_timeout_block_height: Option<u32> = row.get(40)?;
750 let maybe_chain_swap_direction: Option<Direction> = row.get(41)?;
751 let maybe_chain_swap_preimage: Option<String> = row.get(42)?;
752 let maybe_chain_swap_description: Option<String> = row.get(43)?;
753 let maybe_chain_swap_refund_tx_id: Option<String> = row.get(44)?;
754 let maybe_chain_swap_payer_amount_sat = from_row_to_optional_u64(row, 45)?;
755 let maybe_chain_swap_receiver_amount_sat = from_row_to_optional_u64(row, 46)?;
756 let maybe_chain_swap_claim_address: Option<String> = row.get(47)?;
757 let maybe_chain_swap_lockup_address: Option<String> = row.get(48)?;
758 let maybe_chain_swap_state: Option<PaymentState> = row.get(49)?;
759 let maybe_chain_swap_pair_fees_json: Option<String> = row.get(50)?;
760 let maybe_chain_swap_pair_fees: Option<ChainPair> =
761 maybe_chain_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok());
762 let maybe_chain_swap_actual_payer_amount_sat = from_row_to_optional_u64(row, 51)?;
763 let maybe_chain_swap_accepted_receiver_amount_sat = from_row_to_optional_u64(row, 52)?;
764 let maybe_chain_swap_auto_accepted_fees: Option<bool> = row.get(53)?;
765 let maybe_chain_swap_user_lockup_tx_id: Option<String> = row.get(54)?;
766 let maybe_chain_swap_claim_tx_id: Option<String> = row.get(55)?;
767
768 let maybe_swap_refund_tx_amount_sat = from_row_to_optional_u64(row, 56)?;
769
770 let maybe_payment_details_destination: Option<String> = row.get(57)?;
771 let maybe_payment_details_description: Option<String> = row.get(58)?;
772 let maybe_payment_details_lnurl_info_json: Option<String> = row.get(59)?;
773 let maybe_payment_details_lnurl_info: Option<LnUrlInfo> =
774 maybe_payment_details_lnurl_info_json.and_then(|info| serde_json::from_str(&info).ok());
775 let maybe_payment_details_bip353_address: Option<String> = row.get(60)?;
776 let maybe_payment_details_payer_note: Option<String> = row.get(61)?;
777 let maybe_payment_details_asset_fees = from_row_to_optional_u64(row, 62)?;
778 let maybe_payment_details_settled_at = row.get(63)?;
779
780 let maybe_asset_metadata_name: Option<String> = row.get(64)?;
781 let maybe_asset_metadata_ticker: Option<String> = row.get(65)?;
782 let maybe_asset_metadata_precision: Option<u8> = row.get(66)?;
783
784 let bitcoin_address = match maybe_chain_swap_direction {
785 Some(Direction::Incoming) => maybe_chain_swap_lockup_address,
786 Some(Direction::Outgoing) => maybe_chain_swap_claim_address,
787 None => None,
788 };
789
790 let (swap, payment_type) = match maybe_receive_swap_id {
791 Some(receive_swap_id) => {
792 let payer_amount_sat = maybe_receive_swap_payer_amount_sat.unwrap_or(0);
793
794 (
795 Some(PaymentSwapData {
796 swap_id: receive_swap_id,
797 swap_type: PaymentSwapType::Receive,
798 created_at: maybe_receive_swap_created_at.unwrap_or(utils::now()),
799 expiration_blockheight: maybe_receive_swap_timeout_block_height
800 .unwrap_or(0),
801 claim_expiration_blockheight: None,
802 preimage: maybe_receive_swap_preimage,
803 invoice: maybe_receive_swap_invoice.clone(),
804 bolt12_offer: maybe_receive_swap_bolt12_offer,
805 payment_hash: maybe_receive_swap_payment_hash,
806 destination_pubkey: maybe_receive_swap_destination_pubkey,
807 description: maybe_receive_swap_description.unwrap_or_else(|| {
808 maybe_receive_swap_invoice
809 .and_then(|invoice| {
810 utils::get_invoice_description(&invoice).ok().flatten()
811 })
812 .unwrap_or("Lightning payment".to_string())
813 }),
814 payer_note: maybe_receive_swap_payer_note,
815 payer_amount_sat,
816 receiver_amount_sat: maybe_receive_swap_receiver_amount_sat.unwrap_or(0),
817 swapper_fees_sat: maybe_receive_swap_pair_fees
818 .map(|pair| pair.fees.boltz(payer_amount_sat))
819 .unwrap_or(0),
820 refund_tx_id: None,
821 refund_tx_amount_sat: None,
822 bitcoin_address: None,
823 status: maybe_receive_swap_receiver_state.unwrap_or(PaymentState::Created),
824 }),
825 PaymentType::Receive,
826 )
827 }
828 None => match maybe_send_swap_id {
829 Some(send_swap_id) => {
830 let receiver_amount_sat = maybe_send_swap_receiver_amount_sat.unwrap_or(0);
831 (
832 Some(PaymentSwapData {
833 swap_id: send_swap_id,
834 swap_type: PaymentSwapType::Send,
835 created_at: maybe_send_swap_created_at.unwrap_or(utils::now()),
836 expiration_blockheight: maybe_send_swap_timeout_block_height
837 .unwrap_or(0),
838 claim_expiration_blockheight: None,
839 preimage: maybe_send_swap_preimage,
840 invoice: maybe_send_swap_invoice,
841 bolt12_offer: maybe_send_swap_bolt12_offer,
842 payment_hash: maybe_send_swap_payment_hash,
843 destination_pubkey: maybe_send_swap_destination_pubkey,
844 description: maybe_send_swap_description
845 .unwrap_or("Lightning payment".to_string()),
846 payer_note: None,
847 payer_amount_sat: maybe_send_swap_payer_amount_sat.unwrap_or(0),
848 receiver_amount_sat,
849 swapper_fees_sat: maybe_send_swap_pair_fees
850 .map(|pair| pair.fees.boltz(receiver_amount_sat))
851 .unwrap_or(0),
852 refund_tx_id: maybe_send_swap_refund_tx_id,
853 refund_tx_amount_sat: maybe_swap_refund_tx_amount_sat,
854 bitcoin_address: None,
855 status: maybe_send_swap_state.unwrap_or(PaymentState::Created),
856 }),
857 PaymentType::Send,
858 )
859 }
860 None => match maybe_chain_swap_id {
861 Some(chain_swap_id) => {
862 let (payer_amount_sat, receiver_amount_sat) = match (
863 maybe_chain_swap_actual_payer_amount_sat,
864 maybe_chain_swap_payer_amount_sat,
865 ) {
866 (Some(actual_payer_amount_sat), Some(0)) => {
869 (actual_payer_amount_sat, actual_payer_amount_sat)
870 }
871 _ => (
873 maybe_chain_swap_payer_amount_sat.unwrap_or(0),
874 maybe_chain_swap_receiver_amount_sat.unwrap_or(0),
875 ),
876 };
877 let receiver_amount_sat =
878 match maybe_chain_swap_accepted_receiver_amount_sat {
879 Some(accepted_receiver_amount_sat) => accepted_receiver_amount_sat,
881 None => receiver_amount_sat,
882 };
883 let swapper_fees_sat = maybe_chain_swap_pair_fees
884 .map(|pair| pair.fees.percentage)
885 .map(|fr| ((fr / 100.0) * payer_amount_sat as f64).ceil() as u64)
886 .unwrap_or(0);
887
888 (
889 Some(PaymentSwapData {
890 swap_id: chain_swap_id,
891 swap_type: PaymentSwapType::Chain,
892 created_at: maybe_chain_swap_created_at.unwrap_or(utils::now()),
893 expiration_blockheight: maybe_chain_swap_timeout_block_height
894 .unwrap_or(0),
895 claim_expiration_blockheight:
896 maybe_chain_swap_claim_timeout_block_height,
897 preimage: maybe_chain_swap_preimage,
898 invoice: None,
899 bolt12_offer: None, payment_hash: None,
901 destination_pubkey: None,
902 description: maybe_chain_swap_description
903 .unwrap_or("Bitcoin transfer".to_string()),
904 payer_note: None,
905 payer_amount_sat,
906 receiver_amount_sat,
907 swapper_fees_sat,
908 refund_tx_id: maybe_chain_swap_refund_tx_id,
909 refund_tx_amount_sat: maybe_swap_refund_tx_amount_sat,
910 bitcoin_address: bitcoin_address.clone(),
911 status: maybe_chain_swap_state.unwrap_or(PaymentState::Created),
912 }),
913 maybe_chain_swap_direction
914 .unwrap_or(Direction::Outgoing)
915 .into(),
916 )
917 }
918 None => (None, PaymentType::Send),
919 },
920 },
921 };
922
923 let maybe_claim_tx_id = maybe_receive_swap_claim_tx_id.or(maybe_chain_swap_claim_tx_id);
924 let description = swap.as_ref().map(|s| s.description.clone());
925 let payment_details = match swap.clone() {
926 Some(
927 PaymentSwapData {
928 swap_type: PaymentSwapType::Receive,
929 swap_id,
930 invoice,
931 bolt12_offer,
932 payment_hash,
933 destination_pubkey,
934 payer_note,
935 refund_tx_id,
936 preimage,
937 refund_tx_amount_sat,
938 expiration_blockheight,
939 ..
940 }
941 | PaymentSwapData {
942 swap_type: PaymentSwapType::Send,
943 swap_id,
944 invoice,
945 bolt12_offer,
946 payment_hash,
947 destination_pubkey,
948 payer_note,
949 preimage,
950 refund_tx_id,
951 refund_tx_amount_sat,
952 expiration_blockheight,
953 ..
954 },
955 ) => PaymentDetails::Lightning {
956 swap_id,
957 preimage,
958 invoice: invoice.clone(),
959 bolt12_offer: bolt12_offer.clone(),
960 payment_hash,
961 destination_pubkey: destination_pubkey.or_else(|| {
962 invoice.and_then(|invoice| {
963 utils::get_invoice_destination_pubkey(&invoice, bolt12_offer.is_some()).ok()
964 })
965 }),
966 lnurl_info: maybe_payment_details_lnurl_info,
967 bip353_address: maybe_payment_details_bip353_address,
968 payer_note: payer_note.or(maybe_payment_details_payer_note),
969 claim_tx_id: maybe_claim_tx_id,
970 refund_tx_id,
971 refund_tx_amount_sat,
972 description: maybe_payment_details_description
973 .unwrap_or(description.unwrap_or("Lightning transfer".to_string())),
974 liquid_expiration_blockheight: expiration_blockheight,
975 settled_at: maybe_payment_details_settled_at,
976 },
977 Some(PaymentSwapData {
978 swap_type: PaymentSwapType::Chain,
979 swap_id,
980 refund_tx_id,
981 refund_tx_amount_sat,
982 expiration_blockheight,
983 claim_expiration_blockheight,
984 ..
985 }) => {
986 let (bitcoin_expiration_blockheight, liquid_expiration_blockheight) =
987 match maybe_chain_swap_direction {
988 Some(Direction::Incoming) => (
989 expiration_blockheight,
990 claim_expiration_blockheight.unwrap_or_default(),
991 ),
992 Some(Direction::Outgoing) | None => (
993 claim_expiration_blockheight.unwrap_or_default(),
994 expiration_blockheight,
995 ),
996 };
997 let auto_accepted_fees = maybe_chain_swap_auto_accepted_fees.unwrap_or(false);
998
999 PaymentDetails::Bitcoin {
1000 swap_id,
1001 bitcoin_address: bitcoin_address.unwrap_or_default(),
1002 lockup_tx_id: maybe_chain_swap_user_lockup_tx_id,
1003 claim_tx_id: maybe_claim_tx_id,
1004 refund_tx_id,
1005 refund_tx_amount_sat,
1006 description: description.unwrap_or("Bitcoin transfer".to_string()),
1007 liquid_expiration_blockheight,
1008 bitcoin_expiration_blockheight,
1009 auto_accepted_fees,
1010 }
1011 }
1012 _ => {
1013 let (amount, asset_id) = tx_with_balance.clone().map_or(
1014 (0, utils::lbtc_asset_id(self.network).to_string()),
1015 |(_, b)| (b.amount, b.asset_id),
1016 );
1017 let asset_info = match (
1018 maybe_asset_metadata_name,
1019 maybe_asset_metadata_ticker,
1020 maybe_asset_metadata_precision,
1021 ) {
1022 (Some(name), Some(ticker), Some(precision)) => {
1023 let asset_metadata = AssetMetadata {
1024 asset_id: asset_id.clone(),
1025 name: name.clone(),
1026 ticker: ticker.clone(),
1027 precision,
1028 fiat_id: None,
1029 };
1030 let (amount, fees) =
1031 maybe_payment_details_asset_fees.map_or((amount, None), |fees| {
1032 (
1033 amount.saturating_sub(fees),
1034 Some(asset_metadata.amount_from_sat(fees)),
1035 )
1036 });
1037
1038 Some(AssetInfo {
1039 name,
1040 ticker,
1041 amount: asset_metadata.amount_from_sat(amount),
1042 fees,
1043 })
1044 }
1045 _ => None,
1046 };
1047
1048 PaymentDetails::Liquid {
1049 destination: maybe_payment_details_destination
1050 .unwrap_or("Destination unknown".to_string()),
1051 description: maybe_payment_details_description
1052 .unwrap_or("Liquid transfer".to_string()),
1053 asset_id,
1054 asset_info,
1055 lnurl_info: maybe_payment_details_lnurl_info,
1056 bip353_address: maybe_payment_details_bip353_address,
1057 payer_note: maybe_payment_details_payer_note,
1058 }
1059 }
1060 };
1061
1062 match (tx_with_balance, swap.clone()) {
1063 (None, None) => Err(maybe_tx_tx_id.err().unwrap()),
1064 (None, Some(swap)) => Ok(Payment::from_pending_swap(
1065 swap,
1066 payment_type,
1067 payment_details,
1068 )),
1069 (Some((tx, balance)), None) => {
1070 Ok(Payment::from_tx_data(tx, balance, None, payment_details))
1071 }
1072 (Some((tx, balance)), Some(swap)) => Ok(Payment::from_tx_data(
1073 tx,
1074 balance,
1075 Some(swap),
1076 payment_details,
1077 )),
1078 }
1079 }
1080
1081 pub fn get_payment(&self, id: &str) -> Result<Option<Payment>> {
1082 Ok(self
1083 .get_connection()?
1084 .query_row(
1085 &self.select_payment_query(
1086 Some("(ptx.tx_id = ?1 OR COALESCE(rs.id, ss.id, cs.id) = ?1)"),
1087 None,
1088 None,
1089 None,
1090 None,
1091 ),
1092 params![id],
1093 |row| self.sql_row_to_payment(row),
1094 )
1095 .optional()?)
1096 }
1097
1098 pub fn get_payment_by_request(&self, req: &GetPaymentRequest) -> Result<Option<Payment>> {
1099 let (where_clause, param) = match req {
1100 GetPaymentRequest::PaymentHash { payment_hash } => (
1101 "(rs.payment_hash = ?1 OR ss.payment_hash = ?1)",
1102 payment_hash,
1103 ),
1104 GetPaymentRequest::SwapId { swap_id } => (
1105 "(rs.id = ?1 OR ss.id = ?1 OR cs.id = ?1 OR \
1106 rs.id_hash = ?1 OR ss.id_hash = ?1 OR cs.id_hash = ?1)",
1107 swap_id,
1108 ),
1109 };
1110 Ok(self
1111 .get_connection()?
1112 .query_row(
1113 &self.select_payment_query(Some(where_clause), None, None, None, Some(true)),
1114 params![param],
1115 |row| self.sql_row_to_payment(row),
1116 )
1117 .optional()?)
1118 }
1119
1120 pub fn get_payments(&self, req: &ListPaymentsRequest) -> Result<Vec<Payment>> {
1121 let (where_clause, where_params) = filter_to_where_clause(req);
1122 let maybe_where_clause = match where_clause.is_empty() {
1123 false => Some(where_clause.as_str()),
1124 true => None,
1125 };
1126
1127 let con = self.get_connection()?;
1129 let mut stmt = con.prepare(&self.select_payment_query(
1130 maybe_where_clause,
1131 req.offset,
1132 req.limit,
1133 req.sort_ascending,
1134 None,
1135 ))?;
1136 let payments: Vec<Payment> = stmt
1137 .query_map(params_from_iter(where_params), |row| {
1138 self.sql_row_to_payment(row)
1139 })?
1140 .map(|i| i.unwrap())
1141 .collect();
1142 Ok(payments)
1143 }
1144
1145 pub fn get_payments_by_tx_id(
1146 &self,
1147 req: &ListPaymentsRequest,
1148 ) -> Result<HashMap<String, Payment>> {
1149 let res: HashMap<String, Payment> = self
1150 .get_payments(req)?
1151 .into_iter()
1152 .flat_map(|payment| {
1153 let mut res = vec![];
1155 if let Some(tx_id) = payment.tx_id.clone() {
1156 res.push((tx_id, payment.clone()));
1157 }
1158 if let Some(refund_tx_id) = payment.get_refund_tx_id() {
1159 res.push((refund_tx_id, payment));
1160 }
1161 res
1162 })
1163 .collect();
1164 Ok(res)
1165 }
1166}
1167
1168fn filter_to_where_clause(req: &ListPaymentsRequest) -> (String, Vec<Box<dyn ToSql + '_>>) {
1169 let mut where_clause: Vec<String> = Vec::new();
1170 let mut where_params: Vec<Box<dyn ToSql>> = Vec::new();
1171
1172 if let Some(t) = req.from_timestamp {
1173 where_clause.push("coalesce(ptx.timestamp, rs.created_at) >= ?".to_string());
1174 where_params.push(Box::new(t));
1175 };
1176 if let Some(t) = req.to_timestamp {
1177 where_clause.push("coalesce(ptx.timestamp, rs.created_at) <= ?".to_string());
1178 where_params.push(Box::new(t));
1179 };
1180
1181 if let Some(filters) = &req.filters {
1182 if !filters.is_empty() {
1183 let mut type_filter_clause: HashSet<i8> = HashSet::new();
1184
1185 for type_filter in filters {
1186 type_filter_clause.insert(*type_filter as i8);
1187 }
1188
1189 where_clause.push(format!(
1190 "pb.payment_type in ({})",
1191 type_filter_clause
1192 .iter()
1193 .map(|t| format!("{t}"))
1194 .collect::<Vec<_>>()
1195 .join(", ")
1196 ));
1197 }
1198 }
1199
1200 if let Some(states) = &req.states {
1201 if !states.is_empty() {
1202 let deduped_states: Vec<PaymentState> = states
1203 .clone()
1204 .into_iter()
1205 .collect::<HashSet<PaymentState>>()
1206 .into_iter()
1207 .collect();
1208 let states_param = deduped_states
1209 .iter()
1210 .map(|t| (*t as i8).to_string())
1211 .collect::<Vec<_>>()
1212 .join(", ");
1213 let tx_comfirmed_param = deduped_states
1214 .iter()
1215 .filter_map(|state| match state {
1216 PaymentState::Pending | PaymentState::Complete => {
1217 Some(((*state == PaymentState::Complete) as i8).to_string())
1218 }
1219 _ => None,
1220 })
1221 .collect::<Vec<_>>()
1222 .join(", ");
1223 let states_query = match tx_comfirmed_param.is_empty() {
1224 true => format!("COALESCE(rs.state, ss.state, cs.state) in ({states_param})"),
1225 false => format!("(COALESCE(rs.id, ss.id, cs.id) IS NULL AND ptx.is_confirmed in ({tx_comfirmed_param}) OR COALESCE(rs.state, ss.state, cs.state) in ({states_param}))"),
1226 };
1227 where_clause.push(states_query);
1228 }
1229 }
1230
1231 if let Some(details) = &req.details {
1232 match details {
1233 ListPaymentDetails::Bitcoin { address } => {
1234 where_clause.push("cs.id IS NOT NULL".to_string());
1235 if let Some(address) = address {
1236 where_clause.push(
1238 "(cs.direction = 0 AND cs.lockup_address = ? OR cs.direction = 1 AND cs.claim_address = ?)"
1239 .to_string(),
1240 );
1241 where_params.push(Box::new(address));
1242 where_params.push(Box::new(address));
1243 }
1244 }
1245 ListPaymentDetails::Liquid {
1246 asset_id,
1247 destination,
1248 } => {
1249 where_clause.push("COALESCE(rs.id, ss.id, cs.id) IS NULL".to_string());
1250 if let Some(asset_id) = asset_id {
1251 where_clause.push("pb.asset_id = ?".to_string());
1252 where_params.push(Box::new(asset_id));
1253 }
1254 if let Some(destination) = destination {
1255 where_clause.push("pd.destination = ?".to_string());
1256 where_params.push(Box::new(destination));
1257 }
1258 }
1259 }
1260 }
1261
1262 (where_clause.join(" and "), where_params)
1263}
1264
1265#[cfg(test)]
1266mod tests {
1267 use anyhow::Result;
1268
1269 use crate::{
1270 model::LiquidNetwork,
1271 persist::PaymentTxDetails,
1272 prelude::ListPaymentsRequest,
1273 test_utils::persist::{
1274 create_persister, new_payment_tx_data, new_receive_swap, new_send_swap,
1275 },
1276 };
1277
1278 use super::{PaymentState, PaymentType};
1279
1280 #[cfg(feature = "browser-tests")]
1281 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1282
1283 #[sdk_macros::test_all]
1284 fn test_get_payments() -> Result<()> {
1285 create_persister!(storage);
1286
1287 let (payment_tx_data, payment_tx_balance) =
1288 new_payment_tx_data(LiquidNetwork::Testnet, PaymentType::Send);
1289 storage.insert_or_update_payment(
1290 payment_tx_data.clone(),
1291 &[payment_tx_balance],
1292 Some(PaymentTxDetails {
1293 destination: "mock-address".to_string(),
1294 ..Default::default()
1295 }),
1296 false,
1297 )?;
1298
1299 assert!(!storage
1300 .get_payments(&ListPaymentsRequest {
1301 ..Default::default()
1302 })?
1303 .is_empty());
1304 assert!(storage.get_payment(&payment_tx_data.tx_id)?.is_some());
1305
1306 Ok(())
1307 }
1308
1309 #[sdk_macros::test_all]
1310 fn test_list_ongoing_swaps() -> Result<()> {
1311 create_persister!(storage);
1312
1313 storage.insert_or_update_send_swap(&new_send_swap(None, None))?;
1314 storage
1315 .insert_or_update_receive_swap(&new_receive_swap(Some(PaymentState::Pending), None))?;
1316
1317 assert_eq!(storage.list_ongoing_swaps()?.len(), 2);
1318
1319 Ok(())
1320 }
1321}
1322
1323#[cfg(feature = "test-utils")]
1324pub mod test_helpers {
1325 use super::*;
1326
1327 impl Persister {
1328 pub fn test_insert_or_update_send_swap(&self, swap: &SendSwap) -> Result<()> {
1329 self.insert_or_update_send_swap(swap)
1330 }
1331
1332 pub fn test_insert_or_update_receive_swap(&self, swap: &ReceiveSwap) -> Result<()> {
1333 self.insert_or_update_receive_swap(swap)
1334 }
1335
1336 pub fn test_list_ongoing_swaps(&self) -> Result<Vec<Swap>> {
1337 self.list_ongoing_swaps()
1338 }
1339 }
1340}