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 delete_payment_tx_data(&self, tx_id: &str) -> Result<()> {
417 let con = self.get_connection()?;
418
419 con.execute("DELETE FROM payment_tx_data WHERE tx_id = ?", [tx_id])?;
420
421 Ok(())
422 }
423
424 fn insert_or_update_payment_details_inner(
425 con: &Connection,
426 payment_tx_details: &PaymentTxDetails,
427 skip_destination_update: bool,
428 ) -> Result<()> {
429 let destination_update = if skip_destination_update.not() {
430 "destination = excluded.destination,"
431 } else {
432 Default::default()
433 };
434 con.execute(
435 &format!(
436 "INSERT INTO payment_details (
437 tx_id,
438 destination,
439 description,
440 lnurl_info_json,
441 bip353_address,
442 payer_note,
443 asset_fees
444 )
445 VALUES (?, ?, ?, ?, ?, ?, ?)
446 ON CONFLICT (tx_id)
447 DO UPDATE SET
448 {destination_update}
449 description = COALESCE(excluded.description, description),
450 lnurl_info_json = COALESCE(excluded.lnurl_info_json, lnurl_info_json),
451 bip353_address = COALESCE(excluded.bip353_address, bip353_address),
452 payer_note = COALESCE(excluded.payer_note, payer_note),
453 asset_fees = COALESCE(excluded.asset_fees, asset_fees)
454 "
455 ),
456 (
457 &payment_tx_details.tx_id,
458 &payment_tx_details.destination,
459 &payment_tx_details.description,
460 payment_tx_details
461 .lnurl_info
462 .as_ref()
463 .map(|info| serde_json::to_string(&info).ok()),
464 &payment_tx_details.bip353_address,
465 &payment_tx_details.payer_note,
466 from_optional_u64_to_row(&payment_tx_details.asset_fees)?,
467 ),
468 )?;
469 Ok(())
470 }
471
472 pub(crate) fn insert_or_update_payment_details(
473 &self,
474 payment_tx_details: PaymentTxDetails,
475 ) -> Result<()> {
476 let mut con = self.get_connection()?;
477 let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?;
478
479 Self::insert_or_update_payment_details_inner(&tx, &payment_tx_details, false)?;
480 self.commit_outgoing(
481 &tx,
482 &payment_tx_details.tx_id,
483 RecordType::PaymentDetails,
484 None,
485 )?;
486 tx.commit()?;
487 self.trigger_sync();
488
489 Ok(())
490 }
491
492 pub(crate) fn get_payment_details(&self, tx_id: &str) -> Result<Option<PaymentTxDetails>> {
493 let con = self.get_connection()?;
494 let mut stmt = con.prepare(
495 "SELECT destination, description, lnurl_info_json, bip353_address, payer_note, asset_fees
496 FROM payment_details
497 WHERE tx_id = ?",
498 )?;
499 let res = stmt.query_row([tx_id], |row| {
500 let destination = row.get(0)?;
501 let description = row.get(1)?;
502 let maybe_lnurl_info_json: Option<String> = row.get(2)?;
503 let maybe_bip353_address = row.get(3)?;
504 let maybe_payer_note = row.get(4)?;
505 let maybe_asset_fees = from_row_to_optional_u64(row, 5)?;
506 Ok(PaymentTxDetails {
507 tx_id: tx_id.to_string(),
508 destination,
509 description,
510 lnurl_info: maybe_lnurl_info_json
511 .and_then(|info| serde_json::from_str::<LnUrlInfo>(&info).ok()),
512 bip353_address: maybe_bip353_address,
513 payer_note: maybe_payer_note,
514 asset_fees: maybe_asset_fees,
515 })
516 });
517 Ok(res.ok())
518 }
519
520 pub(crate) fn list_ongoing_swaps(&self) -> Result<Vec<Swap>> {
521 let ongoing_send_swaps: Vec<Swap> = self
522 .list_ongoing_send_swaps()?
523 .into_iter()
524 .map(Swap::Send)
525 .collect();
526 let ongoing_receive_swaps: Vec<Swap> = self
527 .list_ongoing_receive_swaps()?
528 .into_iter()
529 .map(Swap::Receive)
530 .collect();
531 let ongoing_chain_swaps: Vec<Swap> = self
532 .list_ongoing_chain_swaps()?
533 .into_iter()
534 .map(Swap::Chain)
535 .collect();
536 Ok([
537 ongoing_send_swaps,
538 ongoing_receive_swaps,
539 ongoing_chain_swaps,
540 ]
541 .concat())
542 }
543
544 fn select_payment_query(
545 &self,
546 where_clause: Option<&str>,
547 offset: Option<u32>,
548 limit: Option<u32>,
549 sort_ascending: Option<bool>,
550 include_all_states: Option<bool>,
551 ) -> String {
552 let (where_receive_swap_clause, where_chain_swap_clause) = if include_all_states
553 .unwrap_or_default()
554 {
555 ("true", "true")
556 } else {
557 (
558 "COALESCE(claim_tx_id, lockup_tx_id, mrh_tx_id) IS NOT NULL AND state NOT IN (0, 3, 4)",
560 "COALESCE(user_lockup_tx_id, claim_tx_id) IS NOT NULL AND state NOT IN (0, 4)",
562 )
563 };
564
565 format!(
566 "
567 SELECT
568 ptx.tx_id,
569 ptx.timestamp,
570 ptx.fees_sat,
571 ptx.is_confirmed,
572 ptx.unblinding_data,
573 pb.amount,
574 pb.asset_id,
575 pb.payment_type,
576 rs.id,
577 rs.created_at,
578 rs.timeout_block_height,
579 rs.invoice,
580 rs.bolt12_offer,
581 rs.payment_hash,
582 rs.destination_pubkey,
583 rs.description,
584 rs.payer_note,
585 rs.preimage,
586 rs.payer_amount_sat,
587 rs.receiver_amount_sat,
588 rs.state,
589 rs.pair_fees_json,
590 rs.claim_tx_id,
591 ss.id,
592 ss.created_at,
593 ss.timeout_block_height,
594 ss.invoice,
595 ss.bolt12_offer,
596 ss.payment_hash,
597 ss.destination_pubkey,
598 ss.description,
599 ss.preimage,
600 ss.refund_tx_id,
601 ss.payer_amount_sat,
602 ss.receiver_amount_sat,
603 ss.state,
604 ss.pair_fees_json,
605 cs.id,
606 cs.created_at,
607 cs.timeout_block_height,
608 cs.claim_timeout_block_height,
609 cs.direction,
610 cs.preimage,
611 cs.description,
612 cs.refund_tx_id,
613 cs.payer_amount_sat,
614 cs.receiver_amount_sat,
615 cs.claim_address,
616 cs.lockup_address,
617 cs.state,
618 cs.pair_fees_json,
619 cs.actual_payer_amount_sat,
620 cs.accepted_receiver_amount_sat,
621 cs.auto_accepted_fees,
622 cs.user_lockup_tx_id,
623 cs.claim_tx_id,
624 rb.amount,
625 pd.destination,
626 pd.description,
627 pd.lnurl_info_json,
628 pd.bip353_address,
629 pd.payer_note,
630 pd.asset_fees,
631 am.name,
632 am.ticker,
633 am.precision
634 FROM payment_tx_data AS ptx -- Payment tx (each tx results in a Payment)
635 LEFT JOIN payment_balance AS pb
636 ON pb.tx_id = ptx.tx_id -- Payment tx balances, split by asset
637 FULL JOIN (
638 SELECT * FROM receive_swaps WHERE {}
639 ) rs -- Receive Swap data
640 ON ptx.tx_id in (rs.claim_tx_id, rs.mrh_tx_id)
641 FULL JOIN (
642 SELECT * FROM chain_swaps WHERE {}
643 ) cs -- Chain Swap data
644 ON ptx.tx_id in (cs.user_lockup_tx_id, cs.claim_tx_id)
645 LEFT JOIN send_swaps AS ss -- Send Swap data
646 ON ptx.tx_id = ss.lockup_tx_id
647 LEFT JOIN payment_balance AS rb -- Refund tx balance
648 ON rb.tx_id in (ss.refund_tx_id, cs.refund_tx_id)
649 LEFT JOIN payment_details AS pd -- Payment details
650 ON pd.tx_id = ptx.tx_id
651 LEFT JOIN asset_metadata AS am -- Asset metadata
652 ON am.asset_id = pb.asset_id
653 WHERE
654 (ptx.tx_id IS NULL -- Filter out refund txs from Chain/Send Swaps
655 OR ptx.tx_id NOT IN (SELECT refund_tx_id FROM send_swaps WHERE refund_tx_id NOT NULL)
656 AND ptx.tx_id NOT IN (SELECT refund_tx_id FROM chain_swaps WHERE refund_tx_id NOT NULL))
657 AND {}
658 ORDER BY -- Order by swap creation time or tx timestamp (in case of direct tx)
659 COALESCE(rs.created_at, ss.created_at, cs.created_at, ptx.timestamp) {}
660 LIMIT {}
661 OFFSET {}
662 ",
663 where_receive_swap_clause,
664 where_chain_swap_clause,
665 where_clause.unwrap_or("true"),
666 match sort_ascending.unwrap_or(false) {
667 true => "ASC",
668 false => "DESC",
669 },
670 limit.unwrap_or(u32::MAX),
671 offset.unwrap_or(0),
672 )
673 }
674
675 fn sql_row_to_payment(&self, row: &Row) -> Result<Payment, rusqlite::Error> {
676 let maybe_tx_tx_id: Result<String, rusqlite::Error> = row.get(0);
677 let tx_with_balance = match maybe_tx_tx_id {
678 Ok(ref tx_id) => Some((
679 PaymentTxData {
680 tx_id: tx_id.to_string(),
681 timestamp: row.get(1)?,
682 fees_sat: from_row_to_u64(row, 2)?,
683 is_confirmed: row.get(3)?,
684 unblinding_data: row.get(4)?,
685 },
686 PaymentTxBalance {
687 amount: from_row_to_u64(row, 5)?,
688 asset_id: row.get(6)?,
689 payment_type: row.get(7)?,
690 },
691 )),
692 _ => None,
693 };
694
695 let maybe_receive_swap_id: Option<String> = row.get(8)?;
696 let maybe_receive_swap_created_at: Option<u32> = row.get(9)?;
697 let maybe_receive_swap_timeout_block_height: Option<u32> = row.get(10)?;
698 let maybe_receive_swap_invoice: Option<String> = row.get(11)?;
699 let maybe_receive_swap_bolt12_offer: Option<String> = row.get(12)?;
700 let maybe_receive_swap_payment_hash: Option<String> = row.get(13)?;
701 let maybe_receive_swap_destination_pubkey: Option<String> = row.get(14)?;
702 let maybe_receive_swap_description: Option<String> = row.get(15)?;
703 let maybe_receive_swap_payer_note: Option<String> = row.get(16)?;
704 let maybe_receive_swap_preimage: Option<String> = row.get(17)?;
705 let maybe_receive_swap_payer_amount_sat = from_row_to_optional_u64(row, 18)?;
706 let maybe_receive_swap_receiver_amount_sat = from_row_to_optional_u64(row, 19)?;
707 let maybe_receive_swap_receiver_state: Option<PaymentState> = row.get(20)?;
708 let maybe_receive_swap_pair_fees_json: Option<String> = row.get(21)?;
709 let maybe_receive_swap_pair_fees: Option<ReversePair> =
710 maybe_receive_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok());
711 let maybe_receive_swap_claim_tx_id: Option<String> = row.get(22)?;
712
713 let maybe_send_swap_id: Option<String> = row.get(23)?;
714 let maybe_send_swap_created_at: Option<u32> = row.get(24)?;
715 let maybe_send_swap_timeout_block_height: Option<u32> = row.get(25)?;
716 let maybe_send_swap_invoice: Option<String> = row.get(26)?;
717 let maybe_send_swap_bolt12_offer: Option<String> = row.get(27)?;
718 let maybe_send_swap_payment_hash: Option<String> = row.get(28)?;
719 let maybe_send_swap_destination_pubkey: Option<String> = row.get(29)?;
720 let maybe_send_swap_description: Option<String> = row.get(30)?;
721 let maybe_send_swap_preimage: Option<String> = row.get(31)?;
722 let maybe_send_swap_refund_tx_id: Option<String> = row.get(32)?;
723 let maybe_send_swap_payer_amount_sat = from_row_to_optional_u64(row, 33)?;
724 let maybe_send_swap_receiver_amount_sat = from_row_to_optional_u64(row, 34)?;
725 let maybe_send_swap_state: Option<PaymentState> = row.get(35)?;
726 let maybe_send_swap_pair_fees_json: Option<String> = row.get(36)?;
727 let maybe_send_swap_pair_fees: Option<SubmarinePair> =
728 maybe_send_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok());
729
730 let maybe_chain_swap_id: Option<String> = row.get(37)?;
731 let maybe_chain_swap_created_at: Option<u32> = row.get(38)?;
732 let maybe_chain_swap_timeout_block_height: Option<u32> = row.get(39)?;
733 let maybe_chain_swap_claim_timeout_block_height: Option<u32> = row.get(40)?;
734 let maybe_chain_swap_direction: Option<Direction> = row.get(41)?;
735 let maybe_chain_swap_preimage: Option<String> = row.get(42)?;
736 let maybe_chain_swap_description: Option<String> = row.get(43)?;
737 let maybe_chain_swap_refund_tx_id: Option<String> = row.get(44)?;
738 let maybe_chain_swap_payer_amount_sat = from_row_to_optional_u64(row, 45)?;
739 let maybe_chain_swap_receiver_amount_sat = from_row_to_optional_u64(row, 46)?;
740 let maybe_chain_swap_claim_address: Option<String> = row.get(47)?;
741 let maybe_chain_swap_lockup_address: Option<String> = row.get(48)?;
742 let maybe_chain_swap_state: Option<PaymentState> = row.get(49)?;
743 let maybe_chain_swap_pair_fees_json: Option<String> = row.get(50)?;
744 let maybe_chain_swap_pair_fees: Option<ChainPair> =
745 maybe_chain_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok());
746 let maybe_chain_swap_actual_payer_amount_sat = from_row_to_optional_u64(row, 51)?;
747 let maybe_chain_swap_accepted_receiver_amount_sat = from_row_to_optional_u64(row, 52)?;
748 let maybe_chain_swap_auto_accepted_fees: Option<bool> = row.get(53)?;
749 let maybe_chain_swap_user_lockup_tx_id: Option<String> = row.get(54)?;
750 let maybe_chain_swap_claim_tx_id: Option<String> = row.get(55)?;
751
752 let maybe_swap_refund_tx_amount_sat = from_row_to_optional_u64(row, 56)?;
753
754 let maybe_payment_details_destination: Option<String> = row.get(57)?;
755 let maybe_payment_details_description: Option<String> = row.get(58)?;
756 let maybe_payment_details_lnurl_info_json: Option<String> = row.get(59)?;
757 let maybe_payment_details_lnurl_info: Option<LnUrlInfo> =
758 maybe_payment_details_lnurl_info_json.and_then(|info| serde_json::from_str(&info).ok());
759 let maybe_payment_details_bip353_address: Option<String> = row.get(60)?;
760 let maybe_payment_details_payer_note: Option<String> = row.get(61)?;
761 let maybe_payment_details_asset_fees = from_row_to_optional_u64(row, 62)?;
762
763 let maybe_asset_metadata_name: Option<String> = row.get(63)?;
764 let maybe_asset_metadata_ticker: Option<String> = row.get(64)?;
765 let maybe_asset_metadata_precision: Option<u8> = row.get(65)?;
766
767 let bitcoin_address = match maybe_chain_swap_direction {
768 Some(Direction::Incoming) => maybe_chain_swap_lockup_address,
769 Some(Direction::Outgoing) => maybe_chain_swap_claim_address,
770 None => None,
771 };
772
773 let (swap, payment_type) = match maybe_receive_swap_id {
774 Some(receive_swap_id) => {
775 let payer_amount_sat = maybe_receive_swap_payer_amount_sat.unwrap_or(0);
776
777 (
778 Some(PaymentSwapData {
779 swap_id: receive_swap_id,
780 swap_type: PaymentSwapType::Receive,
781 created_at: maybe_receive_swap_created_at.unwrap_or(utils::now()),
782 expiration_blockheight: maybe_receive_swap_timeout_block_height
783 .unwrap_or(0),
784 claim_expiration_blockheight: None,
785 preimage: maybe_receive_swap_preimage,
786 invoice: maybe_receive_swap_invoice.clone(),
787 bolt12_offer: maybe_receive_swap_bolt12_offer,
788 payment_hash: maybe_receive_swap_payment_hash,
789 destination_pubkey: maybe_receive_swap_destination_pubkey,
790 description: maybe_receive_swap_description.unwrap_or_else(|| {
791 maybe_receive_swap_invoice
792 .and_then(|invoice| {
793 utils::get_invoice_description(&invoice).ok().flatten()
794 })
795 .unwrap_or("Lightning payment".to_string())
796 }),
797 payer_note: maybe_receive_swap_payer_note,
798 payer_amount_sat,
799 receiver_amount_sat: maybe_receive_swap_receiver_amount_sat.unwrap_or(0),
800 swapper_fees_sat: maybe_receive_swap_pair_fees
801 .map(|pair| pair.fees.boltz(payer_amount_sat))
802 .unwrap_or(0),
803 refund_tx_id: None,
804 refund_tx_amount_sat: None,
805 bitcoin_address: None,
806 status: maybe_receive_swap_receiver_state.unwrap_or(PaymentState::Created),
807 }),
808 PaymentType::Receive,
809 )
810 }
811 None => match maybe_send_swap_id {
812 Some(send_swap_id) => {
813 let receiver_amount_sat = maybe_send_swap_receiver_amount_sat.unwrap_or(0);
814 (
815 Some(PaymentSwapData {
816 swap_id: send_swap_id,
817 swap_type: PaymentSwapType::Send,
818 created_at: maybe_send_swap_created_at.unwrap_or(utils::now()),
819 expiration_blockheight: maybe_send_swap_timeout_block_height
820 .unwrap_or(0),
821 claim_expiration_blockheight: None,
822 preimage: maybe_send_swap_preimage,
823 invoice: maybe_send_swap_invoice,
824 bolt12_offer: maybe_send_swap_bolt12_offer,
825 payment_hash: maybe_send_swap_payment_hash,
826 destination_pubkey: maybe_send_swap_destination_pubkey,
827 description: maybe_send_swap_description
828 .unwrap_or("Lightning payment".to_string()),
829 payer_note: None,
830 payer_amount_sat: maybe_send_swap_payer_amount_sat.unwrap_or(0),
831 receiver_amount_sat,
832 swapper_fees_sat: maybe_send_swap_pair_fees
833 .map(|pair| pair.fees.boltz(receiver_amount_sat))
834 .unwrap_or(0),
835 refund_tx_id: maybe_send_swap_refund_tx_id,
836 refund_tx_amount_sat: maybe_swap_refund_tx_amount_sat,
837 bitcoin_address: None,
838 status: maybe_send_swap_state.unwrap_or(PaymentState::Created),
839 }),
840 PaymentType::Send,
841 )
842 }
843 None => match maybe_chain_swap_id {
844 Some(chain_swap_id) => {
845 let (payer_amount_sat, receiver_amount_sat) = match (
846 maybe_chain_swap_actual_payer_amount_sat,
847 maybe_chain_swap_payer_amount_sat,
848 ) {
849 (Some(actual_payer_amount_sat), Some(0)) => {
852 (actual_payer_amount_sat, actual_payer_amount_sat)
853 }
854 _ => (
856 maybe_chain_swap_payer_amount_sat.unwrap_or(0),
857 maybe_chain_swap_receiver_amount_sat.unwrap_or(0),
858 ),
859 };
860 let receiver_amount_sat =
861 match maybe_chain_swap_accepted_receiver_amount_sat {
862 Some(accepted_receiver_amount_sat) => accepted_receiver_amount_sat,
864 None => receiver_amount_sat,
865 };
866 let swapper_fees_sat = maybe_chain_swap_pair_fees
867 .map(|pair| pair.fees.percentage)
868 .map(|fr| ((fr / 100.0) * payer_amount_sat as f64).ceil() as u64)
869 .unwrap_or(0);
870
871 (
872 Some(PaymentSwapData {
873 swap_id: chain_swap_id,
874 swap_type: PaymentSwapType::Chain,
875 created_at: maybe_chain_swap_created_at.unwrap_or(utils::now()),
876 expiration_blockheight: maybe_chain_swap_timeout_block_height
877 .unwrap_or(0),
878 claim_expiration_blockheight:
879 maybe_chain_swap_claim_timeout_block_height,
880 preimage: maybe_chain_swap_preimage,
881 invoice: None,
882 bolt12_offer: None, payment_hash: None,
884 destination_pubkey: None,
885 description: maybe_chain_swap_description
886 .unwrap_or("Bitcoin transfer".to_string()),
887 payer_note: None,
888 payer_amount_sat,
889 receiver_amount_sat,
890 swapper_fees_sat,
891 refund_tx_id: maybe_chain_swap_refund_tx_id,
892 refund_tx_amount_sat: maybe_swap_refund_tx_amount_sat,
893 bitcoin_address: bitcoin_address.clone(),
894 status: maybe_chain_swap_state.unwrap_or(PaymentState::Created),
895 }),
896 maybe_chain_swap_direction
897 .unwrap_or(Direction::Outgoing)
898 .into(),
899 )
900 }
901 None => (None, PaymentType::Send),
902 },
903 },
904 };
905
906 let maybe_claim_tx_id = maybe_receive_swap_claim_tx_id.or(maybe_chain_swap_claim_tx_id);
907 let description = swap.as_ref().map(|s| s.description.clone());
908 let payment_details = match swap.clone() {
909 Some(
910 PaymentSwapData {
911 swap_type: PaymentSwapType::Receive,
912 swap_id,
913 invoice,
914 bolt12_offer,
915 payment_hash,
916 destination_pubkey,
917 payer_note,
918 refund_tx_id,
919 preimage,
920 refund_tx_amount_sat,
921 expiration_blockheight,
922 ..
923 }
924 | PaymentSwapData {
925 swap_type: PaymentSwapType::Send,
926 swap_id,
927 invoice,
928 bolt12_offer,
929 payment_hash,
930 destination_pubkey,
931 payer_note,
932 preimage,
933 refund_tx_id,
934 refund_tx_amount_sat,
935 expiration_blockheight,
936 ..
937 },
938 ) => PaymentDetails::Lightning {
939 swap_id,
940 preimage,
941 invoice: invoice.clone(),
942 bolt12_offer: bolt12_offer.clone(),
943 payment_hash,
944 destination_pubkey: destination_pubkey.or_else(|| {
945 invoice.and_then(|invoice| {
946 utils::get_invoice_destination_pubkey(&invoice, bolt12_offer.is_some()).ok()
947 })
948 }),
949 lnurl_info: maybe_payment_details_lnurl_info,
950 bip353_address: maybe_payment_details_bip353_address,
951 payer_note: payer_note.or(maybe_payment_details_payer_note),
952 claim_tx_id: maybe_claim_tx_id,
953 refund_tx_id,
954 refund_tx_amount_sat,
955 description: maybe_payment_details_description
956 .unwrap_or(description.unwrap_or("Lightning transfer".to_string())),
957 liquid_expiration_blockheight: expiration_blockheight,
958 },
959 Some(PaymentSwapData {
960 swap_type: PaymentSwapType::Chain,
961 swap_id,
962 refund_tx_id,
963 refund_tx_amount_sat,
964 expiration_blockheight,
965 claim_expiration_blockheight,
966 ..
967 }) => {
968 let (bitcoin_expiration_blockheight, liquid_expiration_blockheight) =
969 match maybe_chain_swap_direction {
970 Some(Direction::Incoming) => (
971 expiration_blockheight,
972 claim_expiration_blockheight.unwrap_or_default(),
973 ),
974 Some(Direction::Outgoing) | None => (
975 claim_expiration_blockheight.unwrap_or_default(),
976 expiration_blockheight,
977 ),
978 };
979 let auto_accepted_fees = maybe_chain_swap_auto_accepted_fees.unwrap_or(false);
980
981 PaymentDetails::Bitcoin {
982 swap_id,
983 bitcoin_address: bitcoin_address.unwrap_or_default(),
984 lockup_tx_id: maybe_chain_swap_user_lockup_tx_id,
985 claim_tx_id: maybe_claim_tx_id,
986 refund_tx_id,
987 refund_tx_amount_sat,
988 description: description.unwrap_or("Bitcoin transfer".to_string()),
989 liquid_expiration_blockheight,
990 bitcoin_expiration_blockheight,
991 auto_accepted_fees,
992 }
993 }
994 _ => {
995 let (amount, asset_id) = tx_with_balance.clone().map_or(
996 (0, utils::lbtc_asset_id(self.network).to_string()),
997 |(_, b)| (b.amount, b.asset_id),
998 );
999 let asset_info = match (
1000 maybe_asset_metadata_name,
1001 maybe_asset_metadata_ticker,
1002 maybe_asset_metadata_precision,
1003 ) {
1004 (Some(name), Some(ticker), Some(precision)) => {
1005 let asset_metadata = AssetMetadata {
1006 asset_id: asset_id.clone(),
1007 name: name.clone(),
1008 ticker: ticker.clone(),
1009 precision,
1010 fiat_id: None,
1011 };
1012 let (amount, fees) =
1013 maybe_payment_details_asset_fees.map_or((amount, None), |fees| {
1014 (
1015 amount.saturating_sub(fees),
1016 Some(asset_metadata.amount_from_sat(fees)),
1017 )
1018 });
1019
1020 Some(AssetInfo {
1021 name,
1022 ticker,
1023 amount: asset_metadata.amount_from_sat(amount),
1024 fees,
1025 })
1026 }
1027 _ => None,
1028 };
1029
1030 PaymentDetails::Liquid {
1031 destination: maybe_payment_details_destination
1032 .unwrap_or("Destination unknown".to_string()),
1033 description: maybe_payment_details_description
1034 .unwrap_or("Liquid transfer".to_string()),
1035 asset_id,
1036 asset_info,
1037 lnurl_info: maybe_payment_details_lnurl_info,
1038 bip353_address: maybe_payment_details_bip353_address,
1039 payer_note: maybe_payment_details_payer_note,
1040 }
1041 }
1042 };
1043
1044 match (tx_with_balance, swap.clone()) {
1045 (None, None) => Err(maybe_tx_tx_id.err().unwrap()),
1046 (None, Some(swap)) => Ok(Payment::from_pending_swap(
1047 swap,
1048 payment_type,
1049 payment_details,
1050 )),
1051 (Some((tx, balance)), None) => {
1052 Ok(Payment::from_tx_data(tx, balance, None, payment_details))
1053 }
1054 (Some((tx, balance)), Some(swap)) => Ok(Payment::from_tx_data(
1055 tx,
1056 balance,
1057 Some(swap),
1058 payment_details,
1059 )),
1060 }
1061 }
1062
1063 pub fn get_payment(&self, id: &str) -> Result<Option<Payment>> {
1064 Ok(self
1065 .get_connection()?
1066 .query_row(
1067 &self.select_payment_query(
1068 Some("(ptx.tx_id = ?1 OR COALESCE(rs.id, ss.id, cs.id) = ?1)"),
1069 None,
1070 None,
1071 None,
1072 None,
1073 ),
1074 params![id],
1075 |row| self.sql_row_to_payment(row),
1076 )
1077 .optional()?)
1078 }
1079
1080 pub fn get_payment_by_request(&self, req: &GetPaymentRequest) -> Result<Option<Payment>> {
1081 let (where_clause, param) = match req {
1082 GetPaymentRequest::PaymentHash { payment_hash } => (
1083 "(rs.payment_hash = ?1 OR ss.payment_hash = ?1)",
1084 payment_hash,
1085 ),
1086 GetPaymentRequest::SwapId { swap_id } => (
1087 "(rs.id = ?1 OR ss.id = ?1 OR cs.id = ?1 OR \
1088 rs.id_hash = ?1 OR ss.id_hash = ?1 OR cs.id_hash = ?1)",
1089 swap_id,
1090 ),
1091 };
1092 Ok(self
1093 .get_connection()?
1094 .query_row(
1095 &self.select_payment_query(Some(where_clause), None, None, None, Some(true)),
1096 params![param],
1097 |row| self.sql_row_to_payment(row),
1098 )
1099 .optional()?)
1100 }
1101
1102 pub fn get_payments(&self, req: &ListPaymentsRequest) -> Result<Vec<Payment>> {
1103 let (where_clause, where_params) = filter_to_where_clause(req);
1104 let maybe_where_clause = match where_clause.is_empty() {
1105 false => Some(where_clause.as_str()),
1106 true => None,
1107 };
1108
1109 let con = self.get_connection()?;
1111 let mut stmt = con.prepare(&self.select_payment_query(
1112 maybe_where_clause,
1113 req.offset,
1114 req.limit,
1115 req.sort_ascending,
1116 None,
1117 ))?;
1118 let payments: Vec<Payment> = stmt
1119 .query_map(params_from_iter(where_params), |row| {
1120 self.sql_row_to_payment(row)
1121 })?
1122 .map(|i| i.unwrap())
1123 .collect();
1124 Ok(payments)
1125 }
1126
1127 pub fn get_payments_by_tx_id(
1128 &self,
1129 req: &ListPaymentsRequest,
1130 ) -> Result<HashMap<String, Payment>> {
1131 let res: HashMap<String, Payment> = self
1132 .get_payments(req)?
1133 .into_iter()
1134 .flat_map(|payment| {
1135 let mut res = vec![];
1137 if let Some(tx_id) = payment.tx_id.clone() {
1138 res.push((tx_id, payment.clone()));
1139 }
1140 if let Some(refund_tx_id) = payment.get_refund_tx_id() {
1141 res.push((refund_tx_id, payment));
1142 }
1143 res
1144 })
1145 .collect();
1146 Ok(res)
1147 }
1148}
1149
1150fn filter_to_where_clause(req: &ListPaymentsRequest) -> (String, Vec<Box<dyn ToSql + '_>>) {
1151 let mut where_clause: Vec<String> = Vec::new();
1152 let mut where_params: Vec<Box<dyn ToSql>> = Vec::new();
1153
1154 if let Some(t) = req.from_timestamp {
1155 where_clause.push("coalesce(ptx.timestamp, rs.created_at) >= ?".to_string());
1156 where_params.push(Box::new(t));
1157 };
1158 if let Some(t) = req.to_timestamp {
1159 where_clause.push("coalesce(ptx.timestamp, rs.created_at) <= ?".to_string());
1160 where_params.push(Box::new(t));
1161 };
1162
1163 if let Some(filters) = &req.filters {
1164 if !filters.is_empty() {
1165 let mut type_filter_clause: HashSet<i8> = HashSet::new();
1166
1167 for type_filter in filters {
1168 type_filter_clause.insert(*type_filter as i8);
1169 }
1170
1171 where_clause.push(format!(
1172 "pb.payment_type in ({})",
1173 type_filter_clause
1174 .iter()
1175 .map(|t| format!("{t}"))
1176 .collect::<Vec<_>>()
1177 .join(", ")
1178 ));
1179 }
1180 }
1181
1182 if let Some(states) = &req.states {
1183 if !states.is_empty() {
1184 let deduped_states: Vec<PaymentState> = states
1185 .clone()
1186 .into_iter()
1187 .collect::<HashSet<PaymentState>>()
1188 .into_iter()
1189 .collect();
1190 let states_param = deduped_states
1191 .iter()
1192 .map(|t| (*t as i8).to_string())
1193 .collect::<Vec<_>>()
1194 .join(", ");
1195 let tx_comfirmed_param = deduped_states
1196 .iter()
1197 .filter_map(|state| match state {
1198 PaymentState::Pending | PaymentState::Complete => {
1199 Some(((*state == PaymentState::Complete) as i8).to_string())
1200 }
1201 _ => None,
1202 })
1203 .collect::<Vec<_>>()
1204 .join(", ");
1205 let states_query = match tx_comfirmed_param.is_empty() {
1206 true => format!("COALESCE(rs.state, ss.state, cs.state) in ({states_param})"),
1207 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}))"),
1208 };
1209 where_clause.push(states_query);
1210 }
1211 }
1212
1213 if let Some(details) = &req.details {
1214 match details {
1215 ListPaymentDetails::Bitcoin { address } => {
1216 where_clause.push("cs.id IS NOT NULL".to_string());
1217 if let Some(address) = address {
1218 where_clause.push(
1220 "(cs.direction = 0 AND cs.lockup_address = ? OR cs.direction = 1 AND cs.claim_address = ?)"
1221 .to_string(),
1222 );
1223 where_params.push(Box::new(address));
1224 where_params.push(Box::new(address));
1225 }
1226 }
1227 ListPaymentDetails::Liquid {
1228 asset_id,
1229 destination,
1230 } => {
1231 where_clause.push("COALESCE(rs.id, ss.id, cs.id) IS NULL".to_string());
1232 if let Some(asset_id) = asset_id {
1233 where_clause.push("pb.asset_id = ?".to_string());
1234 where_params.push(Box::new(asset_id));
1235 }
1236 if let Some(destination) = destination {
1237 where_clause.push("pd.destination = ?".to_string());
1238 where_params.push(Box::new(destination));
1239 }
1240 }
1241 }
1242 }
1243
1244 (where_clause.join(" and "), where_params)
1245}
1246
1247#[cfg(test)]
1248mod tests {
1249 use anyhow::Result;
1250
1251 use crate::{
1252 model::LiquidNetwork,
1253 persist::PaymentTxDetails,
1254 prelude::ListPaymentsRequest,
1255 test_utils::persist::{
1256 create_persister, new_payment_tx_data, new_receive_swap, new_send_swap,
1257 },
1258 };
1259
1260 use super::{PaymentState, PaymentType};
1261
1262 #[cfg(feature = "browser-tests")]
1263 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1264
1265 #[sdk_macros::test_all]
1266 fn test_get_payments() -> Result<()> {
1267 create_persister!(storage);
1268
1269 let (payment_tx_data, payment_tx_balance) =
1270 new_payment_tx_data(LiquidNetwork::Testnet, PaymentType::Send);
1271 storage.insert_or_update_payment(
1272 payment_tx_data.clone(),
1273 &[payment_tx_balance],
1274 Some(PaymentTxDetails {
1275 destination: "mock-address".to_string(),
1276 ..Default::default()
1277 }),
1278 false,
1279 )?;
1280
1281 assert!(!storage
1282 .get_payments(&ListPaymentsRequest {
1283 ..Default::default()
1284 })?
1285 .is_empty());
1286 assert!(storage.get_payment(&payment_tx_data.tx_id)?.is_some());
1287
1288 Ok(())
1289 }
1290
1291 #[sdk_macros::test_all]
1292 fn test_list_ongoing_swaps() -> Result<()> {
1293 create_persister!(storage);
1294
1295 storage.insert_or_update_send_swap(&new_send_swap(None, None))?;
1296 storage
1297 .insert_or_update_receive_swap(&new_receive_swap(Some(PaymentState::Pending), None))?;
1298
1299 assert_eq!(storage.list_ongoing_swaps()?.len(), 2);
1300
1301 Ok(())
1302 }
1303}
1304
1305#[cfg(feature = "test-utils")]
1306pub mod test_helpers {
1307 use super::*;
1308
1309 impl Persister {
1310 pub fn test_insert_or_update_send_swap(&self, swap: &SendSwap) -> Result<()> {
1311 self.insert_or_update_send_swap(swap)
1312 }
1313
1314 pub fn test_insert_or_update_receive_swap(&self, swap: &ReceiveSwap) -> Result<()> {
1315 self.insert_or_update_receive_swap(swap)
1316 }
1317
1318 pub fn test_list_ongoing_swaps(&self) -> Result<Vec<Swap>> {
1319 self.list_ongoing_swaps()
1320 }
1321 }
1322}