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