mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-12-18 04:14:32 -05:00
ASB manual recovery commands
Adds `cancel`, `refund`, `punish`, `redeem` and `safely-abort` commands to the ASB that can be used to trigger the specific scenario for the swap by ID.
This commit is contained in:
parent
efcd39eeef
commit
4deb96a3c5
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -100,9 +100,9 @@ jobs:
|
|||||||
happy_path_restart_bob_after_xmr_locked,
|
happy_path_restart_bob_after_xmr_locked,
|
||||||
happy_path_restart_bob_before_xmr_locked,
|
happy_path_restart_bob_before_xmr_locked,
|
||||||
happy_path_restart_alice_after_xmr_locked,
|
happy_path_restart_alice_after_xmr_locked,
|
||||||
bob_refunds_using_cancel_and_refund_command,
|
alice_and_bob_refund_using_cancel_and_refund_command,
|
||||||
bob_refunds_using_cancel_and_refund_command_timelock_not_expired,
|
alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired,
|
||||||
bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force,
|
alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force,
|
||||||
punish,
|
punish,
|
||||||
alice_punishes_after_restart_punish_timelock_expired,
|
alice_punishes_after_restart_punish_timelock_expired,
|
||||||
alice_refunds_after_restart_bob_refunded,
|
alice_refunds_after_restart_bob_refunded,
|
||||||
|
@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Cancel command for the ASB that allows cancelling a specific swap by id.
|
||||||
|
Using the cancel command requires the cancel timelock to be expired, but `--force` can be used to circumvent this check.
|
||||||
|
- Refund command for the ASB that allows refunding a specific swap by id.
|
||||||
|
Using the refund command to refund the XMR locked by the ASB requires the CLI to first refund the BTC of the swap.
|
||||||
|
If the BTC was not refunded yet the command will print an error accordingly.
|
||||||
|
The command has a `--force` flag that allows executing the command without checking for cancel constraints.
|
||||||
- Resume-only mode for the ASB.
|
- Resume-only mode for the ASB.
|
||||||
When started with `--resume-only` the ASB does not accept new, incoming swap requests but only finishes swaps that are resumed upon startup.
|
When started with `--resume-only` the ASB does not accept new, incoming swap requests but only finishes swaps that are resumed upon startup.
|
||||||
|
|
||||||
|
@ -10,9 +10,9 @@ status = [
|
|||||||
"docker_tests (happy_path_restart_bob_after_xmr_locked)",
|
"docker_tests (happy_path_restart_bob_after_xmr_locked)",
|
||||||
"docker_tests (happy_path_restart_alice_after_xmr_locked)",
|
"docker_tests (happy_path_restart_alice_after_xmr_locked)",
|
||||||
"docker_tests (happy_path_restart_bob_before_xmr_locked)",
|
"docker_tests (happy_path_restart_bob_before_xmr_locked)",
|
||||||
"docker_tests (bob_refunds_using_cancel_and_refund_command)",
|
"docker_tests (alice_and_bob_refund_using_cancel_and_refund_command)",
|
||||||
"docker_tests (bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force)",
|
"docker_tests (alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired)",
|
||||||
"docker_tests (bob_refunds_using_cancel_and_refund_command_timelock_not_expired)",
|
"docker_tests (alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force)",
|
||||||
"docker_tests (punish)",
|
"docker_tests (punish)",
|
||||||
"docker_tests (alice_punishes_after_restart_punish_timelock_expired)",
|
"docker_tests (alice_punishes_after_restart_punish_timelock_expired)",
|
||||||
"docker_tests (alice_refunds_after_restart_bob_refunded)",
|
"docker_tests (alice_refunds_after_restart_bob_refunded)",
|
||||||
|
@ -3,6 +3,7 @@ use bitcoin::util::amount::ParseAmountError;
|
|||||||
use bitcoin::{Address, Denomination};
|
use bitcoin::{Address, Denomination};
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(structopt::StructOpt, Debug)]
|
#[derive(structopt::StructOpt, Debug)]
|
||||||
#[structopt(
|
#[structopt(
|
||||||
@ -58,6 +59,42 @@ pub enum Command {
|
|||||||
about = "Prints the Bitcoin and Monero balance. Requires the monero-wallet-rpc to be running."
|
about = "Prints the Bitcoin and Monero balance. Requires the monero-wallet-rpc to be running."
|
||||||
)]
|
)]
|
||||||
Balance,
|
Balance,
|
||||||
|
#[structopt(about = "Contains sub-commands for recovering a swap manually.")]
|
||||||
|
ManualRecovery(ManualRecovery),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(structopt::StructOpt, Debug)]
|
||||||
|
pub enum ManualRecovery {
|
||||||
|
#[structopt(
|
||||||
|
about = "Publishes the Bitcoin cancel transaction. By default, the cancel timelock will be enforced. A confirmed cancel transaction enables refund and punish."
|
||||||
|
)]
|
||||||
|
Cancel {
|
||||||
|
#[structopt(flatten)]
|
||||||
|
cancel_params: RecoverCommandParams,
|
||||||
|
},
|
||||||
|
#[structopt(
|
||||||
|
about = "Publishes the Monero refund transaction. By default, a swap-state where the cancel transaction was already published will be enforced. This command requires the counterparty Bitcoin refund transaction and will error if it was not published yet. "
|
||||||
|
)]
|
||||||
|
Refund {
|
||||||
|
#[structopt(flatten)]
|
||||||
|
refund_params: RecoverCommandParams,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(structopt::StructOpt, Debug)]
|
||||||
|
pub struct RecoverCommandParams {
|
||||||
|
#[structopt(
|
||||||
|
long = "swap-id",
|
||||||
|
help = "The swap id can be retrieved using the history subcommand"
|
||||||
|
)]
|
||||||
|
pub swap_id: Uuid,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "Circumvents certain checks when recovering. It is recommended to run a recovery command without --force first to see what is returned."
|
||||||
|
)]
|
||||||
|
pub force: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_btc(s: &str) -> Result<Amount, ParseAmountError> {
|
fn parse_btc(s: &str) -> Result<Amount, ParseAmountError> {
|
||||||
|
@ -20,7 +20,7 @@ use prettytable::{row, Table};
|
|||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
use swap::asb::command::{Arguments, Command};
|
use swap::asb::command::{Arguments, Command, ManualRecovery, RecoverCommandParams};
|
||||||
use swap::asb::config::{
|
use swap::asb::config::{
|
||||||
default_config_path, initial_setup, query_user_for_initial_testnet_config, read_config, Config,
|
default_config_path, initial_setup, query_user_for_initial_testnet_config, read_config, Config,
|
||||||
ConfigNotInitialized,
|
ConfigNotInitialized,
|
||||||
@ -29,6 +29,7 @@ use swap::database::Database;
|
|||||||
use swap::env::GetConfig;
|
use swap::env::GetConfig;
|
||||||
use swap::monero::Amount;
|
use swap::monero::Amount;
|
||||||
use swap::network::swarm;
|
use swap::network::swarm;
|
||||||
|
use swap::protocol::alice;
|
||||||
use swap::protocol::alice::event_loop::KrakenRate;
|
use swap::protocol::alice::event_loop::KrakenRate;
|
||||||
use swap::protocol::alice::{run, EventLoop};
|
use swap::protocol::alice::{run, EventLoop};
|
||||||
use swap::seed::Seed;
|
use swap::seed::Seed;
|
||||||
@ -205,6 +206,33 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
tracing::info!("Current balance: {}, {}", bitcoin_balance, monero_balance);
|
tracing::info!("Current balance: {}, {}", bitcoin_balance, monero_balance);
|
||||||
}
|
}
|
||||||
|
Command::ManualRecovery(ManualRecovery::Cancel {
|
||||||
|
cancel_params: RecoverCommandParams { swap_id, force },
|
||||||
|
}) => {
|
||||||
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
|
|
||||||
|
let (txid, _) =
|
||||||
|
alice::cancel(swap_id, Arc::new(bitcoin_wallet), Arc::new(db), force).await??;
|
||||||
|
|
||||||
|
tracing::info!("Cancel transaction successfully published with id {}", txid);
|
||||||
|
}
|
||||||
|
Command::ManualRecovery(ManualRecovery::Refund {
|
||||||
|
refund_params: RecoverCommandParams { swap_id, force },
|
||||||
|
}) => {
|
||||||
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
|
let monero_wallet = init_monero_wallet(&config, env_config).await?;
|
||||||
|
|
||||||
|
alice::refund(
|
||||||
|
swap_id,
|
||||||
|
Arc::new(bitcoin_wallet),
|
||||||
|
Arc::new(monero_wallet),
|
||||||
|
Arc::new(db),
|
||||||
|
force,
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
tracing::info!("Monero successfully refunded");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -7,13 +7,17 @@ use std::sync::Arc;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub use self::behaviour::{Behaviour, OutEvent};
|
pub use self::behaviour::{Behaviour, OutEvent};
|
||||||
|
pub use self::cancel::cancel;
|
||||||
pub use self::event_loop::{EventLoop, EventLoopHandle};
|
pub use self::event_loop::{EventLoop, EventLoopHandle};
|
||||||
|
pub use self::refund::refund;
|
||||||
pub use self::state::*;
|
pub use self::state::*;
|
||||||
pub use self::swap::{run, run_until};
|
pub use self::swap::{run, run_until};
|
||||||
|
|
||||||
mod behaviour;
|
mod behaviour;
|
||||||
|
pub mod cancel;
|
||||||
pub mod event_loop;
|
pub mod event_loop;
|
||||||
mod execution_setup;
|
mod execution_setup;
|
||||||
|
pub mod refund;
|
||||||
mod spot_price;
|
mod spot_price;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod swap;
|
pub mod swap;
|
||||||
|
77
swap/src/protocol/alice/cancel.rs
Normal file
77
swap/src/protocol/alice/cancel.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
use crate::bitcoin::{ExpiredTimelocks, Txid, Wallet};
|
||||||
|
use crate::database::{Database, Swap};
|
||||||
|
use crate::protocol::alice::AliceState;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error, Clone, Copy)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("The cancel transaction cannot be published because the cancel timelock has not expired yet. Please try again later")]
|
||||||
|
CancelTimelockNotExpiredYet,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cancel(
|
||||||
|
swap_id: Uuid,
|
||||||
|
bitcoin_wallet: Arc<Wallet>,
|
||||||
|
db: Arc<Database>,
|
||||||
|
force: bool,
|
||||||
|
) -> Result<Result<(Txid, AliceState), Error>> {
|
||||||
|
let state = db.get_state(swap_id)?.try_into_alice()?.into();
|
||||||
|
|
||||||
|
let (monero_wallet_restore_blockheight, transfer_proof, state3) = match state {
|
||||||
|
|
||||||
|
// In case no XMR has been locked, move to Safely Aborted
|
||||||
|
AliceState::Started { .. }
|
||||||
|
| AliceState::BtcLocked { .. } => bail!("Cannot cancel swap {} because it is in state {} where no XMR was locked.", swap_id, state),
|
||||||
|
|
||||||
|
AliceState::XmrLockTransactionSent { monero_wallet_restore_blockheight, transfer_proof, state3, }
|
||||||
|
| AliceState::XmrLocked { monero_wallet_restore_blockheight, transfer_proof, state3 }
|
||||||
|
| AliceState::XmrLockTransferProofSent { monero_wallet_restore_blockheight, transfer_proof, state3 }
|
||||||
|
// in cancel mode we do not care about the fact that we could redeem, but always wait for cancellation (leading either refund or punish)
|
||||||
|
| AliceState::EncSigLearned { monero_wallet_restore_blockheight, transfer_proof, state3, .. }
|
||||||
|
| AliceState::CancelTimelockExpired { monero_wallet_restore_blockheight, transfer_proof, state3} => {
|
||||||
|
(monero_wallet_restore_blockheight, transfer_proof, state3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cancel tx was already published, but Alice not yet in final state
|
||||||
|
AliceState::BtcCancelled { .. }
|
||||||
|
| AliceState::BtcRefunded { .. }
|
||||||
|
| AliceState::BtcPunishable { .. }
|
||||||
|
|
||||||
|
// Alice already in final state
|
||||||
|
| AliceState::BtcRedeemed
|
||||||
|
| AliceState::XmrRefunded
|
||||||
|
| AliceState::BtcPunished
|
||||||
|
| AliceState::SafelyAborted => bail!("Cannot cancel swap {} because it is in state {} which is not cancelable", swap_id, state),
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!(%swap_id, "Trying to manually cancel swap");
|
||||||
|
|
||||||
|
if !force {
|
||||||
|
tracing::debug!(%swap_id, "Checking if cancel timelock is expired");
|
||||||
|
|
||||||
|
if let ExpiredTimelocks::None = state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
|
||||||
|
return Ok(Err(Error::CancelTimelockNotExpiredYet));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let txid = if let Ok(tx) = state3.check_for_tx_cancel(bitcoin_wallet.as_ref()).await {
|
||||||
|
let txid = tx.txid();
|
||||||
|
tracing::debug!(%swap_id, "Cancel transaction has already been published: {}", txid);
|
||||||
|
txid
|
||||||
|
} else {
|
||||||
|
state3.submit_tx_cancel(bitcoin_wallet.as_ref()).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = AliceState::BtcCancelled {
|
||||||
|
monero_wallet_restore_blockheight,
|
||||||
|
transfer_proof,
|
||||||
|
state3,
|
||||||
|
};
|
||||||
|
let db_state = (&state).into();
|
||||||
|
db.insert_latest_state(swap_id, Swap::Alice(db_state))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Ok((txid, state)))
|
||||||
|
}
|
125
swap/src/protocol/alice/refund.rs
Normal file
125
swap/src/protocol/alice/refund.rs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
use crate::bitcoin::{self};
|
||||||
|
use crate::database::{Database, Swap};
|
||||||
|
use crate::monero;
|
||||||
|
use crate::protocol::alice::AliceState;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use libp2p::PeerId;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
// Errors indicating the the swap can *currently* not be refunded but might be later
|
||||||
|
#[error("Swap is not in a cancelled state. Make sure to cancel the swap before trying to refund or use --force.")]
|
||||||
|
SwapNotCancelled,
|
||||||
|
#[error(
|
||||||
|
"Counterparty {0} did not refund the BTC yet. You can try again later or try to punish."
|
||||||
|
)]
|
||||||
|
RefundTransactionNotPublishedYet(PeerId),
|
||||||
|
|
||||||
|
// Errors indicating that the swap cannot be refunded because because it is in a abort/final
|
||||||
|
// state
|
||||||
|
#[error("Swa is in state {0} where no XMR was locked. Try aborting instead.")]
|
||||||
|
NoXmrLocked(AliceState),
|
||||||
|
#[error("Swap is in state {0} which is not refundable")]
|
||||||
|
SwapNotRefundable(AliceState),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refund(
|
||||||
|
swap_id: Uuid,
|
||||||
|
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||||
|
monero_wallet: Arc<monero::Wallet>,
|
||||||
|
db: Arc<Database>,
|
||||||
|
force: bool,
|
||||||
|
) -> Result<Result<AliceState, Error>> {
|
||||||
|
let state = db.get_state(swap_id)?.try_into_alice()?.into();
|
||||||
|
|
||||||
|
let (monero_wallet_restore_blockheight, transfer_proof, state3) = if force {
|
||||||
|
match state {
|
||||||
|
|
||||||
|
// In case no XMR has been locked, move to Safely Aborted
|
||||||
|
AliceState::Started { .. }
|
||||||
|
| AliceState::BtcLocked { .. } => bail!(Error::NoXmrLocked(state)),
|
||||||
|
|
||||||
|
// Refund potentially possible (no knowledge of cancel transaction)
|
||||||
|
AliceState::XmrLockTransactionSent { monero_wallet_restore_blockheight, transfer_proof, state3, }
|
||||||
|
| AliceState::XmrLocked { monero_wallet_restore_blockheight, transfer_proof, state3 }
|
||||||
|
| AliceState::XmrLockTransferProofSent { monero_wallet_restore_blockheight, transfer_proof, state3 }
|
||||||
|
| AliceState::EncSigLearned { monero_wallet_restore_blockheight, transfer_proof, state3, .. }
|
||||||
|
| AliceState::CancelTimelockExpired { monero_wallet_restore_blockheight, transfer_proof, state3 }
|
||||||
|
|
||||||
|
// Refund possible due to cancel transaction already being published
|
||||||
|
| AliceState::BtcCancelled { monero_wallet_restore_blockheight, transfer_proof, state3 }
|
||||||
|
| AliceState::BtcRefunded { monero_wallet_restore_blockheight, transfer_proof, state3, .. }
|
||||||
|
| AliceState::BtcPunishable { monero_wallet_restore_blockheight, transfer_proof, state3, .. } => {
|
||||||
|
(monero_wallet_restore_blockheight, transfer_proof, state3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice already in final state
|
||||||
|
AliceState::BtcRedeemed
|
||||||
|
| AliceState::XmrRefunded
|
||||||
|
| AliceState::BtcPunished
|
||||||
|
| AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match state {
|
||||||
|
AliceState::Started { .. } | AliceState::BtcLocked { .. } => {
|
||||||
|
bail!(Error::NoXmrLocked(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
AliceState::BtcCancelled {
|
||||||
|
monero_wallet_restore_blockheight,
|
||||||
|
transfer_proof,
|
||||||
|
state3,
|
||||||
|
}
|
||||||
|
| AliceState::BtcRefunded {
|
||||||
|
monero_wallet_restore_blockheight,
|
||||||
|
transfer_proof,
|
||||||
|
state3,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| AliceState::BtcPunishable {
|
||||||
|
monero_wallet_restore_blockheight,
|
||||||
|
transfer_proof,
|
||||||
|
state3,
|
||||||
|
..
|
||||||
|
} => (monero_wallet_restore_blockheight, transfer_proof, state3),
|
||||||
|
|
||||||
|
AliceState::BtcRedeemed
|
||||||
|
| AliceState::XmrRefunded
|
||||||
|
| AliceState::BtcPunished
|
||||||
|
| AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)),
|
||||||
|
|
||||||
|
_ => return Ok(Err(Error::SwapNotCancelled)),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!(%swap_id, "Trying to manually refund swap");
|
||||||
|
|
||||||
|
let spend_key = if let Ok(published_refund_tx) =
|
||||||
|
state3.fetch_tx_refund(bitcoin_wallet.as_ref()).await
|
||||||
|
{
|
||||||
|
tracing::debug!(%swap_id, "Bitcoin refund transaction found, extracting key to refund Monero");
|
||||||
|
state3.extract_monero_private_key(published_refund_tx)?
|
||||||
|
} else {
|
||||||
|
let bob_peer_id = db.get_peer_id(swap_id)?;
|
||||||
|
return Ok(Err(Error::RefundTransactionNotPublishedYet(bob_peer_id)));
|
||||||
|
};
|
||||||
|
|
||||||
|
state3
|
||||||
|
.refund_xmr(
|
||||||
|
&monero_wallet,
|
||||||
|
monero_wallet_restore_blockheight,
|
||||||
|
swap_id.to_string(),
|
||||||
|
spend_key,
|
||||||
|
transfer_proof,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let state = AliceState::XmrRefunded;
|
||||||
|
let db_state = (&state).into();
|
||||||
|
db.insert_latest_state(swap_id, Swap::Alice(db_state))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Ok(state))
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
use crate::bitcoin::{
|
use crate::bitcoin::{
|
||||||
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund,
|
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
|
||||||
|
TxPunish, TxRefund, Txid,
|
||||||
};
|
};
|
||||||
use crate::env::Config;
|
use crate::env::Config;
|
||||||
use crate::monero::wallet::{TransferRequest, WatchRequest};
|
use crate::monero::wallet::{TransferRequest, WatchRequest};
|
||||||
@ -460,6 +461,55 @@ impl State3 {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn check_for_tx_cancel(
|
||||||
|
&self,
|
||||||
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
|
) -> Result<Transaction> {
|
||||||
|
let tx_cancel = self.tx_cancel();
|
||||||
|
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
|
||||||
|
Ok(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_tx_refund(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Transaction> {
|
||||||
|
let tx_refund = self.tx_refund();
|
||||||
|
let tx = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
|
||||||
|
Ok(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Txid> {
|
||||||
|
let transaction = self.signed_cancel_transaction()?;
|
||||||
|
let (tx_id, _) = bitcoin_wallet.broadcast(transaction, "cancel").await?;
|
||||||
|
Ok(tx_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refund_xmr(
|
||||||
|
&self,
|
||||||
|
monero_wallet: &monero::Wallet,
|
||||||
|
monero_wallet_restore_blockheight: BlockHeight,
|
||||||
|
file_name: String,
|
||||||
|
spend_key: monero::PrivateKey,
|
||||||
|
transfer_proof: TransferProof,
|
||||||
|
) -> Result<()> {
|
||||||
|
let view_key = self.v;
|
||||||
|
|
||||||
|
// Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations
|
||||||
|
// on the lock transaction
|
||||||
|
monero_wallet
|
||||||
|
.watch_for_transfer(self.lock_xmr_watch_request(transfer_proof, 10))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
monero_wallet
|
||||||
|
.create_from(
|
||||||
|
file_name,
|
||||||
|
spend_key,
|
||||||
|
view_key,
|
||||||
|
monero_wallet_restore_blockheight,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn signed_redeem_transaction(
|
pub fn signed_redeem_transaction(
|
||||||
&self,
|
&self,
|
||||||
sig: bitcoin::EncryptedSignature,
|
sig: bitcoin::EncryptedSignature,
|
||||||
|
@ -229,23 +229,17 @@ async fn next_state(
|
|||||||
transfer_proof,
|
transfer_proof,
|
||||||
state3,
|
state3,
|
||||||
} => {
|
} => {
|
||||||
let transaction = state3.signed_cancel_transaction()?;
|
if state3.check_for_tx_cancel(bitcoin_wallet).await.is_err() {
|
||||||
|
// If Bob hasn't yet broadcasted the cancel transaction, Alice has to publish it
|
||||||
// If Bob hasn't yet broadcasted the tx cancel, we do it
|
// to be able to eventually punish. Since the punish timelock is
|
||||||
if bitcoin_wallet
|
// relative to the publication of the cancel transaction we have to ensure it
|
||||||
.get_raw_transaction(transaction.txid())
|
// gets published once the cancel timelock expires.
|
||||||
.await
|
if let Err(e) = state3.submit_tx_cancel(bitcoin_wallet).await {
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
if let Err(e) = bitcoin_wallet.broadcast(transaction, "cancel").await {
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Assuming transaction is already broadcasted because: {:#}",
|
"Assuming cancel transaction is already broadcasted because: {:#}",
|
||||||
e
|
e
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(Franck): Wait until transaction is mined and
|
|
||||||
// returned mined block height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AliceState::BtcCancelled {
|
AliceState::BtcCancelled {
|
||||||
@ -291,20 +285,13 @@ async fn next_state(
|
|||||||
spend_key,
|
spend_key,
|
||||||
state3,
|
state3,
|
||||||
} => {
|
} => {
|
||||||
let view_key = state3.v;
|
state3
|
||||||
|
.refund_xmr(
|
||||||
// Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations
|
monero_wallet,
|
||||||
// on the lock transaction
|
monero_wallet_restore_blockheight,
|
||||||
monero_wallet
|
|
||||||
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof, 10))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
monero_wallet
|
|
||||||
.create_from(
|
|
||||||
swap_id.to_string(),
|
swap_id.to_string(),
|
||||||
spend_key,
|
spend_key,
|
||||||
view_key,
|
transfer_proof,
|
||||||
monero_wallet_restore_blockheight,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone, Copy)]
|
#[derive(thiserror::Error, Debug, Clone, Copy)]
|
||||||
#[error("Cannot refund because swap {0} was not cancelled yet. Make sure to cancel the swap before trying to refund.")]
|
#[error("Cannot refund because swap {0} was not cancelled yet. Make sure to cancel the swap before trying to refund.")]
|
||||||
pub struct SwapNotCancelledYet(Uuid);
|
pub struct SwapNotCancelledYet(pub Uuid);
|
||||||
|
|
||||||
pub async fn refund(
|
pub async fn refund(
|
||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
|
@ -1,23 +1,31 @@
|
|||||||
pub mod harness;
|
pub mod harness;
|
||||||
|
|
||||||
|
use harness::alice_run_until::is_xmr_lock_transaction_sent;
|
||||||
use harness::bob_run_until::is_btc_locked;
|
use harness::bob_run_until::is_btc_locked;
|
||||||
use harness::FastCancelConfig;
|
use harness::FastCancelConfig;
|
||||||
|
use swap::protocol::alice::AliceState;
|
||||||
use swap::protocol::bob::BobState;
|
use swap::protocol::bob::BobState;
|
||||||
use swap::protocol::{alice, bob};
|
use swap::protocol::{alice, bob};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
|
async fn given_alice_and_bob_manually_refund_after_funds_locked_both_refund() {
|
||||||
harness::setup_test(FastCancelConfig, |mut ctx| async move {
|
harness::setup_test(FastCancelConfig, |mut ctx| async move {
|
||||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||||
let bob_swap_id = bob_swap.id;
|
let bob_swap_id = bob_swap.id;
|
||||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||||
|
|
||||||
let alice_swap = ctx.alice_next_swap().await;
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
let alice_swap = tokio::spawn(alice::run(alice_swap));
|
let alice_swap = tokio::spawn(alice::run_until(alice_swap, is_xmr_lock_transaction_sent));
|
||||||
|
|
||||||
let bob_state = bob_swap.await??;
|
let bob_state = bob_swap.await??;
|
||||||
assert!(matches!(bob_state, BobState::BtcLocked { .. }));
|
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
|
let (bob_swap, bob_join_handle) = ctx
|
||||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
||||||
.await;
|
.await;
|
||||||
@ -52,7 +60,36 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
|
|||||||
|
|
||||||
ctx.assert_bob_refunded(bob_state).await;
|
ctx.assert_bob_refunded(bob_state).await;
|
||||||
|
|
||||||
let alice_state = alice_swap.await??;
|
// manually cancel ALice's swap (effectively just notice that Bob already
|
||||||
|
// cancelled and record that)
|
||||||
|
ctx.restart_alice().await;
|
||||||
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
|
assert!(matches!(
|
||||||
|
alice_swap.state,
|
||||||
|
AliceState::XmrLockTransactionSent { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
alice::cancel(
|
||||||
|
alice_swap.swap_id,
|
||||||
|
alice_swap.bitcoin_wallet,
|
||||||
|
alice_swap.db,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// manually refund ALice's swap
|
||||||
|
ctx.restart_alice().await;
|
||||||
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
|
assert!(matches!(alice_swap.state, AliceState::BtcCancelled { .. }));
|
||||||
|
let alice_state = alice::refund(
|
||||||
|
alice_swap.swap_id,
|
||||||
|
alice_swap.bitcoin_wallet,
|
||||||
|
alice_swap.monero_wallet,
|
||||||
|
alice_swap.db,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
|
||||||
ctx.assert_alice_refunded(alice_state).await;
|
ctx.assert_alice_refunded(alice_state).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
@ -0,0 +1,109 @@
|
|||||||
|
pub mod harness;
|
||||||
|
|
||||||
|
use harness::alice_run_until::is_xmr_lock_transaction_sent;
|
||||||
|
use harness::bob_run_until::is_btc_locked;
|
||||||
|
use harness::SlowCancelConfig;
|
||||||
|
use swap::protocol::alice::AliceState;
|
||||||
|
use swap::protocol::bob::BobState;
|
||||||
|
use swap::protocol::{alice, bob};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn given_alice_and_bob_manually_cancel_when_timelock_not_expired_errors() {
|
||||||
|
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||||
|
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||||
|
let 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));
|
||||||
|
|
||||||
|
let bob_state = bob_swap.await??;
|
||||||
|
assert!(matches!(bob_state, BobState::BtcLocked { .. }));
|
||||||
|
|
||||||
|
let (bob_swap, bob_join_handle) = ctx
|
||||||
|
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||||
|
.await;
|
||||||
|
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||||
|
|
||||||
|
let alice_state = alice_swap.await??;
|
||||||
|
assert!(matches!(
|
||||||
|
alice_state,
|
||||||
|
AliceState::XmrLockTransactionSent { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
// Bob tries but fails to manually cancel
|
||||||
|
let result = bob::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, false)
|
||||||
|
.await?
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
bob::cancel::Error::CancelTimelockNotExpiredYet
|
||||||
|
));
|
||||||
|
|
||||||
|
ctx.restart_alice().await;
|
||||||
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
|
assert!(matches!(
|
||||||
|
alice_swap.state,
|
||||||
|
AliceState::XmrLockTransactionSent { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
// Alice tries but fails manual cancel
|
||||||
|
let result = alice::cancel(
|
||||||
|
alice_swap.swap_id,
|
||||||
|
alice_swap.bitcoin_wallet,
|
||||||
|
alice_swap.db,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
alice::cancel::Error::CancelTimelockNotExpiredYet
|
||||||
|
));
|
||||||
|
|
||||||
|
let (bob_swap, bob_join_handle) = ctx
|
||||||
|
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||||
|
.await;
|
||||||
|
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||||
|
|
||||||
|
// Bob tries but fails to manually refund
|
||||||
|
let result = bob::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, false)
|
||||||
|
.await?
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(result, bob::refund::SwapNotCancelledYet(_)));
|
||||||
|
|
||||||
|
let (bob_swap, _) = ctx
|
||||||
|
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||||
|
.await;
|
||||||
|
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||||
|
|
||||||
|
ctx.restart_alice().await;
|
||||||
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
|
assert!(matches!(
|
||||||
|
alice_swap.state,
|
||||||
|
AliceState::XmrLockTransactionSent { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
// Alice tries but fails manual cancel
|
||||||
|
let result = alice::refund(
|
||||||
|
alice_swap.swap_id,
|
||||||
|
alice_swap.bitcoin_wallet,
|
||||||
|
alice_swap.monero_wallet,
|
||||||
|
alice_swap.db,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(result, alice::refund::Error::SwapNotCancelled));
|
||||||
|
|
||||||
|
ctx.restart_alice().await;
|
||||||
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
|
assert!(matches!(
|
||||||
|
alice_swap.state,
|
||||||
|
AliceState::XmrLockTransactionSent { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
pub mod harness;
|
||||||
|
|
||||||
|
use harness::alice_run_until::is_xmr_lock_transaction_sent;
|
||||||
|
use harness::bob_run_until::is_btc_locked;
|
||||||
|
use harness::SlowCancelConfig;
|
||||||
|
use swap::protocol::alice::AliceState;
|
||||||
|
use swap::protocol::bob::BobState;
|
||||||
|
use swap::protocol::{alice, bob};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn given_alice_and_bob_manually_force_cancel_when_timelock_not_expired_errors() {
|
||||||
|
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||||
|
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||||
|
let 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));
|
||||||
|
|
||||||
|
let bob_state = bob_swap.await??;
|
||||||
|
assert!(matches!(bob_state, BobState::BtcLocked { .. }));
|
||||||
|
|
||||||
|
let (bob_swap, bob_join_handle) = ctx
|
||||||
|
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||||
|
.await;
|
||||||
|
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||||
|
|
||||||
|
let alice_state = alice_swap.await??;
|
||||||
|
assert!(matches!(
|
||||||
|
alice_state,
|
||||||
|
AliceState::XmrLockTransactionSent { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
// Bob tries but fails to manually cancel
|
||||||
|
let result = bob::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, true).await;
|
||||||
|
assert!(matches!(result, Err(_)));
|
||||||
|
|
||||||
|
ctx.restart_alice().await;
|
||||||
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
|
assert!(matches!(
|
||||||
|
alice_swap.state,
|
||||||
|
AliceState::XmrLockTransactionSent { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
// Alice tries but fails manual cancel
|
||||||
|
let is_outer_err = alice::cancel(
|
||||||
|
alice_swap.swap_id,
|
||||||
|
alice_swap.bitcoin_wallet,
|
||||||
|
alice_swap.db,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err();
|
||||||
|
assert!(is_outer_err);
|
||||||
|
|
||||||
|
let (bob_swap, bob_join_handle) = ctx
|
||||||
|
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||||
|
.await;
|
||||||
|
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||||
|
|
||||||
|
// Bob tries but fails to manually refund
|
||||||
|
let is_outer_err = bob::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, true)
|
||||||
|
.await
|
||||||
|
.is_err();
|
||||||
|
assert!(is_outer_err);
|
||||||
|
|
||||||
|
let (bob_swap, _) = ctx
|
||||||
|
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||||
|
.await;
|
||||||
|
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||||
|
|
||||||
|
ctx.restart_alice().await;
|
||||||
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
|
assert!(matches!(
|
||||||
|
alice_swap.state,
|
||||||
|
AliceState::XmrLockTransactionSent { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
// Alice tries but fails manual cancel
|
||||||
|
let refund_tx_not_published_yet = alice::refund(
|
||||||
|
alice_swap.swap_id,
|
||||||
|
alice_swap.bitcoin_wallet,
|
||||||
|
alice_swap.monero_wallet,
|
||||||
|
alice_swap.db,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
refund_tx_not_published_yet,
|
||||||
|
alice::refund::Error::RefundTransactionNotPublishedYet(..)
|
||||||
|
));
|
||||||
|
|
||||||
|
ctx.restart_alice().await;
|
||||||
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
|
assert!(matches!(
|
||||||
|
alice_swap.state,
|
||||||
|
AliceState::XmrLockTransactionSent { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
@ -1,54 +0,0 @@
|
|||||||
pub mod harness;
|
|
||||||
|
|
||||||
use bob::cancel::Error;
|
|
||||||
use harness::bob_run_until::is_btc_locked;
|
|
||||||
use harness::SlowCancelConfig;
|
|
||||||
use swap::protocol::bob::BobState;
|
|
||||||
use swap::protocol::{alice, bob};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn given_bob_manually_cancels_when_timelock_not_expired_errors() {
|
|
||||||
harness::setup_test(SlowCancelConfig, |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 _ = tokio::spawn(alice::run(alice_swap));
|
|
||||||
|
|
||||||
let bob_state = bob_swap.await??;
|
|
||||||
assert!(matches!(bob_state, BobState::BtcLocked { .. }));
|
|
||||||
|
|
||||||
let (bob_swap, bob_join_handle) = ctx
|
|
||||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
|
||||||
.await;
|
|
||||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
|
||||||
|
|
||||||
// Bob tries but fails to manually cancel
|
|
||||||
let result = bob::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, false)
|
|
||||||
.await?
|
|
||||||
.err()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(result, Error::CancelTimelockNotExpiredYet));
|
|
||||||
|
|
||||||
let (bob_swap, bob_join_handle) = ctx
|
|
||||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
|
||||||
.await;
|
|
||||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
|
||||||
|
|
||||||
// Bob tries but fails to manually refund
|
|
||||||
bob::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, false)
|
|
||||||
.await?
|
|
||||||
.err()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (bob_swap, _) = ctx
|
|
||||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
|
||||||
.await;
|
|
||||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
pub mod harness;
|
|
||||||
|
|
||||||
use harness::bob_run_until::is_btc_locked;
|
|
||||||
use harness::SlowCancelConfig;
|
|
||||||
use swap::protocol::bob::BobState;
|
|
||||||
use swap::protocol::{alice, bob};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() {
|
|
||||||
harness::setup_test(SlowCancelConfig, |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 _ = tokio::spawn(alice::run(alice_swap));
|
|
||||||
|
|
||||||
let bob_state = bob_swap.await??;
|
|
||||||
assert!(matches!(bob_state, BobState::BtcLocked { .. }));
|
|
||||||
|
|
||||||
let (bob_swap, bob_join_handle) = ctx
|
|
||||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
|
||||||
.await;
|
|
||||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
|
||||||
|
|
||||||
// Bob forces a cancel that will fail
|
|
||||||
let is_error = bob::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, true)
|
|
||||||
.await
|
|
||||||
.is_err();
|
|
||||||
|
|
||||||
assert!(is_error);
|
|
||||||
|
|
||||||
let (bob_swap, bob_join_handle) = ctx
|
|
||||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
|
||||||
.await;
|
|
||||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
|
||||||
|
|
||||||
// Bob forces a refund that will fail
|
|
||||||
let is_error = bob::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, true)
|
|
||||||
.await
|
|
||||||
.is_err();
|
|
||||||
|
|
||||||
assert!(is_error);
|
|
||||||
let (bob_swap, _) = ctx
|
|
||||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
|
||||||
.await;
|
|
||||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user