From 76c2f7bdc1a770d65cd43e088521520977db83fe Mon Sep 17 00:00:00 2001 From: "Sergey O. Boyko" <58207208+sergeyboyko0791@users.noreply.github.com> Date: Tue, 9 Feb 2021 20:59:45 +0700 Subject: [PATCH] Pass coin dust to dex fee amount (#813) * Pass coin dust to 'dex_fee_amount' function * Add MakerCoinOps::min_tx_amount() * Fix 'cargo test' warnings * Remove the non-async 'lp_coinfind' function * Take dex_fee_threshold by reference within 'dex_fee_amount' and 'dex_fee_amount_from_taker_coin' --- mm2src/coins/eth.rs | 2 + mm2src/coins/lp_coins.rs | 46 ++++++--------- mm2src/coins/qrc20.rs | 2 + mm2src/coins/test_coin.rs | 2 + mm2src/coins/utxo/qtum.rs | 2 + mm2src/coins/utxo/utxo_common.rs | 4 ++ mm2src/coins/utxo/utxo_standard.rs | 2 + mm2src/common/header.rs | 4 +- mm2src/docker_tests.rs | 64 +++++++++++++++++++- mm2src/docker_tests/qrc20_tests.rs | 18 ++++-- mm2src/lp_native_dex.rs | 10 ++-- mm2src/lp_network.rs | 1 - mm2src/lp_ordermatch.rs | 41 ++++++------- mm2src/lp_swap.rs | 47 ++++++++++----- mm2src/lp_swap/maker_swap.rs | 31 +++++----- mm2src/lp_swap/taker_swap.rs | 94 ++++++++++++++++-------------- mm2src/mm2_tests.rs | 6 +- mm2src/ordermatch_tests.rs | 2 - mm2src/rpc.rs | 23 +++++--- mm2src/rpc/lp_commands.rs | 8 +-- 20 files changed, 249 insertions(+), 160 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 75ea425d7f..e7291a0cd0 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -1056,6 +1056,8 @@ 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) } } 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 7d9141fe06..41762035ed 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -271,6 +271,9 @@ pub trait MarketCoinOps { fn address_from_pubkey_str(&self, pubkey: &str) -> Result; fn display_priv_key(&self) -> String; + + /// Get the minimum amount to send. + fn min_tx_amount(&self) -> BigDecimal; } #[derive(Deserialize)] @@ -399,13 +402,6 @@ impl TransactionDetails { } } -pub enum TradeInfo { - // going to act as maker - Maker, - // going to act as taker with expected dexfee amount - Taker(BigDecimal), -} - #[derive(Clone, Debug, PartialEq, Serialize)] pub struct TradeFee { pub coin: String, @@ -894,14 +890,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result Result, String> { - let cctx = try_s!(CoinsContext::from_ctx(ctx)); - let coins = block_on(cctx.coins.lock()); - Ok(coins.get(ticker).cloned()) -} - -/// NB: Returns only the enabled (aka active) coins. -pub async fn lp_coinfindᵃ(ctx: &MmArc, ticker: &str) -> Result, String> { +pub async fn lp_coinfind(ctx: &MmArc, ticker: &str) -> Result, String> { let cctx = try_s!(CoinsContext::from_ctx(ctx)); let coins = cctx.coins.lock().await; Ok(coins.get(ticker).cloned()) @@ -917,7 +906,7 @@ struct ConvertAddressReq { pub async fn convert_address(ctx: MmArc, req: Json) -> Result>, String> { let req: ConvertAddressReq = try_s!(json::from_value(req)); - let coin = match lp_coinfindᵃ(&ctx, &req.coin).await { + let coin = match lp_coinfind(&ctx, &req.coin).await { Ok(Some(t)) => t, Ok(None) => return ERR!("No such coin: {}", req.coin), Err(err) => return ERR!("!lp_coinfind({}): {}", req.coin, err), @@ -932,8 +921,7 @@ pub async fn convert_address(ctx: MmArc, req: Json) -> Result>, } pub async fn kmd_rewards_info(ctx: MmArc) -> Result>, String> { - let coin = match lp_coinfindᵃ(&ctx, "KMD").await { - // Use lp_coinfindᵃ when async. + let coin = match lp_coinfind(&ctx, "KMD").await { Ok(Some(MmCoinEnum::UtxoCoin(t))) => t, Ok(Some(_)) => return ERR!("KMD was expected to be UTXO"), Ok(None) => return ERR!("KMD is not activated"), @@ -962,7 +950,7 @@ pub struct ValidateAddressResult { pub async fn validate_address(ctx: MmArc, req: Json) -> Result>, String> { let req: ValidateAddressReq = try_s!(json::from_value(req)); - let coin = match lp_coinfindᵃ(&ctx, &req.coin).await { + let coin = match lp_coinfind(&ctx, &req.coin).await { Ok(Some(t)) => t, Ok(None) => return ERR!("No such coin: {}", req.coin), Err(err) => return ERR!("!lp_coinfind({}): {}", req.coin, err), @@ -975,7 +963,7 @@ pub async fn validate_address(ctx: MmArc, req: Json) -> Result> pub async fn withdraw(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 { + 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), @@ -988,7 +976,7 @@ pub async fn withdraw(ctx: MmArc, req: Json) -> Result>, String pub async fn send_raw_transaction(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 { + 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), @@ -1027,8 +1015,8 @@ struct MyTxHistoryRequest { /// Transactions are sorted by number of confirmations in ascending order. pub fn my_tx_history(ctx: MmArc, req: Json) -> HyRes { let request: MyTxHistoryRequest = try_h!(json::from_value(req)); - let coin = match lp_coinfind(&ctx, &request.coin) { - // Should switch to lp_coinfindᵃ when my_tx_history is async. + // Should remove `block_on` when my_tx_history is async. + let coin = match block_on(lp_coinfind(&ctx, &request.coin)) { Ok(Some(t)) => t, Ok(None) => return rpc_err_response(500, &fomat!("No such coin: "(request.coin))), Err(err) => return rpc_err_response(500, &fomat!("!lp_coinfind(" (request.coin) "): " (err))), @@ -1098,7 +1086,7 @@ pub fn my_tx_history(ctx: MmArc, req: Json) -> HyRes { pub async fn get_trade_fee(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 { + 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), @@ -1156,7 +1144,7 @@ pub struct ConfirmationsReq { pub async fn set_required_confirmations(ctx: MmArc, req: Json) -> Result>, String> { let req: ConfirmationsReq = try_s!(json::from_value(req)); - let coin = match lp_coinfindᵃ(&ctx, &req.coin).await { + let coin = match lp_coinfind(&ctx, &req.coin).await { Ok(Some(t)) => t, Ok(None) => return ERR!("No such coin {}", req.coin), Err(err) => return ERR!("!lp_coinfind ({}): {}", req.coin, err), @@ -1179,7 +1167,7 @@ pub struct RequiresNotaReq { pub async fn set_requires_notarization(ctx: MmArc, req: Json) -> Result>, String> { let req: RequiresNotaReq = try_s!(json::from_value(req)); - let coin = match lp_coinfindᵃ(&ctx, &req.coin).await { + let coin = match lp_coinfind(&ctx, &req.coin).await { Ok(Some(t)) => t, Ok(None) => return ERR!("No such coin {}", req.coin), Err(err) => return ERR!("!lp_coinfind ({}): {}", req.coin, err), @@ -1196,7 +1184,7 @@ pub async fn set_requires_notarization(ctx: MmArc, req: Json) -> Result 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 { + 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), @@ -1215,7 +1203,7 @@ pub async fn check_balance_update_loop(ctx: MmArc, ticker: String) { let mut current_balance = None; loop { Timer::sleep(10.).await; - match lp_coinfindᵃ(&ctx, &ticker).await { + match lp_coinfind(&ctx, &ticker).await { Ok(Some(coin)) => { let balance = match coin.my_balance().compat().await { Ok(b) => b, @@ -1291,7 +1279,7 @@ struct ConvertUtxoAddressReq { pub async fn convert_utxo_address(ctx: MmArc, req: Json) -> Result>, String> { let req: ConvertUtxoAddressReq = try_s!(json::from_value(req)); let mut addr: utxo::Address = try_s!(req.address.parse()); - let coin = match lp_coinfindᵃ(&ctx, &req.to_coin).await { + let coin = match lp_coinfind(&ctx, &req.to_coin).await { Ok(Some(c)) => c, _ => return ERR!("Coin {} is not activated", req.to_coin), }; diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 08717a4886..6e2fd62e71 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -894,6 +894,8 @@ 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) } } impl MmCoin for Qrc20Coin { diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index d66df02bf1..6986726863 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -56,6 +56,8 @@ impl MarketCoinOps for TestCoin { fn address_from_pubkey_str(&self, pubkey: &str) -> Result { unimplemented!() } fn display_priv_key(&self) -> String { unimplemented!() } + + fn min_tx_amount(&self) -> BigDecimal { unimplemented!() } } #[mockable] diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 28975144b8..6a889d95b0 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -461,6 +461,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()) } } impl MmCoin for QtumCoin { diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index a6f4dc114b..996e106207 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1181,6 +1181,10 @@ where pub fn display_priv_key(coin: &UtxoCoinFields) -> String { format!("{}", coin.key_pair.private()) } +pub fn min_tx_amount(coin: &UtxoCoinFields) -> BigDecimal { + big_decimal_from_sat(coin.dust_amount as i64, coin.decimals) +} + pub fn is_asset_chain(coin: &UtxoCoinFields) -> bool { coin.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 15adcae2fc..c23624cac7 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -373,6 +373,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()) } } impl MmCoin for UtxoStandardCoin { diff --git a/mm2src/common/header.rs b/mm2src/common/header.rs index 1441259ad3..baf0cc679b 100644 --- a/mm2src/common/header.rs +++ b/mm2src/common/header.rs @@ -1,7 +1,7 @@ //! Indirect routing between crates. //! //! Sometimes we need to call downstream, from a dependency and into a dependent crate, -//! such as when calling `mm2::rpc::rpc_serviceʹ` from `common::MarketMakerIt::rpc`. +//! such as when calling `mm2::rpc::process_rpc_request` from `common::MarketMakerIt::rpc`. //! Here we can use C-like headers and/or constructible slots for that. #[cfg(not(feature = "native"))] use crate::mm_ctx::MmArc; @@ -16,7 +16,7 @@ #[cfg(not(feature = "native"))] use std::pin::Pin; -/// Access to `rpc::rpc_serviceʹ` defined downstream. +/// Access to `rpc::process_rpc_request` defined downstream. /// Initialized in `rpc::init_header_slots`. #[cfg(not(feature = "native"))] pub static RPC_SERVICE: Constructible< diff --git a/mm2src/docker_tests.rs b/mm2src/docker_tests.rs index 7bae7060e9..92656e5d61 100644 --- a/mm2src/docker_tests.rs +++ b/mm2src/docker_tests.rs @@ -1549,6 +1549,8 @@ mod docker_tests { }); assert_eq!(actual, expected); + let dex_fee_threshold = MmNumber::from("0.0001"); + let rc = unwrap!(block_on(mm.rpc(json!({ "userpass": mm.userpass, "method": "max_taker_vol", @@ -1558,7 +1560,7 @@ mod docker_tests { let json: Json = json::from_str(&rc.1).unwrap(); let mycoin_max_vol: MmNumber = json::from_value(json["result"].clone()).expect("Expected a number in fraction representation"); - let mycoin_taker_fee = dex_fee_amount("MYCOIN", "MYCOIN1", &mycoin_max_vol); + let mycoin_taker_fee = dex_fee_amount("MYCOIN", "MYCOIN1", &mycoin_max_vol, &dex_fee_threshold); let rc = unwrap!(block_on(mm.rpc(json!({ "userpass": mm.userpass, @@ -1569,7 +1571,7 @@ mod docker_tests { let json: Json = json::from_str(&rc.1).unwrap(); let mycoin1_max_vol: MmNumber = json::from_value(json["result"].clone()).expect("Expected a number in fraction representation"); - let mycoin1_taker_fee = dex_fee_amount("MYCOIN", "MYCOIN1", &mycoin1_max_vol); + let mycoin1_taker_fee = dex_fee_amount("MYCOIN", "MYCOIN1", &mycoin1_max_vol, &dex_fee_threshold); let rc = unwrap!(block_on(mm.rpc(json!({ "userpass": mm.userpass, @@ -1763,6 +1765,64 @@ mod docker_tests { unwrap!(block_on(mm_alice.stop())); } + /// Test if the `max_taker_vol` cannot return a volume less than the coin's dust. + /// The minimum required balance for trading can be obtained by solving the equation: + /// `volume + taker_fee + trade_fee + fee_to_send_taker_fee = x`. + /// Let `dust = 0.000728` like for Qtum, `trade_fee = 0.0001`, `fee_to_send_taker_fee = 0.0001` and `taker_fee` is the `0.000728` threshold, + /// therefore to find a minimum required balance, we should pass the `dust` as the `volume` into the equation above: + /// `2 * 0.000728 + 0.0002 = x`, so `x = 0.001656` + #[test] + fn test_get_max_taker_vol_dust_threshold() { + // first, try to test with the balance slightly less than required + let (_ctx, coin, priv_key) = generate_coin_with_random_privkey("MYCOIN1", "0.001656".parse().unwrap()); + let coins = json! ([ + {"coin":"MYCOIN","asset":"MYCOIN","txversion":4,"overwintered":1,"txfee":10000,"protocol":{"type":"UTXO"}}, + {"coin":"MYCOIN1","asset":"MYCOIN1","txversion":4,"overwintered":1,"txfee":10000,"protocol":{"type":"UTXO"},"dust":72800}, + ]); + let mut mm = unwrap!(MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_see": true, + }), + "pass".to_string(), + None, + )); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm.log_path); + unwrap!(block_on( + mm.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats ")) + )); + + log!([block_on(enable_native(&mm, "MYCOIN1", vec![]))]); + log!([block_on(enable_native(&mm, "MYCOIN", vec![]))]); + + let rc = unwrap!(block_on(mm.rpc(json!({ + "userpass": mm.userpass, + "method": "max_taker_vol", + "coin": "MYCOIN1", + })))); + assert!(!rc.0.is_success(), "max_taker_vol success, but should fail: {}", rc.1); + + fill_address(&coin, &coin.my_address().unwrap(), "0.00001".parse().unwrap(), 30); + + let rc = unwrap!(block_on(mm.rpc(json! ({ + "userpass": mm.userpass, + "method": "max_taker_vol", + "coin": "MYCOIN1", + })))); + assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); + let json: Json = json::from_str(&rc.1).unwrap(); + // the result of equation x + 0.000728 (dex fee) + 0.0002 (miner fee * 2) = 0.001666 + assert_eq!(json["result"]["numer"], Json::from("369")); + assert_eq!(json["result"]["denom"], Json::from("500000")); + + unwrap!(block_on(mm.stop())); + } + #[test] fn test_get_max_taker_vol_with_kmd() { let (_ctx, _, alice_priv_key) = generate_coin_with_random_privkey("MYCOIN1", 1.into()); diff --git a/mm2src/docker_tests/qrc20_tests.rs b/mm2src/docker_tests/qrc20_tests.rs index 32d912e136..068fa3db98 100644 --- a/mm2src/docker_tests/qrc20_tests.rs +++ b/mm2src/docker_tests/qrc20_tests.rs @@ -1253,6 +1253,7 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ log!([block_on(enable_native(&mm, "QTUM", vec![]))]); let qtum_balance = coin.my_balance().wait().expect("!my_balance"); + let qtum_dex_fee_threshold = MmNumber::from("0.000728"); // - `max_possible = balance - locked_amount`, where `locked_amount = 0` // - `max_trade_fee = trade_fee(balance)` @@ -1271,7 +1272,12 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ // - `max_possible_2 = balance - locked_amount - max_trade_fee`, where `locked_amount = 0` let max_possible_2 = &qtum_balance - &max_trade_fee; // - `max_dex_fee = dex_fee(max_possible_2)` - let max_dex_fee = dex_fee_amount("QTUM", "MYCOIN", &MmNumber::from(max_possible_2)); + let max_dex_fee = dex_fee_amount( + "QTUM", + "MYCOIN", + &MmNumber::from(max_possible_2), + &qtum_dex_fee_threshold, + ); debug!("max_dex_fee: {:?}", max_dex_fee.to_fraction()); // - `max_fee_to_send_taker_fee = fee_to_send_taker_fee(max_dex_fee)` @@ -1287,9 +1293,11 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ // where `available = balance - locked_amount - max_trade_fee - max_fee_to_send_taker_fee` let available = &qtum_balance - &max_trade_fee - &max_fee_to_send_taker_fee; debug!("total_available: {}", available); - let expected_max_taker_vol = max_taker_vol_from_available(MmNumber::from(available), "QTUM", "MYCOIN") - .expect("max_taker_vol_from_available"); - let real_dex_fee = dex_fee_amount("QTUM", "MYCOIN", &expected_max_taker_vol); + let min_tx_amount = qtum_dex_fee_threshold.clone(); + let expected_max_taker_vol = + max_taker_vol_from_available(MmNumber::from(available), "QTUM", "MYCOIN", &min_tx_amount) + .expect("max_taker_vol_from_available"); + let real_dex_fee = dex_fee_amount("QTUM", "MYCOIN", &expected_max_taker_vol, &qtum_dex_fee_threshold); debug!("real_max_dex_fee: {:?}", real_dex_fee.to_fraction()); // check if the actual max_taker_vol equals to the expected @@ -1320,7 +1328,7 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ let timelock = (now_ms() / 1000) as u32 - 200; let secret_hash = &[0; 20]; - let dex_fee_amount = dex_fee_amount("QTUM", "MYCOIN", &expected_max_taker_vol); + let dex_fee_amount = dex_fee_amount("QTUM", "MYCOIN", &expected_max_taker_vol, &qtum_dex_fee_threshold); let _taker_fee_tx = coin .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, dex_fee_amount.to_decimal()) .wait() diff --git a/mm2src/lp_native_dex.rs b/mm2src/lp_native_dex.rs index b4a49555ac..59d92f8f56 100644 --- a/mm2src/lp_native_dex.rs +++ b/mm2src/lp_native_dex.rs @@ -527,11 +527,9 @@ pub async fn lp_init(mypubport: u16, ctx: MmArc) -> Result<(), String> { *(try_s!(ctx.coins_needed_for_kick_start.lock())) = coins_needed_for_kick_start; } - let ctxʹ = ctx.clone(); - spawn(lp_ordermatch_loop(ctxʹ)); + spawn(lp_ordermatch_loop(ctx.clone())); - let ctxʹ = ctx.clone(); - spawn(broadcast_maker_orders_keep_alive_loop(ctxʹ)); + spawn(broadcast_maker_orders_keep_alive_loop(ctx.clone())); #[cfg(not(feature = "native"))] { @@ -543,9 +541,9 @@ pub async fn lp_init(mypubport: u16, ctx: MmArc) -> Result<(), String> { let ctx_id = try_s!(ctx.ffi_handle()); spawn_rpc(ctx_id); - let ctxʹ = ctx.clone(); + let ctx_c = ctx.clone(); spawn(async move { - if let Err(err) = ctxʹ.init_metrics() { + if let Err(err) = ctx_c.init_metrics() { log!("Warning: couldn't initialize metrics system: "(err)); } }); diff --git a/mm2src/lp_network.rs b/mm2src/lp_network.rs index 72030399aa..bbf60c50d0 100644 --- a/mm2src/lp_network.rs +++ b/mm2src/lp_network.rs @@ -17,7 +17,6 @@ // marketmaker // use common::executor::spawn; -#[cfg(not(feature = "native"))] use common::helperᶜ; use common::mm_ctx::MmArc; use common::HyRes; use futures::{channel::oneshot, lock::Mutex as AsyncMutex, StreamExt}; diff --git a/mm2src/lp_ordermatch.rs b/mm2src/lp_ordermatch.rs index da2d20e804..aeb1002c22 100644 --- a/mm2src/lp_ordermatch.rs +++ b/mm2src/lp_ordermatch.rs @@ -24,7 +24,7 @@ use bigdecimal::BigDecimal; use blake2::digest::{Update, VariableOutput}; use blake2::VarBlake2b; use coins::utxo::{compressed_pub_key_from_priv_raw, ChecksumType}; -use coins::{lp_coinfindᵃ, BalanceTradeFeeUpdatedHandler, FeeApproxStage, MmCoinEnum}; +use coins::{lp_coinfind, BalanceTradeFeeUpdatedHandler, FeeApproxStage, MmCoinEnum}; use common::executor::{spawn, Timer}; use common::log::error; use common::mm_ctx::{from_ctx, MmArc, MmWeak}; @@ -2047,7 +2047,7 @@ impl OrdermatchContext { fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerOrder) { spawn(async move { // aka "maker_loop" - let taker_coin = match lp_coinfindᵃ(&ctx, &maker_match.reserved.rel).await { + let taker_coin = match lp_coinfind(&ctx, &maker_match.reserved.rel).await { Ok(Some(c)) => c, Ok(None) => { log::error!("Coin {} is not found/enabled", maker_match.reserved.rel); @@ -2059,7 +2059,7 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO }, }; - let maker_coin = match lp_coinfindᵃ(&ctx, &maker_match.reserved.base).await { + let maker_coin = match lp_coinfind(&ctx, &maker_match.reserved.base).await { Ok(Some(c)) => c, Ok(None) => { log::error!("Coin {} is not found/enabled", maker_match.reserved.base); @@ -2134,7 +2134,7 @@ fn lp_connected_alice(ctx: MmArc, taker_request: TakerRequest, taker_match: Take // aka "taker_loop" let mut maker = bits256::default(); maker.bytes = taker_match.reserved.sender_pubkey.0; - let taker_coin = match lp_coinfindᵃ(&ctx, &taker_match.reserved.rel).await { + let taker_coin = match lp_coinfind(&ctx, &taker_match.reserved.rel).await { Ok(Some(c)) => c, Ok(None) => { log::error!("Coin {} is not found/enabled", taker_match.reserved.rel); @@ -2146,7 +2146,7 @@ fn lp_connected_alice(ctx: MmArc, taker_request: TakerRequest, taker_match: Take }, }; - let maker_coin = match lp_coinfindᵃ(&ctx, &taker_match.reserved.base).await { + let maker_coin = match lp_coinfind(&ctx, &taker_match.reserved.base).await { Ok(Some(c)) => c, Ok(None) => { log::error!("Coin {} is not found/enabled", taker_match.reserved.base); @@ -2308,8 +2308,8 @@ pub async fn lp_ordermatch_loop(ctx: MmArc) { let my_maker_orders = ordermatch_ctx.my_maker_orders.lock().await; for (uuid, order) in my_maker_orders.iter() { if !ordermatch_ctx.orderbook.lock().await.order_set.contains_key(uuid) { - if let Ok(Some(_)) = lp_coinfindᵃ(&ctx, &order.base).await { - if let Ok(Some(_)) = lp_coinfindᵃ(&ctx, &order.rel).await { + if let Ok(Some(_)) = lp_coinfind(&ctx, &order.base).await { + if let Ok(Some(_)) = lp_coinfind(&ctx, &order.rel).await { let topic = orderbook_topic_from_base_rel(&order.base, &order.rel); if !ordermatch_ctx.orderbook.lock().await.is_subscribed_to(&topic) { let request_orderbook = false; @@ -2433,11 +2433,11 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: for (uuid, order) in filtered { if let OrderMatchResult::Matched((base_amount, rel_amount)) = order.match_with_request(&taker_request) { - let base_coin = match lp_coinfindᵃ(&ctx, &order.base).await { + let base_coin = match lp_coinfind(&ctx, &order.base).await { Ok(Some(c)) => c, _ => return, // attempt to match with deactivated coin }; - let rel_coin = match lp_coinfindᵃ(&ctx, &order.rel).await { + let rel_coin = match lp_coinfind(&ctx, &order.rel).await { Ok(Some(c)) => c, _ => return, // attempt to match with deactivated coin }; @@ -2562,9 +2562,9 @@ pub async fn buy(ctx: MmArc, req: Json) -> Result>, String> { if input.base == input.rel { return ERR!("Base and rel must be different coins"); } - let rel_coin = try_s!(lp_coinfindᵃ(&ctx, &input.rel).await); + let rel_coin = try_s!(lp_coinfind(&ctx, &input.rel).await); let rel_coin = try_s!(rel_coin.ok_or("Rel coin is not found or inactive")); - let base_coin = try_s!(lp_coinfindᵃ(&ctx, &input.base).await); + let base_coin = try_s!(lp_coinfind(&ctx, &input.base).await); let base_coin: MmCoinEnum = try_s!(base_coin.ok_or("Base coin is not found or inactive")); if base_coin.wallet_only() { return ERR!("Base coin is wallet only"); @@ -2594,9 +2594,9 @@ pub async fn sell(ctx: MmArc, req: Json) -> Result>, String> { if input.base == input.rel { return ERR!("Base and rel must be different coins"); } - let base_coin = try_s!(lp_coinfindᵃ(&ctx, &input.base).await); + let base_coin = try_s!(lp_coinfind(&ctx, &input.base).await); let base_coin = try_s!(base_coin.ok_or("Base coin is not found or inactive")); - let rel_coin = try_s!(lp_coinfindᵃ(&ctx, &input.rel).await); + let rel_coin = try_s!(lp_coinfind(&ctx, &input.rel).await); let rel_coin = try_s!(rel_coin.ok_or("Rel coin is not found or inactive")); if base_coin.wallet_only() { return ERR!("Base coin is wallet only"); @@ -2955,12 +2955,12 @@ impl<'a> From<&'a MakerOrder> for MakerOrderForRpc<'a> { pub async fn set_price(ctx: MmArc, req: Json) -> Result>, String> { let req: SetPriceReq = try_s!(json::from_value(req)); - let base_coin: MmCoinEnum = match try_s!(lp_coinfindᵃ(&ctx, &req.base).await) { + let base_coin: MmCoinEnum = match try_s!(lp_coinfind(&ctx, &req.base).await) { Some(coin) => coin, None => return ERR!("Base coin {} is not found", req.base), }; - let rel_coin: MmCoinEnum = match try_s!(lp_coinfindᵃ(&ctx, &req.rel).await) { + let rel_coin: MmCoinEnum = match try_s!(lp_coinfind(&ctx, &req.rel).await) { Some(coin) => coin, None => return ERR!("Rel coin {} is not found", req.rel), }; @@ -3012,9 +3012,6 @@ pub async fn set_price(ctx: MmArc, req: Json) -> Result>, Strin // note the `calc_max_maker_vol` returns [`CheckBalanceError::NotSufficientBalance`] error if the balance of `base_coin` is not sufficient try_s!(calc_max_maker_vol(&ctx, &base_coin, &my_balance, FeeApproxStage::OrderIssue).await) } else { - if req.volume <= MmNumber::from(0) { - return ERR!("Maker volume {} cannot be zero or negative", req.volume); - } try_s!( check_balance_for_maker_swap( &ctx, @@ -3537,9 +3534,9 @@ pub async fn orderbook(ctx: MmArc, req: Json) -> Result>, Strin if req.base == req.rel { return ERR!("Base and rel must be different coins"); } - let rel_coin = try_s!(lp_coinfindᵃ(&ctx, &req.rel).await); + let rel_coin = try_s!(lp_coinfind(&ctx, &req.rel).await); let rel_coin = try_s!(rel_coin.ok_or("Rel coin is not found or inactive")); - let base_coin = try_s!(lp_coinfindᵃ(&ctx, &req.base).await); + let base_coin = try_s!(lp_coinfind(&ctx, &req.base).await); let base_coin: MmCoinEnum = try_s!(base_coin.ok_or("Base coin is not found or inactive")); let request_orderbook = true; try_s!(subscribe_to_orderbook_topic(&ctx, &req.base, &req.rel, request_orderbook).await); @@ -3634,8 +3631,8 @@ pub async fn orderbook(ctx: MmArc, req: Json) -> Result>, Strin rel: req.rel, timestamp: now_ms() / 1000, }; - let responseʲ = try_s!(json::to_vec(&response)); - Ok(try_s!(Response::builder().body(responseʲ))) + let response = try_s!(json::to_vec(&response)); + Ok(try_s!(Response::builder().body(response))) } fn choose_maker_confs_and_notas( diff --git a/mm2src/lp_swap.rs b/mm2src/lp_swap.rs index 6eef3014c9..e2f3bebcdc 100644 --- a/mm2src/lp_swap.rs +++ b/mm2src/lp_swap.rs @@ -707,6 +707,16 @@ pub fn lp_atomic_locktime(maker_coin: &str, taker_coin: &str, version: AtomicLoc } } +fn dex_fee_threshold(min_tx_amount: MmNumber) -> MmNumber { + // 0.0001 + let min_fee = MmNumber::from((1, 10000)); + if min_fee < min_tx_amount { + min_tx_amount + } else { + min_fee + } +} + fn dex_fee_rate(base: &str, rel: &str) -> MmNumber { if base == "KMD" || rel == "KMD" { // 1/777 - 10% @@ -716,18 +726,22 @@ fn dex_fee_rate(base: &str, rel: &str) -> MmNumber { } } -pub fn dex_fee_amount(base: &str, rel: &str, trade_amount: &MmNumber) -> MmNumber { +pub fn dex_fee_amount(base: &str, rel: &str, trade_amount: &MmNumber, dex_fee_threshold: &MmNumber) -> MmNumber { let rate = dex_fee_rate(base, rel); - // 0.0001 - let min_fee = BigRational::new(1.into(), 10000.into()).into(); let fee_amount = trade_amount * &rate; - if fee_amount < min_fee { - min_fee + if &fee_amount < dex_fee_threshold { + dex_fee_threshold.clone() } else { fee_amount } } +pub fn dex_fee_amount_from_taker_coin(taker_coin: &MmCoinEnum, maker_coin: &str, trade_amount: &MmNumber) -> MmNumber { + let min_tx_amount = MmNumber::from(taker_coin.min_tx_amount()); + let dex_fee_threshold = dex_fee_threshold(min_tx_amount); + dex_fee_amount(taker_coin.ticker(), maker_coin, trade_amount, &dex_fee_threshold) +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct NegotiationDataMsg { started_at: u64, @@ -829,14 +843,16 @@ impl SavedSwap { fn recover_funds(self, ctx: MmArc) -> Result { let maker_ticker = try_s!(self.maker_coin_ticker()); - let maker_coin = match lp_coinfind(&ctx, &maker_ticker) { + // Should remove `block_on` when recover_funds is async. + let maker_coin = match block_on(lp_coinfind(&ctx, &maker_ticker)) { Ok(Some(c)) => c, Ok(None) => return ERR!("Coin {} is not activated", maker_ticker), Err(e) => return ERR!("Error {} on {} coin find attempt", e, maker_ticker), }; let taker_ticker = try_s!(self.taker_coin_ticker()); - let taker_coin = match lp_coinfind(&ctx, &taker_ticker) { + // Should remove `block_on` when recover_funds is async. + let taker_coin = match block_on(lp_coinfind(&ctx, &taker_ticker)) { Ok(Some(c)) => c, Ok(None) => return ERR!("Coin {} is not activated", taker_ticker), Err(e) => return ERR!("Error {} on {} coin find attempt", e, taker_ticker), @@ -1210,7 +1226,7 @@ pub fn swap_kick_starts(ctx: MmArc) -> HashSet { let ctx = ctx.clone(); move || { let taker_coin = loop { - match lp_coinfind(&ctx, &taker_coin_ticker) { + match block_on(lp_coinfind(&ctx, &taker_coin_ticker)) { Ok(Some(c)) => break c, Ok(None) => { log!("Can't kickstart the swap " (swap.uuid()) " until the coin " (taker_coin_ticker) " is activated"); @@ -1224,7 +1240,7 @@ pub fn swap_kick_starts(ctx: MmArc) -> HashSet { }; let maker_coin = loop { - match lp_coinfind(&ctx, &maker_coin_ticker) { + match block_on(lp_coinfind(&ctx, &maker_coin_ticker)) { Ok(Some(c)) => break c, Ok(None) => { log!("Can't kickstart the swap " (swap.uuid()) " until the coin " (maker_coin_ticker) " is activated"); @@ -1423,33 +1439,34 @@ mod lp_swap_tests { #[test] fn test_dex_fee_amount() { + let dex_fee_threshold = MmNumber::from("0.0001"); + let base = "BTC"; let rel = "ETH"; let amount = 1.into(); - let actual_fee = dex_fee_amount(base, rel, &amount); + let actual_fee = dex_fee_amount(base, rel, &amount, &dex_fee_threshold); let expected_fee = amount / 777u64.into(); assert_eq!(expected_fee, actual_fee); let base = "KMD"; let rel = "ETH"; let amount = 1.into(); - let actual_fee = dex_fee_amount(base, rel, &amount); + let actual_fee = dex_fee_amount(base, rel, &amount, &dex_fee_threshold); let expected_fee = amount * (9, 7770).into(); assert_eq!(expected_fee, actual_fee); let base = "BTC"; let rel = "KMD"; let amount = 1.into(); - let actual_fee = dex_fee_amount(base, rel, &amount); + let actual_fee = dex_fee_amount(base, rel, &amount, &dex_fee_threshold); let expected_fee = amount * (9, 7770).into(); assert_eq!(expected_fee, actual_fee); let base = "BTC"; let rel = "KMD"; let amount: MmNumber = unwrap!("0.001".parse::()).into(); - let actual_fee = dex_fee_amount(base, rel, &amount); - let expected_fee: MmNumber = unwrap!("0.0001".parse::()).into(); - assert_eq!(expected_fee, actual_fee); + let actual_fee = dex_fee_amount(base, rel, &amount, &dex_fee_threshold); + assert_eq!(dex_fee_threshold, actual_fee); } #[test] diff --git a/mm2src/lp_swap/maker_swap.rs b/mm2src/lp_swap/maker_swap.rs index 8796d9558a..f9c48ddc5b 100644 --- a/mm2src/lp_swap/maker_swap.rs +++ b/mm2src/lp_swap/maker_swap.rs @@ -1,17 +1,17 @@ #![cfg_attr(not(feature = "native"), allow(dead_code))] use super::{ban_pubkey, broadcast_my_swap_status, broadcast_swap_message_every, check_base_coin_balance_for_swap, - check_my_coin_balance_for_swap, check_other_coin_balance_for_swap, dex_fee_amount, get_locked_amount, - my_swap_file_path, my_swaps_dir, recv_swap_msg, swap_topic, AtomicSwap, CheckBalanceError, DetailedVolume, - LockedAmount, MySwapInfo, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedTradeFee, - SwapConfirmationsSettings, SwapError, SwapMsg, SwapsContext, TradeFeeResponse, TradePreimageRequest, - TradePreimageResponse, TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; + check_my_coin_balance_for_swap, check_other_coin_balance_for_swap, dex_fee_amount_from_taker_coin, + get_locked_amount, my_swap_file_path, my_swaps_dir, recv_swap_msg, swap_topic, AtomicSwap, + CheckBalanceError, DetailedVolume, LockedAmount, MySwapInfo, RecoveredSwap, RecoveredSwapAction, + SavedSwap, SavedTradeFee, SwapConfirmationsSettings, SwapError, SwapMsg, SwapsContext, TradeFeeResponse, + TradePreimageRequest, TradePreimageResponse, TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; use crate::mm2::{lp_network::subscribe_to_topic, lp_swap::NegotiationDataMsg}; use atomic::Atomic; use bigdecimal::BigDecimal; use bitcrypto::dhash160; -use coins::{lp_coinfindᵃ, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, TradeFee, TradePreimageValue, TransactionEnum}; +use coins::{lp_coinfind, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, TradeFee, TradePreimageValue, TransactionEnum}; use common::{bits256, executor::Timer, file_lock::FileLock, mm_ctx::MmArc, mm_number::MmNumber, now_ms, slurp, write, Traceable, DEX_FEE_ADDR_RAW_PUBKEY, MM_VERSION}; use futures::{compat::Future01CompatExt, select, FutureExt}; @@ -454,11 +454,8 @@ impl MakerSwap { let hash = taker_fee.tx_hash(); log!({ "Taker fee tx {:02x}", hash }); - let fee_amount = dex_fee_amount( - &self.r().data.maker_coin, - &self.r().data.taker_coin, - &self.taker_amount.clone().into(), - ); + let taker_amount = MmNumber::from(self.taker_amount.clone()); + let fee_amount = dex_fee_amount_from_taker_coin(&self.taker_coin, &self.r().data.maker_coin, &taker_amount); let mut attempts = 0; loop { @@ -1495,12 +1492,12 @@ pub async fn maker_swap_trade_preimage( ctx: &MmArc, req: TradePreimageRequest, ) -> Result { - let base_coin = match lp_coinfindᵃ(&ctx, &req.base).await { + let base_coin = match lp_coinfind(&ctx, &req.base).await { Ok(Some(t)) => t, Ok(None) => return ERR!("No such coin: {}", req.base), Err(err) => return ERR!("!lp_coinfind({}): {}", req.base, err), }; - let rel_coin = match lp_coinfindᵃ(&ctx, &req.rel).await { + let rel_coin = match lp_coinfind(&ctx, &req.rel).await { Ok(Some(t)) => t, Ok(None) => return ERR!("No such coin: {}", req.rel), Err(err) => return ERR!("!lp_coinfind({}): {}", req.rel, err), @@ -1569,11 +1566,13 @@ pub async fn calc_max_maker_vol( .await .trace(source!())?; } - if vol <= MmNumber::from(0) { + let min_tx_amount = MmNumber::from(coin.min_tx_amount()); + if vol < min_tx_amount { let err = ERRL!( - "Not enough funds for swap: balance: {}, locked by swaps: {:.8}", + "Not enough funds for swap: balance: {}, locked by swaps: {:.8}, required at least {:.8}", balance, - locked + locked, + min_tx_amount ); return Err(CheckBalanceError::NotSufficientBalance(err)); } diff --git a/mm2src/lp_swap/taker_swap.rs b/mm2src/lp_swap/taker_swap.rs index 4df4da0332..718b7428ab 100644 --- a/mm2src/lp_swap/taker_swap.rs +++ b/mm2src/lp_swap/taker_swap.rs @@ -1,16 +1,16 @@ #![cfg_attr(not(feature = "native"), allow(dead_code))] use super::{ban_pubkey, broadcast_my_swap_status, broadcast_swap_message_every, check_my_coin_balance_for_swap, - check_other_coin_balance_for_swap, dex_fee_amount, dex_fee_rate, get_locked_amount, my_swap_file_path, - my_swaps_dir, recv_swap_msg, swap_topic, AtomicSwap, CheckBalanceError, DetailedTakerFee, DetailedVolume, - LockedAmount, MySwapInfo, NegotiationDataMsg, RecoveredSwap, RecoveredSwapAction, SavedSwap, - SavedTradeFee, SwapConfirmationsSettings, SwapError, SwapMsg, SwapsContext, TakerFeeAdditionalInfo, - TradeFeeResponse, TradePreimageMethod, TradePreimageRequest, TradePreimageResponse, TransactionIdentifier, - WAIT_CONFIRM_INTERVAL}; + check_other_coin_balance_for_swap, dex_fee_amount_from_taker_coin, dex_fee_rate, dex_fee_threshold, + get_locked_amount, my_swap_file_path, my_swaps_dir, recv_swap_msg, swap_topic, AtomicSwap, + CheckBalanceError, DetailedTakerFee, DetailedVolume, LockedAmount, MySwapInfo, NegotiationDataMsg, + RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedTradeFee, SwapConfirmationsSettings, SwapError, + SwapMsg, SwapsContext, TakerFeeAdditionalInfo, TradeFeeResponse, TradePreimageMethod, + TradePreimageRequest, TradePreimageResponse, TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; use crate::mm2::lp_network::subscribe_to_topic; use atomic::Atomic; use bigdecimal::BigDecimal; -use coins::{lp_coinfindᵃ, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, TradeFee, TradePreimageValue}; +use coins::{lp_coinfind, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, TradeFee, TradePreimageValue}; use common::executor::Timer; use common::log::debug; use common::mm_ctx::MmArc; @@ -673,11 +673,7 @@ impl TakerSwap { async fn start(&self) -> Result<(Option, Vec), String> { let stage = FeeApproxStage::StartSwap; - let dex_fee = dex_fee_amount( - &self.r().data.maker_coin, - &self.r().data.taker_coin, - &self.taker_amount.clone(), - ); + let dex_fee = dex_fee_amount_from_taker_coin(&self.taker_coin, &self.r().data.maker_coin, &self.taker_amount); let preimage_value = TradePreimageValue::Exact(self.taker_amount.to_decimal()); let fee_to_send_dex_fee_fut = self @@ -871,11 +867,8 @@ impl TakerSwap { ])); } - let fee_amount = dex_fee_amount( - &self.r().data.maker_coin, - &self.r().data.taker_coin, - &self.taker_amount.clone(), - ); + let fee_amount = + dex_fee_amount_from_taker_coin(&self.taker_coin, &self.r().data.maker_coin, &self.taker_amount); let fee_tx = self .taker_coin .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, fee_amount.into()) @@ -1419,11 +1412,8 @@ impl AtomicSwap for TakerSwap { let mut result = Vec::new(); // if taker fee is not sent yet it must be virtually locked - let taker_fee_amount = dex_fee_amount( - self.maker_coin.ticker(), - self.taker_coin.ticker(), - &self.taker_amount.clone(), - ); + let taker_fee_amount = + dex_fee_amount_from_taker_coin(&self.taker_coin, &self.r().data.maker_coin, &self.taker_amount); let trade_fee = self.r().data.fee_to_send_taker_fee.clone().map(TradeFee::from); if self.r().taker_fee.is_none() { result.push(LockedAmount { @@ -1482,7 +1472,7 @@ pub async fn check_balance_for_taker_swap( let params = match prepared_params { Some(params) => params, None => { - let dex_fee = dex_fee_amount(my_coin.ticker(), other_coin.ticker(), &volume); + let dex_fee = dex_fee_amount_from_taker_coin(my_coin, other_coin.ticker(), &volume); let fee_to_send_dex_fee = try_s!( my_coin .get_fee_to_send_taker_fee(dex_fee.to_decimal(), stage.clone()) @@ -1535,12 +1525,12 @@ pub async fn taker_swap_trade_preimage( TradePreimageMethod::Sell => (req.base, req.rel), TradePreimageMethod::Buy => (req.rel, req.base), }; - let my_coin = match lp_coinfindᵃ(&ctx, &my_coin_ticker).await { + let my_coin = match lp_coinfind(&ctx, &my_coin_ticker).await { Ok(Some(t)) => t, Ok(None) => return ERR!("No such coin: {}", my_coin_ticker), Err(err) => return ERR!("!lp_coinfind({}): {}", my_coin_ticker, err), }; - let other_coin = match lp_coinfindᵃ(&ctx, &other_coin_ticker).await { + let other_coin = match lp_coinfind(&ctx, &other_coin_ticker).await { Ok(Some(t)) => t, Ok(None) => return ERR!("No such coin: {}", other_coin_ticker), Err(err) => return ERR!("!lp_coinfind({}): {}", other_coin_ticker, err), @@ -1553,7 +1543,7 @@ pub async fn taker_swap_trade_preimage( req.volume }; - let dex_amount = dex_fee_amount(&my_coin_ticker, &other_coin_ticker, &volume); + let dex_amount = dex_fee_amount_from_taker_coin(&my_coin, &other_coin_ticker, &volume); let fee_to_send_dex_fee = try_s!( my_coin .get_fee_to_send_taker_fee(dex_amount.to_decimal(), stage.clone()) @@ -1596,7 +1586,7 @@ struct MaxTakerVolRequest { pub async fn max_taker_vol(ctx: MmArc, req: Json) -> Result>, String> { let req: MaxTakerVolRequest = try_s!(json::from_value(req)); - let coin = match lp_coinfindᵃ(&ctx, &req.coin).await { + let coin = match lp_coinfind(&ctx, &req.coin).await { Ok(Some(t)) => t, Ok(None) => return ERR!("No such coin: {}", req.coin), Err(err) => return ERR!("!lp_coinfind({}): {}", req.coin, err), @@ -1641,6 +1631,7 @@ pub async fn calc_max_taker_vol( let my_coin = coin.ticker(); let balance = MmNumber::from(try_map!(coin.my_balance().compat().await, CheckBalanceError::Other)); let locked = get_locked_amount(ctx, my_coin); + let min_tx_amount = MmNumber::from(coin.min_tx_amount()); let max_possible = &balance - &locked; let preimage_value = TradePreimageValue::UpperBound(max_possible.to_decimal()); @@ -1653,7 +1644,7 @@ pub async fn calc_max_taker_vol( let max_vol = if my_coin == max_trade_fee.coin { // second case let max_possible_2 = &max_possible - &max_trade_fee.amount; - let max_dex_fee = dex_fee_amount(my_coin, other_coin, &max_possible_2); + let max_dex_fee = dex_fee_amount_from_taker_coin(coin, other_coin, &max_possible_2); let max_fee_to_send_taker_fee = coin .get_fee_to_send_taker_fee(max_dex_fee.to_decimal(), stage) .compat() @@ -1670,22 +1661,28 @@ pub async fn calc_max_taker_vol( max_dex_fee.to_fraction(), max_fee_to_send_taker_fee.amount.to_fraction() ); - max_taker_vol_from_available(min_max_possible, my_coin, other_coin).trace(source!())? + max_taker_vol_from_available(min_max_possible, my_coin, other_coin, &min_tx_amount).trace(source!())? } else { // first case debug!( - "max_taker_vol case 1: balance {:?}, locked {:?}, ", + "max_taker_vol case 1: balance {:?}, locked {:?}", balance.to_fraction(), locked.to_fraction() ); - max_taker_vol_from_available(max_possible, my_coin, other_coin).trace(source!())? + max_taker_vol_from_available(max_possible, my_coin, other_coin, &min_tx_amount).trace(source!())? }; + // do not check if `max_vol < min_tx_amount`, because it is checked within `max_taker_vol_from_available` already Ok(max_vol) } -pub fn max_taker_vol_from_available(available: MmNumber, base: &str, rel: &str) -> Result { +pub fn max_taker_vol_from_available( + available: MmNumber, + base: &str, + rel: &str, + min_tx_amount: &MmNumber, +) -> Result { + let fee_threshold = dex_fee_threshold(min_tx_amount.clone()); let dex_fee_rate = dex_fee_rate(base, rel); - let fee_threshold = MmNumber::from("0.0001"); let threshold_coef = &(&MmNumber::from(1) + &dex_fee_rate) / &dex_fee_rate; let max_vol = if available > &fee_threshold * &threshold_coef { available / (MmNumber::from(1) + dex_fee_rate) @@ -1693,8 +1690,12 @@ pub fn max_taker_vol_from_available(available: MmNumber, base: &str, rel: &str) available - fee_threshold }; - if max_vol <= MmNumber::from(0) { - let err = ERRL!("Max taker volume {} cannot be zero or negative", max_vol); + if &max_vol <= min_tx_amount { + let err = ERRL!( + "Max taker volume {:?} less than minimum transaction amount {:?}", + max_vol.to_fraction(), + min_tx_amount.to_fraction() + ); return Err(CheckBalanceError::NotSufficientBalance(err)); } Ok(max_vol) @@ -1703,6 +1704,7 @@ pub fn max_taker_vol_from_available(available: MmNumber, base: &str, rel: &str) #[cfg(test)] mod taker_swap_tests { use super::*; + use crate::mm2::lp_swap::dex_fee_amount; use coins::eth::{addr_from_str, signed_eth_tx_from_bytes, SignedEthTx}; use coins::utxo::UtxoTx; use coins::{FoundSwapTxSpend, MarketCoinOps, MmCoin, SwapOps, TestCoin}; @@ -2107,6 +2109,7 @@ mod taker_swap_tests { #[test] fn test_max_taker_vol_from_available() { let dex_fee_threshold = MmNumber::from("0.0001"); + let min_tx_amount = MmNumber::from("0.00001"); // For these `availables` the dex_fee must be greater than threshold let source = vec![ @@ -2125,10 +2128,12 @@ mod taker_swap_tests { let available = MmNumber::from(available); // no matter base or rel is KMD let base = if is_kmd { "RICK" } else { "MORTY" }; - let max_taker_vol = max_taker_vol_from_available(available.clone(), "RICK", "MORTY") + let max_taker_vol = max_taker_vol_from_available(available.clone(), "RICK", "MORTY", &min_tx_amount) .expect("!max_taker_vol_from_available"); - let dex_fee = dex_fee_amount(base, "MORTY", &max_taker_vol); + + let dex_fee = dex_fee_amount(base, "MORTY", &max_taker_vol, &dex_fee_threshold); assert!(dex_fee_threshold < dex_fee); + assert!(min_tx_amount <= max_taker_vol); assert_eq!(max_taker_vol + dex_fee, available); } @@ -2138,23 +2143,26 @@ mod taker_swap_tests { ("0.0863333333333333333333333333333333333333333333333331", true), ("0.0777999999999999999999999999999999999999999999999999", false), ("0.0777", false), - ("0.00011", false), - ("0.0001000000000000000000000000000000000000000000000001", false), + ("0.0002", false), ]; for (available, is_kmd) in source { let available = MmNumber::from(available); // no matter base or rel is KMD let base = if is_kmd { "KMD" } else { "RICK" }; - let max_taker_vol = - max_taker_vol_from_available(available.clone(), base, "MORTY").expect("!max_taker_vol_from_available"); - let dex_fee = dex_fee_amount(base, "MORTY", &max_taker_vol); + let max_taker_vol = max_taker_vol_from_available(available.clone(), base, "MORTY", &min_tx_amount) + .expect("!max_taker_vol_from_available"); + let dex_fee = dex_fee_amount(base, "MORTY", &max_taker_vol, &dex_fee_threshold); log!("available "[available.to_decimal()]" max_taker_vol "[max_taker_vol.to_decimal()]", dex_fee "[dex_fee.to_decimal()]); assert_eq!(dex_fee_threshold, dex_fee); + assert!(min_tx_amount <= max_taker_vol); assert_eq!(max_taker_vol + dex_fee, available); } // these `availables` must be the cause of an error let availables = vec![ + "0.0001999", + "0.00011", + "0.0001000000000000000000000000000000000000000000000001", "0.0001", "0.0000999999999999999999999999999999999999999999999999", "0.0000000000000000000000000000000000000000000000000001", @@ -2163,7 +2171,7 @@ mod taker_swap_tests { ]; for available in availables { let available = MmNumber::from(available); - max_taker_vol_from_available(available.clone(), "KMD", "MORTY") + max_taker_vol_from_available(available.clone(), "KMD", "MORTY", &dex_fee_threshold) .expect_err("!max_taker_vol_from_available success but should be error"); } } diff --git a/mm2src/mm2_tests.rs b/mm2src/mm2_tests.rs index 49ac7d1c5a..193fb91b13 100644 --- a/mm2src/mm2_tests.rs +++ b/mm2src/mm2_tests.rs @@ -145,9 +145,9 @@ fn chdir(dir: &Path) { #[cfg(not(windows))] { use std::ffi::CString; - let dirˢ = unwrap!(dir.to_str()); - let dirᶜ = unwrap!(CString::new(dirˢ)); - let rc = unsafe { libc::chdir(dirᶜ.as_ptr()) }; + let dir_s = unwrap!(dir.to_str()); + let dir_c = unwrap!(CString::new(dir_s)); + let rc = unsafe { libc::chdir(dir_c.as_ptr()) }; assert_eq!(rc, 0, "Can not chdir to {:?}", dir); } diff --git a/mm2src/ordermatch_tests.rs b/mm2src/ordermatch_tests.rs index 0db4f04b73..fb89cfacb9 100644 --- a/mm2src/ordermatch_tests.rs +++ b/mm2src/ordermatch_tests.rs @@ -2205,7 +2205,6 @@ fn test_process_sync_pubkey_orderbook_state_points_to_not_uptodate_trie_root() { let mut roots = HashMap::new(); roots.insert(alb_pair.clone(), old_root); - let propagated_from_peer = String::default(); let SyncPubkeyOrderbookStateRes { mut pair_orders_diff, .. } = block_on(process_sync_pubkey_orderbook_state(ctx.clone(), pubkey, roots)) @@ -2315,7 +2314,6 @@ fn test_remove_and_purge_pubkey_pair_orders() { } let rick_morty_pair = alb_ordered_pair("RICK", "MORTY"); - let rick_kmd_pair = alb_ordered_pair("RICK", "KMD"); let ordermatch_ctx = OrdermatchContext::from_ctx(&ctx).unwrap(); let mut orderbook = block_on(ordermatch_ctx.orderbook.lock()); diff --git a/mm2src/rpc.rs b/mm2src/rpc.rs index 7fd89d0acc..96c2548c60 100644 --- a/mm2src/rpc.rs +++ b/mm2src/rpc.rs @@ -190,14 +190,19 @@ pub fn dispatcher(req: Json, ctx: MmArc) -> DispatcherRes { }) } -async fn rpc_serviceʹ(ctx: MmArc, req: Parts, reqᵇ: Body, client: SocketAddr) -> Result>, String> { +async fn process_rpc_request( + ctx: MmArc, + req: Parts, + req_body: Body, + client: SocketAddr, +) -> Result>, String> { if req.method != Method::POST { return ERR!("Only POST requests are supported!"); } - let reqᵇ = try_s!(hyper::body::to_bytes(reqᵇ).await); - let reqʲ: Json = try_s!(json::from_slice(&reqᵇ)); - match reqʲ.as_array() { + let req_bytes = try_s!(hyper::body::to_bytes(req_body).await); + let req_json: Json = try_s!(json::from_slice(&req_bytes)); + match req_json.as_array() { Some(requests) => { let mut futures = Vec::with_capacity(requests.len()); for request in requests { @@ -220,7 +225,7 @@ async fn rpc_serviceʹ(ctx: MmArc, req: Parts, reqᵇ: Body, client: SocketAddr) let res = try_s!(json::to_vec(&responses)); Ok(try_s!(Response::builder().body(res))) }, - None => process_single_request(ctx, reqʲ, client).await, + None => process_single_request(ctx, req_json, client).await, } } @@ -263,8 +268,8 @@ async fn rpc_service(req: Request, ctx_h: u32, client: SocketAddr) -> Resp }; // Convert the native Hyper stream into a portable stream of `Bytes`. - let (req, reqᵇ) = req.into_parts(); - let (mut parts, body) = match rpc_serviceʹ(ctx, req, reqᵇ, client).await { + let (req, req_body) = req.into_parts(); + let (mut parts, body) = match process_rpc_request(ctx, req, req_body, client).await { Ok(r) => r.into_parts(), Err(err) => { log!("RPC error response: "(err)); @@ -353,10 +358,10 @@ pub fn init_header_slots() { fn rpc_service_fn( ctx: MmArc, req: Parts, - reqᵇ: Box + Send>, + req_body: Box + Send>, client: SocketAddr, ) -> Pin>, String>> + Send>> { - Box::pin(rpc_serviceʹ(ctx, req, reqᵇ, client)) + Box::pin(process_rpc_request(ctx, req, req_body, client)) } let _ = RPC_SERVICE.pin(rpc_service_fn); } diff --git a/mm2src/rpc/lp_commands.rs b/mm2src/rpc/lp_commands.rs index ffd732868a..ffb491ed5c 100644 --- a/mm2src/rpc/lp_commands.rs +++ b/mm2src/rpc/lp_commands.rs @@ -20,7 +20,7 @@ #![cfg_attr(not(feature = "native"), allow(dead_code))] #![cfg_attr(not(feature = "native"), allow(unused_imports))] -use coins::{disable_coin as disable_coin_impl, lp_coinfindᵃ, lp_coininit, MmCoinEnum}; +use coins::{disable_coin as disable_coin_impl, lp_coinfind, lp_coininit, MmCoinEnum}; use common::executor::{spawn, Timer}; use common::mm_ctx::MmArc; use common::{rpc_err_response, rpc_response, HyRes, MM_DATETIME, MM_VERSION}; @@ -35,8 +35,7 @@ use crate::mm2::lp_swap::active_swaps_using_coin; /// Attempts to disable the coin pub async fn disable_coin(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 { - // Use lp_coinfindᵃ when async. + 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({}): ", err), @@ -145,8 +144,7 @@ pub fn metrics(ctx: MmArc) -> HyRes { /// Get my_balance of a coin pub async fn my_balance(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 { - // Use lp_coinfindᵃ when async. + 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),