diff --git a/etomic_build/client/min_trading_vol b/etomic_build/client/min_trading_vol new file mode 100755 index 0000000000..7718cc185a --- /dev/null +++ b/etomic_build/client/min_trading_vol @@ -0,0 +1,9 @@ +#!/bin/bash +source userpass +curl --url "http://127.0.0.1:7783" --data ' +{ + "userpass":"'$userpass'", + "method":"min_trading_vol", + "coin": "'$1'" +} +' diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 0859d98725..808c5f5db1 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -62,6 +62,7 @@ pub use rlp; mod web3_transport; use self::web3_transport::Web3Transport; +use common::mm_number::MmNumber; #[cfg(test)] mod eth_tests; #[cfg(target_arch = "wasm32")] mod eth_wasm_tests; @@ -1084,6 +1085,11 @@ impl MarketCoinOps for EthCoin { fn display_priv_key(&self) -> String { format!("{:#02x}", self.key_pair.secret()) } fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } + + fn min_trading_vol(&self) -> MmNumber { + let pow = self.decimals / 3; + MmNumber::from(1) / MmNumber::from(10u64.pow(pow as u32)) + } } pub fn signed_eth_tx_from_bytes(bytes: &[u8]) -> Result { diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 4902e51b6c..d012f5a3c6 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -298,6 +298,9 @@ pub trait MarketCoinOps { /// Get the minimum amount to send. fn min_tx_amount(&self) -> BigDecimal; + + /// Get the minimum amount to trade. + fn min_trading_vol(&self) -> MmNumber; } #[derive(Deserialize)] diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 0ebad5ffbf..8f66e8b02b 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -18,6 +18,7 @@ use common::executor::Timer; use common::jsonrpc_client::{JsonRpcClient, JsonRpcError, JsonRpcRequest, RpcRes}; use common::log::{error, warn}; use common::mm_ctx::MmArc; +use common::mm_number::MmNumber; use common::{block_on, Traceable}; use ethabi::{Function, Token}; use ethereum_types::{H160, U256}; @@ -914,6 +915,11 @@ impl MarketCoinOps for Qrc20Coin { fn display_priv_key(&self) -> String { utxo_common::display_priv_key(&self.utxo) } fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } + + fn min_trading_vol(&self) -> MmNumber { + let pow = self.utxo.decimals / 3; + MmNumber::from(1) / MmNumber::from(10u64.pow(pow as u32)) + } } impl MmCoin for Qrc20Coin { diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 7360f0c96c..9ac4d9366e 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -4,6 +4,7 @@ use crate::{FeeApproxStage, FoundSwapTxSpend, TradePreimageError, TradePreimageV WithdrawRequest}; use bigdecimal::BigDecimal; use common::mm_ctx::MmArc; +use common::mm_number::MmNumber; use futures01::Future; use mocktopus::macros::*; use rpc::v1::types::Bytes as BytesJson; @@ -68,6 +69,8 @@ impl MarketCoinOps for TestCoin { fn display_priv_key(&self) -> String { unimplemented!() } fn min_tx_amount(&self) -> BigDecimal { unimplemented!() } + + fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } } #[mockable] diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index a5aa3ea9b2..eac70c7055 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -1,6 +1,7 @@ use super::*; use crate::{eth, CanRefundHtlc, CoinBalance, SwapOps, TradePreimageError, TradePreimageValue, ValidateAddressResult}; use common::mm_metrics::MetricsArc; +use common::mm_number::MmNumber; use ethereum_types::H160; use futures::{FutureExt, TryFutureExt}; @@ -491,6 +492,8 @@ impl MarketCoinOps for QtumCoin { fn display_priv_key(&self) -> String { utxo_common::display_priv_key(&self.utxo_arc) } fn min_tx_amount(&self) -> BigDecimal { utxo_common::min_tx_amount(self.as_ref()) } + + fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } } impl MmCoin for QtumCoin { diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index e46149015d..71f92e8f91 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -33,8 +33,11 @@ pub use chain::Transaction as UtxoTx; use self::rpc_clients::{electrum_script_hash, UnspentInfo, UtxoRpcClientEnum}; use crate::utxo::rpc_clients::UtxoRpcClientOps; use crate::{CanRefundHtlc, CoinBalance, FeeApproxStage, TradePreimageError, TradePreimageValue, ValidateAddressResult}; +use common::mm_number::MmNumber; use common::{block_on, Traceable}; +const MIN_BTC_TRADING_VOL: &str = "0.00777"; + macro_rules! true_or { ($cond: expr, $etype: expr) => { if !$cond { @@ -1316,6 +1319,14 @@ pub fn min_tx_amount(coin: &UtxoCoinFields) -> BigDecimal { big_decimal_from_sat(coin.dust_amount as i64, coin.decimals) } +pub fn min_trading_vol(coin: &UtxoCoinFields) -> MmNumber { + if coin.conf.ticker == "BTC" { + return MmNumber::from(MIN_BTC_TRADING_VOL); + } + let dust_multiplier = MmNumber::from(10); + dust_multiplier * min_tx_amount(coin).into() +} + pub fn is_asset_chain(coin: &UtxoCoinFields) -> bool { coin.conf.asset_chain } pub async fn withdraw(coin: T, req: WithdrawRequest) -> Result diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 98bd6bfbe7..afe771b28b 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -1,6 +1,7 @@ use super::*; use crate::{CanRefundHtlc, CoinBalance, SwapOps, TradePreimageError, TradePreimageValue, ValidateAddressResult}; use common::mm_metrics::MetricsArc; +use common::mm_number::MmNumber; use futures::{FutureExt, TryFutureExt}; #[derive(Clone, Debug)] @@ -386,6 +387,8 @@ impl MarketCoinOps for UtxoStandardCoin { fn display_priv_key(&self) -> String { utxo_common::display_priv_key(&self.utxo_arc) } fn min_tx_amount(&self) -> BigDecimal { utxo_common::min_tx_amount(self.as_ref()) } + + fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } } impl MmCoin for UtxoStandardCoin { diff --git a/mm2src/common/mm_number.rs b/mm2src/common/mm_number.rs index ad1461fce3..8c03135e3d 100644 --- a/mm2src/common/mm_number.rs +++ b/mm2src/common/mm_number.rs @@ -1,13 +1,13 @@ use crate::big_int_str::BigIntStr; -use bigdecimal::BigDecimal; use core::ops::{Add, AddAssign, Div, Mul, Sub}; -use num_rational::BigRational; use num_traits::{Pow, Zero}; use serde::{de, Deserialize, Deserializer, Serialize}; use serde_json::value::RawValue; use std::str::FromStr; +pub use bigdecimal::BigDecimal; pub use num_bigint::{BigInt, Sign}; +pub use num_rational::BigRational; pub use paste::paste; /// Construct a `$name` detailed number that have decimal, fraction and rational representations. @@ -32,13 +32,13 @@ macro_rules! construct_detailed { $crate::mm_number::paste! { #[derive(Clone, Debug, Serialize)] pub struct $name { - $base_field: BigDecimal, - [<$base_field _fraction>]: Fraction, - [<$base_field _rat>]: BigRational, + $base_field: $crate::mm_number::BigDecimal, + [<$base_field _fraction>]: $crate::mm_number::Fraction, + [<$base_field _rat>]: $crate::mm_number::BigRational, } - impl From for $name { - fn from(mm_num: MmNumber) -> Self { + impl From<$crate::mm_number::MmNumber> for $name { + fn from(mm_num: $crate::mm_number::MmNumber) -> Self { Self { $base_field: mm_num.to_decimal(), [<$base_field _fraction>]: mm_num.to_fraction(), @@ -49,7 +49,7 @@ macro_rules! construct_detailed { #[allow(dead_code)] impl $name { - pub fn as_ratio(&self) -> &BigRational { + pub fn as_ratio(&self) -> &$crate::mm_number::BigRational { &self.[<$base_field _rat>] } } diff --git a/mm2src/lp_ordermatch.rs b/mm2src/lp_ordermatch.rs index 64ef415663..f1bc75ef79 100644 --- a/mm2src/lp_ordermatch.rs +++ b/mm2src/lp_ordermatch.rs @@ -81,7 +81,6 @@ const MAKER_ORDER_TIMEOUT: u64 = MIN_ORDER_KEEP_ALIVE_INTERVAL * 3; const TAKER_ORDER_TIMEOUT: u64 = 30; const ORDER_MATCH_TIMEOUT: u64 = 30; const ORDERBOOK_REQUESTING_TIMEOUT: u64 = MIN_ORDER_KEEP_ALIVE_INTERVAL * 2; -const MIN_TRADING_VOL: &str = "0.00777"; const MAX_ORDERS_NUMBER_IN_ORDERBOOK_RESPONSE: usize = 1000; /// Alphabetically ordered orderbook pair @@ -958,6 +957,11 @@ enum TakerOrderBuildError { actual: MmNumber, threshold: MmNumber, }, + /// Max vol below min base vol + MaxBaseVolBelowMinBaseVol { + max: MmNumber, + min: MmNumber, + }, SenderPubkeyIsZero, ConfsSettingsNotSet, } @@ -984,6 +988,12 @@ impl fmt::Display for TakerOrderBuildError { actual.to_decimal(), threshold.to_decimal() ), + TakerOrderBuildError::MaxBaseVolBelowMinBaseVol { min, max } => write!( + f, + "Max base vol {} is below min base vol: {}", + max.to_decimal(), + min.to_decimal() + ), TakerOrderBuildError::SenderPubkeyIsZero => write!(f, "Sender pubkey can not be zero"), TakerOrderBuildError::ConfsSettingsNotSet => write!(f, "Confirmation settings must be set"), } @@ -1054,11 +1064,8 @@ impl<'a> TakerOrderBuilder<'a> { /// Validate fields and build fn build(self) -> Result { - let min_vol_threshold = MmNumber::from(MIN_TRADING_VOL); - let min_tx_multiplier = MmNumber::from(10); - let min_base_amount = - (&self.base_coin.min_tx_amount().into() * &min_tx_multiplier).max(min_vol_threshold.clone()); - let min_rel_amount = (&self.rel_coin.min_tx_amount().into() * &min_tx_multiplier).max(min_vol_threshold); + let min_base_amount = self.base_coin.min_trading_vol(); + let min_rel_amount = self.rel_coin.min_trading_vol(); if self.base_coin.ticker() == self.rel_coin.ticker() { return Err(TakerOrderBuildError::BaseEqualRel); @@ -1086,12 +1093,23 @@ impl<'a> TakerOrderBuilder<'a> { return Err(TakerOrderBuildError::ConfsSettingsNotSet); } - let min_volume = self.min_volume.unwrap_or_else(|| min_base_amount.clone()); + let price = &self.rel_amount / &self.base_amount; + let base_min_by_rel = &min_rel_amount / &price; + let base_min_vol_threshold = min_base_amount.max(base_min_by_rel); + + let min_volume = self.min_volume.unwrap_or_else(|| base_min_vol_threshold.clone()); - if min_volume < min_base_amount { + if min_volume < base_min_vol_threshold { return Err(TakerOrderBuildError::MinVolumeTooLow { actual: min_volume, - threshold: min_base_amount, + threshold: base_min_vol_threshold, + }); + } + + if self.base_amount < min_volume { + return Err(TakerOrderBuildError::MaxBaseVolBelowMinBaseVol { + max: self.base_amount, + min: min_volume, }); } @@ -1363,11 +1381,8 @@ impl<'a> MakerOrderBuilder<'a> { /// Validate fields and build fn build(self) -> Result { let min_price = MmNumber::from(BigRational::new(1.into(), 100_000_000.into())); - let min_vol_threshold = MmNumber::from(MIN_TRADING_VOL); - let min_tx_multiplier = MmNumber::from(10); - let min_base_amount = - (&self.base_coin.min_tx_amount().into() * &min_tx_multiplier).max(min_vol_threshold.clone()); - let min_rel_amount = (&self.rel_coin.min_tx_amount().into() * &min_tx_multiplier).max(min_vol_threshold); + let min_base_amount = self.base_coin.min_trading_vol(); + let min_rel_amount = self.rel_coin.min_trading_vol(); if self.base_coin.ticker() == self.rel_coin.ticker() { return Err(MakerOrderBuildError::BaseEqualRel); @@ -1395,11 +1410,14 @@ impl<'a> MakerOrderBuilder<'a> { }); } - let min_base_vol = self.min_base_vol.unwrap_or_else(|| min_base_amount.clone()); - if min_base_vol < min_base_amount { + let base_min_by_rel = &min_rel_amount / &self.price; + let base_min_vol_threshold = min_base_amount.max(base_min_by_rel); + + let min_base_vol = self.min_base_vol.unwrap_or_else(|| base_min_vol_threshold.clone()); + if min_base_vol < base_min_vol_threshold { return Err(MakerOrderBuildError::MinBaseVolTooLow { actual: min_base_vol, - threshold: min_base_amount, + threshold: base_min_vol_threshold, }); } @@ -1435,7 +1453,7 @@ impl<'a> MakerOrderBuilder<'a> { rel: self.rel_coin.ticker().to_owned(), created_at: now_ms(), max_base_vol: self.max_base_vol, - min_base_vol: self.min_base_vol.unwrap_or(MIN_TRADING_VOL.into()), + min_base_vol: self.min_base_vol.unwrap_or(self.base_coin.min_trading_vol()), price: self.price, matches: HashMap::new(), started_swaps: Vec::new(), diff --git a/mm2src/lp_ordermatch/orderbook_rpc.rs b/mm2src/lp_ordermatch/orderbook_rpc.rs index 6ea6938364..4632b08024 100644 --- a/mm2src/lp_ordermatch/orderbook_rpc.rs +++ b/mm2src/lp_ordermatch/orderbook_rpc.rs @@ -1,9 +1,6 @@ use super::{subscribe_to_orderbook_topic, OrdermatchContext, RpcOrderbookEntry}; -use bigdecimal::BigDecimal; use coins::{address_by_coin_conf_and_pubkey_str, coin_conf}; -use common::{mm_ctx::MmArc, - mm_number::{Fraction, MmNumber}, - now_ms}; +use common::{mm_ctx::MmArc, mm_number::MmNumber, now_ms}; use http::Response; use num_rational::BigRational; use num_traits::Zero; diff --git a/mm2src/lp_swap.rs b/mm2src/lp_swap.rs index 46277f881f..14215f7e4a 100644 --- a/mm2src/lp_swap.rs +++ b/mm2src/lp_swap.rs @@ -63,7 +63,7 @@ use common::{bits256, block_on, calc_total_pages, executor::{spawn, Timer}, log::{error, info}, mm_ctx::{from_ctx, MmArc}, - mm_number::{Fraction, MmNumber}, + mm_number::MmNumber, now_ms, read_dir, rpc_response, slurp, var, write, HyRes, TraceSource, Traceable}; use futures::compat::Future01CompatExt; use futures::future::{abortable, AbortHandle, TryFutureExt}; diff --git a/mm2src/mm2_tests.rs b/mm2src/mm2_tests.rs index 406bda5021..c00d371a7c 100644 --- a/mm2src/mm2_tests.rs +++ b/mm2src/mm2_tests.rs @@ -2302,7 +2302,7 @@ fn orderbook_should_display_base_rel_volumes() { let orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("orderbook "[orderbook]); assert_eq!(orderbook.asks.len(), 1, "RICK/MORTY orderbook must have exactly 1 ask"); - let min_volume = BigRational::new(777.into(), 100000.into()); + let min_volume = BigRational::new(1.into(), 10000.into()); assert_eq!(volume, orderbook.asks[0].base_max_volume_rat); assert_eq!(min_volume, orderbook.asks[0].base_min_volume_rat); @@ -2322,7 +2322,7 @@ fn orderbook_should_display_base_rel_volumes() { let orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("orderbook "[orderbook]); assert_eq!(orderbook.bids.len(), 1, "MORTY/RICK orderbook must have exactly 1 bid"); - let min_volume = BigRational::new(777.into(), 100000.into()); + let min_volume = BigRational::new(1.into(), 10000.into()); assert_eq!(volume, orderbook.bids[0].rel_max_volume_rat); assert_eq!(min_volume, orderbook.bids[0].rel_min_volume_rat); @@ -2495,7 +2495,7 @@ fn check_too_low_volume_order_creation_fails(mm: &MarketMakerIt, base: &str, rel "base": base, "rel": rel, "price": "1", - "volume": "0.00776", + "volume": "0.00000099", "cancel_previous": false, }))) .unwrap(); @@ -2506,7 +2506,7 @@ fn check_too_low_volume_order_creation_fails(mm: &MarketMakerIt, base: &str, rel "method": "setprice", "base": base, "rel": rel, - "price": "0.00776", + "price": "0.00000099", "volume": "1", "cancel_previous": false, }))) @@ -2519,7 +2519,7 @@ fn check_too_low_volume_order_creation_fails(mm: &MarketMakerIt, base: &str, rel "base": base, "rel": rel, "price": "1", - "volume": "0.00776", + "volume": "0.00000099", }))) .unwrap(); assert!(!rc.0.is_success(), "sell success, but should be error {}", rc.1); @@ -2530,7 +2530,7 @@ fn check_too_low_volume_order_creation_fails(mm: &MarketMakerIt, base: &str, rel "base": base, "rel": rel, "price": "1", - "volume": "0.00776", + "volume": "0.00000099", }))) .unwrap(); assert!(!rc.0.is_success(), "buy success, but should be error {}", rc.1); diff --git a/mm2src/rpc.rs b/mm2src/rpc.rs index 5b6ccc967c..ee971dff07 100644 --- a/mm2src/rpc.rs +++ b/mm2src/rpc.rs @@ -154,8 +154,9 @@ pub fn dispatcher(req: Json, ctx: MmArc) -> DispatcherRes { "kmd_rewards_info" => hyres(kmd_rewards_info(ctx)), // "inventory" => inventory (ctx, req), "list_banned_pubkeys" => hyres(list_banned_pubkeys_rpc(ctx)), - "metrics" => metrics(ctx), "max_taker_vol" => hyres(max_taker_vol(ctx, req)), + "metrics" => metrics(ctx), + "min_trading_vol" => hyres(min_trading_vol(ctx, req)), "my_balance" => hyres(my_balance(ctx, req)), "my_orders" => hyres(my_orders(ctx)), "my_recent_swaps" => my_recent_swaps(ctx, req), diff --git a/mm2src/rpc/lp_commands.rs b/mm2src/rpc/lp_commands.rs index 1763acdf5f..e41478adb2 100644 --- a/mm2src/rpc/lp_commands.rs +++ b/mm2src/rpc/lp_commands.rs @@ -314,6 +314,35 @@ pub async fn get_my_peer_id(ctx: MmArc) -> Result>, String> { Ok(try_s!(Response::builder().body(res))) } +construct_detailed!(DetailedMinTradingVol, min_trading_vol); + +#[derive(Serialize)] +struct MinTradingVolResponse<'a> { + coin: &'a str, + #[serde(flatten)] + volume: DetailedMinTradingVol, +} + +/// Get min_trading_vol of a coin +pub async fn min_trading_vol(ctx: MmArc, req: Json) -> Result>, String> { + let ticker = try_s!(req["coin"].as_str().ok_or("No 'coin' field")).to_owned(); + let coin = match lp_coinfind(&ctx, &ticker).await { + Ok(Some(t)) => t, + Ok(None) => return ERR!("No such coin: {}", ticker), + Err(err) => return ERR!("!lp_coinfind({}): {}", ticker, err), + }; + let min_trading_vol = coin.min_trading_vol(); + let response = MinTradingVolResponse { + coin: &ticker, + volume: min_trading_vol.into(), + }; + let res = json!({ + "result": response, + }); + let res = try_s!(json::to_vec(&res)); + Ok(try_s!(Response::builder().body(res))) +} + // AP: Inventory is not documented and not used as of now, commented out /* pub fn inventory (ctx: MmArc, req: Json) -> HyRes {