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_mempool_conflict_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_mempool_conflict_error(&e) => {
438 Err(PaymentError::AlreadyClaimed)
439 }
440 Err(err) => {
441 debug!(
442 "Could not broadcast claim tx via chain service for Receive swap {swap_id}: {err:?}"
443 );
444 let claim_tx_hex = claim_tx.serialize().to_lower_hex_string();
445 self.swapper
446 .broadcast_tx(self.config.network.into(), &claim_tx_hex)
447 .await
448 }
449 };
450 match broadcast_res {
451 Ok(claim_tx_id) => {
452 self.persister.insert_or_update_payment(
455 PaymentTxData {
456 tx_id: claim_tx_id.clone(),
457 timestamp: Some(utils::now()),
458 fees_sat: 0,
459 is_confirmed: false,
460 unblinding_data: None,
461 },
462 &[PaymentTxBalance {
463 amount: swap.receiver_amount_sat,
464 payment_type: PaymentType::Receive,
465 asset_id: self.config.lbtc_asset_id(),
466 }],
467 None,
468 false,
469 )?;
470
471 info!("Successfully broadcast claim tx {claim_tx_id} for Receive Swap {swap_id}");
472 _ = self.subscription_notifier.send(claim_tx_id);
475 Ok(())
476 }
477 Err(err) => {
478 debug!(
480 "Could not broadcast claim tx via swapper for Receive swap {swap_id}: {err:?}"
481 );
482 self.persister
483 .unset_receive_swap_claim_tx_id(swap_id, &tx_id)?;
484 Err(err)
485 }
486 }
487 }
488 Err(err) => {
489 debug!(
490 "Failed to set claim_tx_id after creating tx for Receive swap {swap_id}: txid {tx_id}"
491 );
492 Err(err)
493 }
494 }
495 }
496
497 async fn claim_confirmed_lockups(&self, height: u32) -> Result<()> {
498 let receive_swaps: Vec<ReceiveSwap> = self
499 .persister
500 .list_ongoing_receive_swaps()?
501 .into_iter()
502 .filter(|s| s.lockup_tx_id.is_some() && s.claim_tx_id.is_none())
503 .collect();
504 info!(
505 "Rescanning {} Receive Swap(s) lockup txs at height {}",
506 receive_swaps.len(),
507 height
508 );
509 for swap in receive_swaps {
510 if let Err(e) = self.claim_confirmed_lockup(&swap).await {
511 error!("Error rescanning Receive Swap {}: {e:?}", swap.id,);
512 }
513 }
514 Ok(())
515 }
516
517 async fn claim_confirmed_lockup(&self, receive_swap: &ReceiveSwap) -> Result<()> {
518 let Some(tx_id) = receive_swap.lockup_tx_id.clone() else {
519 return Ok(());
521 };
522 let swap_id = &receive_swap.id;
523 let tx_hex = self
524 .liquid_chain_service
525 .get_transaction_hex(&Txid::from_str(&tx_id)?)
526 .await?
527 .ok_or(anyhow!("Lockup tx not found for Receive swap {swap_id}"))?
528 .serialize()
529 .to_lower_hex_string();
530 let lockup_tx = self
531 .verify_lockup_tx_status(receive_swap, &tx_id, &tx_hex, true)
532 .await?;
533 if let Err(e) = self.verify_lockup_tx_amount(receive_swap, &lockup_tx).await {
534 self.update_swap_info(swap_id, Failed, None, None, None, None)?;
535 return Err(e);
536 }
537 info!("Receive Swap {swap_id} lockup tx is confirmed");
538 self.claim(swap_id)
539 .await
540 .map_err(|e| anyhow!("Could not claim Receive Swap {swap_id}: {e:?}"))
541 }
542
543 fn validate_state_transition(
544 from_state: PaymentState,
545 to_state: PaymentState,
546 ) -> Result<(), PaymentError> {
547 match (from_state, to_state) {
548 (_, Created) => Err(PaymentError::Generic {
549 err: "Cannot transition to Created state".to_string(),
550 }),
551
552 (Created | Pending, Pending) => Ok(()),
553 (_, Pending) => Err(PaymentError::Generic {
554 err: format!("Cannot transition from {from_state:?} to Pending state"),
555 }),
556
557 (Created | Pending, Complete) => Ok(()),
558 (_, Complete) => Err(PaymentError::Generic {
559 err: format!("Cannot transition from {from_state:?} to Complete state"),
560 }),
561
562 (Created | TimedOut, TimedOut) => Ok(()),
563 (_, TimedOut) => Err(PaymentError::Generic {
564 err: format!("Cannot transition from {from_state:?} to TimedOut state"),
565 }),
566
567 (_, Refundable) => Err(PaymentError::Generic {
568 err: format!("Cannot transition from {from_state:?} to Refundable state"),
569 }),
570
571 (_, RefundPending) => Err(PaymentError::Generic {
572 err: format!("Cannot transition from {from_state:?} to RefundPending state"),
573 }),
574
575 (Complete, Failed) => Err(PaymentError::Generic {
576 err: format!("Cannot transition from {from_state:?} to Failed state"),
577 }),
578 (_, Failed) => Ok(()),
579
580 (_, WaitingFeeAcceptance) => Err(PaymentError::Generic {
581 err: format!("Cannot transition from {from_state:?} to WaitingFeeAcceptance state"),
582 }),
583 }
584 }
585
586 async fn verify_lockup_tx_status(
587 &self,
588 receive_swap: &ReceiveSwap,
589 tx_id: &str,
590 tx_hex: &str,
591 verify_confirmation: bool,
592 ) -> Result<Transaction> {
593 let script = receive_swap.get_swap_script()?;
595 let address = script
596 .to_address(self.config.network.into())
597 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
598 self.liquid_chain_service
599 .verify_tx(&address, tx_id, tx_hex, verify_confirmation)
600 .await
601 }
602
603 async fn verify_lockup_tx_amount(
604 &self,
605 receive_swap: &ReceiveSwap,
606 lockup_tx: &Transaction,
607 ) -> Result<()> {
608 let secp = Secp256k1::new();
609 let script = receive_swap.get_swap_script()?;
610 let address = script
611 .to_address(self.config.network.into())
612 .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?;
613 let blinding_key = receive_swap
614 .get_boltz_create_response()?
615 .blinding_key
616 .ok_or(anyhow!("Missing blinding key"))?;
617 let tx_out = lockup_tx
618 .output
619 .iter()
620 .find(|tx_out| tx_out.script_pubkey == address.script_pubkey())
621 .ok_or(anyhow!("Failed to get tx output"))?;
622 let lockup_amount_sat = tx_out
623 .unblind(&secp, SecretKey::from_str(&blinding_key)?)
624 .map(|o| o.value)?;
625 let expected_lockup_amount_sat =
626 receive_swap.receiver_amount_sat + receive_swap.claim_fees_sat;
627 if lockup_amount_sat < expected_lockup_amount_sat {
628 bail!(
629 "Failed to verify lockup amount for Receive Swap {}: {} sat vs {} sat",
630 receive_swap.id,
631 expected_lockup_amount_sat,
632 lockup_amount_sat
633 );
634 }
635 Ok(())
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use std::collections::{HashMap, HashSet};
642
643 use anyhow::Result;
644
645 use crate::{
646 model::PaymentState::{self, *},
647 test_utils::{
648 persist::{create_persister, new_receive_swap},
649 receive_swap::new_receive_swap_handler,
650 },
651 };
652
653 #[cfg(feature = "browser-tests")]
654 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
655
656 #[sdk_macros::async_test_all]
657 async fn test_receive_swap_state_transitions() -> Result<()> {
658 create_persister!(persister);
659
660 let receive_swap_state_handler = new_receive_swap_handler(persister.clone())?;
661
662 let valid_combinations = HashMap::from([
664 (
665 Created,
666 HashSet::from([Pending, Complete, TimedOut, Failed]),
667 ),
668 (Pending, HashSet::from([Pending, Complete, Failed])),
669 (TimedOut, HashSet::from([TimedOut, Failed])),
670 (Complete, HashSet::from([])),
671 (Refundable, HashSet::from([Failed])),
672 (RefundPending, HashSet::from([Failed])),
673 (Failed, HashSet::from([Failed])),
674 ]);
675
676 for (first_state, allowed_states) in valid_combinations.iter() {
677 for allowed_state in allowed_states {
678 let receive_swap = new_receive_swap(Some(*first_state), None);
679 persister.insert_or_update_receive_swap(&receive_swap)?;
680
681 assert!(receive_swap_state_handler
682 .update_swap_info(&receive_swap.id, *allowed_state, None, None, None, None)
683 .is_ok());
684 }
685 }
686
687 let all_states = HashSet::from([Created, Pending, Complete, TimedOut, Failed]);
689 let invalid_combinations: HashMap<PaymentState, HashSet<PaymentState>> = valid_combinations
690 .iter()
691 .map(|(first_state, allowed_states)| {
692 (
693 *first_state,
694 all_states.difference(allowed_states).cloned().collect(),
695 )
696 })
697 .collect();
698
699 for (first_state, disallowed_states) in invalid_combinations.iter() {
700 for disallowed_state in disallowed_states {
701 let receive_swap = new_receive_swap(Some(*first_state), None);
702 persister.insert_or_update_receive_swap(&receive_swap)?;
703
704 assert!(receive_swap_state_handler
705 .update_swap_info(&receive_swap.id, *disallowed_state, None, None, None, None)
706 .is_err());
707 }
708 }
709
710 Ok(())
711 }
712}