diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index e95c7e32..9c856a53 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -52,8 +52,9 @@ jobs: - uses: Swatinem/rust-cache@v2.2.0 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@master with: + toolchain: 1.63 targets: armv7-unknown-linux-gnueabihf - name: Build ${{ matrix.target }} ${{ matrix.bin }} release binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f476e58..0fa9eae1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,8 +75,9 @@ jobs: - uses: Swatinem/rust-cache@v2.2.0 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@master with: + toolchain: 1.63 targets: armv7-unknown-linux-gnueabihf - name: Build binary @@ -134,6 +135,7 @@ jobs: happy_path_restart_bob_before_xmr_locked, happy_path_restart_alice_after_xmr_locked, alice_and_bob_refund_using_cancel_and_refund_command, + alice_and_bob_refund_using_cancel_then_refund_command, alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired, punish, alice_punishes_after_restart_bob_dead, diff --git a/CHANGELOG.md b/CHANGELOG.md index dd272006..51a1b67a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Minimum Supported Rust Version (MSRV) bumped to 1.63 + +## [0.12.1] - 2023-01-09 + +### Changed + +- Swap: merge separate cancel/refund commands into one `cancel-and-refund` command for stuck swaps + ## [0.12.0] - 2022-12-31 ### Changed @@ -332,7 +342,8 @@ It is possible to migrate critical data from the old db to the sqlite but there - Fixed an issue where Alice would not verify if Bob's Bitcoin lock transaction is semantically correct, i.e. pays the agreed upon amount to an output owned by both of them. Fixing this required a **breaking change** on the network layer and hence old versions are not compatible with this version. -[Unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.0...HEAD +[unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.1...HEAD +[0.12.1]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.0...0.12.1 [0.12.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.11.0...0.12.0 [0.11.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.10.2...0.11.0 [0.10.2]: https://github.com/comit-network/xmr-btc-swap/compare/0.10.1...0.10.2 diff --git a/Cargo.lock b/Cargo.lock index 4ee776ea..78164564 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,9 +130,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" +checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282" dependencies = [ "proc-macro2", "quote", @@ -4214,7 +4214,7 @@ checksum = "8049cf85f0e715d6af38dde439cb0ccb91f67fb9f5f63c80f8b43e48356e1a3f" [[package]] name = "swap" -version = "0.12.0" +version = "0.12.1" dependencies = [ "anyhow", "async-compression", diff --git a/README.md b/README.md index 9b4a0476..3e6c3765 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Please have a look at the [contribution guidelines](./CONTRIBUTING.md). ## Rust Version Support Please note that only the latest stable Rust toolchain is supported. -All stable toolchains since 1.62 _should_ work. +All stable toolchains since 1.63 _should_ work. ## Contact diff --git a/bors.toml b/bors.toml index e02a79db..92e8a84c 100644 --- a/bors.toml +++ b/bors.toml @@ -13,6 +13,7 @@ status = [ "docker_tests (happy_path_restart_alice_after_xmr_locked)", "docker_tests (happy_path_restart_bob_before_xmr_locked)", "docker_tests (alice_and_bob_refund_using_cancel_and_refund_command)", + "docker_tests (alice_and_bob_refund_using_cancel_then_refund_command)", "docker_tests (alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired)", "docker_tests (punish)", "docker_tests (alice_punishes_after_restart_bob_dead)", diff --git a/monero-rpc/src/monerod.rs b/monero-rpc/src/monerod.rs index 32e79cec..830aafdb 100644 --- a/monero-rpc/src/monerod.rs +++ b/monero-rpc/src/monerod.rs @@ -157,7 +157,7 @@ pub struct OutKey { pub unlocked: bool, } -#[derive(Clone, Debug, Deserialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] pub struct BaseResponse { pub credits: u64, pub status: Status, @@ -165,7 +165,7 @@ pub struct BaseResponse { pub untrusted: bool, } -#[derive(Clone, Debug, Deserialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] pub struct GetOIndexesResponse { #[serde(flatten)] pub base: BaseResponse, @@ -173,7 +173,7 @@ pub struct GetOIndexesResponse { pub o_indexes: Vec, } -#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)] pub enum Status { #[serde(rename = "OK")] Ok, diff --git a/monero-rpc/src/wallet.rs b/monero-rpc/src/wallet.rs index 404e9693..bc78e7a6 100644 --- a/monero-rpc/src/wallet.rs +++ b/monero-rpc/src/wallet.rs @@ -157,7 +157,7 @@ pub struct Transfer { pub unsigned_txset: String, } -#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct BlockHeight { pub height: u32, } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 54776f48..8e45b7f1 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.62" +channel = "1.63" # also update this in the readme, changelog, and github actions components = ["clippy"] targets = ["armv7-unknown-linux-gnueabihf"] diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 229b2729..ac40f4c3 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swap" -version = "0.12.0" +version = "0.12.1" authors = [ "The COMIT guys " ] edition = "2021" description = "XMR/BTC trustless atomic swaps." diff --git a/swap/src/api.rs b/swap/src/api.rs index 46af21ae..638fc8a8 100644 --- a/swap/src/api.rs +++ b/swap/src/api.rs @@ -301,7 +301,7 @@ pub mod api_test { swap_id: Some(Uuid::from_str(SWAP_ID).unwrap()), ..Default::default() }, - cmd: Method::Cancel, + cmd: Method::CancelAndRefund, shutdown: Shutdown::new(tx.subscribe()), } } @@ -312,7 +312,7 @@ pub mod api_test { swap_id: Some(Uuid::from_str(SWAP_ID).unwrap()), ..Default::default() }, - cmd: Method::Refund, + cmd: Method::CancelAndRefund, shutdown: Shutdown::new(tx.subscribe()), } } diff --git a/swap/src/api/request.rs b/swap/src/api/request.rs index 63818713..d56ef473 100644 --- a/swap/src/api/request.rs +++ b/swap/src/api/request.rs @@ -377,17 +377,10 @@ impl Request { "result": [] }) } - Method::Cancel => { + Method::CancelAndRefund => { let bitcoin_wallet = context.bitcoin_wallet.as_ref().unwrap(); - let (txid, _) = cli::cancel( - self.params.swap_id.unwrap(), - Arc::clone(bitcoin_wallet), - Arc::clone(&context.db), - ) - .await?; - - tracing::debug!("Cancel transaction successfully published with id {}", txid); + let (txid, _) = cli::cancel_and_refund(swap_id, Arc::clone(bitcoin_wallet), Arc::clone(&context.db).await?; json!({ "txid": txid, diff --git a/swap/src/asb/command.rs b/swap/src/asb/command.rs index 9355568e..6831e595 100644 --- a/swap/src/asb/command.rs +++ b/swap/src/asb/command.rs @@ -171,7 +171,7 @@ fn env_config(is_testnet: bool) -> env::Config { } } -#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize)] +#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[error("Invalid Bitcoin address provided, expected address on network {expected:?} but address provided is on {actual:?}")] pub struct BitcoinAddressNetworkMismatch { #[serde(with = "crate::bitcoin::network")] @@ -180,7 +180,7 @@ pub struct BitcoinAddressNetworkMismatch { actual: bitcoin::Network, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub struct Arguments { pub testnet: bool, pub json: bool, @@ -190,7 +190,7 @@ pub struct Arguments { pub cmd: Command, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Command { Start { resume_only: bool, diff --git a/swap/src/asb/config.rs b/swap/src/asb/config.rs index 0aef46bc..7e8492ec 100644 --- a/swap/src/asb/config.rs +++ b/swap/src/asb/config.rs @@ -84,7 +84,7 @@ const DEFAULT_MIN_BUY_AMOUNT: f64 = 0.002f64; const DEFAULT_MAX_BUY_AMOUNT: f64 = 0.02f64; const DEFAULT_SPREAD: f64 = 0.02f64; -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct Config { pub data: Data, @@ -123,13 +123,13 @@ impl TryFrom for Config { } } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct Data { pub dir: PathBuf, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct Network { #[serde(deserialize_with = "addr_list::deserialize")] @@ -181,7 +181,7 @@ mod addr_list { } } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct Bitcoin { pub electrum_rpc_url: Url, @@ -191,7 +191,7 @@ pub struct Bitcoin { pub network: bitcoin::Network, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct Monero { pub wallet_rpc_url: Url, @@ -200,14 +200,14 @@ pub struct Monero { pub network: monero::Network, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct TorConf { pub control_port: u16, pub socks5_port: u16, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct Maker { #[serde(with = "::bitcoin::util::amount::serde::as_btc")] diff --git a/swap/src/asb/rate.rs b/swap/src/asb/rate.rs index c394e91f..70208e0e 100644 --- a/swap/src/asb/rate.rs +++ b/swap/src/asb/rate.rs @@ -5,7 +5,7 @@ use rust_decimal::Decimal; use std::fmt::{Debug, Display, Formatter}; /// Represents the rate at which we are willing to trade 1 XMR. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Rate { /// Represents the asking price from the market. ask: bitcoin::Amount, diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 1ed587a9..f3e42f63 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -108,7 +108,7 @@ impl SecretKey { } } -#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PublicKey(Point); impl PublicKey { diff --git a/swap/src/bitcoin/lock.rs b/swap/src/bitcoin/lock.rs index 9e2b8795..42819ad5 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; const SCRIPT_SIZE: usize = 34; const TX_LOCK_WEIGHT: usize = 485; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TxLock { inner: PartiallySignedTransaction, pub(in crate::bitcoin) output_descriptor: Descriptor<::bitcoin::PublicKey>, diff --git a/swap/src/bitcoin/timelocks.rs b/swap/src/bitcoin/timelocks.rs index 04f589a9..e8b72ea6 100644 --- a/swap/src/bitcoin/timelocks.rs +++ b/swap/src/bitcoin/timelocks.rs @@ -37,7 +37,7 @@ impl Add for BlockHeight { } } -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExpiredTimelocks { None, Cancel, diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 61bff14d..cf1530d1 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -876,7 +876,7 @@ impl EstimateFeeRate for Client { } } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ScriptStatus { Unseen, InMempool, @@ -893,7 +893,7 @@ impl ScriptStatus { } } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Confirmed { /// The depth of this transaction within the blockchain. /// diff --git a/swap/src/cli.rs b/swap/src/cli.rs index 8f5a547f..c98634d2 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -1,17 +1,15 @@ mod behaviour; -pub mod cancel; +pub mod cancel_and_refund; pub mod command; mod event_loop; mod list_sellers; -pub mod refund; pub mod tracing; pub mod transport; pub use behaviour::{Behaviour, OutEvent}; -pub use cancel::cancel; +pub use cancel_and_refund::{cancel, cancel_and_refund, refund}; pub use event_loop::{EventLoop, EventLoopHandle}; pub use list_sellers::{list_sellers, Seller, Status as SellerStatus}; -pub use refund::refund; #[cfg(test)] mod tests { diff --git a/swap/src/cli/cancel.rs b/swap/src/cli/cancel.rs deleted file mode 100644 index b0a80758..00000000 --- a/swap/src/cli/cancel.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::bitcoin::{parse_rpc_error_code, RpcErrorCode, Txid, Wallet}; -use crate::protocol::bob::BobState; -use crate::protocol::Database; -use anyhow::{bail, Result}; -use std::convert::TryInto; -use std::sync::Arc; -use uuid::Uuid; - -pub async fn cancel( - swap_id: Uuid, - bitcoin_wallet: Arc, - db: Arc, -) -> Result<(Txid, BobState)> { - let state = db.get_state(swap_id).await?.try_into()?; - - let state6 = match state { - BobState::BtcLocked { state3, .. } => state3.cancel(), - BobState::XmrLockProofReceived { state, .. } => state.cancel(), - BobState::XmrLocked(state4) => state4.cancel(), - BobState::EncSigSent(state4) => state4.cancel(), - BobState::CancelTimelockExpired(state6) => state6, - BobState::BtcRefunded(state6) => state6, - BobState::BtcCancelled(state6) => state6, - - BobState::Started { .. } - | BobState::SwapSetupCompleted(_) - | BobState::BtcRedeemed(_) - | BobState::XmrRedeemed { .. } - | BobState::BtcPunished { .. } - | BobState::SafelyAborted => bail!( - "Cannot cancel swap {} because it is in state {} which is not refundable.", - swap_id, - state - ), - }; - - tracing::info!(%swap_id, "Manually cancelling swap"); - - let txid = match state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await { - Ok(txid) => txid, - Err(err) => { - 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); - } - }; - - let state = BobState::BtcCancelled(state6); - db.insert_latest_state(swap_id, state.clone().into()) - .await?; - - Ok((txid, state)) -} diff --git a/swap/src/cli/cancel_and_refund.rs b/swap/src/cli/cancel_and_refund.rs new file mode 100644 index 00000000..938e33e0 --- /dev/null +++ b/swap/src/cli/cancel_and_refund.rs @@ -0,0 +1,115 @@ +use crate::bitcoin::wallet::Subscription; +use crate::bitcoin::{parse_rpc_error_code, RpcErrorCode, Wallet}; +use crate::protocol::bob::BobState; +use crate::protocol::Database; +use anyhow::{bail, Result}; +use bitcoin::Txid; +use std::sync::Arc; +use uuid::Uuid; + +pub async fn cancel_and_refund( + swap_id: Uuid, + bitcoin_wallet: Arc, + db: Arc, +) -> Result { + if let Err(err) = cancel(swap_id, bitcoin_wallet.clone(), db.clone()).await { + tracing::info!(%err, "Could not submit cancel transaction"); + }; + + let state = match refund(swap_id, bitcoin_wallet, db).await { + Ok(s) => s, + Err(e) => bail!(e), + }; + + tracing::info!("Refund transaction submitted"); + Ok(state) +} + +pub async fn cancel( + swap_id: Uuid, + bitcoin_wallet: Arc, + db: Arc, +) -> Result<(Txid, Subscription, BobState)> { + let state = db.get_state(swap_id).await?.try_into()?; + + let state6 = match state { + BobState::BtcLocked { state3, .. } => state3.cancel(), + BobState::XmrLockProofReceived { state, .. } => state.cancel(), + BobState::XmrLocked(state4) => state4.cancel(), + BobState::EncSigSent(state4) => state4.cancel(), + BobState::CancelTimelockExpired(state6) => state6, + BobState::BtcRefunded(state6) => state6, + BobState::BtcCancelled(state6) => state6, + + BobState::Started { .. } + | BobState::SwapSetupCompleted(_) + | BobState::BtcRedeemed(_) + | BobState::XmrRedeemed { .. } + | BobState::BtcPunished { .. } + | BobState::SafelyAborted => bail!( + "Cannot cancel swap {} because it is in state {} which is not refundable.", + swap_id, + state + ), + }; + + tracing::info!(%swap_id, "Manually cancelling swap"); + + let (txid, subscription) = match state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await { + Ok(txid) => txid, + 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"); + } + } + 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( + swap_id: Uuid, + bitcoin_wallet: Arc, + db: Arc, +) -> Result { + let state = db.get_state(swap_id).await?.try_into()?; + + let state6 = match state { + BobState::BtcLocked { state3, .. } => state3.cancel(), + BobState::XmrLockProofReceived { state, .. } => state.cancel(), + BobState::XmrLocked(state4) => state4.cancel(), + BobState::EncSigSent(state4) => state4.cancel(), + BobState::CancelTimelockExpired(state6) => state6, + BobState::BtcCancelled(state6) => state6, + BobState::Started { .. } + | BobState::SwapSetupCompleted(_) + | BobState::BtcRedeemed(_) + | BobState::BtcRefunded(_) + | BobState::XmrRedeemed { .. } + | BobState::BtcPunished { .. } + | BobState::SafelyAborted => bail!( + "Cannot refund swap {} because it is in state {} which is not refundable.", + swap_id, + state + ), + }; + + tracing::info!(%swap_id, "Manually refunding swap"); + state6.publish_refund_btc(bitcoin_wallet.as_ref()).await?; + + let state = BobState::BtcRefunded(state6); + db.insert_latest_state(swap_id, state.clone().into()) + .await?; + + Ok(state) +} diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index 00c7f937..7ed77291 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -227,7 +227,7 @@ where .await?; (context, request) } - CliCommand::Cancel { + CliCommand::CancelAndRefund { swap_id: SwapId { swap_id }, bitcoin, tor, @@ -237,35 +237,7 @@ where swap_id: Some(swap_id), ..Default::default() }, - cmd: Method::Cancel, - shutdown: Shutdown::new(rx.subscribe()), - }; - - let context = Context::build( - Some(bitcoin), - None, - Some(tor), - data, - is_testnet, - debug, - json, - None, - rx, - ) - .await?; - (context, request) - } - CliCommand::Refund { - swap_id: SwapId { swap_id }, - bitcoin, - tor, - } => { - let request = Request { - params: Params { - swap_id: Some(swap_id), - ..Default::default() - }, - cmd: Method::Refund, + cmd: Method::CancelAndRefund, shutdown: Shutdown::new(rx.subscribe()), }; @@ -465,21 +437,9 @@ enum CliCommand { #[structopt(flatten)] tor: Tor, }, - /// Force submission of the cancel transaction overriding the protocol state - /// machine and blockheight checks (expert users only) - Cancel { - #[structopt(flatten)] - swap_id: SwapId, - - #[structopt(flatten)] - bitcoin: Bitcoin, - - #[structopt(flatten)] - tor: Tor, - }, - /// Force submission of the refund transaction overriding the protocol state - /// machine and blockheight checks (expert users only) - Refund { + /// Force the submission of the cancel and refund transactions of a swap + #[structopt(aliases = &["cancel", "refund"])] + CancelAndRefund { #[structopt(flatten)] swap_id: SwapId, diff --git a/swap/src/cli/refund.rs b/swap/src/cli/refund.rs deleted file mode 100644 index 4e66f911..00000000 --- a/swap/src/cli/refund.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::bitcoin::Wallet; -use crate::protocol::bob::BobState; -use crate::protocol::Database; -use anyhow::{bail, Result}; -use std::convert::TryInto; -use std::sync::Arc; -use uuid::Uuid; - -pub async fn refund( - swap_id: Uuid, - bitcoin_wallet: Arc, - db: Arc, -) -> Result { - let state = db.get_state(swap_id).await?.try_into()?; - - let state6 = match state { - BobState::BtcLocked { state3, .. } => state3.cancel(), - BobState::XmrLockProofReceived { state, .. } => state.cancel(), - BobState::XmrLocked(state4) => state4.cancel(), - BobState::EncSigSent(state4) => state4.cancel(), - BobState::CancelTimelockExpired(state6) => state6, - BobState::BtcCancelled(state6) => state6, - BobState::Started { .. } - | BobState::SwapSetupCompleted(_) - | BobState::BtcRedeemed(_) - | BobState::BtcRefunded(_) - | BobState::XmrRedeemed { .. } - | BobState::BtcPunished { .. } - | BobState::SafelyAborted => bail!( - "Cannot refund swap {} because it is in state {} which is not refundable.", - swap_id, - state - ), - }; - - state6.publish_refund_btc(bitcoin_wallet.as_ref()).await?; - - let state = BobState::BtcRefunded(state6); - db.insert_latest_state(swap_id, state.clone().into()) - .await?; - - Ok(state) -} diff --git a/swap/src/common.rs b/swap/src/common.rs index 959d0a3b..98b9b99d 100644 --- a/swap/src/common.rs +++ b/swap/src/common.rs @@ -2,7 +2,7 @@ use anyhow::anyhow; const LATEST_RELEASE_URL: &str = "https://github.com/comit-network/xmr-btc-swap/releases/latest"; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Version { Current, Available, diff --git a/swap/src/database/alice.rs b/swap/src/database/alice.rs index 1262d04a..4ed61790 100644 --- a/swap/src/database/alice.rs +++ b/swap/src/database/alice.rs @@ -70,7 +70,7 @@ pub enum Alice { Done(AliceEndState), } -#[derive(Copy, Clone, strum::Display, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Copy, Clone, strum::Display, Debug, Deserialize, Serialize, PartialEq, Eq)] pub enum AliceEndState { SafelyAborted, BtcRedeemed, diff --git a/swap/src/env.rs b/swap/src/env.rs index 73c6d665..a80d402d 100644 --- a/swap/src/env.rs +++ b/swap/src/env.rs @@ -5,7 +5,7 @@ use std::cmp::max; use std::time::Duration; use time::ext::NumericalStdDuration; -#[derive(Debug, Copy, Clone, PartialEq, Serialize)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)] pub struct Config { pub bitcoin_lock_mempool_timeout: Duration, pub bitcoin_lock_confirmed_timeout: Duration, diff --git a/swap/src/kraken.rs b/swap/src/kraken.rs index 9e43ac2c..29062114 100644 --- a/swap/src/kraken.rs +++ b/swap/src/kraken.rs @@ -230,7 +230,7 @@ mod wire { use bitcoin::util::amount::ParseAmountError; use serde_json::Value; - #[derive(Debug, Deserialize, PartialEq)] + #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(tag = "event")] pub enum Event { #[serde(rename = "systemStatus")] diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 0e9b5614..158a582b 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -39,7 +39,7 @@ pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> PrivateKey PrivateKey::from_scalar(Scalar::from_bytes_mod_order(bytes)) } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey); impl PrivateViewKey { @@ -78,7 +78,7 @@ impl From for PublicKey { #[derive(Clone, Copy, Debug)] pub struct PublicViewKey(PublicKey); -#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)] +#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd)] pub struct Amount(u64); // Median tx fees on Monero as found here: https://www.monero.how/monero-transaction-fees, XMR 0.000_008 * 2 (to be on the safe side) @@ -185,7 +185,7 @@ impl fmt::Display for Amount { } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct TransferProof { tx_hash: TxHash, #[serde(with = "monero_private_key")] @@ -205,7 +205,7 @@ impl TransferProof { } // TODO: add constructor/ change String to fixed length byte array -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct TxHash(pub String); impl From for String { @@ -227,7 +227,7 @@ pub struct InsufficientFunds { pub actual: Amount, } -#[derive(thiserror::Error, Debug, Clone, PartialEq)] +#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] #[error("Overflow, cannot convert {0} to u64")] pub struct OverflowError(pub String); @@ -507,10 +507,10 @@ mod tests { use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; - #[derive(Debug, Serialize, Deserialize, PartialEq)] + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey); - #[derive(Debug, Serialize, Deserialize, PartialEq)] + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount); #[test] diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs index a8fccc5d..3f548c41 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -26,15 +26,16 @@ impl Wallet { pub async fn open_or_create(url: Url, name: String, env_config: Config) -> Result { let client = wallet::Client::new(url)?; - let open_wallet_response = client.open_wallet(name.clone()).await; - if open_wallet_response.is_err() { - client.create_wallet(name.clone(), "English".to_owned()).await.context( - "Unable to create Monero wallet, please ensure that the monero-wallet-rpc is available", - )?; + match client.open_wallet(name.clone()).await { + Err(error) => { + tracing::debug!(%error, "Open wallet response error"); + client.create_wallet(name.clone(), "English".to_owned()).await.context( + "Unable to create Monero wallet, please ensure that the monero-wallet-rpc is available", + )?; - tracing::debug!(monero_wallet_name = %name, "Created Monero wallet"); - } else { - tracing::debug!(monero_wallet_name = %name, "Opened Monero wallet"); + tracing::debug!(monero_wallet_name = %name, "Created Monero wallet"); + } + Ok(_) => tracing::debug!(monero_wallet_name = %name, "Opened Monero wallet"), } Self::connect(client, name, env_config).await diff --git a/swap/src/network/rendezvous.rs b/swap/src/network/rendezvous.rs index 86a698a2..bbc0276b 100644 --- a/swap/src/network/rendezvous.rs +++ b/swap/src/network/rendezvous.rs @@ -1,7 +1,7 @@ use libp2p::rendezvous::Namespace; use std::fmt; -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum XmrBtcNamespace { Mainnet, Testnet, diff --git a/swap/src/network/swap_setup.rs b/swap/src/network/swap_setup.rs index 4b9cf3c2..eb48a2f2 100644 --- a/swap/src/network/swap_setup.rs +++ b/swap/src/network/swap_setup.rs @@ -37,7 +37,7 @@ pub mod protocol { >; } -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] pub struct BlockchainNetwork { #[serde(with = "crate::bitcoin::network")] pub bitcoin: bitcoin::Network, diff --git a/swap/src/network/swap_setup/bob.rs b/swap/src/network/swap_setup/bob.rs index ae0b921f..18972319 100644 --- a/swap/src/network/swap_setup/bob.rs +++ b/swap/src/network/swap_setup/bob.rs @@ -258,7 +258,7 @@ impl From for Result { } } -#[derive(Clone, Debug, thiserror::Error, PartialEq)] +#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] pub enum Error { #[error("Seller currently does not accept incoming swap requests, please try again later")] NoSwapsAccepted, diff --git a/swap/src/protocol.rs b/swap/src/protocol.rs index 70ffd648..794f27a9 100644 --- a/swap/src/protocol.rs +++ b/swap/src/protocol.rs @@ -102,11 +102,11 @@ impl From for State { } } -#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)] +#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)] #[error("Not in the role of Alice")] pub struct NotAlice; -#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)] +#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)] #[error("Not in the role of Bob")] pub struct NotBob; diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 2bec026d..f683ffa7 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -1,4 +1,4 @@ -use crate::bitcoin::wallet::EstimateFeeRate; +use crate::bitcoin::wallet::{EstimateFeeRate, Subscription}; use crate::bitcoin::{ self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel, TxLock, Txid, @@ -561,7 +561,7 @@ impl State4 { } } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct State5 { #[serde(with = "monero_private_key")] s_a: monero::PrivateKey, @@ -642,7 +642,10 @@ impl State6 { Ok(tx) } - pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { + pub async fn submit_tx_cancel( + &self, + bitcoin_wallet: &bitcoin::Wallet, + ) -> Result<(Txid, Subscription)> { let transaction = bitcoin::TxCancel::new( &self.tx_lock, self.cancel_timelock, @@ -653,9 +656,9 @@ impl State6 { .complete_as_bob(self.A, self.b.clone(), self.tx_cancel_sig_a.clone()) .context("Failed to complete Bitcoin cancel transaction")?; - let (tx_id, _) = bitcoin_wallet.broadcast(transaction, "cancel").await?; + let (tx_id, subscription) = bitcoin_wallet.broadcast(transaction, "cancel").await?; - Ok(tx_id) + Ok((tx_id, subscription)) } pub async fn publish_refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> { diff --git a/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command.rs b/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command.rs index 33c33ba7..1870bed4 100644 --- a/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command.rs +++ b/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command.rs @@ -50,7 +50,7 @@ async fn given_alice_and_bob_manually_refund_after_funds_locked_both_refund() { // Bob manually cancels bob_join_handle.abort(); - let (_, state) = cli::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db).await?; + let (_, _, state) = cli::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db).await?; assert!(matches!(state, BobState::BtcCancelled { .. })); let (bob_swap, bob_join_handle) = ctx @@ -64,7 +64,7 @@ async fn given_alice_and_bob_manually_refund_after_funds_locked_both_refund() { ctx.assert_bob_refunded(bob_state).await; - // manually refund ALice's swap + // manually refund Alice's swap ctx.restart_alice().await; let alice_swap = ctx.alice_next_swap().await; let alice_state = asb::refund( diff --git a/swap/tests/alice_and_bob_refund_using_cancel_then_refund_command.rs b/swap/tests/alice_and_bob_refund_using_cancel_then_refund_command.rs new file mode 100644 index 00000000..d1302ec6 --- /dev/null +++ b/swap/tests/alice_and_bob_refund_using_cancel_then_refund_command.rs @@ -0,0 +1,74 @@ +pub mod harness; + +use harness::alice_run_until::is_xmr_lock_transaction_sent; +use harness::bob_run_until::is_btc_locked; +use harness::FastCancelConfig; +use swap::asb::FixedRate; +use swap::protocol::alice::AliceState; +use swap::protocol::bob::BobState; +use swap::protocol::{alice, bob}; +use swap::{asb, cli}; + +#[tokio::test] +async fn given_alice_and_bob_manually_cancel_and_refund_after_funds_locked_both_refund() { + harness::setup_test(FastCancelConfig, |mut ctx| async move { + let (bob_swap, bob_join_handle) = ctx.bob_swap().await; + let bob_swap_id = bob_swap.id; + let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); + + let alice_swap = ctx.alice_next_swap().await; + let alice_swap = tokio::spawn(alice::run_until( + alice_swap, + is_xmr_lock_transaction_sent, + FixedRate::default(), + )); + + let bob_state = bob_swap.await??; + assert!(matches!(bob_state, BobState::BtcLocked { .. })); + + let alice_state = alice_swap.await??; + assert!(matches!( + alice_state, + AliceState::XmrLockTransactionSent { .. } + )); + + let (bob_swap, bob_join_handle) = ctx + .stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id) + .await; + + // Ensure cancel timelock is expired + if let BobState::BtcLocked { state3, .. } = bob_swap.state.clone() { + bob_swap + .bitcoin_wallet + .subscribe_to(state3.tx_lock) + .await + .wait_until_confirmed_with(state3.cancel_timelock) + .await?; + } else { + panic!("Bob in unexpected state {}", bob_swap.state); + } + + // Bob manually cancels and refunds + bob_join_handle.abort(); + let bob_state = + cli::cancel_and_refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db).await?; + + ctx.assert_bob_refunded(bob_state).await; + + // manually refund Alice's swap + ctx.restart_alice().await; + let alice_swap = ctx.alice_next_swap().await; + let alice_state = asb::refund( + alice_swap.swap_id, + alice_swap.bitcoin_wallet, + alice_swap.monero_wallet, + alice_swap.db, + ) + .await?; + + ctx.assert_alice_refunded(alice_state).await; + + Ok(()) + }) + .await +}