feat (Cli): Display reason for failed cancel-refund operation to the user (#1668)

We now display the reason for a failed cancel-refund operation to the user.

Fixes #683
This commit is contained in:
patrini32 2024-07-01 21:14:44 +00:00 committed by GitHub
parent 23a27680a4
commit 173d077751
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 224 additions and 61 deletions

View file

@ -821,6 +821,7 @@ impl Request {
.await
.map_err(|err| {
method_span.in_scope(|| {
// The {:?} formatter is used to print the entire error chain
tracing::debug!(err = format!("{:?}", err), "API call resulted in an error");
});
err

View file

@ -1,5 +1,4 @@
use crate::bitcoin::wallet::Subscription;
use crate::bitcoin::{parse_rpc_error_code, RpcErrorCode, Wallet};
use crate::bitcoin::{ExpiredTimelocks, Wallet};
use crate::protocol::bob::BobState;
use crate::protocol::Database;
use anyhow::{bail, Result};
@ -13,7 +12,7 @@ pub async fn cancel_and_refund(
db: Arc<dyn Database + Send + Sync>,
) -> Result<BobState> {
if let Err(err) = cancel(swap_id, bitcoin_wallet.clone(), db.clone()).await {
tracing::info!(%err, "Could not submit cancel transaction");
tracing::warn!(%err, "Could not cancel swap. Attempting to refund anyway");
};
let state = match refund(swap_id, bitcoin_wallet, db).await {
@ -21,7 +20,6 @@ pub async fn cancel_and_refund(
Err(e) => bail!(e),
};
tracing::info!("Refund transaction submitted");
Ok(state)
}
@ -29,7 +27,7 @@ pub async fn cancel(
swap_id: Uuid,
bitcoin_wallet: Arc<Wallet>,
db: Arc<dyn Database + Send + Sync>,
) -> Result<(Txid, Subscription, BobState)> {
) -> Result<(Txid, BobState)> {
let state = db.get_state(swap_id).await?.try_into()?;
let state6 = match state {
@ -47,34 +45,69 @@ pub async fn cancel(
| BobState::XmrRedeemed { .. }
| BobState::BtcPunished { .. }
| BobState::SafelyAborted => bail!(
"Cannot cancel swap {} because it is in state {} which is not refundable.",
"Cannot cancel swap {} because it is in state {} which is not cancellable.",
swap_id,
state
),
};
tracing::info!(%swap_id, "Manually cancelling swap");
tracing::info!(%swap_id, "Attempting to manually cancel swap");
let (txid, subscription) = match state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await {
Ok(txid) => txid,
// Attempt to just publish the cancel transaction
match state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await {
Ok((txid, _)) => {
let state = BobState::BtcCancelled(state6);
db.insert_latest_state(swap_id, state.clone().into())
.await?;
Ok((txid, state))
}
// If we fail to submit the cancel transaction it can have one of two reasons:
// 1. The cancel timelock hasn't expired yet
// 2. The cancel transaction has already been published by Alice
Err(err) => {
if let Ok(error_code) = parse_rpc_error_code(&err) {
tracing::debug!(%error_code, "parse rpc error");
if error_code == i64::from(RpcErrorCode::RpcVerifyAlreadyInChain) {
tracing::info!("Cancel transaction has already been confirmed on chain");
} else if error_code == i64::from(RpcErrorCode::RpcVerifyError) {
tracing::info!("General error trying to submit cancel transaction");
// Check if Alice has already published the cancel transaction while we were absent
if let Ok(tx) = state6.check_for_tx_cancel(bitcoin_wallet.as_ref()).await {
let state = BobState::BtcCancelled(state6);
db.insert_latest_state(swap_id, state.clone().into())
.await?;
tracing::info!("Alice has already cancelled the swap");
return Ok((tx.txid(), state));
}
// The cancel transaction has not been published yet and we failed to publish it ourselves
// Here we try to figure out why
match state6.expired_timelock(bitcoin_wallet.as_ref()).await {
// We cannot cancel because Alice has already cancelled and punished afterwards
Ok(ExpiredTimelocks::Punish { .. }) => {
let state = BobState::BtcPunished {
tx_lock_id: state6.tx_lock_id(),
};
db.insert_latest_state(swap_id, state.clone().into())
.await?;
tracing::info!("You have been punished for not refunding in time");
bail!(err.context("Cannot cancel swap because we have already been punished"));
}
// We cannot cancel because the cancel timelock has not expired yet
Ok(ExpiredTimelocks::None { blocks_left }) => {
bail!(err.context(
format!(
"Cannot cancel swap because the cancel timelock has not expired yet. Blocks left: {}",
blocks_left
)
));
}
Ok(ExpiredTimelocks::Cancel { .. }) => {
bail!(err.context("Failed to cancel swap even though cancel timelock has expired. This is unexpected."));
}
Err(timelock_err) => {
bail!(err
.context(timelock_err)
.context("Failed to cancel swap and could not check timelock status"));
}
}
bail!(err);
}
};
let state = BobState::BtcCancelled(state6);
db.insert_latest_state(swap_id, state.clone().into())
.await?;
Ok((txid, subscription, state))
}
}
pub async fn refund(
@ -104,12 +137,51 @@ pub async fn refund(
),
};
tracing::info!(%swap_id, "Manually refunding swap");
state6.publish_refund_btc(bitcoin_wallet.as_ref()).await?;
tracing::info!(%swap_id, "Attempting to manually refund swap");
let state = BobState::BtcRefunded(state6);
db.insert_latest_state(swap_id, state.clone().into())
.await?;
// Attempt to just publish the refund transaction
match state6.publish_refund_btc(bitcoin_wallet.as_ref()).await {
Ok(_) => {
let state = BobState::BtcRefunded(state6);
db.insert_latest_state(swap_id, state.clone().into())
.await?;
Ok(state)
Ok(state)
}
// If we fail to submit the refund transaction it can have one of two reasons:
// 1. The cancel transaction has not been published yet
// 2. The refund timelock has already expired and we have been punished
Err(bitcoin_publication_err) => {
match state6.expired_timelock(bitcoin_wallet.as_ref()).await {
// We have been punished
Ok(ExpiredTimelocks::Punish { .. }) => {
let state = BobState::BtcPunished {
tx_lock_id: state6.tx_lock_id(),
};
db.insert_latest_state(swap_id, state.clone().into())
.await?;
tracing::info!("You have been punished for not refunding in time");
bail!(bitcoin_publication_err
.context("Cannot refund swap because we have already been punished"));
}
Ok(ExpiredTimelocks::None { blocks_left }) => {
bail!(
bitcoin_publication_err.context(format!(
"Cannot refund swap because the cancel timelock has not expired yet. Blocks left: {}",
blocks_left
))
);
}
Ok(ExpiredTimelocks::Cancel { .. }) => {
bail!(bitcoin_publication_err.context("Failed to refund swap even though cancel timelock has expired. This should is unexpected."));
}
Err(e) => {
bail!(bitcoin_publication_err
.context(e)
.context("Failed to refund swap and could not check timelock status"));
}
}
}
}
}

View file

@ -646,18 +646,20 @@ impl State6 {
tx_cancel_status,
))
}
pub async fn check_for_tx_cancel(
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<Transaction> {
let tx_cancel = bitcoin::TxCancel::new(
pub fn construct_tx_cancel(&self) -> Result<bitcoin::TxCancel> {
bitcoin::TxCancel::new(
&self.tx_lock,
self.cancel_timelock,
self.A,
self.b.public(),
self.tx_cancel_fee,
)?;
)
}
pub async fn check_for_tx_cancel(
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<Transaction> {
let tx_cancel = self.construct_tx_cancel()?;
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
@ -668,15 +670,10 @@ impl State6 {
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<(Txid, Subscription)> {
let transaction = bitcoin::TxCancel::new(
&self.tx_lock,
self.cancel_timelock,
self.A,
self.b.public(),
self.tx_cancel_fee,
)?
.complete_as_bob(self.A, self.b.clone(), self.tx_cancel_sig_a.clone())
.context("Failed to complete Bitcoin cancel transaction")?;
let transaction = self
.construct_tx_cancel()?
.complete_as_bob(self.A, self.b.clone(), self.tx_cancel_sig_a.clone())
.context("Failed to complete Bitcoin cancel transaction")?;
let (tx_id, subscription) = bitcoin_wallet.broadcast(transaction, "cancel").await?;
@ -691,13 +688,7 @@ impl State6 {
}
pub fn signed_refund_transaction(&self) -> Result<Transaction> {
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.cancel_timelock,
self.A,
self.b.public(),
self.tx_cancel_fee,
)?;
let tx_cancel = self.construct_tx_cancel()?;
let tx_refund =
bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee);