mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-08-10 15:30:14 -04: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
16 changed files with 601 additions and 143 deletions
|
@ -3,6 +3,7 @@ use bitcoin::util::amount::ParseAmountError;
|
|||
use bitcoin::{Address, Denomination};
|
||||
use rust_decimal::Decimal;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(structopt::StructOpt, Debug)]
|
||||
#[structopt(
|
||||
|
@ -58,6 +59,42 @@ pub enum Command {
|
|||
about = "Prints the Bitcoin and Monero balance. Requires the monero-wallet-rpc to be running."
|
||||
)]
|
||||
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> {
|
||||
|
|
|
@ -20,7 +20,7 @@ use prettytable::{row, Table};
|
|||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use structopt::StructOpt;
|
||||
use swap::asb::command::{Arguments, Command};
|
||||
use swap::asb::command::{Arguments, Command, ManualRecovery, RecoverCommandParams};
|
||||
use swap::asb::config::{
|
||||
default_config_path, initial_setup, query_user_for_initial_testnet_config, read_config, Config,
|
||||
ConfigNotInitialized,
|
||||
|
@ -29,6 +29,7 @@ use swap::database::Database;
|
|||
use swap::env::GetConfig;
|
||||
use swap::monero::Amount;
|
||||
use swap::network::swarm;
|
||||
use swap::protocol::alice;
|
||||
use swap::protocol::alice::event_loop::KrakenRate;
|
||||
use swap::protocol::alice::{run, EventLoop};
|
||||
use swap::seed::Seed;
|
||||
|
@ -205,6 +206,33 @@ async fn main() -> Result<()> {
|
|||
|
||||
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(())
|
||||
|
|
|
@ -7,13 +7,17 @@ use std::sync::Arc;
|
|||
use uuid::Uuid;
|
||||
|
||||
pub use self::behaviour::{Behaviour, OutEvent};
|
||||
pub use self::cancel::cancel;
|
||||
pub use self::event_loop::{EventLoop, EventLoopHandle};
|
||||
pub use self::refund::refund;
|
||||
pub use self::state::*;
|
||||
pub use self::swap::{run, run_until};
|
||||
|
||||
mod behaviour;
|
||||
pub mod cancel;
|
||||
pub mod event_loop;
|
||||
mod execution_setup;
|
||||
pub mod refund;
|
||||
mod spot_price;
|
||||
pub mod state;
|
||||
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::{
|
||||
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund,
|
||||
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
|
||||
TxPunish, TxRefund, Txid,
|
||||
};
|
||||
use crate::env::Config;
|
||||
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(
|
||||
&self,
|
||||
sig: bitcoin::EncryptedSignature,
|
||||
|
|
|
@ -229,23 +229,17 @@ async fn next_state(
|
|||
transfer_proof,
|
||||
state3,
|
||||
} => {
|
||||
let transaction = state3.signed_cancel_transaction()?;
|
||||
|
||||
// If Bob hasn't yet broadcasted the tx cancel, we do it
|
||||
if bitcoin_wallet
|
||||
.get_raw_transaction(transaction.txid())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
if let Err(e) = bitcoin_wallet.broadcast(transaction, "cancel").await {
|
||||
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
|
||||
// to be able to eventually punish. Since the punish timelock is
|
||||
// relative to the publication of the cancel transaction we have to ensure it
|
||||
// gets published once the cancel timelock expires.
|
||||
if let Err(e) = state3.submit_tx_cancel(bitcoin_wallet).await {
|
||||
tracing::debug!(
|
||||
"Assuming transaction is already broadcasted because: {:#}",
|
||||
"Assuming cancel transaction is already broadcasted because: {:#}",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(Franck): Wait until transaction is mined and
|
||||
// returned mined block height
|
||||
}
|
||||
|
||||
AliceState::BtcCancelled {
|
||||
|
@ -291,20 +285,13 @@ async fn next_state(
|
|||
spend_key,
|
||||
state3,
|
||||
} => {
|
||||
let view_key = state3.v;
|
||||
|
||||
// Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations
|
||||
// on the lock transaction
|
||||
monero_wallet
|
||||
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof, 10))
|
||||
.await?;
|
||||
|
||||
monero_wallet
|
||||
.create_from(
|
||||
state3
|
||||
.refund_xmr(
|
||||
monero_wallet,
|
||||
monero_wallet_restore_blockheight,
|
||||
swap_id.to_string(),
|
||||
spend_key,
|
||||
view_key,
|
||||
monero_wallet_restore_blockheight,
|
||||
transfer_proof,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ use uuid::Uuid;
|
|||
|
||||
#[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.")]
|
||||
pub struct SwapNotCancelledYet(Uuid);
|
||||
pub struct SwapNotCancelledYet(pub Uuid);
|
||||
|
||||
pub async fn refund(
|
||||
swap_id: Uuid,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue