use crate ::bitcoin ::{ ExpiredTimelocks , TxCancel , TxRefund } ;
use crate ::cli ::EventLoopHandle ;
use crate ::network ::swap_setup ::bob ::NewSwap ;
use crate ::protocol ::bob ;
use crate ::protocol ::bob ::state ::* ;
use crate ::{ bitcoin , monero } ;
use anyhow ::{ bail , Context , Result } ;
use tokio ::select ;
use uuid ::Uuid ;
pub fn is_complete ( state : & BobState ) -> bool {
matches! (
state ,
BobState ::BtcRefunded ( .. )
| BobState ::XmrRedeemed { .. }
| BobState ::BtcPunished { .. }
| BobState ::SafelyAborted
)
}
#[ allow(clippy::too_many_arguments) ]
pub async fn run ( swap : bob ::Swap ) -> Result < BobState > {
run_until ( swap , is_complete ) . await
}
pub async fn run_until (
mut swap : bob ::Swap ,
is_target_state : fn ( & BobState ) -> bool ,
) -> Result < BobState > {
let mut current_state = swap . state ;
while ! is_target_state ( & current_state ) {
current_state = next_state (
swap . id ,
current_state . clone ( ) ,
& mut swap . event_loop_handle ,
swap . bitcoin_wallet . as_ref ( ) ,
swap . monero_wallet . as_ref ( ) ,
swap . monero_receive_address ,
)
. await ? ;
swap . db
. insert_latest_state ( swap . id , current_state . clone ( ) . into ( ) )
. await ? ;
}
Ok ( current_state )
}
async fn next_state (
swap_id : Uuid ,
state : BobState ,
event_loop_handle : & mut EventLoopHandle ,
bitcoin_wallet : & bitcoin ::Wallet ,
monero_wallet : & monero ::Wallet ,
monero_receive_address : monero ::Address ,
) -> Result < BobState > {
tracing ::debug ! ( % state , "Advancing state" ) ;
Ok ( match state {
BobState ::Started {
btc_amount ,
change_address ,
} = > {
let tx_refund_fee = bitcoin_wallet
. estimate_fee ( TxRefund ::weight ( ) , btc_amount )
. await ? ;
let tx_cancel_fee = bitcoin_wallet
. estimate_fee ( TxCancel ::weight ( ) , btc_amount )
. await ? ;
let state2 = event_loop_handle
. setup_swap ( NewSwap {
swap_id ,
btc : btc_amount ,
tx_refund_fee ,
tx_cancel_fee ,
bitcoin_refund_address : change_address ,
} )
. await ? ;
tracing ::info ! ( % swap_id , "Starting new swap" ) ;
BobState ::SwapSetupCompleted ( state2 )
}
BobState ::SwapSetupCompleted ( state2 ) = > {
// Record the current monero wallet block height so we don't have to scan from
// block 0 once we create the redeem wallet.
// This has to be done **before** the Bitcoin is locked in order to ensure that
// if Bob goes offline the recorded wallet-height is correct.
// If we only record this later, it can happen that Bob publishes the Bitcoin
// transaction, goes offline, while offline Alice publishes Monero.
// If the Monero transaction gets confirmed before Bob comes online again then
// Bob would record a wallet-height that is past the lock transaction height,
// which can lead to the wallet not detect the transaction.
let monero_wallet_restore_blockheight = monero_wallet . block_height ( ) . 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 ,
monero_wallet_restore_blockheight ,
}
}
// Bob has locked Btc
// Watch for Alice to Lock Xmr or for cancel timelock to elapse
BobState ::BtcLocked {
state3 ,
monero_wallet_restore_blockheight ,
} = > {
let tx_lock_status = bitcoin_wallet . subscribe_to ( state3 . tx_lock . clone ( ) ) . await ;
if let ExpiredTimelocks ::None = state3 . current_epoch ( bitcoin_wallet ) . await ? {
let transfer_proof_watcher = event_loop_handle . recv_transfer_proof ( ) ;
let cancel_timelock_expires =
tx_lock_status . wait_until_confirmed_with ( state3 . cancel_timelock ) ;
tracing ::info ! ( "Waiting for Alice to lock Monero" ) ;
select ! {
transfer_proof = transfer_proof_watcher = > {
let transfer_proof = transfer_proof ? ;
tracing ::info ! ( txid = % transfer_proof . tx_hash ( ) , "Alice locked Monero" ) ;
BobState ::XmrLockProofReceived {
state : state3 ,
lock_transfer_proof : transfer_proof ,
monero_wallet_restore_blockheight
}
} ,
result = cancel_timelock_expires = > {
result ? ;
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 ,
} = > {
let tx_lock_status = bitcoin_wallet . subscribe_to ( state . tx_lock . clone ( ) ) . await ;
if let ExpiredTimelocks ::None = state . current_epoch ( bitcoin_wallet ) . 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 ( monero ::InsufficientFunds { expected , actual } ) = > {
tracing ::warn ! ( % expected , % actual , "Insufficient Monero have been locked!" ) ;
tracing ::info ! ( timelock = % state . cancel_timelock , "Waiting for cancel timelock to expire" ) ;
tx_lock_status . wait_until_confirmed_with ( state . cancel_timelock ) . await ? ;
BobState ::CancelTimelockExpired ( state . cancel ( ) )
} ,
}
}
result = tx_lock_status . wait_until_confirmed_with ( state . cancel_timelock ) = > {
result ? ;
BobState ::CancelTimelockExpired ( state . cancel ( ) )
}
}
} else {
BobState ::CancelTimelockExpired ( state . cancel ( ) )
}
}
BobState ::XmrLocked ( state ) = > {
let tx_lock_status = bitcoin_wallet . subscribe_to ( state . tx_lock . clone ( ) ) . await ;
if let ExpiredTimelocks ::None = state . expired_timelock ( bitcoin_wallet ) . await ? {
// Alice has locked Xmr
// Bob sends Alice his key
select ! {
result = event_loop_handle . send_encrypted_signature ( state . tx_redeem_encsig ( ) ) = > {
match result {
Ok ( _ ) = > BobState ::EncSigSent ( state ) ,
Err ( bmrng ::error ::RequestError ::RecvError | bmrng ::error ::RequestError ::SendError ( _ ) ) = > bail ! ( "Failed to communicate encrypted signature through event loop channel" ) ,
Err ( bmrng ::error ::RequestError ::RecvTimeoutError ) = > unreachable! ( "We construct the channel with no timeout" ) ,
}
} ,
result = tx_lock_status . wait_until_confirmed_with ( state . cancel_timelock ) = > {
result ? ;
BobState ::CancelTimelockExpired ( state . cancel ( ) )
}
}
} else {
BobState ::CancelTimelockExpired ( state . cancel ( ) )
}
}
BobState ::EncSigSent ( state ) = > {
let tx_lock_status = bitcoin_wallet . subscribe_to ( state . tx_lock . clone ( ) ) . await ;
if let ExpiredTimelocks ::None = state . expired_timelock ( bitcoin_wallet ) . await ? {
select ! {
state5 = state . watch_for_redeem_btc ( bitcoin_wallet ) = > {
BobState ::BtcRedeemed ( state5 ? )
} ,
result = tx_lock_status . wait_until_confirmed_with ( state . cancel_timelock ) = > {
result ? ;
BobState ::CancelTimelockExpired ( state . cancel ( ) )
}
}
} else {
BobState ::CancelTimelockExpired ( state . cancel ( ) )
}
}
BobState ::BtcRedeemed ( state ) = > {
let ( spend_key , view_key ) = state . xmr_keys ( ) ;
let wallet_file_name = swap_id . to_string ( ) ;
if let Err ( e ) = monero_wallet
. create_from_and_load (
wallet_file_name . clone ( ) ,
spend_key ,
view_key ,
state . monero_wallet_restore_blockheight ,
)
. await
{
// In case we failed to refresh/sweep, when resuming the wallet might already
// exist! This is a very unlikely scenario, but if we don't take care of it we
// might not be able to ever transfer the Monero.
tracing ::warn ! ( "Failed to generate monero wallet from keys: {:#}" , e ) ;
tracing ::info ! ( % wallet_file_name ,
"Falling back to trying to open the the wallet if it already exists" ,
) ;
monero_wallet . open ( wallet_file_name ) . 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 ( monero_receive_address ) . await ? ;
for tx_hash in tx_hashes {
tracing ::info ! ( % monero_receive_address , txid = % tx_hash . 0 , "Successfully transferred XMR to wallet" ) ;
}
BobState ::XmrRedeemed {
tx_lock_id : state . tx_lock_id ( ) ,
}
}
BobState ::CancelTimelockExpired ( state4 ) = > {
if state4 . check_for_tx_cancel ( bitcoin_wallet ) . await . is_err ( ) {
state4 . submit_tx_cancel ( bitcoin_wallet ) . await ? ;
}
BobState ::BtcCancelled ( state4 )
}
BobState ::BtcCancelled ( state ) = > {
// Bob has cancelled the swap
match state . expired_timelock ( bitcoin_wallet ) . await ? {
ExpiredTimelocks ::None = > {
bail ! (
"Internal error: canceled state reached before cancel timelock was expired"
) ;
}
ExpiredTimelocks ::Cancel = > {
state . publish_refund_btc ( bitcoin_wallet ) . await ? ;
BobState ::BtcRefunded ( state )
}
ExpiredTimelocks ::Punish = > {
tracing ::info ! ( "You have been punished for not refunding in time" ) ;
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 } ,
} )
}