diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 4749e54cc0..f5347cc2cb 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -503,6 +503,22 @@ pub struct RecoveredSwap { transaction: TransactionEnum, } +#[derive(Display, Debug, PartialEq)] +pub enum RecoverSwapError { + // We might not find the original payment tx on chain (e.g. re-orged). This doesn't mean though that nobody has it. + // TODO: These coins should be spent ASAP to avoid them getting locked (or stolen). + #[display(fmt = "The payment tx is not on-chain. Nothing to recover.")] + PaymentTxNotFound, + #[display(fmt = "An unknown error occurred. Retrying might fix it: {}", _0)] + Temporary(String), + #[display(fmt = "The swap is not recoverable: {}", _0)] + Irrecoverable(String), + #[display(fmt = "Wait {}s and try to recover again.", _0)] + WaitAndRetry(u64), + #[display(fmt = "The funds will be automatically recovered after lock-time: {}", _0)] + AutoRecoverableAfter(u64), +} + /// Represents the amount of a coin locked by ongoing swap #[derive(Debug)] pub struct LockedAmount { @@ -547,8 +563,11 @@ struct LockedAmountInfo { locked_amount: LockedAmount, } +/// A running swap is the swap accompanied by the abort handle of the thread the swap is running on. +type RunningSwap = (Arc, AbortOnDropHandle); + struct SwapsContext { - running_swaps: Mutex>>, + running_swaps: Mutex>, active_swaps_v2_infos: Mutex>, banned_pubkeys: Mutex>, swap_msgs: Mutex>, @@ -651,21 +670,20 @@ pub fn get_locked_amount(ctx: &MmArc, coin: &str) -> MmNumber { let swap_ctx = SwapsContext::from_ctx(ctx).unwrap(); let swap_lock = swap_ctx.running_swaps.lock().unwrap(); - let mut locked = - swap_lock - .values() - .flat_map(|swap| swap.locked_amount()) - .fold(MmNumber::from(0), |mut total_amount, locked| { - if locked.coin == coin { - total_amount += locked.amount; - } - if let Some(trade_fee) = locked.trade_fee { - if trade_fee.coin == coin && !trade_fee.paid_from_trading_vol { - total_amount += trade_fee.amount; - } + let mut locked = swap_lock.values().flat_map(|(swap, _)| swap.locked_amount()).fold( + MmNumber::from(0), + |mut total_amount, locked| { + if locked.coin == coin { + total_amount += locked.amount; + } + if let Some(trade_fee) = locked.trade_fee { + if trade_fee.coin == coin && !trade_fee.paid_from_trading_vol { + total_amount += trade_fee.amount; } - total_amount - }); + } + total_amount + }, + ); drop(swap_lock); let locked_amounts = swap_ctx.locked_amounts.lock().unwrap(); @@ -688,7 +706,7 @@ pub fn get_locked_amount(ctx: &MmArc, coin: &str) -> MmNumber { /// Clear up all the running swaps. /// -/// This doesn't mean that these swaps will be stopped. They can only be stopped from the abortable systems they are running on top of. +/// This also auto-stops any running swaps since their abort handles will get triggered (assuming these abort handles aren't already triggered by other means). pub fn clear_running_swaps(ctx: &MmArc) { let swap_ctx = SwapsContext::from_ctx(ctx).unwrap(); swap_ctx.running_swaps.lock().unwrap().clear(); @@ -701,8 +719,8 @@ fn get_locked_amount_by_other_swaps(ctx: &MmArc, except_uuid: &Uuid, coin: &str) swap_lock .values() - .filter(|swap| swap.uuid() != except_uuid) - .flat_map(|swap| swap.locked_amount()) + .filter(|(swap, _)| swap.uuid() != except_uuid) + .flat_map(|(swap, _)| swap.locked_amount()) .fold(MmNumber::from(0), |mut total_amount, locked| { if locked.coin == coin { total_amount += locked.amount; @@ -720,7 +738,7 @@ pub fn active_swaps_using_coins(ctx: &MmArc, coins: &HashSet) -> Result< let swap_ctx = try_s!(SwapsContext::from_ctx(ctx)); let swaps = try_s!(swap_ctx.running_swaps.lock()); let mut uuids = vec![]; - for swap in swaps.values() { + for (swap, _) in swaps.values() { if coins.contains(&swap.maker_coin().to_string()) || coins.contains(&swap.taker_coin().to_string()) { uuids.push(*swap.uuid()) } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 1f2bac0e7e..bfe3234f33 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -8,8 +8,8 @@ use super::{ broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_msg_every, check_other_coin_balance_for_swap, detect_secret_hash_algo, get_locked_amount, recv_swap_msg, swap_topic, taker_payment_spend_deadline, tx_helper_topic, wait_for_maker_payment_conf_until, AtomicSwap, LockedAmount, MySwapInfo, NegotiationDataMsg, - NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedSwapIo, SavedTradeFee, - SwapConfirmationsSettings, SwapError, SwapMsg, SwapTxDataMsg, SwapsContext, TransactionIdentifier, + NegotiationDataV2, NegotiationDataV3, RecoverSwapError, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedSwapIo, + SavedTradeFee, SwapConfirmationsSettings, SwapError, SwapMsg, SwapTxDataMsg, SwapsContext, TransactionIdentifier, TAKER_FEE_VALIDATION_ATTEMPTS, TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, WAIT_CONFIRM_INTERVAL_SEC, }; use crate::lp_dispatcher::{DispatcherContext, LpEvents}; @@ -26,11 +26,12 @@ use coins::{ TradePreimageValue, TransactionEnum, ValidateFeeArgs, ValidatePaymentInput, WatcherReward, }; use common::log::{debug, error, info, warn}; +use common::now_sec; use common::{bits256, executor::Timer, now_ms}; -use common::{now_sec, wait_until_sec}; use crypto::privkey::SerializableSecp256k1Keypair; use crypto::secret_hash_algo::SecretHashAlgo; use crypto::CryptoCtx; +use futures::future::abortable; use futures::{compat::Future01CompatExt, select, FutureExt}; use keys::KeyPair; use mm2_core::mm_ctx::MmArc; @@ -1360,95 +1361,87 @@ impl MakerSwap { )); } - let maker_payment = self.r().maker_payment.clone().unwrap().tx_hex; - let locktime = self.r().data.maker_payment_lock; - if self.maker_coin.is_auto_refundable() { - return match self.maker_coin.wait_for_htlc_refund(&maker_payment, locktime).await { - Ok(()) => Ok(( - Some(MakerSwapCommand::FinalizeMakerPaymentRefund), - vec![MakerSwapEvent::MakerPaymentRefunded(None)], - )), - Err(err) => Ok(( - Some(MakerSwapCommand::Finish), - vec![MakerSwapEvent::MakerPaymentRefundFailed( - ERRL!("!maker_coin.wait_for_htlc_refund: {}", err.to_string()).into(), - )], - )), - }; - } - + // Keep trying to recover funds (by refunding the maker payment or spending the taker payment) until successful or face an irrecoverable error. loop { - match self.maker_coin.can_refund_htlc(locktime).await { - Ok(CanRefundHtlc::CanRefundNow) => break, - Ok(CanRefundHtlc::HaveToWait(to_sleep)) => Timer::sleep(to_sleep as f64).await, - Err(e) => { - error!("Error {} on can_refund_htlc, retrying in 30 seconds", e); - Timer::sleep(30.).await; + match self.recover_funds().await { + // We recovered the swap successfully. + Ok(recovered_swap) => { + let tx_ident = TransactionIdentifier { + tx_hex: recovered_swap.transaction.tx_hex().into(), + tx_hash: recovered_swap.transaction.tx_hash_as_bytes(), + }; + return match recovered_swap.action { + // We recovered the swap by refunding the maker payment. + RecoveredSwapAction::RefundedMyPayment => { + info!("Maker payment refund tx {:02x}", tx_ident.tx_hash); + Ok(( + Some(MakerSwapCommand::FinalizeMakerPaymentRefund), + vec![MakerSwapEvent::MakerPaymentRefunded(Some(tx_ident))], + )) + }, + // We recovered the swap by proceeding forward and spending the taker payment. The swap wasn't actually a failure. + // Roll back to confirming the taker payment spend. + RecoveredSwapAction::SpentOtherPayment => { + info!("Refund canceled. Taker payment spend tx {:02x}", tx_ident.tx_hash); + // TODO: We prepared for refund but didn't finalize refund. This must be breaking something for lightning. + Ok(( + Some(MakerSwapCommand::ConfirmTakerPaymentSpend), + vec![ + MakerSwapEvent::TakerPaymentSpent(tx_ident), + MakerSwapEvent::TakerPaymentSpendConfirmStarted, + ], + )) + }, + }; + }, + // Encountered an error during swap recover. + Err(err) => match err.into_inner() { + // The payment tx we want to refund isn't even on-chain. There is nothing to refund/spend. + RecoverSwapError::PaymentTxNotFound => { + return Ok(( + Some(MakerSwapCommand::Finish), + vec![MakerSwapEvent::MakerPaymentRefundFailed( + "MakerPayment isn't even on-chain to refund it.".into(), + )], + )); + }, + // The error is unrecoverable, retrying will not fix the issue. + RecoverSwapError::Irrecoverable(e) => { + return Ok(( + Some(MakerSwapCommand::Finish), + vec![MakerSwapEvent::MakerPaymentRefundFailed( + ERRL!("!maker_coin.recover_funds: {}", e).into(), + )], + )); + }, + // The error is temporary, retrying may fix the issue. + RecoverSwapError::Temporary(e) => { + error!("Error {} on recover_funds, retrying in 30 seconds", e); + Timer::sleep(30.).await; + }, + // We should wait for this many seconds and try again. + RecoverSwapError::WaitAndRetry(secs) => { + Timer::sleep(secs as f64).await; + }, + // The swap will be automatically recovered after the specified locktime. + RecoverSwapError::AutoRecoverableAfter(locktime) => { + let maker_payment = self.r().maker_payment.as_ref().unwrap().tx_hex.clone(); + match self.maker_coin.wait_for_htlc_refund(&maker_payment, locktime).await { + Ok(()) => { + return Ok(( + Some(MakerSwapCommand::FinalizeMakerPaymentRefund), + vec![MakerSwapEvent::MakerPaymentRefunded(None)], + )) + }, + Err(e) => { + error!("Error {} on wait_for_htlc_refund, retrying in 30 seconds", e); + Timer::sleep(30.).await; + }, + }; + }, }, } } - - let other_maker_coin_htlc_pub = self.r().other_maker_coin_htlc_pub; - let maker_coin_swap_contract_address = self.r().data.maker_coin_swap_contract_address.clone(); - let watcher_reward = self.r().watcher_reward; - let spend_result = self - .maker_coin - .send_maker_refunds_payment(RefundPaymentArgs { - payment_tx: &maker_payment, - time_lock: locktime, - other_pubkey: other_maker_coin_htlc_pub.as_slice(), - tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { - maker_secret_hash: self.secret_hash().as_slice(), - }, - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &self.unique_swap_data(), - watcher_reward, - }) - .await; - - let transaction = match spend_result { - Ok(t) => t, - Err(err) => { - if let Some(tx) = err.get_tx() { - broadcast_p2p_tx_msg( - &self.ctx, - tx_helper_topic(self.maker_coin.ticker()), - &tx, - &self.p2p_privkey, - ); - } - - return Ok(( - Some(MakerSwapCommand::Finish), - vec![MakerSwapEvent::MakerPaymentRefundFailed( - ERRL!( - "!maker_coin.send_maker_refunds_payment: {}", - err.get_plain_text_format() - ) - .into(), - )], - )); - }, - }; - - broadcast_p2p_tx_msg( - &self.ctx, - tx_helper_topic(self.maker_coin.ticker()), - &transaction, - &self.p2p_privkey, - ); - - let tx_hash = transaction.tx_hash_as_bytes(); - info!("Maker payment refund tx {:02x}", tx_hash); - let tx_ident = TransactionIdentifier { - tx_hex: BytesJson::from(transaction.tx_hex()), - tx_hash, - }; - - Ok(( - Some(MakerSwapCommand::FinalizeMakerPaymentRefund), - vec![MakerSwapEvent::MakerPaymentRefunded(Some(tx_ident))], - )) } async fn finalize_maker_payment_refund(&self) -> Result<(Option, Vec), String> { @@ -1546,13 +1539,13 @@ impl MakerSwap { Ok((swap, command)) } - pub async fn recover_funds(&self) -> Result { - async fn try_spend_taker_payment(selfi: &MakerSwap, secret_hash: &[u8]) -> Result { + pub async fn recover_funds(&self) -> MmResult { + async fn try_spend_taker_payment(selfi: &MakerSwap) -> MmResult { let taker_payment_hex = &selfi .r() .taker_payment .clone() - .ok_or(ERRL!("No info about taker payment, swap is not recoverable"))? + .ok_or(RecoverSwapError::Irrecoverable("taker payment not found".to_string()))? .tx_hex; // have to do this because std::sync::RwLockReadGuard returned by r() is not Send, @@ -1570,34 +1563,33 @@ impl MakerSwap { let search_input = SearchForSwapTxSpendInput { time_lock: timelock, other_pub: other_taker_coin_htlc_pub.as_slice(), - secret_hash, + secret_hash: &selfi.secret_hash(), tx: taker_payment_hex, search_from_block: taker_coin_start_block, swap_contract_address: &taker_coin_swap_contract_address, swap_unique_data: &unique_data, watcher_reward, }; - // check if the taker payment is not spent yet + // Check the taker payment status first match selfi.taker_coin.search_for_swap_tx_spend_other(search_input).await { Ok(Some(FoundSwapTxSpend::Spent(tx))) => { - return ERR!( - "Taker payment was already spent by {} tx {:02x}", - selfi.taker_coin.ticker(), - tx.tx_hash_as_bytes() - ) + // We already spent the taker payment. + return Ok(tx); }, Ok(Some(FoundSwapTxSpend::Refunded(tx))) => { - return ERR!( + // The taker refunded their payment. + warn!("TakerPayment was refunded back to the taker."); + return MmError::err(RecoverSwapError::Irrecoverable(format!( "Taker payment was already refunded by {} tx {:02x}", selfi.taker_coin.ticker(), tx.tx_hash_as_bytes() - ) + ))); }, - Err(e) => return ERR!("Error {} when trying to find taker payment spend", e), + Err(e) => return MmError::err(RecoverSwapError::Temporary(e)), Ok(None) => (), // payment is not spent, continue } - selfi + let send_result = selfi .taker_coin .send_maker_spends_taker_payment(SpendPaymentArgs { other_payment_tx: taker_payment_hex, @@ -1609,20 +1601,23 @@ impl MakerSwap { swap_unique_data: &selfi.unique_swap_data(), watcher_reward, }) - .await - .map_err(|e| ERRL!("{:?}", e)) - } - - if self.finished_at.load(Ordering::Relaxed) == 0 { - return ERR!("Swap must be finished before recover funds attempt"); - } + .await; - if self.r().maker_payment_refund.is_some() { - return ERR!("Maker payment is refunded, swap is not recoverable"); - } + match send_result { + Ok(tx) => Ok(tx), + Err(err) => { + if let Some(tx) = err.get_tx() { + broadcast_p2p_tx_msg( + &selfi.ctx, + tx_helper_topic(selfi.taker_coin.ticker()), + &tx, + &selfi.p2p_privkey, + ); + } - if self.r().taker_payment_spend.is_some() && self.r().taker_payment_spend_confirmed { - return ERR!("Taker payment spend transaction has been sent and confirmed"); + MmError::err(RecoverSwapError::Temporary(err.get_plain_text_format())) + }, + } } let secret_hash = self.secret_hash(); @@ -1641,23 +1636,23 @@ impl MakerSwap { let maker_payment = match maybe_maker_payment { Some(tx) => tx.tx_hex.0, None => { - let maybe_maker_payment = try_s!( - self.maker_coin - .check_if_my_payment_sent(CheckIfMyPaymentSentArgs { - time_lock: maker_payment_lock, - other_pub: other_maker_coin_htlc_pub.as_slice(), - secret_hash: secret_hash.as_slice(), - search_from_block: maker_coin_start_block, - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - amount: &self.maker_amount, - payment_instructions: &payment_instructions, - }) - .await - ); + let maybe_maker_payment = self + .maker_coin + .check_if_my_payment_sent(CheckIfMyPaymentSentArgs { + time_lock: maker_payment_lock, + other_pub: other_maker_coin_htlc_pub.as_slice(), + secret_hash: secret_hash.as_slice(), + search_from_block: maker_coin_start_block, + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &unique_data, + amount: &self.maker_amount, + payment_instructions: &payment_instructions, + }) + .await + .map_err(RecoverSwapError::Temporary)?; match maybe_maker_payment { Some(tx) => tx.tx_hex(), - None => return ERR!("Maker payment transaction was not found"), + None => return MmError::err(RecoverSwapError::PaymentTxNotFound), } }, }; @@ -1676,7 +1671,7 @@ impl MakerSwap { match self.maker_coin.search_for_swap_tx_spend_my(search_input).await { Ok(Some(FoundSwapTxSpend::Spent(_))) => { warn!("MakerPayment spent, but TakerPayment is not yet. Trying to spend TakerPayment"); - let transaction = try_s!(try_spend_taker_payment(self, secret_hash.as_slice()).await); + let transaction = try_spend_taker_payment(self).await?; Ok(RecoveredSwap { action: RecoveredSwapAction::SpentOtherPayment, @@ -1684,20 +1679,24 @@ impl MakerSwap { transaction, }) }, - Ok(Some(FoundSwapTxSpend::Refunded(tx))) => ERR!( - "Maker payment was already refunded by {} tx {:02x}", - self.maker_coin.ticker(), - tx.tx_hash_as_bytes() - ), - Err(e) => ERR!("Error {} when trying to find maker payment spend", e), + Ok(Some(FoundSwapTxSpend::Refunded(tx))) => Ok(RecoveredSwap { + action: RecoveredSwapAction::RefundedMyPayment, + coin: self.maker_coin.ticker().to_string(), + transaction: tx, + }), + Err(e) => MmError::err(RecoverSwapError::Temporary(e)), Ok(None) => { if self.maker_coin.is_auto_refundable() { - return ERR!("Maker payment will be refunded automatically!"); + return MmError::err(RecoverSwapError::AutoRecoverableAfter(maker_payment_lock)); } - let can_refund_htlc = try_s!(self.maker_coin.can_refund_htlc(maker_payment_lock).await); + let can_refund_htlc = self + .maker_coin + .can_refund_htlc(maker_payment_lock) + .await + .map_err(RecoverSwapError::Temporary)?; if let CanRefundHtlc::HaveToWait(seconds_to_wait) = can_refund_htlc { - return ERR!("Too early to refund, wait until {}", wait_until_sec(seconds_to_wait)); + return MmError::err(RecoverSwapError::WaitAndRetry(seconds_to_wait)); } let fut = self.maker_coin.send_maker_refunds_payment(RefundPaymentArgs { payment_tx: &maker_payment, @@ -1723,7 +1722,7 @@ impl MakerSwap { ); } - return ERR!("{}", err.get_plain_text_format()); + return MmError::err(RecoverSwapError::Temporary(err.get_plain_text_format())); }, }; @@ -2258,21 +2257,11 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { subscribe_to_topic(&ctx, swap_topic(&swap.uuid)); let mut status = ctx.log.status_handle(); let uuid_str = swap.uuid.to_string(); - macro_rules! swap_tags { - () => { - &[&"swap", &("uuid", uuid_str.as_str())] - }; - } let running_swap = Arc::new(swap); let swap_ctx = SwapsContext::from_ctx(&ctx).unwrap(); swap_ctx.init_msg_store(running_swap.uuid, running_swap.taker_pubkey); - // Register the swap in the running swaps map. - swap_ctx - .running_swaps - .lock() - .unwrap() - .insert(uuid, running_swap.clone()); - let mut swap_fut = Box::pin( + let mut swap_fut = Box::pin({ + let running_swap = running_swap.clone(); async move { loop { let res = running_swap.handle_command(command).await.expect("!handle_command"); @@ -2314,7 +2303,7 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { error!("[swap uuid={uuid_str}] {event:?}"); } - status.status(swap_tags!(), &event.status_str()); + status.status(&[&"swap", &("uuid", uuid_str.as_str())], &event.status_str()); running_swap.apply_event(event); } match res.0 { @@ -2323,23 +2312,37 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { }, None => { if let Err(e) = mark_swap_as_finished(ctx.clone(), running_swap.uuid).await { - error!("!mark_swap_finished({}): {}", uuid, e); + error!("!mark_swap_finished({}): {}", uuid_str, e); } - if let Err(e) = broadcast_my_swap_status(&ctx, uuid).await { - covered_error!("!broadcast_my_swap_status({}): {}", uuid, e); + if let Err(e) = broadcast_my_swap_status(&ctx, running_swap.uuid).await { + covered_error!("!broadcast_my_swap_status({}): {}", uuid_str, e); } + break; }, } } } - .fuse(), - ); - select! { - _swap = swap_fut => (), // swap finished normally - _touch = touch_loop => unreachable!("Touch loop can not stop!"), - }; + .fuse() + }); + + let (abortable, handle) = abortable(async move { + select! { + _swap = swap_fut => (), // swap finished normally + _touch = touch_loop => unreachable!("Touch loop can not stop!"), + } + }); + let uuid = running_swap.uuid; + swap_ctx + .running_swaps + .lock() + .unwrap() + .insert(uuid, (running_swap, handle.into())); + // Wait until the swap has finished (or interrupted, i.e. aborted/panic). + if abortable.await.is_err() { + info!("Swap uuid={} interrupted!", uuid); + } // Remove the swap from the running swaps map. swap_ctx.running_swaps.lock().unwrap().remove(&uuid); } @@ -2649,8 +2652,9 @@ mod maker_swap_tests { assert!(MAKER_REFUND_CALLED.load(Ordering::Relaxed)); } + /// The maker payment was already refunded. `recover_funds` won't error though and will return the refund transaction. #[test] - fn test_recover_funds_maker_payment_refund_errored_already_refunded() { + fn test_recover_funds_maker_payment_refund_already_refunded() { let ctx = mm_ctx_with_iguana(PASSPHRASE); // the swap ends up with MakerPaymentRefundFailed error @@ -2668,11 +2672,19 @@ mod maker_swap_tests { let maker_coin = MmCoinEnum::Test(TestCoin::default()); let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); - assert!(block_on(maker_swap.recover_funds()).is_err()); + let expected = Ok(RecoveredSwap { + action: RecoveredSwapAction::RefundedMyPayment, + coin: "ticker".to_string(), + transaction: eth_tx_for_test().into(), + }); + assert_eq!(block_on(maker_swap.recover_funds()), expected); } + /// The maker payment was spent by the taker and also the taker payment we refunded to the taker as well. + /// This is really not an imaginable scenario since the maker spends the takers payment first (and reveals the secret) + /// but could happen if the taker payment spend was re-orged and the taker refunded the taker payment after wards. #[test] - fn test_recover_funds_maker_payment_refund_errored_already_spent() { + fn test_recover_funds_maker_payment_refund_errored_already_refunded_to_taker() { let ctx = mm_ctx_with_iguana(PASSPHRASE); // the swap ends up with MakerPaymentRefundFailed error @@ -2700,8 +2712,8 @@ mod maker_swap_tests { let maker_coin = MmCoinEnum::Test(TestCoin::default()); let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); - let err = block_on(maker_swap.recover_funds()).expect_err("Expected an error"); - assert!(err.contains("Taker payment was already refunded")); + + assert_eq!(block_on(maker_swap.recover_funds()).unwrap_err().into_inner(), RecoverSwapError::Irrecoverable("Taker payment was already refunded by ticker tx 0869be3e5d4456a29d488a533ad6c118620fef450f36778aecf31d356ff8b41f".to_string())); assert!(SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED.load(Ordering::Relaxed)); assert!(SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED.load(Ordering::Relaxed)); } @@ -2731,7 +2743,7 @@ mod maker_swap_tests { let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let error = block_on(maker_swap.recover_funds()).unwrap_err(); - assert!(error.contains("Too early to refund")); + assert!(matches!(error.into_inner(), RecoverSwapError::WaitAndRetry(_))); assert!(MY_PAYMENT_SENT_CALLED.load(Ordering::Relaxed)); } @@ -2759,22 +2771,6 @@ mod maker_swap_tests { assert!(MY_PAYMENT_SENT_CALLED.load(Ordering::Relaxed)); } - #[test] - fn test_recover_funds_maker_swap_not_finished() { - let ctx = mm_ctx_with_iguana(PASSPHRASE); - - // return error if swap is not finished - let maker_saved_json = r#"{"error_events":["StartFailed","NegotiateFailed","TakerFeeValidateFailed","MakerPaymentTransactionFailed","MakerPaymentDataSendFailed","TakerPaymentValidateFailed","TakerPaymentSpendFailed","TakerPaymentSpendConfirmFailed","MakerPaymentRefunded","MakerPaymentRefundFailed"],"events":[{"event":{"data":{"lock_duration":7800,"maker_amount":"3.54932734","maker_coin":"KMD","maker_coin_start_block":1452970,"maker_payment_confirmations":1,"maker_payment_lock":1563759539,"my_persistent_pub":"031bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8","secret":"0000000000000000000000000000000000000000000000000000000000000000","started_at":1563743939,"taker":"101ace6b08605b9424b0582b5cce044b70a3c8d8d10cb2965e039b0967ae92b9","taker_amount":"0.02004833998671660000000000","taker_coin":"ETH","taker_coin_start_block":8196380,"taker_payment_confirmations":1,"uuid":"3447b727-fe93-4357-8e5a-8cf2699b7e86"},"type":"Started"},"timestamp":1563743939211},{"event":{"data":{"taker_payment_locktime":1563751737,"taker_pubkey":"03101ace6b08605b9424b0582b5cce044b70a3c8d8d10cb2965e039b0967ae92b9"},"type":"Negotiated"},"timestamp":1563743979835},{"event":{"data":{"tx_hash":"a59203eb2328827de00bed699a29389792906e4f39fdea145eb40dc6b3821bd6","tx_hex":"f8690284ee6b280082520894d8997941dd1346e9231118d5685d866294f59e5b865af3107a4000801ca0743d2b7c9fad65805d882179062012261be328d7628ae12ee08eff8d7657d993a07eecbd051f49d35279416778faa4664962726d516ce65e18755c9b9406a9c2fd"},"type":"TakerFeeValidated"},"timestamp":1563744052878}],"success_events":["Started","Negotiated","TakerFeeValidated","MakerPaymentSent","TakerPaymentReceived","TakerPaymentWaitConfirmStarted","TakerPaymentValidatedAndConfirmed","TakerPaymentSpent","TakerPaymentSpendConfirmStarted","TakerPaymentSpendConfirmed","Finished"],"uuid":"3447b727-fe93-4357-8e5a-8cf2699b7e86"}"#; - let maker_saved_swap: MakerSavedSwap = json::from_str(maker_saved_json).unwrap(); - - TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); - TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); - let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); - assert!(block_on(maker_swap.recover_funds()).is_err()); - } - #[test] fn test_recover_funds_maker_swap_taker_payment_spent() { let ctx = mm_ctx_with_iguana(PASSPHRASE); @@ -2805,28 +2801,16 @@ mod maker_swap_tests { let maker_coin = MmCoinEnum::Test(TestCoin::default()); let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); - let err = block_on(maker_swap.recover_funds()).expect_err("Expected an error"); - assert!(err.contains("Taker payment was already spent")); + let expected = Ok(RecoveredSwap { + action: RecoveredSwapAction::SpentOtherPayment, + coin: "ticker".to_string(), + transaction: eth_tx_for_test().into(), + }); + assert_eq!(block_on(maker_swap.recover_funds()), expected); assert!(SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED.load(Ordering::Relaxed)); assert!(SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED.load(Ordering::Relaxed)); } - #[test] - fn test_recover_funds_maker_swap_maker_payment_refunded() { - let ctx = mm_ctx_with_iguana(PASSPHRASE); - - // return error if maker payment was refunded - let maker_saved_json = r#"{"error_events":["StartFailed","NegotiateFailed","TakerFeeValidateFailed","MakerPaymentTransactionFailed","MakerPaymentDataSendFailed","TakerPaymentValidateFailed","TakerPaymentSpendFailed","TakerPaymentSpendConfirmFailed","MakerPaymentRefunded","MakerPaymentRefundFailed"],"events":[{"event":{"data":{"lock_duration":7800,"maker_amount":"9.38455187130897","maker_coin":"VRSC","maker_coin_start_block":604407,"maker_payment_confirmations":1,"maker_payment_lock":1564317372,"my_persistent_pub":"03c2e08e48e6541b3265ccd430c5ecec7efc7d0d9fc4e310a9b052f9642673fb0a","secret":"0000000000000000000000000000000000000000000000000000000000000000","started_at":1564301772,"taker":"39c4bcdb1e6bbb29a3b131c2b82eba2552f4f8a804021b2064114ab857f00848","taker_amount":"0.999999999999999880468812552729","taker_coin":"KMD","taker_coin_start_block":1462209,"taker_payment_confirmations":1,"uuid":"8f5b267a-efa8-49d6-a92d-ec0523cca891"},"type":"Started"},"timestamp":1564301773193},{"event":{"data":{"taker_payment_locktime":1564309572,"taker_pubkey":"0339c4bcdb1e6bbb29a3b131c2b82eba2552f4f8a804021b2064114ab857f00848"},"type":"Negotiated"},"timestamp":1564301813664},{"event":{"data":{"tx_hash":"cf54a5f5dfdf2eb404855eaba6a05b41f893a20327d43770c0138bb9ed2cf9eb","tx_hex":"0400008085202f89018f03a4d46831ec541279d01998be6092a98ee0f103b69ab84697cdc3eea7e93c000000006a473044022046eb76ecf610832ef063a6d210b5d07bc90fd0f3b68550fd2945ce86b317252a02202d3438d2e83df49f1c8ab741553af65a0d97e6edccbb6c4d0c769b05426c637001210339c4bcdb1e6bbb29a3b131c2b82eba2552f4f8a804021b2064114ab857f00848ffffffff0276c40100000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88acddf7bd54000000001976a9144df806990ae0197402aeaa6d9b1ec60078d9eadf88ac01573d5d000000000000000000000000000000"},"type":"TakerFeeValidated"},"timestamp":1564301864738},{"event":{"data":{"tx_hash":"2252c9929707995aff6dbb03d23b7e7eb786611d26b6ae748ca13007e71d1de6","tx_hex":"0400008085202f8901f63aed15c53b794df1a9446755f452e9fd9db250e1f608636f6172b7d795358c010000006b483045022100b5adb583fbb4b1a628b9c58ec292bb7b1319bb881c2cf018af6fe33b7a182854022020d89a2d6cbf15a117e2e1122046941f95466af7507883c4fa05955f0dfb81f2012103c2e08e48e6541b3265ccd430c5ecec7efc7d0d9fc4e310a9b052f9642673fb0affffffff0293b0ef370000000017a914ca41def369fc07d8aea10ba26cf3e64a12470d4087163149f61c0000001976a914f4f89313803d610fa472a5849d2389ca6df3b90088ac285a3d5d000000000000000000000000000000"},"type":"MakerPaymentSent"},"timestamp":1564301867675},{"event":{"data":{"error":"timeout (2690.6 > 2690.0)"},"type":"TakerPaymentValidateFailed"},"timestamp":1564304558269},{"event":{"data":{"tx_hash":"96d0b50bc2371ab88052bc4d656f1b91b3e3e64eba650eac28ebce9387d234cb","tx_hex":"0400008085202f8901e61d1de70730a18c74aeb6261d6186b77e7e3bd203bb6dff5a99079792c9522200000000b647304402207d36206295eee6c936d0204552cc5a001d4de4bbc0c5ae1c6218cf8548b4f08b02204c2a6470e06a6caf407ea8f2704fdc1b1dee39f89d145f8c0460130cb1875b2b01514c6b6304bc963d5db1752103c2e08e48e6541b3265ccd430c5ecec7efc7d0d9fc4e310a9b052f9642673fb0aac6782012088a9145f5598259da7c0c0beffcc3e9da35e553bac727388210339c4bcdb1e6bbb29a3b131c2b82eba2552f4f8a804021b2064114ab857f00848ac68feffffff01abacef37000000001976a914f4f89313803d610fa472a5849d2389ca6df3b90088ac26973d5d000000000000000000000000000000"},"type":"MakerPaymentRefunded"},"timestamp":1564321080407},{"event":{"type":"Finished"},"timestamp":1564321080409}],"success_events":["Started","Negotiated","TakerFeeValidated","MakerPaymentSent","TakerPaymentReceived","TakerPaymentWaitConfirmStarted","TakerPaymentValidatedAndConfirmed","TakerPaymentSpent","TakerPaymentSpendConfirmStarted","TakerPaymentSpendConfirmed","Finished"],"uuid":"8f5b267a-efa8-49d6-a92d-ec0523cca891"}"#; - let maker_saved_swap: MakerSavedSwap = json::from_str(maker_saved_json).unwrap(); - - TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); - TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); - let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); - assert!(block_on(maker_swap.recover_funds()).is_err()); - } - #[test] /// https://github.com/KomodoPlatform/atomicDEX-API/issues/774 fn test_recover_funds_my_payment_spent_other_not() { @@ -2874,46 +2858,6 @@ mod maker_swap_tests { assert!(SEND_MAKER_SPENDS_TAKER_PAYMENT_CALLED.load(Ordering::Relaxed)); } - #[test] - fn test_recover_funds_should_not_refund_on_the_successful_swap() { - let ctx = mm_ctx_with_iguana(PASSPHRASE); - - let maker_saved_json = r#"{"type":"Maker","uuid":"12456076-58dd-4772-9d88-167d5fa103d2","my_order_uuid":"5ae22bf5-09cf-4828-87a7-c3aa7339ba10","events":[{"timestamp":1631695364907,"event":{"type":"Started","data":{"taker_coin":"KMD","maker_coin":"TKL","taker":"2b20b92e19e9e11b07f8309cebb1fcd1cce1606be8ab0de2c1b91f979c937996","secret":"0000000000000000000000000000000000000000000000000000000000000000","secret_hash":"65a10bd6dbdf6ebf7ec1f3bfb7451cde0582f9cb","my_persistent_pub":"03789c206e830f9e0083571f79e80eb58601d37bde8abb0c380d81127613060b74","lock_duration":31200,"maker_amount":"500","taker_amount":"140.7","maker_payment_confirmations":1,"maker_payment_requires_nota":false,"taker_payment_confirmations":2,"taker_payment_requires_nota":true,"maker_payment_lock":1631757764,"uuid":"12456076-58dd-4772-9d88-167d5fa103d2","started_at":1631695364,"maker_coin_start_block":61066,"taker_coin_start_block":2569118,"maker_payment_trade_fee":{"coin":"TKL","amount":"0.00001","paid_from_trading_vol":false},"taker_payment_spend_trade_fee":{"coin":"KMD","amount":"0.00001","paid_from_trading_vol":true}}}},{"timestamp":1631695366908,"event":{"type":"Negotiated","data":{"taker_payment_locktime":1631726564,"taker_pubkey":"032b20b92e19e9e11b07f8309cebb1fcd1cce1606be8ab0de2c1b91f979c937996","maker_coin_swap_contract_addr":null,"taker_coin_swap_contract_addr":null}}},{"timestamp":1631695367917,"event":{"type":"TakerFeeValidated","data":{"tx_hex":"0400008085202f8901562fdec6bbdac4c5c3212394e1fd439d3647ff04bdd79d51b9bbf697c9a925e7000000006a473044022074c71fcdc12654e3aa01c780b10d6c84b1d6ba28f0db476010002a1ed00e75cf022018e115923b1c1b5e872893fd6a1f270c0e8e3e84a869181c349aa78553e1423b0121032b20b92e19e9e11b07f8309cebb1fcd1cce1606be8ab0de2c1b91f979c937996ffffffff0251adf800000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ac72731e4c080000001976a914dc1bea5367613f189da622e9bc5bdb2d61667e5b88ac08aa4161000000000000000000000000000000","tx_hash":"f315170aba20ff4d432b8a2d0a8fa0211444c8d27b56fc0d4fc2058e9f3c6e08"}}},{"timestamp":1631695368024,"event":{"type":"MakerPaymentSent","data":{"tx_hex":"0400008085202f8901bc488c4e0f9a3fe9d7f5dbcc17f61e7711a75c7ed277843988f3be4d236b9a02020000006a473044022027ac57a4a34b0d8561afc1ad63f9e1fb271d58577a80f26ba519017d65d882f802200b5617f32427b86b423de6740cd134fdb8b86c511943e778c65781573224cf4a012103789c206e830f9e0083571f79e80eb58601d37bde8abb0c380d81127613060b74ffffffff0300743ba40b00000017a914022be92579878d04c80d128cdfdcba4ed29a9f9a870000000000000000166a1465a10bd6dbdf6ebf7ec1f3bfb7451cde0582f9cb6494f93503c801001976a914bde146a76acf122caf5e460d01ddaf3be714247e88ac07b24161000000000000000000000000000000","tx_hash":"8693723462ef5ee6c3014230fd4a4aefe6bcd0eaeb727e1e5b33fe1105e9f8ad"}}},{"timestamp":1631696319310,"event":{"type":"TakerPaymentReceived","data":{"tx_hex":"0400008085202f8901086e3c9f8e05c24f0dfc567bd2c8441421a08f0a2d8a2b434dff20ba0a1715f3010000006a47304402207190691940b4834394c2a9e08a32b775f1c62a47ab76737c96c08e2937173988022040229094d51acb3d948413c349e36795b888a9b425b29ed7a96ed8eb97407d050121032b20b92e19e9e11b07f8309cebb1fcd1cce1606be8ab0de2c1b91f979c937996ffffffff038029a3460300000017a9146d0db00d111fcd0b83505cb805a3255cbaa8c747870000000000000000166a1465a10bd6dbdf6ebf7ec1f3bfb7451cde0582f9cb0a467b05050000001976a914dc1bea5367613f189da622e9bc5bdb2d61667e5b88acbfad4161000000000000000000000000000000","tx_hash":"a9b97c4c12c8eb637a7016459de644eae9e307efd2d051601d7d9f615fd62461"}}},{"timestamp":1631696319310,"event":{"type":"TakerPaymentWaitConfirmStarted"}},{"timestamp":1631697459816,"event":{"type":"TakerPaymentValidatedAndConfirmed"}},{"timestamp":1631697459821,"event":{"type":"TakerPaymentSpent","data":{"tx_hex":"0400008085202f89016124d65f619f7d1d6051d0d2ef07e3e9ea44e69d4516707a63ebc8124c7cb9a900000000d84830450221008c50c144382346247d7052a32e12f4d839fa22c12064b199d589cc62ead00c99022017e88f543e181fd92ebf32e1313ca6fb12f93226fd294c808b0904601102424f012068e659c506d57d94369ca520158d641ea997b0db39fdafb1e59b07867ad4be9d004c6b6304e42b4261b17521032b20b92e19e9e11b07f8309cebb1fcd1cce1606be8ab0de2c1b91f979c937996ac6782012088a91465a10bd6dbdf6ebf7ec1f3bfb7451cde0582f9cb882103789c206e830f9e0083571f79e80eb58601d37bde8abb0c380d81127613060b74ac68ffffffff019825a346030000001976a914bde146a76acf122caf5e460d01ddaf3be714247e88ace42b4261000000000000000000000000000000","tx_hash":"8a6d65518d3a01f6f659f11e0667373052ebfc2e600f80c6592dec556bee4a39"}}},{"timestamp":1631697459822,"event":{"type":"TakerPaymentSpendConfirmStarted"}},{"timestamp":1631697489840,"event":{"type":"TakerPaymentSpendConfirmed"}},{"timestamp":1631697489841,"event":{"type":"Finished"}}],"maker_amount":"500","maker_coin":"TKL","taker_amount":"140.7","taker_coin":"KMD","gui":"TOKEL-IDO","mm_version":"41170748d","success_events":["Started","Negotiated","TakerFeeValidated","MakerPaymentSent","TakerPaymentReceived","TakerPaymentWaitConfirmStarted","TakerPaymentValidatedAndConfirmed","TakerPaymentSpent","TakerPaymentSpendConfirmStarted","TakerPaymentSpendConfirmed","Finished"],"error_events":["StartFailed","NegotiateFailed","TakerFeeValidateFailed","MakerPaymentTransactionFailed","MakerPaymentDataSendFailed","MakerPaymentWaitConfirmFailed","TakerPaymentValidateFailed","TakerPaymentWaitConfirmFailed","TakerPaymentSpendFailed","TakerPaymentSpendConfirmFailed","MakerPaymentWaitRefundStarted","MakerPaymentRefunded","MakerPaymentRefundFailed"]}"#; - let maker_saved_swap: MakerSavedSwap = json::from_str(maker_saved_json).unwrap(); - - TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); - TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - - static SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED: AtomicBool = AtomicBool::new(false); - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _| { - SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED.store(true, Ordering::Relaxed); - MockResult::Return(Box::pin(futures::future::ready(Ok(Some(FoundSwapTxSpend::Spent( - eth_tx_for_test().into(), - )))))) - }); - - static SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED: AtomicBool = AtomicBool::new(false); - TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _| { - SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED.store(true, Ordering::Relaxed); - MockResult::Return(Box::pin(futures::future::ready(Ok(None)))) - }); - - static SEND_MAKER_REFUNDS_PAYMENT_CALLED: AtomicBool = AtomicBool::new(false); - TestCoin::send_maker_refunds_payment.mock_safe(|_, _| { - SEND_MAKER_REFUNDS_PAYMENT_CALLED.store(true, Ordering::Relaxed); - MockResult::Return(Box::pin(futures::future::ok(eth_tx_for_test().into()))) - }); - - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); - let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); - let err = block_on(maker_swap.recover_funds()).unwrap_err(); - assert!(err.contains("Taker payment spend transaction has been sent and confirmed")); - assert!(!SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED.load(Ordering::Relaxed)); - assert!(!SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED.load(Ordering::Relaxed)); - assert!(!SEND_MAKER_REFUNDS_PAYMENT_CALLED.load(Ordering::Relaxed)); - } - #[test] fn swap_must_not_lock_funds_by_default() { use crate::lp_swap::get_locked_amount; diff --git a/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs b/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs index 9915978152..d23670f254 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs @@ -4,10 +4,12 @@ use super::my_swaps_storage::{MySwapsError, MySwapsOps, MySwapsStorage}; use super::taker_swap::TakerSavedSwap; use super::taker_swap_v2::TakerSwapEvent; use super::{ - active_swaps, MySwapsFilter, SavedSwap, SavedSwapError, SavedSwapIo, LEGACY_SWAP_TYPE, MAKER_SWAP_V2_TYPE, - TAKER_SWAP_V2_TYPE, + active_swaps, run_maker_swap, run_taker_swap, MySwapsFilter, RunMakerSwapInput, RunTakerSwapInput, SavedSwap, + SavedSwapError, SavedSwapIo, SwapsContext, LEGACY_SWAP_TYPE, MAKER_SWAP_V2_TYPE, TAKER_SWAP_V2_TYPE, }; -use common::log::{error, warn}; +use coins::lp_coinfind; +use common::executor::SpawnFuture; +use common::log::{error, info, warn}; use common::{calc_total_pages, HttpStatusCode, PagingOptions}; use derive_more::Display; use http::StatusCode; @@ -28,7 +30,6 @@ cfg_native!( ); cfg_wasm32!( - use super::SwapsContext; use super::maker_swap_v2::MakerSwapDbRepr; use super::taker_swap_v2::TakerSwapDbRepr; use crate::lp_swap::swap_wasm_db::{MySwapsFiltersTable, SavedSwapTable}; @@ -534,3 +535,144 @@ pub(crate) async fn active_swaps_rpc( statuses, }) } + +#[derive(Deserialize)] +pub(crate) struct StopSwapRequest { + uuid: Uuid, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub(crate) enum StopSwapErr { + Internal(String), + NotRunning, +} + +impl HttpStatusCode for StopSwapErr { + fn status_code(&self) -> StatusCode { + match self { + StopSwapErr::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + StopSwapErr::NotRunning => StatusCode::BAD_REQUEST, + } + } +} + +#[derive(Serialize)] +pub(crate) struct StopSwapResponse { + result: String, +} + +pub(crate) async fn stop_swap_rpc(ctx: MmArc, req: StopSwapRequest) -> MmResult { + let swap_ctx = SwapsContext::from_ctx(&ctx).map_err(StopSwapErr::Internal)?; + // By just removing the swap's abort handle from the running swaps map, the swap will terminate. + if swap_ctx.running_swaps.lock().unwrap().remove(&req.uuid).is_none() { + return MmError::err(StopSwapErr::NotRunning); + } + info!("Swap {} stopped via RPC", req.uuid); + Ok(StopSwapResponse { + result: "Success".to_string(), + }) +} + +#[derive(Deserialize)] +pub(crate) struct KickStartSwapRequest { + uuid: Uuid, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub(crate) enum KickStartSwapErr { + Internal(String), + AlreadyRunning, + AlreadyFinished, + NeedsCoinActivation(String), + NotFound, +} + +impl HttpStatusCode for KickStartSwapErr { + fn status_code(&self) -> StatusCode { + match self { + KickStartSwapErr::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + KickStartSwapErr::AlreadyRunning + | KickStartSwapErr::AlreadyFinished + | KickStartSwapErr::NeedsCoinActivation(_) => StatusCode::BAD_REQUEST, + KickStartSwapErr::NotFound => StatusCode::NOT_FOUND, + } + } +} + +#[derive(Serialize)] +pub(crate) struct KickStartSwapResponse { + result: String, +} + +pub(crate) async fn kickstart_swap_rpc( + ctx: MmArc, + req: KickStartSwapRequest, +) -> MmResult { + // Make sure first that the swap isn't already running. Note that this is not atomic and we might still end + // up with the same swap being kickstarted twice, but we have filesystem swap locks for that. This check is + // rather for convenience. + let swap_ctx = SwapsContext::from_ctx(&ctx).map_err(KickStartSwapErr::Internal)?; + if swap_ctx.running_swaps.lock().unwrap().contains_key(&req.uuid) { + return MmError::err(KickStartSwapErr::AlreadyRunning); + } + // Load the swap from the DB. + let swap = match SavedSwap::load_my_swap_from_db(&ctx, None, req.uuid).await { + Ok(Some(s)) => s, + Ok(None) => { + return MmError::err(KickStartSwapErr::NotFound); + }, + Err(e) => { + return MmError::err(KickStartSwapErr::Internal(format!( + "Error getting the swap from the DB: {e}" + ))) + }, + }; + // Make sure that the swap isn't finished. + if swap.is_finished() { + return MmError::err(KickStartSwapErr::AlreadyFinished); + } + // Get the maker and taker coins. + let find_swap_coin = |ctx: MmArc, maybe_ticker: Result, ticker_type: &'static str| async move { + match maybe_ticker { + Ok(coin) => match lp_coinfind(&ctx, &coin).await { + Ok(Some(coin)) => Ok(coin), + Ok(None) => MmError::err(KickStartSwapErr::NeedsCoinActivation(format!( + "{ticker_type} coin {} must be activated", + coin + ))), + Err(e) => MmError::err(KickStartSwapErr::Internal(format!( + "Error trying to find {ticker_type} coin: {e}" + ))), + }, + Err(e) => MmError::err(KickStartSwapErr::Internal(format!( + "Error getting {ticker_type} ticker of swap: {e}" + ))), + } + }; + let maker_coin = find_swap_coin(ctx.clone(), swap.maker_coin_ticker(), "maker").await?; + let taker_coin = find_swap_coin(ctx.clone(), swap.taker_coin_ticker(), "taker").await?; + // Kickstart the swap. A new abort handle will show up shortly for the swap. + match swap { + SavedSwap::Maker(saved_swap) => ctx.spawner().spawn(run_maker_swap( + RunMakerSwapInput::KickStart { + maker_coin, + taker_coin, + swap_uuid: saved_swap.uuid, + }, + ctx, + )), + SavedSwap::Taker(saved_swap) => ctx.spawner().spawn(run_taker_swap( + RunTakerSwapInput::KickStart { + maker_coin, + taker_coin, + swap_uuid: saved_swap.uuid, + }, + ctx, + )), + } + Ok(KickStartSwapResponse { + result: "Success".to_string(), + }) +} diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 9484f0c811..cdee8756c3 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -19,7 +19,7 @@ use crate::lp_swap::swap_v2_common::mark_swap_as_finished; use crate::lp_swap::taker_restart::get_command_based_on_maker_or_watcher_activity; use crate::lp_swap::{ broadcast_p2p_tx_msg, broadcast_swap_msg_every_delayed, tx_helper_topic, wait_for_maker_payment_conf_duration, - TakerSwapWatcherData, MAX_STARTED_AT_DIFF, + RecoverSwapError, TakerSwapWatcherData, MAX_STARTED_AT_DIFF, }; use coins::lp_price::fetch_swap_coins_price; use coins::{ @@ -30,8 +30,9 @@ use coins::{ }; use common::executor::Timer; use common::log::{debug, error, info, warn}; -use common::{bits256, now_ms, now_sec, wait_until_sec}; +use common::{bits256, now_ms, now_sec}; use crypto::{privkey::SerializableSecp256k1Keypair, CryptoCtx}; +use futures::future::abortable; use futures::{compat::Future01CompatExt, future::try_join, select, FutureExt}; use http::Response; use keys::KeyPair; @@ -462,13 +463,8 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { let running_swap = Arc::new(swap); let swap_ctx = SwapsContext::from_ctx(&ctx).unwrap(); swap_ctx.init_msg_store(running_swap.uuid, running_swap.maker_pubkey); - // Register the swap in the running swaps map. - swap_ctx - .running_swaps - .lock() - .unwrap() - .insert(uuid, running_swap.clone()); - let mut swap_fut = Box::pin( + let mut swap_fut = Box::pin({ + let running_swap = running_swap.clone(); async move { let mut events; loop { @@ -525,12 +521,25 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { } } } - .fuse(), - ); - select! { - _swap = swap_fut => (), // swap finished normally - _touch = touch_loop => unreachable!("Touch loop can not stop!"), - }; + .fuse() + }); + + let (abortable, handle) = abortable(async move { + select! { + _swap = swap_fut => (), // swap finished normally + _touch = touch_loop => unreachable!("Touch loop can not stop!"), + } + }); + let uuid = running_swap.uuid; + swap_ctx + .running_swaps + .lock() + .unwrap() + .insert(uuid, (running_swap, handle.into())); + // Wait until the swap has finished (or interrupted, i.e. aborted/panic). + if abortable.await.is_err() { + info!("Swap uuid={} interrupted!", uuid); + } // Remove the swap from the running swaps map. swap_ctx.running_swaps.lock().unwrap().remove(&uuid); } @@ -2081,92 +2090,85 @@ impl TakerSwap { panic!("{}", REFUND_TEST_FAILURE_LOG); } - let taker_payment = self.r().taker_payment.clone().unwrap().tx_hex; - let locktime = self.r().data.taker_payment_lock; - if self.taker_coin.is_auto_refundable() { - return match self.taker_coin.wait_for_htlc_refund(&taker_payment, locktime).await { - Ok(()) => Ok(( - Some(TakerSwapCommand::FinalizeTakerPaymentRefund), - vec![TakerSwapEvent::TakerPaymentRefunded(None)], - )), - Err(err) => Ok(( - Some(TakerSwapCommand::Finish), - vec![TakerSwapEvent::TakerPaymentRefundFailed( - ERRL!("!taker_coin.wait_for_htlc_refund: {}", err.to_string()).into(), - )], - )), - }; - } - + // Keep trying to recover funds (by refunding the taker payment or spending the maker payment) until successful or face an irrecoverable error. loop { - match self.taker_coin.can_refund_htlc(locktime).await { - Ok(CanRefundHtlc::CanRefundNow) => break, - Ok(CanRefundHtlc::HaveToWait(to_sleep)) => Timer::sleep(to_sleep as f64).await, - Err(e) => { - error!("Error {} on can_refund_htlc, retrying in 30 seconds", e); - Timer::sleep(30.).await; + match self.recover_funds().await { + // We recovered the swap successfully. + Ok(recovered_swap) => { + let tx_ident = TransactionIdentifier { + tx_hex: recovered_swap.transaction.tx_hex().into(), + tx_hash: recovered_swap.transaction.tx_hash_as_bytes(), + }; + return match recovered_swap.action { + // We recovered the swap by refunding the taker payment. + RecoveredSwapAction::RefundedMyPayment => { + info!("Taker payment refund tx {:02x}", tx_ident.tx_hash); + Ok(( + Some(TakerSwapCommand::FinalizeTakerPaymentRefund), + vec![TakerSwapEvent::TakerPaymentRefunded(Some(tx_ident))], + )) + }, + // We recovered the swap by proceeding forward and spending the maker payment. The swap wasn't actually a failure. + // Roll back to confirming the maker payment spend. + RecoveredSwapAction::SpentOtherPayment => { + info!("Refund canceled. Maker payment spend tx {:02x}", tx_ident.tx_hash); + // We better find a way to rollback the state machine and remove erroneous events, + // the swap at this point will be marked as errored but in fact it recovered from the error. + Ok(( + Some(TakerSwapCommand::ConfirmMakerPaymentSpend), + vec![TakerSwapEvent::MakerPaymentSpent(tx_ident)], + )) + }, + }; + }, + // Encountered an error during swap recover. + Err(err) => match err.into_inner() { + // The payment tx we want to refund isn't even on-chain. There is nothing to refund/spend. + RecoverSwapError::PaymentTxNotFound => { + return Ok(( + Some(TakerSwapCommand::Finish), + vec![TakerSwapEvent::TakerPaymentRefundFailed( + "TakerPayment isn't even on-chain to refund it.".into(), + )], + )); + }, + // The error is unrecoverable, retrying will not fix the issue. + RecoverSwapError::Irrecoverable(e) => { + return Ok(( + Some(TakerSwapCommand::Finish), + vec![TakerSwapEvent::TakerPaymentRefundFailed( + ERRL!("!taker_coin.recover_funds: {}", e).into(), + )], + )); + }, + // The error is temporary, retrying may fix the issue. + RecoverSwapError::Temporary(e) => { + error!("Error {} on recover_funds, retrying in 30 seconds", e); + Timer::sleep(30.).await; + }, + // We should wait for this many seconds and try again. + RecoverSwapError::WaitAndRetry(secs) => { + Timer::sleep(secs as f64).await; + }, + // The swap will be automatically recovered after the specified locktime. + RecoverSwapError::AutoRecoverableAfter(locktime) => { + let taker_payment = self.r().taker_payment.as_ref().unwrap().tx_hex.clone(); + match self.taker_coin.wait_for_htlc_refund(&taker_payment, locktime).await { + Ok(()) => { + return Ok(( + Some(TakerSwapCommand::FinalizeTakerPaymentRefund), + vec![TakerSwapEvent::TakerPaymentRefunded(None)], + )) + }, + Err(e) => { + error!("Error {} on wait_for_htlc_refund, retrying in 30 seconds", e); + Timer::sleep(30.).await; + }, + }; + }, }, } } - - let other_taker_coin_htlc_pub = self.r().other_taker_coin_htlc_pub; - let secret_hash = self.r().secret_hash.clone(); - let swap_contract_address = self.r().data.taker_coin_swap_contract_address.clone(); - let watcher_reward = self.r().watcher_reward; - let refund_result = self - .taker_coin - .send_taker_refunds_payment(RefundPaymentArgs { - payment_tx: &taker_payment, - time_lock: locktime, - other_pubkey: other_taker_coin_htlc_pub.as_slice(), - tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { - maker_secret_hash: &secret_hash, - }, - swap_contract_address: &swap_contract_address, - swap_unique_data: &self.unique_swap_data(), - watcher_reward, - }) - .await; - - let transaction = match refund_result { - Ok(t) => t, - Err(err) => { - if let Some(tx) = err.get_tx() { - broadcast_p2p_tx_msg( - &self.ctx, - tx_helper_topic(self.taker_coin.ticker()), - &tx, - &self.p2p_privkey, - ); - } - - return Ok(( - Some(TakerSwapCommand::Finish), - vec![TakerSwapEvent::TakerPaymentRefundFailed( - ERRL!("{:?}", err.get_plain_text_format()).into(), - )], - )); - }, - }; - - broadcast_p2p_tx_msg( - &self.ctx, - tx_helper_topic(self.taker_coin.ticker()), - &transaction, - &self.p2p_privkey, - ); - - let tx_hash = transaction.tx_hash_as_bytes(); - info!("Taker refund tx hash {:02x}", tx_hash); - let tx_ident = TransactionIdentifier { - tx_hex: BytesJson::from(transaction.tx_hex()), - tx_hash, - }; - - Ok(( - Some(TakerSwapCommand::FinalizeTakerPaymentRefund), - vec![TakerSwapEvent::TakerPaymentRefunded(Some(tx_ident))], - )) } async fn finalize_taker_payment_refund(&self) -> Result<(Option, Vec), String> { @@ -2282,138 +2284,123 @@ impl TakerSwap { Ok((swap, Some(command))) } - pub async fn recover_funds(&self) -> Result { - if self.finished_at.load(Ordering::Relaxed) == 0 { - return ERR!("Swap must be finished before recover funds attempt"); - } + pub async fn recover_funds(&self) -> MmResult { + async fn try_spend_maker_payment( + selfi: &TakerSwap, + secret: &[u8], + ) -> MmResult { + let maker_payment = match &selfi.r().maker_payment { + Some(tx) => tx.tx_hex.0.clone(), + None => return MmError::err(RecoverSwapError::Irrecoverable("maker payment not found".to_string())), + }; + let other_maker_coin_htlc_pub = selfi.r().other_maker_coin_htlc_pub; + let secret_hash = selfi.r().secret_hash.0.clone(); + let maker_coin_start_block = selfi.r().data.maker_coin_start_block; + let maker_coin_swap_contract_address = selfi.r().data.maker_coin_swap_contract_address.clone(); + let watcher_reward = selfi.r().watcher_reward; + let unique_data = selfi.unique_swap_data(); + + let search_input = SearchForSwapTxSpendInput { + time_lock: selfi.maker_payment_lock.load(Ordering::Relaxed), + other_pub: other_maker_coin_htlc_pub.as_slice(), + secret_hash: &secret_hash, + tx: &maker_payment, + search_from_block: maker_coin_start_block, + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &unique_data, + watcher_reward, + }; - if self.r().taker_payment_refund.is_some() { - return ERR!("Taker payment is refunded, swap is not recoverable"); - } + match selfi.maker_coin.search_for_swap_tx_spend_other(search_input).await { + Ok(Some(FoundSwapTxSpend::Spent(tx))) => { + // We already spent the maker payment. + return Ok(tx); + }, + Ok(Some(FoundSwapTxSpend::Refunded(tx))) => { + // The maker refunded their payment. + warn!("MakerPayment was refunded back to the maker."); + return MmError::err(RecoverSwapError::Irrecoverable(format!( + "Maker payment was already refunded by {} tx {:02x}", + selfi.maker_coin.ticker(), + tx.tx_hash_as_bytes() + ))); + }, + Err(e) => return MmError::err(RecoverSwapError::Temporary(e)), + Ok(None) => (), // payment is not spent, continue + } - if self.r().maker_payment_spend.is_some() && self.r().maker_payment_spend_confirmed { - return ERR!("Maker payment is spent and confirmed, swap is not recoverable"); - } + let maybe_spend_tx = selfi + .maker_coin + .send_taker_spends_maker_payment(SpendPaymentArgs { + other_payment_tx: &maker_payment, + time_lock: selfi.maker_payment_lock.load(Ordering::Relaxed), + other_pubkey: other_maker_coin_htlc_pub.as_slice(), + secret, + secret_hash: &secret_hash, + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &unique_data, + watcher_reward, + }) + .await; - let maker_payment = match &self.r().maker_payment { - Some(tx) => tx.tx_hex.0.clone(), - None => return ERR!("No info about maker payment, swap is not recoverable"), - }; + match maybe_spend_tx { + Ok(tx) => Ok(tx), + Err(err) => { + if let Some(tx) = err.get_tx() { + broadcast_p2p_tx_msg( + &selfi.ctx, + tx_helper_topic(selfi.maker_coin.ticker()), + &tx, + &selfi.p2p_privkey, + ); + } + + MmError::err(RecoverSwapError::Temporary(err.get_plain_text_format())) + }, + } + } // have to do this because std::sync::RwLockReadGuard returned by r() is not Send, // so it can't be used across await - let other_maker_coin_htlc_pub = self.r().other_maker_coin_htlc_pub; let other_taker_coin_htlc_pub = self.r().other_taker_coin_htlc_pub; let secret_hash = self.r().secret_hash.0.clone(); - let maker_coin_start_block = self.r().data.maker_coin_start_block; - let maker_coin_swap_contract_address = self.r().data.maker_coin_swap_contract_address.clone(); - let taker_payment_lock = self.r().data.taker_payment_lock; let taker_coin_start_block = self.r().data.taker_coin_start_block; let taker_coin_swap_contract_address = self.r().data.taker_coin_swap_contract_address.clone(); let watcher_reward = self.r().watcher_reward; - let unique_data = self.unique_swap_data(); - macro_rules! check_maker_payment_is_not_spent { - // validate that maker payment is not spent - () => { - let search_input = SearchForSwapTxSpendInput { - time_lock: self.maker_payment_lock.load(Ordering::Relaxed), - other_pub: other_maker_coin_htlc_pub.as_slice(), - secret_hash: &secret_hash, - tx: &maker_payment, - search_from_block: maker_coin_start_block, - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - watcher_reward, - }; - - match self.maker_coin.search_for_swap_tx_spend_other(search_input).await { - Ok(Some(FoundSwapTxSpend::Spent(tx))) => { - return ERR!( - "Maker payment was already spent by {} tx {:02x}", - self.maker_coin.ticker(), - tx.tx_hash_as_bytes() - ) - }, - Ok(Some(FoundSwapTxSpend::Refunded(tx))) => { - return ERR!( - "Maker payment was already refunded by {} tx {:02x}", - self.maker_coin.ticker(), - tx.tx_hash_as_bytes() - ) - }, - Err(e) => return ERR!("Error {} when trying to find maker payment spend", e), - Ok(None) => (), // payment is not spent, continue - } - }; - } - - let maybe_taker_payment = self.r().taker_payment.clone(); let payment_instructions = self.r().payment_instructions.clone(); + let maybe_taker_payment = self.r().taker_payment.clone(); let taker_payment = match maybe_taker_payment { Some(tx) => tx.tx_hex.0, None => { - let maybe_sent = try_s!( - self.taker_coin - .check_if_my_payment_sent(CheckIfMyPaymentSentArgs { - time_lock: taker_payment_lock, - other_pub: other_taker_coin_htlc_pub.as_slice(), - secret_hash: &secret_hash, - search_from_block: taker_coin_start_block, - swap_contract_address: &taker_coin_swap_contract_address, - swap_unique_data: &unique_data, - amount: &self.taker_amount.to_decimal(), - payment_instructions: &payment_instructions, - }) - .await - ); + let maybe_sent = self + .taker_coin + .check_if_my_payment_sent(CheckIfMyPaymentSentArgs { + time_lock: taker_payment_lock, + other_pub: other_taker_coin_htlc_pub.as_slice(), + secret_hash: &secret_hash, + search_from_block: taker_coin_start_block, + swap_contract_address: &taker_coin_swap_contract_address, + swap_unique_data: &unique_data, + amount: &self.taker_amount.to_decimal(), + payment_instructions: &payment_instructions, + }) + .await + .map_err(RecoverSwapError::Temporary)?; match maybe_sent { Some(tx) => tx.tx_hex(), - None => return ERR!("Taker payment is not found, swap is not recoverable"), + None => return MmError::err(RecoverSwapError::PaymentTxNotFound), } }, }; + // If we already have the taker payment spend, we can use the secret in it to spend the maker payment. if self.r().taker_payment_spend.is_some() { - check_maker_payment_is_not_spent!(); - // has to do this because std::sync::RwLockReadGuard returned by r() is not Send, - // so it can't be used across await - let other_maker_coin_htlc_pub = self.r().other_maker_coin_htlc_pub; + // Since we have the taker payment spend, we also already have the secret so no need to query let secret = self.r().secret.0; - let maker_coin_swap_contract_address = self.r().data.maker_coin_swap_contract_address.clone(); - let watcher_reward = self.r().watcher_reward; - - let maybe_spend_tx = self - .maker_coin - .send_taker_spends_maker_payment(SpendPaymentArgs { - other_payment_tx: &maker_payment, - time_lock: self.maker_payment_lock.load(Ordering::Relaxed), - other_pubkey: other_maker_coin_htlc_pub.as_slice(), - secret: &secret, - secret_hash: &secret_hash, - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - watcher_reward, - }) - .await; - - let transaction = match maybe_spend_tx { - Ok(t) => t, - Err(err) => { - if let Some(tx) = err.get_tx() { - broadcast_p2p_tx_msg( - &self.ctx, - tx_helper_topic(self.maker_coin.ticker()), - &tx, - &self.p2p_privkey, - ); - } - - return ERR!("{}", err.get_plain_text_format()); - }, - }; + let transaction = try_spend_maker_payment(self, &secret).await?; return Ok(RecoveredSwap { action: RecoveredSwapAction::SpentOtherPayment, @@ -2422,6 +2409,7 @@ impl TakerSwap { }); } + // We don't have the taker payment spend, thus we will need to look for it on-chain and extract the secret from it. let search_input = SearchForSwapTxSpendInput { time_lock: taker_payment_lock, other_pub: other_taker_coin_htlc_pub.as_slice(), @@ -2432,50 +2420,18 @@ impl TakerSwap { swap_unique_data: &unique_data, watcher_reward, }; - let taker_payment_spend = try_s!(self.taker_coin.search_for_swap_tx_spend_my(search_input).await); - - match taker_payment_spend { - Some(spend) => match spend { + match self.taker_coin.search_for_swap_tx_spend_my(search_input).await { + Ok(Some(spend)) => match spend { FoundSwapTxSpend::Spent(tx) => { - check_maker_payment_is_not_spent!(); let secret_hash = self.r().secret_hash.clone(); let tx_hex = tx.tx_hex(); - let secret = try_s!( - self.taker_coin - .extract_secret(&secret_hash.0, &tx_hex, watcher_reward) - .await - ); - - let taker_spends_payment_args = SpendPaymentArgs { - other_payment_tx: &maker_payment, - time_lock: self.maker_payment_lock.load(Ordering::Relaxed), - other_pubkey: other_maker_coin_htlc_pub.as_slice(), - secret: &secret, - secret_hash: &secret_hash, - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - watcher_reward: self.r().watcher_reward, - }; - let maybe_spend_tx = self - .maker_coin - .send_taker_spends_maker_payment(taker_spends_payment_args) - .await; - - let transaction = match maybe_spend_tx { - Ok(t) => t, - Err(err) => { - if let Some(tx) = err.get_tx() { - broadcast_p2p_tx_msg( - &self.ctx, - tx_helper_topic(self.maker_coin.ticker()), - &tx, - &self.p2p_privkey, - ); - } + let secret = self + .taker_coin + .extract_secret(&secret_hash.0, &tx_hex, watcher_reward) + .await + .map_err(RecoverSwapError::Irrecoverable)?; - return ERR!("{}", err.get_plain_text_format()); - }, - }; + let transaction = try_spend_maker_payment(self, &secret).await?; Ok(RecoveredSwap { action: RecoveredSwapAction::SpentOtherPayment, @@ -2483,21 +2439,25 @@ impl TakerSwap { transaction, }) }, - FoundSwapTxSpend::Refunded(tx) => ERR!( - "Taker payment has been refunded already by transaction {:02x}", - tx.tx_hash_as_bytes() - ), + FoundSwapTxSpend::Refunded(tx) => Ok(RecoveredSwap { + action: RecoveredSwapAction::RefundedMyPayment, + coin: self.taker_coin.ticker().to_string(), + transaction: tx, + }), }, - None => { + Ok(None) => { if self.taker_coin.is_auto_refundable() { - return ERR!("Taker payment will be refunded automatically!"); + return MmError::err(RecoverSwapError::AutoRecoverableAfter(taker_payment_lock)); } - let can_refund = try_s!(self.taker_coin.can_refund_htlc(taker_payment_lock).await); + let can_refund = self + .taker_coin + .can_refund_htlc(taker_payment_lock) + .await + .map_err(RecoverSwapError::Temporary)?; if let CanRefundHtlc::HaveToWait(seconds_to_wait) = can_refund { - return ERR!("Too early to refund, wait until {}", wait_until_sec(seconds_to_wait)); + return MmError::err(RecoverSwapError::WaitAndRetry(seconds_to_wait)); } - let fut = self.taker_coin.send_taker_refunds_payment(RefundPaymentArgs { payment_tx: &taker_payment, time_lock: taker_payment_lock, @@ -2522,7 +2482,7 @@ impl TakerSwap { ); } - return ERR!("{:?}", err.get_plain_text_format()); + return MmError::err(RecoverSwapError::Temporary(err.get_plain_text_format())); }, }; @@ -2532,6 +2492,7 @@ impl TakerSwap { transaction, }) }, + Err(e) => MmError::err(RecoverSwapError::Temporary(e)), } } } @@ -2973,6 +2934,7 @@ mod taker_swap_tests { use coins::eth::{addr_from_str, signed_eth_tx_from_bytes, SignedEthTx}; use coins::utxo::UtxoTx; use coins::{FoundSwapTxSpend, MarketCoinOps, MmCoin, SwapOps, TestCoin}; + use common::executor::spawn_abortable; use common::{block_on, new_uuid}; use mm2_test_helpers::for_tests::{mm_ctx_with_iguana, ETH_SEPOLIA_SWAP_CONTRACT}; use mocktopus::mocking::*; @@ -3206,7 +3168,7 @@ mod taker_swap_tests { )) .unwrap(); let error = block_on(taker_swap.recover_funds()).unwrap_err(); - assert!(error.contains("Too early to refund")); + assert!(matches!(error.into_inner(), RecoverSwapError::WaitAndRetry(_))); assert!(SEARCH_TX_SPEND_CALLED.load(Ordering::Relaxed)); } @@ -3256,28 +3218,6 @@ mod taker_swap_tests { assert!(MAKER_PAYMENT_SPEND_CALLED.load(Ordering::Relaxed)); } - #[test] - fn test_recover_funds_taker_swap_not_finished() { - let ctx = mm_ctx_with_iguana(PASSPHRASE); - - // the json doesn't have Finished event at the end - let taker_saved_json = r#"{"error_events":["StartFailed","NegotiateFailed","TakerFeeSendFailed","MakerPaymentValidateFailed","TakerPaymentTransactionFailed","TakerPaymentDataSendFailed","TakerPaymentWaitForSpendFailed","MakerPaymentSpendFailed","MakerPaymentSpendConfirmFailed","TakerPaymentRefunded","TakerPaymentRefundedByWatcher","TakerPaymentRefundFailed"],"events":[{"event":{"data":{"lock_duration":7800,"maker":"1bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8","maker_amount":"0.12596566232185483","maker_coin":"KMD","maker_coin_start_block":1458035,"maker_payment_confirmations":1,"maker_payment_wait":1564053079,"my_persistent_pub":"0326846707a52a233cfc49a61ef51b1698bbe6aa78fa8b8d411c02743c09688f0a","started_at":1564050479,"taker_amount":"50.000000000000001504212457800000","taker_coin":"DOGE","taker_coin_start_block":2823448,"taker_payment_confirmations":1,"taker_payment_lock":1564058279,"uuid":"41383f43-46a5-478c-9386-3b2cce0aca20"},"type":"Started"},"timestamp":1564050480269},{"event":{"data":{"maker_payment_locktime":1564066080,"maker_pubkey":"031bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8","secret_hash":"3669eb83a007a3c507448d79f45a9f06ec2f36a8"},"type":"Negotiated"},"timestamp":1564050540991},{"event":{"data":{"tx_hash":"bdde828b492d6d1cc25cd2322fd592dafd722fcc7d8b0fedce4d3bb4a1a8c8ff","tx_hex":"0100000002c7efa995c8b7be0a8b6c2d526c6c444c1634d65584e9ee89904e9d8675eac88c010000006a473044022051f34d5e3b7d0b9098d5e35333f3550f9cb9e57df83d5e4635b7a8d2986d6d5602200288c98da05de6950e01229a637110a1800ba643e75cfec59d4eb1021ad9b40801210326846707a52a233cfc49a61ef51b1698bbe6aa78fa8b8d411c02743c09688f0affffffffae6c233989efa7c7d2aa6534adc96078917ff395b7f09f734a147b2f44ade164000000006a4730440220393a784c2da74d0e2a28ec4f7df6c8f9d8b2af6ae6957f1e68346d744223a8fd02201b7a96954ac06815a43a6c7668d829ae9cbb5de76fa77189ddfd9e3038df662c01210326846707a52a233cfc49a61ef51b1698bbe6aa78fa8b8d411c02743c09688f0affffffff02115f5800000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ac41a84641020000001976a914444f0e1099709ba4d742454a7d98a5c9c162ceab88ac6d84395d"},"type":"TakerFeeSent"},"timestamp":1564050545296},{"event":{"data":{"tx_hash":"0a0f11fa82802c2c30862c50ab2162185dae8de7f7235f32c506f814c142b382","tx_hex":"0400008085202f8902ace337db2dd4c56b0697f58fb8cfb6bd1cd6f469d925fc0376d1dcfb7581bf82000000006b483045022100d1f95be235c5c8880f5d703ace287e2768548792c58c5dbd27f5578881b30ea70220030596106e21c7e0057ee0dab283f9a1fe273f15208cba80870c447bd559ef0d0121031bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8ffffffff9f339752567c404427fd77f2b35cecdb4c21489edc64e25e729fdb281785e423000000006a47304402203179e95877dbc107123a417f1e648e3ff13d384890f1e4a67b6dd5087235152e0220102a8ab799fadb26b5d89ceb9c7bc721a7e0c2a0d0d7e46bbe0cf3d130010d430121031bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8ffffffff025635c0000000000017a91480a95d366d65e34a465ab17b0c9eb1d5a33bae08876cbfce05000000001976a914c3f710deb7320b0efa6edb14e3ebeeb9155fa90d88ac8d7c395d000000000000000000000000000000"},"type":"MakerPaymentReceived"},"timestamp":1564050588176},{"event":{"type":"MakerPaymentWaitConfirmStarted"},"timestamp":1564050588178},{"event":{"type":"MakerPaymentValidatedAndConfirmed"},"timestamp":1564050693585},{"event":{"data":{"tx_hash":"539cb6dbdc25465bbccc575554f05d1bb04c70efce4316e41194e747375c3659","tx_hex":"0100000001ffc8a8a1b43b4dceed0f8b7dcc2f72fdda92d52f32d25cc21c6d2d498b82debd010000006a47304402203967b7f9f5532fa47116585c7d1bcba51861ea2059cca00409f34660db18e33a0220640991911852533a12fdfeb039fb9c8ca2c45482c6993bd84636af3670d49c1501210326846707a52a233cfc49a61ef51b1698bbe6aa78fa8b8d411c02743c09688f0affffffff0200f2052a0100000017a914f2fa08ae416b576779ae5da975e5442663215fce87415173f9000000001976a914444f0e1099709ba4d742454a7d98a5c9c162ceab88ac0585395d"},"type":"TakerPaymentSent"},"timestamp":1564050695611},{"event":{"data":{"secret":"1b8886b8a2cdb62505699400b694ac20f04d7bd4abd80e1ab154aa8d861fc093","transaction":{"tx_hash":"cc5af1cf68d246419fee49c3d74c0cd173599d115b86efe274368a614951bc47","tx_hex":"010000000159365c3747e79411e41643ceef704cb01b5df0545557ccbc5b4625dcdbb69c5300000000d747304402200e78e27d2f1c18676f98ca3dfa4e4a9eeaa8209b55f57b4dd5d9e1abdf034cfa0220623b5c22b62234cec230342aa306c497e43494b44ec2425b84e236b1bf01257001201b8886b8a2cdb62505699400b694ac20f04d7bd4abd80e1ab154aa8d861fc093004c6b6304a7a2395db175210326846707a52a233cfc49a61ef51b1698bbe6aa78fa8b8d411c02743c09688f0aac6782012088a9143669eb83a007a3c507448d79f45a9f06ec2f36a88821031bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8ac68ffffffff01008d380c010000001976a914c3f710deb7320b0efa6edb14e3ebeeb9155fa90d88ac8c77395d"}},"type":"TakerPaymentSpent"},"timestamp":1564051092890},{"event":{"data":{"error":"lp_swap:1981] utxo:891] rpc_clients:738] JsonRpcError { request: JsonRpcRequest { jsonrpc: \"2.0\", id: \"67\", method: \"blockchain.transaction.broadcast\", params: [String(\"0400008085202f890182b342c114f806c5325f23f7e78dae5d186221ab502c86302c2c8082fa110f0a00000000d7473044022035791ea5548f87484065c9e1f0bdca9ebc699f2c7f51182c84f360102e32dc3d02200612ed53bca52d9c2568437f087598531534badf26229fe0f652ea72ddf03ca501201b8886b8a2cdb62505699400b694ac20f04d7bd4abd80e1ab154aa8d861fc093004c6b630420c1395db17521031bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8ac6782012088a9143669eb83a007a3c507448d79f45a9f06ec2f36a888210326846707a52a233cfc49a61ef51b1698bbe6aa78fa8b8d411c02743c09688f0aac68ffffffff01460ec000000000001976a914444f0e1099709ba4d742454a7d98a5c9c162ceab88ac967e395d000000000000000000000000000000\")] }, error: Transport(\"rpc_clients:668] All electrums are currently disconnected\") }"},"type":"MakerPaymentSpendFailed"},"timestamp":1564051092897}],"success_events":["Started","Negotiated","TakerFeeSent","MakerPaymentReceived","MakerPaymentWaitConfirmStarted","MakerPaymentValidatedAndConfirmed","TakerPaymentSent","TakerPaymentSpent","MakerPaymentSpent","MakerPaymentSpendConfirmed","Finished"],"uuid":"41383f43-46a5-478c-9386-3b2cce0aca20"}"#; - let taker_saved_swap: TakerSavedSwap = json::from_str(taker_saved_json).unwrap(); - - TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); - TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); - let (taker_swap, _) = block_on(TakerSwap::load_from_saved( - ctx, - maker_coin, - taker_coin, - taker_saved_swap, - )) - .unwrap(); - assert!(block_on(taker_swap.recover_funds()).is_err()); - } - #[test] fn test_taker_swap_event_should_ban() { let event = TakerSwapEvent::TakerPaymentWaitConfirmFailed("err".into()); @@ -3529,7 +3469,13 @@ mod taker_swap_tests { .unwrap(); let swaps_ctx = SwapsContext::from_ctx(&ctx).unwrap(); let arc = Arc::new(swap); - swaps_ctx.running_swaps.lock().unwrap().insert(arc.uuid, arc); + // Create a dummy abort handle as if it was a running swap. + let abortable_swap = spawn_abortable(async move {}); + swaps_ctx + .running_swaps + .lock() + .unwrap() + .insert(arc.uuid, (arc, abortable_swap)); let actual = get_locked_amount(&ctx, "RICK"); assert_eq!(actual, MmNumber::from(0)); diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index facb4ba899..6632e343e2 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -217,6 +217,7 @@ pub async fn lp_run(ctx: MmArc) { } // Clearing up the running swaps removes any circular references that might prevent the context from being dropped. + // Note that this obviously isn't needed for native targets since the executable will terminate either way, but in WASM we need to have the memory cleaned up. lp_swap::clear_running_swaps(&ctx); } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index c51e9ef27b..7cc458523a 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -12,7 +12,9 @@ use crate::lp_stats::{ add_node_to_version_stat, remove_node_from_version_stat, start_version_stat_collection, stop_version_stat_collection, update_version_stat_collection, }; -use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swap_status_rpc}; +use crate::lp_swap::swap_v2_rpcs::{ + active_swaps_rpc, kickstart_swap_rpc, my_recent_swaps_rpc, my_swap_status_rpc, stop_swap_rpc, +}; use crate::lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc}; use crate::lp_wallet::{change_mnemonic_password, delete_wallet_rpc, get_mnemonic_rpc, get_wallet_names_rpc}; use crate::rpc::lp_commands::db_id::get_shared_db_id; @@ -272,6 +274,8 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, max_maker_vol).await, "my_recent_swaps" => handle_mmrpc(ctx, request, my_recent_swaps_rpc).await, "my_swap_status" => handle_mmrpc(ctx, request, my_swap_status_rpc).await, + "stop_swap" => handle_mmrpc(ctx, request, stop_swap_rpc).await, + "kickstart_swap" => handle_mmrpc(ctx, request, kickstart_swap_rpc).await, "my_tx_history" => handle_mmrpc(ctx, request, my_tx_history_v2_rpc).await, "orderbook" => handle_mmrpc(ctx, request, orderbook_rpc_v2).await, "recreate_swap_data" => handle_mmrpc(ctx, request, recreate_swap_data).await,