risq 0.4.1

Re-implementation of Bisq (https://github.com/bisq-network/bisq) in rust
mod query;

use crate::{
    api::Client,
    bisq::{constants::*, NodeAddress},
    daemon::{self, DaemonConfig},
    domain::{currency::Currency, market::Market},
};
use clap::{clap_app, crate_version, App, ArgMatches};
use env_logger::Env;
use log::Level;
use query::*;
use reqwest;
use std::{collections::HashMap, env, path::PathBuf, str::FromStr};

fn app() -> App<'static, 'static> {
    let app = clap_app!(risq =>
        (version: crate_version!())
        (@setting VersionlessSubcommands)
        (@setting SubcommandRequiredElseHelp)
        (@subcommand daemon =>
         (about: "Runs the risq p2p node")
         (visible_alias: "d")
         (@arg API_PORT: --("api-port") default_value("7477") {port} "API port")
         (@arg LOG_LEVEL: -l --("log-level") default_value("info") {level} "(error|warn|info|debug|trace)")
         (@arg NETWORK: -n --network default_value("BtcMainnet") {network} "(BtcRegtest|BtcTestnet|BtcMainnet)")
         (@arg P2P_PORT: -p --("p2p-port") default_value("5000") {port} "Port of p2p node")
         (@arg FORCE_SEED: --("force-seed") +takes_value {node_address} "Force usage of seed node")
         (@arg NO_TOR: --("no-tor") "Disable tor / run on localhost")
         (@arg TOR_CONTROL_PORT: --("tor-controll-port") default_value("9051") {port} "Tor Control port")
         (@arg TOR_HIDDEN_SERVICE_PORT: --("tor-hidden-service-port") default_value("9999") {port} "Public port of the hidden service")
         (@arg TOR_SOCKS_PORT: --("tor-socks-port") default_value("9050") {port} "Tor SOCKSPort")
        )
        (@subcommand offers =>
         (about: "Subcomand to interact with offers")
         (@arg API_PORT: --("api-port") default_value("7477") {port} "API port")
         (@arg MARKET: --("market") default_value("all") {market} "Filter by market pair")
        )
    );

    let app = add_checker_cmd(app);
    add_dummy_seed_cmd(app)
}

pub fn run() {
    let matches = app().get_matches();
    match matches.subcommand() {
        ("daemon", Some(matches)) => daemon(matches),
        ("offers", Some(matches)) => offers(matches),
        #[cfg(feature = "checker")]
        ("check-node", Some(matches)) => check_node(matches),
        #[cfg(feature = "dummy-seed")]
        ("dummy-seed", Some(matches)) => dummy_seed(matches),
        _ => unreachable!(),
    }
}

fn network(network: String) -> Result<(), String> {
    match BaseCurrencyNetwork::from_str(&network) {
        Err(_) => Err("(BtcMainnet|BtcTestnet|BtcRegtest)".into()),
        Ok(_) => Ok(()),
    }
}
fn port(port: String) -> Result<(), String> {
    match u16::from_str(&port) {
        Err(_) => Err(format!("'{}' is not a valid port number", port)),
        Ok(_) => Ok(()),
    }
}
fn node_address(addr: String) -> Result<(), String> {
    NodeAddress::from_str(&addr).map(|_| ())
}
fn market(market: String) -> Result<(), String> {
    if &market == "all" {
        return Ok(());
    }
    if Currency::from_code(&market).is_none() {
        return Err(format!("'{}' is not a valid currency code", market));
    }
    Ok(())
}
fn level(level: String) -> Result<(), String> {
    match Level::from_str(&level) {
        Err(_) => Err(format!("'{}' is not a valid logging level", level)),
        Ok(_) => Ok(()),
    }
}
#[cfg(feature = "dummy-seed")]
fn file(file: String) -> Result<(), String> {
    use std::path::Path;
    let path = Path::new(&file);
    match (path.exists(), path.is_file()) {
        (true, true) => Ok(()),
        (false, _) => Err(format!("File '{}' does not exist", file)),
        (_, false) => Err(format!("'{}' is not a file", file)),
    }
}

const RISQ_HOME_VAR: &str = "RISQ_HOME";

fn daemon(matches: &ArgMatches) {
    let risq_home = env::var_os(RISQ_HOME_VAR)
        .map(PathBuf::from)
        .unwrap_or_else(|| {
            let mut risq_dir = dirs::home_dir().expect("Couldn't determin home dir");
            risq_dir.push(".risq");
            risq_dir
        });

    let network: BaseCurrencyNetwork = matches.value_of("NETWORK").unwrap().parse().unwrap();
    let api_port = matches.value_of("API_PORT").unwrap().parse().unwrap();
    let server_port = matches.value_of("P2P_PORT").unwrap().parse().unwrap();
    let tor_active: bool = !matches.is_present("NO_TOR");

    init_log(matches);

    let force_seed = matches
        .value_of("FORCE_SEED")
        .and_then(|seed| NodeAddress::from_str(&seed).ok());

    let (tor_proxy_port, tor_control_port, hidden_service_port) = if tor_active {
        (
            Some(matches.value_of("TOR_SOCKS_PORT").unwrap().parse().unwrap()),
            Some(
                matches
                    .value_of("TOR_CONTROL_PORT")
                    .unwrap()
                    .parse()
                    .unwrap(),
            ),
            Some(
                matches
                    .value_of("TOR_HIDDEN_SERVICE_PORT")
                    .unwrap()
                    .parse()
                    .unwrap(),
            ),
        )
    } else {
        (None, None, None)
    };
    daemon::run(DaemonConfig {
        api_port,
        server_port,
        network,
        force_seed,
        risq_home,
        tor_control_port,
        tor_proxy_port,
        hidden_service_port,
    });
}

fn offers(matches: &ArgMatches) {
    let api_port = matches.value_of("API_PORT").unwrap().parse().unwrap();
    let mut vars = HashMap::new();
    let currency: Result<&Currency, ()> = matches.value_of("MARKET").unwrap().parse();
    if let Ok(currency) = currency {
        let market: &Market = currency.into();
        Offers::add_variables(&market, &mut vars);
    }
    let response: reqwest::Result<Offers> = Client::new(api_port).query(vars);
    match response {
        Ok(offers) => {
            println!("OPEN OFFERS");
            if offers.len() == 0 {
                println!("<currently no offers available>");
                return;
            }
            for offer in offers {
                println!("{}", offer)
            }
        }
        Err(_) => println!("Error trying to reach api"),
    }
}
#[cfg(not(feature = "checker"))]
fn add_checker_cmd(app: App<'static, 'static>) -> App<'static, 'static> {
    app
}
#[cfg(feature = "checker")]
fn add_checker_cmd(app: App<'static, 'static>) -> App<'static, 'static> {
    use clap::{Arg, SubCommand};
    app.subcommand(
        SubCommand::with_name("check-node")
            .about("Send a ping to a node. Used for monitoring.")
            .arg(
                Arg::with_name("TOR_SOCKS_PORT")
                    .long("tor-socks-port")
                    .validator(port)
                    .default_value("9050"),
            )
            .arg(
                Arg::with_name("NETWORK")
                    .long("network")
                    .short("n")
                    .validator(network)
                    .default_value("BtcMainnet"),
            )
            .arg(Arg::with_name("NODE_HOST").index(1).required(true))
            .arg(
                Arg::with_name("NODE_PORT")
                    .index(2)
                    .required(true)
                    .validator(port),
            )
            .after_help("Returns exit code 0 on success, 2 otherwise."),
    )
}

#[cfg(not(feature = "dummy-seed"))]
fn add_dummy_seed_cmd(app: App<'static, 'static>) -> App<'static, 'static> {
    app
}
#[cfg(feature = "dummy-seed")]
fn add_dummy_seed_cmd(app: App<'static, 'static>) -> App<'static, 'static> {
    use clap::{Arg, SubCommand};

    app.subcommand(
        SubCommand::with_name("dummy-seed")
            .about("Start a seed node used for testing")
            .arg(
                Arg::with_name("P2P_PORT")
                    .short("p")
                    .validator(port)
                    .default_value("4002"),
            )
            .arg(
                Arg::with_name("FIXTURES")
                    .short("f")
                    .takes_value(true)
                    .validator(file),
            )
            .arg(
                Arg::with_name("LOG_LEVEL")
                    .short("l")
                    .default_value("info")
                    .validator(level),
            ),
    )
}

#[cfg(feature = "checker")]
fn check_node(matches: &ArgMatches) {
    use crate::checker;

    let socks_port = matches.value_of("TOR_SOCKS_PORT").unwrap().parse().unwrap();
    let host_name: String = matches.value_of("NODE_HOST").unwrap().into();
    let port = matches.value_of("NODE_PORT").unwrap().parse().unwrap();
    let network: BaseCurrencyNetwork = matches.value_of("NETWORK").unwrap().parse().unwrap();
    checker::check_node(network, NodeAddress { host_name, port }, socks_port);
}

#[cfg(feature = "dummy-seed")]
fn dummy_seed(matches: &ArgMatches) {
    use crate::dummy_seed;
    use std::path::Path;

    init_log(matches);

    let port = matches.value_of("P2P_PORT").unwrap().parse().unwrap();
    let fixtures: Option<&Path> = matches.value_of("FIXTURES").map(Path::new);
    dummy_seed::run(port, fixtures);
}

fn init_log(matches: &ArgMatches) {
    let level: String = matches.value_of("LOG_LEVEL").unwrap().parse().unwrap();
    let env = Env::default().filter_or("RUST_LOG", level);
    env_logger::init_from_env(env);
}