Skip to content

Commit

Permalink
Provide commands buy, sell, orderbook, enable, get-enabled etc.
Browse files Browse the repository at this point in the history
Use common protocol data types
  • Loading branch information
rozhkovdmitrii committed May 4, 2023
1 parent e041dbb commit e788710
Show file tree
Hide file tree
Showing 56 changed files with 2,291 additions and 606 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion mm2src/adex_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,29 @@ description = "Provides a CLI interface and facilitates interoperating to komodo
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
clap = "2.33.3"
async-trait = "0.1.68"
clap = { version = "4.2", features = ["derive"] }
common = { path = "../common" }
derive_more = "0.99"
directories = "5.0"
env_logger = "0.7.1"
http = "0.2"
gstuff = { version = "=0.7.4" , features = [ "nightly" ]}
inquire = "0.6"
itertools = "0.10"
log = "0.4"
mm2_net = { path = "../mm2_net" }
mm2_number = { path = "../mm2_number" }
mm2_rpc = { path = "../mm2_rpc" }
passwords = "3.1"
serde = "1.0"
serde_json = { version = "1", features = ["preserve_order", "raw_value"] }
sysinfo = "0.28"
tiny-bip39 = "0.8.0"
tokio = { version = "1.20", features = [ "macros" ] }
uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] }
rpc = { path = "../mm2_bitcoin/rpc" }

[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.3", features = ["processthreadsapi", "winnt"] }

82 changes: 82 additions & 0 deletions mm2src/adex_cli/src/activation_scheme_db/activation_scheme_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use log::{debug, error, warn};
use serde_json::{json, Value as Json};
use std::collections::HashMap;

use super::init_activation_scheme::get_activation_scheme_path;
use crate::helpers::read_json_file;

pub(crate) trait ActivationScheme {
type ActivationCommand;
fn get_activation_method(&self, coin: &str) -> Option<Self::ActivationCommand>;
fn get_coins_list(&self) -> Vec<String>;
}

struct ActivationSchemeJson {
scheme: HashMap<String, Json>,
}

impl ActivationSchemeJson {
fn new() -> Self {
let mut new = Self {
scheme: HashMap::<String, Json>::new(),
};

let Ok(Json::Array(mut results)) = Self::load_json_file() else {
return new;
};
new.scheme = results.iter_mut().map(Self::get_coin_pair).collect();
new
}

fn get_coin_pair(element: &mut Json) -> (String, Json) {
let presence = element.to_string();
let Ok(result) = Self::get_coin_pair_impl(element) else {
warn!("Failed to process: {presence}");
return ("".to_string(), Json::Null)
};
result
}

fn get_coin_pair_impl(element: &mut Json) -> Result<(String, Json), ()> {
let command = element.get_mut("command").ok_or(())?.take();
let coin = element.get("coin").ok_or(())?.as_str().ok_or(())?.to_string();
Ok((coin, command))
}

fn load_json_file() -> Result<Json, ()> {
let activation_scheme_path = get_activation_scheme_path()?;
debug!("Start reading activation_scheme from: {activation_scheme_path:?}");

let mut activation_scheme: Json = read_json_file(&activation_scheme_path)?;

let results = activation_scheme
.get_mut("results")
.ok_or_else(|| error!("Failed to get results section"))?
.take();

Ok(results)
}
}

impl ActivationScheme for ActivationSchemeJson {
type ActivationCommand = Json;
fn get_activation_method(&self, coin: &str) -> Option<Self::ActivationCommand> {
let Some(Json::Object(object)) = self.scheme.get(coin) else { return None };
let mut copy = json!({});
for (k, v) in object.iter() {
// WORKAROUND: serde_json::Value does not support removing key
if *k == "userpass" {
continue;
}
copy[k] = v.clone();
}
Some(copy)
}

fn get_coins_list(&self) -> Vec<String> { vec!["".to_string()] }
}

pub(crate) fn get_activation_scheme() -> Box<dyn ActivationScheme<ActivationCommand = Json>> {
let activation_scheme: Box<dyn ActivationScheme<ActivationCommand = Json>> = Box::new(ActivationSchemeJson::new());
activation_scheme
}
42 changes: 42 additions & 0 deletions mm2src/adex_cli/src/activation_scheme_db/init_activation_scheme.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use common::log::{error, info};
use mm2_net::transport::slurp_url;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;

use crate::adex_config::AdexConfigImpl;

const ACTIVATION_SCHEME_FILE: &str = "activation_scheme.json";
const COIN_ACTIVATION_SOURCE: &str = "https://stats.kmd.io/api/table/coin_activation/";

pub(crate) async fn init_activation_scheme() -> Result<(), ()> {
let config_path = get_activation_scheme_path()?;
info!("Start getting activation_scheme from: {config_path:?}");

let mut writer = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(config_path)
.map_err(|error| error!("Failed to open activation_scheme file to write: {error}"))?;

let activation_scheme = get_activation_scheme_data().await?;
writer
.write_all(&activation_scheme)
.map_err(|error| error!("Failed to write activation_scheme: {error}"))
}

pub(crate) fn get_activation_scheme_path() -> Result<PathBuf, ()> {
let mut config_path = AdexConfigImpl::get_config_dir()?;
config_path.push(ACTIVATION_SCHEME_FILE);
Ok(config_path)
}

async fn get_activation_scheme_data() -> Result<Vec<u8>, ()> {
info!("Download activation_scheme from: {COIN_ACTIVATION_SOURCE}");
let (_status_code, _headers, data) = slurp_url(COIN_ACTIVATION_SOURCE).await.map_err(|error| {
error!("Failed to get activation_scheme from: {COIN_ACTIVATION_SOURCE}, error: {error}");
})?;

Ok(data)
}
5 changes: 5 additions & 0 deletions mm2src/adex_cli/src/activation_scheme_db/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod activation_scheme_impl;
mod init_activation_scheme;

pub(crate) use activation_scheme_impl::get_activation_scheme;
pub(crate) use init_activation_scheme::init_activation_scheme;
24 changes: 24 additions & 0 deletions mm2src/adex_cli/src/adex_app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use std::env;
use std::io::Write;

use super::adex_config::AdexConfigImpl;
use super::adex_proc::ResponseHandlerImpl;
use super::cli;

pub(crate) struct AdexApp {
config: AdexConfigImpl,
}

impl AdexApp {
pub fn new() -> Result<AdexApp, ()> {
let config = AdexConfigImpl::read_config()?;
Ok(AdexApp { config })
}
pub async fn execute(&self) {
let mut writer = std::io::stdout();
let response_handler = ResponseHandlerImpl {
writer: (&mut writer as &mut dyn Write).into(),
};
let _ = cli::Cli::execute(env::args(), &self.config, &response_handler).await;
}
}
170 changes: 170 additions & 0 deletions mm2src/adex_cli/src/adex_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use directories::ProjectDirs;
use inquire::Password;
use log::{error, info, warn};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::fs;
use std::path::{Path, PathBuf};

use crate::helpers::rewrite_json_file;

const PROJECT_QUALIFIER: &str = "com";
const PROJECT_COMPANY: &str = "komodoplatform";
const PROJECT_APP: &str = "adex-cli";
const ADEX_CFG: &str = "adex_cfg.json";

const PRICE_PRECISION_MIN: usize = 8;
const PRICE_PRECISION_MAX: usize = 8;
const VOLUME_PRECISION_MIN: usize = 2;
const VOLUME_PRECISION_MAX: usize = 5;
const VOLUME_PRECISION: VolumePrecision = (VOLUME_PRECISION_MIN, VOLUME_PRECISION_MAX);
const PRICE_PRECISION: PricePrecision = (PRICE_PRECISION_MIN, PRICE_PRECISION_MAX);

pub(crate) type PricePrecision = (usize, usize);
pub(crate) type VolumePrecision = (usize, usize);

pub fn get_config() {
let Ok(adex_cfg) = AdexConfigImpl::from_config_path() else { return; };
info!("{}", adex_cfg)
}

pub fn set_config(set_password: bool, rpc_api_uri: Option<String>) {
let mut adex_cfg = AdexConfigImpl::from_config_path().unwrap_or_else(|()| AdexConfigImpl::default());
let mut is_changes_happened = false;
if set_password {
let rpc_password = Password::new("Enter RPC API password:")
.prompt()
.map(|value| {
is_changes_happened = true;
value
})
.map_err(|error| error!("Failed to get rpc_api_password: {error}"))
.ok();
adex_cfg.set_rpc_password(rpc_password);
}
if rpc_api_uri.is_some() {
adex_cfg.set_rpc_uri(rpc_api_uri);
is_changes_happened = true;
}

if is_changes_happened && adex_cfg.write_to_config_path().is_ok() {
info!("Configuration has been set");
} else {
warn!("Nothing changed");
}
}

pub(crate) trait AdexConfig {
fn rpc_password(&self) -> String;
fn rpc_uri(&self) -> String;
fn orderbook_price_precision(&self) -> &PricePrecision;
fn orderbook_volume_precision(&self) -> &VolumePrecision;
}

#[derive(Deserialize, Serialize, Debug, Default)]
pub(crate) struct AdexConfigImpl {
#[serde(skip_serializing_if = "Option::is_none")]
rpc_password: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
rpc_uri: Option<String>,
}

impl AdexConfig for AdexConfigImpl {
fn rpc_password(&self) -> String { self.rpc_password.as_ref().expect("No rpc_password in config").clone() }
fn rpc_uri(&self) -> String { self.rpc_uri.as_ref().expect("No rpc_uri in config").clone() }
fn orderbook_price_precision(&self) -> &PricePrecision { &PRICE_PRECISION }
fn orderbook_volume_precision(&self) -> &VolumePrecision { &VOLUME_PRECISION }
}

impl Display for AdexConfigImpl {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if !self.is_set() {
return writeln!(f, "adex configuration is not set");
}
if let Some(rpc_api_uri) = &self.rpc_uri {
writeln!(f, "kmd wallet RPC URL: {}", rpc_api_uri)?
};

if self.rpc_password.is_some() {
writeln!(f, "kmd wallet RPC password: *************")?
}
Ok(())
}
}

impl AdexConfigImpl {
#[cfg(test)]
pub fn new(rpc_password: &str, rpc_uri: &str) -> Self {
Self {
rpc_password: Some(rpc_password.to_string()),
rpc_uri: Some(rpc_uri.to_string()),
}
}

pub fn read_config() -> Result<AdexConfigImpl, ()> {
let config = AdexConfigImpl::from_config_path().map_err(|_| error!("Failed to get adex_config"))?;
match config {
config @ AdexConfigImpl {
rpc_password: Some(_),
rpc_uri: Some(_),
} => Ok(config),
_ => {
warn!("Failed to process, adex_config is not fully set");
Err(())
},
}
}

fn is_set(&self) -> bool { self.rpc_uri.is_some() && self.rpc_password.is_some() }

pub fn get_config_dir() -> Result<PathBuf, ()> {
let project_dirs = ProjectDirs::from(PROJECT_QUALIFIER, PROJECT_COMPANY, PROJECT_APP)
.ok_or_else(|| error!("Failed to get project_dirs"))?;
let config_path: PathBuf = project_dirs.config_dir().into();
fs::create_dir_all(&config_path)
.map_err(|error| error!("Failed to create config_dir: {config_path:?}, error: {error}"))?;
Ok(config_path)
}

fn get_config_path() -> Result<PathBuf, ()> {
let mut config_path = Self::get_config_dir()?;
config_path.push(ADEX_CFG);
Ok(config_path)
}

fn from_config_path() -> Result<AdexConfigImpl, ()> {
let config_path = Self::get_config_path()?;

if !config_path.exists() {
warn!("Config is not set");
return Err(());
}
Self::read_from(&config_path)
}

fn write_to_config_path(&self) -> Result<(), ()> {
let config_path = Self::get_config_path()?;
self.write_to(&config_path)
}

fn read_from(cfg_path: &Path) -> Result<AdexConfigImpl, ()> {
let adex_path_str = cfg_path.to_str().unwrap_or("Undefined");
let adex_cfg_file = fs::File::open(cfg_path).map_err(|error| {
error!("Failed to open: {adex_path_str}, error: {error}");
})?;

serde_json::from_reader(adex_cfg_file)
.map_err(|error| error!("Failed to read adex_cfg to read from: {adex_path_str}, error: {error}"))
}

fn write_to(&self, cfg_path: &Path) -> Result<(), ()> {
let adex_path_str = cfg_path
.to_str()
.ok_or_else(|| error!("Failed to get cfg_path as str"))?;
rewrite_json_file(self, adex_path_str)
}

fn set_rpc_password(&mut self, rpc_password: Option<String>) { self.rpc_password = rpc_password; }

fn set_rpc_uri(&mut self, rpc_uri: Option<String>) { self.rpc_uri = rpc_uri; }
}
Loading

0 comments on commit e788710

Please sign in to comment.