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