diff --git a/swap/src/api.rs b/swap/src/api.rs index b4342e56..0b752fbd 100644 --- a/swap/src/api.rs +++ b/swap/src/api.rs @@ -1,5 +1,6 @@ +pub mod request; use crate::bitcoin::{Amount, TxLock}; -use crate::cli::command::{Bitcoin, Command, Monero, Tor}; +use crate::cli::command::{Bitcoin, Monero, Tor}; use crate::cli::{list_sellers, EventLoop, SellerStatus}; use crate::database::open_db; use crate::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet}; @@ -33,23 +34,6 @@ use uuid::Uuid; static START: Once = Once::new(); -#[derive(PartialEq, Debug)] -pub struct Request { - pub params: Params, - pub cmd: Command, -} - -#[derive(Default, PartialEq, Debug)] -pub struct Params { - pub seller: Option, - pub bitcoin_change_address: Option, - pub monero_receive_address: Option, - pub rendezvous_point: Option, - pub swap_id: Option, - pub amount: Option, - pub address: Option, -} - #[derive(Clone, PartialEq, Debug)] pub struct Config { tor_socks5_port: Option, @@ -70,377 +54,6 @@ pub struct Context { pub config: Config, } -impl Request { - pub async fn call(&self, context: Arc) -> Result { - let result = match self.cmd { - Command::BuyXmr => { - let swap_id = Uuid::new_v4(); - - let seed = context.config.seed.as_ref().unwrap(); - let env_config = context.config.env_config; - let btc = context.bitcoin_wallet.as_ref().unwrap(); - let seller = self.params.seller.clone().unwrap(); - let monero_receive_address = self.params.monero_receive_address.unwrap(); - let bitcoin_change_address = self.params.bitcoin_change_address.clone().unwrap(); - - let bitcoin_wallet = btc; - let seller_peer_id = self - .params - .seller - .as_ref() - .unwrap() - .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.unwrap(), - behaviour, - ) - .await?; - swarm.behaviour_mut().add_address(seller_peer_id, seller); - - tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized"); - - let (event_loop, mut event_loop_handle) = - EventLoop::new(swap_id, swarm, seller_peer_id)?; - let event_loop = tokio::spawn(event_loop.run()); - - let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size()); - let estimate_fee = |amount| bitcoin_wallet.estimate_fee(TxLock::weight(), amount); - - let (amount, fees) = match determine_btc_to_swap( - context.config.json, - event_loop_handle.request_quote(), - bitcoin_wallet.new_address(), - || bitcoin_wallet.balance(), - max_givable, - || bitcoin_wallet.sync(), - estimate_fee, - ) - .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?; - context - .db - .insert_monero_address(swap_id, monero_receive_address) - .await?; - let monero_wallet = context.monero_wallet.as_ref().unwrap(); - - let swap = Swap::new( - Arc::clone(&context.db), - swap_id, - Arc::clone(&bitcoin_wallet), - Arc::clone(&monero_wallet), - env_config, - event_loop_handle, - monero_receive_address, - bitcoin_change_address, - amount, - ); - - tokio::select! { - result = event_loop => { - result - .context("EventLoop panicked")?; - }, - result = bob::run(swap) => { - result - .context("Failed to complete swap")?; - } - } - json!({ - "empty": "true" - }) - } - Command::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())); - } - json!({ "swaps": vec }) - } - Command::Config => { - // 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"); - - json!({ - "result": [] - }) - } - Command::WithdrawBtc => { - let bitcoin_wallet = context.bitcoin_wallet.as_ref().unwrap(); - - let address = self.params.address.clone().unwrap(); - - let amount = match self.params.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?; - - json!({ - "signed_tx": signed_tx, - "amount": amount.as_sat(), - "txid": signed_tx.txid(), - }) - } - Command::StartDaemon => { - let addr2 = "127.0.0.1:1234".parse()?; - - let server_handle = { - if let Some(addr) = context.config.server_address { - let (_addr, handle) = rpc::run_server(addr, context).await?; - Some(handle) - } else { - let (_addr, handle) = rpc::run_server(addr2, context).await?; - Some(handle) - } - }; - loop {} - json!({ - "result": [] - }) - } - Command::Balance => { - let bitcoin_wallet = context.bitcoin_wallet.as_ref().unwrap(); - - bitcoin_wallet.sync().await?; - let bitcoin_balance = bitcoin_wallet.balance().await?; - tracing::info!( - balance = %bitcoin_balance, - "Checked Bitcoin balance", - ); - - json!({ - "balance": bitcoin_balance.as_sat() - }) - } - Command::Resume => { - let swap_id = self.params.swap_id.unwrap(); - - 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().unwrap().derive_libp2p_identity(); - - let behaviour = cli::Behaviour::new( - seller_peer_id, - context.config.env_config, - Arc::clone(context.bitcoin_wallet.as_ref().unwrap()), - (seed.clone(), context.config.namespace), - ); - let mut swarm = swarm::cli( - seed.clone(), - context.config.tor_socks5_port.clone().unwrap(), - 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)?; - let handle = tokio::spawn(event_loop.run()); - - 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().unwrap()), - Arc::clone(context.monero_wallet.as_ref().unwrap()), - context.config.env_config, - event_loop_handle, - monero_receive_address, - ) - .await?; - - tokio::select! { - event_loop_result = handle => { - event_loop_result?; - }, - swap_result = bob::run(swap) => { - swap_result?; - } - } - json!({ - "result": [] - }) - } - Command::Cancel => { - let bitcoin_wallet = context.bitcoin_wallet.as_ref().unwrap(); - - let (txid, _) = cli::cancel( - self.params.swap_id.unwrap(), - Arc::clone(bitcoin_wallet), - Arc::clone(&context.db), - ) - .await?; - - tracing::debug!("Cancel transaction successfully published with id {}", txid); - - json!({ - "txid": txid, - }) - } - Command::Refund => { - let bitcoin_wallet = context.bitcoin_wallet.as_ref().unwrap(); - - let state = cli::refund( - self.params.swap_id.unwrap(), - Arc::clone(bitcoin_wallet), - Arc::clone(&context.db), - ) - .await?; - - json!({ "result": state }) - } - Command::ListSellers => { - let rendezvous_point = self.params.rendezvous_point.clone().unwrap(); - 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().unwrap().derive_libp2p_identity(); - - let sellers = list_sellers( - rendezvous_node_peer_id, - rendezvous_point, - context.config.namespace, - context.config.tor_socks5_port.unwrap(), - 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" - ); - } - } - } - - json!({ "sellers": sellers }) - } - Command::ExportBitcoinWallet => { - let bitcoin_wallet = context.bitcoin_wallet.as_ref().unwrap(); - - let wallet_export = bitcoin_wallet.wallet_export("cli").await?; - tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet"); - json!({ - "result": [] - }) - } - Command::MoneroRecovery => { - let swap_state: BobState = context - .db - .get_state(self.params.swap_id.clone().unwrap()) - .await? - .try_into()?; - - match swap_state { - BobState::Started { .. } - | BobState::SwapSetupCompleted(_) - | BobState::BtcLocked { .. } - | BobState::XmrLockProofReceived { .. } - | BobState::XmrLocked(_) - | BobState::EncSigSent(_) - | BobState::CancelTimelockExpired(_) - | BobState::BtcCancelled(_) - | BobState::BtcRefunded(_) - | BobState::BtcPunished { .. } - | BobState::SafelyAborted - | BobState::XmrRedeemed { .. } => { - bail!("Cannot print monero recovery information in state {}, only possible for BtcRedeemed", swap_state) - } - BobState::BtcRedeemed(state5) => { - let (spend_key, view_key) = state5.xmr_keys(); - - 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!("Wallet address: {}", address.to_string()); - - let view_key = serde_json::to_string(&view_key)?; - println!("View key: {}", view_key); - - println!("Spend key: {}", spend_key); - } - } - json!({ - "result": [] - }) - } - }; - Ok(result) - } -} impl Context { pub async fn build( @@ -523,19 +136,6 @@ impl Context { } } -impl Serialize for Context { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // 3 is the number of fields in the struct. - let mut state = serializer.serialize_struct("Context", 3)?; - state.serialize_field("debug", &self.config.debug)?; - state.serialize_field("json", &self.config.json)?; - state.end() - } -} - impl fmt::Debug for Context { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Testing {}", true) @@ -566,107 +166,6 @@ async fn init_bitcoin_wallet( Ok(wallet) } -fn qr_code(value: &impl ToString) -> Result { - let code = QrCode::new(value.to_string())?; - let qr_code = code - .render::() - .dark_color(unicode::Dense1x2::Light) - .light_color(unicode::Dense1x2::Dark) - .build(); - Ok(qr_code) -} -pub async fn determine_btc_to_swap( - json: bool, - bid_quote: impl Future>, - get_new_address: impl Future>, - balance: FB, - max_giveable_fn: FMG, - sync: FS, - estimate_fee: FFE, -) -> Result<(bitcoin::Amount, bitcoin::Amount)> -where - TB: Future>, - FB: Fn() -> TB, - TMG: Future>, - FMG: Fn() -> TMG, - TS: Future>, - FS: Fn() -> TS, - FFE: Fn(bitcoin::Amount) -> TFE, - TFE: Future>, -{ - tracing::debug!("Requesting quote"); - let bid_quote = bid_quote.await?; - - if bid_quote.max_quantity == bitcoin::Amount::ZERO { - bail!(ZeroQuoteReceived) - } - - tracing::info!( - price = %bid_quote.price, - minimum_amount = %bid_quote.min_quantity, - maximum_amount = %bid_quote.max_quantity, - "Received quote", - ); - - let mut max_giveable = max_giveable_fn().await?; - - if max_giveable == bitcoin::Amount::ZERO || max_giveable < bid_quote.min_quantity { - let deposit_address = get_new_address.await?; - let minimum_amount = bid_quote.min_quantity; - let maximum_amount = bid_quote.max_quantity; - - if !json { - eprintln!("{}", qr_code(&deposit_address)?); - } - - loop { - let min_outstanding = bid_quote.min_quantity - max_giveable; - let min_fee = estimate_fee(min_outstanding).await?; - let min_deposit = min_outstanding + min_fee; - - tracing::info!( - "Deposit at least {} to cover the min quantity with fee!", - min_deposit - ); - tracing::info!( - %deposit_address, - %min_deposit, - %max_giveable, - %minimum_amount, - %maximum_amount, - "Waiting for Bitcoin deposit", - ); - - max_giveable = loop { - sync().await?; - let new_max_givable = max_giveable_fn().await?; - - if new_max_givable > max_giveable { - break new_max_givable; - } - - tokio::time::sleep(Duration::from_secs(1)).await; - }; - - let new_balance = balance().await?; - tracing::info!(%new_balance, %max_giveable, "Received Bitcoin"); - - if max_giveable < bid_quote.min_quantity { - tracing::info!("Deposited amount is less than `min_quantity`"); - continue; - } - - break; - } - }; - - let balance = balance().await?; - let fees = balance - max_giveable; - let max_accepted = bid_quote.max_quantity; - let btc_swap_amount = min(max_giveable, max_accepted); - - Ok((btc_swap_amount, fees)) -} async fn init_monero_wallet( data_dir: PathBuf, @@ -784,7 +283,7 @@ pub mod api_test { monero_receive_address: Some(monero_receive_address), ..Default::default() }, - cmd: Command::BuyXmr, + cmd: Method::BuyXmr, } } @@ -794,7 +293,7 @@ pub mod api_test { swap_id: Some(Uuid::from_str(SWAP_ID).unwrap()), ..Default::default() }, - cmd: Command::Resume, + cmd: Method::Resume, } } @@ -804,7 +303,7 @@ pub mod api_test { swap_id: Some(Uuid::from_str(SWAP_ID).unwrap()), ..Default::default() }, - cmd: Command::Cancel, + cmd: Method::Cancel, } } @@ -814,7 +313,7 @@ pub mod api_test { swap_id: Some(Uuid::from_str(SWAP_ID).unwrap()), ..Default::default() }, - cmd: Command::Refund, + cmd: Method::Refund, } } } diff --git a/swap/src/api/request.rs b/swap/src/api/request.rs new file mode 100644 index 00000000..372840ea --- /dev/null +++ b/swap/src/api/request.rs @@ -0,0 +1,540 @@ +use crate::bitcoin::{Amount, TxLock}; +use crate::cli::command::{Bitcoin, Monero, Tor}; +use crate::cli::{list_sellers, EventLoop, SellerStatus}; +use crate::database::open_db; +use crate::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet}; +use crate::fs::system_data_dir; +use crate::libp2p_ext::MultiAddrExt; +use crate::network::quote::{BidQuote, ZeroQuoteReceived}; +use crate::network::rendezvous::XmrBtcNamespace; +use crate::network::swarm; +use crate::protocol::bob::{BobState, Swap}; +use crate::protocol::{bob, Database}; +use crate::seed::Seed; +use crate::{bitcoin, cli, monero, rpc}; +use anyhow::{bail, Context as AnyContext, Result}; +use comfy_table::Table; +use libp2p::core::Multiaddr; +use qrcode::render::unicode; +use qrcode::QrCode; +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use serde_json::json; +use std::cmp::min; +use std::convert::TryInto; +use std::fmt; +use std::future::Future; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use url::Url; +use uuid::Uuid; +use crate::api::{Config, Context}; + + +#[derive(PartialEq, Debug)] +pub struct Request { + pub params: Params, + pub cmd: Method, +} + +#[derive(Default, PartialEq, Debug)] +pub struct Params { + pub seller: Option, + pub bitcoin_change_address: Option, + pub monero_receive_address: Option, + pub rendezvous_point: Option, + pub swap_id: Option, + pub amount: Option, + pub address: Option, +} + +#[derive(Debug, PartialEq)] +pub enum Method { + BuyXmr, + History, + Config, + WithdrawBtc, + Balance, + Resume, + Cancel, + Refund, + ListSellers, + ExportBitcoinWallet, + MoneroRecovery, + StartDaemon, +} + +impl Request { + pub async fn call(&self, context: Arc) -> Result { + let result = match self.cmd { + Method::BuyXmr => { + let swap_id = Uuid::new_v4(); + + let seed = context.config.seed.as_ref().unwrap(); + let env_config = context.config.env_config; + let btc = context.bitcoin_wallet.as_ref().unwrap(); + let seller = self.params.seller.clone().unwrap(); + let monero_receive_address = self.params.monero_receive_address.unwrap(); + let bitcoin_change_address = self.params.bitcoin_change_address.clone().unwrap(); + + let bitcoin_wallet = btc; + let seller_peer_id = self + .params + .seller + .as_ref() + .unwrap() + .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.unwrap(), + behaviour, + ) + .await?; + swarm.behaviour_mut().add_address(seller_peer_id, seller); + + tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized"); + + let (event_loop, mut event_loop_handle) = + EventLoop::new(swap_id, swarm, seller_peer_id)?; + let event_loop = tokio::spawn(event_loop.run()); + + let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size()); + let estimate_fee = |amount| bitcoin_wallet.estimate_fee(TxLock::weight(), amount); + + let (amount, fees) = match determine_btc_to_swap( + context.config.json, + event_loop_handle.request_quote(), + bitcoin_wallet.new_address(), + || bitcoin_wallet.balance(), + max_givable, + || bitcoin_wallet.sync(), + estimate_fee, + ) + .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?; + context + .db + .insert_monero_address(swap_id, monero_receive_address) + .await?; + let monero_wallet = context.monero_wallet.as_ref().unwrap(); + + let swap = Swap::new( + Arc::clone(&context.db), + swap_id, + Arc::clone(&bitcoin_wallet), + Arc::clone(&monero_wallet), + env_config, + event_loop_handle, + monero_receive_address, + bitcoin_change_address, + amount, + ); + + tokio::select! { + result = event_loop => { + result + .context("EventLoop panicked")?; + }, + result = bob::run(swap) => { + result + .context("Failed to complete swap")?; + } + } + json!({ + "empty": "true" + }) + } + 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())); + } + json!({ "swaps": vec }) + } + Method::Config => { + // 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"); + + json!({ + "result": [] + }) + } + Method::WithdrawBtc => { + let bitcoin_wallet = context.bitcoin_wallet.as_ref().unwrap(); + + let address = self.params.address.clone().unwrap(); + + let amount = match self.params.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?; + + json!({ + "signed_tx": signed_tx, + "amount": amount.as_sat(), + "txid": signed_tx.txid(), + }) + } + Method::StartDaemon => { + let addr2 = "127.0.0.1:1234".parse()?; + + let server_handle = { + if let Some(addr) = context.config.server_address { + let (_addr, handle) = rpc::run_server(addr, context).await?; + Some(handle) + } else { + let (_addr, handle) = rpc::run_server(addr2, context).await?; + Some(handle) + } + }; + loop {} + json!({ + "result": [] + }) + } + Method::Balance => { + let bitcoin_wallet = context.bitcoin_wallet.as_ref().unwrap(); + + bitcoin_wallet.sync().await?; + let bitcoin_balance = bitcoin_wallet.balance().await?; + tracing::info!( + balance = %bitcoin_balance, + "Checked Bitcoin balance", + ); + + json!({ + "balance": bitcoin_balance.as_sat() + }) + } + Method::Resume => { + let swap_id = self.params.swap_id.unwrap(); + + 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().unwrap().derive_libp2p_identity(); + + let behaviour = cli::Behaviour::new( + seller_peer_id, + context.config.env_config, + Arc::clone(context.bitcoin_wallet.as_ref().unwrap()), + (seed.clone(), context.config.namespace), + ); + let mut swarm = swarm::cli( + seed.clone(), + context.config.tor_socks5_port.clone().unwrap(), + 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)?; + let handle = tokio::spawn(event_loop.run()); + + 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().unwrap()), + Arc::clone(context.monero_wallet.as_ref().unwrap()), + context.config.env_config, + event_loop_handle, + monero_receive_address, + ) + .await?; + + tokio::select! { + event_loop_result = handle => { + event_loop_result?; + }, + swap_result = bob::run(swap) => { + swap_result?; + } + } + json!({ + "result": [] + }) + } + Method::Cancel => { + let bitcoin_wallet = context.bitcoin_wallet.as_ref().unwrap(); + + let (txid, _) = cli::cancel( + self.params.swap_id.unwrap(), + Arc::clone(bitcoin_wallet), + Arc::clone(&context.db), + ) + .await?; + + tracing::debug!("Cancel transaction successfully published with id {}", txid); + + json!({ + "txid": txid, + }) + } + Method::Refund => { + let bitcoin_wallet = context.bitcoin_wallet.as_ref().unwrap(); + + let state = cli::refund( + self.params.swap_id.unwrap(), + Arc::clone(bitcoin_wallet), + Arc::clone(&context.db), + ) + .await?; + + json!({ "result": state }) + } + Method::ListSellers => { + let rendezvous_point = self.params.rendezvous_point.clone().unwrap(); + 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().unwrap().derive_libp2p_identity(); + + let sellers = list_sellers( + rendezvous_node_peer_id, + rendezvous_point, + context.config.namespace, + context.config.tor_socks5_port.unwrap(), + 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" + ); + } + } + } + + json!({ "sellers": sellers }) + } + Method::ExportBitcoinWallet => { + let bitcoin_wallet = context.bitcoin_wallet.as_ref().unwrap(); + + let wallet_export = bitcoin_wallet.wallet_export("cli").await?; + tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet"); + json!({ + "result": [] + }) + } + Method::MoneroRecovery => { + let swap_state: BobState = context + .db + .get_state(self.params.swap_id.clone().unwrap()) + .await? + .try_into()?; + + match swap_state { + BobState::Started { .. } + | BobState::SwapSetupCompleted(_) + | BobState::BtcLocked { .. } + | BobState::XmrLockProofReceived { .. } + | BobState::XmrLocked(_) + | BobState::EncSigSent(_) + | BobState::CancelTimelockExpired(_) + | BobState::BtcCancelled(_) + | BobState::BtcRefunded(_) + | BobState::BtcPunished { .. } + | BobState::SafelyAborted + | BobState::XmrRedeemed { .. } => { + bail!("Cannot print monero recovery information in state {}, only possible for BtcRedeemed", swap_state) + } + BobState::BtcRedeemed(state5) => { + let (spend_key, view_key) = state5.xmr_keys(); + + 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!("Wallet address: {}", address.to_string()); + + let view_key = serde_json::to_string(&view_key)?; + println!("View key: {}", view_key); + + println!("Spend key: {}", spend_key); + } + } + json!({ + "result": [] + }) + } + }; + Ok(result) + } +} + +fn qr_code(value: &impl ToString) -> Result { + let code = QrCode::new(value.to_string())?; + let qr_code = code + .render::() + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build(); + Ok(qr_code) +} +pub async fn determine_btc_to_swap( + json: bool, + bid_quote: impl Future>, + get_new_address: impl Future>, + balance: FB, + max_giveable_fn: FMG, + sync: FS, + estimate_fee: FFE, +) -> Result<(bitcoin::Amount, bitcoin::Amount)> +where + TB: Future>, + FB: Fn() -> TB, + TMG: Future>, + FMG: Fn() -> TMG, + TS: Future>, + FS: Fn() -> TS, + FFE: Fn(bitcoin::Amount) -> TFE, + TFE: Future>, +{ + tracing::debug!("Requesting quote"); + let bid_quote = bid_quote.await?; + + if bid_quote.max_quantity == bitcoin::Amount::ZERO { + bail!(ZeroQuoteReceived) + } + + tracing::info!( + price = %bid_quote.price, + minimum_amount = %bid_quote.min_quantity, + maximum_amount = %bid_quote.max_quantity, + "Received quote", + ); + + let mut max_giveable = max_giveable_fn().await?; + + if max_giveable == bitcoin::Amount::ZERO || max_giveable < bid_quote.min_quantity { + let deposit_address = get_new_address.await?; + let minimum_amount = bid_quote.min_quantity; + let maximum_amount = bid_quote.max_quantity; + + if !json { + eprintln!("{}", qr_code(&deposit_address)?); + } + + loop { + let min_outstanding = bid_quote.min_quantity - max_giveable; + let min_fee = estimate_fee(min_outstanding).await?; + let min_deposit = min_outstanding + min_fee; + + tracing::info!( + "Deposit at least {} to cover the min quantity with fee!", + min_deposit + ); + tracing::info!( + %deposit_address, + %min_deposit, + %max_giveable, + %minimum_amount, + %maximum_amount, + "Waiting for Bitcoin deposit", + ); + + max_giveable = loop { + sync().await?; + let new_max_givable = max_giveable_fn().await?; + + if new_max_givable > max_giveable { + break new_max_givable; + } + + tokio::time::sleep(Duration::from_secs(1)).await; + }; + + let new_balance = balance().await?; + tracing::info!(%new_balance, %max_giveable, "Received Bitcoin"); + + if max_giveable < bid_quote.min_quantity { + tracing::info!("Deposited amount is less than `min_quantity`"); + continue; + } + + break; + } + }; + + let balance = balance().await?; + let fees = balance - max_giveable; + let max_accepted = bid_quote.max_quantity; + let btc_swap_amount = min(max_giveable, max_accepted); + + Ok((btc_swap_amount, fees)) +} diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index f8c72ea9..ac7f60c6 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -1,4 +1,5 @@ -use crate::api::{Context, Params, Request, Config}; +use crate::api::{Context, Config}; +use crate::api::request::{Request, Params, Method}; use crate::bitcoin::{Amount, bitcoin_address}; use crate::monero::monero_address; use crate::fs::system_data_dir; @@ -48,8 +49,8 @@ where I: IntoIterator, T: Into + Clone, { - let args = match RawArguments::clap().get_matches_from_safe(raw_args) { - Ok(matches) => RawArguments::from_clap(&matches), + let args = match Arguments::clap().get_matches_from_safe(raw_args) { + Ok(matches) => Arguments::from_clap(&matches), Err(clap::Error { message, kind: clap::ErrorKind::HelpDisplayed | clap::ErrorKind::VersionDisplayed, @@ -64,7 +65,7 @@ where let data = args.data; let (context, request) = match args.cmd { - RawCommand::BuyXmr { + CliCommand::BuyXmr { seller: Seller { seller }, bitcoin, bitcoin_change_address, @@ -94,31 +95,31 @@ where seller: Some(seller), ..Default::default() }, - cmd: Command::BuyXmr, + cmd: Method::BuyXmr, }; (context, request) } - RawCommand::History => { + CliCommand::History => { let context = Context::build(None, None, None, data, is_testnet, debug, json, None).await?; let request = Request { params: Params::default(), - cmd: Command::History, + cmd: Method::History, }; (context, request) } - RawCommand::Config => { + CliCommand::Config => { let context = Context::build(None, None, None, data, is_testnet, debug, json, None).await?; let request = Request { params: Params::default(), - cmd: Command::Config, + cmd: Method::Config, }; (context, request) } - RawCommand::Balance { bitcoin } => { + CliCommand::Balance { bitcoin } => { let context = Context::build( Some(bitcoin), None, @@ -132,11 +133,11 @@ where .await?; let request = Request { params: Params::default(), - cmd: Command::Balance, + cmd: Method::Balance, }; (context, request) } - RawCommand::StartDaemon { + CliCommand::StartDaemon { server_address, bitcoin, monero, @@ -155,11 +156,11 @@ where .await?; let request = Request { params: Params::default(), - cmd: Command::StartDaemon, + cmd: Method::StartDaemon, }; (context, request) } - RawCommand::WithdrawBtc { + CliCommand::WithdrawBtc { bitcoin, amount, address, @@ -184,11 +185,11 @@ where address: Some(address), ..Default::default() }, - cmd: Command::WithdrawBtc, + cmd: Method::WithdrawBtc, }; (context, request) } - RawCommand::Resume { + CliCommand::Resume { swap_id: SwapId { swap_id }, bitcoin, monero, @@ -210,11 +211,11 @@ where swap_id: Some(swap_id), ..Default::default() }, - cmd: Command::Resume, + cmd: Method::Resume, }; (context, request) } - RawCommand::Cancel { + CliCommand::Cancel { swap_id: SwapId { swap_id }, bitcoin, tor, @@ -235,11 +236,11 @@ where swap_id: Some(swap_id), ..Default::default() }, - cmd: Command::Cancel, + cmd: Method::Cancel, }; (context, request) } - RawCommand::Refund { + CliCommand::Refund { swap_id: SwapId { swap_id }, bitcoin, tor, @@ -260,11 +261,11 @@ where swap_id: Some(swap_id), ..Default::default() }, - cmd: Command::Refund, + cmd: Method::Refund, }; (context, request) } - RawCommand::ListSellers { + CliCommand::ListSellers { rendezvous_point, tor, } => { @@ -276,11 +277,11 @@ where rendezvous_point: Some(rendezvous_point), ..Default::default() }, - cmd: Command::ListSellers, + cmd: Method::ListSellers, }; (context, request) } - RawCommand::ExportBitcoinWallet { bitcoin } => { + CliCommand::ExportBitcoinWallet { bitcoin } => { let context = Context::build( Some(bitcoin), None, @@ -294,11 +295,11 @@ where .await?; let request = Request { params: Params::default(), - cmd: Command::ExportBitcoinWallet, + cmd: Method::ExportBitcoinWallet, }; (context, request) } - RawCommand::MoneroRecovery { swap_id } => { + CliCommand::MoneroRecovery { swap_id } => { let context = Context::build(None, None, None, data, is_testnet, debug, json, None).await?; @@ -307,7 +308,7 @@ where swap_id: Some(swap_id.swap_id), ..Default::default() }, - cmd: Command::MoneroRecovery, + cmd: Method::MoneroRecovery, }; (context, request) } @@ -315,21 +316,6 @@ where Ok(ParseResult::Context(Arc::new(context), Box::new(request))) } -#[derive(Debug, PartialEq)] -pub enum Command { - BuyXmr, - History, - Config, - WithdrawBtc, - Balance, - Resume, - Cancel, - Refund, - ListSellers, - ExportBitcoinWallet, - MoneroRecovery, - StartDaemon, -} #[derive(structopt::StructOpt, Debug)] #[structopt( @@ -338,7 +324,7 @@ pub enum Command { author, version = env!("VERGEN_GIT_SEMVER_LIGHTWEIGHT") )] -struct RawArguments { +struct Arguments { // global is necessary to ensure that clap can match against testnet in subcommands #[structopt( long, @@ -365,11 +351,11 @@ struct RawArguments { json: bool, #[structopt(subcommand)] - cmd: RawCommand, + cmd: CliCommand, } #[derive(structopt::StructOpt, Debug)] -enum RawCommand { +enum CliCommand { /// Start a BTC for XMR swap BuyXmr { #[structopt(flatten)] diff --git a/swap/src/rpc/methods.rs b/swap/src/rpc/methods.rs index 10d57693..ab1c5046 100644 --- a/swap/src/rpc/methods.rs +++ b/swap/src/rpc/methods.rs @@ -1,6 +1,7 @@ -use crate::api::{Context, Params, Request}; -use crate::cli::command::Command; -use crate::rpc::Error; +use crate::api::{Context}; +use crate::api::request::{Params, Request, Method}; +//use crate::rpc::Error; +use anyhow::{Error, Result}; use crate::{bitcoin, monero}; use jsonrpsee::http_server::RpcModule; use libp2p::core::Multiaddr; @@ -14,16 +15,12 @@ pub fn register_modules(context: Arc) -> RpcModule> { let mut module = RpcModule::new(context); module .register_async_method("get_bitcoin_balance", |_, context| async move { - get_bitcoin_balance(&context) - .await - .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string())) + get_bitcoin_balance(&context).await }) .unwrap(); module .register_async_method("get_history", |_, context| async move { - get_history(&context) - .await - .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string())) + get_history(&context).await }) .unwrap(); module @@ -38,22 +35,24 @@ pub fn register_modules(context: Arc) -> RpcModule> { module .register_async_method("get_seller", |params, context| async move { let params: HashMap = params.parse()?; + let swap_id = Uuid::from_str(params.get("swap_id").ok_or_else(|| { jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()) })?) - .unwrap(); + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + let peerId = context .db .get_peer_id(swap_id) .await - .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string())) - .unwrap(); + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + let addresses = context .db .get_addresses(peerId) .await - .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string())) - .unwrap(); + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + Ok(json!({ "peerId": peerId.to_base58(), "addresses": addresses @@ -63,16 +62,17 @@ pub fn register_modules(context: Arc) -> RpcModule> { module .register_async_method("get_swap_start_date", |params, context| async move { let params: HashMap = params.parse()?; + let swap_id = Uuid::from_str(params.get("swap_id").ok_or_else(|| { jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()) })?) - .unwrap(); + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + let start_date = context .db .get_swap_start_date(swap_id) .await - .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string())) - .unwrap(); + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; Ok(json!({ "start_date": start_date, @@ -82,18 +82,19 @@ pub fn register_modules(context: Arc) -> RpcModule> { module .register_async_method("resume_swap", |params, context| async move { let params: HashMap = params.parse()?; + let swap_id = Uuid::from_str(params.get("swap_id").ok_or_else(|| { jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()) })?) - .unwrap(); - resume_swap(swap_id, &context) - .await - .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string())) + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + + resume_swap(swap_id, &context).await }) .unwrap(); module .register_async_method("withdraw_btc", |params, context| async move { let params: HashMap = params.parse()?; + let amount = if let Some(amount_str) = params.get("amount") { Some( ::bitcoin::Amount::from_str_in(amount_str, ::bitcoin::Denomination::Bitcoin) @@ -104,19 +105,20 @@ pub fn register_modules(context: Arc) -> RpcModule> { } else { None }; + let withdraw_address = bitcoin::Address::from_str(params.get("address").ok_or_else(|| { jsonrpsee_core::Error::Custom("Does not contain address".to_string()) })?) - .unwrap(); - withdraw_btc(withdraw_address, amount, &context) - .await - .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string())) + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + + withdraw_btc(withdraw_address, amount, &context).await }) .unwrap(); module .register_async_method("buy_xmr", |params, context| async move { let params: HashMap = params.parse()?; + let bitcoin_change_address = bitcoin::Address::from_str( params.get("bitcoin_change_address").ok_or_else(|| { jsonrpsee_core::Error::Custom( @@ -124,7 +126,8 @@ pub fn register_modules(context: Arc) -> RpcModule> { ) })?, ) - .unwrap(); + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + let monero_receive_address = monero::Address::from_str( params.get("monero_receive_address").ok_or_else(|| { jsonrpsee_core::Error::Custom( @@ -132,19 +135,19 @@ pub fn register_modules(context: Arc) -> RpcModule> { ) })?, ) - .unwrap(); + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + let seller = Multiaddr::from_str(params.get("seller").ok_or_else(|| { jsonrpsee_core::Error::Custom("Does not contain seller".to_string()) })?) - .unwrap(); + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + buy_xmr( bitcoin_change_address, monero_receive_address, seller, &context, - ) - .await - .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string())) + ).await }) .unwrap(); module @@ -154,62 +157,71 @@ pub fn register_modules(context: Arc) -> RpcModule> { Multiaddr::from_str(params.get("rendezvous_point").ok_or_else(|| { jsonrpsee_core::Error::Custom("Does not contain rendezvous_point".to_string()) })?) - .unwrap(); - list_sellers(rendezvous_point, &context) - .await - .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string())) + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + + list_sellers(rendezvous_point, &context).await }) .unwrap(); module } -async fn get_bitcoin_balance(context: &Arc) -> anyhow::Result { +async fn get_bitcoin_balance(context: &Arc) -> Result { let request = Request { params: Params::default(), - cmd: Command::Balance, + cmd: Method::Balance, }; - let balance = request.call(Arc::clone(context)).await.unwrap(); + let balance = request.call(Arc::clone(context)) + .await + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + Ok(balance) } -async fn get_history(context: &Arc) -> anyhow::Result { +async fn get_history(context: &Arc) -> Result { let request = Request { params: Params::default(), - cmd: Command::History, + cmd: Method::History, }; - let history = request.call(Arc::clone(context)).await.unwrap(); + let history = request.call(Arc::clone(context)) + .await + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + Ok(history) } async fn resume_swap( swap_id: Uuid, context: &Arc, -) -> anyhow::Result { +) -> Result { let request = Request { params: Params { swap_id: Some(swap_id), ..Default::default() }, - cmd: Command::Resume, + cmd: Method::Resume, }; - let result = request.call(Arc::clone(context)).await.unwrap(); + let result = request.call(Arc::clone(context)) + .await + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; Ok(result) } async fn withdraw_btc( withdraw_address: bitcoin::Address, amount: Option, context: &Arc, -) -> anyhow::Result { +) -> Result { let request = Request { params: Params { amount, address: Some(withdraw_address), ..Default::default() }, - cmd: Command::WithdrawBtc, + cmd: Method::WithdrawBtc, }; - let result = request.call(Arc::clone(context)).await.unwrap(); + let result = request.call(Arc::clone(context)) + .await + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; Ok(result) } @@ -218,7 +230,7 @@ async fn buy_xmr( monero_receive_address: monero::Address, seller: Multiaddr, context: &Arc, -) -> anyhow::Result { +) -> Result { let request = Request { params: Params { bitcoin_change_address: Some(bitcoin_change_address), @@ -226,23 +238,27 @@ async fn buy_xmr( seller: Some(seller), ..Default::default() }, - cmd: Command::BuyXmr, + cmd: Method::BuyXmr, }; - let swap = request.call(Arc::clone(context)).await.unwrap(); + let swap = request.call(Arc::clone(context)) + .await + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; Ok(swap) } async fn list_sellers( rendezvous_point: Multiaddr, context: &Arc, -) -> anyhow::Result { +) -> Result { let request = Request { params: Params { rendezvous_point: Some(rendezvous_point), ..Default::default() }, - cmd: Command::ListSellers, + cmd: Method::ListSellers, }; - let result = request.call(Arc::clone(context)).await.unwrap(); + let result = request.call(Arc::clone(context)) + .await + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; Ok(result) }