breez_sdk_core/persist/
swap.rs

1use rusqlite::{named_params, OptionalExtension, Params, Row, Transaction, TransactionBehavior};
2
3use crate::{
4    models::{OpeningFeeParams, SwapInfo, SwapStatus},
5    ListSwapsRequest,
6};
7
8use super::{
9    db::{SqliteStorage, StringArray},
10    error::{PersistError, PersistResult},
11};
12
13#[derive(Debug, Clone)]
14pub(crate) struct SwapChainInfo {
15    pub(crate) unconfirmed_sats: u64,
16    pub(crate) unconfirmed_tx_ids: Vec<String>,
17    pub(crate) confirmed_sats: u64,
18    pub(crate) confirmed_tx_ids: Vec<String>,
19    pub(crate) confirmed_at: Option<u32>,
20    pub(crate) total_incoming_txs: u64,
21}
22
23impl SqliteStorage {
24    pub(crate) fn insert_swap(&self, swap_info: SwapInfo) -> PersistResult<()> {
25        let mut con = self.get_connection()?;
26        let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?;
27
28        tx.execute("
29         INSERT INTO sync.swaps (
30           bitcoin_address, 
31           created_at, 
32           lock_height, 
33           payment_hash, 
34           preimage, 
35           private_key, 
36           public_key, 
37           swapper_public_key, 
38           script,
39           min_allowed_deposit, 
40           max_allowed_deposit,
41           max_swapper_payable
42         )
43         VALUES (:bitcoin_address, :created_at, :lock_height, :payment_hash, :preimage, :private_key, :public_key, :swapper_public_key, :script, :min_allowed_deposit, :max_allowed_deposit, :max_swapper_payable)",
44         named_params! {
45             ":bitcoin_address": swap_info.bitcoin_address,
46             ":created_at": swap_info.created_at,
47             ":lock_height": swap_info.lock_height,
48             ":payment_hash": swap_info.payment_hash,
49             ":preimage": swap_info.preimage,
50             ":private_key": swap_info.private_key,
51             ":public_key": swap_info.public_key,
52             ":swapper_public_key": swap_info.swapper_public_key,            
53             ":script": swap_info.script,             
54             ":min_allowed_deposit": swap_info.min_allowed_deposit,
55             ":max_allowed_deposit": swap_info.max_allowed_deposit,
56             ":max_swapper_payable": swap_info.max_swapper_payable,
57         },
58        )?;
59
60        tx.execute(
61            "
62        INSERT INTO swaps_info (
63          bitcoin_address, 
64          status,
65          bolt11,
66          paid_msat, 
67          unconfirmed_sats, 
68          unconfirmed_tx_ids, 
69          confirmed_sats,
70          confirmed_tx_ids,
71          confirmed_at,
72          total_incoming_txs
73        ) VALUES (:bitcoin_address, :status, :bolt11, :paid_msat, :unconfirmed_sats, :unconfirmed_tx_ids, :confirmed_sats, :confirmed_tx_ids, :confirmed_at, :total_incoming_txs)",
74            named_params! {
75               ":bitcoin_address": swap_info.bitcoin_address,
76               ":status": swap_info.status as i32,
77               ":bolt11": None::<String>,
78               ":paid_msat": swap_info.paid_msat,
79               ":unconfirmed_sats": swap_info.unconfirmed_sats,
80               ":unconfirmed_tx_ids": StringArray(swap_info.unconfirmed_tx_ids),
81               ":confirmed_sats": swap_info.confirmed_sats,
82               ":confirmed_tx_ids": StringArray(swap_info.confirmed_tx_ids),
83               ":confirmed_at": swap_info.confirmed_at,
84               ":total_incoming_txs": swap_info.total_incoming_txs,
85            },
86        )?;
87
88        Self::insert_swaps_fees(
89            &tx,
90            swap_info.bitcoin_address,
91            swap_info.channel_opening_fees.ok_or_else(|| {
92                PersistError::generic("Dynamic fees must be set when creating a new swap")
93            })?,
94        )?;
95
96        tx.commit()?;
97        Ok(())
98    }
99
100    pub(crate) fn update_swap_paid_amount(
101        &self,
102        bitcoin_address: String,
103        paid_msat: u64,
104        status: SwapStatus,
105    ) -> PersistResult<()> {
106        self.get_connection()?.execute(
107            "UPDATE swaps_info SET paid_msat=:paid_msat, status=:status where bitcoin_address=:bitcoin_address",
108            named_params! {
109             ":paid_msat": paid_msat,
110             ":bitcoin_address": bitcoin_address,
111             ":status": status as u32,
112            },
113        )?;
114        Ok(())
115    }
116
117    pub(crate) fn update_swap_max_allowed_deposit(
118        &self,
119        bitcoin_address: String,
120        max_allowed_deposit: i64,
121    ) -> PersistResult<()> {
122        self.get_connection()?.execute(
123            "UPDATE sync.swaps SET max_allowed_deposit=:max_allowed_deposit where bitcoin_address=:bitcoin_address",
124            named_params! {
125             ":max_allowed_deposit": max_allowed_deposit,
126             ":bitcoin_address": bitcoin_address,
127            },
128        )?;
129
130        Ok(())
131    }
132
133    pub(crate) fn update_swap_redeem_error(
134        &self,
135        bitcoin_address: String,
136        redeem_err: String,
137    ) -> PersistResult<()> {
138        self.get_connection()?.execute(
139            "UPDATE swaps_info SET last_redeem_error=:redeem_err where bitcoin_address=:bitcoin_address",
140            named_params! {
141             ":redeem_err": redeem_err,
142             ":bitcoin_address": bitcoin_address,
143            },
144        )?;
145
146        Ok(())
147    }
148
149    pub(crate) fn update_swap_bolt11(
150        &self,
151        bitcoin_address: String,
152        bolt11: String,
153    ) -> PersistResult<()> {
154        self.get_connection()?.execute(
155            "UPDATE swaps_info SET bolt11=:bolt11 where bitcoin_address=:bitcoin_address",
156            named_params! {
157             ":bolt11": bolt11,
158             ":bitcoin_address": bitcoin_address,
159            },
160        )?;
161
162        Ok(())
163    }
164
165    fn insert_swaps_fees(
166        tx: &Transaction,
167        bitcoin_address: String,
168        channel_opening_fees: OpeningFeeParams,
169    ) -> PersistResult<()> {
170        tx.execute(
171            "INSERT OR REPLACE INTO sync.swaps_fees (bitcoin_address, created_at, channel_opening_fees) VALUES(:bitcoin_address, CURRENT_TIMESTAMP, :channel_opening_fees)",
172            named_params! {
173             ":bitcoin_address": bitcoin_address,
174             ":channel_opening_fees": channel_opening_fees,
175            },
176        )?;
177
178        Ok(())
179    }
180
181    /// Update the dynamic fees associated with a swap
182    pub(crate) fn update_swap_fees(
183        &self,
184        bitcoin_address: String,
185        channel_opening_fees: OpeningFeeParams,
186    ) -> PersistResult<()> {
187        let mut con = self.get_connection()?;
188        let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?;
189
190        Self::insert_swaps_fees(&tx, bitcoin_address, channel_opening_fees)?;
191
192        tx.commit()?;
193        Ok(())
194    }
195
196    pub(crate) fn insert_swap_refund_tx_ids(
197        &self,
198        bitcoin_address: String,
199        refund_tx_id: String,
200    ) -> PersistResult<()> {
201        self.get_connection()?.execute(
202            "INSERT OR IGNORE INTO sync.swap_refunds (bitcoin_address, refund_tx_id) VALUES(:bitcoin_address, :refund_tx_id)",
203            named_params! {
204             ":bitcoin_address": bitcoin_address,
205             ":refund_tx_id": refund_tx_id,
206            },
207        )?;
208
209        Ok(())
210    }
211
212    pub(crate) fn update_swap_chain_info(
213        &self,
214        bitcoin_address: String,
215        chain_info: SwapChainInfo,
216        status: SwapStatus,
217    ) -> PersistResult<SwapInfo> {
218        self.get_connection()?.execute(
219            "UPDATE swaps_info SET total_incoming_txs=:total_incoming_txs, unconfirmed_sats=:unconfirmed_sats, unconfirmed_tx_ids=:unconfirmed_tx_ids, confirmed_sats=:confirmed_sats, confirmed_tx_ids=:confirmed_tx_ids, status=:status, confirmed_at=:confirmed_at where bitcoin_address=:bitcoin_address",
220            named_params! {
221             ":unconfirmed_sats": chain_info.unconfirmed_sats,
222             ":unconfirmed_tx_ids": StringArray(chain_info.unconfirmed_tx_ids),
223             ":confirmed_sats": chain_info.confirmed_sats,
224             ":bitcoin_address": bitcoin_address,             
225             ":confirmed_tx_ids": StringArray(chain_info.confirmed_tx_ids),
226             ":status": status as u32,
227             ":confirmed_at": chain_info.confirmed_at,
228             ":total_incoming_txs": chain_info.total_incoming_txs,
229            },
230        )?;
231        Ok(self.get_swap_info_by_address(bitcoin_address)?.unwrap())
232    }
233    //(SELECT json_group_array(value) FROM json_each(json_group_array(refund_tx_id)) WHERE refund_tx_id is not null) as refund_tx_ids,
234    pub(crate) fn select_swap_query(&self, where_clause: &str, prefix: &str) -> String {
235        let swap_fields = format!("        
236          swaps.bitcoin_address  as {prefix}bitcoin_address,
237          swaps.created_at as {prefix}created_at,
238          lock_height as {prefix}lock_height,
239          payment_hash as {prefix}payment_hash,
240          preimage as {prefix}preimage,
241          private_key as {prefix}private_key,
242          public_key as {prefix}public_key,
243          swapper_public_key as {prefix}swapper_public_key,
244          script as {prefix}script,
245          min_allowed_deposit as {prefix}min_allowed_deposit,
246          max_allowed_deposit as {prefix}max_allowed_deposit,
247          max_swapper_payable as {prefix}max_swapper_payable,
248          bolt11 as {prefix}bolt11,
249          paid_msat as {prefix}paid_msat,
250          unconfirmed_sats as {prefix}unconfirmed_sats,
251          confirmed_sats as {prefix}confirmed_sats,
252          total_incoming_txs as {prefix}total_incoming_txs,
253          status as {prefix}status,             
254          (SELECT json_group_array(refund_tx_id) FROM sync.swap_refunds as swap_refunds where bitcoin_address = swaps.bitcoin_address) as {prefix}refund_tx_ids,
255          unconfirmed_tx_ids as {prefix}unconfirmed_tx_ids,
256          confirmed_tx_ids as {prefix}confirmed_tx_ids,
257          last_redeem_error as {prefix}last_redeem_error,
258          swaps_fees.channel_opening_fees as {prefix}channel_opening_fees,
259          swaps_info.confirmed_at as {prefix}confirmed_at          
260        ");
261
262        format!(
263            "
264            SELECT
265             {swap_fields}
266            FROM sync.swaps as swaps
267             LEFT JOIN swaps_info ON swaps.bitcoin_address = swaps_info.bitcoin_address
268             LEFT JOIN sync.swaps_fees as swaps_fees ON swaps.bitcoin_address = swaps_fees.bitcoin_address
269             LEFT JOIN sync.swap_refunds as swap_refunds ON swaps.bitcoin_address = swap_refunds.bitcoin_address
270            WHERE {}
271            ",
272            where_clause
273        )
274    }
275
276    pub(crate) fn select_swap_fields(&self, prefix: &str) -> String {
277        format!(
278            "        
279          {prefix}bitcoin_address,
280          {prefix}created_at,
281          {prefix}lock_height,
282          {prefix}payment_hash,
283          {prefix}preimage,
284          {prefix}private_key,
285          {prefix}public_key,
286          {prefix}swapper_public_key,
287          {prefix}script,
288          {prefix}min_allowed_deposit,
289          {prefix}max_allowed_deposit,
290          {prefix}max_swapper_payable,
291          {prefix}bolt11,
292          {prefix}paid_msat,
293          {prefix}unconfirmed_sats,
294          {prefix}confirmed_sats,
295          {prefix}total_incoming_txs,
296          {prefix}status,             
297          {prefix}refund_tx_ids,
298          {prefix}unconfirmed_tx_ids,
299          {prefix}confirmed_tx_ids,
300          {prefix}last_redeem_error,
301          {prefix}channel_opening_fees,
302          {prefix}confirmed_at          
303          "
304        )
305    }
306
307    fn select_single_swap<P>(
308        &self,
309        where_clause: &str,
310        params: P,
311    ) -> PersistResult<Option<SwapInfo>>
312    where
313        P: Params,
314    {
315        Ok(self
316            .get_connection()?
317            .query_row(&self.select_swap_query(where_clause, ""), params, |row| {
318                self.sql_row_to_swap(row, "")
319            })
320            .optional()?)
321    }
322
323    pub(crate) fn get_swap_info_by_hash(&self, hash: &Vec<u8>) -> PersistResult<Option<SwapInfo>> {
324        self.select_single_swap("payment_hash = ?1", [hash])
325    }
326
327    pub(crate) fn get_swap_info_by_address(
328        &self,
329        address: String,
330    ) -> PersistResult<Option<SwapInfo>> {
331        self.select_single_swap("swaps.bitcoin_address = ?1", [address])
332    }
333
334    pub(crate) fn list_swaps(&self, req: ListSwapsRequest) -> PersistResult<Vec<SwapInfo>> {
335        let con = self.get_connection()?;
336        let mut where_clauses = Vec::new();
337        if let Some(status) = req.status {
338            if status.is_empty() {
339                return Ok(Vec::new());
340            }
341
342            where_clauses.push(format!(
343                "status in ({})",
344                status
345                    .into_iter()
346                    .map(|s| (s as u32).to_string())
347                    .collect::<Vec<_>>()
348                    .join(",")
349            ));
350        }
351
352        if let Some(from_timestamp) = req.from_timestamp {
353            where_clauses.push(format!("created_at >= {}", from_timestamp));
354        }
355
356        if let Some(to_timestamp) = req.to_timestamp {
357            where_clauses.push(format!("created_at < {}", to_timestamp));
358        }
359
360        let where_clause = match where_clauses.is_empty() {
361            true => String::from("true"),
362            false => where_clauses.join(" AND "),
363        };
364
365        let mut query = self.select_swap_query(&where_clause, "");
366
367        match req.limit {
368            Some(limit) => query.push_str(&format!("LIMIT {}\n", limit)),
369            None => query.push_str("LIMIT -1\n"),
370        }
371
372        if let Some(offset) = req.offset {
373            query.push_str(&format!("OFFSET {}\n", offset));
374        }
375
376        let mut stmt = con.prepare(&query)?;
377
378        let vec: Vec<SwapInfo> = stmt
379            .query_map([], |row| self.sql_row_to_swap(row, ""))?
380            .map(|i| i.unwrap())
381            .collect();
382
383        Ok(vec)
384    }
385
386    pub(crate) fn sql_row_to_swap(
387        &self,
388        row: &Row,
389        prefix: &str,
390    ) -> PersistResult<SwapInfo, rusqlite::Error> {
391        let status: i32 = row
392            .get::<&str, Option<i32>>(format!("{prefix}status").as_str())?
393            .unwrap_or(SwapStatus::Initial as i32);
394        let status: SwapStatus = status.try_into().unwrap_or(SwapStatus::Initial);
395        let refund_txs_raw: String = row
396            .get::<&str, Option<String>>(format!("{prefix}refund_tx_ids").as_str())?
397            .unwrap_or("[]".to_string());
398        let refund_tx_ids: Vec<String> = serde_json::from_str(refund_txs_raw.as_str()).unwrap();
399        // let t: Vec<String> =
400        //     serde_json::from_value(refund_txs_raw).map_err(|e| FromSqlError::InvalidType)?;
401
402        let unconfirmed_tx_ids: StringArray = row
403            .get::<&str, Option<StringArray>>(format!("{prefix}unconfirmed_tx_ids").as_str())?
404            .unwrap_or(StringArray(vec![]));
405        let confirmed_txs_raw: StringArray = row
406            .get::<&str, Option<StringArray>>(format!("{prefix}confirmed_tx_ids").as_str())?
407            .unwrap_or(StringArray(vec![]));
408        let bitcoin_address = row.get(format!("{prefix}bitcoin_address").as_str())?;
409        Ok(SwapInfo {
410            bitcoin_address,
411            created_at: row.get(format!("{prefix}created_at").as_str())?,
412            lock_height: row.get(format!("{prefix}lock_height").as_str())?,
413            payment_hash: row.get(format!("{prefix}payment_hash").as_str())?,
414            preimage: row.get(format!("{prefix}preimage").as_str())?,
415            private_key: row.get(format!("{prefix}private_key").as_str())?,
416            public_key: row.get(format!("{prefix}public_key").as_str())?,
417            swapper_public_key: row.get(format!("{prefix}swapper_public_key").as_str())?,
418            script: row.get(format!("{prefix}script").as_str())?,
419            bolt11: row.get(format!("{prefix}bolt11").as_str())?,
420            paid_msat: row
421                .get::<&str, Option<u64>>(format!("{prefix}paid_msat").as_str())?
422                .unwrap_or_default(),
423            unconfirmed_sats: row
424                .get::<&str, Option<u64>>(format!("{prefix}unconfirmed_sats").as_str())?
425                .unwrap_or_default(),
426            confirmed_sats: row
427                .get::<&str, Option<u64>>(format!("{prefix}confirmed_sats").as_str())?
428                .unwrap_or_default(),
429            total_incoming_txs: row
430                .get::<&str, Option<u64>>(format!("{prefix}total_incoming_txs").as_str())?
431                .unwrap_or_default(),
432            status,
433            refund_tx_ids,
434            unconfirmed_tx_ids: unconfirmed_tx_ids.0,
435            confirmed_tx_ids: confirmed_txs_raw.0,
436            min_allowed_deposit: row.get(format!("{prefix}min_allowed_deposit").as_str())?,
437            max_allowed_deposit: row.get(format!("{prefix}max_allowed_deposit").as_str())?,
438            max_swapper_payable: row.get(format!("{prefix}max_swapper_payable").as_str())?,
439            last_redeem_error: row.get(format!("{prefix}last_redeem_error").as_str())?,
440            channel_opening_fees: row.get(format!("{prefix}channel_opening_fees").as_str())?,
441            confirmed_at: row.get(format!("{prefix}confirmed_at").as_str())?,
442        })
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use crate::persist::db::SqliteStorage;
449    use crate::persist::error::PersistResult;
450    use crate::persist::swap::SwapChainInfo;
451    use crate::test_utils::get_test_ofp_48h;
452    use crate::{ListSwapsRequest, OpeningFeeParams, SwapInfo, SwapStatus};
453    use rusqlite::{named_params, Connection};
454
455    #[test]
456    fn test_swaps() -> PersistResult<(), Box<dyn std::error::Error>> {
457        use crate::persist::test_utils;
458        fn list_in_progress_swaps(storage: &SqliteStorage) -> PersistResult<Vec<SwapInfo>> {
459            storage.list_swaps(ListSwapsRequest {
460                status: Some(SwapStatus::in_progress()),
461                ..Default::default()
462            })
463        }
464
465        let storage = SqliteStorage::new(test_utils::create_test_sql_dir());
466
467        storage.init()?;
468        let tested_swap_info = SwapInfo {
469            bitcoin_address: String::from("1"),
470            created_at: 0,
471            lock_height: 100,
472            payment_hash: vec![1],
473            preimage: vec![2],
474            private_key: vec![3],
475            public_key: vec![4],
476            swapper_public_key: vec![5],
477            script: vec![5],
478            bolt11: None,
479            paid_msat: 0,
480            unconfirmed_sats: 0,
481            confirmed_sats: 0,
482            total_incoming_txs: 0,
483            status: SwapStatus::Initial,
484            refund_tx_ids: Vec::new(),
485            unconfirmed_tx_ids: Vec::new(),
486            confirmed_tx_ids: Vec::new(),
487            min_allowed_deposit: 0,
488            max_allowed_deposit: 100,
489            max_swapper_payable: 200,
490            last_redeem_error: None,
491            channel_opening_fees: Some(get_test_ofp_48h(1, 1).into()),
492            confirmed_at: None,
493        };
494        storage.insert_swap(tested_swap_info.clone())?;
495        let item_value = storage.get_swap_info_by_address("1".to_string())?.unwrap();
496        assert_eq!(item_value, tested_swap_info);
497
498        let in_progress = list_in_progress_swaps(&storage)?;
499        assert_eq!(in_progress.len(), 0);
500
501        let non_existent_swap = storage.get_swap_info_by_address("non-existent".to_string())?;
502        assert!(non_existent_swap.is_none());
503
504        let empty_swaps = storage.list_swaps(ListSwapsRequest {
505            status: Some(vec![SwapStatus::Refundable]),
506            ..Default::default()
507        })?;
508        assert_eq!(empty_swaps.len(), 0);
509
510        let swaps = storage.list_swaps(ListSwapsRequest {
511            status: Some(vec![SwapStatus::Initial]),
512            ..Default::default()
513        })?;
514        assert_eq!(swaps.len(), 1);
515
516        let err = storage.insert_swap(tested_swap_info.clone());
517        //assert_eq!(swaps.len(), 1);
518        assert!(err.is_err());
519
520        let chain_info = SwapChainInfo {
521            unconfirmed_sats: 20,
522            unconfirmed_tx_ids: vec![String::from("333"), String::from("444")],
523            confirmed_sats: 0,
524            confirmed_tx_ids: vec![],
525            confirmed_at: None,
526            total_incoming_txs: 0,
527        };
528
529        let swap_after_chain_update = storage.update_swap_chain_info(
530            tested_swap_info.bitcoin_address.clone(),
531            chain_info.clone(),
532            tested_swap_info
533                .with_chain_info(chain_info.clone(), 0)
534                .status,
535        )?;
536        let in_progress = list_in_progress_swaps(&storage)?;
537        assert_eq!(in_progress[0], swap_after_chain_update);
538
539        let chain_info = SwapChainInfo {
540            unconfirmed_sats: 0,
541            unconfirmed_tx_ids: vec![],
542            confirmed_sats: 20,
543            confirmed_tx_ids: vec![String::from("333"), String::from("444")],
544            confirmed_at: Some(1000),
545            total_incoming_txs: 1,
546        };
547        let swap_after_chain_update = storage.update_swap_chain_info(
548            tested_swap_info.bitcoin_address.clone(),
549            chain_info.clone(),
550            tested_swap_info.with_chain_info(chain_info, 1001).status,
551        )?;
552        let in_progress = list_in_progress_swaps(&storage)?;
553        assert_eq!(in_progress[0], swap_after_chain_update);
554
555        let chain_info = SwapChainInfo {
556            unconfirmed_sats: 0,
557            unconfirmed_tx_ids: vec![],
558            confirmed_sats: 20,
559            confirmed_tx_ids: vec![String::from("333"), String::from("444")],
560            confirmed_at: Some(1000),
561            total_incoming_txs: 1,
562        };
563        storage.update_swap_chain_info(
564            tested_swap_info.bitcoin_address.clone(),
565            chain_info.clone(),
566            tested_swap_info.with_chain_info(chain_info, 10000).status,
567        )?;
568        storage.insert_swap_refund_tx_ids(
569            tested_swap_info.bitcoin_address.clone(),
570            String::from("111"),
571        )?;
572        storage.insert_swap_refund_tx_ids(
573            tested_swap_info.bitcoin_address.clone(),
574            String::from("222"),
575        )?;
576        let in_progress = list_in_progress_swaps(&storage)?;
577        assert_eq!(in_progress.len(), 0);
578
579        storage.update_swap_redeem_error(
580            tested_swap_info.bitcoin_address.clone(),
581            String::from("test error"),
582        )?;
583        let updated_swap = storage
584            .get_swap_info_by_address(tested_swap_info.bitcoin_address.clone())?
585            .unwrap();
586        assert_eq!(
587            updated_swap.last_redeem_error.clone().unwrap(),
588            String::from("test error")
589        );
590
591        storage.update_swap_bolt11(tested_swap_info.bitcoin_address.clone(), "bolt11".into())?;
592        storage.update_swap_paid_amount(
593            tested_swap_info.bitcoin_address.clone(),
594            30_000,
595            updated_swap.with_paid_amount(30_000, 10000).status,
596        )?;
597        let updated_swap = storage
598            .get_swap_info_by_address(tested_swap_info.bitcoin_address.clone())?
599            .unwrap();
600        assert_eq!(updated_swap.bolt11.unwrap(), "bolt11".to_string());
601        assert_eq!(updated_swap.paid_msat, 30_000);
602        assert_eq!(updated_swap.confirmed_sats, 20);
603        assert_eq!(
604            updated_swap.refund_tx_ids,
605            vec![String::from("111"), String::from("222")]
606        );
607        assert_eq!(
608            updated_swap.confirmed_tx_ids,
609            vec![String::from("333"), String::from("444")]
610        );
611        assert_eq!(updated_swap.status, SwapStatus::Completed);
612
613        let chain_info = SwapChainInfo {
614            unconfirmed_sats: 0,
615            unconfirmed_tx_ids: vec![],
616            confirmed_sats: 20,
617            confirmed_tx_ids: vec![String::from("333"), String::from("444")],
618            confirmed_at: Some(1000),
619            total_incoming_txs: 2,
620        };
621        storage.update_swap_chain_info(
622            tested_swap_info.bitcoin_address.clone(),
623            chain_info.clone(),
624            tested_swap_info.with_chain_info(chain_info, 10000).status,
625        )?;
626        let updated_swap = storage
627            .get_swap_info_by_address(tested_swap_info.bitcoin_address)?
628            .unwrap();
629        assert_eq!(updated_swap.status, SwapStatus::Refundable);
630        Ok(())
631    }
632
633    #[test]
634    /// Checks if an empty column is converted to None
635    fn test_rusqlite_empty_col_handling() -> PersistResult<()> {
636        let db = Connection::open_in_memory()?;
637
638        // Insert a NULL
639        db.execute_batch("CREATE TABLE foo (fees_optional TEXT)")?;
640        db.execute(
641            "
642         INSERT INTO foo ( fees_optional )
643         VALUES ( NULL )",
644            named_params! {},
645        )?;
646
647        // Read the column, expect None
648        let res = db.query_row("SELECT fees_optional FROM foo", [], |row| {
649            row.get::<usize, Option<OpeningFeeParams>>(0)
650        })?;
651        assert_eq!(res, None);
652
653        Ok(())
654    }
655}