Skip to content

Commit

Permalink
Pass coin dust to dex fee amount (#813)
Browse files Browse the repository at this point in the history
* 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'
  • Loading branch information
sergeyboyko0791 committed Feb 9, 2021
1 parent 0734e7a commit 76c2f7b
Show file tree
Hide file tree
Showing 20 changed files with 249 additions and 160 deletions.
2 changes: 2 additions & 0 deletions mm2src/coins/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SignedEthTx, String> {
Expand Down
46 changes: 17 additions & 29 deletions mm2src/coins/lp_coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ pub trait MarketCoinOps {
fn address_from_pubkey_str(&self, pubkey: &str) -> Result<String, String>;

fn display_priv_key(&self) -> String;

/// Get the minimum amount to send.
fn min_tx_amount(&self) -> BigDecimal;
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -894,14 +890,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result<MmCoin
}

/// NB: Returns only the enabled (aka active) coins.
pub fn lp_coinfind(ctx: &MmArc, ticker: &str) -> Result<Option<MmCoinEnum>, 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<Option<MmCoinEnum>, String> {
pub async fn lp_coinfind(ctx: &MmArc, ticker: &str) -> Result<Option<MmCoinEnum>, String> {
let cctx = try_s!(CoinsContext::from_ctx(ctx));
let coins = cctx.coins.lock().await;
Ok(coins.get(ticker).cloned())
Expand All @@ -917,7 +906,7 @@ struct ConvertAddressReq {

pub async fn convert_address(ctx: MmArc, req: Json) -> Result<Response<Vec<u8>>, 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),
Expand All @@ -932,8 +921,7 @@ pub async fn convert_address(ctx: MmArc, req: Json) -> Result<Response<Vec<u8>>,
}

pub async fn kmd_rewards_info(ctx: MmArc) -> Result<Response<Vec<u8>>, 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"),
Expand Down Expand Up @@ -962,7 +950,7 @@ pub struct ValidateAddressResult {

pub async fn validate_address(ctx: MmArc, req: Json) -> Result<Response<Vec<u8>>, 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),
Expand All @@ -975,7 +963,7 @@ pub async fn validate_address(ctx: MmArc, req: Json) -> Result<Response<Vec<u8>>

pub async fn withdraw(ctx: MmArc, req: Json) -> Result<Response<Vec<u8>>, 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),
Expand All @@ -988,7 +976,7 @@ pub async fn withdraw(ctx: MmArc, req: Json) -> Result<Response<Vec<u8>>, String

pub async fn send_raw_transaction(ctx: MmArc, req: Json) -> Result<Response<Vec<u8>>, 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),
Expand Down Expand Up @@ -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))),
Expand Down Expand Up @@ -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<Response<Vec<u8>>, 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),
Expand Down Expand Up @@ -1156,7 +1144,7 @@ pub struct ConfirmationsReq {

pub async fn set_required_confirmations(ctx: MmArc, req: Json) -> Result<Response<Vec<u8>>, 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),
Expand All @@ -1179,7 +1167,7 @@ pub struct RequiresNotaReq {

pub async fn set_requires_notarization(ctx: MmArc, req: Json) -> Result<Response<Vec<u8>>, 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),
Expand All @@ -1196,7 +1184,7 @@ pub async fn set_requires_notarization(ctx: MmArc, req: Json) -> Result<Response

pub async fn show_priv_key(ctx: MmArc, req: Json) -> Result<Response<Vec<u8>>, 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),
Expand All @@ -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,
Expand Down Expand Up @@ -1291,7 +1279,7 @@ struct ConvertUtxoAddressReq {
pub async fn convert_utxo_address(ctx: MmArc, req: Json) -> Result<Response<Vec<u8>>, 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),
};
Expand Down
2 changes: 2 additions & 0 deletions mm2src/coins/qrc20.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions mm2src/coins/test_coin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ impl MarketCoinOps for TestCoin {
fn address_from_pubkey_str(&self, pubkey: &str) -> Result<String, String> { unimplemented!() }

fn display_priv_key(&self) -> String { unimplemented!() }

fn min_tx_amount(&self) -> BigDecimal { unimplemented!() }
}

#[mockable]
Expand Down
2 changes: 2 additions & 0 deletions mm2src/coins/utxo/qtum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions mm2src/coins/utxo/utxo_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(coin: T, req: WithdrawRequest) -> Result<TransactionDetails, String>
Expand Down
2 changes: 2 additions & 0 deletions mm2src/coins/utxo/utxo_standard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions mm2src/common/header.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<
Expand Down
64 changes: 62 additions & 2 deletions mm2src/docker_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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());
Expand Down
18 changes: 13 additions & 5 deletions mm2src/docker_tests/qrc20_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand All @@ -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)`
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
10 changes: 4 additions & 6 deletions mm2src/lp_native_dex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
{
Expand All @@ -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));
}
});
Expand Down
1 change: 0 additions & 1 deletion mm2src/lp_network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
Loading

0 comments on commit 76c2f7b

Please sign in to comment.