From 472e3a57b3a1cd93809adff1285a3f5f80ec6860 Mon Sep 17 00:00:00 2001 From: binarybaron Date: Wed, 7 Aug 2024 22:51:18 +0200 Subject: [PATCH] wip: refactor request.rs to allow type safety --- .zed/settings.json | 15 + src-xmr-btc-swap/Cargo.lock | 2 + src-xmr-btc-swap/src-tauri/Cargo.toml | 22 +- src-xmr-btc-swap/src-tauri/src/lib.rs | 26 +- src-xmr-btc-swap/swap/Cargo.toml | 77 +- src-xmr-btc-swap/swap/src/api.rs | 5 +- src-xmr-btc-swap/swap/src/api/request.rs | 1403 +++++++++-------- src-xmr-btc-swap/swap/src/bin/swap.rs | 5 +- src-xmr-btc-swap/swap/src/cli/command.rs | 25 +- src-xmr-btc-swap/swap/src/lib.rs | 4 +- .../swap/src/protocol/bob/swap.rs | 10 +- src-xmr-btc-swap/swap/src/rpc.rs | 15 +- src-xmr-btc-swap/swap/src/rpc/methods.rs | 47 +- .../components/modal/swap/pages/DebugPage.tsx | 18 +- src/renderer/components/other/Units.tsx | 10 +- src/renderer/rpc.ts | 14 +- 16 files changed, 955 insertions(+), 743 deletions(-) create mode 100644 .zed/settings.json diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000..f4d65165 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,15 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings +{ + "lsp": { + "rust-analyzer": { + "initialization_options": { + "rust": { + "analyzerTargetDir": "./src-xmr-btc-swap" + } + } + } + } +} diff --git a/src-xmr-btc-swap/Cargo.lock b/src-xmr-btc-swap/Cargo.lock index 3e14ccb9..6cde2b39 100644 --- a/src-xmr-btc-swap/Cargo.lock +++ b/src-xmr-btc-swap/Cargo.lock @@ -6376,6 +6376,7 @@ dependencies = [ "directories-next", "ecdsa_fun", "ed25519-dalek", + "erased-serde", "futures", "get-port", "hex", @@ -7629,6 +7630,7 @@ dependencies = [ name = "unstoppableswap-gui-rs" version = "0.0.0" dependencies = [ + "anyhow", "once_cell", "serde", "serde_json", diff --git a/src-xmr-btc-swap/src-tauri/Cargo.toml b/src-xmr-btc-swap/src-tauri/Cargo.toml index 82095041..754294e4 100644 --- a/src-xmr-btc-swap/src-tauri/Cargo.toml +++ b/src-xmr-btc-swap/src-tauri/Cargo.toml @@ -1,24 +1,24 @@ [package] name = "unstoppableswap-gui-rs" version = "0.0.0" -description = "A Tauri App" -authors = ["you"] +authors = [ "you" ] edition = "2021" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +description = "A Tauri App" + [lib] name = "unstoppableswap_gui_rs_lib" -crate-type = ["lib", "cdylib", "staticlib"] +crate-type = [ "lib", "cdylib", "staticlib" ] [build-dependencies] -tauri-build = { version = "2.0.0-beta", features = [] } +tauri-build = { version = "2.0.0-beta", features = [ ] } [dependencies] -tauri = { version = "2.0.0-beta", features = [] } -tauri-plugin-shell = "2.0.0-beta" -serde = { version = "1", features = ["derive"] } -serde_json = "1" +anyhow = "1" once_cell = "1" -swap = { path= "../swap" } - +serde = { version = "1", features = [ "derive" ] } +serde_json = "1" +swap = { path = "../swap" } +tauri = { version = "2.0.0-beta", features = [ ] } +tauri-plugin-shell = "2.0.0-beta" diff --git a/src-xmr-btc-swap/src-tauri/src/lib.rs b/src-xmr-btc-swap/src-tauri/src/lib.rs index 0f20e4df..c4102031 100644 --- a/src-xmr-btc-swap/src-tauri/src/lib.rs +++ b/src-xmr-btc-swap/src-tauri/src/lib.rs @@ -1,17 +1,29 @@ use std::sync::Arc; use once_cell::sync::OnceCell; -use swap::{api::{request::{Method, Request}, Context}, cli::command::{Bitcoin, Monero}}; +use swap::{ + api::{ + request::{get_balance, BalanceArgs, BalanceResponse}, + Context, + }, + cli::command::{Bitcoin, Monero}, +}; // Lazy load the Context static CONTEXT: OnceCell> = OnceCell::new(); #[tauri::command] -async fn balance() -> String { +async fn balance() -> Result { let context = CONTEXT.get().unwrap(); - let request = Request::new(Method::Balance { force_refresh: true }); - let response = request.call(context.clone()).await.unwrap(); - response.to_string() + + get_balance( + BalanceArgs { + force_refresh: true, + }, + context.clone(), + ) + .await + .map_err(|e| e.to_string()) } fn setup<'a>(app: &'a mut tauri::App) -> Result<(), Box> { @@ -34,7 +46,9 @@ fn setup<'a>(app: &'a mut tauri::App) -> Result<(), Box> .await .unwrap(); - CONTEXT.set(Arc::new(context)).expect("Failed to initialize cli context"); + CONTEXT + .set(Arc::new(context)) + .expect("Failed to initialize cli context"); }); Ok(()) } diff --git a/src-xmr-btc-swap/swap/Cargo.toml b/src-xmr-btc-swap/swap/Cargo.toml index 7e7968ff..f30af3a5 100644 --- a/src-xmr-btc-swap/swap/Cargo.toml +++ b/src-xmr-btc-swap/swap/Cargo.toml @@ -27,17 +27,31 @@ data-encoding = "2.6" dialoguer = "0.11" digest = "0.10.7" directories-next = "2" -ecdsa_fun = { version = "0.10", default-features = false, features = [ "libsecp_compat", "serde", "adaptor" ] } +ecdsa_fun = { version = "0.10", default-features = false, features = [ + "libsecp_compat", + "serde", + "adaptor", +] } ed25519-dalek = "1" +erased-serde = "0.4.5" futures = { version = "0.3", default-features = false } hex = "0.4" +hyper = "0.14.20" itertools = "0.13" jsonrpsee = { version = "0.16.2", features = [ "server" ] } -tower-http = { version = "0.3.4", features = ["full"] } -tower = { version = "0.4.13", features = ["full"] } -hyper = "0.14.20" jsonrpsee-core = "0.16.2" -libp2p = { version = "0.42.2", default-features = false, features = [ "tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response", "websocket", "ping", "rendezvous", "identify" ] } +libp2p = { version = "0.42.2", default-features = false, features = [ + "tcp-tokio", + "yamux", + "mplex", + "dns-tokio", + "noise", + "request-response", + "websocket", + "ping", + "rendezvous", + "identify", +] } monero = { version = "0.12", features = [ "serde_support" ] } monero-rpc = { path = "../monero-rpc" } pem = "3.0" @@ -45,7 +59,12 @@ proptest = "1" qrcode = "0.14" rand = "0.8" rand_chacha = "0.3" -reqwest = { version = "0.12", features = [ "http2", "rustls-tls", "stream", "socks" ], default-features = false } +reqwest = { version = "0.12", features = [ + "http2", + "rustls-tls", + "stream", + "socks", +], default-features = false } rust_decimal = { version = "1", features = [ "serde-float" ] } rust_decimal_macros = "1" serde = { version = "1", features = [ "derive" ] } @@ -53,22 +72,52 @@ serde_cbor = "0.11" serde_json = "1" serde_with = { version = "1", features = [ "macros" ] } sha2 = "0.10" -sigma_fun = { version = "0.7", default-features = false, features = [ "ed25519", "serde", "secp256k1", "alloc" ] } -sqlx = { version = "0.6.3", features = [ "sqlite", "runtime-tokio-rustls", "offline" ] } +sigma_fun = { version = "0.7", default-features = false, features = [ + "ed25519", + "serde", + "secp256k1", + "alloc", +] } +sqlx = { version = "0.6.3", features = [ + "sqlite", + "runtime-tokio-rustls", + "offline", +] } structopt = "0.3" strum = { version = "0.26", features = [ "derive" ] } thiserror = "1" time = "0.3" -tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs", "net", "parking_lot" ] } +tokio = { version = "1", features = [ + "rt-multi-thread", + "time", + "macros", + "sync", + "process", + "fs", + "net", + "parking_lot", +] } tokio-socks = "0.5" tokio-tungstenite = { version = "0.15", features = [ "rustls-tls" ] } tokio-util = { version = "0.7", features = [ "io", "codec" ] } toml = "0.8" -torut = { version = "0.2", default-features = false, features = [ "v3", "control" ] } +torut = { version = "0.2", default-features = false, features = [ + "v3", + "control", +] } +tower = { version = "0.4.13", features = [ "full" ] } +tower-http = { version = "0.3.4", features = [ "full" ] } tracing = { version = "0.1", features = [ "attributes" ] } tracing-appender = "0.2" tracing-futures = { version = "0.2", features = [ "std-future", "futures-03" ] } -tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", "ansi", "env-filter", "time", "tracing-log", "json" ] } +tracing-subscriber = { version = "0.3", default-features = false, features = [ + "fmt", + "ansi", + "env-filter", + "time", + "tracing-log", + "json", +] } url = { version = "2", features = [ "serde" ] } uuid = { version = "1.9", features = [ "serde", "v4" ] } void = "1" @@ -96,4 +145,8 @@ testcontainers = "0.15" [build-dependencies] anyhow = "1" -vergen = { version = "8.3", default-features = false, features = [ "build", "git", "git2" ] } +vergen = { version = "8.3", default-features = false, features = [ + "build", + "git", + "git2", +] } diff --git a/src-xmr-btc-swap/swap/src/api.rs b/src-xmr-btc-swap/swap/src/api.rs index 340c2e39..06fdca91 100644 --- a/src-xmr-btc-swap/swap/src/api.rs +++ b/src-xmr-btc-swap/swap/src/api.rs @@ -375,6 +375,7 @@ pub mod api_test { use crate::api::request::{Method, Request}; use libp2p::Multiaddr; + use request::BuyXmrArgs; use std::str::FromStr; use uuid::Uuid; @@ -431,12 +432,12 @@ pub mod api_test { } }; - Request::new(Method::BuyXmr { + Request::new(Method::BuyXmr(BuyXmrArgs { seller, bitcoin_change_address, monero_receive_address, swap_id: Uuid::new_v4(), - }) + })) } pub fn resume() -> Request { diff --git a/src-xmr-btc-swap/swap/src/api/request.rs b/src-xmr-btc-swap/swap/src/api/request.rs index 50274ab1..41c8806d 100644 --- a/src-xmr-btc-swap/swap/src/api/request.rs +++ b/src-xmr-btc-swap/swap/src/api/request.rs @@ -1,6 +1,6 @@ use crate::api::Context; use crate::bitcoin::{Amount, ExpiredTimelocks, TxLock}; -use crate::cli::{list_sellers, EventLoop, SellerStatus}; +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; @@ -11,7 +11,9 @@ use anyhow::{bail, Context as AnyContext, Result}; use libp2p::core::Multiaddr; 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; @@ -27,44 +29,87 @@ pub struct Request { 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, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ResumeSwapResponse { + pub result: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct BalanceResponse { + pub balance: u64, // in satoshis +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct BuyXmrResponse { + pub swap_id: String, + pub quote: BidQuote, // You'll need to import or define BidQuote +} + +#[derive(Debug, Eq, PartialEq)] +pub struct WithdrawBtcArgs { + pub amount: Option, + pub address: bitcoin::Address, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct BalanceArgs { + pub force_refresh: bool, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct ListSellersArgs { + pub rendezvous_point: Multiaddr, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct StartDaemonArgs { + pub server_address: Option, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct GetSwapInfoArgs { + pub swap_id: Uuid, +} + #[derive(Debug, PartialEq)] pub enum Method { - BuyXmr { - seller: Multiaddr, - bitcoin_change_address: bitcoin::Address, - monero_receive_address: monero::Address, - swap_id: Uuid, - }, - Resume { - swap_id: Uuid, - }, - CancelAndRefund { - swap_id: Uuid, - }, - MoneroRecovery { - swap_id: Uuid, - }, + BuyXmr(BuyXmrArgs), + Resume(ResumeArgs), + CancelAndRefund(CancelAndRefundArgs), + MoneroRecovery(MoneroRecoveryArgs), History, Config, - WithdrawBtc { - amount: Option, - address: bitcoin::Address, - }, - Balance { - force_refresh: bool, - }, - ListSellers { - rendezvous_point: Multiaddr, - }, + WithdrawBtc(WithdrawBtcArgs), + Balance(BalanceArgs), + ListSellers(ListSellersArgs), ExportBitcoinWallet, SuspendCurrentSwap, - StartDaemon { - server_address: Option, - }, + StartDaemon(StartDaemonArgs), GetCurrentSwap, - GetSwapInfo { - swap_id: Uuid, - }, + GetSwapInfo(GetSwapInfoArgs), GetRawStates, } @@ -78,13 +123,13 @@ impl Method { log_reference_id = field::Empty ) } - Method::BuyXmr { swap_id, .. } => { + Method::BuyXmr(BuyXmrArgs { swap_id, .. }) => { debug_span!("method", method_name="BuyXmr", swap_id=%swap_id, log_reference_id=field::Empty) } - Method::CancelAndRefund { swap_id } => { + Method::CancelAndRefund(CancelAndRefundArgs { swap_id }) => { debug_span!("method", method_name="CancelAndRefund", swap_id=%swap_id, log_reference_id=field::Empty) } - Method::Resume { swap_id } => { + Method::Resume(ResumeArgs { swap_id }) => { debug_span!("method", method_name="Resume", swap_id=%swap_id, log_reference_id=field::Empty) } Method::Config => { @@ -170,6 +215,663 @@ impl Method { } } +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 })) + } else { + bail!("No swap is currently running") + } +} + +async fn get_swap_info( + get_swap_info: GetSwapInfoArgs, + context: Arc, +) -> Result { + let GetSwapInfoArgs { swap_id } = get_swap_info; + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + let state = context.db.get_state(swap_id).await?; + let is_completed = state.swap_finished(); + + let peerId = context + .db + .get_peer_id(swap_id) + .await + .with_context(|| "Could not get PeerID")?; + + let addresses = context + .db + .get_addresses(peerId) + .await + .with_context(|| "Could not get addressess")?; + + let start_date = context.db.get_swap_start_date(swap_id).await?; + + let swap_state: BobState = state.try_into()?; + let state_name = format!("{}", swap_state); + + 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(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().to_sat(); + let tx_cancel_fee = state2.tx_cancel_fee.to_sat(); + let tx_refund_fee = state2.tx_refund_fee.to_sat(); + 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, + 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 { + 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(Ok(ExpiredTimelocks::Punish)), + BobState::BtcRefunded(_) | BobState::BtcRedeemed(_) | BobState::XmrRedeemed { .. } => None, + }; + + Ok(json!({ + "swapId": swap_id, + "seller": { + "peerId": peerId.to_string(), + "addresses": addresses + }, + "completed": is_completed, + "startDate": start_date, + "stateName": state_name, + "xmrAmount": xmr_amount, + "btcAmount": btc_amount, + "txLockId": tx_lock_id, + "txCancelFee": tx_cancel_fee, + "txRefundFee": tx_refund_fee, + "txLockFee": tx_lock_fee, + "btcRefundAddress": btc_refund_address.to_string(), + "cancelTimelock": cancel_timelock, + "punishTimelock": punish_timelock, + "timelock": timelock.map(|tl| tl.map(|tl| json!(tl)).unwrap_or(json!(null))).unwrap_or(json!(null)), + })) +} + +async fn buy_xmr(buy_xmr: BuyXmrArgs, context: Arc) -> Result { + let BuyXmrArgs { + seller, + bitcoin_change_address, + monero_receive_address, + swap_id, + } = buy_xmr; + 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."); + 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"); + bail!(error); + } + }; + + 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."); + 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, + ); + + 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, + ); + + 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"); + Ok::<_, anyhow::Error>(()) + }.in_current_span()).await; + + Ok(json!({ + "swapId": swap_id.to_string(), + "quote": bid_quote, + })) +} + +async fn resume_swap(resume: ResumeArgs, context: Arc) -> Result { + let ResumeArgs { 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?; + + 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."); + 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"); + Ok::<(), anyhow::Error>(()) + } + .in_current_span(), + ).await; + Ok(json!({ + "result": "ok", + })) +} + +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"); + + state.map(|state| { + json!({ + "result": state, + }) + }) +} + +async fn get_history(context: Arc) -> Result { + let swaps = context.db.all().await?; + let mut vec: Vec<(Uuid, String)> = Vec::new(); + for (swap_id, state) in swaps { + let state: BobState = state.try_into()?; + vec.push((swap_id, state.to_string())); + } + + Ok(json!({ "swaps": vec })) +} + +async fn get_raw_states(context: Arc) -> Result { + let raw_history = context.db.raw_all().await?; + + Ok(json!({ "raw_states": raw_history })) +} + +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), + })) +} + +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(json!({ + "signed_tx": signed_tx, + "amount": amount.to_sat(), + "txid": signed_tx.txid(), + })) +} + +async fn start_daemon( + start_daemon: StartDaemonArgs, + context: Arc, +) -> 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, Arc::clone(&context)).await?; + + tracing::info!(%addr, "Started RPC server"); + + server_handle.stopped().await; + + tracing::info!("Stopped RPC server"); + + Ok(json!({})) +} + +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.to_sat(), + }) +} + +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 })) +} + +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(), + })) +} + +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 + ) + } +} + +async fn get_current_swap(context: Arc) -> Result { + Ok(json!({ + "swap_id": context.swap_lock.get_current_swap_id().await + })) +} + impl Request { pub fn new(cmd: Method) -> Request { Request { @@ -185,635 +887,17 @@ impl Request { } } - async fn handle_cmd(self, context: Arc) -> Result { + async fn handle_cmd(self, context: Arc) -> Result> { match self.cmd { - Method::SuspendCurrentSwap => { - 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 })) - } else { - bail!("No swap is currently running") - } + Method::Balance(args) => { + let response = get_balance(args, context).await?; + Ok(Box::new(response) as Box) } - Method::GetSwapInfo { swap_id } => { - let bitcoin_wallet = context - .bitcoin_wallet - .as_ref() - .context("Could not get Bitcoin wallet")?; - - let state = context.db.get_state(swap_id).await?; - let is_completed = state.swap_finished(); - - let peerId = context - .db - .get_peer_id(swap_id) - .await - .with_context(|| "Could not get PeerID")?; - - let addresses = context - .db - .get_addresses(peerId) - .await - .with_context(|| "Could not get addressess")?; - - let start_date = context.db.get_swap_start_date(swap_id).await?; - - let swap_state: BobState = state.try_into()?; - let state_name = format!("{}", swap_state); - - 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(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().to_sat(); - let tx_cancel_fee = state2.tx_cancel_fee.to_sat(); - let tx_refund_fee = state2.tx_refund_fee.to_sat(); - 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, - 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 { - 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(Ok(ExpiredTimelocks::Punish)), - BobState::BtcRefunded(_) - | BobState::BtcRedeemed(_) - | BobState::XmrRedeemed { .. } => None, - }; - - Ok(json!({ - "swapId": swap_id, - "seller": { - "peerId": peerId.to_string(), - "addresses": addresses - }, - "completed": is_completed, - "startDate": start_date, - "stateName": state_name, - "xmrAmount": xmr_amount, - "btcAmount": btc_amount, - "txLockId": tx_lock_id, - "txCancelFee": tx_cancel_fee, - "txRefundFee": tx_refund_fee, - "txLockFee": tx_lock_fee, - "btcRefundAddress": btc_refund_address.to_string(), - "cancelTimelock": cancel_timelock, - "punishTimelock": punish_timelock, - // If the timelock is None, it means that the swap is in a state where the timelock is not accessible to us. - // If that is the case, we return null. Otherwise, we return the timelock. - "timelock": timelock.map(|tl| tl.map(|tl| json!(tl)).unwrap_or(json!(null))).unwrap_or(json!(null)), - })) - } - Method::BuyXmr { - seller, - bitcoin_change_address, - monero_receive_address, - swap_id, - } => { - 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."); - 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"); - bail!(error); - } - }; - - 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."); - 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, - ); - - 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, - ); - - 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"); - Ok::<_, anyhow::Error>(()) - }.in_current_span()).await; - - Ok(json!({ - "swapId": swap_id.to_string(), - "quote": bid_quote, - })) - } - Method::Resume { swap_id } => { - 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?; - - 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."); - 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"); - Ok::<(), anyhow::Error>(()) - } - .in_current_span(), - ).await; - Ok(json!({ - "result": "ok", - })) - } - Method::CancelAndRefund { swap_id } => { - 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"); - - state.map(|state| { - json!({ - "result": state, - }) - }) - } - Method::History => { - let swaps = context.db.all().await?; - let mut vec: Vec<(Uuid, String)> = Vec::new(); - for (swap_id, state) in swaps { - let state: BobState = state.try_into()?; - vec.push((swap_id, state.to_string())); - } - - Ok(json!({ "swaps": vec })) - } - Method::GetRawStates => { - let raw_history = context.db.raw_all().await?; - - Ok(json!({ "raw_states": raw_history })) - } - Method::Config => { - 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), - })) - } - Method::WithdrawBtc { address, amount } => { - 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(json!({ - "signed_tx": signed_tx, - "amount": amount.to_sat(), - "txid": signed_tx.txid(), - })) - } - Method::StartDaemon { server_address } => { - // 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, Arc::clone(&context)).await?; - - tracing::info!(%addr, "Started RPC server"); - - server_handle.stopped().await; - - tracing::info!("Stopped RPC server"); - - Ok(json!({})) - } - Method::Balance { force_refresh } => { - 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(json!({ - "balance": bitcoin_balance.to_sat() - })) - } - Method::ListSellers { rendezvous_point } => { - 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( - 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 })) - } - Method::ExportBitcoinWallet => { - 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(), - })) - } - Method::MoneroRecovery { swap_id } => { - 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 - ) - } - } - Method::GetCurrentSwap => Ok(json!({ - "swap_id": context.swap_lock.get_current_swap_id().await - })), + _ => todo!(), } } - pub async fn call(self, context: Arc) -> Result { + pub async fn call(self, context: Arc) -> Result { let method_span = self.cmd.get_tracing_span(self.log_reference.clone()); self.handle_cmd(context) @@ -826,6 +910,7 @@ impl Request { }); err }) + .map(|result| json!(result)) } } diff --git a/src-xmr-btc-swap/swap/src/bin/swap.rs b/src-xmr-btc-swap/swap/src/bin/swap.rs index eb81b285..7603ef3a 100644 --- a/src-xmr-btc-swap/swap/src/bin/swap.rs +++ b/src-xmr-btc-swap/swap/src/bin/swap.rs @@ -12,9 +12,12 @@ #![forbid(unsafe_code)] #![allow(non_snake_case)] +use crate::{ + cli::command::{parse_args_and_apply_defaults, ParseResult}, + common::check_latest_version, +}; use anyhow::Result; use std::env; -use crate::{cli::command::{parse_args_and_apply_defaults, ParseResult}, common::check_latest_version}; #[tokio::main] pub async fn main() -> Result<()> { diff --git a/src-xmr-btc-swap/swap/src/cli/command.rs b/src-xmr-btc-swap/swap/src/cli/command.rs index af72df1b..cc917b33 100644 --- a/src-xmr-btc-swap/swap/src/cli/command.rs +++ b/src-xmr-btc-swap/swap/src/cli/command.rs @@ -1,4 +1,7 @@ -use crate::api::request::{Method, Request}; +use crate::api::request::{ + BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, ListSellersArgs, Method, MoneroRecoveryArgs, + Request, ResumeArgs, StartDaemonArgs, WithdrawBtcArgs, +}; use crate::api::Context; use crate::bitcoin::{bitcoin_address, Amount}; use crate::monero; @@ -74,12 +77,12 @@ where let bitcoin_change_address = bitcoin_address::validate_is_testnet(bitcoin_change_address, is_testnet)?; - let request = Request::new(Method::BuyXmr { + let request = Request::new(Method::BuyXmr(BuyXmrArgs { seller, bitcoin_change_address, monero_receive_address, swap_id: Uuid::new_v4(), - }); + })); let context = Context::build( Some(bitcoin), @@ -109,9 +112,9 @@ where (context, request) } CliCommand::Balance { bitcoin } => { - let request = Request::new(Method::Balance { + let request = Request::new(Method::Balance(BalanceArgs { force_refresh: true, - }); + })); let context = Context::build( Some(bitcoin), @@ -132,7 +135,7 @@ where monero, tor, } => { - let request = Request::new(Method::StartDaemon { server_address }); + let request = Request::new(Method::StartDaemon(StartDaemonArgs { server_address })); let context = Context::build( Some(bitcoin), @@ -153,7 +156,7 @@ where address, } => { let address = bitcoin_address::validate_is_testnet(address, is_testnet)?; - let request = Request::new(Method::WithdrawBtc { amount, address }); + let request = Request::new(Method::WithdrawBtc(WithdrawBtcArgs { amount, address })); let context = Context::build( Some(bitcoin), @@ -174,7 +177,7 @@ where monero, tor, } => { - let request = Request::new(Method::Resume { swap_id }); + let request = Request::new(Method::Resume(ResumeArgs { swap_id })); let context = Context::build( Some(bitcoin), @@ -194,7 +197,7 @@ where bitcoin, tor, } => { - let request = Request::new(Method::CancelAndRefund { swap_id }); + let request = Request::new(Method::CancelAndRefund(CancelAndRefundArgs { swap_id })); let context = Context::build( Some(bitcoin), @@ -213,7 +216,7 @@ where rendezvous_point, tor, } => { - let request = Request::new(Method::ListSellers { rendezvous_point }); + let request = Request::new(Method::ListSellers(ListSellersArgs { rendezvous_point })); let context = Context::build(None, None, Some(tor), data, is_testnet, debug, json, None).await?; @@ -239,7 +242,7 @@ where CliCommand::MoneroRecovery { swap_id: SwapId { swap_id }, } => { - let request = Request::new(Method::MoneroRecovery { swap_id }); + let request = Request::new(Method::MoneroRecovery(MoneroRecoveryArgs { swap_id })); let context = Context::build(None, None, None, data, is_testnet, debug, json, None).await?; diff --git a/src-xmr-btc-swap/swap/src/lib.rs b/src-xmr-btc-swap/swap/src/lib.rs index ae84ea62..86467999 100644 --- a/src-xmr-btc-swap/swap/src/lib.rs +++ b/src-xmr-btc-swap/swap/src/lib.rs @@ -18,6 +18,7 @@ pub mod api; pub mod asb; +pub mod bin; pub mod bitcoin; pub mod cli; pub mod common; @@ -27,14 +28,13 @@ pub mod fs; pub mod kraken; pub mod libp2p_ext; pub mod monero; +mod monero_ext; pub mod network; pub mod protocol; pub mod rpc; pub mod seed; pub mod tor; pub mod tracing_ext; -pub mod bin; -mod monero_ext; #[cfg(test)] mod proptest; diff --git a/src-xmr-btc-swap/swap/src/protocol/bob/swap.rs b/src-xmr-btc-swap/swap/src/protocol/bob/swap.rs index 42031007..5db98690 100644 --- a/src-xmr-btc-swap/swap/src/protocol/bob/swap.rs +++ b/src-xmr-btc-swap/swap/src/protocol/bob/swap.rs @@ -338,12 +338,18 @@ async fn next_state( } } Ok(Rejected { reason, .. }) => { - tracing::error!(?reason, "Alice rejected our request for cooperative XMR redeem"); + tracing::error!( + ?reason, + "Alice rejected our request for cooperative XMR redeem" + ); return Err(reason) .context("Alice rejected our request for cooperative XMR redeem"); } Err(error) => { - tracing::error!(?error, "Failed to request cooperative XMR redeem from Alice"); + tracing::error!( + ?error, + "Failed to request cooperative XMR redeem from Alice" + ); return Err(error) .context("Failed to request cooperative XMR redeem from Alice"); } diff --git a/src-xmr-btc-swap/swap/src/rpc.rs b/src-xmr-btc-swap/swap/src/rpc.rs index 73487cc0..473111f2 100644 --- a/src-xmr-btc-swap/swap/src/rpc.rs +++ b/src-xmr-btc-swap/swap/src/rpc.rs @@ -1,11 +1,11 @@ use crate::api::Context; use std::{net::SocketAddr, sync::Arc}; use thiserror::Error; -use tower_http::cors::{Any, CorsLayer}; +use tower_http::cors::CorsLayer; use jsonrpsee::{ - core::server::host_filtering::AllowHosts, - server::{RpcModule, ServerBuilder, ServerHandle}, + core::server::host_filtering::AllowHosts, + server::{RpcModule, ServerBuilder, ServerHandle}, }; pub mod methods; @@ -21,10 +21,13 @@ pub async fn run_server( context: Arc, ) -> anyhow::Result<(SocketAddr, ServerHandle)> { let cors = CorsLayer::permissive(); - let middleware = tower::ServiceBuilder::new().layer(cors); + let middleware = tower::ServiceBuilder::new().layer(cors); - let server = ServerBuilder::default().set_host_filtering(AllowHosts::Any) - .set_middleware(middleware).build(server_address).await?; + let server = ServerBuilder::default() + .set_host_filtering(AllowHosts::Any) + .set_middleware(middleware) + .build(server_address) + .await?; let mut modules = RpcModule::new(()); { modules diff --git a/src-xmr-btc-swap/swap/src/rpc/methods.rs b/src-xmr-btc-swap/swap/src/rpc/methods.rs index 83100d4a..2b8ef7da 100644 --- a/src-xmr-btc-swap/swap/src/rpc/methods.rs +++ b/src-xmr-btc-swap/swap/src/rpc/methods.rs @@ -1,4 +1,7 @@ -use crate::api::request::{Method, Request}; +use crate::api::request::{ + BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, GetSwapInfoArgs, ListSellersArgs, Method, + MoneroRecoveryArgs, Request, ResumeArgs, WithdrawBtcArgs, +}; use crate::api::Context; use crate::bitcoin::bitcoin_address; use crate::monero::monero_address; @@ -29,7 +32,12 @@ pub fn register_modules(context: Arc) -> Result> let swap_id = as_uuid(swap_id) .ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?; - execute_request(params_raw, Method::GetSwapInfo { swap_id }, &context).await + execute_request( + params_raw, + Method::GetSwapInfo(GetSwapInfoArgs { swap_id }), + &context, + ) + .await })?; module.register_async_method("get_bitcoin_balance", |params_raw, context| async move { @@ -45,7 +53,12 @@ pub fn register_modules(context: Arc) -> Result> jsonrpsee_core::Error::Custom("force_refesh is not a boolean".to_string()) })?; - execute_request(params_raw, Method::Balance { force_refresh }, &context).await + execute_request( + params_raw, + Method::Balance(BalanceArgs { force_refresh }), + &context, + ) + .await })?; module.register_async_method("get_history", |params, context| async move { @@ -66,7 +79,7 @@ pub fn register_modules(context: Arc) -> Result> let swap_id = as_uuid(swap_id) .ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?; - execute_request(params_raw, Method::Resume { swap_id }, &context).await + execute_request(params_raw, Method::Resume(ResumeArgs { swap_id }), &context).await })?; module.register_async_method("cancel_refund_swap", |params_raw, context| async move { @@ -79,7 +92,12 @@ pub fn register_modules(context: Arc) -> Result> let swap_id = as_uuid(swap_id) .ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?; - execute_request(params_raw, Method::CancelAndRefund { swap_id }, &context).await + execute_request( + params_raw, + Method::CancelAndRefund(CancelAndRefundArgs { swap_id }), + &context, + ) + .await })?; module.register_async_method( @@ -95,7 +113,12 @@ pub fn register_modules(context: Arc) -> Result> jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()) })?; - execute_request(params_raw, Method::MoneroRecovery { swap_id }, &context).await + execute_request( + params_raw, + Method::MoneroRecovery(MoneroRecoveryArgs { swap_id }), + &context, + ) + .await }, )?; @@ -123,10 +146,10 @@ pub fn register_modules(context: Arc) -> Result> execute_request( params_raw, - Method::WithdrawBtc { + Method::WithdrawBtc(WithdrawBtcArgs { amount, address: withdraw_address, - }, + }), &context, ) .await @@ -165,12 +188,12 @@ pub fn register_modules(context: Arc) -> Result> execute_request( params_raw, - Method::BuyXmr { + Method::BuyXmr(BuyXmrArgs { bitcoin_change_address, monero_receive_address, seller, swap_id: Uuid::new_v4(), - }, + }), &context, ) .await @@ -192,9 +215,9 @@ pub fn register_modules(context: Arc) -> Result> execute_request( params_raw, - Method::ListSellers { + Method::ListSellers(ListSellersArgs { rendezvous_point: rendezvous_point.clone(), - }, + }), &context, ) .await diff --git a/src/renderer/components/modal/swap/pages/DebugPage.tsx b/src/renderer/components/modal/swap/pages/DebugPage.tsx index a3aee47c..991e396d 100644 --- a/src/renderer/components/modal/swap/pages/DebugPage.tsx +++ b/src/renderer/components/modal/swap/pages/DebugPage.tsx @@ -1,12 +1,12 @@ -import { Box, DialogContentText } from '@material-ui/core'; -import { useActiveSwapInfo, useAppSelector } from 'store/hooks'; -import CliLogsBox from '../../../other/RenderedCliLog'; -import JsonTreeView from '../../../other/JSONViewTree'; +import { Box, DialogContentText } from "@material-ui/core"; +import { useActiveSwapInfo, useAppSelector } from "store/hooks"; +import CliLogsBox from "../../../other/RenderedCliLog"; +import JsonTreeView from "../../../other/JSONViewTree"; export default function DebugPage() { const torStdOut = useAppSelector((s) => s.tor.stdOut); const logs = useAppSelector((s) => s.swap.logs); - const guiState = useAppSelector((s) => s.swap); + const guiState = useAppSelector((s) => s); const cliState = useActiveSwapInfo(); return ( @@ -14,9 +14,9 @@ export default function DebugPage() { @@ -28,7 +28,7 @@ export default function DebugPage() { data={cliState} label="Swap Daemon State (exposed via API)" /> - + diff --git a/src/renderer/components/other/Units.tsx b/src/renderer/components/other/Units.tsx index 445f2dcf..90420dde 100644 --- a/src/renderer/components/other/Units.tsx +++ b/src/renderer/components/other/Units.tsx @@ -1,6 +1,6 @@ -import { piconerosToXmr, satsToBtc } from 'utils/conversionUtils'; -import { Tooltip } from '@material-ui/core'; -import { useAppSelector } from 'store/hooks'; +import { piconerosToXmr, satsToBtc } from "utils/conversionUtils"; +import { Tooltip } from "@material-ui/core"; +import { useAppSelector } from "store/hooks"; type Amount = number | null | undefined; @@ -21,13 +21,13 @@ export function AmountWithUnit({ title={ dollarRate != null && amount != null ? `≈ $${(dollarRate * amount).toFixed(2)}` - : '' + : "" } > {amount != null ? Number.parseFloat(amount.toFixed(fixedPrecision)) - : '?'}{' '} + : "?"}{" "} {unit} diff --git a/src/renderer/rpc.ts b/src/renderer/rpc.ts index b12f48f9..126ca21c 100644 --- a/src/renderer/rpc.ts +++ b/src/renderer/rpc.ts @@ -4,11 +4,15 @@ import { store } from "./store/storeRenderer"; import { rpcSetBalance } from "store/features/rpcSlice"; export async function checkBitcoinBalance() { - const response = await invoke('balance') as BalanceBitcoinResponse; - store.dispatch(rpcSetBalance(response.balance)); + // TODO: use tauri-bindgen here + const response = (await invoke("balance")) as { + balance: number; + }; + + store.dispatch(rpcSetBalance(response.balance)); } export async function getRawSwapInfos() { - const response = await invoke('swap_infos'); - console.log(response); -} \ No newline at end of file + const response = await invoke("swap_infos"); + console.log(response); +}