mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-01 01:45:40 -04:00
Merge #325
325: Misc cleanup r=thomaseizinger a=thomaseizinger Miscellaneous cleanups of the `swap`, `state` and `steps` modules. Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
commit
113a29839f
@ -1,12 +1,12 @@
|
||||
use crate::bitcoin;
|
||||
use crate::bitcoin::wallet::Watchable;
|
||||
use crate::bitcoin::{
|
||||
build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock,
|
||||
TX_FEE,
|
||||
};
|
||||
use ::bitcoin::util::bip143::SigHashCache;
|
||||
use ::bitcoin::{OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid};
|
||||
use ::bitcoin::{OutPoint, Script, SigHash, SigHashType, TxIn, TxOut, Txid};
|
||||
use anyhow::Result;
|
||||
use bitcoin::Script;
|
||||
use ecdsa_fun::Signature;
|
||||
use miniscript::{Descriptor, DescriptorTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -149,7 +149,39 @@ impl TxCancel {
|
||||
OutPoint::new(self.inner.txid(), 0)
|
||||
}
|
||||
|
||||
pub fn add_signatures(
|
||||
pub fn complete_as_alice(
|
||||
self,
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
tx_cancel_sig_B: bitcoin::Signature,
|
||||
) -> Result<Transaction> {
|
||||
let sig_a = a.sign(self.digest());
|
||||
let sig_b = tx_cancel_sig_B;
|
||||
|
||||
let tx_cancel = self
|
||||
.add_signatures((a.public(), sig_a), (B, sig_b))
|
||||
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||
|
||||
Ok(tx_cancel)
|
||||
}
|
||||
|
||||
pub fn complete_as_bob(
|
||||
self,
|
||||
A: bitcoin::PublicKey,
|
||||
b: bitcoin::SecretKey,
|
||||
tx_cancel_sig_A: bitcoin::Signature,
|
||||
) -> Result<Transaction> {
|
||||
let sig_a = tx_cancel_sig_A;
|
||||
let sig_b = b.sign(self.digest());
|
||||
|
||||
let tx_cancel = self
|
||||
.add_signatures((A, sig_a), (b.public(), sig_b))
|
||||
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||
|
||||
Ok(tx_cancel)
|
||||
}
|
||||
|
||||
fn add_signatures(
|
||||
self,
|
||||
(A, sig_a): (PublicKey, Signature),
|
||||
(B, sig_b): (PublicKey, Signature),
|
||||
|
@ -3,10 +3,10 @@ use crate::bitcoin::{
|
||||
verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs,
|
||||
Transaction, TxCancel,
|
||||
};
|
||||
use crate::{bitcoin, monero};
|
||||
use ::bitcoin::util::bip143::SigHashCache;
|
||||
use ::bitcoin::{SigHash, SigHashType, Txid};
|
||||
use ::bitcoin::{Script, SigHash, SigHashType, Txid};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use bitcoin::Script;
|
||||
use ecdsa_fun::Signature;
|
||||
use miniscript::{Descriptor, DescriptorTrait};
|
||||
use std::collections::HashMap;
|
||||
@ -77,7 +77,31 @@ impl TxRefund {
|
||||
Ok(tx_refund)
|
||||
}
|
||||
|
||||
pub fn extract_signature_by_key(
|
||||
pub fn extract_monero_private_key(
|
||||
&self,
|
||||
published_refund_tx: bitcoin::Transaction,
|
||||
s_a: monero::Scalar,
|
||||
a: bitcoin::SecretKey,
|
||||
S_b_bitcoin: bitcoin::PublicKey,
|
||||
) -> Result<monero::PrivateKey> {
|
||||
let s_a = monero::PrivateKey { scalar: s_a };
|
||||
|
||||
let tx_refund_sig = self
|
||||
.extract_signature_by_key(published_refund_tx, a.public())
|
||||
.context("Failed to extract signature from Bitcoin refund tx")?;
|
||||
let tx_refund_encsig = a.encsign(S_b_bitcoin, self.digest());
|
||||
|
||||
let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig)
|
||||
.context("Failed to recover Monero secret key from Bitcoin signature")?;
|
||||
|
||||
let s_b = monero::private_key_from_secp256k1_scalar(s_b.into());
|
||||
|
||||
let spend_key = s_a + s_b;
|
||||
|
||||
Ok(spend_key)
|
||||
}
|
||||
|
||||
fn extract_signature_by_key(
|
||||
&self,
|
||||
candidate_transaction: Transaction,
|
||||
B: PublicKey,
|
||||
|
@ -29,8 +29,8 @@ pub enum Bob {
|
||||
state4: bob::State4,
|
||||
},
|
||||
BtcRedeemed(bob::State5),
|
||||
CancelTimelockExpired(bob::State4),
|
||||
BtcCancelled(bob::State4),
|
||||
CancelTimelockExpired(bob::State6),
|
||||
BtcCancelled(bob::State6),
|
||||
Done(BobEndState),
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ pub enum Bob {
|
||||
pub enum BobEndState {
|
||||
SafelyAborted,
|
||||
XmrRedeemed { tx_lock_id: bitcoin::Txid },
|
||||
BtcRefunded(Box<bob::State4>),
|
||||
BtcRefunded(Box<bob::State6>),
|
||||
BtcPunished { tx_lock_id: bitcoin::Txid },
|
||||
}
|
||||
|
||||
@ -60,9 +60,9 @@ impl From<BobState> for Bob {
|
||||
BobState::XmrLocked(state4) => Bob::XmrLocked { state4 },
|
||||
BobState::EncSigSent(state4) => Bob::EncSigSent { state4 },
|
||||
BobState::BtcRedeemed(state5) => Bob::BtcRedeemed(state5),
|
||||
BobState::CancelTimelockExpired(state4) => Bob::CancelTimelockExpired(state4),
|
||||
BobState::BtcCancelled(state4) => Bob::BtcCancelled(state4),
|
||||
BobState::BtcRefunded(state4) => Bob::Done(BobEndState::BtcRefunded(Box::new(state4))),
|
||||
BobState::CancelTimelockExpired(state6) => Bob::CancelTimelockExpired(state6),
|
||||
BobState::BtcCancelled(state6) => Bob::BtcCancelled(state6),
|
||||
BobState::BtcRefunded(state6) => Bob::Done(BobEndState::BtcRefunded(Box::new(state6))),
|
||||
BobState::XmrRedeemed { tx_lock_id } => {
|
||||
Bob::Done(BobEndState::XmrRedeemed { tx_lock_id })
|
||||
}
|
||||
@ -92,12 +92,12 @@ impl From<Bob> for BobState {
|
||||
Bob::XmrLocked { state4 } => BobState::XmrLocked(state4),
|
||||
Bob::EncSigSent { state4 } => BobState::EncSigSent(state4),
|
||||
Bob::BtcRedeemed(state5) => BobState::BtcRedeemed(state5),
|
||||
Bob::CancelTimelockExpired(state4) => BobState::CancelTimelockExpired(state4),
|
||||
Bob::BtcCancelled(state4) => BobState::BtcCancelled(state4),
|
||||
Bob::CancelTimelockExpired(state6) => BobState::CancelTimelockExpired(state6),
|
||||
Bob::BtcCancelled(state6) => BobState::BtcCancelled(state6),
|
||||
Bob::Done(end_state) => match end_state {
|
||||
BobEndState::SafelyAborted => BobState::SafelyAborted,
|
||||
BobEndState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id },
|
||||
BobEndState::BtcRefunded(state4) => BobState::BtcRefunded(*state4),
|
||||
BobEndState::BtcRefunded(state6) => BobState::BtcRefunded(*state6),
|
||||
BobEndState::BtcPunished { tx_lock_id } => BobState::BtcPunished { tx_lock_id },
|
||||
},
|
||||
}
|
||||
|
@ -182,7 +182,7 @@ impl fmt::Display for TxHash {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||
#[error("transaction does not pay enough: expected {expected}, got {actual}")]
|
||||
#[error("expected {expected}, got {actual}")]
|
||||
pub struct InsufficientFunds {
|
||||
pub expected: Amount,
|
||||
pub actual: Amount,
|
||||
|
@ -120,12 +120,13 @@ impl Wallet {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
amount: Amount,
|
||||
) -> Result<TransferProof> {
|
||||
pub async fn transfer(&self, request: TransferRequest) -> Result<TransferProof> {
|
||||
let TransferRequest {
|
||||
public_spend_key,
|
||||
public_view_key,
|
||||
amount,
|
||||
} = request;
|
||||
|
||||
let destination_address =
|
||||
Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||
|
||||
@ -149,14 +150,15 @@ impl Wallet {
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn watch_for_transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
transfer_proof: TransferProof,
|
||||
expected: Amount,
|
||||
conf_target: u32,
|
||||
) -> Result<(), InsufficientFunds> {
|
||||
pub async fn watch_for_transfer(&self, request: WatchRequest) -> Result<()> {
|
||||
let WatchRequest {
|
||||
conf_target,
|
||||
public_view_key,
|
||||
public_spend_key,
|
||||
transfer_proof,
|
||||
expected,
|
||||
} = request;
|
||||
|
||||
let txid = transfer_proof.tx_hash();
|
||||
|
||||
tracing::info!(%txid, "Waiting for {} confirmation{} of Monero transaction", conf_target, if conf_target > 1 { "s" } else { "" });
|
||||
@ -222,6 +224,22 @@ impl Wallet {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TransferRequest {
|
||||
pub public_spend_key: PublicKey,
|
||||
pub public_view_key: PublicViewKey,
|
||||
pub amount: Amount,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WatchRequest {
|
||||
pub public_spend_key: PublicKey,
|
||||
pub public_view_key: PublicViewKey,
|
||||
pub transfer_proof: TransferProof,
|
||||
pub conf_target: u32,
|
||||
pub expected: Amount,
|
||||
}
|
||||
|
||||
async fn wait_for_confirmations<Fut>(
|
||||
txid: String,
|
||||
fetch_tx: impl Fn(String) -> Fut,
|
||||
|
@ -19,7 +19,6 @@ mod encrypted_signature;
|
||||
pub mod event_loop;
|
||||
mod execution_setup;
|
||||
pub mod state;
|
||||
mod steps;
|
||||
pub mod swap;
|
||||
mod transfer_proof;
|
||||
|
||||
|
@ -42,12 +42,6 @@ pub struct EventLoop<RS> {
|
||||
swap_sender: mpsc::Sender<Swap>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventLoopHandle {
|
||||
recv_encrypted_signature: Option<oneshot::Receiver<EncryptedSignature>>,
|
||||
send_transfer_proof: Option<oneshot::Sender<TransferProof>>,
|
||||
}
|
||||
|
||||
impl<LR> EventLoop<LR>
|
||||
where
|
||||
LR: LatestRate,
|
||||
@ -310,22 +304,30 @@ impl LatestRate for kraken::RateUpdateStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventLoopHandle {
|
||||
recv_encrypted_signature: Option<oneshot::Receiver<EncryptedSignature>>,
|
||||
send_transfer_proof: Option<oneshot::Sender<TransferProof>>,
|
||||
}
|
||||
|
||||
impl EventLoopHandle {
|
||||
pub async fn recv_encrypted_signature(&mut self) -> Result<EncryptedSignature> {
|
||||
pub async fn recv_encrypted_signature(&mut self) -> Result<bitcoin::EncryptedSignature> {
|
||||
let signature = self
|
||||
.recv_encrypted_signature
|
||||
.take()
|
||||
.context("Encrypted signature was already received")?
|
||||
.await?;
|
||||
.await?
|
||||
.tx_redeem_encsig;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
pub async fn send_transfer_proof(&mut self, msg: TransferProof) -> Result<()> {
|
||||
|
||||
pub async fn send_transfer_proof(&mut self, msg: monero::TransferProof) -> Result<()> {
|
||||
if self
|
||||
.send_transfer_proof
|
||||
.take()
|
||||
.context("Transfer proof was already sent")?
|
||||
.send(msg)
|
||||
.send(TransferProof { tx_lock_proof: msg })
|
||||
.is_err()
|
||||
{
|
||||
bail!("Failed to send transfer proof, receiver no longer listening?")
|
||||
|
@ -2,6 +2,7 @@ use crate::bitcoin::{
|
||||
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund,
|
||||
};
|
||||
use crate::env::Config;
|
||||
use crate::monero::wallet::TransferRequest;
|
||||
use crate::protocol::alice::{Message1, Message3};
|
||||
use crate::protocol::bob::{Message0, Message2, Message4};
|
||||
use crate::protocol::CROSS_CURVE_PROOF_SYSTEM;
|
||||
@ -343,6 +344,19 @@ impl State3 {
|
||||
))
|
||||
}
|
||||
|
||||
pub fn lock_xmr_transfer_request(&self) -> TransferRequest {
|
||||
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_a });
|
||||
|
||||
let public_spend_key = S_a + self.S_b_monero;
|
||||
let public_view_key = self.v.public();
|
||||
|
||||
TransferRequest {
|
||||
public_spend_key,
|
||||
public_view_key,
|
||||
amount: self.xmr,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tx_cancel(&self) -> TxCancel {
|
||||
TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B)
|
||||
}
|
||||
|
@ -1,149 +0,0 @@
|
||||
use crate::bitcoin::{
|
||||
CancelTimelock, EncryptedSignature, PunishTimelock, TxCancel, TxLock, TxRefund,
|
||||
};
|
||||
use crate::protocol::alice;
|
||||
use crate::protocol::alice::event_loop::EventLoopHandle;
|
||||
use crate::protocol::alice::TransferProof;
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use futures::pin_mut;
|
||||
|
||||
pub async fn lock_xmr(
|
||||
state3: alice::State3,
|
||||
event_loop_handle: &mut EventLoopHandle,
|
||||
monero_wallet: &monero::Wallet,
|
||||
) -> Result<()> {
|
||||
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: state3.s_a });
|
||||
|
||||
let public_spend_key = S_a + state3.S_b_monero;
|
||||
let public_view_key = state3.v.public();
|
||||
|
||||
let transfer_proof = monero_wallet
|
||||
.transfer(public_spend_key, public_view_key, state3.xmr)
|
||||
.await?;
|
||||
|
||||
// TODO(Franck): Wait for Monero to be confirmed once
|
||||
// Waiting for XMR confirmations should not be done in here, but in a separate
|
||||
// state! We have to record that Alice has already sent the transaction.
|
||||
// Otherwise Alice might publish the lock tx twice!
|
||||
|
||||
event_loop_handle
|
||||
.send_transfer_proof(TransferProof {
|
||||
tx_lock_proof: transfer_proof,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn wait_for_bitcoin_encrypted_signature(
|
||||
event_loop_handle: &mut EventLoopHandle,
|
||||
) -> Result<EncryptedSignature> {
|
||||
let msg3 = event_loop_handle
|
||||
.recv_encrypted_signature()
|
||||
.await
|
||||
.context("Failed to receive Bitcoin encrypted signature from Bob")?;
|
||||
|
||||
tracing::debug!("Message 3 received, returning it");
|
||||
|
||||
Ok(msg3.tx_redeem_encsig)
|
||||
}
|
||||
|
||||
pub async fn publish_cancel_transaction(
|
||||
tx_lock: TxLock,
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
cancel_timelock: CancelTimelock,
|
||||
tx_cancel_sig_bob: bitcoin::Signature,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<()> {
|
||||
bitcoin_wallet
|
||||
.watch_until_status(&tx_lock, |status| status.is_confirmed_with(cancel_timelock))
|
||||
.await?;
|
||||
|
||||
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B);
|
||||
|
||||
// If Bob hasn't yet broadcasted the tx cancel, we do it
|
||||
if bitcoin_wallet
|
||||
.get_raw_transaction(tx_cancel.txid())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
// TODO(Franck): Maybe the cancel transaction is already mined, in this case,
|
||||
// the broadcast will error out.
|
||||
|
||||
let sig_a = a.sign(tx_cancel.digest());
|
||||
let sig_b = tx_cancel_sig_bob.clone();
|
||||
|
||||
let tx_cancel = tx_cancel
|
||||
.add_signatures((a.public(), sig_a), (B, sig_b))
|
||||
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||
|
||||
// TODO(Franck): Error handling is delicate, why can't we broadcast?
|
||||
let (..) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?;
|
||||
|
||||
// TODO(Franck): Wait until transaction is mined and returned mined
|
||||
// block height
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn wait_for_bitcoin_refund(
|
||||
tx_cancel: &TxCancel,
|
||||
tx_refund: &TxRefund,
|
||||
punish_timelock: PunishTimelock,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<Option<bitcoin::Transaction>> {
|
||||
let refund_tx_id = tx_refund.txid();
|
||||
let seen_refund_tx =
|
||||
bitcoin_wallet.watch_until_status(tx_refund, |status| status.has_been_seen());
|
||||
|
||||
let punish_timelock_expired = bitcoin_wallet.watch_until_status(tx_cancel, |status| {
|
||||
status.is_confirmed_with(punish_timelock)
|
||||
});
|
||||
|
||||
pin_mut!(punish_timelock_expired);
|
||||
pin_mut!(seen_refund_tx);
|
||||
|
||||
tokio::select! {
|
||||
seen_refund = seen_refund_tx => {
|
||||
match seen_refund {
|
||||
Ok(()) => {
|
||||
let published_refund_tx = bitcoin_wallet.get_raw_transaction(refund_tx_id).await?;
|
||||
|
||||
Ok(Some(published_refund_tx))
|
||||
}
|
||||
Err(e) => {
|
||||
bail!(e.context("Failed to monitor refund transaction"))
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = punish_timelock_expired => {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_monero_private_key(
|
||||
published_refund_tx: bitcoin::Transaction,
|
||||
tx_refund: &TxRefund,
|
||||
s_a: monero::Scalar,
|
||||
a: bitcoin::SecretKey,
|
||||
S_b_bitcoin: bitcoin::PublicKey,
|
||||
) -> Result<monero::PrivateKey> {
|
||||
let s_a = monero::PrivateKey { scalar: s_a };
|
||||
|
||||
let tx_refund_sig = tx_refund
|
||||
.extract_signature_by_key(published_refund_tx, a.public())
|
||||
.context("Failed to extract signature from Bitcoin refund tx")?;
|
||||
let tx_refund_encsig = a.encsign(S_b_bitcoin, tx_refund.digest());
|
||||
|
||||
let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig)
|
||||
.context("Failed to recover Monero secret key from Bitcoin signature")?;
|
||||
let s_b = monero::private_key_from_secp256k1_scalar(s_b.into());
|
||||
|
||||
let spend_key = s_a + s_b;
|
||||
|
||||
Ok(spend_key)
|
||||
}
|
@ -6,18 +6,13 @@ use crate::env::Config;
|
||||
use crate::monero_ext::ScalarExt;
|
||||
use crate::protocol::alice;
|
||||
use crate::protocol::alice::event_loop::EventLoopHandle;
|
||||
use crate::protocol::alice::steps::{
|
||||
extract_monero_private_key, lock_xmr, publish_cancel_transaction,
|
||||
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund,
|
||||
};
|
||||
use crate::protocol::alice::AliceState;
|
||||
use crate::{bitcoin, database, monero};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use futures::future::{select, Either};
|
||||
use futures::pin_mut;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::sync::Arc;
|
||||
use tokio::select;
|
||||
use tokio::time::timeout;
|
||||
use tracing::{error, info};
|
||||
use uuid::Uuid;
|
||||
@ -73,382 +68,275 @@ async fn run_until_internal(
|
||||
) -> Result<AliceState> {
|
||||
info!("Current state: {}", state);
|
||||
if is_target_state(&state) {
|
||||
Ok(state)
|
||||
} else {
|
||||
match state {
|
||||
AliceState::Started { state3 } => {
|
||||
timeout(
|
||||
env_config.bob_time_to_act,
|
||||
bitcoin_wallet
|
||||
.watch_until_status(&state3.tx_lock, |status| status.has_been_seen()),
|
||||
)
|
||||
.await
|
||||
.context("Failed to find lock Bitcoin tx")??;
|
||||
return Ok(state);
|
||||
}
|
||||
|
||||
bitcoin_wallet
|
||||
.watch_until_status(&state3.tx_lock, |status| {
|
||||
status.is_confirmed_with(env_config.bitcoin_finality_confirmations)
|
||||
})
|
||||
.await?;
|
||||
let new_state = match state {
|
||||
AliceState::Started { state3 } => {
|
||||
timeout(
|
||||
env_config.bob_time_to_act,
|
||||
bitcoin_wallet.watch_until_status(&state3.tx_lock, |status| status.has_been_seen()),
|
||||
)
|
||||
.await
|
||||
.context("Failed to find lock Bitcoin tx")??;
|
||||
|
||||
let state = AliceState::BtcLocked { state3 };
|
||||
bitcoin_wallet
|
||||
.watch_until_status(&state3.tx_lock, |status| {
|
||||
status.is_confirmed_with(env_config.bitcoin_finality_confirmations)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let db_state = (&state).into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
|
||||
.await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
env_config,
|
||||
swap_id,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
}
|
||||
AliceState::BtcLocked { state3 } => {
|
||||
// Record the current monero wallet block height so we don't have to scan from
|
||||
// block 0 for scenarios where we create a refund wallet.
|
||||
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
|
||||
AliceState::BtcLocked { state3 }
|
||||
}
|
||||
AliceState::BtcLocked { state3 } => {
|
||||
// Record the current monero wallet block height so we don't have to scan from
|
||||
// block 0 for scenarios where we create a refund wallet.
|
||||
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
|
||||
|
||||
lock_xmr(*state3.clone(), &mut event_loop_handle, &monero_wallet).await?;
|
||||
let transfer_proof = monero_wallet
|
||||
.transfer(state3.lock_xmr_transfer_request())
|
||||
.await?;
|
||||
|
||||
let state = AliceState::XmrLocked {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
};
|
||||
// TODO(Franck): Wait for Monero to be confirmed once
|
||||
// Waiting for XMR confirmations should not be done in here, but in a separate
|
||||
// state! We have to record that Alice has already sent the transaction.
|
||||
// Otherwise Alice might publish the lock tx twice!
|
||||
|
||||
event_loop_handle
|
||||
.send_transfer_proof(transfer_proof)
|
||||
.await?;
|
||||
|
||||
let db_state = (&state).into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
|
||||
.await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
env_config,
|
||||
swap_id,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
}
|
||||
AliceState::XmrLocked {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
|
||||
ExpiredTimelocks::None => {
|
||||
let wait_for_enc_sig =
|
||||
wait_for_bitcoin_encrypted_signature(&mut event_loop_handle);
|
||||
let state3_clone = state3.clone();
|
||||
let cancel_timelock_expires = state3_clone
|
||||
.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref());
|
||||
|
||||
pin_mut!(wait_for_enc_sig);
|
||||
pin_mut!(cancel_timelock_expires);
|
||||
|
||||
match select(cancel_timelock_expires, wait_for_enc_sig).await {
|
||||
Either::Left(_) => AliceState::CancelTimelockExpired {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
},
|
||||
Either::Right((enc_sig, _)) => AliceState::EncSigLearned {
|
||||
state3,
|
||||
encrypted_signature: Box::new(enc_sig?),
|
||||
monero_wallet_restore_blockheight,
|
||||
},
|
||||
}
|
||||
}
|
||||
AliceState::XmrLocked {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
|
||||
ExpiredTimelocks::None => {
|
||||
select! {
|
||||
_ = state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => {
|
||||
AliceState::CancelTimelockExpired {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
}
|
||||
}
|
||||
_ => AliceState::CancelTimelockExpired {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
},
|
||||
};
|
||||
enc_sig = event_loop_handle.recv_encrypted_signature() => {
|
||||
tracing::info!("Received encrypted signature");
|
||||
|
||||
let db_state = (&state).into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
|
||||
.await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet.clone(),
|
||||
monero_wallet,
|
||||
env_config,
|
||||
swap_id,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
AliceState::EncSigLearned {
|
||||
state3,
|
||||
encrypted_signature: Box::new(enc_sig?),
|
||||
monero_wallet_restore_blockheight,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AliceState::EncSigLearned {
|
||||
_ => AliceState::CancelTimelockExpired {
|
||||
state3,
|
||||
encrypted_signature,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
|
||||
ExpiredTimelocks::None => {
|
||||
match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete(
|
||||
*encrypted_signature,
|
||||
state3.a.clone(),
|
||||
state3.s_a.to_secpfun_scalar(),
|
||||
state3.B,
|
||||
) {
|
||||
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
|
||||
Ok((_, finality)) => match finality.await {
|
||||
Ok(_) => AliceState::BtcRedeemed,
|
||||
Err(e) => {
|
||||
bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e);
|
||||
state3
|
||||
.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref())
|
||||
.await?;
|
||||
|
||||
AliceState::CancelTimelockExpired {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
AliceState::EncSigLearned {
|
||||
state3,
|
||||
encrypted_signature,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
|
||||
ExpiredTimelocks::None => {
|
||||
match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete(
|
||||
*encrypted_signature,
|
||||
state3.a.clone(),
|
||||
state3.s_a.to_secpfun_scalar(),
|
||||
state3.B,
|
||||
) {
|
||||
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
|
||||
Ok((_, finality)) => match finality.await {
|
||||
Ok(_) => AliceState::BtcRedeemed,
|
||||
Err(e) => {
|
||||
error!("Constructing the redeem transaction failed with {}, attempting to wait for cancellation now.", e);
|
||||
state3
|
||||
.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref())
|
||||
.await?;
|
||||
bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e);
|
||||
state3
|
||||
.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref())
|
||||
.await?;
|
||||
|
||||
AliceState::CancelTimelockExpired {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
}
|
||||
AliceState::CancelTimelockExpired {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => AliceState::CancelTimelockExpired {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
},
|
||||
};
|
||||
Err(e) => {
|
||||
error!("Constructing the redeem transaction failed with {}, attempting to wait for cancellation now.", e);
|
||||
state3
|
||||
.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref())
|
||||
.await?;
|
||||
|
||||
let db_state = (&state).into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
|
||||
.await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
env_config,
|
||||
swap_id,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
AliceState::CancelTimelockExpired {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AliceState::CancelTimelockExpired {
|
||||
_ => AliceState::CancelTimelockExpired {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
publish_cancel_transaction(
|
||||
state3.tx_lock.clone(),
|
||||
state3.a.clone(),
|
||||
state3.B,
|
||||
state3.cancel_timelock,
|
||||
state3.tx_cancel_sig_bob.clone(),
|
||||
&bitcoin_wallet,
|
||||
)
|
||||
.await?;
|
||||
},
|
||||
},
|
||||
AliceState::CancelTimelockExpired {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let tx_cancel = state3.tx_cancel();
|
||||
|
||||
let state = AliceState::BtcCancelled {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
};
|
||||
let db_state = (&state).into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
|
||||
.await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
env_config,
|
||||
swap_id,
|
||||
db,
|
||||
)
|
||||
// If Bob hasn't yet broadcasted the tx cancel, we do it
|
||||
if bitcoin_wallet
|
||||
.get_raw_transaction(tx_cancel.txid())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
let transaction = tx_cancel
|
||||
.complete_as_alice(state3.a.clone(), state3.B, state3.tx_cancel_sig_bob.clone())
|
||||
.context("Failed to complete Bitcoin cancel transaction")?;
|
||||
|
||||
if let Err(e) = bitcoin_wallet.broadcast(transaction, "cancel").await {
|
||||
tracing::debug!(
|
||||
"Assuming transaction is already broadcasted because: {:#}",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(Franck): Wait until transaction is mined and
|
||||
// returned mined block height
|
||||
}
|
||||
|
||||
AliceState::BtcCancelled {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let published_refund_tx = wait_for_bitcoin_refund(
|
||||
&state3.tx_cancel(),
|
||||
&state3.tx_refund(),
|
||||
state3.punish_timelock,
|
||||
&bitcoin_wallet,
|
||||
)
|
||||
}
|
||||
}
|
||||
AliceState::BtcCancelled {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let tx_refund = state3.tx_refund();
|
||||
let tx_cancel = state3.tx_cancel();
|
||||
|
||||
let seen_refund_tx =
|
||||
bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen());
|
||||
|
||||
let punish_timelock_expired = bitcoin_wallet.watch_until_status(&tx_cancel, |status| {
|
||||
status.is_confirmed_with(state3.punish_timelock)
|
||||
});
|
||||
|
||||
select! {
|
||||
seen_refund = seen_refund_tx => {
|
||||
seen_refund.context("Failed to monitor refund transaction")?;
|
||||
let published_refund_tx = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
|
||||
|
||||
let spend_key = tx_refund.extract_monero_private_key(
|
||||
published_refund_tx,
|
||||
state3.s_a,
|
||||
state3.a.clone(),
|
||||
state3.S_b_bitcoin,
|
||||
)?;
|
||||
|
||||
AliceState::BtcRefunded {
|
||||
spend_key,
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
}
|
||||
}
|
||||
_ = punish_timelock_expired => {
|
||||
AliceState::BtcPunishable {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AliceState::BtcRefunded {
|
||||
spend_key,
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let view_key = state3.v;
|
||||
|
||||
monero_wallet
|
||||
.create_from(spend_key, view_key, monero_wallet_restore_blockheight)
|
||||
.await?;
|
||||
|
||||
// TODO(Franck): Review error handling
|
||||
match published_refund_tx {
|
||||
None => {
|
||||
let state = AliceState::BtcPunishable {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
};
|
||||
let db_state = (&state).into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
|
||||
.await?;
|
||||
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet.clone(),
|
||||
monero_wallet,
|
||||
env_config,
|
||||
swap_id,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Some(published_refund_tx) => {
|
||||
let spend_key = extract_monero_private_key(
|
||||
published_refund_tx,
|
||||
&state3.tx_refund(),
|
||||
state3.s_a,
|
||||
state3.a.clone(),
|
||||
state3.S_b_bitcoin,
|
||||
)?;
|
||||
|
||||
let state = AliceState::BtcRefunded {
|
||||
spend_key,
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
};
|
||||
let db_state = (&state).into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
|
||||
.await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet.clone(),
|
||||
monero_wallet,
|
||||
env_config,
|
||||
swap_id,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
AliceState::BtcRefunded {
|
||||
spend_key,
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let view_key = state3.v;
|
||||
|
||||
monero_wallet
|
||||
.create_from(spend_key, view_key, monero_wallet_restore_blockheight)
|
||||
.await?;
|
||||
|
||||
let state = AliceState::XmrRefunded;
|
||||
let db_state = (&state).into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
|
||||
.await?;
|
||||
Ok(state)
|
||||
}
|
||||
AliceState::BtcPunishable {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let signed_tx_punish = state3.tx_punish().complete(
|
||||
state3.tx_punish_sig_bob.clone(),
|
||||
state3.a.clone(),
|
||||
state3.B,
|
||||
)?;
|
||||
|
||||
let punish_tx_finalised = async {
|
||||
let (txid, finality) =
|
||||
bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?;
|
||||
|
||||
finality.await?;
|
||||
|
||||
Result::<_, anyhow::Error>::Ok(txid)
|
||||
};
|
||||
|
||||
let tx_refund = state3.tx_refund();
|
||||
let refund_tx_seen =
|
||||
bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen());
|
||||
|
||||
pin_mut!(punish_tx_finalised);
|
||||
pin_mut!(refund_tx_seen);
|
||||
|
||||
match select(refund_tx_seen, punish_tx_finalised).await {
|
||||
Either::Left((Ok(()), _)) => {
|
||||
let published_refund_tx =
|
||||
bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
|
||||
|
||||
let spend_key = extract_monero_private_key(
|
||||
published_refund_tx,
|
||||
&tx_refund,
|
||||
state3.s_a,
|
||||
state3.a.clone(),
|
||||
state3.S_b_bitcoin,
|
||||
)?;
|
||||
let state = AliceState::BtcRefunded {
|
||||
spend_key,
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
};
|
||||
let db_state = (&state).into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
|
||||
.await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet.clone(),
|
||||
monero_wallet,
|
||||
env_config,
|
||||
swap_id,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Either::Left((Err(e), _)) => {
|
||||
bail!(e.context("Failed to monitor refund transaction"))
|
||||
}
|
||||
Either::Right(_) => {
|
||||
let state = AliceState::BtcPunished;
|
||||
let db_state = (&state).into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
|
||||
.await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet.clone(),
|
||||
monero_wallet,
|
||||
env_config,
|
||||
swap_id,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
AliceState::XmrRefunded => Ok(AliceState::XmrRefunded),
|
||||
AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed),
|
||||
AliceState::BtcPunished => Ok(AliceState::BtcPunished),
|
||||
AliceState::SafelyAborted => Ok(AliceState::SafelyAborted),
|
||||
AliceState::XmrRefunded
|
||||
}
|
||||
}
|
||||
AliceState::BtcPunishable {
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let signed_tx_punish = state3.tx_punish().complete(
|
||||
state3.tx_punish_sig_bob.clone(),
|
||||
state3.a.clone(),
|
||||
state3.B,
|
||||
)?;
|
||||
|
||||
let punish_tx_finalised = async {
|
||||
let (txid, finality) = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?;
|
||||
|
||||
finality.await?;
|
||||
|
||||
Result::<_, anyhow::Error>::Ok(txid)
|
||||
};
|
||||
|
||||
let tx_refund = state3.tx_refund();
|
||||
let refund_tx_seen =
|
||||
bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen());
|
||||
|
||||
select! {
|
||||
result = refund_tx_seen => {
|
||||
result.context("Failed to monitor refund transaction")?;
|
||||
|
||||
let published_refund_tx =
|
||||
bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
|
||||
|
||||
let spend_key = tx_refund.extract_monero_private_key(
|
||||
published_refund_tx,
|
||||
state3.s_a,
|
||||
state3.a.clone(),
|
||||
state3.S_b_bitcoin,
|
||||
)?;
|
||||
AliceState::BtcRefunded {
|
||||
spend_key,
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
}
|
||||
}
|
||||
_ = punish_tx_finalised => {
|
||||
AliceState::BtcPunished
|
||||
}
|
||||
}
|
||||
}
|
||||
AliceState::XmrRefunded => AliceState::XmrRefunded,
|
||||
AliceState::BtcRedeemed => AliceState::BtcRedeemed,
|
||||
AliceState::BtcPunished => AliceState::BtcPunished,
|
||||
AliceState::SafelyAborted => AliceState::SafelyAborted,
|
||||
};
|
||||
|
||||
let db_state = (&new_state).into();
|
||||
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
|
||||
.await?;
|
||||
run_until_internal(
|
||||
new_state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
env_config,
|
||||
swap_id,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
@ -20,12 +20,12 @@ pub async fn cancel(
|
||||
db: Database,
|
||||
force: bool,
|
||||
) -> Result<Result<(Txid, BobState), Error>> {
|
||||
let state4 = match state {
|
||||
let state6 = match state {
|
||||
BobState::BtcLocked(state3) => state3.cancel(),
|
||||
BobState::XmrLockProofReceived { state, .. } => state.cancel(),
|
||||
BobState::XmrLocked(state4) => state4,
|
||||
BobState::EncSigSent(state4) => state4,
|
||||
BobState::CancelTimelockExpired(state4) => state4,
|
||||
BobState::XmrLocked(state4) => state4.cancel(),
|
||||
BobState::EncSigSent(state4) => state4.cancel(),
|
||||
BobState::CancelTimelockExpired(state6) => state6,
|
||||
_ => bail!(
|
||||
"Cannot cancel swap {} because it is in state {} which is not refundable.",
|
||||
swap_id,
|
||||
@ -34,16 +34,16 @@ pub async fn cancel(
|
||||
};
|
||||
|
||||
if !force {
|
||||
if let ExpiredTimelocks::None = state4.expired_timelock(bitcoin_wallet.as_ref()).await? {
|
||||
if let ExpiredTimelocks::None = state6.expired_timelock(bitcoin_wallet.as_ref()).await? {
|
||||
return Ok(Err(Error::CancelTimelockNotExpiredYet));
|
||||
}
|
||||
|
||||
if state4
|
||||
if state6
|
||||
.check_for_tx_cancel(bitcoin_wallet.as_ref())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
let state = BobState::BtcCancelled(state4);
|
||||
let state = BobState::BtcCancelled(state6);
|
||||
let db_state = state.into();
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
|
||||
@ -51,9 +51,9 @@ pub async fn cancel(
|
||||
}
|
||||
}
|
||||
|
||||
let txid = state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
|
||||
let txid = state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
|
||||
|
||||
let state = BobState::BtcCancelled(state4);
|
||||
let state = BobState::BtcCancelled(state6);
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
|
||||
|
@ -16,14 +16,14 @@ pub async fn refund(
|
||||
db: Database,
|
||||
force: bool,
|
||||
) -> Result<Result<BobState, SwapNotCancelledYet>> {
|
||||
let state4 = if force {
|
||||
let state6 = if force {
|
||||
match state {
|
||||
BobState::BtcLocked(state3) => state3.cancel(),
|
||||
BobState::XmrLockProofReceived { state, .. } => state.cancel(),
|
||||
BobState::XmrLocked(state4) => state4,
|
||||
BobState::EncSigSent(state4) => state4,
|
||||
BobState::CancelTimelockExpired(state4) => state4,
|
||||
BobState::BtcCancelled(state4) => state4,
|
||||
BobState::XmrLocked(state4) => state4.cancel(),
|
||||
BobState::EncSigSent(state4) => state4.cancel(),
|
||||
BobState::CancelTimelockExpired(state6) => state6,
|
||||
BobState::BtcCancelled(state6) => state6,
|
||||
_ => bail!(
|
||||
"Cannot refund swap {} because it is in state {} which is not refundable.",
|
||||
swap_id,
|
||||
@ -32,16 +32,16 @@ pub async fn refund(
|
||||
}
|
||||
} else {
|
||||
match state {
|
||||
BobState::BtcCancelled(state4) => state4,
|
||||
BobState::BtcCancelled(state6) => state6,
|
||||
_ => {
|
||||
return Ok(Err(SwapNotCancelledYet(swap_id)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
state4.refund_btc(bitcoin_wallet.as_ref()).await?;
|
||||
state6.refund_btc(bitcoin_wallet.as_ref()).await?;
|
||||
|
||||
let state = BobState::BtcRefunded(state4);
|
||||
let state = BobState::BtcRefunded(state6);
|
||||
let db_state = state.clone().into();
|
||||
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
|
@ -3,12 +3,13 @@ use crate::bitcoin::{
|
||||
TxLock, Txid,
|
||||
};
|
||||
use crate::monero;
|
||||
use crate::monero::{monero_private_key, InsufficientFunds, TransferProof};
|
||||
use crate::monero::wallet::WatchRequest;
|
||||
use crate::monero::{monero_private_key, TransferProof};
|
||||
use crate::monero_ext::ScalarExt;
|
||||
use crate::protocol::alice::{Message1, Message3};
|
||||
use crate::protocol::bob::{EncryptedSignature, Message0, Message2, Message4};
|
||||
use crate::protocol::CROSS_CURVE_PROOF_SYSTEM;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
||||
use ecdsa_fun::nonce::Deterministic;
|
||||
use ecdsa_fun::Signature;
|
||||
@ -34,9 +35,9 @@ pub enum BobState {
|
||||
XmrLocked(State4),
|
||||
EncSigSent(State4),
|
||||
BtcRedeemed(State5),
|
||||
CancelTimelockExpired(State4),
|
||||
BtcCancelled(State4),
|
||||
BtcRefunded(State4),
|
||||
CancelTimelockExpired(State6),
|
||||
BtcCancelled(State6),
|
||||
BtcRefunded(State6),
|
||||
XmrRedeemed {
|
||||
tx_lock_id: bitcoin::Txid,
|
||||
},
|
||||
@ -305,30 +306,22 @@ pub struct State3 {
|
||||
}
|
||||
|
||||
impl State3 {
|
||||
pub async fn watch_for_lock_xmr(
|
||||
self,
|
||||
xmr_wallet: &monero::Wallet,
|
||||
transfer_proof: TransferProof,
|
||||
monero_wallet_restore_blockheight: BlockHeight,
|
||||
) -> Result<Result<State4, InsufficientFunds>> {
|
||||
pub fn lock_xmr_watch_request(&self, transfer_proof: TransferProof) -> WatchRequest {
|
||||
let S_b_monero =
|
||||
monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(self.s_b));
|
||||
let S = self.S_a_monero + S_b_monero;
|
||||
|
||||
if let Err(e) = xmr_wallet
|
||||
.watch_for_transfer(
|
||||
S,
|
||||
self.v.public(),
|
||||
transfer_proof,
|
||||
self.xmr,
|
||||
self.min_monero_confirmations,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Ok(Err(e));
|
||||
WatchRequest {
|
||||
public_spend_key: S,
|
||||
public_view_key: self.v.public(),
|
||||
transfer_proof,
|
||||
conf_target: self.min_monero_confirmations,
|
||||
expected: self.xmr,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Ok(State4 {
|
||||
pub fn xmr_locked(self, monero_wallet_restore_blockheight: BlockHeight) -> State4 {
|
||||
State4 {
|
||||
A: self.A,
|
||||
b: self.b,
|
||||
s_b: self.s_b,
|
||||
@ -342,7 +335,7 @@ impl State3 {
|
||||
tx_cancel_sig_a: self.tx_cancel_sig_a,
|
||||
tx_refund_encsig: self.tx_refund_encsig,
|
||||
monero_wallet_restore_blockheight,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait_for_cancel_timelock_to_expire(
|
||||
@ -357,23 +350,17 @@ impl State3 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cancel(&self) -> State4 {
|
||||
State4 {
|
||||
pub fn cancel(&self) -> State6 {
|
||||
State6 {
|
||||
A: self.A,
|
||||
b: self.b.clone(),
|
||||
s_b: self.s_b,
|
||||
S_a_bitcoin: self.S_a_bitcoin,
|
||||
v: self.v,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address.clone(),
|
||||
redeem_address: self.redeem_address.clone(),
|
||||
tx_lock: self.tx_lock.clone(),
|
||||
tx_cancel_sig_a: self.tx_cancel_sig_a.clone(),
|
||||
tx_refund_encsig: self.tx_refund_encsig.clone(),
|
||||
// For cancel scenarios the monero wallet rescan blockchain height is irrelevant for
|
||||
// Bob, because Bob's cancel can only lead to refunding on Bitcoin
|
||||
monero_wallet_restore_blockheight: BlockHeight { height: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
@ -428,37 +415,6 @@ impl State4 {
|
||||
self.b.encsign(self.S_a_bitcoin, tx_redeem.digest())
|
||||
}
|
||||
|
||||
pub async fn check_for_tx_cancel(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<Transaction> {
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
|
||||
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Txid> {
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
|
||||
let sig_a = self.tx_cancel_sig_a.clone();
|
||||
let sig_b = self.b.sign(tx_cancel.digest());
|
||||
|
||||
let tx_cancel = tx_cancel
|
||||
.add_signatures((self.A, sig_a), (self.b.public(), sig_b))
|
||||
.expect(
|
||||
"sig_{a,b} to be valid signatures for
|
||||
tx_cancel",
|
||||
);
|
||||
|
||||
let (tx_id, _) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?;
|
||||
|
||||
Ok(tx_id)
|
||||
}
|
||||
|
||||
pub async fn watch_for_redeem_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<State5> {
|
||||
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
|
||||
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest());
|
||||
@ -513,29 +469,18 @@ impl State4 {
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> {
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
|
||||
|
||||
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
|
||||
|
||||
let sig_b = self.b.sign(tx_refund.digest());
|
||||
let sig_a =
|
||||
adaptor.decrypt_signature(&self.s_b.to_secpfun_scalar(), self.tx_refund_encsig.clone());
|
||||
|
||||
let signed_tx_refund =
|
||||
tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?;
|
||||
|
||||
let (_, finality) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?;
|
||||
|
||||
finality.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn tx_lock_id(&self) -> bitcoin::Txid {
|
||||
self.tx_lock.txid()
|
||||
pub fn cancel(self) -> State6 {
|
||||
State6 {
|
||||
A: self.A,
|
||||
b: self.b,
|
||||
s_b: self.s_b,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
tx_lock: self.tx_lock,
|
||||
tx_cancel_sig_a: self.tx_cancel_sig_a,
|
||||
tx_refund_encsig: self.tx_refund_encsig,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -567,3 +512,83 @@ impl State5 {
|
||||
self.tx_lock.txid()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct State6 {
|
||||
A: bitcoin::PublicKey,
|
||||
b: bitcoin::SecretKey,
|
||||
s_b: monero::Scalar,
|
||||
cancel_timelock: CancelTimelock,
|
||||
punish_timelock: PunishTimelock,
|
||||
refund_address: bitcoin::Address,
|
||||
tx_lock: bitcoin::TxLock,
|
||||
tx_cancel_sig_a: Signature,
|
||||
tx_refund_encsig: bitcoin::EncryptedSignature,
|
||||
}
|
||||
|
||||
impl State6 {
|
||||
pub async fn expired_timelock(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<ExpiredTimelocks> {
|
||||
let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
|
||||
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
|
||||
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
|
||||
|
||||
Ok(current_epoch(
|
||||
self.cancel_timelock,
|
||||
self.punish_timelock,
|
||||
tx_lock_status,
|
||||
tx_cancel_status,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn check_for_tx_cancel(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<Transaction> {
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
|
||||
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Txid> {
|
||||
let transaction =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public())
|
||||
.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?;
|
||||
|
||||
Ok(tx_id)
|
||||
}
|
||||
|
||||
pub async fn refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> {
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
|
||||
|
||||
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
|
||||
|
||||
let sig_b = self.b.sign(tx_refund.digest());
|
||||
let sig_a =
|
||||
adaptor.decrypt_signature(&self.s_b.to_secpfun_scalar(), self.tx_refund_encsig.clone());
|
||||
|
||||
let signed_tx_refund =
|
||||
tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?;
|
||||
|
||||
let (_, finality) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?;
|
||||
|
||||
finality.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn tx_lock_id(&self) -> bitcoin::Txid {
|
||||
self.tx_lock.txid()
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
use crate::bitcoin::ExpiredTimelocks;
|
||||
use crate::database::{Database, Swap};
|
||||
use crate::env::Config;
|
||||
use crate::monero::InsufficientFunds;
|
||||
use crate::protocol::bob;
|
||||
use crate::protocol::bob::event_loop::EventLoopHandle;
|
||||
use crate::protocol::bob::state::*;
|
||||
@ -11,7 +10,7 @@ use async_recursion::async_recursion;
|
||||
use rand::rngs::OsRng;
|
||||
use std::sync::Arc;
|
||||
use tokio::select;
|
||||
use tracing::{trace, warn};
|
||||
use tracing::trace;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn is_complete(state: &BobState) -> bool {
|
||||
@ -63,348 +62,205 @@ async fn run_until_internal(
|
||||
) -> Result<BobState> {
|
||||
trace!("Current state: {}", state);
|
||||
if is_target_state(&state) {
|
||||
Ok(state)
|
||||
} else {
|
||||
match state {
|
||||
BobState::Started { btc_amount } => {
|
||||
let bitcoin_refund_address = bitcoin_wallet.new_address().await?;
|
||||
|
||||
event_loop_handle.dial().await?;
|
||||
|
||||
let state2 = request_price_and_setup(
|
||||
btc_amount,
|
||||
&mut event_loop_handle,
|
||||
env_config,
|
||||
bitcoin_refund_address,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let state = BobState::ExecutionSetupDone(state2);
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
swap_id,
|
||||
env_config,
|
||||
receive_monero_address,
|
||||
)
|
||||
.await
|
||||
}
|
||||
BobState::ExecutionSetupDone(state2) => {
|
||||
// Do not lock Bitcoin if not connected to Alice.
|
||||
event_loop_handle.dial().await?;
|
||||
// Alice and Bob have exchanged info
|
||||
let (state3, tx_lock) = state2.lock_btc().await?;
|
||||
let signed_tx = bitcoin_wallet
|
||||
.sign_and_finalize(tx_lock.clone().into())
|
||||
.await
|
||||
.context("Failed to sign Bitcoin lock transaction")?;
|
||||
let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
|
||||
|
||||
let state = BobState::BtcLocked(state3);
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
swap_id,
|
||||
env_config,
|
||||
receive_monero_address,
|
||||
)
|
||||
.await
|
||||
}
|
||||
// Bob has locked Btc
|
||||
// Watch for Alice to Lock Xmr or for cancel timelock to elapse
|
||||
BobState::BtcLocked(state3) => {
|
||||
let state = if let ExpiredTimelocks::None =
|
||||
state3.current_epoch(bitcoin_wallet.as_ref()).await?
|
||||
{
|
||||
event_loop_handle.dial().await?;
|
||||
|
||||
let transfer_proof_watcher = event_loop_handle.recv_transfer_proof();
|
||||
let cancel_timelock_expires =
|
||||
state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref());
|
||||
|
||||
// Record the current monero wallet block height so we don't have to scan from
|
||||
// block 0 once we create the redeem wallet.
|
||||
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
|
||||
|
||||
tracing::info!("Waiting for Alice to lock Monero");
|
||||
|
||||
select! {
|
||||
transfer_proof = transfer_proof_watcher => {
|
||||
let transfer_proof = transfer_proof?.tx_lock_proof;
|
||||
|
||||
tracing::info!(txid = %transfer_proof.tx_hash(), "Alice locked Monero");
|
||||
|
||||
BobState::XmrLockProofReceived {
|
||||
state: state3,
|
||||
lock_transfer_proof: transfer_proof,
|
||||
monero_wallet_restore_blockheight
|
||||
}
|
||||
},
|
||||
_ = cancel_timelock_expires => {
|
||||
tracing::info!("Alice took too long to lock Monero, cancelling the swap");
|
||||
|
||||
let state4 = state3.cancel();
|
||||
BobState::CancelTimelockExpired(state4)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let state4 = state3.cancel();
|
||||
BobState::CancelTimelockExpired(state4)
|
||||
};
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
swap_id,
|
||||
env_config,
|
||||
receive_monero_address,
|
||||
)
|
||||
.await
|
||||
}
|
||||
BobState::XmrLockProofReceived {
|
||||
state,
|
||||
lock_transfer_proof,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let state = if let ExpiredTimelocks::None =
|
||||
state.current_epoch(bitcoin_wallet.as_ref()).await?
|
||||
{
|
||||
event_loop_handle.dial().await?;
|
||||
|
||||
let xmr_lock_watcher = state.clone().watch_for_lock_xmr(
|
||||
monero_wallet.as_ref(),
|
||||
lock_transfer_proof,
|
||||
monero_wallet_restore_blockheight,
|
||||
);
|
||||
let cancel_timelock_expires =
|
||||
state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref());
|
||||
|
||||
select! {
|
||||
state4 = xmr_lock_watcher => {
|
||||
match state4? {
|
||||
Ok(state4) => BobState::XmrLocked(state4),
|
||||
Err(InsufficientFunds {..}) => {
|
||||
warn!("The other party has locked insufficient Monero funds! Waiting for refund...");
|
||||
state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()).await?;
|
||||
let state4 = state.cancel();
|
||||
BobState::CancelTimelockExpired(state4)
|
||||
},
|
||||
}
|
||||
},
|
||||
_ = cancel_timelock_expires => {
|
||||
let state4 = state.cancel();
|
||||
BobState::CancelTimelockExpired(state4)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let state4 = state.cancel();
|
||||
BobState::CancelTimelockExpired(state4)
|
||||
};
|
||||
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
swap_id,
|
||||
env_config,
|
||||
receive_monero_address,
|
||||
)
|
||||
.await
|
||||
}
|
||||
BobState::XmrLocked(state) => {
|
||||
let state = if let ExpiredTimelocks::None =
|
||||
state.expired_timelock(bitcoin_wallet.as_ref()).await?
|
||||
{
|
||||
event_loop_handle.dial().await?;
|
||||
// Alice has locked Xmr
|
||||
// Bob sends Alice his key
|
||||
let tx_redeem_encsig = state.tx_redeem_encsig();
|
||||
|
||||
let state4_clone = state.clone();
|
||||
|
||||
let enc_sig_sent_watcher =
|
||||
event_loop_handle.send_encrypted_signature(tx_redeem_encsig);
|
||||
let bitcoin_wallet = bitcoin_wallet.clone();
|
||||
let cancel_timelock_expires =
|
||||
state4_clone.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref());
|
||||
|
||||
select! {
|
||||
_ = enc_sig_sent_watcher => {
|
||||
BobState::EncSigSent(state)
|
||||
},
|
||||
_ = cancel_timelock_expires => {
|
||||
BobState::CancelTimelockExpired(state)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BobState::CancelTimelockExpired(state)
|
||||
};
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
swap_id,
|
||||
env_config,
|
||||
receive_monero_address,
|
||||
)
|
||||
.await
|
||||
}
|
||||
BobState::EncSigSent(state) => {
|
||||
let state = if let ExpiredTimelocks::None =
|
||||
state.expired_timelock(bitcoin_wallet.as_ref()).await?
|
||||
{
|
||||
let state_clone = state.clone();
|
||||
let redeem_watcher = state_clone.watch_for_redeem_btc(bitcoin_wallet.as_ref());
|
||||
let cancel_timelock_expires =
|
||||
state_clone.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref());
|
||||
|
||||
select! {
|
||||
state5 = redeem_watcher => {
|
||||
BobState::BtcRedeemed(state5?)
|
||||
},
|
||||
_ = cancel_timelock_expires => {
|
||||
BobState::CancelTimelockExpired(state)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BobState::CancelTimelockExpired(state)
|
||||
};
|
||||
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
db,
|
||||
bitcoin_wallet.clone(),
|
||||
monero_wallet,
|
||||
swap_id,
|
||||
env_config,
|
||||
receive_monero_address,
|
||||
)
|
||||
.await
|
||||
}
|
||||
BobState::BtcRedeemed(state) => {
|
||||
// Bob redeems XMR using revealed s_a
|
||||
state.claim_xmr(monero_wallet.as_ref()).await?;
|
||||
|
||||
// Ensure that the generated wallet is synced so we have a proper balance
|
||||
monero_wallet.refresh().await?;
|
||||
// Sweep (transfer all funds) to the given address
|
||||
let tx_hashes = monero_wallet.sweep_all(receive_monero_address).await?;
|
||||
|
||||
for tx_hash in tx_hashes {
|
||||
tracing::info!("Sent XMR to {} in tx {}", receive_monero_address, tx_hash.0);
|
||||
}
|
||||
|
||||
let state = BobState::XmrRedeemed {
|
||||
tx_lock_id: state.tx_lock_id(),
|
||||
};
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
swap_id,
|
||||
env_config,
|
||||
receive_monero_address,
|
||||
)
|
||||
.await
|
||||
}
|
||||
BobState::CancelTimelockExpired(state4) => {
|
||||
if state4
|
||||
.check_for_tx_cancel(bitcoin_wallet.as_ref())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
|
||||
}
|
||||
|
||||
let state = BobState::BtcCancelled(state4);
|
||||
db.insert_latest_state(swap_id, Swap::Bob(state.clone().into()))
|
||||
.await?;
|
||||
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
swap_id,
|
||||
env_config,
|
||||
receive_monero_address,
|
||||
)
|
||||
.await
|
||||
}
|
||||
BobState::BtcCancelled(state) => {
|
||||
// Bob has cancelled the swap
|
||||
let state = match state.expired_timelock(bitcoin_wallet.as_ref()).await? {
|
||||
ExpiredTimelocks::None => {
|
||||
bail!("Internal error: canceled state reached before cancel timelock was expired");
|
||||
}
|
||||
ExpiredTimelocks::Cancel => {
|
||||
state.refund_btc(bitcoin_wallet.as_ref()).await?;
|
||||
BobState::BtcRefunded(state)
|
||||
}
|
||||
ExpiredTimelocks::Punish => BobState::BtcPunished {
|
||||
tx_lock_id: state.tx_lock_id(),
|
||||
},
|
||||
};
|
||||
|
||||
let db_state = state.clone().into();
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until_internal(
|
||||
state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
swap_id,
|
||||
env_config,
|
||||
receive_monero_address,
|
||||
)
|
||||
.await
|
||||
}
|
||||
BobState::BtcRefunded(state4) => Ok(BobState::BtcRefunded(state4)),
|
||||
BobState::BtcPunished { tx_lock_id } => Ok(BobState::BtcPunished { tx_lock_id }),
|
||||
BobState::SafelyAborted => Ok(BobState::SafelyAborted),
|
||||
BobState::XmrRedeemed { tx_lock_id } => Ok(BobState::XmrRedeemed { tx_lock_id }),
|
||||
}
|
||||
return Ok(state);
|
||||
}
|
||||
|
||||
let new_state = match state {
|
||||
BobState::Started { btc_amount } => {
|
||||
let bitcoin_refund_address = bitcoin_wallet.new_address().await?;
|
||||
|
||||
event_loop_handle.dial().await?;
|
||||
|
||||
let state2 = request_price_and_setup(
|
||||
btc_amount,
|
||||
&mut event_loop_handle,
|
||||
env_config,
|
||||
bitcoin_refund_address,
|
||||
)
|
||||
.await?;
|
||||
|
||||
BobState::ExecutionSetupDone(state2)
|
||||
}
|
||||
BobState::ExecutionSetupDone(state2) => {
|
||||
// Do not lock Bitcoin if not connected to Alice.
|
||||
event_loop_handle.dial().await?;
|
||||
// Alice and Bob have exchanged info
|
||||
let (state3, tx_lock) = state2.lock_btc().await?;
|
||||
let signed_tx = bitcoin_wallet
|
||||
.sign_and_finalize(tx_lock.clone().into())
|
||||
.await
|
||||
.context("Failed to sign Bitcoin lock transaction")?;
|
||||
let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
|
||||
|
||||
BobState::BtcLocked(state3)
|
||||
}
|
||||
// Bob has locked Btc
|
||||
// Watch for Alice to Lock Xmr or for cancel timelock to elapse
|
||||
BobState::BtcLocked(state3) => {
|
||||
if let ExpiredTimelocks::None = state3.current_epoch(bitcoin_wallet.as_ref()).await? {
|
||||
event_loop_handle.dial().await?;
|
||||
|
||||
let transfer_proof_watcher = event_loop_handle.recv_transfer_proof();
|
||||
let cancel_timelock_expires =
|
||||
state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref());
|
||||
|
||||
// Record the current monero wallet block height so we don't have to scan from
|
||||
// block 0 once we create the redeem wallet.
|
||||
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
|
||||
|
||||
tracing::info!("Waiting for Alice to lock Monero");
|
||||
|
||||
select! {
|
||||
transfer_proof = transfer_proof_watcher => {
|
||||
let transfer_proof = transfer_proof?.tx_lock_proof;
|
||||
|
||||
tracing::info!(txid = %transfer_proof.tx_hash(), "Alice locked Monero");
|
||||
|
||||
BobState::XmrLockProofReceived {
|
||||
state: state3,
|
||||
lock_transfer_proof: transfer_proof,
|
||||
monero_wallet_restore_blockheight
|
||||
}
|
||||
},
|
||||
_ = cancel_timelock_expires => {
|
||||
tracing::info!("Alice took too long to lock Monero, cancelling the swap");
|
||||
|
||||
let state4 = state3.cancel();
|
||||
BobState::CancelTimelockExpired(state4)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let state4 = state3.cancel();
|
||||
BobState::CancelTimelockExpired(state4)
|
||||
}
|
||||
}
|
||||
BobState::XmrLockProofReceived {
|
||||
state,
|
||||
lock_transfer_proof,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
if let ExpiredTimelocks::None = state.current_epoch(bitcoin_wallet.as_ref()).await? {
|
||||
event_loop_handle.dial().await?;
|
||||
|
||||
let watch_request = state.lock_xmr_watch_request(lock_transfer_proof);
|
||||
|
||||
select! {
|
||||
received_xmr = monero_wallet.watch_for_transfer(watch_request) => {
|
||||
match received_xmr {
|
||||
Ok(()) => BobState::XmrLocked(state.xmr_locked(monero_wallet_restore_blockheight)),
|
||||
Err(e) => {
|
||||
tracing::warn!("Waiting for refund because insufficient Monero have been locked! {}", e);
|
||||
state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()).await?;
|
||||
|
||||
BobState::CancelTimelockExpired(state.cancel())
|
||||
},
|
||||
}
|
||||
}
|
||||
_ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => {
|
||||
BobState::CancelTimelockExpired(state.cancel())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BobState::CancelTimelockExpired(state.cancel())
|
||||
}
|
||||
}
|
||||
BobState::XmrLocked(state) => {
|
||||
if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet.as_ref()).await? {
|
||||
event_loop_handle.dial().await?;
|
||||
// Alice has locked Xmr
|
||||
// Bob sends Alice his key
|
||||
|
||||
select! {
|
||||
_ = event_loop_handle.send_encrypted_signature(state.tx_redeem_encsig()) => {
|
||||
BobState::EncSigSent(state)
|
||||
},
|
||||
_ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => {
|
||||
BobState::CancelTimelockExpired(state.cancel())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BobState::CancelTimelockExpired(state.cancel())
|
||||
}
|
||||
}
|
||||
BobState::EncSigSent(state) => {
|
||||
if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet.as_ref()).await? {
|
||||
select! {
|
||||
state5 = state.watch_for_redeem_btc(bitcoin_wallet.as_ref()) => {
|
||||
BobState::BtcRedeemed(state5?)
|
||||
},
|
||||
_ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => {
|
||||
BobState::CancelTimelockExpired(state.cancel())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BobState::CancelTimelockExpired(state.cancel())
|
||||
}
|
||||
}
|
||||
BobState::BtcRedeemed(state) => {
|
||||
// Bob redeems XMR using revealed s_a
|
||||
state.claim_xmr(monero_wallet.as_ref()).await?;
|
||||
|
||||
// Ensure that the generated wallet is synced so we have a proper balance
|
||||
monero_wallet.refresh().await?;
|
||||
// Sweep (transfer all funds) to the given address
|
||||
let tx_hashes = monero_wallet.sweep_all(receive_monero_address).await?;
|
||||
|
||||
for tx_hash in tx_hashes {
|
||||
tracing::info!("Sent XMR to {} in tx {}", receive_monero_address, tx_hash.0);
|
||||
}
|
||||
|
||||
BobState::XmrRedeemed {
|
||||
tx_lock_id: state.tx_lock_id(),
|
||||
}
|
||||
}
|
||||
BobState::CancelTimelockExpired(state4) => {
|
||||
if state4
|
||||
.check_for_tx_cancel(bitcoin_wallet.as_ref())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
|
||||
}
|
||||
|
||||
BobState::BtcCancelled(state4)
|
||||
}
|
||||
BobState::BtcCancelled(state) => {
|
||||
// Bob has cancelled the swap
|
||||
match state.expired_timelock(bitcoin_wallet.as_ref()).await? {
|
||||
ExpiredTimelocks::None => {
|
||||
bail!(
|
||||
"Internal error: canceled state reached before cancel timelock was expired"
|
||||
);
|
||||
}
|
||||
ExpiredTimelocks::Cancel => {
|
||||
state.refund_btc(bitcoin_wallet.as_ref()).await?;
|
||||
BobState::BtcRefunded(state)
|
||||
}
|
||||
ExpiredTimelocks::Punish => BobState::BtcPunished {
|
||||
tx_lock_id: state.tx_lock_id(),
|
||||
},
|
||||
}
|
||||
}
|
||||
BobState::BtcRefunded(state4) => BobState::BtcRefunded(state4),
|
||||
BobState::BtcPunished { tx_lock_id } => BobState::BtcPunished { tx_lock_id },
|
||||
BobState::SafelyAborted => BobState::SafelyAborted,
|
||||
BobState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id },
|
||||
};
|
||||
|
||||
let db_state = new_state.clone().into();
|
||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||
run_until_internal(
|
||||
new_state,
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
swap_id,
|
||||
env_config,
|
||||
receive_monero_address,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn request_price_and_setup(
|
||||
|
Loading…
Reference in New Issue
Block a user