From 4413a8d489310a3548caf9a0eed9d45275c072a7 Mon Sep 17 00:00:00 2001 From: Lorenzo Tucci Date: Sat, 12 Nov 2022 14:14:11 +0100 Subject: [PATCH] saving: implementing internal api shared by cli and rpc server --- Cargo.lock | 22 +- swap/src/api.rs | 213 ++++++++++++++ swap/src/bin/swap.rs | 634 +--------------------------------------- swap/src/cli/command.rs | 334 ++++++++++----------- swap/src/lib.rs | 1 + swap/src/rpc/mod.rs | 16 + 6 files changed, 410 insertions(+), 810 deletions(-) create mode 100644 swap/src/api.rs create mode 100644 swap/src/rpc/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 1c91e978..506f812c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1601,7 +1601,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa 1.0.1", + "itoa", ] [[package]] @@ -1641,7 +1641,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.1", + "itoa", "pin-project-lite 0.2.9", "socket2 0.4.7", "tokio", @@ -1760,12 +1760,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" - [[package]] name = "itoa" version = "1.0.1" @@ -1902,7 +1896,7 @@ dependencies = [ "soketto", "tokio", "tokio-stream", - "tokio-util 0.7.2", + "tokio-util 0.7.3", "tracing", "tracing-futures", ] @@ -2682,7 +2676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f85842b073145726190373213c63f852020fb884c841a3a1f390637267a2fb8c" dependencies = [ "dtoa", - "itoa 1.0.1", + "itoa", "open-metrics-client-derive-text-encode", "owning_ref", ] @@ -3676,7 +3670,7 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ - "itoa 1.0.1", + "itoa", "ryu", "serde", ] @@ -3688,7 +3682,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.1", + "itoa", "ryu", "serde", ] @@ -3975,7 +3969,7 @@ dependencies = [ "hashlink", "hex", "indexmap", - "itoa 1.0.1", + "itoa", "libc", "libsqlite3-sys", "log", @@ -4330,7 +4324,7 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ - "itoa 1.0.1", + "itoa", "serde", "time-core", "time-macros", diff --git a/swap/src/api.rs b/swap/src/api.rs new file mode 100644 index 00000000..090c8831 --- /dev/null +++ b/swap/src/api.rs @@ -0,0 +1,213 @@ +use anyhow::{bail, Context, Result}; +use comfy_table::Table; +use jsonrpsee::http_server::{HttpServerHandle}; +use qrcode::render::unicode; +use qrcode::QrCode; +use std::cmp::min; +use crate::network::rendezvous::XmrBtcNamespace; +use std::net::SocketAddr; +use libp2p::core::Multiaddr; +use std::convert::TryInto; +use crate::bitcoin::Amount; +use std::env; +use std::future::Future; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use crate::bitcoin::TxLock; +use crate::cli::command::{parse_args_and_apply_defaults, Command, ParseResult, Options}; +use crate::cli::{list_sellers, EventLoop, SellerStatus}; +use crate::common::check_latest_version; +use crate::database::open_db; +use crate::env::Config; +use crate::libp2p_ext::MultiAddrExt; +use crate::network::quote::{BidQuote, ZeroQuoteReceived}; +use crate::network::swarm; +use crate::protocol::bob; +use crate::protocol::bob::{BobState, Swap}; +use crate::seed::Seed; +use crate::rpc; +use crate::{bitcoin, cli, monero}; +use url::Url; +use uuid::Uuid; + +#[derive(Debug, PartialEq)] +pub struct InternalApi { + pub opts: Options, + pub params: Params, + pub cmd: Command, +} + +#[derive(Debug, PartialEq, Default)] +pub struct Params { + pub bitcoin_electrum_rpc_url: Option, + pub bitcoin_target_block: Option, + pub seller: Option, + pub bitcoin_change_address: Option, + pub monero_receive_address: Option, + pub monero_daemon_address: Option, + pub tor_socks5_port: Option, + pub namespace: Option, + pub rendezvous_point: Option, + pub swap_id: Option, + pub server_address: Option, + pub amount: Option, + pub address: Option, +} + +impl InternalApi { + pub async fn call() -> Result<()> { + Ok(()) + } +} + +async fn init_bitcoin_wallet( + electrum_rpc_url: Url, + seed: &Seed, + data_dir: PathBuf, + env_config: Config, + bitcoin_target_block: usize, +) -> Result { + let wallet_dir = data_dir.join("wallet"); + + let wallet = bitcoin::Wallet::new( + electrum_rpc_url.clone(), + &wallet_dir, + seed.derive_extended_private_key(env_config.bitcoin_network)?, + env_config, + bitcoin_target_block, + ) + .await + .context("Failed to initialize Bitcoin wallet")?; + + wallet.sync().await?; + + 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) +} +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, + monero_daemon_address: String, + env_config: Config, +) -> Result<(monero::Wallet, monero::WalletRpcProcess)> { + let network = env_config.monero_network; + + const MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME: &str = "swap-tool-blockchain-monitoring-wallet"; + + let monero_wallet_rpc = monero::WalletRpc::new(data_dir.join("monero")).await?; + + let monero_wallet_rpc_process = monero_wallet_rpc + .run(network, monero_daemon_address.as_str()) + .await?; + + let monero_wallet = monero::Wallet::open_or_create( + monero_wallet_rpc_process.endpoint(), + MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(), + env_config, + ) + .await?; + + Ok((monero_wallet, monero_wallet_rpc_process)) +} diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 287361ab..1a3f6541 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -26,7 +26,7 @@ use std::sync::Arc; use std::time::Duration; use std::net::SocketAddr; use swap::bitcoin::TxLock; -use swap::cli::command::{parse_args_and_apply_defaults, Arguments, Command, ParseResult}; +use swap::cli::command::{parse_args_and_apply_defaults, Options, Command, ParseResult}; use swap::cli::{list_sellers, EventLoop, SellerStatus}; use swap::common::check_latest_version; use swap::database::open_db; @@ -44,642 +44,10 @@ use uuid::Uuid; #[tokio::main] async fn main() -> Result<()> { - let Arguments { - env_config, - data_dir, - debug, - json, - cmd, - } = match parse_args_and_apply_defaults(env::args_os())? { - ParseResult::Arguments(args) => *args, - ParseResult::PrintAndExitZero { message } => { - println!("{}", message); - std::process::exit(0); - } - }; - - if let Err(e) = check_latest_version(env!("CARGO_PKG_VERSION")).await { - eprintln!("{}", e); - } - - match cmd { - Command::BuyXmr { - seller, - bitcoin_electrum_rpc_url, - bitcoin_target_block, - bitcoin_change_address, - monero_receive_address, - monero_daemon_address, - tor_socks5_port, - namespace, - } => { - let swap_id = Uuid::new_v4(); - - cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?; - - let db = open_db(data_dir.join("sqlite")).await?; - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir.clone(), - env_config, - bitcoin_target_block, - ) - .await?; - let (monero_wallet, _process) = - init_monero_wallet(data_dir, monero_daemon_address, env_config).await?; - let bitcoin_wallet = Arc::new(bitcoin_wallet); - let seller_peer_id = seller - .extract_peer_id() - .context("Seller address must contain peer ID")?; - 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(), namespace), - ); - let mut swarm = - swarm::cli(seed.derive_libp2p_identity(), tor_socks5_port, 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( - 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"); - - db.insert_peer_id(swap_id, seller_peer_id).await?; - db.insert_monero_address(swap_id, monero_receive_address) - .await?; - - let swap = Swap::new( - db, - swap_id, - bitcoin_wallet, - Arc::new(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")?; - } - } - } - Command::History => { - cli::tracing::init(debug, json, data_dir.join("logs"), None)?; - - let db = open_db(data_dir.join("sqlite")).await?; - let swaps = db.all().await?; - - if json { - for (swap_id, state) in swaps { - let state: BobState = state.try_into()?; - tracing::info!(swap_id=%swap_id.to_string(), state=%state.to_string(), "Read swap state from database"); - } - } else { - let mut table = Table::new(); - - table.set_header(vec!["SWAP ID", "STATE"]); - - for (swap_id, state) in swaps { - let state: BobState = state.try_into()?; - table.add_row(vec![swap_id.to_string(), state.to_string()]); - } - - println!("{}", table); - } - } - Command::Config => { - cli::tracing::init(debug, json, data_dir.join("logs"), None)?; - - 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"); - } - Command::WithdrawBtc { - bitcoin_electrum_rpc_url, - bitcoin_target_block, - amount, - address, - } => { - cli::tracing::init(debug, json, data_dir.join("logs"), None)?; - - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir.clone(), - env_config, - bitcoin_target_block, - ) - .await?; - - 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, "withdraw").await?; - } - - Command::Balance { - bitcoin_electrum_rpc_url, - bitcoin_target_block, - } => { - cli::tracing::init(debug, json, data_dir.join("logs"), None)?; - - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir.clone(), - env_config, - bitcoin_target_block, - ) - .await?; - - let bitcoin_balance = bitcoin_wallet.balance().await?; - tracing::info!( - balance = %bitcoin_balance, - "Checked Bitcoin balance", - ); - } - Command::StartDaemon { - server_address, - } => { - let handle = rpc::run_server(server_address).await?; - loop { - - } - - }, - Command::Resume { - swap_id, - bitcoin_electrum_rpc_url, - bitcoin_target_block, - monero_daemon_address, - tor_socks5_port, - namespace, - } => { - cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?; - - let db = open_db(data_dir.join("sqlite")).await?; - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir.clone(), - env_config, - bitcoin_target_block, - ) - .await?; - let (monero_wallet, _process) = - init_monero_wallet(data_dir, monero_daemon_address, env_config).await?; - let bitcoin_wallet = Arc::new(bitcoin_wallet); - - let seller_peer_id = db.get_peer_id(swap_id).await?; - let seller_addresses = db.get_addresses(seller_peer_id).await?; - - let behaviour = cli::Behaviour::new( - seller_peer_id, - env_config, - bitcoin_wallet.clone(), - (seed.derive_libp2p_identity(), namespace), - ); - let mut swarm = - swarm::cli(seed.derive_libp2p_identity(), 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)?; - let handle = tokio::spawn(event_loop.run()); - - let monero_receive_address = db.get_monero_address(swap_id).await?; - let swap = Swap::from_db( - db, - swap_id, - bitcoin_wallet, - Arc::new(monero_wallet), - 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?; - } - } - } - Command::Cancel { - swap_id, - bitcoin_electrum_rpc_url, - bitcoin_target_block, - } => { - cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?; - - let db = open_db(data_dir.join("sqlite")).await?; - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir, - env_config, - bitcoin_target_block, - ) - .await?; - - let (txid, _) = cli::cancel(swap_id, Arc::new(bitcoin_wallet), db).await?; - tracing::debug!("Cancel transaction successfully published with id {}", txid); - } - Command::Refund { - swap_id, - bitcoin_electrum_rpc_url, - bitcoin_target_block, - } => { - cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?; - - let db = open_db(data_dir.join("sqlite")).await?; - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir, - env_config, - bitcoin_target_block, - ) - .await?; - - cli::refund(swap_id, Arc::new(bitcoin_wallet), db).await?; - } - Command::ListSellers { - rendezvous_point, - namespace, - tor_socks5_port, - } => { - let rendezvous_node_peer_id = rendezvous_point - .extract_peer_id() - .context("Rendezvous node address must contain peer ID")?; - - cli::tracing::init(debug, json, data_dir.join("logs"), None)?; - - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - let identity = seed.derive_libp2p_identity(); - - let sellers = list_sellers( - rendezvous_node_peer_id, - rendezvous_point, - namespace, - tor_socks5_port, - identity, - ) - .await?; - - if json { - 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" - ); - } - } - } - } else { - let mut table = Table::new(); - - table.set_header(vec![ - "PRICE", - "MIN_QUANTITY", - "MAX_QUANTITY", - "STATUS", - "ADDRESS", - ]); - - for seller in sellers { - let row = match seller.status { - SellerStatus::Online(quote) => { - vec![ - quote.price.to_string(), - quote.min_quantity.to_string(), - quote.max_quantity.to_string(), - "Online".to_owned(), - seller.multiaddr.to_string(), - ] - } - SellerStatus::Unreachable => { - vec![ - "???".to_owned(), - "???".to_owned(), - "???".to_owned(), - "Unreachable".to_owned(), - seller.multiaddr.to_string(), - ] - } - }; - - table.add_row(row); - } - - println!("{}", table); - } - } - Command::ExportBitcoinWallet { - bitcoin_electrum_rpc_url, - bitcoin_target_block, - } => { - cli::tracing::init(debug, json, data_dir.join("logs"), None)?; - - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir.clone(), - env_config, - bitcoin_target_block, - ) - .await?; - let wallet_export = bitcoin_wallet.wallet_export("cli").await?; - tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet"); - } - Command::MoneroRecovery { swap_id } => { - cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?; - - let db = open_db(data_dir.join("sqlite")).await?; - - let swap_state: BobState = db.get_state(swap_id).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( - 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); - } - } - } - }; Ok(()) } -async fn init_bitcoin_wallet( - electrum_rpc_url: Url, - seed: &Seed, - data_dir: PathBuf, - env_config: Config, - bitcoin_target_block: usize, -) -> Result { - let xprivkey = seed.derive_extended_private_key(env_config.bitcoin_network)?; - - let wallet = bitcoin::Wallet::new( - electrum_rpc_url.clone(), - data_dir, - xprivkey, - env_config, - bitcoin_target_block, - ) - .await - .context("Failed to initialize Bitcoin wallet")?; - - wallet.sync().await?; - - Ok(wallet) -} - -async fn init_monero_wallet( - data_dir: PathBuf, - monero_daemon_address: String, - env_config: Config, -) -> Result<(monero::Wallet, monero::WalletRpcProcess)> { - let network = env_config.monero_network; - - const MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME: &str = "swap-tool-blockchain-monitoring-wallet"; - - let monero_wallet_rpc = monero::WalletRpc::new(data_dir.join("monero")).await?; - - let monero_wallet_rpc_process = monero_wallet_rpc - .run(network, monero_daemon_address.as_str()) - .await?; - - let monero_wallet = monero::Wallet::open_or_create( - monero_wallet_rpc_process.endpoint(), - MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(), - env_config, - ) - .await?; - - Ok((monero_wallet, monero_wallet_rpc_process)) -} - -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) -} - -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)) -} #[cfg(test)] mod tests { diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index b0cf7090..d4e129fb 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -3,6 +3,7 @@ use crate::env::GetConfig; use crate::fs::system_data_dir; use crate::network::rendezvous::XmrBtcNamespace; use crate::{env, monero}; +use crate::api::{InternalApi, Params}; use anyhow::{bail, Context, Result}; use bitcoin::{Address, AddressType}; use libp2p::core::Multiaddr; @@ -30,19 +31,18 @@ const DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET: usize = 1; const DEFAULT_TOR_SOCKS5_PORT: &str = "9050"; #[derive(Debug, PartialEq)] -pub struct Arguments { +pub struct Options { pub env_config: env::Config, pub debug: bool, pub json: bool, pub data_dir: PathBuf, - pub cmd: Command, } /// Represents the result of parsing the command-line parameters. #[derive(Debug, PartialEq)] pub enum ParseResult { /// The arguments we were invoked in. - Arguments(Box), + InternalApi(Box), /// A flag or command was given that does not need further processing other /// than printing the provided message. /// @@ -70,7 +70,7 @@ where let is_testnet = args.testnet; let data = args.data; - let arguments = match args.cmd { + let api = match args.cmd { RawCommand::BuyXmr { seller: Seller { seller }, bitcoin, @@ -87,35 +87,49 @@ where let bitcoin_change_address = validate_bitcoin_address(bitcoin_change_address, is_testnet)?; - Arguments { + InternalApi { + opts: Options { + env_config: env_config_from(is_testnet), + debug, + json, + data_dir: data::data_dir_from(data, is_testnet)?, + }, + params: Params { + seller: Some(seller), + bitcoin_electrum_rpc_url: Some(bitcoin_electrum_rpc_url), + bitcoin_target_block: Some(bitcoin_target_block), + bitcoin_change_address: Some(bitcoin_change_address), + monero_receive_address: Some(monero_receive_address), + monero_daemon_address: Some(monero_daemon_address), + tor_socks5_port: Some(tor_socks5_port), + namespace: Some(XmrBtcNamespace::from_is_testnet(is_testnet)), + ..Default::default() + }, + cmd: Command::BuyXmr, + } + } + RawCommand::History => InternalApi { + opts: Options { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::BuyXmr { - seller, - bitcoin_electrum_rpc_url, - bitcoin_target_block, - bitcoin_change_address, - monero_receive_address, - monero_daemon_address, - tor_socks5_port, - namespace: XmrBtcNamespace::from_is_testnet(is_testnet), - }, - } - } - RawCommand::History => Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, + }, + params: Params { + ..Default::default() + }, cmd: Command::History, }, - RawCommand::Config => Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, + RawCommand::Config => InternalApi { + opts: Options { + env_config: env_config_from(is_testnet), + debug, + json, + data_dir: data::data_dir_from(data, is_testnet)?, + }, + params: Params { + ..Default::default() + }, cmd: Command::Config, }, RawCommand::Balance { @@ -128,31 +142,39 @@ where let (bitcoin_electrum_rpc_url, bitcoin_target_block) = bitcoin.apply_defaults(is_testnet)?; - Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::Balance { - bitcoin_electrum_rpc_url, - bitcoin_target_block, + InternalApi { + opts: Options { + env_config: env_config_from(is_testnet), + debug, + json, + data_dir: data::data_dir_from(data, is_testnet)?, }, + params: Params { + bitcoin_electrum_rpc_url: Some(bitcoin_electrum_rpc_url), + bitcoin_target_block: Some(bitcoin_target_block), + ..Default::default() + }, + cmd: Command::Balance, } } RawCommand::StartDaemon { server_address, } => { let server_address = "127.0.0.1:1234".parse()?; - Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::StartDaemon { - server_address, - }, - } + InternalApi { + opts: Options { + env_config: env_config_from(is_testnet), + debug, + json, + data_dir: data::data_dir_from(data, is_testnet)?, + }, + params: Params { + server_address: Some(server_address), + ..Default::default() + }, + cmd: Command::StartDaemon, + } } RawCommand::WithdrawBtc { bitcoin, @@ -162,17 +184,21 @@ where let (bitcoin_electrum_rpc_url, bitcoin_target_block) = bitcoin.apply_defaults(is_testnet)?; - Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::WithdrawBtc { - bitcoin_electrum_rpc_url, - bitcoin_target_block, - amount, - address: bitcoin_address(address, is_testnet)?, + InternalApi { + opts: Options { + env_config: env_config_from(is_testnet), + debug, + json, + data_dir: data::data_dir_from(data, is_testnet)?, }, + params: Params { + bitcoin_electrum_rpc_url: Some(bitcoin_electrum_rpc_url), + bitcoin_target_block: Some(bitcoin_target_block), + amount, + address: Some(bitcoin_address(address, is_testnet)?), + ..Default::default() + }, + cmd: Command::WithdrawBtc, } } RawCommand::Resume { @@ -185,19 +211,24 @@ where bitcoin.apply_defaults(is_testnet)?; let monero_daemon_address = monero.apply_defaults(is_testnet); - Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::Resume { - swap_id, - bitcoin_electrum_rpc_url, - bitcoin_target_block, - monero_daemon_address, - tor_socks5_port, - namespace: XmrBtcNamespace::from_is_testnet(is_testnet), + InternalApi { + opts: Options { + env_config: env_config_from(is_testnet), + debug, + json, + data_dir: data::data_dir_from(data, is_testnet)?, + }, + params: Params { + swap_id: Some(swap_id), + bitcoin_electrum_rpc_url: Some(bitcoin_electrum_rpc_url), + bitcoin_target_block: Some(bitcoin_target_block), + monero_daemon_address: Some(monero_daemon_address), + tor_socks5_port: Some(tor_socks5_port), + namespace: Some(XmrBtcNamespace::from_is_testnet(is_testnet)), + ..Default::default() + }, + cmd: Command::Resume, } } RawCommand::Cancel { @@ -207,16 +238,20 @@ where let (bitcoin_electrum_rpc_url, bitcoin_target_block) = bitcoin.apply_defaults(is_testnet)?; - Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::Cancel { - swap_id, - bitcoin_electrum_rpc_url, - bitcoin_target_block, + InternalApi { + opts: Options { + env_config: env_config_from(is_testnet), + debug, + json, + data_dir: data::data_dir_from(data, is_testnet)?, }, + params: Params { + swap_id: Some(swap_id), + bitcoin_electrum_rpc_url: Some(bitcoin_electrum_rpc_url), + bitcoin_target_block: Some(bitcoin_target_block), + ..Default::default() + }, + cmd: Command::Cancel, } } RawCommand::Refund { @@ -226,121 +261,94 @@ where let (bitcoin_electrum_rpc_url, bitcoin_target_block) = bitcoin.apply_defaults(is_testnet)?; - Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::Refund { - swap_id, - bitcoin_electrum_rpc_url, - bitcoin_target_block, + InternalApi { + opts: Options { + env_config: env_config_from(is_testnet), + debug, + json, + data_dir: data::data_dir_from(data, is_testnet)?, }, + params: Params { + swap_id: Some(swap_id), + bitcoin_electrum_rpc_url: Some(bitcoin_electrum_rpc_url), + bitcoin_target_block: Some(bitcoin_target_block), + ..Default::default() + }, + cmd: Command::Refund, + } } RawCommand::ListSellers { rendezvous_point, tor: Tor { tor_socks5_port }, - } => Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::ListSellers { - rendezvous_point, - tor_socks5_port, - namespace: XmrBtcNamespace::from_is_testnet(is_testnet), + } => InternalApi { + opts: Options { + env_config: env_config_from(is_testnet), + debug, + json, + data_dir: data::data_dir_from(data, is_testnet)?, }, + params: Params { + rendezvous_point: Some(rendezvous_point), + tor_socks5_port: Some(tor_socks5_port), + namespace: Some(XmrBtcNamespace::from_is_testnet(is_testnet)), + ..Default::default() + }, + cmd: Command::ListSellers, }, RawCommand::ExportBitcoinWallet { bitcoin } => { let (bitcoin_electrum_rpc_url, bitcoin_target_block) = bitcoin.apply_defaults(is_testnet)?; - Arguments { + InternalApi { + opts: Options { + env_config: env_config_from(is_testnet), + debug, + json, + data_dir: data::data_dir_from(data, is_testnet)?, + }, + params: Params { + bitcoin_target_block: Some(bitcoin_target_block), + bitcoin_electrum_rpc_url: Some(bitcoin_electrum_rpc_url), + ..Default::default() + }, + cmd: Command::ExportBitcoinWallet, + } + }, + RawCommand::MoneroRecovery { swap_id } => InternalApi { + opts: Options { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::ExportBitcoinWallet { - bitcoin_electrum_rpc_url, - bitcoin_target_block, - }, - } - }, - RawCommand::MoneroRecovery { swap_id } => Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::MoneroRecovery { - swap_id: swap_id.swap_id, }, + params: Params { + swap_id: Some(swap_id.swap_id), + ..Default::default() + }, + cmd: Command::MoneroRecovery, }, }; - Ok(ParseResult::Arguments(Box::new(arguments))) + Ok(ParseResult::InternalApi(Box::new(api))) } - #[derive(Debug, PartialEq)] pub enum Command { - BuyXmr { - seller: Multiaddr, - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - bitcoin_change_address: bitcoin::Address, - monero_receive_address: monero::Address, - monero_daemon_address: String, - tor_socks5_port: u16, - namespace: XmrBtcNamespace, - }, + BuyXmr, History, Config, - WithdrawBtc { - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - amount: Option, - address: Address, - }, - Balance { - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - }, - StartDaemon { - server_address: SocketAddr, - - }, - Resume { - swap_id: Uuid, - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - monero_daemon_address: String, - tor_socks5_port: u16, - namespace: XmrBtcNamespace, - }, - Cancel { - swap_id: Uuid, - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - }, - Refund { - swap_id: Uuid, - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - }, - ListSellers { - rendezvous_point: Multiaddr, - namespace: XmrBtcNamespace, - tor_socks5_port: u16, - }, - ExportBitcoinWallet { - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - }, - MoneroRecovery { - swap_id: Uuid, - }, + WithdrawBtc, + Balance, + Resume, + Cancel, + Refund, + ListSellers, + ExportBitcoinWallet, + MoneroRecovery, + StartDaemon, } + #[derive(structopt::StructOpt, Debug)] #[structopt( name = "swap", diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 2bf932a5..c04c7059 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -24,6 +24,7 @@ pub mod database; pub mod env; pub mod fs; pub mod kraken; +pub mod api; pub mod libp2p_ext; pub mod monero; pub mod network; diff --git a/swap/src/rpc/mod.rs b/swap/src/rpc/mod.rs new file mode 100644 index 00000000..d83827ff --- /dev/null +++ b/swap/src/rpc/mod.rs @@ -0,0 +1,16 @@ +use std::net::SocketAddr; +use jsonrpsee::http_server::{RpcModule, HttpServerBuilder, HttpServerHandle}; + +pub async fn run_server(server_address: SocketAddr) -> anyhow::Result<(SocketAddr, HttpServerHandle)> { + let server = HttpServerBuilder::default().build(server_address).await?; + let mut module = RpcModule::new(()); + module.register_async_method("balance", |_, _| get_balance())?; + + let addr = server.local_addr()?; + let server_handle = server.start(module)?; + Ok((addr, server_handle)) +} + +async fn get_balance() -> Result<&'static str, jsonrpsee::core::Error> { + Ok("hey") +}