breez_sdk_liquid/persist/
mod.rs

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
43/// Builds a WHERE clause that checks if `state` is any of the given arguments
44fn 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    /// Creates a new Persister that stores data on the provided `working_dir`.
66    #[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    /// Creates a new Persister that only keeps data in memory.
81    ///
82    /// Multiple persisters accessing the same in-memory data can be created by providing the
83    /// same `database_id`.
84    #[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        // Remove the Liquid Bitcoin asset balance
204        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 the balances are still not empty pop the asset balance
209        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            // If the update comes from the wallet tx:
341            // - Skip updating the destination from the script_pubkey
342            // - Skip syncing the payment_tx_details
343            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                // Receive Swap has a tx id and state not in Created, Failed, TimedOut
504                "COALESCE(claim_tx_id, lockup_tx_id, mrh_tx_id) IS NOT NULL AND state NOT IN (0, 3, 4)",
505                // Chain Swap has a tx id and state not in Created, TimedOut
506                "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                            // For amountless chain swaps use the actual payer amount when
779                            // set as the payer amount and receiver amount
780                            (Some(actual_payer_amount_sat), Some(0)) => {
781                                (actual_payer_amount_sat, actual_payer_amount_sat)
782                            }
783                            // Otherwise use the precalculated payer and receiver amounts
784                            _ => (
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                                // If the accepted receiver amount is set, use it
792                                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, // Bolt12 not supported for Chain Swaps
810                                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        // Assumes there is no swap chaining (send swap lockup tx = receive swap claim tx)
1019        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                // Index payments by both tx_id (lockup/claim) and refund_tx_id
1045                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                    // Use the lockup address if it's incoming, else use the claim address
1128                    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}