diff --git a/Cargo.lock b/Cargo.lock index 26c27b34..55440813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -466,7 +466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "244fbce0d47e97e8ef2f63b81d5e05882cb518c68531eb33194990d7b7e85845" dependencies = [ "stream-cipher", - "zeroize", + "zeroize 1.2.0", ] [[package]] @@ -479,7 +479,7 @@ dependencies = [ "chacha20", "poly1305", "stream-cipher", - "zeroize", + "zeroize 1.2.0", ] [[package]] @@ -506,6 +506,18 @@ dependencies = [ "bitflags", ] +[[package]] +name = "config" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" +dependencies = [ + "lazy_static", + "nom", + "serde", + "toml", +] + [[package]] name = "conquer-once" version = "0.3.1" @@ -521,6 +533,22 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e763eef8846b13b380f37dfecda401770b0ca4e56e95170237bd7c25c7db3582" +[[package]] +name = "console" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50aab2529019abfabfa93f1e6c41ef392f91fbf179b347a7e96abb524884a08" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "unicode-width", + "winapi 0.3.9", + "winapi-util", +] + [[package]] name = "const_fn" version = "0.4.4" @@ -677,7 +705,7 @@ dependencies = [ "rand_core 0.5.1", "serde", "subtle 2.4.0", - "zeroize", + "zeroize 1.2.0", ] [[package]] @@ -690,7 +718,7 @@ dependencies = [ "digest 0.9.0", "rand_core 0.5.1", "subtle 2.4.0", - "zeroize", + "zeroize 1.2.0", ] [[package]] @@ -710,6 +738,18 @@ dependencies = [ "syn", ] +[[package]] +name = "dialoguer" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f807b2943dc90f9747497d9d65d7e92472149be0b88bf4ce1201b4ac979c26" +dependencies = [ + "console", + "lazy_static", + "tempfile", + "zeroize 0.9.3", +] + [[package]] name = "digest" version = "0.8.1" @@ -741,6 +781,16 @@ dependencies = [ "sha2 0.8.2", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + [[package]] name = "dirs" version = "1.0.5" @@ -752,6 +802,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99de365f605554ae33f115102a02057d4fc18b01f3284d6870be0938743cfe7d" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "discard" version = "1.0.4" @@ -789,7 +850,7 @@ dependencies = [ "serde", "serde_bytes", "sha2 0.9.2", - "zeroize", + "zeroize 1.2.0", ] [[package]] @@ -1526,6 +1587,19 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexical-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 0.1.10", + "ryu", + "static_assertions 1.1.0", +] + [[package]] name = "libc" version = "0.2.81" @@ -1590,7 +1664,7 @@ dependencies = [ "thiserror", "unsigned-varint", "void", - "zeroize", + "zeroize 1.2.0", ] [[package]] @@ -1651,7 +1725,7 @@ dependencies = [ "snow", "static_assertions 1.1.0", "x25519-dalek", - "zeroize", + "zeroize 1.2.0", ] [[package]] @@ -2020,6 +2094,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "num" version = "0.1.42" @@ -3291,10 +3376,13 @@ dependencies = [ "base64 0.12.3", "bitcoin", "bitcoin-harness", + "config", "conquer-once", "cross-curve-dleq", "curve25519-dalek 2.1.0", "derivative", + "dialoguer", + "directories-next", "ecdsa_fun", "ed25519-dalek", "futures", @@ -3326,6 +3414,7 @@ dependencies = [ "thiserror", "time", "tokio", + "toml", "tracing", "tracing-core", "tracing-futures", @@ -3384,6 +3473,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "terminal_size" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "testcontainers" version = "0.11.0" @@ -3580,6 +3679,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + [[package]] name = "tower-service" version = "0.3.0" @@ -3777,6 +3885,7 @@ dependencies = [ "idna", "matches", "percent-encoding", + "serde", ] [[package]] @@ -3959,6 +4068,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -3992,7 +4110,7 @@ checksum = "bc614d95359fd7afc321b66d2107ede58b246b844cf5d8a0adcca413e439f088" dependencies = [ "curve25519-dalek 3.0.0", "rand_core 0.5.1", - "zeroize", + "zeroize 1.2.0", ] [[package]] @@ -4009,6 +4127,12 @@ dependencies = [ "static_assertions 1.1.0", ] +[[package]] +name = "zeroize" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45af6a010d13e4cf5b54c94ba5a2b2eba5596b9e46bf5875612d332a1f2b3f86" + [[package]] name = "zeroize" version = "1.2.0" diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 028c5796..406a3c71 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -14,10 +14,13 @@ backoff = { version = "0.2", features = ["tokio"] } base64 = "0.12" bitcoin = { version = "0.25", features = ["rand", "use-serde"] } bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "864b55fcba2e770105f135781dd2e3002c503d12" } +config = { version = "0.10", default-features = false, features = ["toml"] } conquer-once = "0.3" cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "eddcdea1d1f16fa33ef581d1744014ece535c920", features = ["serde"] } curve25519-dalek = "2" derivative = "2" +dialoguer = "0.7" +directories-next = "2" ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "cdfbc766045ea678a41780919d6228dd5acee3be", features = ["libsecp_compat", "serde"] } ed25519-dalek = { version = "1.0.0-pre.4", features = ["serde"] }# Cannot be 1 because they depend on curve25519-dalek version 3 futures = { version = "0.3", default-features = false } @@ -44,12 +47,13 @@ tempfile = "3" thiserror = "1" time = "0.2" tokio = { version = "0.2", features = ["rt-threaded", "time", "macros", "sync"] } +toml = "0.5" tracing = { version = "0.1", features = ["attributes"] } tracing-core = "0.1" tracing-futures = { version = "0.2", features = ["std-future", "futures-03"] } tracing-log = "0.1" tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter"] } -url = "2.1" +url = { version = "2.1", features = ["serde"] } uuid = { version = "0.8", features = ["serde", "v4"] } void = "1" diff --git a/swap/src/cli.rs b/swap/src/cli.rs index a2a48a48..5b0c30e2 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -1,6 +1,5 @@ use crate::{bitcoin, monero}; use libp2p::{core::Multiaddr, PeerId}; -use url::Url; use uuid::Uuid; #[derive(structopt::StructOpt, Debug)] @@ -17,18 +16,6 @@ pub struct Options { #[structopt(name = "xmr_btc-swap", about = "XMR BTC atomic swap")] pub enum Command { SellXmr { - #[structopt(long = "bitcoind-rpc", default_value = "http://127.0.0.1:8332")] - bitcoind_url: Url, - - #[structopt(long = "bitcoin-wallet-name")] - bitcoin_wallet_name: String, - - #[structopt( - long = "monero-wallet-rpc", - default_value = "http://127.0.0.1:18083/json_rpc" - )] - monero_wallet_rpc_url: Url, - #[structopt(long = "p2p-address", default_value = "/ip4/0.0.0.0/tcp/9876")] listen_addr: Multiaddr, @@ -45,18 +32,6 @@ pub enum Command { #[structopt(long = "connect-addr")] alice_addr: Multiaddr, - #[structopt(long = "bitcoind-rpc", default_value = "http://127.0.0.1:8332")] - bitcoind_url: Url, - - #[structopt(long = "bitcoin-wallet-name")] - bitcoin_wallet_name: String, - - #[structopt( - long = "monero-wallet-rpc", - default_value = "http://127.0.0.1:18083/json_rpc" - )] - monero_wallet_rpc_url: Url, - #[structopt(long = "send-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))] send_bitcoin: bitcoin::Amount, @@ -73,18 +48,6 @@ pub enum Resume { #[structopt(long = "swap-id")] swap_id: Uuid, - #[structopt(long = "bitcoind-rpc", default_value = "http://127.0.0.1:8332")] - bitcoind_url: Url, - - #[structopt(long = "bitcoin-wallet-name")] - bitcoin_wallet_name: String, - - #[structopt( - long = "monero-wallet-rpc", - default_value = "http://127.0.0.1:18083/json_rpc" - )] - monero_wallet_rpc_url: Url, - #[structopt(long = "listen-address", default_value = "/ip4/127.0.0.1/tcp/9876")] listen_addr: Multiaddr, }, @@ -97,18 +60,6 @@ pub enum Resume { #[structopt(long = "counterpart-addr")] alice_addr: Multiaddr, - - #[structopt(long = "bitcoind-rpc", default_value = "http://127.0.0.1:8332")] - bitcoind_url: Url, - - #[structopt(long = "bitcoin-wallet-name")] - bitcoin_wallet_name: String, - - #[structopt( - long = "monero-wallet-rpc", - default_value = "http://127.0.0.1:18083/json_rpc" - )] - monero_wallet_rpc_url: Url, }, } diff --git a/swap/src/config.rs b/swap/src/config.rs index 01fbddaa..291e717c 100644 --- a/swap/src/config.rs +++ b/swap/src/config.rs @@ -1 +1,111 @@ +use crate::fs::{default_config_path, ensure_directory_exists}; +use anyhow::{Context, Result}; +use config::{Config, ConfigError}; +use dialoguer::{theme::ColorfulTheme, Input}; +use serde::{Deserialize, Serialize}; +use std::{ + ffi::OsStr, + fs, + path::{Path, PathBuf}, +}; +use tracing::info; +use url::Url; + pub mod seed; + +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"; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct File { + pub bitcoin: Bitcoin, + pub monero: Monero, +} + +impl File { + pub fn read(config_file: D) -> Result + where + D: AsRef, + { + let config_file = Path::new(&config_file); + + let mut 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 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, +} + +pub fn read_config() -> anyhow::Result { + let default_path = default_config_path()?; + + if default_path.exists() { + info!( + "Using config file at default path: {}", + default_path.display() + ); + } else { + initial_setup(default_path.clone())?; + } + + File::read(&default_path) + .with_context(|| format!("failed to read config file {}", default_path.display())) +} + +fn initial_setup(config_path: PathBuf) -> Result<()> { + info!("Config file not found, running initial setup..."); + ensure_directory_exists(config_path.as_path())?; + let initial_config = query_user_for_initial_testnet_config()?; + + let toml = toml::to_string(&initial_config)?; + fs::write(config_path.clone(), toml)?; + + info!( + "Initial setup complete, config file created at {} ", + config_path.as_path().display() + ); + Ok(()) +} + +fn query_user_for_initial_testnet_config() -> Result { + println!(); + let bitcoind_url: String = 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 = Url::parse(bitcoind_url.as_str())?; + + let bitcoin_wallet_name: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter Bitcoind wallet name") + .interact_text()?; + + let monero_wallet_rpc_url: String = 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 = Url::parse(monero_wallet_rpc_url.as_str())?; + println!(); + + Ok(File { + bitcoin: Bitcoin { + bitcoind_url, + wallet_name: bitcoin_wallet_name, + }, + monero: Monero { + wallet_rpc_url: monero_wallet_rpc_url, + }, + }) +} diff --git a/swap/src/fs.rs b/swap/src/fs.rs index f3dc889b..ee12585b 100644 --- a/swap/src/fs.rs +++ b/swap/src/fs.rs @@ -1,4 +1,19 @@ -use std::path::Path; +use anyhow::Context; +use directories_next::ProjectDirs; +use std::path::{Path, PathBuf}; + +/// This is to store the configuration and seed files +// Linux: /home//.config/xmr-btc-swap/ +// OSX: /Users//Library/Preferences/xmr-btc-swap/ +fn config_dir() -> Option { + ProjectDirs::from("", "", "xmr-btc-swap").map(|proj_dirs| proj_dirs.config_dir().to_path_buf()) +} + +pub fn default_config_path() -> anyhow::Result { + config_dir() + .map(|dir| Path::join(&dir, "config.toml")) + .context("Could not generate default configuration path") +} pub fn ensure_directory_exists(file: &Path) -> Result<(), std::io::Error> { if let Some(path) = file.parent() { diff --git a/swap/src/main.rs b/swap/src/main.rs index 3c042736..69668bd7 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -12,7 +12,10 @@ #![forbid(unsafe_code)] #![allow(non_snake_case)] -use crate::cli::{Command, Options, Resume}; +use crate::{ + cli::{Command, Options, Resume}, + config::read_config, +}; use anyhow::{Context, Result}; use database::Database; use prettytable::{row, Table}; @@ -60,15 +63,11 @@ async fn main() -> Result<()> { match opt.cmd { Command::SellXmr { - bitcoind_url, - bitcoin_wallet_name, - monero_wallet_rpc_url, listen_addr, send_monero, receive_bitcoin, } => { - let settings = - Settings::testnet(bitcoind_url, bitcoin_wallet_name, monero_wallet_rpc_url); + let settings = Settings::from_config_file_and_defaults(read_config()?); let swap_amounts = SwapAmounts { xmr: send_monero, @@ -103,14 +102,10 @@ async fn main() -> Result<()> { Command::BuyXmr { alice_peer_id, alice_addr, - bitcoind_url, - bitcoin_wallet_name, - monero_wallet_rpc_url, send_bitcoin, receive_monero, } => { - let settings = - Settings::testnet(bitcoind_url, bitcoin_wallet_name, monero_wallet_rpc_url); + let settings = Settings::from_config_file_and_defaults(read_config()?); let swap_amounts = SwapAmounts { btc: send_bitcoin, @@ -157,13 +152,9 @@ async fn main() -> Result<()> { } Command::Resume(Resume::SellXmr { swap_id, - bitcoind_url, - bitcoin_wallet_name, - monero_wallet_rpc_url, listen_addr, }) => { - let settings = - Settings::testnet(bitcoind_url, bitcoin_wallet_name, monero_wallet_rpc_url); + let settings = Settings::from_config_file_and_defaults(read_config()?); let (bitcoin_wallet, monero_wallet) = setup_wallets(settings.wallets).await?; @@ -184,14 +175,10 @@ async fn main() -> Result<()> { } Command::Resume(Resume::BuyXmr { swap_id, - bitcoind_url, - bitcoin_wallet_name, - monero_wallet_rpc_url, alice_peer_id, alice_addr, }) => { - let settings = - Settings::testnet(bitcoind_url, bitcoin_wallet_name, monero_wallet_rpc_url); + let settings = Settings::from_config_file_and_defaults(read_config()?); let (bitcoin_wallet, monero_wallet) = setup_wallets(settings.wallets).await?; diff --git a/swap/src/settings.rs b/swap/src/settings.rs index 74571078..dee7b84d 100644 --- a/swap/src/settings.rs +++ b/swap/src/settings.rs @@ -1,4 +1,4 @@ -use crate::bitcoin::Timelock; +use crate::{bitcoin::Timelock, config::File}; use conquer_once::Lazy; use std::time::Duration; use url::Url; @@ -9,11 +9,15 @@ pub struct Settings { } impl Settings { - pub fn testnet( - bitcoind_url: Url, - bitcoin_wallet_name: String, - monero_wallet_rpc_url: Url, - ) -> Self { + pub fn from_config_file_and_defaults(config: File) -> Self { + Settings::testnet( + config.bitcoin.bitcoind_url, + config.bitcoin.wallet_name, + config.monero.wallet_rpc_url, + ) + } + + fn testnet(bitcoind_url: Url, bitcoin_wallet_name: String, monero_wallet_rpc_url: Url) -> Self { Self { wallets: Wallets::testnet(bitcoind_url, bitcoin_wallet_name, monero_wallet_rpc_url), protocol: Protocol::testnet(),