diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 2b28c3d3..a05fe27b 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -20,6 +20,10 @@ use async_recursion::async_recursion; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; +use futures::{ + future::{select, Either}, + pin_mut, +}; use genawaiter::GeneratorState; use libp2p::{ core::{identity::Keypair, Multiaddr}, @@ -34,7 +38,10 @@ use tracing::{debug, info, warn}; use uuid::Uuid; use xmr_btc::{ alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0, State3}, - bitcoin::{BroadcastSignedTransaction, WatchForRawTransaction}, + bitcoin::{ + poll_until_block_height_is_gte, BroadcastSignedTransaction, GetRawTransaction, + TransactionBlockHeight, TxCancel, WatchForRawTransaction, + }, bob, monero::{CreateWalletForOutput, Transfer}, }; @@ -71,8 +78,18 @@ pub enum AliceState { encrypted_signature: EncryptedSignature, }, BtcRedeemed, + BtcCancelled { + state3: State3, + tx_cancel: TxCancel, + }, + BtcRefunded { + refund_tx: ::bitcoin::Transaction, + }, + BtcPunishable, XmrRefunded, - WaitingToCancel, + WaitingToCancel { + state3: State3, + }, Punished, SafelyAborted, } @@ -283,7 +300,7 @@ where // TODO(Franck): Insert in DB simple_swap( - AliceState::WaitingToCancel, + AliceState::WaitingToCancel { state3 }, rng, swarm, db, @@ -296,7 +313,7 @@ where // TODO(Franck): Insert in DB simple_swap( - AliceState::WaitingToCancel, + AliceState::WaitingToCancel { state3 }, rng, swarm, db, @@ -327,7 +344,7 @@ where state3, encrypted_signature, } => { - let (signed_tx_redeem, tx_redeem_txid) = { + let (signed_tx_redeem, _tx_redeem_txid) = { let adaptor = Adaptor::>::default(); let tx_redeem = bitcoin::TxRedeem::new(&state3.tx_lock, &state3.redeem_address); @@ -374,32 +391,92 @@ where ) .await } - AliceState::WaitingToCancel => { - // Wait until t2 or if TxRefund is seen - // If Bob has refunded the Alice should extract Bob's monero secret key and move - // the TxLockXmr output to her wallet. - if unimplemented!("refunded") { - simple_swap( - AliceState::XmrRefunded, - rng, - swarm, - db, - bitcoin_wallet, - monero_wallet, - ) - .await - } else { - simple_swap( - AliceState::Punished, - rng, - swarm, - db, - bitcoin_wallet, - monero_wallet, - ) - .await + AliceState::WaitingToCancel { state3 } => { + let tx_lock_height = bitcoin_wallet + .transaction_block_height(state3.tx_lock.txid()) + .await; + poll_until_block_height_is_gte( + bitcoin_wallet.as_ref(), + tx_lock_height + state3.refund_timelock, + ) + .await; + + let tx_cancel = bitcoin::TxCancel::new( + &state3.tx_lock, + state3.refund_timelock, + state3.a.public(), + state3.B.clone(), + ); + + if let None = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await { + let sig_a = state3.a.sign(tx_cancel.digest()); + let sig_b = state3.tx_cancel_sig_bob.clone(); + + let tx_cancel = tx_cancel + .clone() + .add_signatures( + &state3.tx_lock, + (state3.a.public(), sig_a), + (state3.B.clone(), sig_b), + ) + .expect("sig_{a,b} to be valid signatures for tx_cancel"); + + bitcoin_wallet.broadcast_signed_transaction(tx_cancel).await; + } + + simple_swap( + AliceState::BtcCancelled { state3, tx_cancel }, + rng, + swarm, + db, + bitcoin_wallet, + monero_wallet, + ) + .await + } + AliceState::BtcCancelled { state3, tx_cancel } => { + let tx_cancel_height = bitcoin_wallet + .transaction_block_height(tx_cancel.txid()) + .await; + + let reached_t2 = poll_until_block_height_is_gte( + bitcoin_wallet.as_ref(), + tx_cancel_height + state3.punish_timelock, + ); + + let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state3.refund_address); + let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + + pin_mut!(reached_t2); + pin_mut!(seen_refund_tx); + + match select(reached_t2, seen_refund_tx).await { + Either::Left(_) => { + simple_swap( + AliceState::BtcPunishable, + rng, + swarm, + db, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } + Either::Right((refund_tx, _)) => { + simple_swap( + AliceState::BtcRefunded { refund_tx }, + rng, + swarm, + db, + bitcoin_wallet.clone(), + monero_wallet, + ) + .await + } } } + AliceState::BtcRefunded { .. } => todo!(), + AliceState::BtcPunishable => todo!(), AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), AliceState::Punished => Ok(AliceState::Punished), @@ -412,11 +489,11 @@ where pub async fn abort(state: AliceState) -> Result { match state { AliceState::Started { .. } => { - // Nothing has been commited by either party, abort swap. + // Nothing has been committed by either party, abort swap. abort(AliceState::SafelyAborted).await } AliceState::Negotiated { .. } => { - // Nothing has been commited by either party, abort swap. + // Nothing has been committed by either party, abort swap. abort(AliceState::SafelyAborted).await } AliceState::BtcLocked { .. } => { @@ -432,13 +509,13 @@ pub async fn abort(state: AliceState) -> Result { abort(AliceState::BtcRedeemed).await } else if unimplemented!("T1Elapsed") { // publish TxCancel or see if it has been published - abort(AliceState::WaitingToCancel).await + abort(AliceState::WaitingToCancel { state3 }).await } else { Err(unimplemented!()) } } AliceState::EncSignLearned { .. } => todo!(), - AliceState::WaitingToCancel => { + AliceState::WaitingToCancel { state3 } => { // Alice has cancelled the swap // Alice waits watches for t2 or TxRefund if unimplemented!("TxRefundSeen") { @@ -456,6 +533,9 @@ pub async fn abort(state: AliceState) -> Result { AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), AliceState::Punished => Ok(AliceState::Punished), AliceState::SafelyAborted => Ok(AliceState::SafelyAborted), + AliceState::BtcCancelled { .. } => todo!(), + AliceState::BtcRefunded { .. } => todo!(), + AliceState::BtcPunishable => todo!(), } } diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 7d5eb916..aefa8dae 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -153,3 +153,10 @@ impl TransactionBlockHeight for Wallet { .expect("transient errors to be retried") } } + +#[async_trait] +impl GetRawTransaction for Wallet { + async fn get_raw_transaction(&self, txid: Txid) -> Option { + todo!() + } +} diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index 95b06ef2..9ee13b73 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -201,6 +201,16 @@ pub trait TransactionBlockHeight { async fn transaction_block_height(&self, txid: Txid) -> u32; } +#[async_trait] +pub trait WaitForBlockHeight { + async fn wait_for_block_height(&self, height: u32); +} + +#[async_trait] +pub trait GetRawTransaction { + async fn get_raw_transaction(&self, txid: Txid) -> Option; +} + pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result { let adaptor = Adaptor::>::default();