diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 4248294eb7..0859d98725 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -2488,6 +2488,7 @@ impl MmCoin for EthCoin { Ok(TradeFee { coin: fee_coin.into(), amount: try_s!(u256_to_big_decimal(fee, 18)).into(), + paid_from_trading_vol: false, }) })) } @@ -2535,6 +2536,7 @@ impl MmCoin for EthCoin { Ok(TradeFee { coin: fee_coin.into(), amount: amount.into(), + paid_from_trading_vol: false, }) }; Box::new(fut.boxed().compat()) @@ -2557,6 +2559,7 @@ impl MmCoin for EthCoin { Ok(TradeFee { coin: fee_coin.into(), amount: amount.into(), + paid_from_trading_vol: false, }) }; Box::new(fut.boxed().compat()) @@ -2613,6 +2616,7 @@ impl MmCoin for EthCoin { Ok(TradeFee { coin: fee_coin.into(), amount: amount.into(), + paid_from_trading_vol: false, }) }; Box::new(fut.boxed().compat()) diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 50688f1768..9971044267 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -684,6 +684,7 @@ fn get_sender_trade_preimage() { TradeFee { coin: "ETH".to_owned(), amount: amount.into(), + paid_from_trading_vol: false, } } @@ -739,6 +740,7 @@ fn get_erc20_sender_trade_preimage() { TradeFee { coin: "ETH".to_owned(), amount: amount.into(), + paid_from_trading_vol: false, } } @@ -806,6 +808,7 @@ fn get_receiver_trade_preimage() { let expected_fee = TradeFee { coin: "ETH".to_owned(), amount: amount.into(), + paid_from_trading_vol: false, }; let actual = coin @@ -829,6 +832,7 @@ fn test_get_fee_to_send_taker_fee() { let expected_fee = TradeFee { coin: "ETH".to_owned(), amount: amount.into(), + paid_from_trading_vol: false, }; let dex_fee_amount = u256_to_big_decimal(DEX_FEE_AMOUNT.into(), 18).expect("!u256_to_big_decimal"); diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 5d2011f390..ccd45ca51a 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -430,6 +430,7 @@ impl TransactionDetails { pub struct TradeFee { pub coin: String, pub amount: MmNumber, + pub paid_from_trading_vol: bool, } #[derive(Clone, Debug, PartialEq, PartialOrd)] diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 7139b0e402..0ebad5ffbf 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -948,6 +948,7 @@ impl MmCoin for Qrc20Coin { Ok(TradeFee { coin: selfi.platform.clone(), amount: big_decimal_from_sat(fee as i64, selfi.utxo.decimals).into(), + paid_from_trading_vol: false, }) }; Box::new(fut.boxed().compat()) @@ -1016,6 +1017,7 @@ impl MmCoin for Qrc20Coin { Ok(TradeFee { coin: selfi.platform.clone(), amount: total_fee.into(), + paid_from_trading_vol: false, }) }; Box::new(fut.boxed().compat()) @@ -1046,6 +1048,7 @@ impl MmCoin for Qrc20Coin { Ok(TradeFee { coin: selfi.platform.clone(), amount: total_fee.into(), + paid_from_trading_vol: false, }) }; Box::new(fut.boxed().compat()) @@ -1077,6 +1080,7 @@ impl MmCoin for Qrc20Coin { Ok(TradeFee { coin: selfi.platform.clone(), amount: total_fee.into(), + paid_from_trading_vol: false, }) }; Box::new(fut.boxed().compat()) diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index e7745d7323..341f07d41c 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -20,7 +20,7 @@ pub fn qrc20_coin_for_test(priv_key: &[u8]) -> (MmArc, Qrc20Coin) { "wiftype":128, "segwit":true, "mm2":1, - "mature_confirmations":500, + "mature_confirmations":2000, }); let req = json!({ "method": "electrum", @@ -602,6 +602,7 @@ fn test_get_trade_fee() { let expected = TradeFee { coin: "QTUM".into(), amount: expected_trade_fee_amount.into(), + paid_from_trading_vol: false, }; assert_eq!(actual_trade_fee, expected); } @@ -635,6 +636,7 @@ fn test_sender_trade_preimage_zero_allowance() { let expected = TradeFee { coin: "QTUM".to_owned(), amount: (erc20_payment_fee_with_one_approve + sender_refund_fee).into(), + paid_from_trading_vol: false, }; assert_eq!(actual, expected); } @@ -671,6 +673,7 @@ fn test_sender_trade_preimage_with_allowance() { let expected = TradeFee { coin: "QTUM".to_owned(), amount: (erc20_payment_fee_without_approve + sender_refund_fee.clone()).into(), + paid_from_trading_vol: false, }; assert_eq!(actual, expected); @@ -682,6 +685,7 @@ fn test_sender_trade_preimage_with_allowance() { let expected = TradeFee { coin: "QTUM".to_owned(), amount: (erc20_payment_fee_with_two_approves + sender_refund_fee).into(), + paid_from_trading_vol: false, }; assert_eq!(actual, expected); } @@ -708,6 +712,7 @@ fn test_receiver_trade_preimage() { let expected = TradeFee { coin: "QTUM".to_owned(), amount: expected_receiver_fee.into(), + paid_from_trading_vol: false, }; assert_eq!(actual, expected); } @@ -740,6 +745,7 @@ fn test_taker_fee_tx_fee() { let expected = TradeFee { coin: "QTUM".to_owned(), amount: expected_receiver_fee.into(), + paid_from_trading_vol: false, }; assert_eq!(actual, expected); } @@ -759,7 +765,7 @@ fn test_coin_from_conf_without_decimals() { "wiftype":128, "segwit":true, "mm2":1, - "mature_confirmations":500, + "mature_confirmations":2000, }); let req = json!({ "method": "electrum", diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 0b41c099ac..7360f0c96c 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -12,12 +12,22 @@ use serde_json::Value as Json; /// Dummy coin struct used in tests which functions are unimplemented but then mocked /// in specific test to emulate the required behaviour #[derive(Clone, Debug)] -pub struct TestCoin {} +pub struct TestCoin { + ticker: String, +} + +impl Default for TestCoin { + fn default() -> Self { TestCoin { ticker: "test".into() } } +} + +impl TestCoin { + pub fn new(ticker: &str) -> TestCoin { TestCoin { ticker: ticker.into() } } +} #[mockable] #[allow(clippy::forget_ref, clippy::forget_copy, clippy::cast_ref_to_mut)] impl MarketCoinOps for TestCoin { - fn ticker(&self) -> &str { "test" } + fn ticker(&self) -> &str { &self.ticker } fn my_address(&self) -> Result { unimplemented!() } diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 5fbcd33ae6..a5aa3ea9b2 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -531,7 +531,7 @@ impl MmCoin for QtumCoin { &self, _stage: FeeApproxStage, ) -> Box + Send> { - utxo_common::get_receiver_trade_fee(self) + utxo_common::get_receiver_trade_fee(self.clone()) } fn get_fee_to_send_taker_fee( diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 3e7a66e86f..e46149015d 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1852,6 +1852,7 @@ where Ok(TradeFee { coin: ticker, amount: big_decimal_from_sat(amount as i64, decimals).into(), + paid_from_trading_vol: false, }) }; Box::new(fut.boxed().compat()) @@ -1972,22 +1973,27 @@ where Ok(TradeFee { coin: coin.as_ref().conf.ticker.clone(), amount: fee_amount.into(), + paid_from_trading_vol: false, }) }; Box::new(fut.boxed().compat()) } -/// Payment sender should not pay fee for sending Maker Payment. -/// Even if refund will be required the fee will be deducted from P2SH input. -pub fn get_receiver_trade_fee(coin: &T) -> Box + Send> +/// The fee to spend (receive) other payment is deducted from the trading amount so we should display it +pub fn get_receiver_trade_fee(coin: T) -> Box + Send> where - T: AsRef, + T: AsRef + UtxoCommonOps + Send + Sync + 'static, { - let trade_fee = TradeFee { - coin: coin.as_ref().conf.ticker.clone(), - amount: 0.into(), + let fut = async move { + let amount_sat = try_map!(get_htlc_spend_fee(&coin).await, TradePreimageError::Other); + let amount = big_decimal_from_sat_unsigned(amount_sat, coin.as_ref().decimals).into(); + Ok(TradeFee { + coin: coin.as_ref().conf.ticker.clone(), + amount, + paid_from_trading_vol: true, + }) }; - Box::new(futures01::future::ok(trade_fee)) + Box::new(fut.boxed().compat()) } pub fn get_fee_to_send_taker_fee( @@ -2016,6 +2022,7 @@ where Ok(TradeFee { coin: coin.ticker().to_owned(), amount: fee_amount.into(), + paid_from_trading_vol: false, }) }; Box::new(fut.boxed().compat()) @@ -2229,6 +2236,10 @@ pub fn big_decimal_from_sat(satoshis: i64, decimals: u8) -> BigDecimal { BigDecimal::from(satoshis) / BigDecimal::from(10u64.pow(decimals as u32)) } +pub fn big_decimal_from_sat_unsigned(satoshis: u64, decimals: u8) -> BigDecimal { + BigDecimal::from(satoshis) / BigDecimal::from(10u64.pow(decimals as u32)) +} + pub fn address_from_raw_pubkey( pub_key: &[u8], prefix: u8, diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index ecd8a4fc32..98bd6bfbe7 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -425,7 +425,7 @@ impl MmCoin for UtxoStandardCoin { &self, _stage: FeeApproxStage, ) -> Box + Send> { - utxo_common::get_receiver_trade_fee(&self) + utxo_common::get_receiver_trade_fee(self.clone()) } fn get_fee_to_send_taker_fee( diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 4c44711de4..82091b35cf 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -2160,7 +2160,7 @@ fn test_qtum_is_unspent_mature() { #[test] #[ignore] -// TODO it fails in certain cases of the tx fee, need to investigate +// TODO it fails at least when fee is 2055837 sat per kbyte, need to investigate fn test_get_sender_trade_fee_dynamic_tx_fee() { let rpc_client = electrum_client_for_test(&["95.217.83.126:10001"]); let mut coin_fields = utxo_coin_fields_for_test( diff --git a/mm2src/common/big_int_str.rs b/mm2src/common/big_int_str.rs index 2ac405ab91..22b8b95241 100644 --- a/mm2src/common/big_int_str.rs +++ b/mm2src/common/big_int_str.rs @@ -3,7 +3,7 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; /// BigInt wrapper de/serializable from/to string representation -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq)] pub struct BigIntStr(BigInt); impl fmt::Debug for BigIntStr { diff --git a/mm2src/common/build.rs b/mm2src/common/build.rs index 34956bf482..000d895b15 100644 --- a/mm2src/common/build.rs +++ b/mm2src/common/build.rs @@ -130,7 +130,11 @@ fn root() -> PathBuf { /// Absolute path taken from SuperNET's root + `path`. fn rabs(rrel: &str) -> PathBuf { root().join(rrel) } -fn path2s(path: PathBuf) -> String { path.to_str().expect(&format!("Non-stringy path {:?}", path)).into() } +fn path2s(path: PathBuf) -> String { + path.to_str() + .unwrap_or_else(|| panic!("Non-stringy path {:?}", path)) + .into() +} /// Loads the `path`, runs `update` on it and saves back the result if it differs. fn _in_place(path: &dyn AsRef, update: &mut dyn FnMut(Vec) -> Vec) { diff --git a/mm2src/common/mm_number.rs b/mm2src/common/mm_number.rs index 10ac1e88a9..2cadbd7f07 100644 --- a/mm2src/common/mm_number.rs +++ b/mm2src/common/mm_number.rs @@ -1,12 +1,13 @@ use crate::big_int_str::BigIntStr; use bigdecimal::BigDecimal; -use core::ops::{Add, Div, Mul, Sub}; +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 num_bigint::{BigInt, Sign}; pub use paste::paste; /// Construct a `$name` detailed number that have decimal, fraction and rational representations. @@ -45,18 +46,23 @@ macro_rules! construct_detailed { } } } + + #[allow(dead_code)] + impl $name { + pub fn as_ratio(&self) -> &BigRational { + &self.[<$base_field _rat>] + } + } } }; } -pub use num_bigint::{BigInt, Sign}; - #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize)] pub struct MmNumber(BigRational); /// Rational number representation de/serializable in human readable form /// Should simplify the visual perception and parsing in code -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct Fraction { /// Numerator numer: BigIntStr, @@ -209,6 +215,14 @@ impl Add for MmNumber { fn add(self, rhs: Self) -> Self::Output { (self.0 + rhs.0).into() } } +impl AddAssign for MmNumber { + fn add_assign(&mut self, rhs: Self) { self.0 += rhs.0; } +} + +impl AddAssign<&MmNumber> for MmNumber { + fn add_assign(&mut self, rhs: &Self) { self.0 += &rhs.0; } +} + impl Add for &MmNumber { type Output = MmNumber; diff --git a/mm2src/docker_tests.rs b/mm2src/docker_tests.rs index 64a975799c..8b13f17411 100644 --- a/mm2src/docker_tests.rs +++ b/mm2src/docker_tests.rs @@ -86,6 +86,7 @@ mod docker_tests { mod qrc20_tests; use crate::mm2::lp_swap::dex_fee_amount; + use crate::mm2::mm2_tests::structs::*; use bigdecimal::BigDecimal; use bitcrypto::ChecksumType; use chain::OutPoint; @@ -1462,32 +1463,6 @@ mod docker_tests { block_on(mm_alice.stop()).unwrap(); } - fn assert_eq_trade_preimages(mut actual: Json, mut expected: Json) { - #[derive(Debug, Deserialize, Eq, PartialEq)] - struct TradeFeeHelper { - coin: String, - amount: Json, - amount_fraction: Json, - amount_rat: Json, - } - - // `total_fees` are arrays, they can be in a different order. - // Extract them and sort by coins before the comparison - - let actual_total_fees = actual["result"]["total_fees"].take(); - let mut actual_total_fees: Vec = json::from_value(actual_total_fees.clone()) - .expect(&format!("Expected an array of fees, found {:?}", actual_total_fees)); - actual_total_fees.sort_by(|fee1, fee2| fee1.coin.cmp(&fee2.coin)); - - let expected_total_fees = expected["result"]["total_fees"].take(); - let mut expected_total_fees: Vec = json::from_value(expected_total_fees.clone()) - .expect(&format!("Expected an array of fees, found {:?}", expected_total_fees)); - expected_total_fees.sort_by(|fee1, fee2| fee1.coin.cmp(&fee2.coin)); - - assert_eq!(actual_total_fees, expected_total_fees); - assert_eq!(actual, expected); - } - #[test] fn test_maker_trade_preimage() { let priv_key = SecretKey::random(&mut rand4::thread_rng()).serialize(); @@ -1532,35 +1507,25 @@ mod docker_tests { }))) .unwrap(); assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); - let actual: Json = json::from_str(&rc.1).unwrap(); - let expected = json!({ - "result": { - "base_coin_fee": { - "coin": "MYCOIN", - "amount": "0.00001", - "amount_fraction": { "numer": "1", "denom": "100000" }, - "amount_rat": [[1,[1]],[1,[100000]]] - }, - "rel_coin_fee": { - "coin": "MYCOIN1", - "amount": "0", - "amount_fraction": { "numer": "0", "denom": "1" }, - "amount_rat": [[0,[]],[1,[1]]] - }, - "volume": "9.99999", - "volume_fraction": { "numer": "999999", "denom": "100000" }, - "volume_rat": [[1,[999999]],[1,[100000]]], - "total_fees": [ - { - "coin": "MYCOIN", - "amount": "0.00001", - "amount_fraction": { "numer": "1", "denom": "100000" }, - "amount_rat": [[1,[1]],[1,[100000]]] - }, - ], - } + let base_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00001", false); + let rel_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00002", true); + let volume = MmNumber::from("9.99999"); + + let my_coin_total = TotalTradeFeeForTest::new("MYCOIN", "0.00001", "0.00001"); + let my_coin1_total = TotalTradeFeeForTest::new("MYCOIN1", "0.00002", "0"); + + let expected = TradePreimageResult::MakerPreimage(MakerPreimage { + base_coin_fee: base_coin_fee.clone(), + rel_coin_fee: rel_coin_fee.clone(), + volume: Some(volume.to_decimal()), + volume_rat: Some(volume.to_ratio()), + volume_fraction: Some(volume.to_fraction()), + total_fees: vec![my_coin_total, my_coin1_total], }); - assert_eq_trade_preimages(actual, expected); + + let mut actual: TradePreimageResponse = json::from_str(&rc.1).unwrap(); + actual.sort_total_fees(); + assert_eq!(expected, actual.result); let rc = block_on(mm.rpc(json!({ "userpass": mm.userpass, @@ -1572,35 +1537,26 @@ mod docker_tests { }))) .unwrap(); assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); - let actual: Json = json::from_str(&rc.1).unwrap(); - let expected = json!({ - "result": { - "base_coin_fee": { - "coin": "MYCOIN1", - "amount": "0.00002", - "amount_fraction": { "numer": "1", "denom": "50000" }, - "amount_rat": [[1,[1]],[1,[50000]]] - }, - "rel_coin_fee": { - "coin": "MYCOIN", - "amount": "0", - "amount_fraction": { "numer": "0", "denom": "1" }, - "amount_rat": [[0,[]],[1,[1]]] - }, - "volume": "19.99998", - "volume_fraction": { "numer": "999999", "denom": "50000" }, - "volume_rat": [[1,[999999]],[1,[50000]]], - "total_fees": [ - { - "coin": "MYCOIN1", - "amount": "0.00002", - "amount_fraction": { "numer": "1", "denom": "50000" }, - "amount_rat": [[1,[1]],[1,[50000]]] - } - ], - } + let mut actual: TradePreimageResponse = json::from_str(&rc.1).unwrap(); + actual.sort_total_fees(); + + let base_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00002", false); + let rel_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00001", true); + let volume = MmNumber::from("19.99998"); + + let my_coin_total = TotalTradeFeeForTest::new("MYCOIN", "0.00001", "0"); + let my_coin1_total = TotalTradeFeeForTest::new("MYCOIN1", "0.00002", "0.00002"); + let expected = TradePreimageResult::MakerPreimage(MakerPreimage { + base_coin_fee: base_coin_fee.clone(), + rel_coin_fee: rel_coin_fee.clone(), + volume: Some(volume.to_decimal()), + volume_rat: Some(volume.to_ratio()), + volume_fraction: Some(volume.to_fraction()), + total_fees: vec![my_coin_total, my_coin1_total], }); - assert_eq_trade_preimages(actual, expected); + + actual.sort_total_fees(); + assert_eq!(expected, actual.result); let rc = block_on(mm.rpc(json!({ "userpass": mm.userpass, @@ -1612,32 +1568,26 @@ mod docker_tests { }))) .unwrap(); assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); - let actual: Json = json::from_str(&rc.1).unwrap(); - let expected = json!({ - "result": { - "base_coin_fee": { - "coin": "MYCOIN1", - "amount": "0.00002", - "amount_fraction": { "numer": "1", "denom": "50000" }, - "amount_rat": [[1,[1]],[1,[50000]]] - }, - "rel_coin_fee": { - "coin": "MYCOIN", - "amount": "0", - "amount_fraction": { "numer": "0", "denom": "1" }, - "amount_rat": [[0,[]],[1,[1]]] - }, - "total_fees": [ - { - "coin": "MYCOIN1", - "amount": "0.00002", - "amount_fraction": { "numer": "1", "denom": "50000" }, - "amount_rat": [[1,[1]],[1,[50000]]] - } - ], - } + let mut actual: TradePreimageResponse = json::from_str(&rc.1).unwrap(); + actual.sort_total_fees(); + + let base_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00002", false); + let rel_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00001", true); + + let total_my_coin = TotalTradeFeeForTest::new("MYCOIN", "0.00001", "0"); + let total_my_coin1 = TotalTradeFeeForTest::new("MYCOIN1", "0.00002", "0.00002"); + + let expected = TradePreimageResult::MakerPreimage(MakerPreimage { + base_coin_fee: base_coin_fee.clone(), + rel_coin_fee: rel_coin_fee.clone(), + volume: None, + volume_rat: None, + volume_fraction: None, + total_fees: vec![total_my_coin, total_my_coin1], }); - assert_eq_trade_preimages(actual, expected); + + actual.sort_total_fees(); + assert_eq!(expected, actual.result); } #[test] @@ -1709,44 +1659,26 @@ mod docker_tests { }))) .unwrap(); assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); - let actual: Json = json::from_str(&rc.1).unwrap(); - let expected = json!({ - "result": { - "base_coin_fee": { - "coin": "MYCOIN", - "amount": "0.00001", - "amount_fraction": { "numer": "1", "denom": "100000" }, - "amount_rat": [[1,[1]],[1,[100000]]] - }, - "rel_coin_fee": { - "coin": "MYCOIN1", - "amount": "0", - "amount_fraction": { "numer": "0", "denom": "1" }, - "amount_rat": [[0,[]],[1,[1]]] - }, - "taker_fee": { - "coin": "MYCOIN", - "amount": "0.01", - "amount_fraction": { "numer": "1", "denom": "100" }, - "amount_rat": [[1,[1]],[1,[100]]] - }, - "fee_to_send_taker_fee": { - "coin": "MYCOIN", - "amount": "0.00001", - "amount_fraction": { "numer": "1", "denom": "100000" }, - "amount_rat": [[1,[1]],[1,[100000]]] - }, - "total_fees": [ - { - "coin": "MYCOIN", - "amount": "0.01002", - "amount_fraction": { "numer": "501", "denom": "50000" }, - "amount_rat": [[1,[501]],[1,[50000]]] - } - ], - } + + let mut actual: TradePreimageResponse = json::from_str(&rc.1).unwrap(); + actual.sort_total_fees(); + + let base_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00001", false); + let rel_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00002", true); + let taker_fee = TradeFeeForTest::new("MYCOIN", "0.01", false); + let fee_to_send_taker_fee = TradeFeeForTest::new("MYCOIN", "0.00001", false); + + let my_coin_total_fee = TotalTradeFeeForTest::new("MYCOIN", "0.01002", "0.01002"); + let my_coin1_total_fee = TotalTradeFeeForTest::new("MYCOIN1", "0.00002", "0"); + + let expected = TradePreimageResult::TakerPreimage(TakerPreimage { + base_coin_fee, + rel_coin_fee, + taker_fee, + fee_to_send_taker_fee, + total_fees: vec![my_coin_total_fee, my_coin1_total_fee], }); - assert_eq!(actual, expected); + assert_eq!(expected, actual.result); let rc = block_on(mm.rpc(json!({ "userpass": mm.userpass, @@ -1759,44 +1691,25 @@ mod docker_tests { }))) .unwrap(); assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); - let actual: Json = json::from_str(&rc.1).unwrap(); - let expected = json!({ - "result": { - "base_coin_fee": { - "coin": "MYCOIN", - "amount": "0", - "amount_fraction": { "numer": "0", "denom": "1" }, - "amount_rat": [[0,[]],[1,[1]]] - }, - "rel_coin_fee": { - "coin": "MYCOIN1", - "amount": "0.00002", - "amount_fraction": { "numer": "1", "denom": "50000" }, - "amount_rat": [[1,[1]],[1,[50000]]] - }, - "taker_fee": { - "coin": "MYCOIN1", - "amount": "0.02", // volume(7.77) * price(2) / 777 - "amount_fraction": { "numer": "1", "denom": "50" }, - "amount_rat": [[1,[1]],[1,[50]]] - }, - "fee_to_send_taker_fee": { - "coin": "MYCOIN1", - "amount": "0.00002", - "amount_fraction": { "numer": "1", "denom": "50000" }, - "amount_rat": [[1,[1]],[1,[50000]]] - }, - "total_fees": [ - { - "coin": "MYCOIN1", - "amount": "0.02004", - "amount_fraction": { "numer": "501", "denom": "25000" }, - "amount_rat": [[1,[501]],[1,[25000]]] - } - ], - } + let mut actual: TradePreimageResponse = json::from_str(&rc.1).unwrap(); + actual.sort_total_fees(); + + let base_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00001", true); + let rel_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00002", false); + let taker_fee = TradeFeeForTest::new("MYCOIN1", "0.02", false); + let fee_to_send_taker_fee = TradeFeeForTest::new("MYCOIN1", "0.00002", false); + + let my_coin_total_fee = TotalTradeFeeForTest::new("MYCOIN", "0.00001", "0"); + let my_coin1_total_fee = TotalTradeFeeForTest::new("MYCOIN1", "0.02004", "0.02004"); + + let expected = TradePreimageResult::TakerPreimage(TakerPreimage { + base_coin_fee, + rel_coin_fee, + taker_fee, + fee_to_send_taker_fee, + total_fees: vec![my_coin_total_fee, my_coin1_total_fee], }); - assert_eq!(actual, expected); + assert_eq!(expected, actual.result); } #[test] diff --git a/mm2src/lp_ordermatch.rs b/mm2src/lp_ordermatch.rs index 698eaeaca3..0733874089 100644 --- a/mm2src/lp_ordermatch.rs +++ b/mm2src/lp_ordermatch.rs @@ -24,8 +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::{address_by_coin_conf_and_pubkey_str, coin_conf, 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}; @@ -64,12 +63,14 @@ use crate::mm2::lp_swap::{calc_max_maker_vol, check_balance_for_maker_swap, chec SwapConfirmationsSettings, TakerSwap}; pub use best_orders::best_orders_rpc; pub use orderbook_depth::orderbook_depth_rpc; +pub use orderbook_rpc::orderbook_rpc; #[path = "lp_ordermatch/best_orders.rs"] mod best_orders; #[path = "lp_ordermatch/new_protocol.rs"] mod new_protocol; #[path = "lp_ordermatch/order_requests_tracker.rs"] mod order_requests_tracker; #[path = "lp_ordermatch/orderbook_depth.rs"] mod orderbook_depth; +#[path = "lp_ordermatch/orderbook_rpc.rs"] mod orderbook_rpc; #[cfg(all(test, not(target_arch = "wasm32")))] #[path = "ordermatch_tests.rs"] mod ordermatch_tests; @@ -3585,7 +3586,7 @@ pub async fn cancel_all_orders(ctx: MmArc, req: Json) -> Result /// # Safety /// /// The function locks [`MmCtx::p2p_ctx`] and [`MmCtx::ordermatch_ctx`] -async fn subscribe_to_orderbook_topic( +pub(self) async fn subscribe_to_orderbook_topic( ctx: &MmArc, base: &str, rel: &str, @@ -3669,110 +3670,6 @@ pub struct RpcOrderbookEntry { rel_min_volume: DetailedRelMinVolume, } -#[derive(Debug, Serialize)] -pub struct OrderbookResponse { - #[serde(rename = "askdepth")] - ask_depth: u32, - asks: Vec, - base: String, - #[serde(rename = "biddepth")] - bid_depth: u32, - bids: Vec, - netid: u16, - #[serde(rename = "numasks")] - num_asks: usize, - #[serde(rename = "numbids")] - num_bids: usize, - rel: String, - timestamp: u64, -} - -#[derive(Deserialize)] -struct OrderbookReq { - base: String, - rel: String, -} - -pub async fn orderbook(ctx: MmArc, req: Json) -> Result>, String> { - let req: OrderbookReq = try_s!(json::from_value(req)); - if req.base == req.rel { - return ERR!("Base and rel must be different coins"); - } - let base_coin_conf = coin_conf(&ctx, &req.base); - if base_coin_conf.is_null() { - return ERR!("Coin {} is not found in config", req.base); - } - let rel_coin_conf = coin_conf(&ctx, &req.rel); - if rel_coin_conf.is_null() { - return ERR!("Coin {} is not found in config", req.rel); - } - let request_orderbook = true; - try_s!(subscribe_to_orderbook_topic(&ctx, &req.base, &req.rel, request_orderbook).await); - let ordermatch_ctx: Arc = try_s!(OrdermatchContext::from_ctx(&ctx)); - let orderbook = ordermatch_ctx.orderbook.lock().await; - let my_pubsecp = hex::encode(&**ctx.secp256k1_key_pair().public()); - - let mut asks = match orderbook.unordered.get(&(req.base.clone(), req.rel.clone())) { - Some(uuids) => { - let mut orderbook_entries = Vec::new(); - for uuid in uuids { - let ask = orderbook.order_set.get(uuid).ok_or(ERRL!( - "Orderbook::unordered contains {:?} uuid that is not in Orderbook::order_set", - uuid - ))?; - - let address = try_s!(address_by_coin_conf_and_pubkey_str( - &req.base, - &base_coin_conf, - &ask.pubkey - )); - let is_mine = my_pubsecp == ask.pubkey; - orderbook_entries.push(ask.as_rpc_entry_ask(address, is_mine)); - } - orderbook_entries - }, - None => Vec::new(), - }; - asks.sort_unstable_by(|ask1, ask2| ask2.price_rat.cmp(&ask1.price_rat)); - - let mut bids = match orderbook.unordered.get(&(req.rel.clone(), req.base.clone())) { - Some(uuids) => { - let mut orderbook_entries = vec![]; - for uuid in uuids { - let bid = orderbook.order_set.get(uuid).ok_or(ERRL!( - "Orderbook::unordered contains {:?} uuid that is not in Orderbook::order_set", - uuid - ))?; - let address = try_s!(address_by_coin_conf_and_pubkey_str( - &req.rel, - &rel_coin_conf, - &bid.pubkey - )); - let is_mine = my_pubsecp == bid.pubkey; - orderbook_entries.push(bid.as_rpc_entry_bid(address, is_mine)); - } - orderbook_entries - }, - None => vec![], - }; - bids.sort_unstable_by(|bid1, bid2| bid2.price_rat.cmp(&bid1.price_rat)); - - let response = OrderbookResponse { - num_asks: asks.len(), - num_bids: bids.len(), - ask_depth: 0, - asks, - base: req.base, - bid_depth: 0, - bids, - netid: ctx.netid(), - rel: req.rel, - timestamp: now_ms() / 1000, - }; - let response = try_s!(json::to_vec(&response)); - Ok(try_s!(Response::builder().body(response))) -} - fn choose_maker_confs_and_notas( maker_confs: Option, taker_req: &TakerRequest, diff --git a/mm2src/lp_ordermatch/orderbook_rpc.rs b/mm2src/lp_ordermatch/orderbook_rpc.rs new file mode 100644 index 0000000000..6ea6938364 --- /dev/null +++ b/mm2src/lp_ordermatch/orderbook_rpc.rs @@ -0,0 +1,164 @@ +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 http::Response; +use num_rational::BigRational; +use num_traits::Zero; +use serde_json::{self as json, Value as Json}; + +#[derive(Deserialize)] +struct OrderbookReq { + base: String, + rel: String, +} + +construct_detailed!(TotalAsksBaseVol, total_asks_base_vol); +construct_detailed!(TotalAsksRelVol, total_asks_rel_vol); +construct_detailed!(TotalBidsBaseVol, total_bids_base_vol); +construct_detailed!(TotalBidsRelVol, total_bids_rel_vol); +construct_detailed!(AggregatedBaseVol, base_max_volume_aggr); +construct_detailed!(AggregatedRelVol, rel_max_volume_aggr); + +#[derive(Debug, Serialize)] +pub struct AggregatedOrderbookEntry { + #[serde(flatten)] + entry: RpcOrderbookEntry, + #[serde(flatten)] + base_max_volume_aggr: AggregatedBaseVol, + #[serde(flatten)] + rel_max_volume_aggr: AggregatedRelVol, +} + +#[derive(Debug, Serialize)] +pub struct OrderbookResponse { + #[serde(rename = "askdepth")] + ask_depth: u32, + asks: Vec, + base: String, + #[serde(rename = "biddepth")] + bid_depth: u32, + bids: Vec, + netid: u16, + #[serde(rename = "numasks")] + num_asks: usize, + #[serde(rename = "numbids")] + num_bids: usize, + rel: String, + timestamp: u64, + #[serde(flatten)] + total_asks_base: TotalAsksBaseVol, + #[serde(flatten)] + total_asks_rel: TotalAsksRelVol, + #[serde(flatten)] + total_bids_base: TotalBidsBaseVol, + #[serde(flatten)] + total_bids_rel: TotalBidsRelVol, +} + +fn build_aggregated_entries(entries: Vec) -> (Vec, MmNumber, MmNumber) { + let mut total_base = BigRational::zero(); + let mut total_rel = BigRational::zero(); + let aggregated = entries + .into_iter() + .map(|entry| { + total_base += entry.base_max_volume.as_ratio(); + total_rel += entry.rel_max_volume.as_ratio(); + AggregatedOrderbookEntry { + entry, + base_max_volume_aggr: MmNumber::from(total_base.clone()).into(), + rel_max_volume_aggr: MmNumber::from(total_rel.clone()).into(), + } + }) + .collect(); + (aggregated, total_base.into(), total_rel.into()) +} + +pub async fn orderbook_rpc(ctx: MmArc, req: Json) -> Result>, String> { + let req: OrderbookReq = try_s!(json::from_value(req)); + if req.base == req.rel { + return ERR!("Base and rel must be different coins"); + } + let base_coin_conf = coin_conf(&ctx, &req.base); + if base_coin_conf.is_null() { + return ERR!("Coin {} is not found in config", req.base); + } + let rel_coin_conf = coin_conf(&ctx, &req.rel); + if rel_coin_conf.is_null() { + return ERR!("Coin {} is not found in config", req.rel); + } + let request_orderbook = true; + try_s!(subscribe_to_orderbook_topic(&ctx, &req.base, &req.rel, request_orderbook).await); + let ordermatch_ctx = try_s!(OrdermatchContext::from_ctx(&ctx)); + let orderbook = ordermatch_ctx.orderbook.lock().await; + let my_pubsecp = hex::encode(&**ctx.secp256k1_key_pair().public()); + + let mut asks = match orderbook.unordered.get(&(req.base.clone(), req.rel.clone())) { + Some(uuids) => { + let mut orderbook_entries = Vec::new(); + for uuid in uuids { + let ask = orderbook.order_set.get(uuid).ok_or(ERRL!( + "Orderbook::unordered contains {:?} uuid that is not in Orderbook::order_set", + uuid + ))?; + + let address = try_s!(address_by_coin_conf_and_pubkey_str( + &req.base, + &base_coin_conf, + &ask.pubkey + )); + let is_mine = my_pubsecp == ask.pubkey; + orderbook_entries.push(ask.as_rpc_entry_ask(address, is_mine)); + } + orderbook_entries + }, + None => Vec::new(), + }; + asks.sort_unstable_by(|ask1, ask2| ask1.price_rat.cmp(&ask2.price_rat)); + let (mut asks, total_asks_base_vol, total_asks_rel_vol) = build_aggregated_entries(asks); + asks.reverse(); + + let mut bids = match orderbook.unordered.get(&(req.rel.clone(), req.base.clone())) { + Some(uuids) => { + let mut orderbook_entries = vec![]; + for uuid in uuids { + let bid = orderbook.order_set.get(uuid).ok_or(ERRL!( + "Orderbook::unordered contains {:?} uuid that is not in Orderbook::order_set", + uuid + ))?; + let address = try_s!(address_by_coin_conf_and_pubkey_str( + &req.rel, + &rel_coin_conf, + &bid.pubkey + )); + let is_mine = my_pubsecp == bid.pubkey; + orderbook_entries.push(bid.as_rpc_entry_bid(address, is_mine)); + } + orderbook_entries + }, + None => vec![], + }; + bids.sort_unstable_by(|bid1, bid2| bid2.price_rat.cmp(&bid1.price_rat)); + let (bids, total_bids_base_vol, total_bids_rel_vol) = build_aggregated_entries(bids); + + let response = OrderbookResponse { + num_asks: asks.len(), + num_bids: bids.len(), + ask_depth: 0, + asks, + base: req.base, + bid_depth: 0, + bids, + netid: ctx.netid(), + rel: req.rel, + timestamp: now_ms() / 1000, + total_asks_base: total_asks_base_vol.into(), + total_asks_rel: total_asks_rel_vol.into(), + total_bids_base: total_bids_base_vol.into(), + total_bids_rel: total_bids_rel_vol.into(), + }; + let response = try_s!(json::to_vec(&response)); + Ok(try_s!(Response::builder().body(response))) +} diff --git a/mm2src/lp_swap.rs b/mm2src/lp_swap.rs index be0bc8df04..1c7f54e5a6 100644 --- a/mm2src/lp_swap.rs +++ b/mm2src/lp_swap.rs @@ -353,11 +353,11 @@ pub fn get_locked_amount(ctx: &MmArc, coin: &str) -> MmNumber { .flatten() .fold(MmNumber::from(0), |mut total_amount, locked| { if locked.coin == coin { - total_amount = total_amount + locked.amount; + total_amount += locked.amount; } if let Some(trade_fee) = locked.trade_fee { - if trade_fee.coin == coin { - total_amount = total_amount + trade_fee.amount; + if trade_fee.coin == coin && !trade_fee.paid_from_trading_vol { + total_amount += trade_fee.amount; } } total_amount @@ -387,11 +387,11 @@ fn get_locked_amount_by_other_swaps(ctx: &MmArc, except_uuid: &Uuid, coin: &str) .flatten() .fold(MmNumber::from(0), |mut total_amount, locked| { if locked.coin == coin { - total_amount = total_amount + locked.amount; + total_amount += locked.amount; } if let Some(trade_fee) = locked.trade_fee { - if trade_fee.coin == coin { - total_amount = total_amount + trade_fee.amount; + if trade_fee.coin == coin && !trade_fee.paid_from_trading_vol { + total_amount += trade_fee.amount; } } total_amount @@ -437,6 +437,9 @@ pub async fn check_other_coin_balance_for_swap( swap_uuid: Option<&Uuid>, trade_fee: TradeFee, ) -> Result<(), CheckBalanceError> { + if trade_fee.paid_from_trading_vol { + return Ok(()); + } let ticker = coin.ticker(); info!("Check other_coin '{}' balance for swap", ticker); let balance = MmNumber::from(try_map!( @@ -525,7 +528,7 @@ pub async fn check_my_coin_balance_for_swap( return Err(CheckBalanceError::Other(err)); } // increase `trade_fee` by the `fee_to_send_dex_fee` - trade_fee.amount = trade_fee.amount + fee_to_send_dex_fee.amount; + trade_fee.amount += fee_to_send_dex_fee.amount; dex_fee }, None => MmNumber::from(0), @@ -925,13 +928,17 @@ impl SavedSwap { pub struct SavedTradeFee { coin: String, amount: BigDecimal, + #[serde(default)] + paid_from_trading_vol: bool, } impl From for TradeFee { fn from(orig: SavedTradeFee) -> Self { + // used to calculate locked amount so paid_from_trading_vol doesn't matter here TradeFee { coin: orig.coin, amount: orig.amount.into(), + paid_from_trading_vol: orig.paid_from_trading_vol, } } } @@ -940,7 +947,8 @@ impl From for SavedTradeFee { fn from(orig: TradeFee) -> Self { SavedTradeFee { coin: orig.coin, - amount: orig.amount.to_decimal(), + amount: orig.amount.into(), + paid_from_trading_vol: orig.paid_from_trading_vol, } } } @@ -1077,14 +1085,14 @@ pub enum TradePreimageResponse { #[serde(skip_serializing_if = "Option::is_none")] #[serde(flatten)] volume: Option, - total_fees: Vec, + total_fees: Vec, }, TakerPreimage { base_coin_fee: TradeFeeResponse, rel_coin_fee: TradeFeeResponse, taker_fee: TradeFeeResponse, fee_to_send_taker_fee: TradeFeeResponse, - total_fees: Vec, + total_fees: Vec, }, } @@ -1100,7 +1108,7 @@ impl From for TradePreimageResponse { let total_fees = total_fees .into_iter() - .filter_map(TradePreimageResponse::filter_zero_fees) + .filter_map(TradePreimageResponse::filter_zero_total_fees) .collect(); let volume = maker.volume.map(DetailedVolume::from); TradePreimageResponse::MakerPreimage { @@ -1130,7 +1138,7 @@ impl From for TradePreimageResponse { let total_fees = total_fees .into_iter() - .filter_map(TradePreimageResponse::filter_zero_fees) + .filter_map(TradePreimageResponse::filter_zero_total_fees) .collect(); TradePreimageResponse::TakerPreimage { base_coin_fee, @@ -1143,24 +1151,23 @@ impl From for TradePreimageResponse { } impl TradePreimageResponse { - fn accumulate_total_fees(total_fees: &mut HashMap, fee: TradeFee) { + fn accumulate_total_fees(total_fees: &mut HashMap, fee: TradeFee) { use std::collections::hash_map::Entry; match total_fees.entry(fee.coin.clone()) { Entry::Occupied(mut entry) => { - let total_fee = entry.get_mut(); - total_fee.amount = &total_fee.amount + &fee.amount; + entry.get_mut().add_trade_fee(fee.amount, fee.paid_from_trading_vol); }, Entry::Vacant(entry) => { - entry.insert(fee); + entry.insert(fee.into()); }, } } - fn filter_zero_fees((_coin, fee): (String, TradeFee)) -> Option { + fn filter_zero_total_fees((_coin, fee): (String, TotalTradeFee)) -> Option { if fee.amount.is_zero() { None } else { - Some(TradeFeeResponse::from(fee)) + Some(TotalTradeFeeResponse::from(fee)) } } } @@ -1170,6 +1177,7 @@ pub struct TradeFeeResponse { coin: String, #[serde(flatten)] amount: DetailedAmount, + paid_from_trading_vol: bool, } impl From for TradeFeeResponse { @@ -1177,12 +1185,64 @@ impl From for TradeFeeResponse { TradeFeeResponse { coin: orig.coin, amount: DetailedAmount::from(orig.amount), + paid_from_trading_vol: orig.paid_from_trading_vol, + } + } +} + +#[derive(Clone)] +pub struct TotalTradeFee { + coin: String, + amount: MmNumber, + required_balance: MmNumber, +} + +impl TotalTradeFee { + fn add_trade_fee(&mut self, amount: MmNumber, paid_from_trading_vol: bool) { + self.amount += &amount; + if !paid_from_trading_vol { + self.required_balance += amount; + } + } +} + +impl From for TotalTradeFee { + fn from(orig: TradeFee) -> TotalTradeFee { + let required_balance = if orig.paid_from_trading_vol { + 0.into() + } else { + orig.amount.clone() + }; + TotalTradeFee { + coin: orig.coin, + amount: orig.amount, + required_balance, + } + } +} + +#[derive(Clone, Serialize)] +pub struct TotalTradeFeeResponse { + coin: String, + #[serde(flatten)] + amount: DetailedAmount, + #[serde(flatten)] + required_balance: DetailedRequiredBalance, +} + +impl From for TotalTradeFeeResponse { + fn from(orig: TotalTradeFee) -> Self { + TotalTradeFeeResponse { + coin: orig.coin, + amount: orig.amount.into(), + required_balance: orig.required_balance.into(), } } } construct_detailed!(DetailedAmount, amount); construct_detailed!(DetailedVolume, volume); +construct_detailed!(DetailedRequiredBalance, required_balance); pub async fn trade_preimage(ctx: MmArc, req: Json) -> Result>, String> { let req: TradePreimageRequest = try_s!(json::from_value(req)); diff --git a/mm2src/lp_swap/maker_swap.rs b/mm2src/lp_swap/maker_swap.rs index cbf0edaf49..823c137c72 100644 --- a/mm2src/lp_swap/maker_swap.rs +++ b/mm2src/lp_swap/maker_swap.rs @@ -1656,8 +1656,8 @@ mod maker_swap_tests { MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| MockResult::Return(Ok(None))); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let actual = maker_swap.recover_funds().unwrap(); let expected = RecoveredSwap { @@ -1690,8 +1690,8 @@ mod maker_swap_tests { }); TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| MockResult::Return(Ok(None))); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let actual = maker_swap.recover_funds().unwrap(); let expected = RecoveredSwap { @@ -1719,8 +1719,8 @@ mod maker_swap_tests { TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| { MockResult::Return(Ok(Some(FoundSwapTxSpend::Refunded(eth_tx_for_test().into())))) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); assert!(maker_swap.recover_funds().is_err()); } @@ -1749,8 +1749,8 @@ mod maker_swap_tests { unsafe { SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED = true } MockResult::Return(Ok(Some(FoundSwapTxSpend::Refunded(eth_tx_for_test().into())))) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let err = maker_swap.recover_funds().expect_err("Expected an error"); log!("Error: "(err)); @@ -1779,8 +1779,8 @@ mod maker_swap_tests { MockResult::Return(Box::new(futures01::future::ok(Some(eth_tx_for_test().into())))) }); TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| MockResult::Return(Ok(None))); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); maker_swap.w().data.maker_payment_lock = (now_ms() / 1000) - 3690; assert!(maker_swap.recover_funds().is_err()); @@ -1806,8 +1806,8 @@ mod maker_swap_tests { unsafe { MY_PAYMENT_SENT_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(None))) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); assert!(maker_swap.recover_funds().is_err()); assert!(unsafe { MY_PAYMENT_SENT_CALLED }); @@ -1825,8 +1825,8 @@ mod maker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); assert!(maker_swap.recover_funds().is_err()); } @@ -1856,8 +1856,8 @@ mod maker_swap_tests { MockResult::Return(Ok(Some(FoundSwapTxSpend::Spent(eth_tx_for_test().into())))) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let err = maker_swap.recover_funds().expect_err("Expected an error"); log!("Error: "(err)); @@ -1878,8 +1878,8 @@ mod maker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); assert!(maker_swap.recover_funds().is_err()); } @@ -1917,8 +1917,8 @@ mod maker_swap_tests { MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let expected = Ok(RecoveredSwap { coin: "ticker".into(), @@ -1944,8 +1944,8 @@ mod maker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (_maker_swap, _) = MakerSwap::load_from_saved(ctx.clone(), maker_coin, taker_coin, maker_saved_swap).unwrap(); @@ -1969,8 +1969,8 @@ mod maker_swap_tests { unsafe { SWAP_CONTRACT_ADDRESS_CALLED += 1 }; MockResult::Return(Some(BytesJson::default())) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx.clone(), maker_coin, taker_coin, maker_saved_swap).unwrap(); @@ -2001,8 +2001,8 @@ mod maker_swap_tests { unsafe { SWAP_CONTRACT_ADDRESS_CALLED += 1 }; MockResult::Return(Some(BytesJson::default())) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx.clone(), maker_coin, taker_coin, maker_saved_swap).unwrap(); diff --git a/mm2src/lp_swap/taker_swap.rs b/mm2src/lp_swap/taker_swap.rs index 771f3c5a6f..67767f31f7 100644 --- a/mm2src/lp_swap/taker_swap.rs +++ b/mm2src/lp_swap/taker_swap.rs @@ -1573,6 +1573,7 @@ pub async fn taker_swap_trade_preimage(ctx: &MmArc, req: TradePreimageRequest) - let taker_fee = TradeFee { coin: my_coin_ticker, amount: dex_amount.clone(), + paid_from_trading_vol: false, }; let fee_to_send_taker_fee = try_s!( @@ -1742,11 +1743,12 @@ pub fn max_taker_vol_from_available( #[cfg(test)] mod taker_swap_tests { use super::*; - use crate::mm2::lp_swap::dex_fee_amount; + use crate::mm2::lp_swap::{dex_fee_amount, get_locked_amount_by_other_swaps}; use coins::eth::{addr_from_str, signed_eth_tx_from_bytes, SignedEthTx}; use coins::utxo::UtxoTx; use coins::{FoundSwapTxSpend, MarketCoinOps, MmCoin, SwapOps, TestCoin}; use common::mm_ctx::MmCtxBuilder; + use common::new_uuid; use common::privkey::key_pair_from_seed; use mocktopus::mocking::*; @@ -1785,8 +1787,8 @@ mod taker_swap_tests { MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _, _| MockResult::Return(Ok(None))); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (taker_swap, _) = TakerSwap::load_from_saved(ctx, maker_coin, taker_coin, taker_saved_swap).unwrap(); let actual = taker_swap.recover_funds().unwrap(); let expected = RecoveredSwap { @@ -1827,8 +1829,8 @@ mod taker_swap_tests { unsafe { TAKER_PAYMENT_REFUND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (taker_swap, _) = TakerSwap::load_from_saved(ctx, maker_coin, taker_coin, taker_saved_swap).unwrap(); let actual = taker_swap.recover_funds().unwrap(); let expected = RecoveredSwap { @@ -1875,8 +1877,8 @@ mod taker_swap_tests { unsafe { MAKER_PAYMENT_SPEND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (taker_swap, _) = TakerSwap::load_from_saved(ctx, maker_coin, taker_coin, taker_saved_swap).unwrap(); let actual = taker_swap.recover_funds().unwrap(); let expected = RecoveredSwap { @@ -1913,8 +1915,8 @@ mod taker_swap_tests { unsafe { REFUND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (taker_swap, _) = TakerSwap::load_from_saved(ctx, maker_coin, taker_coin, taker_saved_swap).unwrap(); let actual = taker_swap.recover_funds().unwrap(); let expected = RecoveredSwap { @@ -1944,8 +1946,8 @@ mod taker_swap_tests { unsafe { SEARCH_TX_SPEND_CALLED = true }; MockResult::Return(Ok(None)) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (taker_swap, _) = TakerSwap::load_from_saved(ctx, maker_coin, taker_coin, taker_saved_swap).unwrap(); taker_swap.w().data.taker_payment_lock = (now_ms() / 1000) - 3690; assert!(taker_swap.recover_funds().is_err()); @@ -1979,8 +1981,8 @@ mod taker_swap_tests { unsafe { MAKER_PAYMENT_SPEND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (taker_swap, _) = TakerSwap::load_from_saved(ctx, maker_coin, taker_coin, taker_saved_swap).unwrap(); let actual = taker_swap.recover_funds().unwrap(); let expected = RecoveredSwap { @@ -2005,8 +2007,8 @@ mod taker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (taker_swap, _) = TakerSwap::load_from_saved(ctx, maker_coin, taker_coin, taker_saved_swap).unwrap(); assert!(taker_swap.recover_funds().is_err()); } @@ -2045,8 +2047,8 @@ mod taker_swap_tests { unsafe { SWAP_CONTRACT_ADDRESS_CALLED += 1 }; MockResult::Return(Some(BytesJson::default())) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (taker_swap, _) = TakerSwap::load_from_saved(ctx.clone(), maker_coin, taker_coin, taker_saved_swap).unwrap(); @@ -2077,8 +2079,8 @@ mod taker_swap_tests { unsafe { SWAP_CONTRACT_ADDRESS_CALLED += 1 }; MockResult::Return(Some(BytesJson::default())) }); - let maker_coin = MmCoinEnum::Test(TestCoin {}); - let taker_coin = MmCoinEnum::Test(TestCoin {}); + let maker_coin = MmCoinEnum::Test(TestCoin::default()); + let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (taker_swap, _) = TakerSwap::load_from_saved(ctx.clone(), maker_coin, taker_coin, taker_saved_swap).unwrap(); @@ -2170,4 +2172,88 @@ mod taker_swap_tests { .expect_err("!max_taker_vol_from_available success but should be error"); } } + + #[test] + fn locked_amount_should_not_use_paid_from_trading_vol_fee() { + use crate::mm2::lp_swap::get_locked_amount; + + let taker_saved_json = r#"{ + "type": "Taker", + "uuid": "af5e0383-97f6-4408-8c03-a8eb8d17e46d", + "my_order_uuid": "af5e0383-97f6-4408-8c03-a8eb8d17e46d", + "events": [ + { + "timestamp": 1617096259172, + "event": { + "type": "Started", + "data": { + "taker_coin": "MORTY", + "maker_coin": "RICK", + "maker": "15d9c51c657ab1be4ae9d3ab6e76a619d3bccfe830d5363fa168424c0d044732", + "my_persistent_pub": "03ad6f89abc2e5beaa8a3ac28e22170659b3209fe2ddf439681b4b8f31508c36fa", + "lock_duration": 7800, + "maker_amount": "0.1", + "taker_amount": "0.11", + "maker_payment_confirmations": 1, + "maker_payment_requires_nota": false, + "taker_payment_confirmations": 1, + "taker_payment_requires_nota": false, + "taker_payment_lock": 1617104058, + "uuid": "af5e0383-97f6-4408-8c03-a8eb8d17e46d", + "started_at": 1617096258, + "maker_payment_wait": 1617099378, + "maker_coin_start_block": 865240, + "taker_coin_start_block": 869167, + "fee_to_send_taker_fee": { + "coin": "MORTY", + "amount": "0.00001", + "paid_from_trading_vol": false + }, + "taker_payment_trade_fee": { + "coin": "MORTY", + "amount": "0.00001", + "paid_from_trading_vol": false + }, + "maker_payment_spend_trade_fee": { + "coin": "RICK", + "amount": "0.00001", + "paid_from_trading_vol": true + } + } + } + } + ], + "maker_amount": "0.1", + "maker_coin": "RICK", + "taker_amount": "0.11", + "taker_coin": "MORTY", + "gui": null, + "mm_version": "21867da64", + "success_events": [], + "error_events": [] + }"#; + let taker_saved_swap: TakerSavedSwap = json::from_str(taker_saved_json).unwrap(); + let key_pair = + key_pair_from_seed("spice describe gravity federal blast come thank unfair canal monkey style afraid") + .unwrap(); + let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); + + let maker_coin = MmCoinEnum::Test(TestCoin::new("RICK")); + let taker_coin = MmCoinEnum::Test(TestCoin::new("MORTY")); + + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(BigDecimal::from(0))); + + let (swap, _) = TakerSwap::load_from_saved(ctx.clone(), maker_coin, taker_coin, taker_saved_swap).unwrap(); + let swaps_ctx = SwapsContext::from_ctx(&ctx).unwrap(); + let arc = Arc::new(swap); + let weak_ref = Arc::downgrade(&arc); + swaps_ctx.running_swaps.lock().unwrap().push(weak_ref); + + let actual = get_locked_amount(&ctx, "RICK"); + assert_eq!(actual, MmNumber::from(0)); + + let actual = get_locked_amount_by_other_swaps(&ctx, &new_uuid(), "RICK"); + assert_eq!(actual, MmNumber::from(0)); + } } diff --git a/mm2src/mm2.rs b/mm2src/mm2.rs index 0c9a4e4d03..c1f6fb8f3b 100644 --- a/mm2src/mm2.rs +++ b/mm2src/mm2.rs @@ -53,7 +53,7 @@ pub mod database; #[cfg(any(test, target_arch = "wasm32"))] #[path = "mm2_tests.rs"] -mod mm2_tests; +pub mod mm2_tests; /// * `ctx_cb` - callback used to share the `MmCtx` ID with the call site. pub fn lp_main(conf: Json, ctx_cb: &dyn Fn(u32)) -> Result<(), String> { diff --git a/mm2src/mm2_tests.rs b/mm2src/mm2_tests.rs index 8c5a2001fb..67dcd58668 100644 --- a/mm2src/mm2_tests.rs +++ b/mm2src/mm2_tests.rs @@ -7,9 +7,8 @@ use common::for_tests::{check_my_swap_status, check_recent_swaps, check_stats_sw find_metrics_in_json, from_env_file, get_passphrase, mm_spat, LocalStart, MarketMakerIt, RaiiDump, MAKER_ERROR_EVENTS, MAKER_SUCCESS_EVENTS, TAKER_ERROR_EVENTS, TAKER_SUCCESS_EVENTS}; use common::mm_metrics::{MetricType, MetricsJson}; -use common::mm_number::Fraction; +use common::mm_number::{Fraction, MmNumber}; use common::privkey::key_pair_from_seed; -use common::BigInt; use common::{block_on, slurp}; use http::StatusCode; #[cfg(not(target_arch = "wasm32"))] @@ -25,7 +24,7 @@ use std::thread; use std::time::Duration; use uuid::Uuid; -#[path = "mm2_tests/structs.rs"] mod structs; +#[path = "mm2_tests/structs.rs"] pub mod structs; use structs::*; // TODO: Consider and/or try moving the integration tests into separate Rust files. @@ -2043,7 +2042,8 @@ fn test_all_orders_per_pair_per_node_must_be_displayed_in_orderbook() { #[test] #[cfg(not(target_arch = "wasm32"))] -fn orderbook_should_display_rational_amounts() { +// https://github.com/KomodoPlatform/atomicDEX-API/issues/859 +fn orderbook_extended_data() { let coins = json!([ {"coin":"RICK","asset":"RICK","protocol":{"type":"UTXO"}}, {"coin":"MORTY","asset":"MORTY","protocol":{"type":"UTXO"}}, @@ -2082,21 +2082,28 @@ fn orderbook_should_display_rational_amounts() { "electrum1.cipig.net:10018", ])); - let price = BigRational::new(9.into(), 10.into()); - let volume = BigRational::new(9.into(), 10.into()); + let bob_orders = &[ + // (base, rel, price, volume) + ("RICK", "MORTY", "0.9", "0.9"), + ("RICK", "MORTY", "0.8", "0.9"), + ("RICK", "MORTY", "0.7", "0.9"), + ("MORTY", "RICK", "0.8", "0.9"), + ("MORTY", "RICK", "1", "0.9"), + ]; - // create order with rational amount and price - let rc = block_on(mm.rpc(json! ({ - "userpass": mm.userpass, - "method": "setprice", - "base": "RICK", - "rel": "MORTY", - "price": price, - "volume": volume, - "cancel_previous": false, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); + for (base, rel, price, volume) in bob_orders { + let rc = block_on(mm.rpc(json!({ + "userpass": mm.userpass, + "method": "setprice", + "base": base, + "rel": rel, + "price": price, + "volume": volume, + "cancel_previous": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + } thread::sleep(Duration::from_secs(1)); log!("Get RICK/MORTY orderbook"); @@ -2110,44 +2117,41 @@ fn orderbook_should_display_rational_amounts() { assert!(rc.0.is_success(), "!orderbook: {}", rc.1); 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"); - assert_eq!(price, orderbook.asks[0].price_rat); - assert_eq!(volume, orderbook.asks[0].max_volume_rat); + log!("orderbook "[rc.1]); + let expected_total_asks_base_vol = MmNumber::from("2.7"); + assert_eq!(expected_total_asks_base_vol.to_decimal(), orderbook.total_asks_base_vol); - let nine = BigInt::from(9); - let ten = BigInt::from(10); - // should also display fraction - assert_eq!(nine, *orderbook.asks[0].price_fraction.numer()); - assert_eq!(ten, *orderbook.asks[0].price_fraction.denom()); + let expected_total_bids_base_vol = MmNumber::from("1.62"); + assert_eq!(expected_total_bids_base_vol.to_decimal(), orderbook.total_bids_base_vol); - assert_eq!(nine, *orderbook.asks[0].max_volume_fraction.numer()); - assert_eq!(ten, *orderbook.asks[0].max_volume_fraction.denom()); + let expected_total_asks_rel_vol = MmNumber::from("2.16"); + assert_eq!(expected_total_asks_rel_vol.to_decimal(), orderbook.total_asks_rel_vol); - log!("Get MORTY/RICK orderbook"); - let rc = block_on(mm.rpc(json! ({ - "userpass": mm.userpass, - "method": "orderbook", - "base": "MORTY", - "rel": "RICK", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + let expected_total_bids_rel_vol = MmNumber::from("1.8"); + assert_eq!(expected_total_bids_rel_vol.to_decimal(), orderbook.total_bids_rel_vol); - 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"); + fn check_price_and_vol_aggr( + order: &OrderbookEntryAggregate, + price: &'static str, + base_aggr: &'static str, + rel_aggr: &'static str, + ) { + let price = MmNumber::from(price); + assert_eq!(price.to_decimal(), order.price); - let price = BigRational::new(10.into(), 9.into()); - assert_eq!(price, orderbook.bids[0].price_rat); - assert_eq!(volume, orderbook.bids[0].max_volume_rat); + let base_aggr = MmNumber::from(base_aggr); + assert_eq!(base_aggr.to_decimal(), order.base_max_volume_aggr); + + let rel_aggr = MmNumber::from(rel_aggr); + assert_eq!(rel_aggr.to_decimal(), order.rel_max_volume_aggr); + } - // should also display fraction - assert_eq!(ten, *orderbook.bids[0].price_fraction.numer()); - assert_eq!(nine, *orderbook.bids[0].price_fraction.denom()); + check_price_and_vol_aggr(&orderbook.asks[0], "0.9", "2.7", "2.16"); + check_price_and_vol_aggr(&orderbook.asks[1], "0.8", "1.8", "1.35"); + check_price_and_vol_aggr(&orderbook.asks[2], "0.7", "0.9", "0.63"); - assert_eq!(nine, *orderbook.bids[0].max_volume_fraction.numer()); - assert_eq!(ten, *orderbook.bids[0].max_volume_fraction.denom()); + check_price_and_vol_aggr(&orderbook.bids[0], "1.25", "0.72", "0.9"); + check_price_and_vol_aggr(&orderbook.bids[1], "1", "1.62", "1.8"); } #[test] @@ -3751,7 +3755,7 @@ fn test_convert_eth_address() { fn test_convert_qrc20_address() { let passphrase = "cV463HpebE2djP9ugJry5wZ9st5cc6AbkHXGryZVPXMH1XJK8cVU"; let coins = json! ([ - {"coin":"QRC20","required_confirmations":0,"pubtype": 120,"p2shtype": 50,"wiftype": 128,"segwit": true,"txfee": 0,"mm2": 1,"mature_confirmations":500, + {"coin":"QRC20","required_confirmations":0,"pubtype": 120,"p2shtype": 50,"wiftype": 128,"segwit": true,"txfee": 0,"mm2": 1,"mature_confirmations":2000, "protocol":{"type":"QRC20","protocol_data":{"platform":"QTUM","contract_address":"0xd362e096e873eb7907e205fadc6175c6fec7bc44"}}}, ]); @@ -4052,7 +4056,7 @@ fn test_validateaddress() { fn qrc20_activate_electrum() { let passphrase = "cV463HpebE2djP9ugJry5wZ9st5cc6AbkHXGryZVPXMH1XJK8cVU"; let coins = json! ([ - {"coin":"QRC20","required_confirmations":0,"pubtype": 120,"p2shtype": 50,"wiftype": 128,"segwit": true,"txfee": 0,"mm2": 1,"mature_confirmations":500, + {"coin":"QRC20","required_confirmations":0,"pubtype": 120,"p2shtype": 50,"wiftype": 128,"segwit": true,"txfee": 0,"mm2": 1,"mature_confirmations":2000, "protocol":{"type":"QRC20","protocol_data":{"platform":"QTUM","contract_address":"0xd362e096e873eb7907e205fadc6175c6fec7bc44"}}}, ]); @@ -4096,7 +4100,7 @@ fn test_qrc20_withdraw() { // corresponding private key: [3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, 172, 110, 180, 13, 123, 179, 10, 49] let passphrase = "cMhHM3PMpMrChygR4bLF7QsTdenhWpFrrmf2UezBG3eeFsz41rtL"; let coins = json!([ - {"coin":"QRC20","required_confirmations":0,"pubtype": 120,"p2shtype": 50,"wiftype": 128,"segwit": true,"txfee": 0,"mm2": 1,"mature_confirmations":500, + {"coin":"QRC20","required_confirmations":0,"pubtype": 120,"p2shtype": 50,"wiftype": 128,"segwit": true,"txfee": 0,"mm2": 1,"mature_confirmations":2000, "protocol":{"type":"QRC20","protocol_data":{"platform":"QTUM","contract_address":"0xd362e096e873eb7907e205fadc6175c6fec7bc44"}}}, ]); @@ -4174,7 +4178,7 @@ fn test_qrc20_withdraw() { fn test_qrc20_withdraw_error() { let passphrase = "album hollow help heart use bird response large lounge fat elbow coral"; let coins = json!([ - {"coin":"QRC20","required_confirmations":0,"pubtype": 120,"p2shtype": 50,"wiftype": 128,"segwit": true,"txfee": 0,"mm2": 1,"mature_confirmations":500, + {"coin":"QRC20","required_confirmations":0,"pubtype": 120,"p2shtype": 50,"wiftype": 128,"segwit": true,"txfee": 0,"mm2": 1,"mature_confirmations":2000, "protocol":{"type":"QRC20","protocol_data":{"platform":"QTUM","contract_address":"0xd362e096e873eb7907e205fadc6175c6fec7bc44"}}}, ]); @@ -4286,7 +4290,7 @@ fn test_qrc20_withdraw_error() { fn test_qrc20_tx_history() { let passphrase = "daring blind measure rebuild grab boost fix favorite nurse stereo april rookie"; let coins = json!([ - {"coin":"QRC20","required_confirmations":0,"pubtype": 120,"p2shtype": 50,"wiftype": 128,"segwit": true,"txfee": 0,"mm2": 1,"mature_confirmations":500, + {"coin":"QRC20","required_confirmations":0,"pubtype": 120,"p2shtype": 50,"wiftype": 128,"segwit": true,"txfee": 0,"mm2": 1,"mature_confirmations":2000, "protocol":{"type":"QRC20","protocol_data":{"platform":"QTUM","contract_address":"0xd362e096e873eb7907e205fadc6175c6fec7bc44"}}}, ]); @@ -5304,7 +5308,7 @@ fn test_setprice_min_volume_dust() { }))) .unwrap(); assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let response: SetPriceResult = json::from_str(&rc.1).unwrap(); + let response: SetPriceResponse = json::from_str(&rc.1).unwrap(); let expected_min = BigDecimal::from(1); assert_eq!(expected_min, response.result.min_base_vol); } diff --git a/mm2src/mm2_tests/structs.rs b/mm2src/mm2_tests/structs.rs index c101eff859..0a32327564 100644 --- a/mm2src/mm2_tests/structs.rs +++ b/mm2src/mm2_tests/structs.rs @@ -2,14 +2,16 @@ /// The helper structs used in testing of RPC responses, these should be separated from actual MM2 code to ensure /// backwards compatibility +/// Use `#[serde(deny_unknown_fields)]` for all structs for tests to fail in case of adding new fields to the response use bigdecimal::BigDecimal; -use common::mm_number::Fraction; +use common::mm_number::{Fraction, MmNumber}; use num_rational::BigRational; use rpc::v1::types::H256 as H256Json; use std::collections::{HashMap, HashSet}; use uuid::Uuid; #[derive(Deserialize)] +#[serde(deny_unknown_fields)] #[serde(tag = "type", content = "data")] pub enum OrderType { FillOrKill, @@ -17,6 +19,7 @@ pub enum OrderType { } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct OrderConfirmationsSettings { pub base_confs: u64, pub base_nota: bool, @@ -25,12 +28,14 @@ pub struct OrderConfirmationsSettings { } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub enum TakerAction { Buy, Sell, } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] #[serde(tag = "type", content = "data")] pub enum MatchBy { Any, @@ -39,6 +44,7 @@ pub enum MatchBy { } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct BuyOrSellRpcRes { pub base: String, pub rel: String, @@ -60,11 +66,13 @@ pub struct BuyOrSellRpcRes { } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct BuyOrSellRpcResult { pub result: BuyOrSellRpcRes, } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct TakerRequest { base: String, rel: String, @@ -82,6 +90,7 @@ pub struct TakerRequest { } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct MakerReserved { base: String, rel: String, @@ -98,6 +107,7 @@ pub struct MakerReserved { } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct TakerConnect { taker_order_uuid: Uuid, maker_order_uuid: Uuid, @@ -107,6 +117,7 @@ pub struct TakerConnect { } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct MakerConnected { taker_order_uuid: Uuid, maker_order_uuid: Uuid, @@ -116,6 +127,7 @@ pub struct MakerConnected { } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct MakerMatch { request: TakerRequest, reserved: MakerReserved, @@ -125,6 +137,7 @@ pub struct MakerMatch { } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct MakerOrderRpcResult { pub max_base_vol: BigDecimal, pub max_base_vol_rat: BigRational, @@ -139,14 +152,36 @@ pub struct MakerOrderRpcResult { pub started_swaps: Vec, pub uuid: Uuid, pub conf_settings: Option, + pub cancellable: bool, + pub available_amount: BigDecimal, } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct SetPriceResult { - pub result: MakerOrderRpcResult, + pub max_base_vol: BigDecimal, + pub max_base_vol_rat: BigRational, + pub min_base_vol: BigDecimal, + pub min_base_vol_rat: BigRational, + pub price: BigDecimal, + pub price_rat: BigRational, + pub created_at: u64, + pub base: String, + pub rel: String, + pub matches: HashMap, + pub started_swaps: Vec, + pub uuid: Uuid, + pub conf_settings: Option, } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SetPriceResponse { + pub result: SetPriceResult, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct TakerMatch { reserved: MakerReserved, connect: TakerConnect, @@ -155,25 +190,30 @@ pub struct TakerMatch { } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct TakerOrderRpcResult { created_at: u64, request: TakerRequest, matches: HashMap, order_type: OrderType, + pub cancellable: bool, } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct MyOrdersRpc { pub maker_orders: HashMap, pub taker_orders: HashMap, } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct MyOrdersRpcResult { pub result: MyOrdersRpc, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct OrderbookEntry { pub coin: String, pub address: String, @@ -206,42 +246,213 @@ pub struct OrderbookEntry { pub is_mine: bool, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OrderbookEntryAggregate { + pub coin: String, + pub address: String, + pub price: BigDecimal, + pub price_rat: BigRational, + pub price_fraction: Fraction, + #[serde(rename = "maxvolume")] + pub max_volume: BigDecimal, + pub max_volume_rat: BigRational, + pub max_volume_fraction: Fraction, + pub base_max_volume: BigDecimal, + pub base_max_volume_rat: BigRational, + pub base_max_volume_fraction: Fraction, + pub base_min_volume: BigDecimal, + pub base_min_volume_rat: BigRational, + pub base_min_volume_fraction: Fraction, + pub rel_max_volume: BigDecimal, + pub rel_max_volume_rat: BigRational, + pub rel_max_volume_fraction: Fraction, + pub rel_min_volume: BigDecimal, + pub rel_min_volume_rat: BigRational, + pub rel_min_volume_fraction: Fraction, + pub min_volume: BigDecimal, + pub min_volume_rat: BigRational, + pub min_volume_fraction: Fraction, + pub pubkey: String, + pub age: i64, + pub zcredits: u64, + pub uuid: Uuid, + pub is_mine: bool, + pub base_max_volume_aggr: BigDecimal, + pub base_max_volume_aggr_rat: BigRational, + pub base_max_volume_aggr_fraction: Fraction, + pub rel_max_volume_aggr: BigDecimal, + pub rel_max_volume_aggr_rat: BigRational, + pub rel_max_volume_aggr_fraction: Fraction, +} + #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct BestOrdersResponse { pub result: HashMap>, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct OrderbookResponse { + pub base: String, + pub rel: String, #[serde(rename = "askdepth")] pub ask_depth: usize, - pub asks: Vec, - pub bids: Vec, + #[serde(rename = "biddepth")] + pub bid_depth: usize, + #[serde(rename = "numasks")] + num_asks: usize, + #[serde(rename = "numbids")] + num_bids: usize, + pub netid: u16, + timestamp: u64, + pub total_asks_base_vol: BigDecimal, + pub total_asks_base_vol_rat: BigRational, + pub total_asks_base_vol_fraction: Fraction, + pub total_asks_rel_vol: BigDecimal, + pub total_asks_rel_vol_rat: BigRational, + pub total_asks_rel_vol_fraction: Fraction, + pub total_bids_base_vol: BigDecimal, + pub total_bids_base_vol_rat: BigRational, + pub total_bids_base_vol_fraction: Fraction, + pub total_bids_rel_vol: BigDecimal, + pub total_bids_rel_vol_rat: BigRational, + pub total_bids_rel_vol_fraction: Fraction, + pub asks: Vec, + pub bids: Vec, } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct PairDepth { pub asks: usize, pub bids: usize, } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct PairWithDepth { pub pair: (String, String), pub depth: PairDepth, } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct OrderbookDepthResponse { pub result: Vec, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct EnableElectrumResponse { pub coin: String, pub address: String, pub balance: BigDecimal, + pub unspendable_balance: BigDecimal, pub required_confirmations: u64, + pub mature_confirmations: Option, pub requires_notarization: bool, pub result: String, } + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct TradeFeeForTest { + pub coin: String, + pub amount: BigDecimal, + pub amount_rat: BigRational, + pub amount_fraction: Fraction, + pub paid_from_trading_vol: bool, +} + +impl TradeFeeForTest { + pub fn new(coin: &str, amount: &'static str, paid_from_trading_vol: bool) -> TradeFeeForTest { + let amount_mm = MmNumber::from(amount); + TradeFeeForTest { + coin: coin.into(), + amount: amount_mm.to_decimal(), + amount_rat: amount_mm.to_ratio(), + amount_fraction: amount_mm.to_fraction(), + paid_from_trading_vol, + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct TotalTradeFeeForTest { + pub coin: String, + pub amount: BigDecimal, + pub amount_rat: BigRational, + pub amount_fraction: Fraction, + pub required_balance: BigDecimal, + pub required_balance_rat: BigRational, + pub required_balance_fraction: Fraction, +} + +impl TotalTradeFeeForTest { + pub fn new(coin: &str, amount: &'static str, required_balance: &'static str) -> TotalTradeFeeForTest { + let amount_mm = MmNumber::from(amount); + let required_mm = MmNumber::from(required_balance); + TotalTradeFeeForTest { + coin: coin.into(), + amount: amount_mm.to_decimal(), + amount_rat: amount_mm.to_ratio(), + amount_fraction: amount_mm.to_fraction(), + required_balance: required_mm.to_decimal(), + required_balance_rat: required_mm.to_ratio(), + required_balance_fraction: required_mm.to_fraction(), + } + } +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct TakerPreimage { + pub base_coin_fee: TradeFeeForTest, + pub rel_coin_fee: TradeFeeForTest, + pub taker_fee: TradeFeeForTest, + pub fee_to_send_taker_fee: TradeFeeForTest, + // the order of fees is not deterministic + pub total_fees: Vec, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct MakerPreimage { + pub base_coin_fee: TradeFeeForTest, + pub rel_coin_fee: TradeFeeForTest, + pub volume: Option, + pub volume_rat: Option, + pub volume_fraction: Option, + // the order of fees is not deterministic + pub total_fees: Vec, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +#[serde(untagged)] +pub enum TradePreimageResult { + TakerPreimage(TakerPreimage), + MakerPreimage(MakerPreimage), +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TradePreimageResponse { + pub result: TradePreimageResult, +} + +impl TradePreimageResponse { + pub fn sort_total_fees(&mut self) { + match &mut self.result { + TradePreimageResult::MakerPreimage(preimage) => { + preimage.total_fees.sort_by(|fee1, fee2| fee1.coin.cmp(&fee2.coin)) + }, + TradePreimageResult::TakerPreimage(preimage) => { + preimage.total_fees.sort_by(|fee1, fee2| fee1.coin.cmp(&fee2.coin)) + }, + } + } +} diff --git a/mm2src/ordermatch_tests.rs b/mm2src/ordermatch_tests.rs index 08ad5cea4a..f0c1c016d2 100644 --- a/mm2src/ordermatch_tests.rs +++ b/mm2src/ordermatch_tests.rs @@ -200,7 +200,7 @@ fn test_match_maker_order_and_taker_request() { // https://github.com/KomodoPlatform/atomicDEX-API/pull/739#discussion_r517275495 #[test] fn maker_order_match_with_request_zero_volumes() { - let coin = MmCoinEnum::Test(TestCoin {}); + let coin = MmCoinEnum::Test(TestCoin::default()); let maker_order = MakerOrderBuilder::new(&coin, &coin) .with_max_base_vol(1.into()) @@ -949,7 +949,7 @@ fn should_process_request_only_once() { #[test] fn test_choose_maker_confs_settings() { - let coin = TestCoin {}.into(); + let coin = TestCoin::default().into(); // no confs set let taker_order = TakerOrderBuilder::new(&coin, &coin).build_unchecked(); TestCoin::requires_notarization.mock_safe(|_| MockResult::Return(true)); @@ -1071,7 +1071,7 @@ fn test_choose_maker_confs_settings() { #[test] fn test_choose_taker_confs_settings_buy_action() { - let coin = TestCoin {}.into(); + let coin = TestCoin::default().into(); // no confs and notas set let taker_order = TakerOrderBuilder::new(&coin, &coin).build_unchecked(); @@ -1183,7 +1183,7 @@ fn test_choose_taker_confs_settings_buy_action() { #[test] fn test_choose_taker_confs_settings_sell_action() { - let coin = TestCoin {}.into(); + let coin = TestCoin::default().into(); // no confs and notas set let taker_order = TakerOrderBuilder::new(&coin, &coin) @@ -1858,7 +1858,7 @@ fn test_subscribe_to_ordermatch_topic_subscribed_filled() { */ #[test] fn test_taker_request_can_match_with_maker_pubkey() { - let coin = TestCoin {}.into(); + let coin = TestCoin::default().into(); let maker_pubkey = H256Json::default(); @@ -1882,7 +1882,7 @@ fn test_taker_request_can_match_with_maker_pubkey() { #[test] fn test_taker_request_can_match_with_uuid() { let uuid = Uuid::new_v4(); - let coin = MmCoinEnum::Test(TestCoin {}); + let coin = MmCoinEnum::Test(TestCoin::default()); // default has MatchBy::Any let mut order = TakerOrderBuilder::new(&coin, &coin).build_unchecked(); diff --git a/mm2src/rpc.rs b/mm2src/rpc.rs index 54e47679c0..edcbd838f8 100644 --- a/mm2src/rpc.rs +++ b/mm2src/rpc.rs @@ -36,7 +36,7 @@ use std::future::Future as Future03; use std::net::SocketAddr; use crate::mm2::lp_ordermatch::{best_orders_rpc, buy, cancel_all_orders, cancel_order, my_orders, order_status, - orderbook, orderbook_depth_rpc, sell, set_price}; + orderbook_depth_rpc, orderbook_rpc, sell, set_price}; use crate::mm2::lp_swap::{active_swaps_rpc, all_swaps_uuids_by_filter, coins_needed_for_kick_start, import_swaps, list_banned_pubkeys, max_taker_vol, my_recent_swaps, my_swap_status, recover_funds_of_swap, stats_swap_status, trade_preimage, unban_pubkeys}; @@ -159,7 +159,7 @@ pub fn dispatcher(req: Json, ctx: MmArc) -> DispatcherRes { "my_swap_status" => my_swap_status(ctx, req), "my_tx_history" => my_tx_history(ctx, req), "order_status" => hyres(order_status(ctx, req)), - "orderbook" => hyres(orderbook(ctx, req)), + "orderbook" => hyres(orderbook_rpc(ctx, req)), "orderbook_depth" => hyres(orderbook_depth_rpc(ctx, req)), "sim_panic" => hyres(sim_panic(req)), "recover_funds_of_swap" => {