breez_sdk_liquid/persist/
mod.rs

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