diff --git a/swap/src/asb/recovery/cancel.rs b/swap/src/asb/recovery/cancel.rs index b02dd22b..32af014f 100644 --- a/swap/src/asb/recovery/cancel.rs +++ b/swap/src/asb/recovery/cancel.rs @@ -1,4 +1,4 @@ -use crate::bitcoin::{Txid, Wallet}; +use crate::bitcoin::{parse_rpc_error_code, RpcErrorCode, Txid, Wallet}; use crate::database::{Database, Swap}; use crate::protocol::alice::AliceState; use anyhow::{bail, Result}; @@ -44,9 +44,11 @@ pub async fn cancel( let txid = match state3.submit_tx_cancel(bitcoin_wallet.as_ref()).await { Ok(txid) => txid, Err(err) => { - if let Some(bdk::Error::TransactionConfirmed) = err.downcast_ref::() { - tracing::info!("Cancel transaction has already been published and confirmed") - }; + if let Ok(code) = parse_rpc_error_code(&err) { + if code == i64::from(RpcErrorCode::RpcVerifyAlreadyInChain) { + tracing::info!("Cancel transaction has already been confirmed on chain") + } + } bail!(err); } }; diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 09ba21cd..10953f6c 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -249,6 +249,58 @@ pub fn current_epoch( ExpiredTimelocks::None } +/// Bitcoin error codes: https://github.com/bitcoin/bitcoin/blob/97d3500601c1d28642347d014a6de1e38f53ae4e/src/rpc/protocol.h#L23 +pub enum RpcErrorCode { + /// Transaction or block was rejected by network rules. Error code -26. + RpcVerifyRejected, + /// Transaction or block was rejected by network rules. Error code -27. + RpcVerifyAlreadyInChain, + /// General error during transaction or block submission + RpcVerifyError, +} + +impl From for i64 { + fn from(code: RpcErrorCode) -> Self { + match code { + RpcErrorCode::RpcVerifyError => -25, + RpcErrorCode::RpcVerifyRejected => -26, + RpcErrorCode::RpcVerifyAlreadyInChain => -27, + } + } +} + +pub fn parse_rpc_error_code(error: &anyhow::Error) -> anyhow::Result { + let string = match error.downcast_ref::() { + Some(bdk::Error::Electrum(bdk::electrum_client::Error::Protocol( + serde_json::Value::String(string), + ))) => string, + _ => bail!("Error is of incorrect variant:{}", error), + }; + + let json = serde_json::from_str(&string.replace("sendrawtransaction RPC error:", ""))?; + + let json_map = match json { + serde_json::Value::Object(map) => map, + _ => bail!("Json error is not json object "), + }; + + let error_code_value = match json_map.get("code") { + Some(val) => val, + None => bail!("No error code field"), + }; + + let error_code_number = match error_code_value { + serde_json::Value::Number(num) => num, + _ => bail!("Error code is not a number"), + }; + + if let Some(int) = error_code_number.as_i64() { + Ok(int) + } else { + bail!("Error code is not an unsigned integer") + } +} + #[derive(Clone, Copy, thiserror::Error, Debug)] #[error("transaction does not spend anything")] pub struct NoInputs; diff --git a/swap/src/cli/cancel.rs b/swap/src/cli/cancel.rs index 3693917f..5828b078 100644 --- a/swap/src/cli/cancel.rs +++ b/swap/src/cli/cancel.rs @@ -1,4 +1,4 @@ -use crate::bitcoin::{Txid, Wallet}; +use crate::bitcoin::{parse_rpc_error_code, RpcErrorCode, Txid, Wallet}; use crate::database::{Database, Swap}; use crate::protocol::bob::BobState; use anyhow::{bail, Result}; @@ -38,9 +38,11 @@ pub async fn cancel( let txid = match state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await { Ok(txid) => txid, Err(err) => { - if let Some(bdk::Error::TransactionConfirmed) = err.downcast_ref::() { - tracing::info!("Cancel transaction has already been published and confirmed") - }; + if let Ok(code) = parse_rpc_error_code(&err) { + if code == i64::from(RpcErrorCode::RpcVerifyAlreadyInChain) { + tracing::info!("Cancel transaction has already been confirmed on chain") + } + } bail!(err); } }; diff --git a/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired.rs b/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired.rs index 73eda852..16953866 100644 --- a/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired.rs +++ b/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired.rs @@ -4,6 +4,7 @@ use harness::alice_run_until::is_xmr_lock_transaction_sent; use harness::bob_run_until::is_btc_locked; use harness::SlowCancelConfig; use swap::asb::FixedRate; +use swap::bitcoin::{parse_rpc_error_code, RpcErrorCode}; use swap::protocol::alice::AliceState; use swap::protocol::bob::BobState; use swap::protocol::{alice, bob}; @@ -41,10 +42,10 @@ async fn given_alice_and_bob_manually_cancel_when_timelock_not_expired_errors() let error = cli::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db) .await .unwrap_err(); - match error.downcast::().unwrap() { - bdk::Error::Electrum(bdk::electrum_client::Error::Protocol(..)) => (), - unexpected => panic!("Failed to cancel due to unexpected error: {}", unexpected), - } + assert_eq!( + parse_rpc_error_code(&error).unwrap(), + i64::from(RpcErrorCode::RpcVerifyRejected) + ); ctx.restart_alice().await; let alice_swap = ctx.alice_next_swap().await; @@ -54,9 +55,13 @@ async fn given_alice_and_bob_manually_cancel_when_timelock_not_expired_errors() )); // Alice tries but fails manual cancel - let result = - asb::cancel(alice_swap.swap_id, alice_swap.bitcoin_wallet, alice_swap.db).await; - assert!(result.is_err()); + let error = asb::cancel(alice_swap.swap_id, alice_swap.bitcoin_wallet, alice_swap.db) + .await + .unwrap_err(); + assert_eq!( + parse_rpc_error_code(&error).unwrap(), + i64::from(RpcErrorCode::RpcVerifyRejected) + ); let (bob_swap, bob_join_handle) = ctx .stop_and_resume_bob_from_db(bob_join_handle, swap_id) @@ -67,10 +72,10 @@ async fn given_alice_and_bob_manually_cancel_when_timelock_not_expired_errors() let error = cli::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db) .await .unwrap_err(); - match error.downcast::().unwrap() { - bdk::Error::Electrum(bdk::electrum_client::Error::Protocol(..)) => (), - unexpected => panic!("Failed to refund due to unexpected error: {}", unexpected), - } + assert_eq!( + parse_rpc_error_code(&error).unwrap(), + i64::from(RpcErrorCode::RpcVerifyError) + ); let (bob_swap, _) = ctx .stop_and_resume_bob_from_db(bob_join_handle, swap_id)