1use std::collections::HashSet;
2use std::str::FromStr;
3use std::sync::Arc;
4
5use anyhow::{anyhow, bail, Context, Result};
6use boltz_client::swaps::boltz::RevSwapStates;
7use boltz_client::{boltz, Serialize, ToHex};
8use log::{debug, error, info, warn};
9use lwk_wollet::elements::secp256k1_zkp::Secp256k1;
10use lwk_wollet::elements::{Transaction, Txid};
11use lwk_wollet::hashes::hex::DisplayHex;
12use lwk_wollet::secp256k1::SecretKey;
13use tokio::sync::{broadcast, Mutex};
14
15use crate::chain::liquid::LiquidChainService;
16use crate::error::is_txn_already_spent_error;
17use crate::model::{BlockListener, PaymentState::*};
18use crate::model::{Config, PaymentTxData, PaymentType, ReceiveSwap};
19use crate::persist::model::{PaymentTxBalance, PaymentTxDetails};
20use crate::prelude::Swap;
21use crate::{ensure_sdk, utils};
22use crate::{
23 error::PaymentError, model::PaymentState, persist::Persister, swapper::Swapper,
24 wallet::OnchainWallet,
25};
26
27pub const DEFAULT_ZERO_CONF_MAX_SAT: u64 = 1_000_000;
29const SETTLED_AT_GRACE_PERIOD: u32 = 120;
33
34pub(crate) struct ReceiveSwapHandler {
35 config: Config,
36 onchain_wallet: Arc<dyn OnchainWallet>,
37 persister: std::sync::Arc<Persister>,
38 swapper: Arc<dyn Swapper>,
39 subscription_notifier: broadcast::Sender<String>,
40 liquid_chain_service: Arc<dyn LiquidChainService>,
41 claiming_swaps: Arc<Mutex<HashSet<String>>>,
42}
43
44#[sdk_macros::async_trait]
45impl BlockListener for ReceiveSwapHandler {
46 async fn on_bitcoin_block(&self, _height: u32) {}
47
48 async fn on_liquid_block(&self, height: u32) {
49 if let Err(e) = self.claim_confirmed_lockups(height).await {
50 error!("Error claiming confirmed lockups: {e:?}");
51 }
52 }
53}
54
55impl ReceiveSwapHandler {
56 pub(crate) fn new(
57 config: Config,
58 onchain_wallet: Arc<dyn OnchainWallet>,
59 persister: std::sync::Arc<Persister>,
60 swapper: Arc<dyn Swapper>,
61 liquid_chain_service: Arc<dyn LiquidChainService>,
62 ) -> Self {
63 let (subscription_notifier, _) = broadcast::channel::<String>(30);
64 Self {
65 config,
66 onchain_wallet,
67 persister,
68 swapper,
69 subscription_notifier,
70 liquid_chain_service,
71 claiming_swaps: Arc::new(Mutex::new(HashSet::new())),
72 }
73 }
74
75 pub(crate) fn subscribe_payment_updates(&self) -> broadcast::Receiver<String> {
76 self.subscription_notifier.subscribe()
77 }
78
79 pub(crate) async fn on_new_status(&self, update: &boltz::SwapStatus) -> Result<()> {
81 let id = &update.id;
82 let status = &update.status;
83 let swap_state = RevSwapStates::from_str(status)
84 .map_err(|_| anyhow!("Invalid RevSwapState for Receive Swap {id}: {status}"))?;
85 let receive_swap = self.fetch_receive_swap_by_id(id)?;
86
87 info!("Handling Receive Swap transition to {swap_state:?} for swap {id}");
88
89 match swap_state {
90 RevSwapStates::SwapExpired
91 | RevSwapStates::InvoiceExpired
92 | RevSwapStates::TransactionFailed
93 | RevSwapStates::TransactionRefunded => {
94 match receive_swap.mrh_tx_id {
95 Some(mrh_tx_id) => {
96 warn!("Swap {id} is expired but MRH payment was received: txid {mrh_tx_id}")
97 }
98 None => {
99 error!("Swap {id} entered into an unrecoverable state: {swap_state:?}");
100 self.update_swap_info(id, Failed, None, None, None, None)?;
101 }
102 }
103 Ok(())
104 }
105 RevSwapStates::TransactionMempool => {
108 let Some(transaction) = update.transaction.clone() else {
109 return Err(anyhow!("Unexpected payload from Boltz status stream"));
110 };
111
112 if let Some(claim_tx_id) = receive_swap.claim_tx_id {
113 return Err(anyhow!(
114 "Claim tx for Receive Swap {id} was already broadcast: txid {claim_tx_id}"
115 ));
116 }
117
118 if let Some(mrh_tx_id) = receive_swap.mrh_tx_id {
120 return Err(anyhow!(
121 "MRH tx for Receive Swap {id} was already broadcast, ignoring swap: txid {mrh_tx_id}"
122 ));
123 }
124
125 let tx_hex = transaction.hex.ok_or(anyhow!(
127 "Missing lockup transaction hex in swap status update"
128 ))?;
129 let lockup_tx = utils::deserialize_tx_hex(&tx_hex)
130 .context("Failed to deserialize tx hex in swap status update")?;
131 debug!(
132 "Broadcasting lockup tx received in swap status update for receive swap {id}"
133 );
134 if let Err(e) = self.liquid_chain_service.broadcast(&lockup_tx).await {
135 warn!(
136 "Failed to broadcast lockup tx in swap status update: {e:?} - maybe the \
137 tx depends on inputs that haven't been seen yet, falling back to waiting for \
138 it to appear in the mempool"
139 );
140 if let Err(e) = self
141 .verify_lockup_tx_status(&receive_swap, &transaction.id, &tx_hex, false)
142 .await
143 {
144 return Err(anyhow!(
145 "Swapper mempool reported lockup could not be verified. txid: {}, err: {}",
146 transaction.id,
147 e
148 ));
149 }
150 }
151
152 if let Err(e) = self
153 .verify_lockup_tx_amount(&receive_swap, &lockup_tx)
154 .await
155 {
156 self.update_swap_info(id, Failed, None, None, None, None)?;
158 return Err(anyhow!(
159 "Swapper underpaid lockup amount. txid: {}, err: {}",
160 transaction.id,
161 e
162 ));
163 }
164 info!("Swapper lockup was verified");
165
166 let lockup_tx_id = &transaction.id;
167 self.update_swap_info(id, Pending, None, Some(lockup_tx_id), None, None)?;
168
169 let max_amount_sat = self.config.zero_conf_max_amount_sat();
171 let receiver_amount_sat = receive_swap.receiver_amount_sat;
172 if receiver_amount_sat > max_amount_sat {
173 warn!("[Receive Swap {id}] Amount is too high to claim with zero-conf ({receiver_amount_sat} sat > {max_amount_sat} sat). Waiting for confirmation...");
174 return Ok(());
175 }
176
177 debug!("[Receive Swap {id}] Amount is within valid range for zero-conf ({receiver_amount_sat} < {max_amount_sat} sat)");
178
179 let rbf_explicit = lockup_tx.input.iter().any(|input| input.sequence.is_rbf());
182 if rbf_explicit {
185 warn!("[Receive Swap {id}] Lockup transaction signals RBF. Waiting for confirmation...");
186 return Ok(());
187 }
188 debug!("[Receive Swap {id}] Lockup tx does not signal RBF. Proceeding...");
189
190 if let Err(err) = self.claim(id).await {
191 match err {
192 PaymentError::AlreadyClaimed => {
193 warn!("Funds already claimed for Receive Swap {id}")
194 }
195 _ => error!("Claim for Receive Swap {id} failed: {err}"),
196 }
197 }
198
199 Ok(())
200 }
201 RevSwapStates::TransactionConfirmed => {
202 let Some(transaction) = update.transaction.clone() else {
203 return Err(anyhow!("Unexpected payload from Boltz status stream"));
204 };
205
206 if let Some(mrh_tx_id) = receive_swap.mrh_tx_id {
208 return Err(anyhow!(
209 "MRH tx for Receive Swap {id} was already broadcast, ignoring swap: txid {mrh_tx_id}"
210 ));
211 }
212
213 let tx_hex = transaction.hex.ok_or(anyhow!(
215 "Missing lockup transaction hex in swap status update"
216 ))?;
217 let lockup_tx = match self
218 .verify_lockup_tx_status(&receive_swap, &transaction.id, &tx_hex, true)
219 .await
220 {
221 Ok(lockup_tx) => lockup_tx,
222 Err(e) => {
223 return Err(anyhow!(
224 "Swapper reported lockup could not be verified. txid: {}, err: {}",
225 transaction.id,
226 e
227 ));
228 }
229 };
230
231 if let Err(e) = self
232 .verify_lockup_tx_amount(&receive_swap, &lockup_tx)
233 .await
234 {
235 self.update_swap_info(id, Failed, None, None, None, None)?;
237 return Err(anyhow!(
238 "Swapper underpaid lockup amount. txid: {}, err: {}",
239 transaction.id,
240 e
241 ));
242 }
243 info!("Swapper lockup was verified, moving to claim");
244
245 match receive_swap.claim_tx_id {
246 Some(claim_tx_id) => {
247 warn!("Claim tx for Receive Swap {id} was already broadcast: txid {claim_tx_id}")
248 }
249 None => {
250 self.update_swap_info(&receive_swap.id, Pending, None, None, None, None)?;
251
252 if let Err(err) = self.claim(id).await {
253 match err {
254 PaymentError::AlreadyClaimed => {
255 warn!("Funds already claimed for Receive Swap {id}")
256 }
257 _ => error!("Claim for Receive Swap {id} failed: {err}"),
258 }
259 }
260 }
261 }
262 Ok(())
263 }
264
265 RevSwapStates::InvoiceSettled => {
266 info!(
267 "Received `InvoiceSettled` state from Boltz, saving invoice settlement time."
268 );
269 let Some(claim_tx_id) = receive_swap.claim_tx_id else {
270 bail!("Could not save invoice settlement time: no claim tx id found.");
271 };
272 if utils::now().saturating_sub(receive_swap.created_at) > SETTLED_AT_GRACE_PERIOD {
273 info!("Received `InvoiceSettled` event after grace period for Receive swap {id}: backfilling to claim tx timestamp.");
274 return Ok(());
275 }
276 self.persister
277 .insert_or_update_payment_details(PaymentTxDetails {
278 tx_id: claim_tx_id,
279 destination: receive_swap.invoice,
280 settled_at: Some(utils::now()),
281 ..Default::default()
282 })
283 .map_err(|err| anyhow!("Could not persist invoice settlement time for Receive Swap {id}: {err}"))?;
284 Ok(())
285 }
286
287 _ => {
288 debug!("Unhandled state for Receive Swap {id}: {swap_state:?}");
289 Ok(())
290 }
291 }
292 }
293
294 fn fetch_receive_swap_by_id(&self, swap_id: &str) -> Result<ReceiveSwap, PaymentError> {
295 self.persister
296 .fetch_receive_swap_by_id(swap_id)
297 .map_err(|e| {
298 error!("Failed to fetch receive swap by id: {e:?}");
299 PaymentError::PersistError
300 })?
301 .ok_or(PaymentError::Generic {
302 err: format!("Receive Swap not found {swap_id}"),
303 })
304 }
305
306 pub(crate) fn update_swap(&self, updated_swap: ReceiveSwap) -> Result<(), PaymentError> {
308 let swap_id = &updated_swap.id;
309 let swap = self.fetch_receive_swap_by_id(swap_id)?;
310 if updated_swap != swap {
311 info!(
312 "Updating Receive swap {swap_id} to {:?} (claim_tx_id = {:?}, lockup_tx_id = {:?}, mrh_tx_id = {:?})",
313 updated_swap.state, updated_swap.claim_tx_id, updated_swap.lockup_tx_id, updated_swap.mrh_tx_id
314 );
315 self.persister
316 .insert_or_update_receive_swap(&updated_swap)?;
317 let _ = self.subscription_notifier.send(swap_id.clone());
318 }
319
320 if updated_swap.state == Complete {
324 utils::update_invoice_settled_at(
325 &self.persister,
326 swap_id,
327 updated_swap.claim_tx_id.as_ref(),
328 updated_swap.invoice.clone(),
329 );
330 }
331 Ok(())
332 }
333
334 pub(crate) fn update_swap_info(
336 &self,
337 swap_id: &str,
338 to_state: PaymentState,
339 claim_tx_id: Option<&str>,
340 lockup_tx_id: Option<&str>,
341 mrh_tx_id: Option<&str>,
342 mrh_amount_sat: Option<u64>,
343 ) -> Result<(), PaymentError> {
344 info!(
345 "Transitioning Receive swap {swap_id} to {to_state:?} (claim_tx_id = {claim_tx_id:?}, lockup_tx_id = {lockup_tx_id:?}, mrh_tx_id = {mrh_tx_id:?})"
346 );
347 let swap = self.fetch_receive_swap_by_id(swap_id)?;
348 Self::validate_state_transition(swap.state, to_state)?;
349 self.persister.try_handle_receive_swap_update(
350 swap_id,
351 to_state,
352 claim_tx_id,
353 lockup_tx_id,
354 mrh_tx_id,
355 mrh_amount_sat,
356 )?;
357 let updated_swap = self.fetch_receive_swap_by_id(swap_id)?;
358
359 if mrh_tx_id.is_some() {
360 self.persister.delete_reserved_address(&swap.mrh_address)?;
361 }
362
363 if updated_swap != swap {
364 let _ = self.subscription_notifier.send(updated_swap.id);
365 }
366 Ok(())
367 }
368
369 async fn claim(&self, swap_id: &str) -> Result<(), PaymentError> {
370 {
371 let mut claiming_guard = self.claiming_swaps.lock().await;
372 if claiming_guard.contains(swap_id) {
373 debug!("Claim for swap {swap_id} already in progress, skipping.");
374 return Ok(());
375 }
376 claiming_guard.insert(swap_id.to_string());
377 }
378
379 let result = self.claim_inner(swap_id).await;
380
381 {
382 let mut claiming_guard = self.claiming_swaps.lock().await;
383 claiming_guard.remove(swap_id);
384 }
385
386 result
387 }
388
389 async fn claim_inner(&self, swap_id: &str) -> Result<(), PaymentError> {
390 let swap = self.fetch_receive_swap_by_id(swap_id)?;
391 ensure_sdk!(swap.claim_tx_id.is_none(), PaymentError::AlreadyClaimed);
392
393 let liquid_tip = self.liquid_chain_service.tip().await?;
395 let is_cooperative = liquid_tip <= swap.timeout_block_height.saturating_sub(10);
396 if !is_cooperative {
397 info!(
398 "Using non-cooperative claim for Receive Swap {swap_id} as timeout block height {} is near or past (liquid tip: {liquid_tip})",
399 swap.timeout_block_height
400 );
401 }
402
403 info!("Initiating claim for Receive Swap {swap_id}");
404 let claim_address = match swap.claim_address {
405 Some(ref claim_address) => claim_address.clone(),
406 None => {
407 let address = self.onchain_wallet.next_unused_address().await?.to_string();
409 self.persister
410 .set_receive_swap_claim_address(&swap.id, &address)?;
411 address
412 }
413 };
414
415 let crate::prelude::Transaction::Liquid(claim_tx) = self
416 .swapper
417 .create_claim_tx(
418 Swap::Receive(swap.clone()),
419 Some(claim_address.clone()),
420 is_cooperative,
421 )
422 .await?
423 else {
424 return Err(PaymentError::Generic {
425 err: format!("Constructed invalid transaction for Receive swap {swap_id}"),
426 });
427 };
428
429 let tx_id = claim_tx.txid().to_hex();
432 match self.persister.set_receive_swap_claim_tx_id(swap_id, &tx_id) {
433 Ok(_) => {
434 let broadcast_res = match self.liquid_chain_service.broadcast(&claim_tx).await {
436 Ok(tx_id) => Ok(tx_id.to_hex()),
437 Err(e) if is_txn_already_spent_error(&e) => Err(PaymentError::AlreadyClaimed),
438 Err(err) => {
439 debug!(
440 "Could not broadcast claim tx via chain service for Receive swap {swap_id}: {err:?}"
441 );
442 let claim_tx_hex = claim_tx.serialize().to_lower_hex_string();
443 self.swapper
444 .broadcast_tx(self.config.network.into(), &claim_tx_hex)
445 .await
446 }
447 };
448 match broadcast_res {
449 Ok(claim_tx_id) => {
450 self.persister.insert_or_update_payment(
453 PaymentTxData {
454 tx_id: claim_tx_id.clone(),
455 timestamp: Some(utils::now()),
456 fees_sat: 0,
457 is_confirmed: false,
458 unblinding_data: None,
459 },
460 &[PaymentTxBalance {
461 amount: swap.receiver_amount_sat,
462 payment_type: PaymentType::Receive,
463 asset_id: self.config.lbtc_asset_id(),
464 }],
465 None,
466 false,
467 )?;
468
469 info!("Successfully broadcast claim tx {claim_tx_id} for Receive Swap {swap_id}");
470 _ = self.subscription_notifier.send(claim_tx_id);
473 Ok(())
474 }
475 Err(err) => {
476 debug!(
478 "Could not broadcast claim tx via swapper for Receive swap {swap_id}: {err:?}"
479 );
480 self.persister
481 .unset_receive_swap_claim_tx_id(swap_id, &tx_id)?;
482 Err(err)
483 }
484 }
485 }
486 Err(err) => {
487 debug!(
488 "Failed to set claim_tx_id after creating tx for Receive swap {swap_id}: txid {tx_id}"
489 );
490 Err(err)
491 }
492 }
493 }
494
495 async fn claim_confirmed_lockups(&self, height: u32) -> Result<()> {
496 let receive_swaps: Vec<ReceiveSwap> = self
497 .persister
498 .list_ongoing_receive_swaps()?
499 .into_iter()
500 .filter(|s| s.lockup_tx_id.is_some() && s.claim_tx_id.is_none())
501 .collect();
502 info!(
503 "Rescanning {} Receive Swap(s) lockup txs at height {}",
504 receive_swaps.len(),
505 height
506 );
507 for swap in receive_swaps {
508 if let Err(e) = self.claim_confirmed_lockup(&swap).await {
509 error!("Error rescanning Receive Swap {}: {e:?}", swap.id,);
510 }
511 }
512 Ok(())
513 }
514
515 async fn claim_confirmed_lockup(&self, receive_swap: &ReceiveSwap) -> Result<()> {
516 let Some(tx_id) = receive_swap.lockup_tx_id.clone() else {
517 return Ok(());
519 };
520 let swap_id = &receive_swap.id;
521 let tx_hex = self
522 .liquid_chain_service
523 .get_transaction_hex(&Txid::from_str(&tx_id)?)
524 .await?
525 .ok_or(anyhow!("Lockup tx not found for Receive swap {swap_id}"))?
526 .serialize()
527 .to_lower_hex_string();
528 let lockup_tx = self
529 .verify_lockup_tx_status(receive_swap, &tx_id, &tx_hex, true)
530 .await?;
531 if let Err(e) = self.verify_lockup_tx_amount(receive_swap, &lockup_tx).await {
532 self.update_swap_info(swap_id, Failed, None, None, None, None)?;
533 return Err(e);
534 }
535 info!("Receive Swap {swap_id} lockup tx is confirmed");
536 self.claim(swap_id)
537 .await
538 .map_err(|e| anyhow!("Could not claim Receive Swap {swap_id}: {e:?}"))
539 }
540
541 fn validate_state_transition(
542 from_state: PaymentState,
543 to_state: PaymentState,
544 ) -> Result<(), PaymentError> {
545 match (from_state, to_state) {
546 (_, Created) => Err(PaymentError::Generic {
547 err: "Cannot transition to Created state".to_string(),
548 }),
549
550 (Created | Pending, Pending) => Ok(()),
551 (_, Pending) => Err(PaymentError::Generic {
552 err: format!("Cannot transition from {from_state:?} to Pending state"),
553 }),
554
555 (Created | Pending, Complete) => Ok(()),
556 (_, Complete) => Err(PaymentError::Generic {
557 err: format!("Cannot transition from {from_state:?} to Complete state"),
558 }),
559
560 (Created | TimedOut, TimedOut) => Ok(()),
561 (_, TimedOut) => Err(PaymentError::Generic {
562 err: format!("Cannot transition from {from_state:?} to TimedOut state"),
563 }),
564
565 (_, Refundable) => Err(PaymentError::Generic {
566 err: format!("Cannot transition from {from_state:?} to Refundable state"),
567 }),
568
569 (_, RefundPending) => Err(PaymentError::Generic {
570 err: format!("Cannot transition from {from_state:?} to RefundPending state"),
571 }),
572
573 (Complete, Failed) => Err(PaymentError::Generic {
574 err: format!("Cannot transition from {from_state:?} to Failed state"),
575 }),
576 (_, Failed) => Ok(()),
577
578 (_, WaitingFeeAcceptance) => Err(PaymentError::Generic {
579 err: format!("Cannot transition from {from_state:?} to WaitingFeeAcceptance state"),
580 }),
581 }
582 }
583
584 async fn verify_lockup_tx_status(
585 &self,
586 receive_swap: &ReceiveSwap,
587 tx_id: &str,
588 tx_hex: &str,
589 verify_confirmation: bool,
590 ) -> Result<Transaction> {
591 let script = receive_swap.get_swap_script()?;
593 let address = script
594 .to_address(self.config.network.into())
595 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
596 self.liquid_chain_service
597 .verify_tx(&address, tx_id, tx_hex, verify_confirmation)
598 .await
599 }
600
601 async fn verify_lockup_tx_amount(
602 &self,
603 receive_swap: &ReceiveSwap,
604 lockup_tx: &Transaction,
605 ) -> Result<()> {
606 let secp = Secp256k1::new();
607 let script = receive_swap.get_swap_script()?;
608 let address = script
609 .to_address(self.config.network.into())
610 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
611 let blinding_key = receive_swap
612 .get_boltz_create_response()?
613 .blinding_key
614 .ok_or(anyhow!("Missing blinding key"))?;
615 let tx_out = lockup_tx
616 .output
617 .iter()
618 .find(|tx_out| tx_out.script_pubkey == address.script_pubkey())
619 .ok_or(anyhow!("Failed to get tx output"))?;
620 let lockup_amount_sat = tx_out
621 .unblind(&secp, SecretKey::from_str(&blinding_key)?)
622 .map(|o| o.value)?;
623 let expected_lockup_amount_sat =
624 receive_swap.receiver_amount_sat + receive_swap.claim_fees_sat;
625 if lockup_amount_sat < expected_lockup_amount_sat {
626 bail!(
627 "Failed to verify lockup amount for Receive Swap {}: {} sat vs {} sat",
628 receive_swap.id,
629 expected_lockup_amount_sat,
630 lockup_amount_sat
631 );
632 }
633 Ok(())
634 }
635}
636
637#[cfg(test)]
638mod tests {
639 use std::collections::{HashMap, HashSet};
640
641 use anyhow::Result;
642
643 use crate::{
644 model::PaymentState::{self, *},
645 test_utils::{
646 persist::{create_persister, new_receive_swap},
647 receive_swap::new_receive_swap_handler,
648 },
649 };
650
651 #[cfg(feature = "browser-tests")]
652 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
653
654 #[sdk_macros::async_test_all]
655 async fn test_receive_swap_state_transitions() -> Result<()> {
656 create_persister!(persister);
657
658 let receive_swap_state_handler = new_receive_swap_handler(persister.clone())?;
659
660 let valid_combinations = HashMap::from([
662 (
663 Created,
664 HashSet::from([Pending, Complete, TimedOut, Failed]),
665 ),
666 (Pending, HashSet::from([Pending, Complete, Failed])),
667 (TimedOut, HashSet::from([TimedOut, Failed])),
668 (Complete, HashSet::from([])),
669 (Refundable, HashSet::from([Failed])),
670 (RefundPending, HashSet::from([Failed])),
671 (Failed, HashSet::from([Failed])),
672 ]);
673
674 for (first_state, allowed_states) in valid_combinations.iter() {
675 for allowed_state in allowed_states {
676 let receive_swap = new_receive_swap(Some(*first_state), None);
677 persister.insert_or_update_receive_swap(&receive_swap)?;
678
679 assert!(receive_swap_state_handler
680 .update_swap_info(&receive_swap.id, *allowed_state, None, None, None, None)
681 .is_ok());
682 }
683 }
684
685 let all_states = HashSet::from([Created, Pending, Complete, TimedOut, Failed]);
687 let invalid_combinations: HashMap<PaymentState, HashSet<PaymentState>> = valid_combinations
688 .iter()
689 .map(|(first_state, allowed_states)| {
690 (
691 *first_state,
692 all_states.difference(allowed_states).cloned().collect(),
693 )
694 })
695 .collect();
696
697 for (first_state, disallowed_states) in invalid_combinations.iter() {
698 for disallowed_state in disallowed_states {
699 let receive_swap = new_receive_swap(Some(*first_state), None);
700 persister.insert_or_update_receive_swap(&receive_swap)?;
701
702 assert!(receive_swap_state_handler
703 .update_swap_info(&receive_swap.id, *disallowed_state, None, None, None, None)
704 .is_err());
705 }
706 }
707
708 Ok(())
709 }
710}