You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wow-btc-swap/swap/src/asb/config.rs

425 lines
15 KiB

use crate::env::{Mainnet, Testnet};
use crate::fs::{ensure_directory_exists, system_config_dir, system_data_dir};
use crate::tor::{DEFAULT_CONTROL_PORT, DEFAULT_SOCKS5_PORT};
use anyhow::{bail, Context, Result};
use config::ConfigError;
use dialoguer::theme::ColorfulTheme;
use dialoguer::Input;
use libp2p::core::Multiaddr;
use rust_decimal::prelude::FromPrimitive;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use url::Url;
pub trait GetDefaults {
fn getConfigFileDefaults() -> Result<Defaults>;
}
pub struct Defaults {
pub config_path: PathBuf,
data_dir: PathBuf,
listen_address_tcp: Multiaddr,
listen_address_ws: Multiaddr,
electrum_rpc_url: Url,
wownero_wallet_rpc_url: Url,
price_ticker_ws_url: Url,
bitcoin_confirmation_target: usize,
}
impl GetDefaults for Testnet {
fn getConfigFileDefaults() -> Result<Defaults> {
let defaults = Defaults {
config_path: default_asb_config_dir()?
.join("testnet")
.join("config.toml"),
data_dir: default_asb_data_dir()?.join("testnet"),
listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?,
listen_address_ws: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9940/ws")?,
electrum_rpc_url: Url::parse("ssl://electrum.blockstream.info:60002")?,
wownero_wallet_rpc_url: Url::parse("http://127.0.0.1:38083/json_rpc")?,
price_ticker_ws_url: Url::parse("wss://ws.kraken.com")?,
bitcoin_confirmation_target: 1,
};
Ok(defaults)
}
}
impl GetDefaults for Mainnet {
fn getConfigFileDefaults() -> Result<Defaults> {
let defaults = Defaults {
config_path: default_asb_config_dir()?
.join("mainnet")
.join("config.toml"),
data_dir: default_asb_data_dir()?.join("mainnet"),
listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?,
listen_address_ws: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9940/ws")?,
electrum_rpc_url: Url::parse("ssl://electrum.blockstream.info:50002")?,
wownero_wallet_rpc_url: Url::parse("http://127.0.0.1:18083/json_rpc")?,
price_ticker_ws_url: Url::parse("wss://ws.kraken.com")?,
bitcoin_confirmation_target: 3,
};
Ok(defaults)
}
}
fn default_asb_config_dir() -> Result<PathBuf> {
system_config_dir()
.map(|dir| Path::join(&dir, "asb"))
.context("Could not generate default config file path")
}
fn default_asb_data_dir() -> Result<PathBuf> {
system_data_dir()
.map(|dir| Path::join(&dir, "asb"))
.context("Could not generate default config file path")
}
const DEFAULT_MIN_BUY_AMOUNT: f64 = 0.002f64;
const DEFAULT_MAX_BUY_AMOUNT: f64 = 0.02f64;
const DEFAULT_SPREAD: f64 = 0.02f64;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub data: Data,
pub network: Network,
pub bitcoin: Bitcoin,
pub wownero: Wownero,
pub tor: TorConf,
pub maker: Maker,
}
impl Config {
pub fn read<D>(config_file: D) -> Result<Self, ConfigError>
where
D: AsRef<OsStr>,
{
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: Vec<Multiaddr>,
#[serde(default)]
pub rendezvous_point: Option<Multiaddr>,
#[serde(default)]
pub external_addresses: Vec<Multiaddr>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Bitcoin {
pub electrum_rpc_url: Url,
pub target_block: usize,
pub finality_confirmations: Option<u32>,
#[serde(with = "crate::bitcoin::network")]
pub network: bitcoin::Network,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Wownero {
pub wallet_rpc_url: Url,
pub finality_confirmations: Option<u64>,
#[serde(with = "crate::wownero::network")]
pub network: wownero::Network,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TorConf {
pub control_port: u16,
pub socks5_port: u16,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Maker {
#[serde(with = "::bitcoin::util::amount::serde::as_btc")]
pub min_buy_btc: bitcoin::Amount,
#[serde(with = "::bitcoin::util::amount::serde::as_btc")]
pub max_buy_btc: bitcoin::Amount,
pub ask_spread: Decimal,
pub price_ticker_ws_url: Url,
}
impl Default for TorConf {
fn default() -> Self {
Self {
control_port: DEFAULT_CONTROL_PORT,
socks5_port: DEFAULT_SOCKS5_PORT,
}
}
}
#[derive(thiserror::Error, Debug, Clone, Copy)]
#[error("config not initialized")]
pub struct ConfigNotInitialized {}
pub fn read_config(config_path: PathBuf) -> Result<Result<Config, ConfigNotInitialized>> {
if config_path.exists() {
tracing::info!(
path = %config_path.display(),
"Reading config file",
);
} else {
return Ok(Err(ConfigNotInitialized {}));
}
let file = Config::read(&config_path)
.with_context(|| format!("Failed to read config file at {}", config_path.display()))?;
Ok(Ok(file))
}
pub fn initial_setup(config_path: PathBuf, config: Config) -> Result<()> {
let toml = toml::to_string(&config)?;
ensure_directory_exists(config_path.as_path())?;
fs::write(&config_path, toml)?;
tracing::info!(
path = %config_path.as_path().display(),
"Initial setup complete, config file created",
);
Ok(())
}
pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
let (bitcoin_network, wownero_network, defaults) = if testnet {
tracing::info!("Running initial setup for testnet");
let bitcoin_network = bitcoin::Network::Testnet;
let wownero_network = wownero::Network::Stagenet;
let defaults = Testnet::getConfigFileDefaults()?;
(bitcoin_network, wownero_network, defaults)
} else {
tracing::info!("Running initial setup for mainnet");
let bitcoin_network = bitcoin::Network::Bitcoin;
let wownero_network = wownero::Network::Mainnet;
let defaults = Mainnet::getConfigFileDefaults()?;
(bitcoin_network, wownero_network, defaults)
};
println!();
let data_dir = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter data directory for asb or hit return to use default")
.default(
defaults
.data_dir
.to_str()
.context("Unsupported characters in default path")?
.to_string(),
)
.interact_text()?;
let data_dir = data_dir.as_str().parse()?;
let target_block = Input::with_theme(&ColorfulTheme::default())
.with_prompt("How fast should your Bitcoin transactions be confirmed? Your transaction fee will be calculated based on this target. Hit return to use default")
.default(defaults.bitcoin_confirmation_target)
.interact_text()?;
let listen_addresses = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter multiaddresses (comma separated) on which asb should list for peer-to-peer communications or hit return to use default")
.default( format!("{},{}", defaults.listen_address_tcp, defaults.listen_address_ws))
.interact_text()?;
let listen_addresses = listen_addresses
.split(',')
.map(|str| str.parse())
.collect::<Result<Vec<Multiaddr>, _>>()?;
let electrum_rpc_url = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter Electrum RPC URL or hit return to use default")
.default(defaults.electrum_rpc_url)
.interact_text()?;
let wownero_wallet_rpc_url = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter Wownero Wallet RPC URL or hit enter to use default")
.default(defaults.wownero_wallet_rpc_url)
.interact_text()?;
let tor_control_port = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter Tor control port or hit enter to use default. If Tor is not running on your machine, no hidden service will be created.")
.default(DEFAULT_CONTROL_PORT.to_owned())
.interact_text()?;
let tor_socks5_port = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter Tor socks5 port or hit enter to use default")
.default(DEFAULT_SOCKS5_PORT.to_owned())
.interact_text()?;
let min_buy = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter minimum Bitcoin amount you are willing to accept per swap or hit enter to use default.")
.default(DEFAULT_MIN_BUY_AMOUNT)
.interact_text()?;
let min_buy = bitcoin::Amount::from_btc(min_buy)?;
let max_buy = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter maximum Bitcoin amount you are willing to accept per swap or hit enter to use default.")
.default(DEFAULT_MAX_BUY_AMOUNT)
.interact_text()?;
let max_buy = bitcoin::Amount::from_btc(max_buy)?;
let ask_spread = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter spread (in percent; value between 0.x and 1.0) to be used on top of the market rate or hit enter to use default.")
.default(DEFAULT_SPREAD)
.interact_text()?;
if !(0.0..=1.0).contains(&ask_spread) {
bail!(format!("Invalid spread {}. For the spread value floating point number in interval [0..1] are allowed.", ask_spread))
}
let ask_spread = Decimal::from_f64(ask_spread).context("Unable to parse spread")?;
let rendezvous_point = Input::<Multiaddr>::with_theme(&ColorfulTheme::default())
.with_prompt("Do you want to advertise your ASB instance with a rendezvous node? Enter an empty string if not.")
.allow_empty(true)
.interact_text()?;
println!();
Ok(Config {
data: Data { dir: data_dir },
network: Network {
listen: listen_addresses,
rendezvous_point: if rendezvous_point.is_empty() {
None
} else {
Some(rendezvous_point)
},
external_addresses: vec![],
},
bitcoin: Bitcoin {
electrum_rpc_url,
target_block,
finality_confirmations: None,
network: bitcoin_network,
},
wownero: Wownero {
wallet_rpc_url: wownero_wallet_rpc_url,
finality_confirmations: None,
network: wownero_network,
},
tor: TorConf {
control_port: tor_control_port,
socks5_port: tor_socks5_port,
},
maker: Maker {
min_buy_btc: min_buy,
max_buy_btc: max_buy,
ask_spread,
price_ticker_ws_url: defaults.price_ticker_ws_url,
},
})
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn config_roundtrip_testnet() {
let temp_dir = tempdir().unwrap().path().to_path_buf();
let config_path = Path::join(&temp_dir, "config.toml");
let defaults = Testnet::getConfigFileDefaults().unwrap();
let expected = Config {
data: Data {
dir: Default::default(),
},
bitcoin: Bitcoin {
electrum_rpc_url: defaults.electrum_rpc_url,
target_block: defaults.bitcoin_confirmation_target,
finality_confirmations: None,
network: bitcoin::Network::Testnet,
},
network: Network {
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
rendezvous_point: None,
external_addresses: vec![],
},
wownero: Wownero {
wallet_rpc_url: defaults.wownero_wallet_rpc_url,
finality_confirmations: None,
network: wownero::Network::Stagenet,
},
tor: Default::default(),
maker: Maker {
min_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MIN_BUY_AMOUNT).unwrap(),
max_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MAX_BUY_AMOUNT).unwrap(),
ask_spread: Decimal::from_f64(DEFAULT_SPREAD).unwrap(),
price_ticker_ws_url: defaults.price_ticker_ws_url,
},
};
initial_setup(config_path.clone(), expected.clone()).unwrap();
let actual = read_config(config_path).unwrap().unwrap();
assert_eq!(expected, actual);
}
#[test]
fn config_roundtrip_mainnet() {
let temp_dir = tempdir().unwrap().path().to_path_buf();
let config_path = Path::join(&temp_dir, "config.toml");
let defaults = Mainnet::getConfigFileDefaults().unwrap();
let expected = Config {
data: Data {
dir: Default::default(),
},
bitcoin: Bitcoin {
electrum_rpc_url: defaults.electrum_rpc_url,
target_block: defaults.bitcoin_confirmation_target,
finality_confirmations: None,
network: bitcoin::Network::Bitcoin,
},
network: Network {
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
rendezvous_point: None,
external_addresses: vec![],
},
wownero: Wownero {
wallet_rpc_url: defaults.wownero_wallet_rpc_url,
finality_confirmations: None,
network: wownero::Network::Mainnet,
},
tor: Default::default(),
maker: Maker {
min_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MIN_BUY_AMOUNT).unwrap(),
max_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MAX_BUY_AMOUNT).unwrap(),
ask_spread: Decimal::from_f64(DEFAULT_SPREAD).unwrap(),
price_ticker_ws_url: defaults.price_ticker_ws_url,
},
};
initial_setup(config_path.clone(), expected.clone()).unwrap();
let actual = read_config(config_path).unwrap().unwrap();
assert_eq!(expected, actual);
}
}