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 delete_payment_tx_data(&self, tx_id: &str) -> Result<()> {
417        let con = self.get_connection()?;
418
419        con.execute("DELETE FROM payment_tx_data WHERE tx_id = ?", [tx_id])?;
420
421        Ok(())
422    }
423
424    fn insert_or_update_payment_details_inner(
425        con: &Connection,
426        payment_tx_details: &PaymentTxDetails,
427        skip_destination_update: bool,
428    ) -> Result<()> {
429        let destination_update = if skip_destination_update.not() {
430            "destination = excluded.destination,"
431        } else {
432            Default::default()
433        };
434        con.execute(
435            &format!(
436                "INSERT INTO payment_details (
437                    tx_id,
438                    destination,
439                    description,
440                    lnurl_info_json,
441                    bip353_address,
442                    payer_note,
443                    asset_fees
444                )
445                VALUES (?, ?, ?, ?, ?, ?, ?)
446                ON CONFLICT (tx_id)
447                DO UPDATE SET
448                    {destination_update}
449                    description = COALESCE(excluded.description, description),
450                    lnurl_info_json = COALESCE(excluded.lnurl_info_json, lnurl_info_json),
451                    bip353_address = COALESCE(excluded.bip353_address, bip353_address),
452                    payer_note = COALESCE(excluded.payer_note, payer_note),
453                    asset_fees = COALESCE(excluded.asset_fees, asset_fees)
454            "
455            ),
456            (
457                &payment_tx_details.tx_id,
458                &payment_tx_details.destination,
459                &payment_tx_details.description,
460                payment_tx_details
461                    .lnurl_info
462                    .as_ref()
463                    .map(|info| serde_json::to_string(&info).ok()),
464                &payment_tx_details.bip353_address,
465                &payment_tx_details.payer_note,
466                from_optional_u64_to_row(&payment_tx_details.asset_fees)?,
467            ),
468        )?;
469        Ok(())
470    }
471
472    pub(crate) fn insert_or_update_payment_details(
473        &self,
474        payment_tx_details: PaymentTxDetails,
475    ) -> Result<()> {
476        let mut con = self.get_connection()?;
477        let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?;
478
479        Self::insert_or_update_payment_details_inner(&tx, &payment_tx_details, false)?;
480        self.commit_outgoing(
481            &tx,
482            &payment_tx_details.tx_id,
483            RecordType::PaymentDetails,
484            None,
485        )?;
486        tx.commit()?;
487        self.trigger_sync();
488
489        Ok(())
490    }
491
492    pub(crate) fn get_payment_details(&self, tx_id: &str) -> Result<Option<PaymentTxDetails>> {
493        let con = self.get_connection()?;
494        let mut stmt = con.prepare(
495            "SELECT destination, description, lnurl_info_json, bip353_address, payer_note, asset_fees
496            FROM payment_details
497            WHERE tx_id = ?",
498        )?;
499        let res = stmt.query_row([tx_id], |row| {
500            let destination = row.get(0)?;
501            let description = row.get(1)?;
502            let maybe_lnurl_info_json: Option<String> = row.get(2)?;
503            let maybe_bip353_address = row.get(3)?;
504            let maybe_payer_note = row.get(4)?;
505            let maybe_asset_fees = from_row_to_optional_u64(row, 5)?;
506            Ok(PaymentTxDetails {
507                tx_id: tx_id.to_string(),
508                destination,
509                description,
510                lnurl_info: maybe_lnurl_info_json
511                    .and_then(|info| serde_json::from_str::<LnUrlInfo>(&info).ok()),
512                bip353_address: maybe_bip353_address,
513                payer_note: maybe_payer_note,
514                asset_fees: maybe_asset_fees,
515            })
516        });
517        Ok(res.ok())
518    }
519
520    pub(crate) fn list_ongoing_swaps(&self) -> Result<Vec<Swap>> {
521        let ongoing_send_swaps: Vec<Swap> = self
522            .list_ongoing_send_swaps()?
523            .into_iter()
524            .map(Swap::Send)
525            .collect();
526        let ongoing_receive_swaps: Vec<Swap> = self
527            .list_ongoing_receive_swaps()?
528            .into_iter()
529            .map(Swap::Receive)
530            .collect();
531        let ongoing_chain_swaps: Vec<Swap> = self
532            .list_ongoing_chain_swaps()?
533            .into_iter()
534            .map(Swap::Chain)
535            .collect();
536        Ok([
537            ongoing_send_swaps,
538            ongoing_receive_swaps,
539            ongoing_chain_swaps,
540        ]
541        .concat())
542    }
543
544    fn select_payment_query(
545        &self,
546        where_clause: Option<&str>,
547        offset: Option<u32>,
548        limit: Option<u32>,
549        sort_ascending: Option<bool>,
550        include_all_states: Option<bool>,
551    ) -> String {
552        let (where_receive_swap_clause, where_chain_swap_clause) = if include_all_states
553            .unwrap_or_default()
554        {
555            ("true", "true")
556        } else {
557            (
558                // Receive Swap has a tx id and state not in Created, Failed, TimedOut
559                "COALESCE(claim_tx_id, lockup_tx_id, mrh_tx_id) IS NOT NULL AND state NOT IN (0, 3, 4)",
560                // Chain Swap has a tx id and state not in Created, TimedOut
561                "COALESCE(user_lockup_tx_id, claim_tx_id) IS NOT NULL AND state NOT IN (0, 4)",
562            )
563        };
564
565        format!(
566            "
567            SELECT
568                ptx.tx_id,
569                ptx.timestamp,
570                ptx.fees_sat,
571                ptx.is_confirmed,
572                ptx.unblinding_data,
573                pb.amount,
574                pb.asset_id,
575                pb.payment_type,
576                rs.id,
577                rs.created_at,
578                rs.timeout_block_height,
579                rs.invoice,
580                rs.bolt12_offer,
581                rs.payment_hash,
582                rs.destination_pubkey,
583                rs.description,
584                rs.payer_note,
585                rs.preimage,
586                rs.payer_amount_sat,
587                rs.receiver_amount_sat,
588                rs.state,
589                rs.pair_fees_json,
590                rs.claim_tx_id,
591                ss.id,
592                ss.created_at,
593                ss.timeout_block_height,
594                ss.invoice,
595                ss.bolt12_offer,
596                ss.payment_hash,
597                ss.destination_pubkey,
598                ss.description,
599                ss.preimage,
600                ss.refund_tx_id,
601                ss.payer_amount_sat,
602                ss.receiver_amount_sat,
603                ss.state,
604                ss.pair_fees_json,
605                cs.id,
606                cs.created_at,
607                cs.timeout_block_height,
608                cs.claim_timeout_block_height,
609                cs.direction,
610                cs.preimage,
611                cs.description,
612                cs.refund_tx_id,
613                cs.payer_amount_sat,
614                cs.receiver_amount_sat,
615                cs.claim_address,
616                cs.lockup_address,
617                cs.state,
618                cs.pair_fees_json,
619                cs.actual_payer_amount_sat,
620                cs.accepted_receiver_amount_sat,
621                cs.auto_accepted_fees,
622                cs.user_lockup_tx_id,
623                cs.claim_tx_id,
624                rb.amount,
625                pd.destination,
626                pd.description,
627                pd.lnurl_info_json,
628                pd.bip353_address,
629                pd.payer_note,
630                pd.asset_fees,
631                am.name,
632                am.ticker,
633                am.precision
634            FROM payment_tx_data AS ptx          -- Payment tx (each tx results in a Payment)
635            LEFT JOIN payment_balance AS pb
636                ON pb.tx_id = ptx.tx_id          -- Payment tx balances, split by asset
637            FULL JOIN (
638                SELECT * FROM receive_swaps WHERE {}
639            ) rs                                 -- Receive Swap data
640                ON ptx.tx_id in (rs.claim_tx_id, rs.mrh_tx_id)
641            FULL JOIN (
642                SELECT * FROM chain_swaps WHERE {}
643            ) cs                                 -- Chain Swap data
644                ON ptx.tx_id in (cs.user_lockup_tx_id, cs.claim_tx_id)
645            LEFT JOIN send_swaps AS ss           -- Send Swap data
646                ON ptx.tx_id = ss.lockup_tx_id
647            LEFT JOIN payment_balance AS rb      -- Refund tx balance
648                ON rb.tx_id in (ss.refund_tx_id, cs.refund_tx_id)
649            LEFT JOIN payment_details AS pd      -- Payment details
650                ON pd.tx_id = ptx.tx_id
651            LEFT JOIN asset_metadata AS am       -- Asset metadata
652                ON am.asset_id = pb.asset_id
653            WHERE
654                (ptx.tx_id IS NULL               -- Filter out refund txs from Chain/Send Swaps
655                    OR ptx.tx_id NOT IN (SELECT refund_tx_id FROM send_swaps WHERE refund_tx_id NOT NULL)
656                    AND ptx.tx_id NOT IN (SELECT refund_tx_id FROM chain_swaps WHERE refund_tx_id NOT NULL))
657            AND {}
658            ORDER BY                             -- Order by swap creation time or tx timestamp (in case of direct tx)
659                COALESCE(rs.created_at, ss.created_at, cs.created_at, ptx.timestamp) {}
660            LIMIT {}
661            OFFSET {}
662            ",
663            where_receive_swap_clause,
664            where_chain_swap_clause,
665            where_clause.unwrap_or("true"),
666            match sort_ascending.unwrap_or(false) {
667                true => "ASC",
668                false => "DESC",
669            },
670            limit.unwrap_or(u32::MAX),
671            offset.unwrap_or(0),
672        )
673    }
674
675    fn sql_row_to_payment(&self, row: &Row) -> Result<Payment, rusqlite::Error> {
676        let maybe_tx_tx_id: Result<String, rusqlite::Error> = row.get(0);
677        let tx_with_balance = match maybe_tx_tx_id {
678            Ok(ref tx_id) => Some((
679                PaymentTxData {
680                    tx_id: tx_id.to_string(),
681                    timestamp: row.get(1)?,
682                    fees_sat: from_row_to_u64(row, 2)?,
683                    is_confirmed: row.get(3)?,
684                    unblinding_data: row.get(4)?,
685                },
686                PaymentTxBalance {
687                    amount: from_row_to_u64(row, 5)?,
688                    asset_id: row.get(6)?,
689                    payment_type: row.get(7)?,
690                },
691            )),
692            _ => None,
693        };
694
695        let maybe_receive_swap_id: Option<String> = row.get(8)?;
696        let maybe_receive_swap_created_at: Option<u32> = row.get(9)?;
697        let maybe_receive_swap_timeout_block_height: Option<u32> = row.get(10)?;
698        let maybe_receive_swap_invoice: Option<String> = row.get(11)?;
699        let maybe_receive_swap_bolt12_offer: Option<String> = row.get(12)?;
700        let maybe_receive_swap_payment_hash: Option<String> = row.get(13)?;
701        let maybe_receive_swap_destination_pubkey: Option<String> = row.get(14)?;
702        let maybe_receive_swap_description: Option<String> = row.get(15)?;
703        let maybe_receive_swap_payer_note: Option<String> = row.get(16)?;
704        let maybe_receive_swap_preimage: Option<String> = row.get(17)?;
705        let maybe_receive_swap_payer_amount_sat = from_row_to_optional_u64(row, 18)?;
706        let maybe_receive_swap_receiver_amount_sat = from_row_to_optional_u64(row, 19)?;
707        let maybe_receive_swap_receiver_state: Option<PaymentState> = row.get(20)?;
708        let maybe_receive_swap_pair_fees_json: Option<String> = row.get(21)?;
709        let maybe_receive_swap_pair_fees: Option<ReversePair> =
710            maybe_receive_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok());
711        let maybe_receive_swap_claim_tx_id: Option<String> = row.get(22)?;
712
713        let maybe_send_swap_id: Option<String> = row.get(23)?;
714        let maybe_send_swap_created_at: Option<u32> = row.get(24)?;
715        let maybe_send_swap_timeout_block_height: Option<u32> = row.get(25)?;
716        let maybe_send_swap_invoice: Option<String> = row.get(26)?;
717        let maybe_send_swap_bolt12_offer: Option<String> = row.get(27)?;
718        let maybe_send_swap_payment_hash: Option<String> = row.get(28)?;
719        let maybe_send_swap_destination_pubkey: Option<String> = row.get(29)?;
720        let maybe_send_swap_description: Option<String> = row.get(30)?;
721        let maybe_send_swap_preimage: Option<String> = row.get(31)?;
722        let maybe_send_swap_refund_tx_id: Option<String> = row.get(32)?;
723        let maybe_send_swap_payer_amount_sat = from_row_to_optional_u64(row, 33)?;
724        let maybe_send_swap_receiver_amount_sat = from_row_to_optional_u64(row, 34)?;
725        let maybe_send_swap_state: Option<PaymentState> = row.get(35)?;
726        let maybe_send_swap_pair_fees_json: Option<String> = row.get(36)?;
727        let maybe_send_swap_pair_fees: Option<SubmarinePair> =
728            maybe_send_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok());
729
730        let maybe_chain_swap_id: Option<String> = row.get(37)?;
731        let maybe_chain_swap_created_at: Option<u32> = row.get(38)?;
732        let maybe_chain_swap_timeout_block_height: Option<u32> = row.get(39)?;
733        let maybe_chain_swap_claim_timeout_block_height: Option<u32> = row.get(40)?;
734        let maybe_chain_swap_direction: Option<Direction> = row.get(41)?;
735        let maybe_chain_swap_preimage: Option<String> = row.get(42)?;
736        let maybe_chain_swap_description: Option<String> = row.get(43)?;
737        let maybe_chain_swap_refund_tx_id: Option<String> = row.get(44)?;
738        let maybe_chain_swap_payer_amount_sat = from_row_to_optional_u64(row, 45)?;
739        let maybe_chain_swap_receiver_amount_sat = from_row_to_optional_u64(row, 46)?;
740        let maybe_chain_swap_claim_address: Option<String> = row.get(47)?;
741        let maybe_chain_swap_lockup_address: Option<String> = row.get(48)?;
742        let maybe_chain_swap_state: Option<PaymentState> = row.get(49)?;
743        let maybe_chain_swap_pair_fees_json: Option<String> = row.get(50)?;
744        let maybe_chain_swap_pair_fees: Option<ChainPair> =
745            maybe_chain_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok());
746        let maybe_chain_swap_actual_payer_amount_sat = from_row_to_optional_u64(row, 51)?;
747        let maybe_chain_swap_accepted_receiver_amount_sat = from_row_to_optional_u64(row, 52)?;
748        let maybe_chain_swap_auto_accepted_fees: Option<bool> = row.get(53)?;
749        let maybe_chain_swap_user_lockup_tx_id: Option<String> = row.get(54)?;
750        let maybe_chain_swap_claim_tx_id: Option<String> = row.get(55)?;
751
752        let maybe_swap_refund_tx_amount_sat = from_row_to_optional_u64(row, 56)?;
753
754        let maybe_payment_details_destination: Option<String> = row.get(57)?;
755        let maybe_payment_details_description: Option<String> = row.get(58)?;
756        let maybe_payment_details_lnurl_info_json: Option<String> = row.get(59)?;
757        let maybe_payment_details_lnurl_info: Option<LnUrlInfo> =
758            maybe_payment_details_lnurl_info_json.and_then(|info| serde_json::from_str(&info).ok());
759        let maybe_payment_details_bip353_address: Option<String> = row.get(60)?;
760        let maybe_payment_details_payer_note: Option<String> = row.get(61)?;
761        let maybe_payment_details_asset_fees = from_row_to_optional_u64(row, 62)?;
762
763        let maybe_asset_metadata_name: Option<String> = row.get(63)?;
764        let maybe_asset_metadata_ticker: Option<String> = row.get(64)?;
765        let maybe_asset_metadata_precision: Option<u8> = row.get(65)?;
766
767        let bitcoin_address = match maybe_chain_swap_direction {
768            Some(Direction::Incoming) => maybe_chain_swap_lockup_address,
769            Some(Direction::Outgoing) => maybe_chain_swap_claim_address,
770            None => None,
771        };
772
773        let (swap, payment_type) = match maybe_receive_swap_id {
774            Some(receive_swap_id) => {
775                let payer_amount_sat = maybe_receive_swap_payer_amount_sat.unwrap_or(0);
776
777                (
778                    Some(PaymentSwapData {
779                        swap_id: receive_swap_id,
780                        swap_type: PaymentSwapType::Receive,
781                        created_at: maybe_receive_swap_created_at.unwrap_or(utils::now()),
782                        expiration_blockheight: maybe_receive_swap_timeout_block_height
783                            .unwrap_or(0),
784                        claim_expiration_blockheight: None,
785                        preimage: maybe_receive_swap_preimage,
786                        invoice: maybe_receive_swap_invoice.clone(),
787                        bolt12_offer: maybe_receive_swap_bolt12_offer,
788                        payment_hash: maybe_receive_swap_payment_hash,
789                        destination_pubkey: maybe_receive_swap_destination_pubkey,
790                        description: maybe_receive_swap_description.unwrap_or_else(|| {
791                            maybe_receive_swap_invoice
792                                .and_then(|invoice| {
793                                    utils::get_invoice_description(&invoice).ok().flatten()
794                                })
795                                .unwrap_or("Lightning payment".to_string())
796                        }),
797                        payer_note: maybe_receive_swap_payer_note,
798                        payer_amount_sat,
799                        receiver_amount_sat: maybe_receive_swap_receiver_amount_sat.unwrap_or(0),
800                        swapper_fees_sat: maybe_receive_swap_pair_fees
801                            .map(|pair| pair.fees.boltz(payer_amount_sat))
802                            .unwrap_or(0),
803                        refund_tx_id: None,
804                        refund_tx_amount_sat: None,
805                        bitcoin_address: None,
806                        status: maybe_receive_swap_receiver_state.unwrap_or(PaymentState::Created),
807                    }),
808                    PaymentType::Receive,
809                )
810            }
811            None => match maybe_send_swap_id {
812                Some(send_swap_id) => {
813                    let receiver_amount_sat = maybe_send_swap_receiver_amount_sat.unwrap_or(0);
814                    (
815                        Some(PaymentSwapData {
816                            swap_id: send_swap_id,
817                            swap_type: PaymentSwapType::Send,
818                            created_at: maybe_send_swap_created_at.unwrap_or(utils::now()),
819                            expiration_blockheight: maybe_send_swap_timeout_block_height
820                                .unwrap_or(0),
821                            claim_expiration_blockheight: None,
822                            preimage: maybe_send_swap_preimage,
823                            invoice: maybe_send_swap_invoice,
824                            bolt12_offer: maybe_send_swap_bolt12_offer,
825                            payment_hash: maybe_send_swap_payment_hash,
826                            destination_pubkey: maybe_send_swap_destination_pubkey,
827                            description: maybe_send_swap_description
828                                .unwrap_or("Lightning payment".to_string()),
829                            payer_note: None,
830                            payer_amount_sat: maybe_send_swap_payer_amount_sat.unwrap_or(0),
831                            receiver_amount_sat,
832                            swapper_fees_sat: maybe_send_swap_pair_fees
833                                .map(|pair| pair.fees.boltz(receiver_amount_sat))
834                                .unwrap_or(0),
835                            refund_tx_id: maybe_send_swap_refund_tx_id,
836                            refund_tx_amount_sat: maybe_swap_refund_tx_amount_sat,
837                            bitcoin_address: None,
838                            status: maybe_send_swap_state.unwrap_or(PaymentState::Created),
839                        }),
840                        PaymentType::Send,
841                    )
842                }
843                None => match maybe_chain_swap_id {
844                    Some(chain_swap_id) => {
845                        let (payer_amount_sat, receiver_amount_sat) = match (
846                            maybe_chain_swap_actual_payer_amount_sat,
847                            maybe_chain_swap_payer_amount_sat,
848                        ) {
849                            // For amountless chain swaps use the actual payer amount when
850                            // set as the payer amount and receiver amount
851                            (Some(actual_payer_amount_sat), Some(0)) => {
852                                (actual_payer_amount_sat, actual_payer_amount_sat)
853                            }
854                            // Otherwise use the precalculated payer and receiver amounts
855                            _ => (
856                                maybe_chain_swap_payer_amount_sat.unwrap_or(0),
857                                maybe_chain_swap_receiver_amount_sat.unwrap_or(0),
858                            ),
859                        };
860                        let receiver_amount_sat =
861                            match maybe_chain_swap_accepted_receiver_amount_sat {
862                                // If the accepted receiver amount is set, use it
863                                Some(accepted_receiver_amount_sat) => accepted_receiver_amount_sat,
864                                None => receiver_amount_sat,
865                            };
866                        let swapper_fees_sat = maybe_chain_swap_pair_fees
867                            .map(|pair| pair.fees.percentage)
868                            .map(|fr| ((fr / 100.0) * payer_amount_sat as f64).ceil() as u64)
869                            .unwrap_or(0);
870
871                        (
872                            Some(PaymentSwapData {
873                                swap_id: chain_swap_id,
874                                swap_type: PaymentSwapType::Chain,
875                                created_at: maybe_chain_swap_created_at.unwrap_or(utils::now()),
876                                expiration_blockheight: maybe_chain_swap_timeout_block_height
877                                    .unwrap_or(0),
878                                claim_expiration_blockheight:
879                                    maybe_chain_swap_claim_timeout_block_height,
880                                preimage: maybe_chain_swap_preimage,
881                                invoice: None,
882                                bolt12_offer: None, // Bolt12 not supported for Chain Swaps
883                                payment_hash: None,
884                                destination_pubkey: None,
885                                description: maybe_chain_swap_description
886                                    .unwrap_or("Bitcoin transfer".to_string()),
887                                payer_note: None,
888                                payer_amount_sat,
889                                receiver_amount_sat,
890                                swapper_fees_sat,
891                                refund_tx_id: maybe_chain_swap_refund_tx_id,
892                                refund_tx_amount_sat: maybe_swap_refund_tx_amount_sat,
893                                bitcoin_address: bitcoin_address.clone(),
894                                status: maybe_chain_swap_state.unwrap_or(PaymentState::Created),
895                            }),
896                            maybe_chain_swap_direction
897                                .unwrap_or(Direction::Outgoing)
898                                .into(),
899                        )
900                    }
901                    None => (None, PaymentType::Send),
902                },
903            },
904        };
905
906        let maybe_claim_tx_id = maybe_receive_swap_claim_tx_id.or(maybe_chain_swap_claim_tx_id);
907        let description = swap.as_ref().map(|s| s.description.clone());
908        let payment_details = match swap.clone() {
909            Some(
910                PaymentSwapData {
911                    swap_type: PaymentSwapType::Receive,
912                    swap_id,
913                    invoice,
914                    bolt12_offer,
915                    payment_hash,
916                    destination_pubkey,
917                    payer_note,
918                    refund_tx_id,
919                    preimage,
920                    refund_tx_amount_sat,
921                    expiration_blockheight,
922                    ..
923                }
924                | PaymentSwapData {
925                    swap_type: PaymentSwapType::Send,
926                    swap_id,
927                    invoice,
928                    bolt12_offer,
929                    payment_hash,
930                    destination_pubkey,
931                    payer_note,
932                    preimage,
933                    refund_tx_id,
934                    refund_tx_amount_sat,
935                    expiration_blockheight,
936                    ..
937                },
938            ) => PaymentDetails::Lightning {
939                swap_id,
940                preimage,
941                invoice: invoice.clone(),
942                bolt12_offer: bolt12_offer.clone(),
943                payment_hash,
944                destination_pubkey: destination_pubkey.or_else(|| {
945                    invoice.and_then(|invoice| {
946                        utils::get_invoice_destination_pubkey(&invoice, bolt12_offer.is_some()).ok()
947                    })
948                }),
949                lnurl_info: maybe_payment_details_lnurl_info,
950                bip353_address: maybe_payment_details_bip353_address,
951                payer_note: payer_note.or(maybe_payment_details_payer_note),
952                claim_tx_id: maybe_claim_tx_id,
953                refund_tx_id,
954                refund_tx_amount_sat,
955                description: maybe_payment_details_description
956                    .unwrap_or(description.unwrap_or("Lightning transfer".to_string())),
957                liquid_expiration_blockheight: expiration_blockheight,
958            },
959            Some(PaymentSwapData {
960                swap_type: PaymentSwapType::Chain,
961                swap_id,
962                refund_tx_id,
963                refund_tx_amount_sat,
964                expiration_blockheight,
965                claim_expiration_blockheight,
966                ..
967            }) => {
968                let (bitcoin_expiration_blockheight, liquid_expiration_blockheight) =
969                    match maybe_chain_swap_direction {
970                        Some(Direction::Incoming) => (
971                            expiration_blockheight,
972                            claim_expiration_blockheight.unwrap_or_default(),
973                        ),
974                        Some(Direction::Outgoing) | None => (
975                            claim_expiration_blockheight.unwrap_or_default(),
976                            expiration_blockheight,
977                        ),
978                    };
979                let auto_accepted_fees = maybe_chain_swap_auto_accepted_fees.unwrap_or(false);
980
981                PaymentDetails::Bitcoin {
982                    swap_id,
983                    bitcoin_address: bitcoin_address.unwrap_or_default(),
984                    lockup_tx_id: maybe_chain_swap_user_lockup_tx_id,
985                    claim_tx_id: maybe_claim_tx_id,
986                    refund_tx_id,
987                    refund_tx_amount_sat,
988                    description: description.unwrap_or("Bitcoin transfer".to_string()),
989                    liquid_expiration_blockheight,
990                    bitcoin_expiration_blockheight,
991                    auto_accepted_fees,
992                }
993            }
994            _ => {
995                let (amount, asset_id) = tx_with_balance.clone().map_or(
996                    (0, utils::lbtc_asset_id(self.network).to_string()),
997                    |(_, b)| (b.amount, b.asset_id),
998                );
999                let asset_info = match (
1000                    maybe_asset_metadata_name,
1001                    maybe_asset_metadata_ticker,
1002                    maybe_asset_metadata_precision,
1003                ) {
1004                    (Some(name), Some(ticker), Some(precision)) => {
1005                        let asset_metadata = AssetMetadata {
1006                            asset_id: asset_id.clone(),
1007                            name: name.clone(),
1008                            ticker: ticker.clone(),
1009                            precision,
1010                            fiat_id: None,
1011                        };
1012                        let (amount, fees) =
1013                            maybe_payment_details_asset_fees.map_or((amount, None), |fees| {
1014                                (
1015                                    amount.saturating_sub(fees),
1016                                    Some(asset_metadata.amount_from_sat(fees)),
1017                                )
1018                            });
1019
1020                        Some(AssetInfo {
1021                            name,
1022                            ticker,
1023                            amount: asset_metadata.amount_from_sat(amount),
1024                            fees,
1025                        })
1026                    }
1027                    _ => None,
1028                };
1029
1030                PaymentDetails::Liquid {
1031                    destination: maybe_payment_details_destination
1032                        .unwrap_or("Destination unknown".to_string()),
1033                    description: maybe_payment_details_description
1034                        .unwrap_or("Liquid transfer".to_string()),
1035                    asset_id,
1036                    asset_info,
1037                    lnurl_info: maybe_payment_details_lnurl_info,
1038                    bip353_address: maybe_payment_details_bip353_address,
1039                    payer_note: maybe_payment_details_payer_note,
1040                }
1041            }
1042        };
1043
1044        match (tx_with_balance, swap.clone()) {
1045            (None, None) => Err(maybe_tx_tx_id.err().unwrap()),
1046            (None, Some(swap)) => Ok(Payment::from_pending_swap(
1047                swap,
1048                payment_type,
1049                payment_details,
1050            )),
1051            (Some((tx, balance)), None) => {
1052                Ok(Payment::from_tx_data(tx, balance, None, payment_details))
1053            }
1054            (Some((tx, balance)), Some(swap)) => Ok(Payment::from_tx_data(
1055                tx,
1056                balance,
1057                Some(swap),
1058                payment_details,
1059            )),
1060        }
1061    }
1062
1063    pub fn get_payment(&self, id: &str) -> Result<Option<Payment>> {
1064        Ok(self
1065            .get_connection()?
1066            .query_row(
1067                &self.select_payment_query(
1068                    Some("(ptx.tx_id = ?1 OR COALESCE(rs.id, ss.id, cs.id) = ?1)"),
1069                    None,
1070                    None,
1071                    None,
1072                    None,
1073                ),
1074                params![id],
1075                |row| self.sql_row_to_payment(row),
1076            )
1077            .optional()?)
1078    }
1079
1080    pub fn get_payment_by_request(&self, req: &GetPaymentRequest) -> Result<Option<Payment>> {
1081        let (where_clause, param) = match req {
1082            GetPaymentRequest::PaymentHash { payment_hash } => (
1083                "(rs.payment_hash = ?1 OR ss.payment_hash = ?1)",
1084                payment_hash,
1085            ),
1086            GetPaymentRequest::SwapId { swap_id } => (
1087                "(rs.id = ?1 OR ss.id = ?1 OR cs.id = ?1 OR \
1088                rs.id_hash = ?1 OR ss.id_hash = ?1 OR cs.id_hash = ?1)",
1089                swap_id,
1090            ),
1091        };
1092        Ok(self
1093            .get_connection()?
1094            .query_row(
1095                &self.select_payment_query(Some(where_clause), None, None, None, Some(true)),
1096                params![param],
1097                |row| self.sql_row_to_payment(row),
1098            )
1099            .optional()?)
1100    }
1101
1102    pub fn get_payments(&self, req: &ListPaymentsRequest) -> Result<Vec<Payment>> {
1103        let (where_clause, where_params) = filter_to_where_clause(req);
1104        let maybe_where_clause = match where_clause.is_empty() {
1105            false => Some(where_clause.as_str()),
1106            true => None,
1107        };
1108
1109        // Assumes there is no swap chaining (send swap lockup tx = receive swap claim tx)
1110        let con = self.get_connection()?;
1111        let mut stmt = con.prepare(&self.select_payment_query(
1112            maybe_where_clause,
1113            req.offset,
1114            req.limit,
1115            req.sort_ascending,
1116            None,
1117        ))?;
1118        let payments: Vec<Payment> = stmt
1119            .query_map(params_from_iter(where_params), |row| {
1120                self.sql_row_to_payment(row)
1121            })?
1122            .map(|i| i.unwrap())
1123            .collect();
1124        Ok(payments)
1125    }
1126
1127    pub fn get_payments_by_tx_id(
1128        &self,
1129        req: &ListPaymentsRequest,
1130    ) -> Result<HashMap<String, Payment>> {
1131        let res: HashMap<String, Payment> = self
1132            .get_payments(req)?
1133            .into_iter()
1134            .flat_map(|payment| {
1135                // Index payments by both tx_id (lockup/claim) and refund_tx_id
1136                let mut res = vec![];
1137                if let Some(tx_id) = payment.tx_id.clone() {
1138                    res.push((tx_id, payment.clone()));
1139                }
1140                if let Some(refund_tx_id) = payment.get_refund_tx_id() {
1141                    res.push((refund_tx_id, payment));
1142                }
1143                res
1144            })
1145            .collect();
1146        Ok(res)
1147    }
1148}
1149
1150fn filter_to_where_clause(req: &ListPaymentsRequest) -> (String, Vec<Box<dyn ToSql + '_>>) {
1151    let mut where_clause: Vec<String> = Vec::new();
1152    let mut where_params: Vec<Box<dyn ToSql>> = Vec::new();
1153
1154    if let Some(t) = req.from_timestamp {
1155        where_clause.push("coalesce(ptx.timestamp, rs.created_at) >= ?".to_string());
1156        where_params.push(Box::new(t));
1157    };
1158    if let Some(t) = req.to_timestamp {
1159        where_clause.push("coalesce(ptx.timestamp, rs.created_at) <= ?".to_string());
1160        where_params.push(Box::new(t));
1161    };
1162
1163    if let Some(filters) = &req.filters {
1164        if !filters.is_empty() {
1165            let mut type_filter_clause: HashSet<i8> = HashSet::new();
1166
1167            for type_filter in filters {
1168                type_filter_clause.insert(*type_filter as i8);
1169            }
1170
1171            where_clause.push(format!(
1172                "pb.payment_type in ({})",
1173                type_filter_clause
1174                    .iter()
1175                    .map(|t| format!("{t}"))
1176                    .collect::<Vec<_>>()
1177                    .join(", ")
1178            ));
1179        }
1180    }
1181
1182    if let Some(states) = &req.states {
1183        if !states.is_empty() {
1184            let deduped_states: Vec<PaymentState> = states
1185                .clone()
1186                .into_iter()
1187                .collect::<HashSet<PaymentState>>()
1188                .into_iter()
1189                .collect();
1190            let states_param = deduped_states
1191                .iter()
1192                .map(|t| (*t as i8).to_string())
1193                .collect::<Vec<_>>()
1194                .join(", ");
1195            let tx_comfirmed_param = deduped_states
1196                .iter()
1197                .filter_map(|state| match state {
1198                    PaymentState::Pending | PaymentState::Complete => {
1199                        Some(((*state == PaymentState::Complete) as i8).to_string())
1200                    }
1201                    _ => None,
1202                })
1203                .collect::<Vec<_>>()
1204                .join(", ");
1205            let states_query = match tx_comfirmed_param.is_empty() {
1206                true => format!("COALESCE(rs.state, ss.state, cs.state) in ({states_param})"),
1207                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}))"),
1208            };
1209            where_clause.push(states_query);
1210        }
1211    }
1212
1213    if let Some(details) = &req.details {
1214        match details {
1215            ListPaymentDetails::Bitcoin { address } => {
1216                where_clause.push("cs.id IS NOT NULL".to_string());
1217                if let Some(address) = address {
1218                    // Use the lockup address if it's incoming, else use the claim address
1219                    where_clause.push(
1220                        "(cs.direction = 0 AND cs.lockup_address = ? OR cs.direction = 1 AND cs.claim_address = ?)"
1221                            .to_string(),
1222                    );
1223                    where_params.push(Box::new(address));
1224                    where_params.push(Box::new(address));
1225                }
1226            }
1227            ListPaymentDetails::Liquid {
1228                asset_id,
1229                destination,
1230            } => {
1231                where_clause.push("COALESCE(rs.id, ss.id, cs.id) IS NULL".to_string());
1232                if let Some(asset_id) = asset_id {
1233                    where_clause.push("pb.asset_id = ?".to_string());
1234                    where_params.push(Box::new(asset_id));
1235                }
1236                if let Some(destination) = destination {
1237                    where_clause.push("pd.destination = ?".to_string());
1238                    where_params.push(Box::new(destination));
1239                }
1240            }
1241        }
1242    }
1243
1244    (where_clause.join(" and "), where_params)
1245}
1246
1247#[cfg(test)]
1248mod tests {
1249    use anyhow::Result;
1250
1251    use crate::{
1252        model::LiquidNetwork,
1253        persist::PaymentTxDetails,
1254        prelude::ListPaymentsRequest,
1255        test_utils::persist::{
1256            create_persister, new_payment_tx_data, new_receive_swap, new_send_swap,
1257        },
1258    };
1259
1260    use super::{PaymentState, PaymentType};
1261
1262    #[cfg(feature = "browser-tests")]
1263    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1264
1265    #[sdk_macros::test_all]
1266    fn test_get_payments() -> Result<()> {
1267        create_persister!(storage);
1268
1269        let (payment_tx_data, payment_tx_balance) =
1270            new_payment_tx_data(LiquidNetwork::Testnet, PaymentType::Send);
1271        storage.insert_or_update_payment(
1272            payment_tx_data.clone(),
1273            &[payment_tx_balance],
1274            Some(PaymentTxDetails {
1275                destination: "mock-address".to_string(),
1276                ..Default::default()
1277            }),
1278            false,
1279        )?;
1280
1281        assert!(!storage
1282            .get_payments(&ListPaymentsRequest {
1283                ..Default::default()
1284            })?
1285            .is_empty());
1286        assert!(storage.get_payment(&payment_tx_data.tx_id)?.is_some());
1287
1288        Ok(())
1289    }
1290
1291    #[sdk_macros::test_all]
1292    fn test_list_ongoing_swaps() -> Result<()> {
1293        create_persister!(storage);
1294
1295        storage.insert_or_update_send_swap(&new_send_swap(None, None))?;
1296        storage
1297            .insert_or_update_receive_swap(&new_receive_swap(Some(PaymentState::Pending), None))?;
1298
1299        assert_eq!(storage.list_ongoing_swaps()?.len(), 2);
1300
1301        Ok(())
1302    }
1303}
1304
1305#[cfg(feature = "test-utils")]
1306pub mod test_helpers {
1307    use super::*;
1308
1309    impl Persister {
1310        pub fn test_insert_or_update_send_swap(&self, swap: &SendSwap) -> Result<()> {
1311            self.insert_or_update_send_swap(swap)
1312        }
1313
1314        pub fn test_insert_or_update_receive_swap(&self, swap: &ReceiveSwap) -> Result<()> {
1315            self.insert_or_update_receive_swap(swap)
1316        }
1317
1318        pub fn test_list_ongoing_swaps(&self) -> Result<Vec<Swap>> {
1319            self.list_ongoing_swaps()
1320        }
1321    }
1322}