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