diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 62ad093d..adedb9b0 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -70,158 +70,146 @@ async fn run_until_internal( ) -> Result { info!("Current state: {}", state); if is_target_state(&state) { - Ok(state) - } else { - match state { - AliceState::Started { state3 } => { - timeout( - env_config.bob_time_to_act, - bitcoin_wallet - .watch_until_status(&state3.tx_lock, |status| status.has_been_seen()), - ) - .await - .context("Failed to find lock Bitcoin tx")??; + return Ok(state); + } - bitcoin_wallet - .watch_until_status(&state3.tx_lock, |status| { - status.is_confirmed_with(env_config.bitcoin_finality_confirmations) - }) - .await?; + match state { + AliceState::Started { state3 } => { + timeout( + env_config.bob_time_to_act, + bitcoin_wallet.watch_until_status(&state3.tx_lock, |status| status.has_been_seen()), + ) + .await + .context("Failed to find lock Bitcoin tx")??; - let state = AliceState::BtcLocked { state3 }; + bitcoin_wallet + .watch_until_status(&state3.tx_lock, |status| { + status.is_confirmed_with(env_config.bitcoin_finality_confirmations) + }) + .await?; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - env_config, - swap_id, - db, - ) - .await - } - AliceState::BtcLocked { state3 } => { - // Record the current monero wallet block height so we don't have to scan from - // block 0 for scenarios where we create a refund wallet. - let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; + let state = AliceState::BtcLocked { state3 }; - let transfer_proof = monero_wallet - .transfer(state3.lock_xmr_transfer_request()) - .await?; + let db_state = (&state).into(); + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) + .await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + bitcoin_wallet, + monero_wallet, + env_config, + swap_id, + db, + ) + .await + } + AliceState::BtcLocked { state3 } => { + // Record the current monero wallet block height so we don't have to scan from + // block 0 for scenarios where we create a refund wallet. + let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; - // TODO(Franck): Wait for Monero to be confirmed once - // Waiting for XMR confirmations should not be done in here, but in a separate - // state! We have to record that Alice has already sent the transaction. - // Otherwise Alice might publish the lock tx twice! + let transfer_proof = monero_wallet + .transfer(state3.lock_xmr_transfer_request()) + .await?; - event_loop_handle - .send_transfer_proof(transfer_proof) - .await?; + // TODO(Franck): Wait for Monero to be confirmed once + // Waiting for XMR confirmations should not be done in here, but in a separate + // state! We have to record that Alice has already sent the transaction. + // Otherwise Alice might publish the lock tx twice! - let state = AliceState::XmrLocked { - state3, - monero_wallet_restore_blockheight, - }; + event_loop_handle + .send_transfer_proof(transfer_proof) + .await?; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - env_config, - swap_id, - db, - ) - .await - } - AliceState::XmrLocked { + let state = AliceState::XmrLocked { state3, monero_wallet_restore_blockheight, - } => { - let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { - ExpiredTimelocks::None => { - select! { - _ = state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => { - AliceState::CancelTimelockExpired { - state3, - monero_wallet_restore_blockheight, - } - } - enc_sig = event_loop_handle.recv_encrypted_signature() => { - tracing::info!("Received encrypted signature"); + }; - AliceState::EncSigLearned { - state3, - encrypted_signature: Box::new(enc_sig?), - monero_wallet_restore_blockheight, - } + let db_state = (&state).into(); + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) + .await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + bitcoin_wallet, + monero_wallet, + env_config, + swap_id, + db, + ) + .await + } + AliceState::XmrLocked { + state3, + monero_wallet_restore_blockheight, + } => { + let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { + ExpiredTimelocks::None => { + select! { + _ = state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => { + AliceState::CancelTimelockExpired { + state3, + monero_wallet_restore_blockheight, + } + } + enc_sig = event_loop_handle.recv_encrypted_signature() => { + tracing::info!("Received encrypted signature"); + + AliceState::EncSigLearned { + state3, + encrypted_signature: Box::new(enc_sig?), + monero_wallet_restore_blockheight, } } } - _ => AliceState::CancelTimelockExpired { - state3, - monero_wallet_restore_blockheight, - }, - }; + } + _ => AliceState::CancelTimelockExpired { + state3, + monero_wallet_restore_blockheight, + }, + }; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet.clone(), - monero_wallet, - env_config, - swap_id, - db, - ) - .await - } - AliceState::EncSigLearned { - state3, - encrypted_signature, - monero_wallet_restore_blockheight, - } => { - let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { - ExpiredTimelocks::None => { - match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete( - *encrypted_signature, - state3.a.clone(), - state3.s_a.to_secpfun_scalar(), - state3.B, - ) { - Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { - Ok((_, finality)) => match finality.await { - Ok(_) => AliceState::BtcRedeemed, - Err(e) => { - bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e) - } - }, + let db_state = (&state).into(); + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) + .await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + bitcoin_wallet.clone(), + monero_wallet, + env_config, + swap_id, + db, + ) + .await + } + AliceState::EncSigLearned { + state3, + encrypted_signature, + monero_wallet_restore_blockheight, + } => { + let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { + ExpiredTimelocks::None => { + match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete( + *encrypted_signature, + state3.a.clone(), + state3.s_a.to_secpfun_scalar(), + state3.B, + ) { + Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { + Ok((_, finality)) => match finality.await { + Ok(_) => AliceState::BtcRedeemed, Err(e) => { - error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e); - state3 - .wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) - .await?; - - AliceState::CancelTimelockExpired { - state3, - monero_wallet_restore_blockheight, - } + bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e) } }, Err(e) => { - error!("Constructing the redeem transaction failed with {}, attempting to wait for cancellation now.", e); + error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e); state3 .wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) .await?; @@ -231,236 +219,241 @@ async fn run_until_internal( monero_wallet_restore_blockheight, } } + }, + Err(e) => { + error!("Constructing the redeem transaction failed with {}, attempting to wait for cancellation now.", e); + state3 + .wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) + .await?; + + AliceState::CancelTimelockExpired { + state3, + monero_wallet_restore_blockheight, + } } } - _ => AliceState::CancelTimelockExpired { - state3, - monero_wallet_restore_blockheight, - }, - }; - - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - env_config, - swap_id, - db, - ) - .await - } - AliceState::CancelTimelockExpired { - state3, - monero_wallet_restore_blockheight, - } => { - let tx_cancel = state3.tx_cancel(); - - // If Bob hasn't yet broadcasted the tx cancel, we do it - if bitcoin_wallet - .get_raw_transaction(tx_cancel.txid()) - .await - .is_err() - { - let transaction = tx_cancel - .complete_as_alice( - state3.a.clone(), - state3.B, - state3.tx_cancel_sig_bob.clone(), - ) - .context("Failed to complete Bitcoin cancel transaction")?; - - if let Err(e) = bitcoin_wallet.broadcast(transaction, "cancel").await { - tracing::debug!( - "Assuming transaction is already broadcasted because: {:#}", - e - ) - } - - // TODO(Franck): Wait until transaction is mined and - // returned mined block height } - - let state = AliceState::BtcCancelled { + _ => AliceState::CancelTimelockExpired { state3, monero_wallet_restore_blockheight, - }; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - env_config, - swap_id, - db, - ) + }, + }; + + let db_state = (&state).into(); + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) + .await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + bitcoin_wallet, + monero_wallet, + env_config, + swap_id, + db, + ) + .await + } + AliceState::CancelTimelockExpired { + state3, + monero_wallet_restore_blockheight, + } => { + let tx_cancel = state3.tx_cancel(); + + // If Bob hasn't yet broadcasted the tx cancel, we do it + if bitcoin_wallet + .get_raw_transaction(tx_cancel.txid()) .await + .is_err() + { + let transaction = tx_cancel + .complete_as_alice(state3.a.clone(), state3.B, state3.tx_cancel_sig_bob.clone()) + .context("Failed to complete Bitcoin cancel transaction")?; + + if let Err(e) = bitcoin_wallet.broadcast(transaction, "cancel").await { + tracing::debug!( + "Assuming transaction is already broadcasted because: {:#}", + e + ) + } + + // TODO(Franck): Wait until transaction is mined and + // returned mined block height } - AliceState::BtcCancelled { + + let state = AliceState::BtcCancelled { state3, monero_wallet_restore_blockheight, - } => { - let tx_refund = state3.tx_refund(); - let tx_cancel = state3.tx_cancel(); + }; + let db_state = (&state).into(); + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) + .await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + bitcoin_wallet, + monero_wallet, + env_config, + swap_id, + db, + ) + .await + } + AliceState::BtcCancelled { + state3, + monero_wallet_restore_blockheight, + } => { + let tx_refund = state3.tx_refund(); + let tx_cancel = state3.tx_cancel(); - let seen_refund_tx = - bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen()); + let seen_refund_tx = + bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen()); - let punish_timelock_expired = bitcoin_wallet - .watch_until_status(&tx_cancel, |status| { - status.is_confirmed_with(state3.punish_timelock) - }); + let punish_timelock_expired = bitcoin_wallet.watch_until_status(&tx_cancel, |status| { + status.is_confirmed_with(state3.punish_timelock) + }); - let state = tokio::select! { - seen_refund = seen_refund_tx => { - seen_refund.context("Failed to monitor refund transaction")?; - let published_refund_tx = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; + let state = tokio::select! { + seen_refund = seen_refund_tx => { + seen_refund.context("Failed to monitor refund transaction")?; + let published_refund_tx = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; - let spend_key = tx_refund.extract_monero_private_key( - published_refund_tx, - state3.s_a, - state3.a.clone(), - state3.S_b_bitcoin, - )?; + let spend_key = tx_refund.extract_monero_private_key( + published_refund_tx, + state3.s_a, + state3.a.clone(), + state3.S_b_bitcoin, + )?; - AliceState::BtcRefunded { - spend_key, - state3, - monero_wallet_restore_blockheight, - } - } - _ = punish_timelock_expired => { - AliceState::BtcPunishable { - state3, - monero_wallet_restore_blockheight, - } - } - }; - - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet.clone(), - monero_wallet, - env_config, - swap_id, - db, - ) - .await - } - AliceState::BtcRefunded { - spend_key, - state3, - monero_wallet_restore_blockheight, - } => { - let view_key = state3.v; - - monero_wallet - .create_from(spend_key, view_key, monero_wallet_restore_blockheight) - .await?; - - let state = AliceState::XmrRefunded; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - Ok(state) - } - AliceState::BtcPunishable { - state3, - monero_wallet_restore_blockheight, - } => { - let signed_tx_punish = state3.tx_punish().complete( - state3.tx_punish_sig_bob.clone(), - state3.a.clone(), - state3.B, - )?; - - let punish_tx_finalised = async { - let (txid, finality) = - bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; - - finality.await?; - - Result::<_, anyhow::Error>::Ok(txid) - }; - - let tx_refund = state3.tx_refund(); - let refund_tx_seen = - bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen()); - - pin_mut!(punish_tx_finalised); - pin_mut!(refund_tx_seen); - - match select(refund_tx_seen, punish_tx_finalised).await { - Either::Left((Ok(()), _)) => { - let published_refund_tx = - bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; - - let spend_key = tx_refund.extract_monero_private_key( - published_refund_tx, - state3.s_a, - state3.a.clone(), - state3.S_b_bitcoin, - )?; - let state = AliceState::BtcRefunded { - spend_key, - state3, - monero_wallet_restore_blockheight, - }; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet.clone(), - monero_wallet, - env_config, - swap_id, - db, - ) - .await - } - Either::Left((Err(e), _)) => { - bail!(e.context("Failed to monitor refund transaction")) - } - Either::Right(_) => { - let state = AliceState::BtcPunished; - let db_state = (&state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - bitcoin_wallet.clone(), - monero_wallet, - env_config, - swap_id, - db, - ) - .await + AliceState::BtcRefunded { + spend_key, + state3, + monero_wallet_restore_blockheight, } } - } - AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), - AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), - AliceState::BtcPunished => Ok(AliceState::BtcPunished), - AliceState::SafelyAborted => Ok(AliceState::SafelyAborted), + _ = punish_timelock_expired => { + AliceState::BtcPunishable { + state3, + monero_wallet_restore_blockheight, + } + } + }; + + let db_state = (&state).into(); + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) + .await?; + + run_until_internal( + state, + is_target_state, + event_loop_handle, + bitcoin_wallet.clone(), + monero_wallet, + env_config, + swap_id, + db, + ) + .await } + AliceState::BtcRefunded { + spend_key, + state3, + monero_wallet_restore_blockheight, + } => { + let view_key = state3.v; + + monero_wallet + .create_from(spend_key, view_key, monero_wallet_restore_blockheight) + .await?; + + let state = AliceState::XmrRefunded; + let db_state = (&state).into(); + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) + .await?; + Ok(state) + } + AliceState::BtcPunishable { + state3, + monero_wallet_restore_blockheight, + } => { + let signed_tx_punish = state3.tx_punish().complete( + state3.tx_punish_sig_bob.clone(), + state3.a.clone(), + state3.B, + )?; + + let punish_tx_finalised = async { + let (txid, finality) = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; + + finality.await?; + + Result::<_, anyhow::Error>::Ok(txid) + }; + + let tx_refund = state3.tx_refund(); + let refund_tx_seen = + bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen()); + + pin_mut!(punish_tx_finalised); + pin_mut!(refund_tx_seen); + + match select(refund_tx_seen, punish_tx_finalised).await { + Either::Left((Ok(()), _)) => { + let published_refund_tx = + bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; + + let spend_key = tx_refund.extract_monero_private_key( + published_refund_tx, + state3.s_a, + state3.a.clone(), + state3.S_b_bitcoin, + )?; + let state = AliceState::BtcRefunded { + spend_key, + state3, + monero_wallet_restore_blockheight, + }; + let db_state = (&state).into(); + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) + .await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + bitcoin_wallet.clone(), + monero_wallet, + env_config, + swap_id, + db, + ) + .await + } + Either::Left((Err(e), _)) => { + bail!(e.context("Failed to monitor refund transaction")) + } + Either::Right(_) => { + let state = AliceState::BtcPunished; + let db_state = (&state).into(); + db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) + .await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + bitcoin_wallet.clone(), + monero_wallet, + env_config, + swap_id, + db, + ) + .await + } + } + } + AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), + AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), + AliceState::BtcPunished => Ok(AliceState::BtcPunished), + AliceState::SafelyAborted => Ok(AliceState::SafelyAborted), } } diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index c2ecc890..f6b7ea78 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -63,347 +63,349 @@ async fn run_until_internal( ) -> Result { trace!("Current state: {}", state); if is_target_state(&state) { - Ok(state) - } else { - match state { - BobState::Started { btc_amount } => { - let bitcoin_refund_address = bitcoin_wallet.new_address().await?; + return Ok(state); + } + match state { + BobState::Started { btc_amount } => { + let bitcoin_refund_address = bitcoin_wallet.new_address().await?; + + event_loop_handle.dial().await?; + + let state2 = request_price_and_setup( + btc_amount, + &mut event_loop_handle, + env_config, + bitcoin_refund_address, + ) + .await?; + + let state = BobState::ExecutionSetupDone(state2); + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + db, + bitcoin_wallet, + monero_wallet, + swap_id, + env_config, + receive_monero_address, + ) + .await + } + BobState::ExecutionSetupDone(state2) => { + // Do not lock Bitcoin if not connected to Alice. + event_loop_handle.dial().await?; + // Alice and Bob have exchanged info + let (state3, tx_lock) = state2.lock_btc().await?; + let signed_tx = bitcoin_wallet + .sign_and_finalize(tx_lock.clone().into()) + .await + .context("Failed to sign Bitcoin lock transaction")?; + let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?; + + let state = BobState::BtcLocked(state3); + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + db, + bitcoin_wallet, + monero_wallet, + swap_id, + env_config, + receive_monero_address, + ) + .await + } + // Bob has locked Btc + // Watch for Alice to Lock Xmr or for cancel timelock to elapse + BobState::BtcLocked(state3) => { + let state = if let ExpiredTimelocks::None = + state3.current_epoch(bitcoin_wallet.as_ref()).await? + { event_loop_handle.dial().await?; - let state2 = request_price_and_setup( - btc_amount, - &mut event_loop_handle, - env_config, - bitcoin_refund_address, - ) + let transfer_proof_watcher = event_loop_handle.recv_transfer_proof(); + let cancel_timelock_expires = + state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); + + // Record the current monero wallet block height so we don't have to scan from + // block 0 once we create the redeem wallet. + let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; + + tracing::info!("Waiting for Alice to lock Monero"); + + select! { + transfer_proof = transfer_proof_watcher => { + let transfer_proof = transfer_proof?.tx_lock_proof; + + tracing::info!(txid = %transfer_proof.tx_hash(), "Alice locked Monero"); + + BobState::XmrLockProofReceived { + state: state3, + lock_transfer_proof: transfer_proof, + monero_wallet_restore_blockheight + } + }, + _ = cancel_timelock_expires => { + tracing::info!("Alice took too long to lock Monero, cancelling the swap"); + + let state4 = state3.cancel(); + BobState::CancelTimelockExpired(state4) + } + } + } else { + let state4 = state3.cancel(); + BobState::CancelTimelockExpired(state4) + }; + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + db, + bitcoin_wallet, + monero_wallet, + swap_id, + env_config, + receive_monero_address, + ) + .await + } + BobState::XmrLockProofReceived { + state, + lock_transfer_proof, + monero_wallet_restore_blockheight, + } => { + let state = if let ExpiredTimelocks::None = + state.current_epoch(bitcoin_wallet.as_ref()).await? + { + event_loop_handle.dial().await?; + + let xmr_lock_watcher = state.clone().watch_for_lock_xmr( + monero_wallet.as_ref(), + lock_transfer_proof, + monero_wallet_restore_blockheight, + ); + let cancel_timelock_expires = + state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); + + select! { + state4 = xmr_lock_watcher => { + match state4? { + Ok(state4) => BobState::XmrLocked(state4), + Err(InsufficientFunds {..}) => { + warn!("The other party has locked insufficient Monero funds! Waiting for refund..."); + state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()).await?; + let state4 = state.cancel(); + BobState::CancelTimelockExpired(state4) + }, + } + }, + _ = cancel_timelock_expires => { + let state4 = state.cancel(); + BobState::CancelTimelockExpired(state4) + } + } + } else { + let state4 = state.cancel(); + BobState::CancelTimelockExpired(state4) + }; + + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + db, + bitcoin_wallet, + monero_wallet, + swap_id, + env_config, + receive_monero_address, + ) + .await + } + BobState::XmrLocked(state) => { + let state = if let ExpiredTimelocks::None = + state.expired_timelock(bitcoin_wallet.as_ref()).await? + { + event_loop_handle.dial().await?; + // Alice has locked Xmr + // Bob sends Alice his key + let tx_redeem_encsig = state.tx_redeem_encsig(); + + let state4_clone = state.clone(); + + let enc_sig_sent_watcher = + event_loop_handle.send_encrypted_signature(tx_redeem_encsig); + let bitcoin_wallet = bitcoin_wallet.clone(); + let cancel_timelock_expires = + state4_clone.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); + + select! { + _ = enc_sig_sent_watcher => { + BobState::EncSigSent(state) + }, + _ = cancel_timelock_expires => { + BobState::CancelTimelockExpired(state) + } + } + } else { + BobState::CancelTimelockExpired(state) + }; + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + db, + bitcoin_wallet, + monero_wallet, + swap_id, + env_config, + receive_monero_address, + ) + .await + } + BobState::EncSigSent(state) => { + let state = if let ExpiredTimelocks::None = + state.expired_timelock(bitcoin_wallet.as_ref()).await? + { + let state_clone = state.clone(); + let redeem_watcher = state_clone.watch_for_redeem_btc(bitcoin_wallet.as_ref()); + let cancel_timelock_expires = + state_clone.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); + + select! { + state5 = redeem_watcher => { + BobState::BtcRedeemed(state5?) + }, + _ = cancel_timelock_expires => { + BobState::CancelTimelockExpired(state) + } + } + } else { + BobState::CancelTimelockExpired(state) + }; + + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + db, + bitcoin_wallet.clone(), + monero_wallet, + swap_id, + env_config, + receive_monero_address, + ) + .await + } + BobState::BtcRedeemed(state) => { + // Bob redeems XMR using revealed s_a + state.claim_xmr(monero_wallet.as_ref()).await?; + + // Ensure that the generated wallet is synced so we have a proper balance + monero_wallet.refresh().await?; + // Sweep (transfer all funds) to the given address + let tx_hashes = monero_wallet.sweep_all(receive_monero_address).await?; + + for tx_hash in tx_hashes { + tracing::info!("Sent XMR to {} in tx {}", receive_monero_address, tx_hash.0); + } + + let state = BobState::XmrRedeemed { + tx_lock_id: state.tx_lock_id(), + }; + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + db, + bitcoin_wallet, + monero_wallet, + swap_id, + env_config, + receive_monero_address, + ) + .await + } + BobState::CancelTimelockExpired(state4) => { + if state4 + .check_for_tx_cancel(bitcoin_wallet.as_ref()) + .await + .is_err() + { + state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; + } + + let state = BobState::BtcCancelled(state4); + db.insert_latest_state(swap_id, Swap::Bob(state.clone().into())) .await?; - let state = BobState::ExecutionSetupDone(state2); - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::ExecutionSetupDone(state2) => { - // Do not lock Bitcoin if not connected to Alice. - event_loop_handle.dial().await?; - // Alice and Bob have exchanged info - let (state3, tx_lock) = state2.lock_btc().await?; - let signed_tx = bitcoin_wallet - .sign_and_finalize(tx_lock.clone().into()) - .await - .context("Failed to sign Bitcoin lock transaction")?; - let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?; - - let state = BobState::BtcLocked(state3); - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - // Bob has locked Btc - // Watch for Alice to Lock Xmr or for cancel timelock to elapse - BobState::BtcLocked(state3) => { - let state = if let ExpiredTimelocks::None = - state3.current_epoch(bitcoin_wallet.as_ref()).await? - { - event_loop_handle.dial().await?; - - let transfer_proof_watcher = event_loop_handle.recv_transfer_proof(); - let cancel_timelock_expires = - state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); - - // Record the current monero wallet block height so we don't have to scan from - // block 0 once we create the redeem wallet. - let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; - - tracing::info!("Waiting for Alice to lock Monero"); - - select! { - transfer_proof = transfer_proof_watcher => { - let transfer_proof = transfer_proof?.tx_lock_proof; - - tracing::info!(txid = %transfer_proof.tx_hash(), "Alice locked Monero"); - - BobState::XmrLockProofReceived { - state: state3, - lock_transfer_proof: transfer_proof, - monero_wallet_restore_blockheight - } - }, - _ = cancel_timelock_expires => { - tracing::info!("Alice took too long to lock Monero, cancelling the swap"); - - let state4 = state3.cancel(); - BobState::CancelTimelockExpired(state4) - } - } - } else { - let state4 = state3.cancel(); - BobState::CancelTimelockExpired(state4) - }; - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::XmrLockProofReceived { + run_until_internal( state, - lock_transfer_proof, - monero_wallet_restore_blockheight, - } => { - let state = if let ExpiredTimelocks::None = - state.current_epoch(bitcoin_wallet.as_ref()).await? - { - event_loop_handle.dial().await?; - - let xmr_lock_watcher = state.clone().watch_for_lock_xmr( - monero_wallet.as_ref(), - lock_transfer_proof, - monero_wallet_restore_blockheight, - ); - let cancel_timelock_expires = - state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); - - select! { - state4 = xmr_lock_watcher => { - match state4? { - Ok(state4) => BobState::XmrLocked(state4), - Err(InsufficientFunds {..}) => { - warn!("The other party has locked insufficient Monero funds! Waiting for refund..."); - state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()).await?; - let state4 = state.cancel(); - BobState::CancelTimelockExpired(state4) - }, - } - }, - _ = cancel_timelock_expires => { - let state4 = state.cancel(); - BobState::CancelTimelockExpired(state4) - } - } - } else { - let state4 = state.cancel(); - BobState::CancelTimelockExpired(state4) - }; - - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::XmrLocked(state) => { - let state = if let ExpiredTimelocks::None = - state.expired_timelock(bitcoin_wallet.as_ref()).await? - { - event_loop_handle.dial().await?; - // Alice has locked Xmr - // Bob sends Alice his key - let tx_redeem_encsig = state.tx_redeem_encsig(); - - let state4_clone = state.clone(); - - let enc_sig_sent_watcher = - event_loop_handle.send_encrypted_signature(tx_redeem_encsig); - let bitcoin_wallet = bitcoin_wallet.clone(); - let cancel_timelock_expires = - state4_clone.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); - - select! { - _ = enc_sig_sent_watcher => { - BobState::EncSigSent(state) - }, - _ = cancel_timelock_expires => { - BobState::CancelTimelockExpired(state) - } - } - } else { - BobState::CancelTimelockExpired(state) - }; - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::EncSigSent(state) => { - let state = if let ExpiredTimelocks::None = - state.expired_timelock(bitcoin_wallet.as_ref()).await? - { - let state_clone = state.clone(); - let redeem_watcher = state_clone.watch_for_redeem_btc(bitcoin_wallet.as_ref()); - let cancel_timelock_expires = - state_clone.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); - - select! { - state5 = redeem_watcher => { - BobState::BtcRedeemed(state5?) - }, - _ = cancel_timelock_expires => { - BobState::CancelTimelockExpired(state) - } - } - } else { - BobState::CancelTimelockExpired(state) - }; - - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet.clone(), - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::BtcRedeemed(state) => { - // Bob redeems XMR using revealed s_a - state.claim_xmr(monero_wallet.as_ref()).await?; - - // Ensure that the generated wallet is synced so we have a proper balance - monero_wallet.refresh().await?; - // Sweep (transfer all funds) to the given address - let tx_hashes = monero_wallet.sweep_all(receive_monero_address).await?; - - for tx_hash in tx_hashes { - tracing::info!("Sent XMR to {} in tx {}", receive_monero_address, tx_hash.0); - } - - let state = BobState::XmrRedeemed { - tx_lock_id: state.tx_lock_id(), - }; - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::CancelTimelockExpired(state4) => { - if state4 - .check_for_tx_cancel(bitcoin_wallet.as_ref()) - .await - .is_err() - { - state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; - } - - let state = BobState::BtcCancelled(state4); - db.insert_latest_state(swap_id, Swap::Bob(state.clone().into())) - .await?; - - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::BtcCancelled(state) => { - // Bob has cancelled the swap - let state = match state.expired_timelock(bitcoin_wallet.as_ref()).await? { - ExpiredTimelocks::None => { - bail!("Internal error: canceled state reached before cancel timelock was expired"); - } - ExpiredTimelocks::Cancel => { - state.refund_btc(bitcoin_wallet.as_ref()).await?; - BobState::BtcRefunded(state) - } - ExpiredTimelocks::Punish => BobState::BtcPunished { - tx_lock_id: state.tx_lock_id(), - }, - }; - - let db_state = state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await - } - BobState::BtcRefunded(state4) => Ok(BobState::BtcRefunded(state4)), - BobState::BtcPunished { tx_lock_id } => Ok(BobState::BtcPunished { tx_lock_id }), - BobState::SafelyAborted => Ok(BobState::SafelyAborted), - BobState::XmrRedeemed { tx_lock_id } => Ok(BobState::XmrRedeemed { tx_lock_id }), + is_target_state, + event_loop_handle, + db, + bitcoin_wallet, + monero_wallet, + swap_id, + env_config, + receive_monero_address, + ) + .await } + BobState::BtcCancelled(state) => { + // Bob has cancelled the swap + let state = match state.expired_timelock(bitcoin_wallet.as_ref()).await? { + ExpiredTimelocks::None => { + bail!( + "Internal error: canceled state reached before cancel timelock was expired" + ); + } + ExpiredTimelocks::Cancel => { + state.refund_btc(bitcoin_wallet.as_ref()).await?; + BobState::BtcRefunded(state) + } + ExpiredTimelocks::Punish => BobState::BtcPunished { + tx_lock_id: state.tx_lock_id(), + }, + }; + + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + run_until_internal( + state, + is_target_state, + event_loop_handle, + db, + bitcoin_wallet, + monero_wallet, + swap_id, + env_config, + receive_monero_address, + ) + .await + } + BobState::BtcRefunded(state4) => Ok(BobState::BtcRefunded(state4)), + BobState::BtcPunished { tx_lock_id } => Ok(BobState::BtcPunished { tx_lock_id }), + BobState::SafelyAborted => Ok(BobState::SafelyAborted), + BobState::XmrRedeemed { tx_lock_id } => Ok(BobState::XmrRedeemed { tx_lock_id }), } }