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