mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-01 01:45:40 -04:00
merging master changes
This commit is contained in:
commit
925c7bc179
3
.github/workflows/build-release-binaries.yml
vendored
3
.github/workflows/build-release-binaries.yml
vendored
@ -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
|
||||
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -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,
|
||||
|
13
CHANGELOG.md
13
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
|
||||
|
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)",
|
||||
|
@ -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<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
#[serde(rename = "OK")]
|
||||
Ok,
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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"]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "swap"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
authors = [ "The COMIT guys <hello@comit.network>" ]
|
||||
edition = "2021"
|
||||
description = "XMR/BTC trustless atomic swaps."
|
||||
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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<config::Config> 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")]
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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>,
|
||||
|
@ -37,7 +37,7 @@ impl Add<u32> for BlockHeight {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ExpiredTimelocks {
|
||||
None,
|
||||
Cancel,
|
||||
|
@ -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.
|
||||
///
|
||||
|
@ -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 {
|
||||
|
@ -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<Wallet>,
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
) -> 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))
|
||||
}
|
115
swap/src/cli/cancel_and_refund.rs
Normal file
115
swap/src/cli/cancel_and_refund.rs
Normal file
@ -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<Wallet>,
|
||||
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");
|
||||
};
|
||||
|
||||
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<Wallet>,
|
||||
db: Arc<dyn Database>,
|
||||
) -> 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<Wallet>,
|
||||
db: Arc<dyn Database>,
|
||||
) -> Result<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::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)
|
||||
}
|
@ -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,
|
||||
|
||||
|
@ -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<Wallet>,
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
) -> Result<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::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)
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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")]
|
||||
|
@ -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<PublicViewKey> 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<TxHash> 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]
|
||||
|
@ -26,15 +26,16 @@ impl Wallet {
|
||||
pub async fn open_or_create(url: Url, name: String, env_config: Config) -> Result<Self> {
|
||||
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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -258,7 +258,7 @@ impl From<SpotPriceResponse> for Result<monero::Amount, Error> {
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
@ -102,11 +102,11 @@ impl From<BobState> 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;
|
||||
|
||||
|
@ -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<Txid> {
|
||||
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<()> {
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user