mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-03-27 08:28:16 -04:00
425 lines
14 KiB
Rust
425 lines
14 KiB
Rust
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,
|
|
monero_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")?,
|
|
monero_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://blockstream.info:700")?,
|
|
monero_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 monero: Monero,
|
|
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 Monero {
|
|
pub wallet_rpc_url: Url,
|
|
pub finality_confirmations: Option<u64>,
|
|
#[serde(with = "crate::monero::network")]
|
|
pub network: monero::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, monero_network, defaults) = if testnet {
|
|
tracing::info!("Running initial setup for testnet");
|
|
|
|
let bitcoin_network = bitcoin::Network::Testnet;
|
|
let monero_network = monero::Network::Stagenet;
|
|
let defaults = Testnet::getConfigFileDefaults()?;
|
|
|
|
(bitcoin_network, monero_network, defaults)
|
|
} else {
|
|
tracing::info!("Running initial setup for mainnet");
|
|
let bitcoin_network = bitcoin::Network::Bitcoin;
|
|
let monero_network = monero::Network::Mainnet;
|
|
let defaults = Mainnet::getConfigFileDefaults()?;
|
|
|
|
(bitcoin_network, monero_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 monero_wallet_rpc_url = Input::with_theme(&ColorfulTheme::default())
|
|
.with_prompt("Enter Monero Wallet RPC URL or hit enter to use default")
|
|
.default(defaults.monero_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,
|
|
},
|
|
monero: Monero {
|
|
wallet_rpc_url: monero_wallet_rpc_url,
|
|
finality_confirmations: None,
|
|
network: monero_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![],
|
|
},
|
|
|
|
monero: Monero {
|
|
wallet_rpc_url: defaults.monero_wallet_rpc_url,
|
|
finality_confirmations: None,
|
|
network: monero::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![],
|
|
},
|
|
|
|
monero: Monero {
|
|
wallet_rpc_url: defaults.monero_wallet_rpc_url,
|
|
finality_confirmations: None,
|
|
network: monero::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);
|
|
}
|
|
}
|