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 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 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 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!(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 fn test_rusqlite_empty_col_handling() -> PersistResult<()> {
636 let db = Connection::open_in_memory()?;
637
638 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 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}