From ee43125bdd7781e21581d8f6d43447d714d708ff Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 11 Feb 2021 15:07:01 +1100 Subject: [PATCH] Add `nectar` binary --- swap/src/bin/nectar.rs | 160 +++++++++++++++++++++++++++++++ swap/src/lib.rs | 1 + swap/src/nectar.rs | 2 + swap/src/nectar/command.rs | 21 +++++ swap/src/nectar/config.rs | 187 +++++++++++++++++++++++++++++++++++++ 5 files changed, 371 insertions(+) create mode 100644 swap/src/bin/nectar.rs create mode 100644 swap/src/nectar.rs create mode 100644 swap/src/nectar/command.rs create mode 100644 swap/src/nectar/config.rs diff --git a/swap/src/bin/nectar.rs b/swap/src/bin/nectar.rs new file mode 100644 index 00000000..f91b7932 --- /dev/null +++ b/swap/src/bin/nectar.rs @@ -0,0 +1,160 @@ +#![warn( + unused_extern_crates, + missing_copy_implementations, + rust_2018_idioms, + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::fallible_impl_from, + clippy::cast_precision_loss, + clippy::cast_possible_wrap, + clippy::dbg_macro +)] +#![forbid(unsafe_code)] +#![allow(non_snake_case)] + +use anyhow::{Context, Result}; +use log::LevelFilter; +use prettytable::{row, Table}; +use std::sync::Arc; +use structopt::StructOpt; +use swap::{ + bitcoin, + database::Database, + execution_params, + execution_params::GetExecutionParams, + fs::default_config_path, + monero, + monero::{CreateWallet, OpenWallet}, + nectar::{ + command::{Arguments, Command}, + config::{ + initial_setup, query_user_for_initial_testnet_config, read_config, Config, + ConfigNotInitialized, + }, + }, + protocol::alice::EventLoop, + seed::Seed, + trace::init_tracing, +}; +use tracing::{info, warn}; + +#[macro_use] +extern crate prettytable; + +const MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME: &str = "swap-tool-blockchain-monitoring-wallet"; +const BITCOIN_NETWORK: bitcoin::Network = bitcoin::Network::Testnet; +const MONERO_NETWORK: monero::Network = monero::Network::Stagenet; + +#[tokio::main] +async fn main() -> Result<()> { + init_tracing(LevelFilter::Debug).expect("initialize tracing"); + + let opt = Arguments::from_args(); + + let config_path = if let Some(config_path) = opt.config { + config_path + } else { + default_config_path()? + }; + + let config = match read_config(config_path.clone())? { + Ok(config) => config, + Err(ConfigNotInitialized {}) => { + initial_setup(config_path.clone(), query_user_for_initial_testnet_config)?; + read_config(config_path)?.expect("after initial setup config can be read") + } + }; + + info!( + "Database and Seed will be stored in directory: {}", + config.data.dir.display() + ); + + let db = Database::open(config.data.dir.join("database").as_path()) + .context("Could not open database")?; + + match opt.cmd { + Command::Start => { + let seed = Seed::from_file_or_generate(&config.data.dir) + .expect("Could not retrieve/initialize seed"); + + let execution_params = execution_params::Testnet::get_execution_params(); + + let (bitcoin_wallet, monero_wallet) = init_wallets(config.clone()).await?; + + let mut event_loop = EventLoop::new( + config.network.listen, + seed, + execution_params, + Arc::new(bitcoin_wallet), + Arc::new(monero_wallet), + Arc::new(db), + ) + .unwrap(); + + info!("Our peer id is {}", event_loop.peer_id()); + + event_loop.run().await; + } + Command::History => { + let mut table = Table::new(); + + table.add_row(row!["SWAP ID", "STATE"]); + + for (swap_id, state) in db.all()? { + table.add_row(row![swap_id, state]); + } + + // Print the table to stdout + table.printstd(); + } + }; + + Ok(()) +} + +async fn init_wallets(config: Config) -> Result<(bitcoin::Wallet, monero::Wallet)> { + let bitcoin_wallet = bitcoin::Wallet::new( + config.bitcoin.wallet_name.as_str(), + config.bitcoin.bitcoind_url, + BITCOIN_NETWORK, + ) + .await?; + let bitcoin_balance = bitcoin_wallet.balance().await?; + info!( + "Connection to Bitcoin wallet succeeded, balance: {}", + bitcoin_balance + ); + + let monero_wallet = monero::Wallet::new(config.monero.wallet_rpc_url.clone(), MONERO_NETWORK); + + // Setup the temporary Monero wallet necessary for monitoring the blockchain + let open_monitoring_wallet_response = monero_wallet + .open_wallet(MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME) + .await; + if open_monitoring_wallet_response.is_err() { + monero_wallet + .create_wallet(MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME) + .await + .context(format!( + "Unable to create Monero wallet for blockchain monitoring.\ + Please ensure that the monero-wallet-rpc is available at {}", + config.monero.wallet_rpc_url + ))?; + + info!( + "Created Monero wallet for blockchain monitoring with name {}", + MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME + ); + } else { + info!( + "Opened Monero wallet for blockchain monitoring with name {}", + MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME + ); + } + + let _test_wallet_connection = monero_wallet.inner.block_height().await?; + info!("The Monero wallet RPC is set up correctly!"); + + Ok((bitcoin_wallet, monero_wallet)) +} diff --git a/swap/src/lib.rs b/swap/src/lib.rs index b23fe07c..9517602b 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -22,6 +22,7 @@ pub mod database; pub mod execution_params; pub mod fs; pub mod monero; +pub mod nectar; pub mod network; pub mod protocol; pub mod seed; diff --git a/swap/src/nectar.rs b/swap/src/nectar.rs new file mode 100644 index 00000000..6d982acc --- /dev/null +++ b/swap/src/nectar.rs @@ -0,0 +1,2 @@ +pub mod command; +pub mod config; diff --git a/swap/src/nectar/command.rs b/swap/src/nectar/command.rs new file mode 100644 index 00000000..a34ae43d --- /dev/null +++ b/swap/src/nectar/command.rs @@ -0,0 +1,21 @@ +use std::path::PathBuf; + +#[derive(structopt::StructOpt, Debug)] +pub struct Arguments { + #[structopt( + long = "config", + help = "Provide a custom path to the configuration file. The configuration file must be a toml file.", + parse(from_os_str) + )] + pub config: Option, + + #[structopt(subcommand)] + pub cmd: Command, +} + +#[derive(structopt::StructOpt, Debug)] +#[structopt(name = "xmr_btc-swap", about = "XMR BTC atomic swap")] +pub enum Command { + Start, + History, +} diff --git a/swap/src/nectar/config.rs b/swap/src/nectar/config.rs new file mode 100644 index 00000000..913d0986 --- /dev/null +++ b/swap/src/nectar/config.rs @@ -0,0 +1,187 @@ +use crate::fs::{default_data_dir, ensure_directory_exists}; +use anyhow::{Context, Result}; +use config::ConfigError; +use dialoguer::{theme::ColorfulTheme, Input}; +use libp2p::core::Multiaddr; +use serde::{Deserialize, Serialize}; +use std::{ + ffi::OsStr, + fs, + path::{Path, PathBuf}, +}; +use tracing::info; +use url::Url; + +const DEFAULT_BITCOIND_TESTNET_URL: &str = "http://127.0.0.1:18332"; +const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc"; +const DEFAULT_LISTEN_ADDRESS: &str = "/ip4/0.0.0.0/tcp/9939"; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] +pub struct Config { + pub data: Data, + pub network: Network, + pub bitcoin: Bitcoin, + pub monero: Monero, +} + +impl Config { + pub fn read(config_file: D) -> Result + where + D: AsRef, + { + let config_file = Path::new(&config_file); + + let mut config = config::Config::new(); + config.merge(config::File::from(config_file))?; + config.try_into() + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Data { + pub dir: PathBuf, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Network { + pub listen: Multiaddr, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Bitcoin { + pub bitcoind_url: Url, + pub wallet_name: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Monero { + pub wallet_rpc_url: Url, +} + +#[derive(thiserror::Error, Debug, Clone, Copy)] +#[error("config not initialized")] +pub struct ConfigNotInitialized {} + +pub fn read_config(config_path: PathBuf) -> Result> { + if config_path.exists() { + info!( + "Using config file at default path: {}", + config_path.display() + ); + } else { + return Ok(Err(ConfigNotInitialized {})); + } + + let file = Config::read(&config_path) + .with_context(|| format!("failed to read config file {}", config_path.display()))?; + + Ok(Ok(file)) +} + +pub fn initial_setup(config_path: PathBuf, config_file: F) -> Result<()> +where + F: Fn() -> Result, +{ + info!("Config file not found, running initial setup..."); + ensure_directory_exists(config_path.as_path())?; + let initial_config = config_file()?; + + let toml = toml::to_string(&initial_config)?; + fs::write(&config_path, toml)?; + + info!( + "Initial setup complete, config file created at {} ", + config_path.as_path().display() + ); + Ok(()) +} + +pub fn query_user_for_initial_testnet_config() -> Result { + println!(); + let data_dir = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter data directory for nectar or hit return to use default") + .default( + default_data_dir() + .context("Not default data dir value for this system")? + .to_str() + .context("Unsupported characters in default path")? + .to_string(), + ) + .interact_text()?; + let data_dir = data_dir.as_str().parse()?; + + let listen_address = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter multiaddress on which nectar should list for peer-to-peer communications or hit return to use default") + .default(DEFAULT_LISTEN_ADDRESS.to_owned()) + .interact_text()?; + let listen_address = listen_address.as_str().parse()?; + + let bitcoind_url = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter Bitcoind URL (including username and password if applicable) or hit return to use default") + .default(DEFAULT_BITCOIND_TESTNET_URL.to_owned()) + .interact_text()?; + let bitcoind_url = bitcoind_url.as_str().parse()?; + + let bitcoin_wallet_name = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter Bitcoind wallet name") + .interact_text()?; + + let monero_wallet_rpc_url = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter Monero Wallet RPC URL or hit enter to use default") + .default(DEFAULT_MONERO_WALLET_RPC_TESTNET_URL.to_owned()) + .interact_text()?; + let monero_wallet_rpc_url = monero_wallet_rpc_url.as_str().parse()?; + println!(); + + Ok(Config { + data: Data { dir: data_dir }, + network: Network { + listen: listen_address, + }, + bitcoin: Bitcoin { + bitcoind_url, + wallet_name: bitcoin_wallet_name, + }, + monero: Monero { + wallet_rpc_url: monero_wallet_rpc_url, + }, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + use tempfile::tempdir; + + #[test] + fn config_roundtrip() { + let temp_dir = tempdir().unwrap().path().to_path_buf(); + let config_path = Path::join(&temp_dir, "config.toml"); + + let expected = Config { + data: Data { + dir: Default::default(), + }, + network: Network { + listen: "/ip4/0.0.0.0/tcp/9939".parse().unwrap(), + }, + bitcoin: Bitcoin { + bitcoind_url: Url::from_str("http://127.0.0.1:18332").unwrap(), + wallet_name: "alice".to_string(), + }, + monero: Monero { + wallet_rpc_url: Url::from_str("http://127.0.0.1:38083/json_rpc").unwrap(), + }, + }; + + initial_setup(config_path.clone(), || Ok(expected.clone())).unwrap(); + let actual = read_config(config_path).unwrap().unwrap(); + + assert_eq!(expected, actual); + } +}