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 tracing ::info ;
use url ::Url ;
const DEFAULT_LISTEN_ADDRESS_TCP : & str = "/ip4/0.0.0.0/tcp/9939" ;
const DEFAULT_LISTEN_ADDRESS_WS : & str = "/ip4/0.0.0.0/tcp/9940/ws" ;
const DEFAULT_ELECTRUM_RPC_URL : & str = "ssl://electrum.blockstream.info:60002" ;
const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL : & str = "http://127.0.0.1:38083/json_rpc" ;
const DEFAULT_BITCOIN_CONFIRMATION_TARGET : usize = 3 ;
const DEFAULT_MIN_BUY_AMOUNT : f64 = 0.002 f64 ;
const DEFAULT_MAX_BUY_AMOUNT : f64 = 0.02 f64 ;
const DEFAULT_SPREAD : f64 = 0.02 f64 ;
#[ derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq) ]
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 > ,
}
#[ derive(Clone, Debug, Deserialize, PartialEq, Serialize) ]
#[ serde(deny_unknown_fields) ]
pub struct Bitcoin {
pub electrum_rpc_url : Url ,
pub target_block : usize ,
}
#[ derive(Clone, Debug, Deserialize, PartialEq, Serialize) ]
#[ serde(deny_unknown_fields) ]
pub struct Monero {
pub wallet_rpc_url : Url ,
}
#[ 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 ,
}
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 ( ) {
info ! (
path = % config_path . display ( ) ,
"Using config file at path" ,
) ;
} 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 ) )
}
/// Default location for storing the config file for the ASB
// Takes the default system config-dir and adds a `/asb/config.toml`
pub fn default_config_path ( ) -> Result < PathBuf > {
system_config_dir ( )
. map ( | dir | Path ::join ( & dir , "asb" ) )
. map ( | dir | Path ::join ( & dir , "config.toml" ) )
. context ( "Could not generate default config file path" )
}
/// Default location for storing data for the CLI
// Takes the default system data-dir and adds a `/asb`
fn default_data_dir ( ) -> Result < PathBuf > {
system_data_dir ( )
. map ( | proj_dir | Path ::join ( & proj_dir , "asb" ) )
. context ( "Could not generate default data dir" )
}
pub fn initial_setup < F > ( config_path : PathBuf , config_file : F ) -> Result < ( ) >
where
F : Fn ( ) -> Result < Config > ,
{
info ! ( "Config file not found, running initial setup..." ) ;
let initial_config = config_file ( ) ? ;
let toml = toml ::to_string ( & initial_config ) ? ;
ensure_directory_exists ( config_path . as_path ( ) ) ? ;
fs ::write ( & config_path , toml ) ? ;
info ! (
path = % config_path . as_path ( ) . display ( ) ,
"Initial setup complete, config file created" ,
) ;
Ok ( ( ) )
}
pub fn query_user_for_initial_testnet_config ( ) -> Result < Config > {
println! ( ) ;
let data_dir = Input ::with_theme ( & ColorfulTheme ::default ( ) )
. with_prompt ( "Enter data directory for asb or hit return to use default" )
. default (
default_data_dir ( )
. context ( "No 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 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 ( DEFAULT_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! ( "{},{}" , DEFAULT_LISTEN_ADDRESS_TCP , DEFAULT_LISTEN_ADDRESS_WS ) )
. interact_text ( ) ? ;
let listen_addresses = listen_addresses
. split ( ',' )
. map ( | str | str . parse ( ) )
. collect ::< Result < Vec < Multiaddr > , _ > > ( ) ? ;
let electrum_rpc_url : String = Input ::with_theme ( & ColorfulTheme ::default ( ) )
. with_prompt ( "Enter Electrum RPC URL or hit return to use default" )
. default ( DEFAULT_ELECTRUM_RPC_URL . to_owned ( ) )
. interact_text ( ) ? ;
let electrum_rpc_url = Url ::parse ( electrum_rpc_url . as_str ( ) ) ? ;
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 ( ) ? ;
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" ) ? ;
println! ( ) ;
Ok ( Config {
data : Data { dir : data_dir } ,
network : Network {
listen : listen_addresses ,
} ,
bitcoin : Bitcoin {
electrum_rpc_url ,
target_block ,
} ,
monero : Monero {
wallet_rpc_url : monero_wallet_rpc_url ,
} ,
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 ,
} ,
} )
}
#[ 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 ( ) ,
} ,
bitcoin : Bitcoin {
electrum_rpc_url : Url ::from_str ( DEFAULT_ELECTRUM_RPC_URL ) . unwrap ( ) ,
target_block : DEFAULT_BITCOIN_CONFIRMATION_TARGET ,
} ,
network : Network {
listen : vec ! [
DEFAULT_LISTEN_ADDRESS_TCP . parse ( ) . unwrap ( ) ,
DEFAULT_LISTEN_ADDRESS_WS . parse ( ) . unwrap ( ) ,
] ,
} ,
monero : Monero {
wallet_rpc_url : Url ::from_str ( DEFAULT_MONERO_WALLET_RPC_TESTNET_URL ) . unwrap ( ) ,
} ,
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 ( ) ,
} ,
} ;
initial_setup ( config_path . clone ( ) , | | Ok ( expected . clone ( ) ) ) . unwrap ( ) ;
let actual = read_config ( config_path ) . unwrap ( ) . unwrap ( ) ;
assert_eq! ( expected , actual ) ;
}
}