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