From 349035d321ceedd95432c5792a3a4189bb099bf6 Mon Sep 17 00:00:00 2001 From: binarybaron Date: Mon, 26 Aug 2024 15:40:50 +0200 Subject: [PATCH] refactor(tauri, swap): move rpc api to cli/api --- Cargo.lock | 23 +- src-tauri/src/lib.rs | 8 +- swap/src/cli.rs | 1 + swap/src/{ => cli}/api.rs | 0 swap/src/cli/api/request.rs | 1188 ++++++++++++++++++++++ swap/src/{ => cli}/api/tauri_bindings.rs | 18 +- swap/src/cli/command.rs | 16 +- swap/src/cli/list_sellers.rs | 1 + swap/src/kraken.rs | 2 + swap/src/lib.rs | 1 - swap/src/network.rs | 2 +- swap/src/protocol/bob.rs | 2 +- swap/src/protocol/bob/swap.rs | 2 +- swap/src/rpc.rs | 2 +- swap/src/rpc/methods.rs | 11 +- 15 files changed, 1231 insertions(+), 46 deletions(-) rename swap/src/{ => cli}/api.rs (100%) create mode 100644 swap/src/cli/api/request.rs rename swap/src/{ => cli}/api/tauri_bindings.rs (90%) diff --git a/Cargo.lock b/Cargo.lock index ea46b0e7..c74ca7bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2252,7 +2252,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 1.1.3", "proc-macro-error", "proc-macro2", "quote", @@ -3761,7 +3761,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "424f6e86263cd5294cbd7f1e95746b95aca0e0d66bff31e5a40d6baa87b4aa99" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 1.1.3", "proc-macro-error", "proc-macro2", "quote", @@ -3897,7 +3897,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 1.1.3", "proc-macro2", "quote", "syn 1.0.109", @@ -4550,12 +4550,12 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ - "once_cell", - "toml_edit 0.19.15", + "thiserror", + "toml 0.5.11", ] [[package]] @@ -7054,6 +7054,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.7.8" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 98462d57..aa1c0467 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,7 +1,7 @@ use std::result::Result; use std::sync::Arc; use swap::{ - api::{ + cli::api::{ request::{ BalanceArgs, BuyXmrArgs, GetHistoryArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs, @@ -25,7 +25,7 @@ impl ToStringResult for Result { /// This macro is used to create boilerplate functions as tauri commands /// that simply delegate handling to the respective request type. -/// +/// /// # Example /// ```ignored /// tauri_command!(get_balance, BalanceArgs); @@ -43,8 +43,8 @@ macro_rules! tauri_command { async fn $fn_name( context: tauri::State<'_, Arc>, args: $request_name, - ) -> Result<<$request_name as swap::api::request::Request>::Response, String> { - <$request_name as swap::api::request::Request>::request(args, context.inner().clone()) + ) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> { + <$request_name as swap::cli::api::request::Request>::request(args, context.inner().clone()) .await .to_string_result() } diff --git a/swap/src/cli.rs b/swap/src/cli.rs index f0faf146..a87f19cf 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -5,6 +5,7 @@ mod event_loop; mod list_sellers; pub mod tracing; pub mod transport; +pub mod api; pub use behaviour::{Behaviour, OutEvent}; pub use cancel_and_refund::{cancel, cancel_and_refund, refund}; diff --git a/swap/src/api.rs b/swap/src/cli/api.rs similarity index 100% rename from swap/src/api.rs rename to swap/src/cli/api.rs diff --git a/swap/src/cli/api/request.rs b/swap/src/cli/api/request.rs new file mode 100644 index 00000000..8c2fa6fa --- /dev/null +++ b/swap/src/cli/api/request.rs @@ -0,0 +1,1188 @@ +use super::tauri_bindings::TauriHandle; +use crate::cli::api::tauri_bindings::{TauriEmitter, TauriSwapProgressEvent}; +use crate::cli::api::Context; +use crate::bitcoin::{CancelTimelock, ExpiredTimelocks, PunishTimelock, TxLock}; +use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus}; +use crate::libp2p_ext::MultiAddrExt; +use crate::network::quote::{BidQuote, ZeroQuoteReceived}; +use crate::network::swarm; +use crate::protocol::bob::{BobState, Swap}; +use crate::protocol::{bob, State}; +use crate::{bitcoin, cli, monero, rpc}; +use ::bitcoin::Txid; +use anyhow::{bail, Context as AnyContext, Result}; +use libp2p::core::Multiaddr; +use libp2p::PeerId; +use qrcode::render::unicode; +use qrcode::QrCode; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::cmp::min; +use std::convert::TryInto; +use std::future::Future; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; +use tracing::Instrument; +use typeshare::typeshare; +use uuid::Uuid; + +/// This trait is implemented by all types of request args that +/// the CLI can handle. +/// It provides a unified abstraction that can be useful for generics. +#[allow(async_fn_in_trait)] +pub trait Request { + type Response: Serialize; + async fn request(self, ctx: Arc) -> Result; +} + +// BuyXmr +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct BuyXmrArgs { + #[typeshare(serialized_as = "string")] + pub seller: Multiaddr, + #[typeshare(serialized_as = "string")] + pub bitcoin_change_address: bitcoin::Address, + #[typeshare(serialized_as = "string")] + pub monero_receive_address: monero::Address, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct BuyXmrResponse { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, + pub quote: BidQuote, +} + +impl Request for BuyXmrArgs { + type Response = BuyXmrResponse; + + async fn request(self, ctx: Arc) -> Result { + buy_xmr(self, ctx).await + } +} + +// ResumeSwap +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ResumeSwapArgs { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct ResumeSwapResponse { + pub result: String, +} + +impl Request for ResumeSwapArgs { + type Response = ResumeSwapResponse; + + async fn request(self, ctx: Arc) -> Result { + resume_swap(self, ctx).await + } +} + +// CancelAndRefund +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct CancelAndRefundArgs { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + +impl Request for CancelAndRefundArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + cancel_and_refund(self, ctx).await + } +} + +// MoneroRecovery +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct MoneroRecoveryArgs { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + +impl Request for MoneroRecoveryArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + monero_recovery(self, ctx).await + } +} + +// WithdrawBtc +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct WithdrawBtcArgs { + #[typeshare(serialized_as = "number")] + #[serde(default, with = "::bitcoin::util::amount::serde::as_sat::opt")] + pub amount: Option, + #[typeshare(serialized_as = "string")] + pub address: bitcoin::Address, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct WithdrawBtcResponse { + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub amount: bitcoin::Amount, + pub txid: String, +} + +impl Request for WithdrawBtcArgs { + type Response = WithdrawBtcResponse; + + async fn request(self, ctx: Arc) -> Result { + withdraw_btc(self, ctx).await + } +} + +// ListSellers +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ListSellersArgs { + #[typeshare(serialized_as = "string")] + pub rendezvous_point: Multiaddr, +} + +impl Request for ListSellersArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + list_sellers(self, ctx).await + } +} + +// StartDaemon +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct StartDaemonArgs { + #[typeshare(serialized_as = "string")] + pub server_address: Option, +} + +impl Request for StartDaemonArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + start_daemon(self, (*ctx).clone()).await + } +} + +// GetSwapInfo +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct GetSwapInfoArgs { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + +#[typeshare] +#[derive(Serialize)] +pub struct GetSwapInfoResponse { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, + pub seller: Seller, + pub completed: bool, + pub start_date: String, + #[typeshare(serialized_as = "string")] + pub state_name: String, + #[typeshare(serialized_as = "number")] + pub xmr_amount: monero::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub btc_amount: bitcoin::Amount, + #[typeshare(serialized_as = "string")] + pub tx_lock_id: Txid, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub tx_cancel_fee: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub tx_refund_fee: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub tx_lock_fee: bitcoin::Amount, + pub btc_refund_address: String, + pub cancel_timelock: CancelTimelock, + pub punish_timelock: PunishTimelock, + pub timelock: Option, +} + +impl Request for GetSwapInfoArgs { + type Response = GetSwapInfoResponse; + + async fn request(self, ctx: Arc) -> Result { + get_swap_info(self, ctx).await + } +} + +// Balance +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct BalanceArgs { + pub force_refresh: bool, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct BalanceResponse { + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub balance: bitcoin::Amount, +} + +impl Request for BalanceArgs { + type Response = BalanceResponse; + + async fn request(self, ctx: Arc) -> Result { + get_balance(self, ctx).await + } +} + +// GetHistory +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct GetHistoryArgs; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetHistoryEntry { + #[typeshare(serialized_as = "string")] + swap_id: Uuid, + state: String, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct GetHistoryResponse { + pub swaps: Vec, +} + +impl Request for GetHistoryArgs { + type Response = GetHistoryResponse; + + async fn request(self, ctx: Arc) -> Result { + get_history(ctx).await + } +} + +// Additional structs +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct Seller { + #[typeshare(serialized_as = "string")] + pub peer_id: PeerId, + pub addresses: Vec, +} + +// Suspend current swap +#[derive(Debug, Deserialize)] +pub struct SuspendCurrentSwapArgs; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct SuspendCurrentSwapResponse { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + +impl Request for SuspendCurrentSwapArgs { + type Response = SuspendCurrentSwapResponse; + + async fn request(self, ctx: Arc) -> Result { + suspend_current_swap(ctx).await + } +} + +pub struct GetCurrentSwap; + +impl Request for GetCurrentSwap { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + get_current_swap(ctx).await + } +} + +pub struct GetConfig; + +impl Request for GetConfig { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + get_config(ctx).await + } +} + +pub struct ExportBitcoinWalletArgs; + +impl Request for ExportBitcoinWalletArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + export_bitcoin_wallet(ctx).await + } +} + +pub struct GetConfigArgs; + +impl Request for GetConfigArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + get_config(ctx).await + } +} + +#[tracing::instrument(fields(method = "suspend_current_swap"), skip(context))] +pub async fn suspend_current_swap(context: Arc) -> Result { + let swap_id = context.swap_lock.get_current_swap_id().await; + + if let Some(id_value) = swap_id { + context.swap_lock.send_suspend_signal().await?; + + Ok(SuspendCurrentSwapResponse { swap_id: id_value }) + } else { + bail!("No swap is currently running") + } +} + +#[tracing::instrument(fields(method = "get_swap_infos_all"), skip(context))] +pub async fn get_swap_infos_all(context: Arc) -> Result> { + let swap_ids = context.db.all().await?; + let mut swap_infos = Vec::new(); + + for (swap_id, _) in swap_ids { + let swap_info = get_swap_info(GetSwapInfoArgs { swap_id }, context.clone()).await?; + swap_infos.push(swap_info); + } + + Ok(swap_infos) +} + +#[tracing::instrument(fields(method = "get_swap_info"), skip(context))] +pub async fn get_swap_info( + args: GetSwapInfoArgs, + context: Arc, +) -> Result { + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + let state = context.db.get_state(args.swap_id).await?; + let is_completed = state.swap_finished(); + + let peer_id = context + .db + .get_peer_id(args.swap_id) + .await + .with_context(|| "Could not get PeerID")?; + + let addresses = context + .db + .get_addresses(peer_id) + .await + .with_context(|| "Could not get addressess")?; + + let start_date = context.db.get_swap_start_date(args.swap_id).await?; + + let swap_state: BobState = state.try_into()?; + + let ( + xmr_amount, + btc_amount, + tx_lock_id, + tx_cancel_fee, + tx_refund_fee, + tx_lock_fee, + btc_refund_address, + cancel_timelock, + punish_timelock, + ) = context + .db + .get_states(args.swap_id) + .await? + .iter() + .find_map(|state| { + if let State::Bob(BobState::SwapSetupCompleted(state2)) = state { + let xmr_amount = state2.xmr; + let btc_amount = state2.tx_lock.lock_amount(); + let tx_cancel_fee = state2.tx_cancel_fee; + let tx_refund_fee = state2.tx_refund_fee; + let tx_lock_id = state2.tx_lock.txid(); + let btc_refund_address = state2.refund_address.to_string(); + + if let Ok(tx_lock_fee) = state2.tx_lock.fee() { + Some(( + xmr_amount, + btc_amount, + tx_lock_id, + tx_cancel_fee, + tx_refund_fee, + tx_lock_fee, + btc_refund_address, + state2.cancel_timelock, + state2.punish_timelock, + )) + } else { + None + } + } else { + None + } + }) + .with_context(|| "Did not find SwapSetupCompleted state for swap")?; + + let timelock = match swap_state.clone() { + BobState::Started { .. } | BobState::SafelyAborted | BobState::SwapSetupCompleted(_) => { + None + } + BobState::BtcLocked { state3: state, .. } + | BobState::XmrLockProofReceived { state, .. } => { + Some(state.expired_timelock(bitcoin_wallet).await?) + } + BobState::XmrLocked(state) | BobState::EncSigSent(state) => { + Some(state.expired_timelock(bitcoin_wallet).await?) + } + BobState::CancelTimelockExpired(state) | BobState::BtcCancelled(state) => { + Some(state.expired_timelock(bitcoin_wallet).await?) + } + BobState::BtcPunished { .. } => Some(ExpiredTimelocks::Punish), + BobState::BtcRefunded(_) | BobState::BtcRedeemed(_) | BobState::XmrRedeemed { .. } => None, + }; + + Ok(GetSwapInfoResponse { + swap_id: args.swap_id, + seller: Seller { + peer_id, + addresses: addresses.iter().map(|a| a.to_string()).collect(), + }, + completed: is_completed, + start_date, + state_name: format!("{}", swap_state), + xmr_amount, + btc_amount, + tx_lock_id, + tx_cancel_fee, + tx_refund_fee, + tx_lock_fee, + btc_refund_address: btc_refund_address.to_string(), + cancel_timelock, + punish_timelock, + timelock, + }) +} + +#[tracing::instrument(fields(method = "buy_xmr"), skip(context))] +pub async fn buy_xmr( + buy_xmr: BuyXmrArgs, + context: Arc, +) -> Result { + let BuyXmrArgs { + seller, + bitcoin_change_address, + monero_receive_address, + } = buy_xmr; + + let swap_id = Uuid::new_v4(); + + let bitcoin_wallet = Arc::clone( + context + .bitcoin_wallet + .as_ref() + .expect("Could not find Bitcoin wallet"), + ); + let monero_wallet = Arc::clone( + context + .monero_wallet + .as_ref() + .context("Could not get Monero wallet")?, + ); + let env_config = context.config.env_config; + let seed = context.config.seed.clone().context("Could not get seed")?; + + let seller_peer_id = seller + .extract_peer_id() + .context("Seller address must contain peer ID")?; + context + .db + .insert_address(seller_peer_id, seller.clone()) + .await?; + + let behaviour = cli::Behaviour::new( + seller_peer_id, + env_config, + bitcoin_wallet.clone(), + (seed.derive_libp2p_identity(), context.config.namespace), + ); + let mut swarm = swarm::cli( + seed.derive_libp2p_identity(), + context.config.tor_socks5_port, + behaviour, + ) + .await?; + + swarm.behaviour_mut().add_address(seller_peer_id, seller); + + context + .db + .insert_monero_address(swap_id, monero_receive_address) + .await?; + + tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized"); + + context.swap_lock.acquire_swap_lock(swap_id).await?; + + let initialize_swap = tokio::select! { + biased; + _ = context.swap_lock.listen_for_swap_force_suspension() => { + tracing::debug!("Shutdown signal received, exiting"); + context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active."); + + context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); + + bail!("Shutdown signal received"); + }, + result = async { + let (event_loop, mut event_loop_handle) = + EventLoop::new(swap_id, swarm, seller_peer_id, context.db.clone())?; + let event_loop = tokio::spawn(event_loop.run().in_current_span()); + + let bid_quote = event_loop_handle.request_quote().await?; + + Ok::<_, anyhow::Error>((event_loop, event_loop_handle, bid_quote)) + } => { + result + }, + }; + + let (event_loop, event_loop_handle, bid_quote) = match initialize_swap { + Ok(result) => result, + Err(error) => { + tracing::error!(%swap_id, "Swap initialization failed: {:#}", error); + context + .swap_lock + .release_swap_lock() + .await + .expect("Could not release swap lock"); + + context + .tauri_handle + .emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); + + bail!(error); + } + }; + + context + .tauri_handle + .emit_swap_progress_event(swap_id, TauriSwapProgressEvent::ReceivedQuote(bid_quote)); + + context.tasks.clone().spawn(async move { + tokio::select! { + biased; + _ = context.swap_lock.listen_for_swap_force_suspension() => { + tracing::debug!("Shutdown signal received, exiting"); + context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active."); + + context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); + + bail!("Shutdown signal received"); + }, + event_loop_result = event_loop => { + match event_loop_result { + Ok(_) => { + tracing::debug!(%swap_id, "EventLoop completed") + } + Err(error) => { + tracing::error!(%swap_id, "EventLoop failed: {:#}", error) + } + } + }, + swap_result = async { + let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size()); + let estimate_fee = |amount| bitcoin_wallet.estimate_fee(TxLock::weight(), amount); + + let determine_amount = determine_btc_to_swap( + context.config.json, + bid_quote, + bitcoin_wallet.new_address(), + || bitcoin_wallet.balance(), + max_givable, + || bitcoin_wallet.sync(), + estimate_fee, + context.tauri_handle.clone() + ); + + let (amount, fees) = match determine_amount.await { + Ok(val) => val, + Err(error) => match error.downcast::() { + Ok(_) => { + bail!("Seller's XMR balance is currently too low to initiate a swap, please try again later") + } + Err(other) => bail!(other), + }, + }; + + tracing::info!(%amount, %fees, "Determined swap amount"); + + context.db.insert_peer_id(swap_id, seller_peer_id).await?; + + let swap = Swap::new( + Arc::clone(&context.db), + swap_id, + Arc::clone(&bitcoin_wallet), + monero_wallet, + env_config, + event_loop_handle, + monero_receive_address, + bitcoin_change_address, + amount, + ).with_event_emitter(context.tauri_handle.clone()); + + bob::run(swap).await + } => { + match swap_result { + Ok(state) => { + tracing::debug!(%swap_id, state=%state, "Swap completed") + } + Err(error) => { + tracing::error!(%swap_id, "Failed to complete swap: {:#}", error) + } + } + }, + }; + tracing::debug!(%swap_id, "Swap completed"); + + context + .swap_lock + .release_swap_lock() + .await + .expect("Could not release swap lock"); + + context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); + + Ok::<_, anyhow::Error>(()) + }.in_current_span()).await; + + Ok(BuyXmrResponse { + swap_id, + quote: bid_quote, + }) +} + +#[tracing::instrument(fields(method = "resume_swap"), skip(context))] +pub async fn resume_swap( + resume: ResumeSwapArgs, + context: Arc, +) -> Result { + let ResumeSwapArgs { swap_id } = resume; + context.swap_lock.acquire_swap_lock(swap_id).await?; + + let seller_peer_id = context.db.get_peer_id(swap_id).await?; + let seller_addresses = context.db.get_addresses(seller_peer_id).await?; + + let seed = context + .config + .seed + .as_ref() + .context("Could not get seed")? + .derive_libp2p_identity(); + + let behaviour = cli::Behaviour::new( + seller_peer_id, + context.config.env_config, + Arc::clone( + context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?, + ), + (seed.clone(), context.config.namespace), + ); + let mut swarm = swarm::cli(seed.clone(), context.config.tor_socks5_port, behaviour).await?; + let our_peer_id = swarm.local_peer_id(); + + tracing::debug!(peer_id = %our_peer_id, "Network layer initialized"); + + for seller_address in seller_addresses { + swarm + .behaviour_mut() + .add_address(seller_peer_id, seller_address); + } + + let (event_loop, event_loop_handle) = + EventLoop::new(swap_id, swarm, seller_peer_id, context.db.clone())?; + let monero_receive_address = context.db.get_monero_address(swap_id).await?; + let swap = Swap::from_db( + Arc::clone(&context.db), + swap_id, + Arc::clone( + context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?, + ), + Arc::clone( + context + .monero_wallet + .as_ref() + .context("Could not get Monero wallet")?, + ), + context.config.env_config, + event_loop_handle, + monero_receive_address, + ) + .await? + .with_event_emitter(context.tauri_handle.clone()); + + context.tasks.clone().spawn( + async move { + let handle = tokio::spawn(event_loop.run().in_current_span()); + tokio::select! { + biased; + _ = context.swap_lock.listen_for_swap_force_suspension() => { + tracing::debug!("Shutdown signal received, exiting"); + context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active."); + + context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); + + bail!("Shutdown signal received"); + }, + + event_loop_result = handle => { + match event_loop_result { + Ok(_) => { + tracing::debug!(%swap_id, "EventLoop completed during swap resume") + } + Err(error) => { + tracing::error!(%swap_id, "EventLoop failed during swap resume: {:#}", error) + } + } + }, + swap_result = bob::run(swap) => { + match swap_result { + Ok(state) => { + tracing::debug!(%swap_id, state=%state, "Swap completed after resuming") + } + Err(error) => { + tracing::error!(%swap_id, "Failed to resume swap: {:#}", error) + } + } + + } + } + context + .swap_lock + .release_swap_lock() + .await + .expect("Could not release swap lock"); + + context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); + + Ok::<(), anyhow::Error>(()) + } + .in_current_span(), + ).await; + + Ok(ResumeSwapResponse { + result: "OK".to_string(), + }) +} + +#[tracing::instrument(fields(method = "cancel_and_refund"), skip(context))] +pub async fn cancel_and_refund( + cancel_and_refund: CancelAndRefundArgs, + context: Arc, +) -> Result { + let CancelAndRefundArgs { swap_id } = cancel_and_refund; + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + context.swap_lock.acquire_swap_lock(swap_id).await?; + + let state = + cli::cancel_and_refund(swap_id, Arc::clone(bitcoin_wallet), Arc::clone(&context.db)).await; + + context + .swap_lock + .release_swap_lock() + .await + .expect("Could not release swap lock"); + + context + .tauri_handle + .emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); + + state.map(|state| { + json!({ + "result": state, + }) + }) +} + +#[tracing::instrument(fields(method = "get_history"), skip(context))] +pub async fn get_history(context: Arc) -> Result { + let swaps = context.db.all().await?; + let mut vec: Vec = Vec::new(); + for (swap_id, state) in swaps { + let state: BobState = state.try_into()?; + vec.push(GetHistoryEntry { + swap_id, + state: state.to_string(), + }) + } + + Ok(GetHistoryResponse { swaps: vec }) +} + +#[tracing::instrument(fields(method = "get_raw_states"), skip(context))] +pub async fn get_raw_states(context: Arc) -> Result { + let raw_history = context.db.raw_all().await?; + + Ok(json!({ "raw_states": raw_history })) +} + +#[tracing::instrument(fields(method = "get_config"), skip(context))] +pub async fn get_config(context: Arc) -> Result { + let data_dir_display = context.config.data_dir.display(); + tracing::info!(path=%data_dir_display, "Data directory"); + tracing::info!(path=%format!("{}/logs", data_dir_display), "Log files directory"); + tracing::info!(path=%format!("{}/sqlite", data_dir_display), "Sqlite file location"); + tracing::info!(path=%format!("{}/seed.pem", data_dir_display), "Seed file location"); + tracing::info!(path=%format!("{}/monero", data_dir_display), "Monero-wallet-rpc directory"); + tracing::info!(path=%format!("{}/wallet", data_dir_display), "Internal bitcoin wallet directory"); + + Ok(json!({ + "log_files": format!("{}/logs", data_dir_display), + "sqlite": format!("{}/sqlite", data_dir_display), + "seed": format!("{}/seed.pem", data_dir_display), + "monero-wallet-rpc": format!("{}/monero", data_dir_display), + "bitcoin_wallet": format!("{}/wallet", data_dir_display), + })) +} + +#[tracing::instrument(fields(method = "withdraw_btc"), skip(context))] +pub async fn withdraw_btc( + withdraw_btc: WithdrawBtcArgs, + context: Arc, +) -> Result { + let WithdrawBtcArgs { address, amount } = withdraw_btc; + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + let amount = match amount { + Some(amount) => amount, + None => { + bitcoin_wallet + .max_giveable(address.script_pubkey().len()) + .await? + } + }; + let psbt = bitcoin_wallet + .send_to_address(address, amount, None) + .await?; + let signed_tx = bitcoin_wallet.sign_and_finalize(psbt).await?; + + bitcoin_wallet + .broadcast(signed_tx.clone(), "withdraw") + .await?; + + Ok(WithdrawBtcResponse { + txid: signed_tx.txid().to_string(), + amount, + }) +} + +#[tracing::instrument(fields(method = "start_daemon"), skip(context))] +pub async fn start_daemon( + start_daemon: StartDaemonArgs, + context: Context, +) -> Result { + let StartDaemonArgs { server_address } = start_daemon; + // Default to 127.0.0.1:1234 + let server_address = server_address.unwrap_or("127.0.0.1:1234".parse()?); + + let (addr, server_handle) = rpc::run_server(server_address, context).await?; + + tracing::info!(%addr, "Started RPC server"); + + server_handle.stopped().await; + + tracing::info!("Stopped RPC server"); + + Ok(json!({})) +} + +#[tracing::instrument(fields(method = "get_balance"), skip(context))] +pub async fn get_balance(balance: BalanceArgs, context: Arc) -> Result { + let BalanceArgs { force_refresh } = balance; + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + if force_refresh { + bitcoin_wallet.sync().await?; + } + + let bitcoin_balance = bitcoin_wallet.balance().await?; + + if force_refresh { + tracing::info!( + balance = %bitcoin_balance, + "Checked Bitcoin balance", + ); + } else { + tracing::debug!( + balance = %bitcoin_balance, + "Current Bitcoin balance as of last sync", + ); + } + + Ok(BalanceResponse { + balance: bitcoin_balance, + }) +} + +#[tracing::instrument(fields(method = "list_sellers"), skip(context))] +pub async fn list_sellers( + list_sellers: ListSellersArgs, + context: Arc, +) -> Result { + let ListSellersArgs { rendezvous_point } = list_sellers; + let rendezvous_node_peer_id = rendezvous_point + .extract_peer_id() + .context("Rendezvous node address must contain peer ID")?; + + let identity = context + .config + .seed + .as_ref() + .context("Cannot extract seed")? + .derive_libp2p_identity(); + + let sellers = list_sellers_impl( + rendezvous_node_peer_id, + rendezvous_point, + context.config.namespace, + context.config.tor_socks5_port, + identity, + ) + .await?; + + for seller in &sellers { + match seller.status { + SellerStatus::Online(quote) => { + tracing::info!( + price = %quote.price.to_string(), + min_quantity = %quote.min_quantity.to_string(), + max_quantity = %quote.max_quantity.to_string(), + status = "Online", + address = %seller.multiaddr.to_string(), + "Fetched peer status" + ); + } + SellerStatus::Unreachable => { + tracing::info!( + status = "Unreachable", + address = %seller.multiaddr.to_string(), + "Fetched peer status" + ); + } + } + } + + Ok(json!({ "sellers": sellers })) +} + +#[tracing::instrument(fields(method = "export_bitcoin_wallet"), skip(context))] +pub async fn export_bitcoin_wallet(context: Arc) -> Result { + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + let wallet_export = bitcoin_wallet.wallet_export("cli").await?; + tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet"); + Ok(json!({ + "descriptor": wallet_export.to_string(), + })) +} + +#[tracing::instrument(fields(method = "monero_recovery"), skip(context))] +pub async fn monero_recovery( + monero_recovery: MoneroRecoveryArgs, + context: Arc, +) -> Result { + let MoneroRecoveryArgs { swap_id } = monero_recovery; + let swap_state: BobState = context.db.get_state(swap_id).await?.try_into()?; + + if let BobState::BtcRedeemed(state5) = swap_state { + let (spend_key, view_key) = state5.xmr_keys(); + let restore_height = state5.monero_wallet_restore_blockheight.height; + + let address = monero::Address::standard( + context.config.env_config.monero_network, + monero::PublicKey::from_private_key(&spend_key), + monero::PublicKey::from(view_key.public()), + ); + + tracing::info!(restore_height=%restore_height, address=%address, spend_key=%spend_key, view_key=%view_key, "Monero recovery information"); + + Ok(json!({ + "address": address, + "spend_key": spend_key.to_string(), + "view_key": view_key.to_string(), + "restore_height": state5.monero_wallet_restore_blockheight.height, + })) + } else { + bail!( + "Cannot print monero recovery information in state {}, only possible for BtcRedeemed", + swap_state + ) + } +} + +#[tracing::instrument(fields(method = "get_current_swap"), skip(context))] +pub async fn get_current_swap(context: Arc) -> Result { + Ok(json!({ + "swap_id": context.swap_lock.get_current_swap_id().await + })) +} + +fn qr_code(value: &impl ToString) -> Result { + let code = QrCode::new(value.to_string())?; + let qr_code = code + .render::() + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build(); + Ok(qr_code) +} + +#[allow(clippy::too_many_arguments)] +pub async fn determine_btc_to_swap( + json: bool, + bid_quote: BidQuote, + get_new_address: impl Future>, + balance: FB, + max_giveable_fn: FMG, + sync: FS, + estimate_fee: FFE, + event_emitter: Option, +) -> Result<(bitcoin::Amount, bitcoin::Amount)> +where + TB: Future>, + FB: Fn() -> TB, + TMG: Future>, + FMG: Fn() -> TMG, + TS: Future>, + FS: Fn() -> TS, + FFE: Fn(bitcoin::Amount) -> TFE, + TFE: Future>, +{ + if bid_quote.max_quantity == bitcoin::Amount::ZERO { + bail!(ZeroQuoteReceived) + } + + tracing::info!( + price = %bid_quote.price, + minimum_amount = %bid_quote.min_quantity, + maximum_amount = %bid_quote.max_quantity, + "Received quote", + ); + + sync().await?; + let mut max_giveable = max_giveable_fn().await?; + + if max_giveable == bitcoin::Amount::ZERO || max_giveable < bid_quote.min_quantity { + let deposit_address = get_new_address.await?; + let minimum_amount = bid_quote.min_quantity; + let maximum_amount = bid_quote.max_quantity; + + if !json { + eprintln!("{}", qr_code(&deposit_address)?); + } + + loop { + let min_outstanding = bid_quote.min_quantity - max_giveable; + let min_bitcoin_lock_tx_fee = estimate_fee(min_outstanding).await?; + let min_deposit_until_swap_will_start = min_outstanding + min_bitcoin_lock_tx_fee; + let max_deposit_until_maximum_amount_is_reached = + maximum_amount - max_giveable + min_bitcoin_lock_tx_fee; + + tracing::info!( + "Deposit at least {} to cover the min quantity with fee!", + min_deposit_until_swap_will_start + ); + tracing::info!( + %deposit_address, + %min_deposit_until_swap_will_start, + %max_deposit_until_maximum_amount_is_reached, + %max_giveable, + %minimum_amount, + %maximum_amount, + %min_bitcoin_lock_tx_fee, + price = %bid_quote.price, + "Waiting for Bitcoin deposit", + ); + + // TODO: Use the real swap id here + event_emitter.emit_swap_progress_event( + Uuid::new_v4(), + TauriSwapProgressEvent::WaitingForBtcDeposit { + deposit_address: deposit_address.clone(), + max_giveable, + min_deposit_until_swap_will_start, + max_deposit_until_maximum_amount_is_reached, + min_bitcoin_lock_tx_fee, + quote: bid_quote.clone(), + }, + ); + + max_giveable = loop { + sync().await?; + let new_max_givable = max_giveable_fn().await?; + + if new_max_givable > max_giveable { + break new_max_givable; + } + + tokio::time::sleep(Duration::from_secs(1)).await; + }; + + let new_balance = balance().await?; + tracing::info!(%new_balance, %max_giveable, "Received Bitcoin"); + + if max_giveable < bid_quote.min_quantity { + tracing::info!("Deposited amount is not enough to cover `min_quantity` when accounting for network fees"); + continue; + } + + break; + } + }; + + let balance = balance().await?; + let fees = balance - max_giveable; + let max_accepted = bid_quote.max_quantity; + let btc_swap_amount = min(max_giveable, max_accepted); + + Ok((btc_swap_amount, fees)) +} diff --git a/swap/src/api/tauri_bindings.rs b/swap/src/cli/api/tauri_bindings.rs similarity index 90% rename from swap/src/api/tauri_bindings.rs rename to swap/src/cli/api/tauri_bindings.rs index f705a3c5..f0788fa4 100644 --- a/swap/src/api/tauri_bindings.rs +++ b/swap/src/cli/api/tauri_bindings.rs @@ -27,11 +27,7 @@ impl TauriHandle { } pub trait TauriEmitter { - fn emit_tauri_event( - &self, - event: &str, - payload: S, - ) -> Result<()>; + fn emit_tauri_event(&self, event: &str, payload: S) -> Result<()>; fn emit_swap_progress_event(&self, swap_id: Uuid, event: TauriSwapProgressEvent) { let _ = self.emit_tauri_event( @@ -42,21 +38,13 @@ pub trait TauriEmitter { } impl TauriEmitter for TauriHandle { - fn emit_tauri_event( - &self, - event: &str, - payload: S, - ) -> Result<()> { + fn emit_tauri_event(&self, event: &str, payload: S) -> Result<()> { self.emit_tauri_event(event, payload) } } impl TauriEmitter for Option { - fn emit_tauri_event( - &self, - event: &str, - payload: S, - ) -> Result<()> { + fn emit_tauri_event(&self, event: &str, payload: S) -> Result<()> { match self { Some(tauri) => tauri.emit_tauri_event(event, payload), None => Ok(()), diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index fa3740f7..e78a4666 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -1,10 +1,9 @@ -use crate::api::request::{ - buy_xmr, cancel_and_refund, export_bitcoin_wallet, get_balance, get_config, get_history, - list_sellers, monero_recovery, resume_swap, start_daemon, withdraw_btc, BalanceArgs, - BuyXmrArgs, CancelAndRefundArgs, ExportBitcoinWalletArgs, GetConfigArgs, GetHistoryArgs, - ListSellersArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, StartDaemonArgs, WithdrawBtcArgs, +use crate::cli::api::request::{ + BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, ExportBitcoinWalletArgs, GetConfigArgs, + GetHistoryArgs, ListSellersArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, StartDaemonArgs, + WithdrawBtcArgs, }; -use crate::api::Context; +use crate::cli::api::Context; use crate::bitcoin::{bitcoin_address, Amount}; use crate::monero; use crate::monero::monero_address; @@ -544,15 +543,14 @@ struct Seller { mod tests { use super::*; - use crate::api::api_test::*; - use crate::api::Config; + use crate::cli::api::api_test::*; + use crate::cli::api::Config; use crate::monero::monero_address::MoneroAddressNetworkMismatch; const BINARY_NAME: &str = "swap"; const ARGS_DATA_DIR: &str = "/tmp/dir/"; #[tokio::test] - // this test is very long, however it just checks that various CLI arguments sets the // internal Context and Request properly. It is unlikely to fail and splitting it in various // tests would require to run the tests sequantially which is very slow (due to the context diff --git a/swap/src/cli/list_sellers.rs b/swap/src/cli/list_sellers.rs index 381c561f..85abe263 100644 --- a/swap/src/cli/list_sellers.rs +++ b/swap/src/cli/list_sellers.rs @@ -73,6 +73,7 @@ pub enum Status { Unreachable, } +#[allow(unused)] #[derive(Debug)] enum OutEvent { Rendezvous(rendezvous::client::Event), diff --git a/swap/src/kraken.rs b/swap/src/kraken.rs index 29062114..b4562dc6 100644 --- a/swap/src/kraken.rs +++ b/swap/src/kraken.rs @@ -264,6 +264,7 @@ mod wire { #[serde(transparent)] pub struct TickerUpdate(Vec); + #[allow(unused)] #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum TickerField { @@ -277,6 +278,7 @@ mod wire { ask: Vec, } + #[allow(unused)] #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum RateElement { diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 3e575fc6..ddad3eab 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -16,7 +16,6 @@ missing_copy_implementations )] -pub mod api; pub mod asb; pub mod bitcoin; pub mod cli; diff --git a/swap/src/network.rs b/swap/src/network.rs index 527c04fc..89388120 100644 --- a/swap/src/network.rs +++ b/swap/src/network.rs @@ -13,5 +13,5 @@ pub mod tor_transport; pub mod transfer_proof; pub mod transport; -#[cfg(any(test, feature = "test"))] +#[cfg(test)] pub mod test; diff --git a/swap/src/protocol/bob.rs b/swap/src/protocol/bob.rs index d9d90ac9..97a20aa3 100644 --- a/swap/src/protocol/bob.rs +++ b/swap/src/protocol/bob.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::Result; use uuid::Uuid; -use crate::api::tauri_bindings::TauriHandle; +use crate::cli::api::tauri_bindings::TauriHandle; use crate::protocol::Database; use crate::{bitcoin, cli, env, monero}; diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 4551414e..0d01228d 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -1,4 +1,4 @@ -use crate::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent}; +use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent}; use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund}; use crate::cli::EventLoopHandle; use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected}; diff --git a/swap/src/rpc.rs b/swap/src/rpc.rs index 75ec9ae0..a6503c12 100644 --- a/swap/src/rpc.rs +++ b/swap/src/rpc.rs @@ -1,4 +1,4 @@ -use crate::api::Context; +use crate::cli::api::Context; use std::net::SocketAddr; use thiserror::Error; use tower_http::cors::CorsLayer; diff --git a/swap/src/rpc/methods.rs b/swap/src/rpc/methods.rs index ca707290..a239733e 100644 --- a/swap/src/rpc/methods.rs +++ b/swap/src/rpc/methods.rs @@ -1,10 +1,9 @@ -use crate::api::request::{ - get_current_swap, get_history, get_raw_states, - suspend_current_swap, - BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, GetSwapInfoArgs, ListSellersArgs, - MoneroRecoveryArgs, Request, ResumeSwapArgs, WithdrawBtcArgs, +use crate::cli::api::request::{ + get_current_swap, get_history, get_raw_states, suspend_current_swap, BalanceArgs, BuyXmrArgs, + CancelAndRefundArgs, GetSwapInfoArgs, ListSellersArgs, MoneroRecoveryArgs, Request, + ResumeSwapArgs, WithdrawBtcArgs, }; -use crate::api::Context; +use crate::cli::api::Context; use crate::bitcoin::bitcoin_address; use crate::monero::monero_address; use crate::{bitcoin, monero};