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