diff --git a/swap/Cargo.toml b/swap/Cargo.toml index f30af3a5..df47942b 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -51,6 +51,7 @@ libp2p = { version = "0.42.2", default-features = false, features = [ "ping", "rendezvous", "identify", + "serde", ] } monero = { version = "0.12", features = [ "serde_support" ] } monero-rpc = { path = "../monero-rpc" } @@ -67,7 +68,7 @@ reqwest = { version = "0.12", features = [ ], default-features = false } rust_decimal = { version = "1", features = [ "serde-float" ] } rust_decimal_macros = "1" -serde = { version = "1", features = [ "derive" ] } +serde = { version = "1.0", features = [ "derive" ] } serde_cbor = "0.11" serde_json = "1" serde_with = { version = "1", features = [ "macros" ] } @@ -85,6 +86,7 @@ sqlx = { version = "0.6.3", features = [ ] } structopt = "0.3" strum = { version = "0.26", features = [ "derive" ] } +tauri = { version = "2.0.0-rc.1", features = [ "config-json5" ] } thiserror = "1" time = "0.3" tokio = { version = "1", features = [ @@ -118,6 +120,7 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [ "tracing-log", "json", ] } +typeshare = "1.0.3" url = { version = "2", features = [ "serde" ] } uuid = { version = "1.9", features = [ "serde", "v4" ] } void = "1" diff --git a/swap/src/api.rs b/swap/src/api.rs index 75427810..dbb86f1e 100644 --- a/swap/src/api.rs +++ b/swap/src/api.rs @@ -1,4 +1,5 @@ pub mod request; +pub mod tauri_bindings; use crate::cli::command::{Bitcoin, Monero, Tor}; use crate::database::open_db; use crate::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet}; @@ -13,10 +14,13 @@ use std::fmt; use std::future::Future; use std::net::SocketAddr; use std::path::PathBuf; -use std::sync::{Arc, Mutex, Once}; +use std::sync::{Arc, Mutex as SyncMutex, Once}; +use tauri::AppHandle; +use tauri_bindings::TauriHandle; use tokio::sync::{broadcast, broadcast::Sender, Mutex as TokioMutex, RwLock}; use tokio::task::JoinHandle; use url::Url; +use uuid::Uuid; static START: Once = Once::new(); @@ -33,8 +37,6 @@ pub struct Config { is_testnet: bool, } -use uuid::Uuid; - #[derive(Default)] pub struct PendingTaskList(TokioMutex>>); @@ -64,6 +66,13 @@ impl PendingTaskList { } } +/// The `SwapLock` manages the state of the current swap, ensuring that only one swap can be active at a time. +/// It includes: +/// - A lock for the current swap (`current_swap`) +/// - A broadcast channel for suspension signals (`suspension_trigger`) +/// +/// The `SwapLock` provides methods to acquire and release the swap lock, and to listen for suspension signals. +/// This ensures that swap operations do not overlap and can be safely suspended if needed. pub struct SwapLock { current_swap: RwLock>, suspension_trigger: Sender<()>, @@ -157,17 +166,22 @@ impl Default for SwapLock { } } -// workaround for warning over monero_rpc_process which we must own but not read -#[allow(dead_code)] +/// Holds shared data for different parts of the CLI. +/// +/// Some components are optional, allowing initialization of only necessary parts. +/// For example, the `history` command doesn't require wallet initialization. +/// +/// Many fields are wrapped in `Arc` for thread-safe sharing. #[derive(Clone)] pub struct Context { pub db: Arc, - bitcoin_wallet: Option>, - monero_wallet: Option>, - monero_rpc_process: Option>>, pub swap_lock: Arc, pub config: Config, pub tasks: Arc, + tauri_handle: Option, + bitcoin_wallet: Option>, + monero_wallet: Option>, + monero_rpc_process: Option>>, } #[allow(clippy::too_many_arguments)] @@ -216,7 +230,7 @@ impl Context { let monero_daemon_address = monero.apply_defaults(is_testnet); let (wlt, prc) = init_monero_wallet(data_dir.clone(), monero_daemon_address, env_config).await?; - (Some(Arc::new(wlt)), Some(prc)) + (Some(Arc::new(wlt)), Some(Arc::new(SyncMutex::new(prc)))) } else { (None, None) } @@ -228,7 +242,7 @@ impl Context { db: open_db(data_dir.join("sqlite")).await?, bitcoin_wallet, monero_wallet, - monero_rpc_process: monero_rpc_process.map(|prc| Arc::new(Mutex::new(prc))), + monero_rpc_process, config: Config { tor_socks5_port, namespace: XmrBtcNamespace::from_is_testnet(is_testnet), @@ -242,11 +256,18 @@ impl Context { }, swap_lock: Arc::new(SwapLock::new()), tasks: Arc::new(PendingTaskList::default()), + tauri_handle: None, }; Ok(context) } + pub fn with_tauri_handle(mut self, tauri_handle: AppHandle) -> Self { + self.tauri_handle = Some(TauriHandle::new(tauri_handle)); + + self + } + pub async fn for_harness( seed: Seed, env_config: EnvConfig, @@ -266,6 +287,7 @@ impl Context { monero_rpc_process: None, swap_lock: Arc::new(SwapLock::new()), tasks: Arc::new(PendingTaskList::default()), + tauri_handle: None, } } @@ -386,12 +408,6 @@ impl Config { #[cfg(test)] pub mod api_test { use super::*; - use crate::api::request::{Method, Request}; - - use libp2p::Multiaddr; - use request::BuyXmrArgs; - use std::str::FromStr; - use uuid::Uuid; pub const MULTI_ADDRESS: &str = "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi"; @@ -426,50 +442,4 @@ pub mod api_test { } } } - - impl Request { - pub fn buy_xmr(is_testnet: bool) -> Request { - let seller = Multiaddr::from_str(MULTI_ADDRESS).unwrap(); - let bitcoin_change_address = { - if is_testnet { - bitcoin::Address::from_str(BITCOIN_TESTNET_ADDRESS).unwrap() - } else { - bitcoin::Address::from_str(BITCOIN_MAINNET_ADDRESS).unwrap() - } - }; - - let monero_receive_address = { - if is_testnet { - monero::Address::from_str(MONERO_STAGENET_ADDRESS).unwrap() - } else { - monero::Address::from_str(MONERO_MAINNET_ADDRESS).unwrap() - } - }; - - Request::new(Method::BuyXmr(BuyXmrArgs { - seller, - bitcoin_change_address, - monero_receive_address, - swap_id: Uuid::new_v4(), - })) - } - - pub fn resume() -> Request { - Request::new(Method::Resume { - swap_id: Uuid::from_str(SWAP_ID).unwrap(), - }) - } - - pub fn cancel() -> Request { - Request::new(Method::CancelAndRefund { - swap_id: Uuid::from_str(SWAP_ID).unwrap(), - }) - } - - pub fn refund() -> Request { - Request::new(Method::CancelAndRefund { - swap_id: Uuid::from_str(SWAP_ID).unwrap(), - }) - } - } } diff --git a/swap/src/api/request.rs b/swap/src/api/request.rs index 93eaaebc..0d7a37b9 100644 --- a/swap/src/api/request.rs +++ b/swap/src/api/request.rs @@ -1,5 +1,7 @@ +use super::tauri_bindings::TauriHandle; +use crate::api::tauri_bindings::{TauriEmitter, TauriSwapProgressEvent}; use crate::api::Context; -use crate::bitcoin::{Amount, ExpiredTimelocks, TxLock}; +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}; @@ -10,11 +12,11 @@ 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 serde_json::Value as JsonValue; use std::cmp::min; use std::convert::TryInto; use std::future::Future; @@ -22,144 +24,334 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use tracing::Instrument; +use typeshare::typeshare; use uuid::Uuid; -#[derive(PartialEq, Debug)] -pub struct Request { - pub cmd: Method, - pub log_reference: Option, -} - -#[derive(Debug, Eq, PartialEq)] -pub struct BuyXmrArgs { - pub seller: Multiaddr, - pub bitcoin_change_address: bitcoin::Address, - pub monero_receive_address: monero::Address, - pub swap_id: Uuid, -} - -#[derive(Debug, Eq, PartialEq)] -pub struct ResumeArgs { - pub swap_id: Uuid, -} - -#[derive(Debug, Eq, PartialEq)] -pub struct CancelAndRefundArgs { - pub swap_id: Uuid, -} - -#[derive(Debug, Eq, PartialEq)] -pub struct MoneroRecoveryArgs { - pub swap_id: 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 WithdrawBtcArgs { - pub amount: Option, - pub address: bitcoin::Address, +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, } -#[derive(Debug, Eq, PartialEq)] -pub struct BalanceArgs { - pub force_refresh: bool, +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct BuyXmrResponse { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, + pub quote: BidQuote, } -#[derive(Debug, Eq, PartialEq)] -pub struct ListSellersArgs { - pub rendezvous_point: Multiaddr, +impl Request for BuyXmrArgs { + type Response = BuyXmrResponse; + + async fn request(self, ctx: Arc) -> Result { + buy_xmr(self, ctx).await + } } -#[derive(Debug, Eq, PartialEq)] -pub struct StartDaemonArgs { - pub server_address: Option, -} - -#[derive(Debug, Eq, PartialEq)] -pub struct GetSwapInfoArgs { +// 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, } -#[derive(Serialize, Deserialize, Debug)] -pub struct BalanceResponse { - pub balance: u64, // in satoshis +impl Request for ResumeSwapArgs { + type Response = ResumeSwapResponse; + + async fn request(self, ctx: Arc) -> Result { + resume_swap(self, ctx).await + } } -#[derive(Serialize, Deserialize, Debug)] -pub struct BuyXmrResponse { - pub swap_id: String, - pub quote: BidQuote, // You'll need to import or define BidQuote +// CancelAndRefund +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct CancelAndRefundArgs { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, } -#[derive(Serialize, Deserialize, Debug)] -pub struct GetHistoryResponse { - swaps: Vec<(Uuid, String)>, +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, - pub xmr_amount: u64, - pub btc_amount: u64, + #[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, - pub tx_cancel_fee: u64, - pub tx_refund_fee: u64, - pub tx_lock_fee: u64, + #[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: u32, - pub punish_timelock: u32, + 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 WithdrawBtcResponse { - amount: u64, - txid: String, +pub struct BalanceResponse { + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub balance: bitcoin::Amount, } -#[derive(Serialize, Deserialize)] +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 { - pub peer_id: String, - pub addresses: Vec, + #[typeshare(serialized_as = "string")] + pub peer_id: PeerId, + pub addresses: Vec, } -// TODO: We probably dont even need this. -// We can just call the method directly from the RPC server, the CLI and the Tauri connector -#[derive(Debug, PartialEq)] -pub enum Method { - BuyXmr(BuyXmrArgs), - Resume(ResumeArgs), - CancelAndRefund(CancelAndRefundArgs), - MoneroRecovery(MoneroRecoveryArgs), - History, - Config, - WithdrawBtc(WithdrawBtcArgs), - Balance(BalanceArgs), - ListSellers(ListSellersArgs), - ExportBitcoinWallet, - SuspendCurrentSwap, - StartDaemon(StartDaemonArgs), - GetCurrentSwap, - GetSwapInfo(GetSwapInfoArgs), - GetRawStates, +// 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 { +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(json!({ "swapId": id_value })) + Ok(SuspendCurrentSwapResponse { swap_id: id_value }) } else { bail!("No swap is currently running") } @@ -191,7 +383,7 @@ pub async fn get_swap_info( let state = context.db.get_state(args.swap_id).await?; let is_completed = state.swap_finished(); - let peerId = context + let peer_id = context .db .get_peer_id(args.swap_id) .await @@ -199,14 +391,13 @@ pub async fn get_swap_info( let addresses = context .db - .get_addresses(peerId) + .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 state_name = format!("{}", swap_state); let ( xmr_amount, @@ -226,15 +417,13 @@ pub async fn get_swap_info( .find_map(|state| { if let State::Bob(BobState::SwapSetupCompleted(state2)) = state { let xmr_amount = state2.xmr; - let btc_amount = state2.tx_lock.lock_amount().to_sat(); - let tx_cancel_fee = state2.tx_cancel_fee.to_sat(); - let tx_refund_fee = state2.tx_refund_fee.to_sat(); + 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() { - let tx_lock_fee = tx_lock_fee.to_sat(); - Some(( xmr_amount, btc_amount, @@ -255,7 +444,7 @@ pub async fn get_swap_info( }) .with_context(|| "Did not find SwapSetupCompleted state for swap")?; - let timelock = match swap_state { + let timelock = match swap_state.clone() { BobState::Started { .. } | BobState::SafelyAborted | BobState::SwapSetupCompleted(_) => { None } @@ -276,21 +465,21 @@ pub async fn get_swap_info( Ok(GetSwapInfoResponse { swap_id: args.swap_id, seller: Seller { - peer_id: peerId.to_string(), - addresses, + peer_id, + addresses: addresses.iter().map(|a| a.to_string()).collect(), }, completed: is_completed, start_date, - state_name, - xmr_amount: xmr_amount.as_piconero(), + 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: cancel_timelock.into(), - punish_timelock: punish_timelock.into(), + cancel_timelock, + punish_timelock, timelock, }) } @@ -299,13 +488,15 @@ pub async fn get_swap_info( pub async fn buy_xmr( buy_xmr: BuyXmrArgs, context: Arc, -) -> Result { +) -> Result { let BuyXmrArgs { seller, bitcoin_change_address, monero_receive_address, - swap_id, } = buy_xmr; + + let swap_id = Uuid::new_v4(); + let bitcoin_wallet = Arc::clone( context .bitcoin_wallet @@ -358,6 +549,9 @@ pub async fn buy_xmr( _ = 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 { @@ -382,16 +576,28 @@ pub async fn buy_xmr( .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 => { @@ -416,6 +622,7 @@ pub async fn buy_xmr( max_givable, || bitcoin_wallet.sync(), estimate_fee, + context.tauri_handle.clone(), ); let (amount, fees) = match determine_amount.await { @@ -442,7 +649,7 @@ pub async fn buy_xmr( monero_receive_address, bitcoin_change_address, amount, - ); + ).with_event_emitter(context.tauri_handle.clone()); bob::run(swap).await } => { @@ -463,18 +670,24 @@ pub async fn buy_xmr( .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(json!({ - "swapId": swap_id.to_string(), - "quote": bid_quote, - })) + Ok(BuyXmrResponse { + swap_id, + quote: bid_quote, + }) } #[tracing::instrument(fields(method = "resume_swap"), skip(context))] -pub async fn resume_swap(resume: ResumeArgs, context: Arc) -> Result { - let ResumeArgs { swap_id } = resume; +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?; @@ -531,7 +744,8 @@ pub async fn resume_swap(resume: ResumeArgs, context: Arc) -> Result) -> Result { 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"); }, @@ -571,13 +788,17 @@ pub async fn resume_swap(resume: ResumeArgs, context: Arc) -> Result(()) } .in_current_span(), ).await; - Ok(json!({ - "result": "ok", - })) + + Ok(ResumeSwapResponse { + result: "OK".to_string(), + }) } #[tracing::instrument(fields(method = "cancel_and_refund"), skip(context))] @@ -602,6 +823,10 @@ pub async fn cancel_and_refund( .await .expect("Could not release swap lock"); + context + .tauri_handle + .emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); + state.map(|state| { json!({ "result": state, @@ -612,10 +837,13 @@ pub async fn cancel_and_refund( #[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<(Uuid, String)> = Vec::new(); + let mut vec: Vec = Vec::new(); for (swap_id, state) in swaps { let state: BobState = state.try_into()?; - vec.push((swap_id, state.to_string())); + vec.push(GetHistoryEntry { + swap_id, + state: state.to_string(), + }) } Ok(GetHistoryResponse { swaps: vec }) @@ -659,7 +887,7 @@ pub async fn withdraw_btc( .context("Could not get Bitcoin wallet")?; let amount = match amount { - Some(amount) => Amount::from_sat(amount), + Some(amount) => amount, None => { bitcoin_wallet .max_giveable(address.script_pubkey().len()) @@ -677,7 +905,7 @@ pub async fn withdraw_btc( Ok(WithdrawBtcResponse { txid: signed_tx.txid().to_string(), - amount: amount.to_sat(), + amount, }) } @@ -728,7 +956,7 @@ pub async fn get_balance(balance: BalanceArgs, context: Arc) -> Result< } Ok(BalanceResponse { - balance: bitcoin_balance.to_sat(), + balance: bitcoin_balance, }) } @@ -838,26 +1066,6 @@ pub async fn get_current_swap(context: Arc) -> Result Request { - Request { - cmd, - log_reference: None, - } - } - - pub fn with_id(cmd: Method, id: Option) -> Request { - Request { - cmd, - log_reference: id, - } - } - - pub async fn call(self, _: Arc) -> Result { - unreachable!("This function should never be called") - } -} - fn qr_code(value: &impl ToString) -> Result { let code = QrCode::new(value.to_string())?; let qr_code = code @@ -868,6 +1076,7 @@ fn qr_code(value: &impl ToString) -> Result { Ok(qr_code) } +#[allow(clippy::too_many_arguments)] pub async fn determine_btc_to_swap( json: bool, bid_quote: BidQuote, @@ -876,18 +1085,19 @@ pub async fn determine_btc_to_swap( max_giveable_fn: FMG, sync: FS, estimate_fee: FFE, -) -> Result<(Amount, Amount)> + event_emitter: Option, +) -> Result<(bitcoin::Amount, bitcoin::Amount)> where - TB: Future>, + TB: Future>, FB: Fn() -> TB, - TMG: Future>, + TMG: Future>, FMG: Fn() -> TMG, TS: Future>, FS: Fn() -> TS, - FFE: Fn(Amount) -> TFE, - TFE: Future>, + FFE: Fn(bitcoin::Amount) -> TFE, + TFE: Future>, { - if bid_quote.max_quantity == Amount::ZERO { + if bid_quote.max_quantity == bitcoin::Amount::ZERO { bail!(ZeroQuoteReceived) } @@ -901,7 +1111,7 @@ where sync().await?; let mut max_giveable = max_giveable_fn().await?; - if max_giveable == Amount::ZERO || max_giveable < bid_quote.min_quantity { + 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; @@ -933,6 +1143,19 @@ where "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?; diff --git a/swap/src/api/tauri_bindings.rs b/swap/src/api/tauri_bindings.rs new file mode 100644 index 00000000..f705a3c5 --- /dev/null +++ b/swap/src/api/tauri_bindings.rs @@ -0,0 +1,141 @@ +/** + * TOOD: Perhaps we should move this to the `src-tauri` package. + */ +use anyhow::Result; +use bitcoin::Txid; +use serde::Serialize; +use std::sync::Arc; +use tauri::{AppHandle, Emitter}; +use typeshare::typeshare; +use uuid::Uuid; + +use crate::{monero, network::quote::BidQuote}; + +static SWAP_PROGRESS_EVENT_NAME: &str = "swap-progress-update"; + +#[derive(Debug, Clone)] +pub struct TauriHandle(Arc); + +impl TauriHandle { + pub fn new(tauri_handle: AppHandle) -> Self { + Self(Arc::new(tauri_handle)) + } + + pub fn emit_tauri_event(&self, event: &str, payload: S) -> Result<()> { + self.0.emit(event, payload).map_err(|e| e.into()) + } +} + +pub trait TauriEmitter { + 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( + SWAP_PROGRESS_EVENT_NAME, + TauriSwapProgressEventWrapper { swap_id, event }, + ); + } +} + +impl TauriEmitter for TauriHandle { + 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<()> { + match self { + Some(tauri) => tauri.emit_tauri_event(event, payload), + None => Ok(()), + } + } +} + +#[derive(Serialize, Clone)] +#[typeshare] +pub struct TauriSwapProgressEventWrapper { + #[typeshare(serialized_as = "string")] + swap_id: Uuid, + event: TauriSwapProgressEvent, +} + +#[derive(Serialize, Clone)] +#[serde(tag = "type", content = "content")] +#[typeshare] +pub enum TauriSwapProgressEvent { + Initiated, + ReceivedQuote(BidQuote), + WaitingForBtcDeposit { + #[typeshare(serialized_as = "string")] + deposit_address: bitcoin::Address, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + max_giveable: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + min_deposit_until_swap_will_start: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + max_deposit_until_maximum_amount_is_reached: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + min_bitcoin_lock_tx_fee: bitcoin::Amount, + quote: BidQuote, + }, + Started { + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + btc_lock_amount: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + btc_tx_lock_fee: bitcoin::Amount, + }, + BtcLockTxInMempool { + #[typeshare(serialized_as = "string")] + btc_lock_txid: bitcoin::Txid, + #[typeshare(serialized_as = "number")] + btc_lock_confirmations: u64, + }, + XmrLockTxInMempool { + #[typeshare(serialized_as = "string")] + xmr_lock_txid: monero::TxHash, + #[typeshare(serialized_as = "number")] + xmr_lock_tx_confirmations: u64, + }, + XmrLocked, + BtcRedeemed, + XmrRedeemInMempool { + #[typeshare(serialized_as = "string")] + xmr_redeem_txid: monero::TxHash, + #[typeshare(serialized_as = "string")] + xmr_redeem_address: monero::Address, + }, + BtcCancelled { + #[typeshare(serialized_as = "string")] + btc_cancel_txid: Txid, + }, + BtcRefunded { + #[typeshare(serialized_as = "string")] + btc_refund_txid: Txid, + }, + BtcPunished, + AttemptingCooperativeRedeem, + CooperativeRedeemAccepted, + CooperativeRedeemRejected { + reason: String, + }, + Released, +} diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 82d9be73..0db97f6c 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -11,10 +11,10 @@ #![forbid(unsafe_code)] #![allow(non_snake_case)] -use swap::cli::command::{parse_args_and_apply_defaults, ParseResult}; -use swap::common::check_latest_version; use anyhow::Result; use std::env; +use swap::cli::command::{parse_args_and_apply_defaults, ParseResult}; +use swap::common::check_latest_version; #[tokio::main] pub async fn main() -> Result<()> { @@ -23,7 +23,9 @@ pub async fn main() -> Result<()> { } match parse_args_and_apply_defaults(env::args_os()).await? { - ParseResult::Success => {} + ParseResult::Success(context) => { + context.tasks.wait_for_tasks().await?; + } ParseResult::PrintAndExitZero { message } => { println!("{}", message); std::process::exit(0); diff --git a/swap/src/bitcoin/cancel.rs b/swap/src/bitcoin/cancel.rs index 34612148..cde8e210 100644 --- a/swap/src/bitcoin/cancel.rs +++ b/swap/src/bitcoin/cancel.rs @@ -16,6 +16,7 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::fmt; use std::ops::Add; +use typeshare::typeshare; /// Represent a timelock, expressed in relative block height as defined in /// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki). @@ -23,6 +24,7 @@ use std::ops::Add; /// mined. #[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(transparent)] +#[typeshare] pub struct CancelTimelock(u32); impl From for u32 { @@ -69,6 +71,7 @@ impl fmt::Display for CancelTimelock { /// mined. #[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(transparent)] +#[typeshare] pub struct PunishTimelock(u32); impl From for u32 { diff --git a/swap/src/bitcoin/timelocks.rs b/swap/src/bitcoin/timelocks.rs index 427bef80..62e11e92 100644 --- a/swap/src/bitcoin/timelocks.rs +++ b/swap/src/bitcoin/timelocks.rs @@ -3,6 +3,7 @@ use bdk::electrum_client::HeaderNotification; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use std::ops::Add; +use typeshare::typeshare; /// Represent a block height, or block number, expressed in absolute block /// count. E.g. The transaction was included in block #655123, 655123 block @@ -37,7 +38,9 @@ impl Add for BlockHeight { } } -#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)] +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[serde(tag = "type", content = "content")] pub enum ExpiredTimelocks { None { blocks_left: u32 }, Cancel { blocks_left: u32 }, diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index b00e65e7..fa3740f7 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -1,8 +1,8 @@ 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, ListSellersArgs, MoneroRecoveryArgs, ResumeArgs, - StartDaemonArgs, WithdrawBtcArgs, + BuyXmrArgs, CancelAndRefundArgs, ExportBitcoinWalletArgs, GetConfigArgs, GetHistoryArgs, + ListSellersArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, StartDaemonArgs, WithdrawBtcArgs, }; use crate::api::Context; use crate::bitcoin::{bitcoin_address, Amount}; @@ -38,7 +38,7 @@ const DEFAULT_TOR_SOCKS5_PORT: &str = "9050"; #[derive(Debug)] pub enum ParseResult { /// The arguments we were invoked in. - Success, + Success(Arc), /// A flag or command was given that does not need further processing other /// than printing the provided message. /// @@ -65,7 +65,7 @@ where let json = args.json; let is_testnet = args.testnet; let data = args.data; - let result = match args.cmd { + let result: Result> = match args.cmd { CliCommand::BuyXmr { seller: Seller { seller }, bitcoin, @@ -93,36 +93,33 @@ where .await?, ); - buy_xmr( - BuyXmrArgs { - seller, - bitcoin_change_address, - monero_receive_address, - swap_id: Uuid::new_v4(), - }, - context, - ) + BuyXmrArgs { + seller, + bitcoin_change_address, + monero_receive_address, + } + .request(context.clone()) .await?; - Ok(()) as Result<(), anyhow::Error> + Ok(context) } CliCommand::History => { let context = Arc::new( Context::build(None, None, None, data, is_testnet, debug, json, None).await?, ); - get_history(context).await?; + GetHistoryArgs {}.request(context.clone()).await?; - Ok(()) + Ok(context) } CliCommand::Config => { let context = Arc::new( Context::build(None, None, None, data, is_testnet, debug, json, None).await?, ); - get_config(context).await?; + GetConfigArgs {}.request(context.clone()).await?; - Ok(()) + Ok(context) } CliCommand::Balance { bitcoin } => { let context = Arc::new( @@ -139,15 +136,13 @@ where .await?, ); - get_balance( - BalanceArgs { - force_refresh: true, - }, - context, - ) + BalanceArgs { + force_refresh: true, + } + .request(context.clone()) .await?; - Ok(()) + Ok(context) } CliCommand::StartDaemon { server_address, @@ -155,21 +150,25 @@ where monero, tor, } => { - let context = Context::build( - Some(bitcoin), - Some(monero), - Some(tor), - data, - is_testnet, - debug, - json, - server_address, - ) - .await?; + let context = Arc::new( + Context::build( + Some(bitcoin), + Some(monero), + Some(tor), + data, + is_testnet, + debug, + json, + server_address, + ) + .await?, + ); - start_daemon(StartDaemonArgs { server_address }, context).await?; + StartDaemonArgs { server_address } + .request(context.clone()) + .await?; - Ok(()) + Ok(context) } CliCommand::WithdrawBtc { bitcoin, @@ -192,16 +191,11 @@ where .await?, ); - withdraw_btc( - WithdrawBtcArgs { - amount: amount.map(Amount::to_sat), - address, - }, - context, - ) - .await?; + WithdrawBtcArgs { amount, address } + .request(context.clone()) + .await?; - Ok(()) + Ok(context) } CliCommand::Resume { swap_id: SwapId { swap_id }, @@ -223,9 +217,9 @@ where .await?, ); - resume_swap(ResumeArgs { swap_id }, context).await?; + ResumeSwapArgs { swap_id }.request(context.clone()).await?; - Ok(()) + Ok(context) } CliCommand::CancelAndRefund { swap_id: SwapId { swap_id }, @@ -246,9 +240,11 @@ where .await?, ); - cancel_and_refund(CancelAndRefundArgs { swap_id }, context).await?; + CancelAndRefundArgs { swap_id } + .request(context.clone()) + .await?; - Ok(()) + Ok(context) } CliCommand::ListSellers { rendezvous_point, @@ -258,9 +254,11 @@ where Context::build(None, None, Some(tor), data, is_testnet, debug, json, None).await?, ); - list_sellers(ListSellersArgs { rendezvous_point }, context).await?; + ListSellersArgs { rendezvous_point } + .request(context.clone()) + .await?; - Ok(()) + Ok(context) } CliCommand::ExportBitcoinWallet { bitcoin } => { let context = Arc::new( @@ -277,9 +275,9 @@ where .await?, ); - export_bitcoin_wallet(context).await?; + ExportBitcoinWalletArgs {}.request(context.clone()).await?; - Ok(()) + Ok(context) } CliCommand::MoneroRecovery { swap_id: SwapId { swap_id }, @@ -288,15 +286,15 @@ where Context::build(None, None, None, data, is_testnet, debug, json, None).await?, ); - monero_recovery(MoneroRecoveryArgs { swap_id }, context).await?; + MoneroRecoveryArgs { swap_id } + .request(context.clone()) + .await?; - Ok(()) + Ok(context) } }; - result?; - - Ok(ParseResult::Success) + Ok(ParseResult::Success(result?)) } #[derive(structopt::StructOpt, Debug)] @@ -1067,18 +1065,14 @@ mod tests { let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); let (is_testnet, debug, json) = (true, true, false); - let (expected_config, expected_request) = ( - Config::default(is_testnet, None, debug, json), - Request::resume(), - ); + let expected_config = Config::default(is_testnet, None, debug, json); - let (actual_config, actual_request) = match args { - ParseResult::Context(context, request) => (context.config.clone(), request), + let actual_config = match args { + ParseResult::Context(context, request) => context.config.clone(), _ => panic!("Couldn't parse result"), }; assert_eq!(actual_config, expected_config); - assert_eq!(actual_request, Box::new(expected_request)); // given_buy_xmr_on_mainnet_with_json_then_json_set let raw_ars = vec![ diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 8205e75f..c02bf5cb 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -4,6 +4,7 @@ mod wallet_rpc; pub use ::monero::network::Network; pub use ::monero::{Address, PrivateKey, PublicKey}; pub use curve25519_dalek::scalar::Scalar; +use typeshare::typeshare; pub use wallet::Wallet; pub use wallet_rpc::{WalletRpc, WalletRpcProcess}; @@ -86,6 +87,7 @@ impl From for PublicKey { pub struct PublicViewKey(PublicKey); #[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd)] +#[typeshare(serialized_as = "number")] pub struct Amount(u64); // Median tx fees on Monero as found here: https://www.monero.how/monero-transaction-fees, XMR 0.000_008 * 2 (to be on the safe side) diff --git a/swap/src/network/quote.rs b/swap/src/network/quote.rs index caf99e09..d5eccf0f 100644 --- a/swap/src/network/quote.rs +++ b/swap/src/network/quote.rs @@ -7,6 +7,7 @@ use libp2p::request_response::{ }; use libp2p::PeerId; use serde::{Deserialize, Serialize}; +use typeshare::typeshare; const PROTOCOL: &str = "/comit/xmr/btc/bid-quote/1.0.0"; pub type OutEvent = RequestResponseEvent<(), BidQuote>; @@ -25,15 +26,20 @@ impl ProtocolName for BidQuoteProtocol { /// Represents a quote for buying XMR. #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[typeshare] pub struct BidQuote { /// The price at which the maker is willing to buy at. #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + #[typeshare(serialized_as = "number")] pub price: bitcoin::Amount, /// The minimum quantity the maker is willing to buy. + /// #[typeshare(serialized_as = "number")] #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + #[typeshare(serialized_as = "number")] pub min_quantity: bitcoin::Amount, /// The maximum quantity the maker is willing to buy. #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + #[typeshare(serialized_as = "number")] pub max_quantity: bitcoin::Amount, } diff --git a/swap/src/protocol/bob.rs b/swap/src/protocol/bob.rs index 0ef3e241..d9d90ac9 100644 --- a/swap/src/protocol/bob.rs +++ b/swap/src/protocol/bob.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::Result; use uuid::Uuid; +use crate::api::tauri_bindings::TauriHandle; use crate::protocol::Database; use crate::{bitcoin, cli, env, monero}; @@ -22,6 +23,7 @@ pub struct Swap { pub env_config: env::Config, pub id: Uuid, pub monero_receive_address: monero::Address, + pub event_emitter: Option, } impl Swap { @@ -49,6 +51,7 @@ impl Swap { env_config, id, monero_receive_address, + event_emitter: None, } } @@ -73,6 +76,12 @@ impl Swap { env_config, id, monero_receive_address, + event_emitter: None, }) } + + pub fn with_event_emitter(mut self, event_emitter: Option) -> Self { + self.event_emitter = event_emitter; + self + } } diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 5db98690..4551414e 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -1,3 +1,4 @@ +use crate::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}; @@ -48,6 +49,7 @@ pub async fn run_until( swap.bitcoin_wallet.as_ref(), swap.monero_wallet.as_ref(), swap.monero_receive_address, + swap.event_emitter.clone(), ) .await?; @@ -73,6 +75,7 @@ async fn next_state( bitcoin_wallet: &bitcoin::Wallet, monero_wallet: &monero::Wallet, monero_receive_address: monero::Address, + event_emitter: Option, ) -> Result { tracing::debug!(%state, "Advancing state"); @@ -120,6 +123,16 @@ async fn next_state( .sign_and_finalize(tx_lock.clone().into()) .await .context("Failed to sign Bitcoin lock transaction")?; + + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::Started { + btc_lock_amount: tx_lock.lock_amount(), + // TODO: Replace this with the actual fee + btc_tx_lock_fee: bitcoin::Amount::ZERO, + }, + ); + let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?; BobState::BtcLocked { @@ -133,6 +146,15 @@ async fn next_state( state3, monero_wallet_restore_blockheight, } => { + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::BtcLockTxInMempool { + btc_lock_txid: state3.tx_lock_id(), + // TODO: Replace this with the actual confirmations + btc_lock_confirmations: 0, + }, + ); + let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; if let ExpiredTimelocks::None { .. } = state3.expired_timelock(bitcoin_wallet).await? { @@ -188,6 +210,14 @@ async fn next_state( lock_transfer_proof, monero_wallet_restore_blockheight, } => { + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::XmrLockTxInMempool { + xmr_lock_txid: lock_transfer_proof.tx_hash(), + xmr_lock_tx_confirmations: 0, + }, + ); + let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await; if let ExpiredTimelocks::None { .. } = state.expired_timelock(bitcoin_wallet).await? { @@ -207,6 +237,7 @@ async fn next_state( }, } } + // TODO: Send Tauri event here everytime we receive a new confirmation result = tx_lock_status.wait_until_confirmed_with(state.cancel_timelock) => { result?; BobState::CancelTimelockExpired(state.cancel(monero_wallet_restore_blockheight)) @@ -217,6 +248,8 @@ async fn next_state( } } BobState::XmrLocked(state) => { + event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::XmrLocked); + // In case we send the encrypted signature to Alice, but she doesn't give us a confirmation // We need to check if she still published the Bitcoin redeem transaction // Otherwise we risk staying stuck in "XmrLocked" @@ -272,10 +305,21 @@ async fn next_state( } } BobState::BtcRedeemed(state) => { + event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::BtcRedeemed); + state .redeem_xmr(monero_wallet, swap_id.to_string(), monero_receive_address) .await?; + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::XmrRedeemInMempool { + // TODO: Replace this with the actual txid + xmr_redeem_txid: monero::TxHash("placeholder".to_string()), + xmr_redeem_address: monero_receive_address, + }, + ); + BobState::XmrRedeemed { tx_lock_id: state.tx_lock_id(), } @@ -288,6 +332,13 @@ async fn next_state( BobState::BtcCancelled(state4) } BobState::BtcCancelled(state) => { + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::BtcCancelled { + btc_cancel_txid: state.construct_tx_cancel()?.txid(), + }, + ); + // Bob has cancelled the swap match state.expired_timelock(bitcoin_wallet).await? { ExpiredTimelocks::None { .. } => { @@ -308,8 +359,24 @@ async fn next_state( } } } - BobState::BtcRefunded(state4) => BobState::BtcRefunded(state4), + BobState::BtcRefunded(state4) => { + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::BtcRefunded { + btc_refund_txid: state4.signed_refund_transaction()?.txid(), + }, + ); + + BobState::BtcRefunded(state4) + } BobState::BtcPunished { state, tx_lock_id } => { + event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::BtcPunished); + + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::AttemptingCooperativeRedeem, + ); + tracing::info!("Attempting to cooperatively redeem XMR after being punished"); let response = event_loop_handle .request_cooperative_xmr_redeem(swap_id) @@ -321,7 +388,13 @@ async fn next_state( "Alice has accepted our request to cooperatively redeem the XMR" ); + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::CooperativeRedeemAccepted, + ); + let s_a = monero::PrivateKey { scalar: s_a }; + let state5 = state.attempt_cooperative_redeem(s_a); match state5 @@ -329,33 +402,78 @@ async fn next_state( .await { Ok(_) => { + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::XmrRedeemInMempool { + xmr_redeem_txid: monero::TxHash("placeholder".to_string()), + xmr_redeem_address: monero_receive_address, + }, + ); + return Ok(BobState::XmrRedeemed { tx_lock_id }); } Err(error) => { - return Err(error) - .context("Failed to redeem XMR with revealed XMR key"); + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::CooperativeRedeemRejected { + reason: error.to_string(), + }, + ); + + let err: std::result::Result<_, anyhow::Error> = + Err(error).context("Failed to redeem XMR with revealed XMR key"); + + return err; } } } Ok(Rejected { reason, .. }) => { + let err = Err(reason.clone()) + .context("Alice rejected our request for cooperative XMR redeem"); + + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::CooperativeRedeemRejected { + reason: reason.to_string(), + }, + ); + tracing::error!( ?reason, "Alice rejected our request for cooperative XMR redeem" ); - return Err(reason) - .context("Alice rejected our request for cooperative XMR redeem"); + + return err; } Err(error) => { tracing::error!( ?error, "Failed to request cooperative XMR redeem from Alice" ); + + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::CooperativeRedeemRejected { + reason: error.to_string(), + }, + ); + return Err(error) .context("Failed to request cooperative XMR redeem from Alice"); } }; } BobState::SafelyAborted => BobState::SafelyAborted, - BobState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id }, + BobState::XmrRedeemed { tx_lock_id } => { + // TODO: Replace this with the actual txid + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::XmrRedeemInMempool { + xmr_redeem_txid: monero::TxHash("placeholder".to_string()), + xmr_redeem_address: monero_receive_address, + }, + ); + BobState::XmrRedeemed { tx_lock_id } + } }) } diff --git a/swap/src/rpc/methods.rs b/swap/src/rpc/methods.rs index d50e029a..ca707290 100644 --- a/swap/src/rpc/methods.rs +++ b/swap/src/rpc/methods.rs @@ -1,8 +1,8 @@ use crate::api::request::{ - buy_xmr, cancel_and_refund, get_balance, get_current_swap, get_history, get_raw_states, - get_swap_info, list_sellers, monero_recovery, resume_swap, suspend_current_swap, withdraw_btc, - BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, GetSwapInfoArgs, ListSellersArgs, Method, - MoneroRecoveryArgs, ResumeArgs, WithdrawBtcArgs, + 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::bitcoin::bitcoin_address; @@ -42,7 +42,8 @@ pub fn register_modules(outer_context: Context) -> Result> { let swap_id = as_uuid(swap_id) .ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?; - get_swap_info(GetSwapInfoArgs { swap_id }, context) + GetSwapInfoArgs { swap_id } + .request(context) .await .to_jsonrpsee_result() })?; @@ -60,7 +61,8 @@ pub fn register_modules(outer_context: Context) -> Result> { jsonrpsee_core::Error::Custom("force_refesh is not a boolean".to_string()) })?; - get_balance(BalanceArgs { force_refresh }, context) + BalanceArgs { force_refresh } + .request(context) .await .to_jsonrpsee_result() })?; @@ -83,7 +85,8 @@ pub fn register_modules(outer_context: Context) -> Result> { let swap_id = as_uuid(swap_id) .ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?; - resume_swap(ResumeArgs { swap_id }, context) + ResumeSwapArgs { swap_id } + .request(context) .await .to_jsonrpsee_result() })?; @@ -98,7 +101,8 @@ pub fn register_modules(outer_context: Context) -> Result> { let swap_id = as_uuid(swap_id) .ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?; - cancel_and_refund(CancelAndRefundArgs { swap_id }, context) + CancelAndRefundArgs { swap_id } + .request(context) .await .to_jsonrpsee_result() })?; @@ -116,7 +120,8 @@ pub fn register_modules(outer_context: Context) -> Result> { jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()) })?; - monero_recovery(MoneroRecoveryArgs { swap_id }, context) + MoneroRecoveryArgs { swap_id } + .request(context) .await .to_jsonrpsee_result() }, @@ -130,8 +135,7 @@ pub fn register_modules(outer_context: Context) -> Result> { ::bitcoin::Amount::from_str_in(amount_str, ::bitcoin::Denomination::Bitcoin) .map_err(|_| { jsonrpsee_core::Error::Custom("Unable to parse amount".to_string()) - })? - .to_sat(), + })?, ) } else { None @@ -145,13 +149,11 @@ pub fn register_modules(outer_context: Context) -> Result> { let withdraw_address = bitcoin_address::validate(withdraw_address, context.config.env_config.bitcoin_network)?; - withdraw_btc( - WithdrawBtcArgs { - amount, - address: withdraw_address, - }, - context, - ) + WithdrawBtcArgs { + amount, + address: withdraw_address, + } + .request(context) .await .to_jsonrpsee_result() })?; @@ -187,15 +189,12 @@ pub fn register_modules(outer_context: Context) -> Result> { })?) .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; - buy_xmr( - BuyXmrArgs { - seller, - bitcoin_change_address, - monero_receive_address, - swap_id: Uuid::new_v4(), - }, - context, - ) + BuyXmrArgs { + seller, + bitcoin_change_address, + monero_receive_address, + } + .request(context) .await .to_jsonrpsee_result() })?; @@ -214,12 +213,10 @@ pub fn register_modules(outer_context: Context) -> Result> { jsonrpsee_core::Error::Custom("Could not parse valid multiaddr".to_string()) })?; - list_sellers( - ListSellersArgs { - rendezvous_point: rendezvous_point.clone(), - }, - context, - ) + ListSellersArgs { + rendezvous_point: rendezvous_point.clone(), + } + .request(context) .await .to_jsonrpsee_result() })?; diff --git a/swap/tests/rpc.rs b/swap/tests/rpc.rs index 5dc640d4..1c92b3cc 100644 --- a/swap/tests/rpc.rs +++ b/swap/tests/rpc.rs @@ -15,7 +15,7 @@ mod test { use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; - use swap::api::request::{Method, Request}; + use swap::api::request::{start_daemon, StartDaemonArgs}; use swap::api::Context; use crate::harness::alice_run_until::is_xmr_lock_transaction_sent; @@ -39,18 +39,14 @@ mod test { harness_ctx: TestContext, ) -> (Client, MakeCapturingWriter, Arc) { let writer = capture_logs(LevelFilter::DEBUG); - let server_address: SocketAddr = SERVER_ADDRESS.parse().unwrap(); - - let request = Request::new(Method::StartDaemon { - server_address: Some(server_address), - }); + let server_address: Option = SERVER_ADDRESS.parse().unwrap().into(); let context = Arc::new(harness_ctx.get_bob_context().await); let context_clone = context.clone(); tokio::spawn(async move { - if let Err(err) = request.call(context_clone).await { + if let Err(err) = start_daemon(StartDaemonArgs { server_address }, context).await { println!("Failed to initialize daemon for testing: {}", err); } });