breez_sdk_liquid/persist/
mod.rs

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