diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index fed65b3f77..0b41c099ac 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -17,7 +17,7 @@ pub struct TestCoin {} #[mockable] #[allow(clippy::forget_ref, clippy::forget_copy, clippy::cast_ref_to_mut)] impl MarketCoinOps for TestCoin { - fn ticker(&self) -> &str { unimplemented!() } + fn ticker(&self) -> &str { "test" } fn my_address(&self) -> Result { unimplemented!() } diff --git a/mm2src/docker_tests.rs b/mm2src/docker_tests.rs index 0c96ceb22e..7b47739f16 100644 --- a/mm2src/docker_tests.rs +++ b/mm2src/docker_tests.rs @@ -2496,12 +2496,13 @@ mod docker_tests { "rel": "MYCOIN1", "price": 1, "volume": 1, + "timeout": 2, }))) .unwrap(); assert!(rc.0.is_success(), "!sell: {}", rc.1); - log!("Give Bob 35 seconds to convert order to maker"); - thread::sleep(Duration::from_secs(35)); + log!("Give Bob 4 seconds to convert order to maker"); + thread::sleep(Duration::from_secs(4)); let rc = block_on(mm_alice.rpc(json! ({ "userpass": mm_alice.userpass, diff --git a/mm2src/lp_ordermatch.rs b/mm2src/lp_ordermatch.rs index 10eda9310f..7822782e0c 100644 --- a/mm2src/lp_ordermatch.rs +++ b/mm2src/lp_ordermatch.rs @@ -80,8 +80,6 @@ const MAKER_ORDER_TIMEOUT: u64 = MIN_ORDER_KEEP_ALIVE_INTERVAL * 3; const TAKER_ORDER_TIMEOUT: u64 = 30; const ORDER_MATCH_TIMEOUT: u64 = 30; const ORDERBOOK_REQUESTING_TIMEOUT: u64 = MIN_ORDER_KEEP_ALIVE_INTERVAL * 2; -#[allow(dead_code)] -const INACTIVE_ORDER_TIMEOUT: u64 = 240; const MIN_TRADING_VOL: &str = "0.00777"; const MAX_ORDERS_NUMBER_IN_ORDERBOOK_RESPONSE: usize = 1000; @@ -911,35 +909,21 @@ impl TakerRequest { fn get_rel_amount(&self) -> &MmNumber { &self.rel_amount } } -struct TakerRequestBuilder { - base: String, - rel: String, +struct TakerOrderBuilder<'a> { + base_coin: &'a MmCoinEnum, + rel_coin: &'a MmCoinEnum, base_amount: MmNumber, rel_amount: MmNumber, sender_pubkey: H256Json, action: TakerAction, match_by: MatchBy, + order_type: OrderType, conf_settings: Option, + min_volume: Option, + timeout: u64, } -impl Default for TakerRequestBuilder { - fn default() -> Self { - TakerRequestBuilder { - base: "".into(), - rel: "".into(), - base_amount: 0.into(), - rel_amount: 0.into(), - sender_pubkey: H256Json::default(), - action: TakerAction::Buy, - match_by: MatchBy::Any, - conf_settings: None, - } - } -} - -enum TakerRequestBuildError { - BaseCoinEmpty, - RelCoinEmpty, +enum TakerOrderBuildError { BaseEqualRel, /// Base amount too low with threshold BaseAmountTooLow { @@ -951,43 +935,58 @@ enum TakerRequestBuildError { actual: MmNumber, threshold: MmNumber, }, + /// Min volume too low with threshold + MinVolumeTooLow { + actual: MmNumber, + threshold: MmNumber, + }, SenderPubkeyIsZero, ConfsSettingsNotSet, } -impl fmt::Display for TakerRequestBuildError { +impl fmt::Display for TakerOrderBuildError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - TakerRequestBuildError::BaseCoinEmpty => write!(f, "Base coin can not be empty"), - TakerRequestBuildError::RelCoinEmpty => write!(f, "Rel coin can not be empty"), - TakerRequestBuildError::BaseEqualRel => write!(f, "Rel coin can not be same as base"), - TakerRequestBuildError::BaseAmountTooLow { actual, threshold } => write!( + TakerOrderBuildError::BaseEqualRel => write!(f, "Rel coin can not be same as base"), + TakerOrderBuildError::BaseAmountTooLow { actual, threshold } => write!( f, "Base amount {} is too low, required: {}", actual.to_decimal(), threshold.to_decimal() ), - TakerRequestBuildError::RelAmountTooLow { actual, threshold } => write!( + TakerOrderBuildError::RelAmountTooLow { actual, threshold } => write!( f, "Rel amount {} is too low, required: {}", actual.to_decimal(), threshold.to_decimal() ), - TakerRequestBuildError::SenderPubkeyIsZero => write!(f, "Sender pubkey can not be zero"), - TakerRequestBuildError::ConfsSettingsNotSet => write!(f, "Confirmation settings must be set"), + TakerOrderBuildError::MinVolumeTooLow { actual, threshold } => write!( + f, + "Min volume {} is too low, required: {}", + actual.to_decimal(), + threshold.to_decimal() + ), + TakerOrderBuildError::SenderPubkeyIsZero => write!(f, "Sender pubkey can not be zero"), + TakerOrderBuildError::ConfsSettingsNotSet => write!(f, "Confirmation settings must be set"), } } } -impl TakerRequestBuilder { - fn with_base_coin(mut self, ticker: String) -> Self { - self.base = ticker; - self - } - - fn with_rel_coin(mut self, ticker: String) -> Self { - self.rel = ticker; - self +impl<'a> TakerOrderBuilder<'a> { + fn new(base_coin: &'a MmCoinEnum, rel_coin: &'a MmCoinEnum) -> TakerOrderBuilder<'a> { + TakerOrderBuilder { + base_coin, + rel_coin, + base_amount: MmNumber::from(0), + rel_amount: MmNumber::from(0), + sender_pubkey: H256Json::default(), + action: TakerAction::Buy, + match_by: MatchBy::Any, + conf_settings: None, + min_volume: None, + order_type: OrderType::GoodTillCancelled, + timeout: TAKER_ORDER_TIMEOUT, + } } fn with_base_amount(mut self, vol: MmNumber) -> Self { @@ -1000,6 +999,11 @@ impl TakerRequestBuilder { self } + fn with_min_volume(mut self, vol: Option) -> Self { + self.min_volume = vol; + self + } + fn with_action(mut self, action: TakerAction) -> Self { self.action = action; self @@ -1010,6 +1014,11 @@ impl TakerRequestBuilder { self } + fn with_order_type(mut self, order_type: OrderType) -> Self { + self.order_type = order_type; + self + } + fn with_conf_settings(mut self, settings: OrderConfirmationsSettings) -> Self { self.conf_settings = Some(settings); self @@ -1020,72 +1029,96 @@ impl TakerRequestBuilder { self } - /// Validate fields and build - fn build(self) -> Result { - let min_vol = MmNumber::from(MIN_TRADING_VOL); - - if self.base.is_empty() { - return Err(TakerRequestBuildError::BaseCoinEmpty); - } + fn with_timeout(mut self, timeout: u64) -> Self { + self.timeout = timeout; + self + } - if self.rel.is_empty() { - return Err(TakerRequestBuildError::RelCoinEmpty); - } + /// Validate fields and build + fn build(self) -> Result { + let min_vol_threshold = MmNumber::from(MIN_TRADING_VOL); + let min_tx_multiplier = MmNumber::from(10); + let min_base_amount = + (&self.base_coin.min_tx_amount().into() * &min_tx_multiplier).max(min_vol_threshold.clone()); + let min_rel_amount = (&self.rel_coin.min_tx_amount().into() * &min_tx_multiplier).max(min_vol_threshold); - if self.base == self.rel { - return Err(TakerRequestBuildError::BaseEqualRel); + if self.base_coin.ticker() == self.rel_coin.ticker() { + return Err(TakerOrderBuildError::BaseEqualRel); } - if self.base_amount < min_vol { - return Err(TakerRequestBuildError::BaseAmountTooLow { + if self.base_amount < min_base_amount { + return Err(TakerOrderBuildError::BaseAmountTooLow { actual: self.base_amount, - threshold: min_vol, + threshold: min_base_amount, }); } - if self.rel_amount < min_vol { - return Err(TakerRequestBuildError::RelAmountTooLow { + if self.rel_amount < min_rel_amount { + return Err(TakerOrderBuildError::RelAmountTooLow { actual: self.rel_amount, - threshold: min_vol, + threshold: min_rel_amount, }); } if self.sender_pubkey == H256Json::default() { - return Err(TakerRequestBuildError::SenderPubkeyIsZero); + return Err(TakerOrderBuildError::SenderPubkeyIsZero); } if self.conf_settings.is_none() { - return Err(TakerRequestBuildError::ConfsSettingsNotSet); + return Err(TakerOrderBuildError::ConfsSettingsNotSet); } - Ok(TakerRequest { - base: self.base, - rel: self.rel, - base_amount: self.base_amount, - rel_amount: self.rel_amount, - action: self.action, - uuid: new_uuid(), - sender_pubkey: self.sender_pubkey, - dest_pub_key: Default::default(), - match_by: self.match_by, - conf_settings: self.conf_settings, + let min_volume = self.min_volume.unwrap_or_else(|| min_base_amount.clone()); + + if min_volume < min_base_amount { + return Err(TakerOrderBuildError::MinVolumeTooLow { + actual: min_volume, + threshold: min_base_amount, + }); + } + + Ok(TakerOrder { + created_at: now_ms(), + request: TakerRequest { + base: self.base_coin.ticker().to_owned(), + rel: self.rel_coin.ticker().to_owned(), + base_amount: self.base_amount, + rel_amount: self.rel_amount, + action: self.action, + uuid: new_uuid(), + sender_pubkey: self.sender_pubkey, + dest_pub_key: Default::default(), + match_by: self.match_by, + conf_settings: self.conf_settings, + }, + matches: Default::default(), + min_volume, + order_type: self.order_type, + timeout: self.timeout, }) } #[cfg(test)] /// skip validation for tests - fn build_unchecked(self) -> TakerRequest { - TakerRequest { - base: self.base, - rel: self.rel, - base_amount: self.base_amount, - rel_amount: self.rel_amount, - action: self.action, - uuid: new_uuid(), - sender_pubkey: self.sender_pubkey, - dest_pub_key: Default::default(), - match_by: self.match_by, - conf_settings: self.conf_settings, + fn build_unchecked(self) -> TakerOrder { + TakerOrder { + created_at: now_ms(), + request: TakerRequest { + base: self.base_coin.ticker().to_owned(), + rel: self.rel_coin.ticker().to_owned(), + base_amount: self.base_amount, + rel_amount: self.rel_amount, + action: self.action, + uuid: new_uuid(), + sender_pubkey: self.sender_pubkey, + dest_pub_key: Default::default(), + match_by: self.match_by, + conf_settings: self.conf_settings, + }, + matches: HashMap::new(), + min_volume: Default::default(), + order_type: Default::default(), + timeout: self.timeout, } } } @@ -1120,6 +1153,7 @@ struct TakerOrder { matches: HashMap, min_volume: MmNumber, order_type: OrderType, + timeout: u64, } /// Result of match_reserved function @@ -1199,31 +1233,16 @@ pub struct MakerOrder { conf_settings: Option, } -struct MakerOrderBuilder { +struct MakerOrderBuilder<'a> { max_base_vol: MmNumber, - min_base_vol: MmNumber, + min_base_vol: Option, price: MmNumber, - base: String, - rel: String, + base_coin: &'a MmCoinEnum, + rel_coin: &'a MmCoinEnum, conf_settings: Option, } -impl Default for MakerOrderBuilder { - fn default() -> MakerOrderBuilder { - MakerOrderBuilder { - base: "".into(), - rel: "".into(), - max_base_vol: 0.into(), - min_base_vol: 0.into(), - price: 0.into(), - conf_settings: None, - } - } -} - enum MakerOrderBuildError { - BaseCoinEmpty, - RelCoinEmpty, BaseEqualRel, /// Max base vol too low with threshold MaxBaseVolTooLow { @@ -1255,8 +1274,6 @@ enum MakerOrderBuildError { impl fmt::Display for MakerOrderBuildError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - MakerOrderBuildError::BaseCoinEmpty => write!(f, "Base coin can not be empty"), - MakerOrderBuildError::RelCoinEmpty => write!(f, "Rel coin can not be empty"), MakerOrderBuildError::BaseEqualRel => write!(f, "Rel coin can not be same as base"), MakerOrderBuildError::MaxBaseVolTooLow { actual, threshold } => write!( f, @@ -1293,15 +1310,16 @@ impl fmt::Display for MakerOrderBuildError { } } -impl MakerOrderBuilder { - fn with_base_coin(mut self, ticker: String) -> Self { - self.base = ticker; - self - } - - fn with_rel_coin(mut self, ticker: String) -> Self { - self.rel = ticker; - self +impl<'a> MakerOrderBuilder<'a> { + fn new(base_coin: &'a MmCoinEnum, rel_coin: &'a MmCoinEnum) -> MakerOrderBuilder<'a> { + MakerOrderBuilder { + base_coin, + rel_coin, + max_base_vol: 0.into(), + min_base_vol: None, + price: 0.into(), + conf_settings: None, + } } fn with_max_base_vol(mut self, vol: MmNumber) -> Self { @@ -1309,7 +1327,7 @@ impl MakerOrderBuilder { self } - fn with_min_base_vol(mut self, vol: MmNumber) -> Self { + fn with_min_base_vol(mut self, vol: Option) -> Self { self.min_base_vol = vol; self } @@ -1327,24 +1345,20 @@ impl MakerOrderBuilder { /// Validate fields and build fn build(self) -> Result { let min_price = MmNumber::from(BigRational::new(1.into(), 100_000_000.into())); - let min_vol = MmNumber::from(MIN_TRADING_VOL); - - if self.base.is_empty() { - return Err(MakerOrderBuildError::BaseCoinEmpty); - } + let min_vol_threshold = MmNumber::from(MIN_TRADING_VOL); + let min_tx_multiplier = MmNumber::from(10); + let min_base_amount = + (&self.base_coin.min_tx_amount().into() * &min_tx_multiplier).max(min_vol_threshold.clone()); + let min_rel_amount = (&self.rel_coin.min_tx_amount().into() * &min_tx_multiplier).max(min_vol_threshold); - if self.rel.is_empty() { - return Err(MakerOrderBuildError::RelCoinEmpty); - } - - if self.base == self.rel { + if self.base_coin.ticker() == self.rel_coin.ticker() { return Err(MakerOrderBuildError::BaseEqualRel); } - if self.max_base_vol < min_vol { + if self.max_base_vol < min_base_amount { return Err(MakerOrderBuildError::MaxBaseVolTooLow { actual: self.max_base_vol, - threshold: min_vol, + threshold: min_base_amount, }); } @@ -1356,23 +1370,24 @@ impl MakerOrderBuilder { } let rel_vol = &self.max_base_vol * &self.price; - if rel_vol < min_vol { + if rel_vol < min_rel_amount { return Err(MakerOrderBuildError::RelVolTooLow { actual: rel_vol, - threshold: min_vol, + threshold: min_rel_amount, }); } - if self.min_base_vol < min_vol { + let min_base_vol = self.min_base_vol.unwrap_or_else(|| min_base_amount.clone()); + if min_base_vol < min_base_amount { return Err(MakerOrderBuildError::MinBaseVolTooLow { - actual: self.min_base_vol, - threshold: min_vol, + actual: min_base_vol, + threshold: min_base_amount, }); } - if self.max_base_vol < self.min_base_vol { + if self.max_base_vol < min_base_vol { return Err(MakerOrderBuildError::MaxBaseVolBelowMinBaseVol { - min: self.min_base_vol, + min: min_base_vol, max: self.max_base_vol, }); } @@ -1382,11 +1397,11 @@ impl MakerOrderBuilder { } Ok(MakerOrder { - base: self.base, - rel: self.rel, + base: self.base_coin.ticker().to_owned(), + rel: self.rel_coin.ticker().to_owned(), created_at: now_ms(), max_base_vol: self.max_base_vol, - min_base_vol: self.min_base_vol, + min_base_vol, price: self.price, matches: HashMap::new(), started_swaps: Vec::new(), @@ -1398,11 +1413,11 @@ impl MakerOrderBuilder { #[cfg(test)] fn build_unchecked(self) -> MakerOrder { MakerOrder { - base: self.base, - rel: self.rel, + base: self.base_coin.ticker().to_owned(), + rel: self.rel_coin.ticker().to_owned(), created_at: now_ms(), max_base_vol: self.max_base_vol, - min_base_vol: self.min_base_vol, + min_base_vol: self.min_base_vol.unwrap_or(MIN_TRADING_VOL.into()), price: self.price, matches: HashMap::new(), started_swaps: Vec::new(), @@ -2178,6 +2193,7 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO taker_amount, my_persistent_pub, uuid, + Some(maker_order.uuid), my_conf_settings, maker_coin, taker_coin, @@ -2264,6 +2280,7 @@ fn lp_connected_alice(ctx: MmArc, taker_request: TakerRequest, taker_match: Take taker_amount, my_persistent_pub, uuid, + Some(uuid), my_conf_settings, maker_coin, taker_coin, @@ -2275,6 +2292,7 @@ fn lp_connected_alice(ctx: MmArc, taker_request: TakerRequest, taker_match: Take pub async fn lp_ordermatch_loop(ctx: MmArc) { let my_pubsecp = hex::encode(&**ctx.secp256k1_key_pair().public()); + let maker_order_timeout = ctx.conf["maker_order_timeout"].as_u64().unwrap_or(MAKER_ORDER_TIMEOUT); loop { if ctx.is_stopping() { break; @@ -2288,7 +2306,7 @@ pub async fn lp_ordermatch_loop(ctx: MmArc) { *my_taker_orders = my_taker_orders .drain() .filter_map(|(uuid, order)| { - if order.created_at + TAKER_ORDER_TIMEOUT * 1000 < now_ms() { + if order.created_at + order.timeout * 1000 < now_ms() { delete_my_taker_order(&ctx, &uuid); if order.matches.is_empty() && order.order_type == OrderType::GoodTillCancelled { let maker_order: MakerOrder = order.into(); @@ -2340,7 +2358,7 @@ pub async fn lp_ordermatch_loop(ctx: MmArc) { let mut uuids_to_remove = vec![]; let mut keys_to_remove = vec![]; orderbook.pubkeys_state.retain(|pubkey, state| { - let to_retain = pubkey == &my_pubsecp || state.last_keep_alive + MAKER_ORDER_TIMEOUT > now_ms() / 1000; + let to_retain = pubkey == &my_pubsecp || state.last_keep_alive + maker_order_timeout > now_ms() / 1000; if !to_retain { for (uuid, _) in &state.orders_uuids { uuids_to_remove.push(*uuid); @@ -2590,15 +2608,13 @@ async fn process_taker_connect(ctx: MmArc, sender_pubkey: H256Json, connect_msg: } } -fn min_trading_vol() -> MmNumber { MmNumber::from(MIN_TRADING_VOL) } - #[derive(Deserialize, Debug)] pub struct AutoBuyInput { base: String, rel: String, price: MmNumber, volume: MmNumber, - timeout: Option, + timeout: Option, /// Not used. Deprecated. duration: Option, // TODO: remove this field on API refactoring, method should be separated from params @@ -2615,8 +2631,7 @@ pub struct AutoBuyInput { base_nota: Option, rel_confs: Option, rel_nota: Option, - #[serde(default = "min_trading_vol")] - min_volume: MmNumber, + min_volume: Option, } pub async fn buy(ctx: MmArc, req: Json) -> Result>, String> { @@ -2776,34 +2791,30 @@ pub async fn lp_auto_buy( rel_confs: input.rel_confs.unwrap_or_else(|| rel_coin.required_confirmations()), rel_nota: input.rel_nota.unwrap_or_else(|| rel_coin.requires_notarization()), }; - let request_builder = TakerRequestBuilder::default() - .with_base_coin(input.base.clone()) - .with_rel_coin(input.rel.clone()) + let mut order_builder = TakerOrderBuilder::new(base_coin, rel_coin) .with_base_amount(input.volume) .with_rel_amount(rel_volume) .with_action(action) .with_match_by(input.match_by) + .with_min_volume(input.min_volume) + .with_order_type(input.order_type) .with_conf_settings(conf_settings) .with_sender_pubkey(H256Json::from(our_public_id.bytes)); - let request = try_s!(request_builder.build()); + if let Some(timeout) = input.timeout { + order_builder = order_builder.with_timeout(timeout); + } + let order = try_s!(order_builder.build()); broadcast_ordermatch_message( &ctx, vec![orderbook_topic_from_base_rel(&input.base, &input.rel)], - request.clone().into(), + order.request.clone().into(), ); let result = json!({ "result": LpautobuyResult { - request: (&request).into(), - order_type: input.order_type, - min_volume: input.min_volume.clone().into(), + request: (&order.request).into(), + order_type: order.order_type, + min_volume: order.min_volume.clone().into(), } }); - let order = TakerOrder { - created_at: now_ms(), - matches: HashMap::new(), - request, - order_type: input.order_type, - min_volume: input.min_volume, - }; save_my_taker_order(ctx, &order); my_taker_orders.insert(order.request.uuid, order); Ok(result.to_string()) @@ -2865,12 +2876,78 @@ impl OrderbookItem { self.min_volume = new_min_volume.into(); } } + + fn as_rpc_entry_ask(&self, address: String, is_mine: bool) -> RpcOrderbookEntry { + let price_mm = MmNumber::from(self.price.clone()); + let max_vol_mm = MmNumber::from(self.max_volume.clone()); + let min_vol_mm = MmNumber::from(self.min_volume.clone()); + + let base_max_volume = max_vol_mm.clone().into(); + let base_min_volume = min_vol_mm.clone().into(); + let rel_max_volume = (&max_vol_mm * &price_mm).into(); + let rel_min_volume = (&min_vol_mm * &price_mm).into(); + + RpcOrderbookEntry { + coin: self.base.clone(), + address, + price: price_mm.to_decimal(), + price_rat: price_mm.to_ratio(), + price_fraction: price_mm.to_fraction(), + max_volume: max_vol_mm.to_decimal(), + max_volume_rat: max_vol_mm.to_ratio(), + max_volume_fraction: max_vol_mm.to_fraction(), + min_volume: min_vol_mm.to_decimal(), + min_volume_rat: min_vol_mm.to_ratio(), + min_volume_fraction: min_vol_mm.to_fraction(), + pubkey: self.pubkey.clone(), + age: (now_ms() as i64 / 1000), + zcredits: 0, + uuid: self.uuid, + is_mine, + base_max_volume, + base_min_volume, + rel_max_volume, + rel_min_volume, + } + } + + fn as_rpc_entry_bid(&self, address: String, is_mine: bool) -> RpcOrderbookEntry { + let price_mm = MmNumber::from(1i32) / self.price.clone().into(); + let max_vol_mm = MmNumber::from(self.max_volume.clone()); + let min_vol_mm = MmNumber::from(self.min_volume.clone()); + + let base_max_volume = (&max_vol_mm / &price_mm).into(); + let base_min_volume = (&min_vol_mm / &price_mm).into(); + let rel_max_volume = max_vol_mm.clone().into(); + let rel_min_volume = min_vol_mm.clone().into(); + + RpcOrderbookEntry { + coin: self.rel.clone(), + address, + price: price_mm.to_decimal(), + price_rat: price_mm.to_ratio(), + price_fraction: price_mm.to_fraction(), + max_volume: max_vol_mm.to_decimal(), + max_volume_rat: max_vol_mm.to_ratio(), + max_volume_fraction: max_vol_mm.to_fraction(), + min_volume: min_vol_mm.to_decimal(), + min_volume_rat: min_vol_mm.to_ratio(), + min_volume_fraction: min_vol_mm.to_fraction(), + pubkey: self.pubkey.clone(), + age: (now_ms() as i64 / 1000), + zcredits: 0, + uuid: self.uuid, + is_mine, + base_max_volume, + base_min_volume, + rel_max_volume, + rel_min_volume, + } + } } fn get_true() -> bool { true } -fn min_volume() -> MmNumber { MmNumber::from(MIN_TRADING_VOL) } - #[derive(Deserialize)] struct SetPriceReq { base: String, @@ -2880,8 +2957,7 @@ struct SetPriceReq { max: bool, #[serde(default)] volume: MmNumber, - #[serde(default = "min_volume")] - min_volume: MmNumber, + min_volume: Option, #[serde(default = "get_true")] cancel_previous: bool, base_confs: Option, @@ -3110,9 +3186,7 @@ pub async fn set_price(ctx: MmArc, req: Json) -> Result>, Strin rel_confs: req.rel_confs.unwrap_or_else(|| rel_coin.required_confirmations()), rel_nota: req.rel_nota.unwrap_or_else(|| rel_coin.requires_notarization()), }; - let builder = MakerOrderBuilder::default() - .with_base_coin(req.base) - .with_rel_coin(req.rel) + let builder = MakerOrderBuilder::new(&base_coin, &rel_coin) .with_max_base_vol(volume) .with_min_base_vol(req.min_volume) .with_price(req.price) @@ -3561,8 +3635,13 @@ async fn subscribe_to_orderbook_topic( Ok(()) } +construct_detailed!(DetailedBaseMaxVolume, base_max_volume); +construct_detailed!(DetailedBaseMinVolume, base_min_volume); +construct_detailed!(DetailedRelMaxVolume, rel_max_volume); +construct_detailed!(DetailedRelMinVolume, rel_min_volume); + #[derive(Debug, Serialize)] -pub struct OrderbookEntry { +pub struct RpcOrderbookEntry { coin: String, address: String, price: BigDecimal, @@ -3580,17 +3659,25 @@ pub struct OrderbookEntry { zcredits: u64, uuid: Uuid, is_mine: bool, + #[serde(flatten)] + base_max_volume: DetailedBaseMaxVolume, + #[serde(flatten)] + base_min_volume: DetailedBaseMinVolume, + #[serde(flatten)] + rel_max_volume: DetailedRelMaxVolume, + #[serde(flatten)] + rel_min_volume: DetailedRelMinVolume, } #[derive(Debug, Serialize)] pub struct OrderbookResponse { #[serde(rename = "askdepth")] ask_depth: u32, - asks: Vec, + asks: Vec, base: String, #[serde(rename = "biddepth")] bid_depth: u32, - bids: Vec, + bids: Vec, netid: u16, #[serde(rename = "numasks")] num_asks: usize, @@ -3633,32 +3720,14 @@ pub async fn orderbook(ctx: MmArc, req: Json) -> Result>, Strin "Orderbook::unordered contains {:?} uuid that is not in Orderbook::order_set", uuid ))?; - let price_mm: MmNumber = ask.price.clone().into(); - let max_vol_mm: MmNumber = ask.max_volume.clone().into(); - let min_vol_mm: MmNumber = ask.min_volume.clone().into(); - - orderbook_entries.push(OrderbookEntry { - coin: req.base.clone(), - address: try_s!(address_by_coin_conf_and_pubkey_str( - &req.base, - &base_coin_conf, - &ask.pubkey - )), - price: price_mm.to_decimal(), - price_rat: price_mm.to_ratio(), - price_fraction: price_mm.to_fraction(), - max_volume: max_vol_mm.to_decimal(), - max_volume_rat: max_vol_mm.to_ratio(), - max_volume_fraction: max_vol_mm.to_fraction(), - min_volume: min_vol_mm.to_decimal(), - min_volume_rat: min_vol_mm.to_ratio(), - min_volume_fraction: min_vol_mm.to_fraction(), - pubkey: ask.pubkey.clone(), - age: (now_ms() as i64 / 1000), - zcredits: 0, - uuid: *uuid, - is_mine: my_pubsecp == ask.pubkey, - }) + + 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 }, @@ -3674,33 +3743,13 @@ pub async fn orderbook(ctx: MmArc, req: Json) -> Result>, Strin "Orderbook::unordered contains {:?} uuid that is not in Orderbook::order_set", uuid ))?; - let price_mm = &MmNumber::from(1i32) / &bid.price.clone().into(); - let max_vol_mm: MmNumber = bid.max_volume.clone().into(); - let min_vol_mm: MmNumber = bid.min_volume.clone().into(); - orderbook_entries.push(OrderbookEntry { - coin: req.rel.clone(), - address: try_s!(address_by_coin_conf_and_pubkey_str( - &req.rel, - &rel_coin_conf, - &bid.pubkey - )), - // NB: 1/x can not be represented as a decimal and introduces a rounding error - // cf. https://github.com/KomodoPlatform/atomicDEX-API/issues/495#issuecomment-516365682 - price: price_mm.to_decimal(), - price_rat: price_mm.to_ratio(), - price_fraction: price_mm.to_fraction(), - max_volume: max_vol_mm.to_decimal(), - max_volume_rat: max_vol_mm.to_ratio(), - max_volume_fraction: max_vol_mm.to_fraction(), - min_volume: min_vol_mm.to_decimal(), - min_volume_rat: min_vol_mm.to_ratio(), - min_volume_fraction: min_vol_mm.to_fraction(), - pubkey: bid.pubkey.clone(), - age: (now_ms() as i64 / 1000), - zcredits: 0, - uuid: *uuid, - is_mine: my_pubsecp == bid.pubkey, - }) + 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 }, diff --git a/mm2src/lp_ordermatch/best_orders.rs b/mm2src/lp_ordermatch/best_orders.rs index cbd62eb174..6b69945282 100644 --- a/mm2src/lp_ordermatch/best_orders.rs +++ b/mm2src/lp_ordermatch/best_orders.rs @@ -1,9 +1,9 @@ -use super::{OrderbookEntry, OrderbookItemWithProof, OrdermatchContext, OrdermatchRequest}; +use super::{OrderbookItemWithProof, OrdermatchContext, OrdermatchRequest}; use crate::mm2::lp_network::{request_any_relay, P2PRequest}; use coins::{address_by_coin_conf_and_pubkey_str, coin_conf}; +use common::log; use common::mm_ctx::MmArc; use common::mm_number::MmNumber; -use common::{log, now_ms}; use http::Response; use num_rational::BigRational; use num_traits::Zero; @@ -132,29 +132,9 @@ pub async fn best_orders_rpc(ctx: MmArc, req: Json) -> Result>, continue; }, }; - let price_mm: MmNumber = match req.action { - BestOrdersAction::Buy => order.price.into(), - BestOrdersAction::Sell => order.price.recip().into(), - }; - let max_vol_mm: MmNumber = order.max_volume.into(); - let min_vol_mm: MmNumber = order.min_volume.into(); - let entry = OrderbookEntry { - coin: coin.clone(), - address, - price: price_mm.to_decimal(), - price_rat: price_mm.to_ratio(), - price_fraction: price_mm.to_fraction(), - max_volume: max_vol_mm.to_decimal(), - max_volume_rat: max_vol_mm.to_ratio(), - max_volume_fraction: max_vol_mm.to_fraction(), - min_volume: min_vol_mm.to_decimal(), - min_volume_rat: min_vol_mm.to_ratio(), - min_volume_fraction: min_vol_mm.to_fraction(), - pubkey: order.pubkey, - age: (now_ms() as i64 / 1000), - zcredits: 0, - uuid: order.uuid, - is_mine: false, + let entry = match req.action { + BestOrdersAction::Buy => order.as_rpc_entry_ask(address, false), + BestOrdersAction::Sell => order.as_rpc_entry_bid(address, false), }; response.entry(coin.clone()).or_insert_with(Vec::new).push(entry); } diff --git a/mm2src/lp_swap/maker_swap.rs b/mm2src/lp_swap/maker_swap.rs index db32f90014..d572f5d7c1 100644 --- a/mm2src/lp_swap/maker_swap.rs +++ b/mm2src/lp_swap/maker_swap.rs @@ -36,6 +36,7 @@ fn save_my_maker_swap_event(ctx: &MmArc, swap: &MakerSwap, event: MakerSavedEven let swap: SavedSwap = if content.is_empty() { SavedSwap::Maker(MakerSavedSwap { uuid: swap.uuid, + my_order_uuid: swap.my_order_uuid, maker_amount: Some(swap.maker_amount.clone()), maker_coin: Some(swap.maker_coin.ticker().to_owned()), taker_amount: Some(swap.taker_amount.clone()), @@ -145,6 +146,7 @@ pub struct MakerSwap { my_persistent_pub: H264, taker: bits256, uuid: Uuid, + my_order_uuid: Option, taker_payment_lock: Atomic, taker_payment_confirmed: Atomic, errors: PaMutex>, @@ -236,6 +238,7 @@ impl MakerSwap { taker_amount: BigDecimal, my_persistent_pub: H264, uuid: Uuid, + my_order_uuid: Option, conf_settings: SwapConfirmationsSettings, maker_coin: MmCoinEnum, taker_coin: MmCoinEnum, @@ -250,6 +253,7 @@ impl MakerSwap { my_persistent_pub, taker, uuid, + my_order_uuid, taker_payment_lock: Atomic::new(0), errors: PaMutex::new(Vec::new()), finished_at: Atomic::new(0), @@ -873,6 +877,7 @@ impl MakerSwap { data.taker_amount.clone(), my_persistent_pub, saved.uuid, + saved.my_order_uuid, conf_settings, maker_coin, taker_coin, @@ -1204,6 +1209,7 @@ impl MakerSavedEvent { #[derive(Debug, Serialize, Deserialize)] pub struct MakerSavedSwap { pub uuid: Uuid, + my_order_uuid: Option, events: Vec, maker_amount: Option, maker_coin: Option, diff --git a/mm2src/lp_swap/taker_swap.rs b/mm2src/lp_swap/taker_swap.rs index 0031834ba2..d2bf5e299b 100644 --- a/mm2src/lp_swap/taker_swap.rs +++ b/mm2src/lp_swap/taker_swap.rs @@ -37,6 +37,7 @@ fn save_my_taker_swap_event(ctx: &MmArc, swap: &TakerSwap, event: TakerSavedEven let swap: SavedSwap = if content.is_empty() { SavedSwap::Taker(TakerSavedSwap { uuid: swap.uuid, + my_order_uuid: swap.my_order_uuid, maker_amount: Some(swap.maker_amount.to_decimal()), maker_coin: Some(swap.maker_coin.ticker().to_owned()), taker_amount: Some(swap.taker_amount.to_decimal()), @@ -127,6 +128,7 @@ impl TakerSavedEvent { #[derive(Debug, Serialize, Deserialize)] pub struct TakerSavedSwap { pub uuid: Uuid, + my_order_uuid: Option, pub events: Vec, maker_amount: Option, maker_coin: Option, @@ -437,6 +439,7 @@ pub struct TakerSwap { my_persistent_pub: H264, maker: bits256, uuid: Uuid, + my_order_uuid: Option, maker_payment_lock: Atomic, maker_payment_confirmed: Atomic, errors: PaMutex>, @@ -632,6 +635,7 @@ impl TakerSwap { taker_amount: MmNumber, my_persistent_pub: H264, uuid: Uuid, + my_order_uuid: Option, conf_settings: SwapConfirmationsSettings, maker_coin: MmCoinEnum, taker_coin: MmCoinEnum, @@ -646,6 +650,7 @@ impl TakerSwap { my_persistent_pub, maker, uuid, + my_order_uuid, maker_payment_confirmed: Atomic::new(false), finished_at: Atomic::new(0), maker_payment_lock: Atomic::new(0), @@ -1241,6 +1246,7 @@ impl TakerSwap { data.taker_amount.clone().into(), my_persistent_pub, saved.uuid, + Some(saved.uuid), conf_settings, maker_coin, taker_coin, diff --git a/mm2src/mm2_tests.rs b/mm2src/mm2_tests.rs index 704b52dc0a..525b89cf47 100644 --- a/mm2src/mm2_tests.rs +++ b/mm2src/mm2_tests.rs @@ -20,6 +20,7 @@ use std::collections::HashMap; use std::convert::identity; use std::env::{self, var}; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::thread; use std::time::Duration; use uuid::Uuid; @@ -288,11 +289,10 @@ fn alice_can_see_the_active_order_after_connection() { .unwrap(); assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - let bob_orderbook: Json = json::from_str(&rc.1).unwrap(); + let bob_orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("Bob orderbook "[bob_orderbook]); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert!(asks.len() > 0, "Bob RICK/MORTY asks are empty"); - assert_eq!(Json::from("0.9"), asks[0]["maxvolume"]); + assert!(bob_orderbook.asks.len() > 0, "Bob RICK/MORTY asks are empty"); + assert_eq!(BigDecimal::from_str("0.9").unwrap(), bob_orderbook.asks[0].max_volume); // start eve and immediately place the order let mut mm_eve = MarketMakerIt::start( @@ -351,12 +351,18 @@ fn alice_can_see_the_active_order_after_connection() { .unwrap(); assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - let eve_orderbook: Json = json::from_str(&rc.1).unwrap(); + let eve_orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("Eve orderbook "[eve_orderbook]); - let asks = eve_orderbook["asks"].as_array().unwrap(); - let bids = eve_orderbook["bids"].as_array().unwrap(); - assert_eq!(asks.len(), 2, "Eve RICK/MORTY orderbook must have exactly 2 asks"); - assert_eq!(bids.len(), 1, "Eve RICK/MORTY orderbook must have exactly 1 bid"); + assert_eq!( + eve_orderbook.asks.len(), + 2, + "Eve RICK/MORTY orderbook must have exactly 2 asks" + ); + assert_eq!( + eve_orderbook.bids.len(), + 1, + "Eve RICK/MORTY orderbook must have exactly 1 bid" + ); log!("Give Bob 2 seconds to import Eve order"); thread::sleep(Duration::from_secs(2)); @@ -370,12 +376,18 @@ fn alice_can_see_the_active_order_after_connection() { .unwrap(); assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - let bob_orderbook: Json = json::from_str(&rc.1).unwrap(); + let bob_orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("Bob orderbook "[bob_orderbook]); - let asks = bob_orderbook["asks"].as_array().unwrap(); - let bids = bob_orderbook["bids"].as_array().unwrap(); - assert_eq!(asks.len(), 2, "Bob RICK/MORTY orderbook must have exactly 2 asks"); - assert_eq!(bids.len(), 1, "Bob RICK/MORTY orderbook must have exactly 1 bid"); + assert_eq!( + bob_orderbook.asks.len(), + 2, + "Bob RICK/MORTY orderbook must have exactly 2 asks" + ); + assert_eq!( + bob_orderbook.bids.len(), + 1, + "Bob RICK/MORTY orderbook must have exactly 1 bid" + ); let mut mm_alice = MarketMakerIt::start( json! ({ @@ -411,12 +423,18 @@ fn alice_can_see_the_active_order_after_connection() { .unwrap(); assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - let alice_orderbook: Json = json::from_str(&rc.1).unwrap(); + let alice_orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("Alice orderbook "[alice_orderbook]); - let asks = alice_orderbook["asks"].as_array().unwrap(); - let bids = alice_orderbook["bids"].as_array().unwrap(); - assert_eq!(asks.len(), 2, "Alice RICK/MORTY orderbook must have exactly 2 asks"); - assert_eq!(bids.len(), 1, "Alice RICK/MORTY orderbook must have exactly 1 bid"); + assert_eq!( + alice_orderbook.asks.len(), + 2, + "Alice RICK/MORTY orderbook must have exactly 2 asks" + ); + assert_eq!( + alice_orderbook.bids.len(), + 1, + "Alice RICK/MORTY orderbook must have exactly 1 bid" + ); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); @@ -980,13 +998,11 @@ async fn trade_base_rel_electrum(pairs: Vec<(&'static str, &'static str)>) { .unwrap(); assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - let bob_orderbook: Json = json::from_str(&rc.1).unwrap(); + let bob_orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!((base) "/" (rel) " orderbook " [bob_orderbook]); - let bids = bob_orderbook["bids"].as_array().unwrap(); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert_eq!(0, bids.len(), "{} {} bids must be empty", base, rel); - assert_eq!(0, asks.len(), "{} {} asks must be empty", base, rel); + assert_eq!(0, bob_orderbook.bids.len(), "{} {} bids must be empty", base, rel); + assert_eq!(0, bob_orderbook.asks.len(), "{} {} asks must be empty", base, rel); } mm_bob.stop().await.unwrap(); mm_alice.stop().await.unwrap(); @@ -1365,107 +1381,6 @@ fn test_startup_passphrase() { assert!(key_pair_from_seed("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141").is_err()); } -/// MM2 should allow to issue several buy/sell calls in a row without delays. -/// https://github.com/artemii235/SuperNET/issues/245 -#[test] -#[cfg(not(target_arch = "wasm32"))] -fn test_multiple_buy_sell_no_delay() { - let coins = json!([ - {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - {"coin":"MORTY","asset":"MORTY","rpcport":11608,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH"}}, - {"coin":"JST","name":"jst","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":"0x2b294F029Fde858b2c62184e8390591755521d8E"}}} - ]); - - let (bob_file_passphrase, _bob_file_userpass) = from_env_file(slurp(&".env.seed").unwrap()); - let bob_passphrase = var("BOB_PASSPHRASE") - .ok() - .or(bob_file_passphrase) - .expect("No BOB_PASSPHRASE or .env.seed/PASSPHRASE"); - - let mut mm = MarketMakerIt::start( - json! ({ - "gui": "nogui", - "netid": 9998, - "myipaddr": env::var ("BOB_TRADE_IP") .ok(), - "rpcip": env::var ("BOB_TRADE_IP") .ok(), - "canbind": env::var ("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), - "passphrase": bob_passphrase, - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - }), - "pass".into(), - match var("LOCAL_THREAD_MM") { - Ok(ref e) if e == "bob" => Some(local_start()), - _ => None, - }, - ) - .unwrap(); - let (_dump_log, _dump_dashboard) = mm.mm_dump(); - log!({"Log path: {}", mm.log_path.display()}); - block_on(mm.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - log!([block_on(enable_coins_eth_electrum(&mm, &["http://195.201.0.6:8565"]))]); - - let rc = block_on(mm.rpc(json! ({ - "userpass": mm.userpass, - "method": "buy", - "base": "RICK", - "rel": "MORTY", - "price": 1, - "volume": 0.1, - }))) - .unwrap(); - assert!(rc.0.is_success(), "buy should have succeed, but got {:?}", rc); - - let rc = block_on(mm.rpc(json! ({ - "userpass": mm.userpass, - "method": "buy", - "base": "RICK", - "rel": "ETH", - "price": 1, - "volume": 0.1, - }))) - .unwrap(); - assert!(rc.0.is_success(), "buy should have succeed, but got {:?}", rc); - thread::sleep(Duration::from_secs(40)); - - log!("Get RICK/MORTY orderbook"); - let rc = block_on(mm.rpc(json! ({ - "userpass": mm.userpass, - "method": "orderbook", - "base": "RICK", - "rel": "MORTY", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = json::from_str(&rc.1).unwrap(); - log!("RICK/MORTY orderbook "[bob_orderbook]); - let bids = bob_orderbook["bids"].as_array().unwrap(); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert!(bids.len() > 0, "RICK/MORTY bids are empty"); - assert_eq!(0, asks.len(), "RICK/MORTY asks are not empty"); - assert_eq!(Json::from("0.1"), bids[0]["maxvolume"]); - - log!("Get RICK/ETH orderbook"); - let rc = block_on(mm.rpc(json! ({ - "userpass": mm.userpass, - "method": "orderbook", - "base": "RICK", - "rel": "ETH", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = json::from_str(&rc.1).unwrap(); - log!("RICK/ETH orderbook "[bob_orderbook]); - let bids = bob_orderbook["bids"].as_array().unwrap(); - assert!(bids.len() > 0, "RICK/ETH bids are empty"); - assert_eq!(asks.len(), 0, "RICK/ETH asks are not empty"); - assert_eq!(Json::from("0.1"), bids[0]["maxvolume"]); -} - /// https://github.com/artemii235/SuperNET/issues/398 #[test] #[cfg(not(target_arch = "wasm32"))] @@ -1557,10 +1472,13 @@ fn test_cancel_order() { .unwrap(); assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - let alice_orderbook: Json = json::from_str(&rc.1).unwrap(); + let alice_orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("Alice orderbook "[alice_orderbook]); - let asks = alice_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "Alice RICK/MORTY orderbook must have exactly 1 ask"); + assert_eq!( + alice_orderbook.asks.len(), + 1, + "Alice RICK/MORTY orderbook must have exactly 1 ask" + ); let cancel_rc = block_on(mm_bob.rpc(json! ({ "userpass": mm_bob.userpass, @@ -1592,10 +1510,9 @@ fn test_cancel_order() { .unwrap(); assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - let bob_orderbook: Json = json::from_str(&rc.1).unwrap(); + let bob_orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("Bob orderbook "[bob_orderbook]); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 0, "Bob RICK/MORTY asks are not empty"); + assert_eq!(bob_orderbook.asks.len(), 0, "Bob RICK/MORTY asks are not empty"); // Alice orderbook must show no orders log!("Get RICK/MORTY orderbook on Alice side"); @@ -1608,10 +1525,9 @@ fn test_cancel_order() { .unwrap(); assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - let alice_orderbook: Json = json::from_str(&rc.1).unwrap(); + let alice_orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("Alice orderbook "[alice_orderbook]); - let asks = alice_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 0, "Alice RICK/MORTY asks are not empty"); + assert_eq!(alice_orderbook.asks.len(), 0, "Alice RICK/MORTY asks are not empty"); } #[test] @@ -1694,8 +1610,8 @@ fn test_cancel_all_orders() { // Enable coins on Alice side. Print the replies in case we need the "address". log! ({"enable_coins (alice): {:?}", block_on (enable_coins_eth_electrum (&mm_alice, &["https://ropsten.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b"]))}); - log!("Give Alice 15 seconds to import the order…"); - thread::sleep(Duration::from_secs(15)); + log!("Give Alice 3 seconds to import the order…"); + thread::sleep(Duration::from_secs(3)); log!("Get RICK/MORTY orderbook on Alice side"); let rc = block_on(mm_alice.rpc(json! ({ @@ -1877,6 +1793,7 @@ fn test_order_should_not_be_displayed_when_node_is_down() { "coins": coins, "seednodes": [fomat!((mm_bob.ip))], "rpc_password": "pass", + "maker_order_timeout": 5, }), "pass".into(), match var("LOCAL_THREAD_MM") { @@ -1938,7 +1855,7 @@ fn test_order_should_not_be_displayed_when_node_is_down() { assert_eq!(asks.len(), 1, "Alice RICK/MORTY orderbook must have exactly 1 ask"); block_on(mm_bob.stop()).unwrap(); - thread::sleep(Duration::from_secs(95)); + thread::sleep(Duration::from_secs(6)); let rc = block_on(mm_alice.rpc(json! ({ "userpass": mm_alice.userpass, @@ -1978,6 +1895,7 @@ fn test_own_orders_should_not_be_removed_from_orderbook() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "maker_order_timeout": 5, }), "pass".into(), match var("LOCAL_THREAD_MM") { @@ -2019,7 +1937,7 @@ fn test_own_orders_should_not_be_removed_from_orderbook() { .unwrap(); assert!(rc.0.is_success(), "!setprice: {}", rc.1); - thread::sleep(Duration::from_secs(95)); + thread::sleep(Duration::from_secs(6)); let rc = block_on(mm_bob.rpc(json! ({ "userpass": mm_bob.userpass, @@ -2105,7 +2023,7 @@ fn test_all_orders_per_pair_per_node_must_be_displayed_in_orderbook() { .unwrap(); assert!(rc.0.is_success(), "!setprice: {}", rc.1); - thread::sleep(Duration::from_secs(12)); + thread::sleep(Duration::from_secs(2)); log!("Get RICK/MORTY orderbook"); let rc = block_on(mm.rpc(json! ({ @@ -2180,7 +2098,7 @@ fn orderbook_should_display_rational_amounts() { .unwrap(); assert!(rc.0.is_success(), "!setprice: {}", rc.1); - thread::sleep(Duration::from_secs(12)); + thread::sleep(Duration::from_secs(1)); log!("Get RICK/MORTY orderbook"); let rc = block_on(mm.rpc(json! ({ "userpass": mm.userpass, @@ -2191,25 +2109,20 @@ fn orderbook_should_display_rational_amounts() { .unwrap(); assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - let orderbook: Json = json::from_str(&rc.1).unwrap(); + let orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("orderbook "[orderbook]); - let asks = orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "RICK/MORTY orderbook must have exactly 1 ask"); - let price_in_orderbook: BigRational = json::from_value(asks[0]["price_rat"].clone()).unwrap(); - let volume_in_orderbook: BigRational = json::from_value(asks[0]["max_volume_rat"].clone()).unwrap(); - assert_eq!(price, price_in_orderbook); - assert_eq!(volume, volume_in_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); let nine = BigInt::from(9); let ten = BigInt::from(10); // should also display fraction - let price_in_orderbook: Fraction = json::from_value(asks[0]["price_fraction"].clone()).unwrap(); - let volume_in_orderbook: Fraction = json::from_value(asks[0]["max_volume_fraction"].clone()).unwrap(); - assert_eq!(nine, *price_in_orderbook.numer()); - assert_eq!(ten, *price_in_orderbook.denom()); + assert_eq!(nine, *orderbook.asks[0].price_fraction.numer()); + assert_eq!(ten, *orderbook.asks[0].price_fraction.denom()); - assert_eq!(nine, *volume_in_orderbook.numer()); - assert_eq!(ten, *volume_in_orderbook.denom()); + assert_eq!(nine, *orderbook.asks[0].max_volume_fraction.numer()); + assert_eq!(ten, *orderbook.asks[0].max_volume_fraction.denom()); log!("Get MORTY/RICK orderbook"); let rc = block_on(mm.rpc(json! ({ @@ -2221,25 +2134,119 @@ fn orderbook_should_display_rational_amounts() { .unwrap(); assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - let orderbook: Json = json::from_str(&rc.1).unwrap(); + let orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("orderbook "[orderbook]); - let bids = orderbook["bids"].as_array().unwrap(); - assert_eq!(bids.len(), 1, "MORTY/RICK orderbook must have exactly 1 bid"); - let price_in_orderbook: BigRational = json::from_value(bids[0]["price_rat"].clone()).unwrap(); - let volume_in_orderbook: BigRational = json::from_value(bids[0]["max_volume_rat"].clone()).unwrap(); + assert_eq!(orderbook.bids.len(), 1, "MORTY/RICK orderbook must have exactly 1 bid"); let price = BigRational::new(10.into(), 9.into()); - assert_eq!(price, price_in_orderbook); - assert_eq!(volume, volume_in_orderbook); + assert_eq!(price, orderbook.bids[0].price_rat); + assert_eq!(volume, orderbook.bids[0].max_volume_rat); // should also display fraction - let price_in_orderbook: Fraction = json::from_value(bids[0]["price_fraction"].clone()).unwrap(); - let volume_in_orderbook: Fraction = json::from_value(bids[0]["max_volume_fraction"].clone()).unwrap(); - assert_eq!(ten, *price_in_orderbook.numer()); - assert_eq!(nine, *price_in_orderbook.denom()); + assert_eq!(ten, *orderbook.bids[0].price_fraction.numer()); + assert_eq!(nine, *orderbook.bids[0].price_fraction.denom()); + + assert_eq!(nine, *orderbook.bids[0].max_volume_fraction.numer()); + assert_eq!(ten, *orderbook.bids[0].max_volume_fraction.denom()); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn orderbook_should_display_base_rel_volumes() { + let coins = json!([ + {"coin":"RICK","asset":"RICK","protocol":{"type":"UTXO"}}, + {"coin":"MORTY","asset":"MORTY","protocol":{"type":"UTXO"}}, + ]); + + let mut mm = MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 9998, + "myipaddr": env::var ("BOB_TRADE_IP") .ok(), + "rpcip": env::var ("BOB_TRADE_IP") .ok(), + "canbind": env::var ("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": "bob passphrase", + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + }), + "pass".into(), + match var("LOCAL_THREAD_MM") { + Ok(ref e) if e == "bob" => Some(local_start()), + _ => None, + }, + ) + .unwrap(); + let (_dump_log, _dump_dashboard) = &mm.mm_dump(); + log!({"Log path: {}", mm.log_path.display()}); + block_on(mm.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + block_on(enable_electrum(&mm, "RICK", false, &[ + "electrum3.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum1.cipig.net:10017", + ])); + block_on(enable_electrum(&mm, "MORTY", false, &[ + "electrum3.cipig.net:10018", + "electrum2.cipig.net:10018", + "electrum1.cipig.net:10018", + ])); + + let price = BigRational::new(2.into(), 1.into()); + let volume = BigRational::new(1.into(), 1.into()); - assert_eq!(nine, *volume_in_orderbook.numer()); - assert_eq!(ten, *volume_in_orderbook.denom()); + // 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); + + thread::sleep(Duration::from_secs(1)); + log!("Get RICK/MORTY orderbook"); + let rc = block_on(mm.rpc(json! ({ + "userpass": mm.userpass, + "method": "orderbook", + "base": "RICK", + "rel": "MORTY", + }))) + .unwrap(); + 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"); + let min_volume = BigRational::new(777.into(), 100000.into()); + assert_eq!(volume, orderbook.asks[0].base_max_volume_rat); + assert_eq!(min_volume, orderbook.asks[0].base_min_volume_rat); + + assert_eq!(&volume * &price, orderbook.asks[0].rel_max_volume_rat); + assert_eq!(&min_volume * &price, orderbook.asks[0].rel_min_volume_rat); + + 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 orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); + log!("orderbook "[orderbook]); + assert_eq!(orderbook.bids.len(), 1, "MORTY/RICK orderbook must have exactly 1 bid"); + let min_volume = BigRational::new(777.into(), 100000.into()); + assert_eq!(volume, orderbook.bids[0].rel_max_volume_rat); + assert_eq!(min_volume, orderbook.bids[0].rel_min_volume_rat); + + assert_eq!(&volume * &price, orderbook.bids[0].base_max_volume_rat); + assert_eq!(&min_volume * &price, orderbook.bids[0].base_min_volume_rat); } fn check_priv_key(mm: &MarketMakerIt, coin: &str, expected_priv_key: &str) { @@ -2757,7 +2764,8 @@ fn test_fill_or_kill_taker_order_should_not_transform_to_maker() { "volume": 0.1, "order_type": { "type": "FillOrKill" - } + }, + "timeout": 2, }))) .unwrap(); assert!(rc.0.is_success(), "!sell: {}", rc.1); @@ -2765,8 +2773,8 @@ fn test_fill_or_kill_taker_order_should_not_transform_to_maker() { let order_type = sell_json["result"]["order_type"]["type"].as_str(); assert_eq!(order_type, Some("FillOrKill")); - log!("Wait for 40 seconds for Bob order to be cancelled"); - thread::sleep(Duration::from_secs(40)); + log!("Wait for 4 seconds for Bob order to be cancelled"); + thread::sleep(Duration::from_secs(4)); let rc = block_on(mm_bob.rpc(json! ({ "userpass": mm_bob.userpass, @@ -2828,15 +2836,16 @@ fn test_gtc_taker_order_should_transform_to_maker() { "volume": 0.1, "order_type": { "type": "GoodTillCancelled" - } + }, + "timeout": 2, }))) .unwrap(); assert!(rc.0.is_success(), "!setprice: {}", rc.1); let rc_json: Json = json::from_str(&rc.1).unwrap(); let uuid: Uuid = json::from_value(rc_json["result"]["uuid"].clone()).unwrap(); - log!("Wait for 40 seconds for Bob order to be converted to maker"); - thread::sleep(Duration::from_secs(40)); + log!("Wait for 4 seconds for Bob order to be converted to maker"); + thread::sleep(Duration::from_secs(4)); let rc = block_on(mm_bob.rpc(json! ({ "userpass": mm_bob.userpass, @@ -3071,7 +3080,7 @@ fn set_price_with_cancel_previous_should_broadcast_cancelled_message() { let rc = block_on(mm_bob.rpc(set_price_json.clone())).unwrap(); assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let pause = 11; + let pause = 2; log!("Waiting (" (pause) " seconds) for Bob to broadcast messages…"); thread::sleep(Duration::from_secs(pause)); @@ -4503,8 +4512,7 @@ fn test_buy_response_format() { }))) .unwrap(); assert!(rc.0.is_success(), "!buy: {}", rc.1); - let json: Json = json::from_str(&rc.1).unwrap(); - let _: BuyOrSellRpcResult = json::from_value(json["result"].clone()).unwrap(); + let _: BuyOrSellRpcResult = json::from_str(&rc.1).unwrap(); } #[test] @@ -4559,8 +4567,7 @@ fn test_sell_response_format() { }))) .unwrap(); assert!(rc.0.is_success(), "!sell: {}", rc.1); - let json: Json = json::from_str(&rc.1).unwrap(); - let _: BuyOrSellRpcResult = json::from_value(json["result"].clone()).unwrap(); + let _: BuyOrSellRpcResult = json::from_str(&rc.1).unwrap(); } #[test] @@ -4640,8 +4647,7 @@ fn test_my_orders_response_format() { .unwrap(); assert!(rc.0.is_success(), "!my_orders: {}", rc.1); - let json: Json = json::from_str(&rc.1).unwrap(); - let _: MyOrdersRpcResult = json::from_value(json["result"].clone()).unwrap(); + let _: MyOrdersRpcResult = json::from_str(&rc.1).unwrap(); } #[test] @@ -4731,8 +4737,7 @@ fn test_my_orders_after_matched() { .unwrap(); assert!(rc.0.is_success(), "!my_orders: {}", rc.1); - let json: Json = json::from_str(&rc.1).unwrap(); - let _: MyOrdersRpcResult = json::from_value(json["result"].clone()).unwrap(); + let _: MyOrdersRpcResult = json::from_str(&rc.1).unwrap(); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); } @@ -5021,9 +5026,6 @@ fn test_orderbook_is_mine_orders() { // Enable coins on Alice side. Print the replies in case we need the "address". log! ({"enable_coins (alice): {:?}", block_on (enable_coins_eth_electrum (&mm_alice, &["https://ropsten.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b"]))}); - log!("Give Alice 15 seconds to import the order…"); - thread::sleep(Duration::from_secs(15)); - // Bob orderbook must show 1 mine order log!("Get RICK/MORTY orderbook on Bob side"); let rc = block_on(mm_bob.rpc(json! ({ @@ -5072,8 +5074,8 @@ fn test_orderbook_is_mine_orders() { .unwrap(); assert!(rc.0.is_success(), "!buy: {}", rc.1); - log!("Give Bob 15 seconds to import the order…"); - thread::sleep(Duration::from_secs(15)); + log!("Give Bob 2 seconds to import the order…"); + thread::sleep(Duration::from_secs(2)); // Bob orderbook must show 1 mine and 1 non-mine orders. // Request orderbook with reverse base and rel coins to check bids instead of asks @@ -5169,7 +5171,8 @@ fn test_sell_min_volume() { "min_volume": min_volume, "order_type": { "type": "GoodTillCancelled" - } + }, + "timeout": 2, }))) .unwrap(); assert!(rc.0.is_success(), "!sell: {}", rc.1); @@ -5178,8 +5181,8 @@ fn test_sell_min_volume() { let min_volume_response: BigDecimal = json::from_value(rc_json["result"]["min_volume"].clone()).unwrap(); assert_eq!(min_volume, min_volume_response); - log!("Wait for 40 seconds for Bob order to be converted to maker"); - thread::sleep(Duration::from_secs(40)); + log!("Wait for 4 seconds for Bob order to be converted to maker"); + thread::sleep(Duration::from_secs(4)); let rc = block_on(mm_bob.rpc(json! ({ "userpass": mm_bob.userpass, @@ -5197,6 +5200,115 @@ fn test_sell_min_volume() { assert_eq!(min_volume, min_volume_maker); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_sell_min_volume_dust() { + let bob_passphrase = get_passphrase(&".env.client", "BOB_PASSPHRASE").unwrap(); + + let coins = json! ([ + {"coin":"RICK","asset":"RICK","dust":10000000,"required_confirmations":0,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, + {"coin":"MORTY","asset":"MORTY","required_confirmations":0,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, + {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH"}}, + {"coin":"JST","name":"jst","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":"0x2b294F029Fde858b2c62184e8390591755521d8E"}}} + ]); + + let mut mm_bob = MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 8999, + "dht": "on", // Enable DHT without delay. + "myipaddr": env::var ("BOB_TRADE_IP") .ok(), + "rpcip": env::var ("BOB_TRADE_IP") .ok(), + "canbind": env::var ("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": bob_passphrase, + "coins": coins, + "rpc_password": "password", + "i_am_seed": true, + }), + "password".into(), + local_start!("bob"), + ) + .unwrap(); + + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log! ({"Bob log path: {}", mm_bob.log_path.display()}); + block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + log!([block_on(enable_coins_eth_electrum(&mm_bob, &[ + "http://195.201.0.6:8565" + ]))]); + + log!("Issue bob RICK/MORTY sell request"); + let rc = block_on(mm_bob.rpc(json! ({ + "userpass": mm_bob.userpass, + "method": "sell", + "base": "RICK", + "rel": "MORTY", + "price": "1", + "volume": "1", + "order_type": { + "type": "FillOrKill" + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + let response: BuyOrSellRpcResult = json::from_str(&rc.1).unwrap(); + let expected_min = BigDecimal::from(1); + assert_eq!(response.result.min_volume, expected_min); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_setprice_min_volume_dust() { + let bob_passphrase = get_passphrase(&".env.client", "BOB_PASSPHRASE").unwrap(); + + let coins = json! ([ + {"coin":"RICK","asset":"RICK","dust":10000000,"required_confirmations":0,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, + {"coin":"MORTY","asset":"MORTY","required_confirmations":0,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, + {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH"}}, + {"coin":"JST","name":"jst","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":"0x2b294F029Fde858b2c62184e8390591755521d8E"}}} + ]); + + let mut mm_bob = MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 8999, + "dht": "on", // Enable DHT without delay. + "myipaddr": env::var ("BOB_TRADE_IP") .ok(), + "rpcip": env::var ("BOB_TRADE_IP") .ok(), + "canbind": env::var ("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": bob_passphrase, + "coins": coins, + "rpc_password": "password", + "i_am_seed": true, + }), + "password".into(), + local_start!("bob"), + ) + .unwrap(); + + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log! ({"Bob log path: {}", mm_bob.log_path.display()}); + block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + log!([block_on(enable_coins_eth_electrum(&mm_bob, &[ + "http://195.201.0.6:8565" + ]))]); + + log!("Issue bob RICK/MORTY sell request"); + let rc = block_on(mm_bob.rpc(json! ({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "RICK", + "rel": "MORTY", + "price": "1", + "volume": "1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + let response: SetPriceResult = json::from_str(&rc.1).unwrap(); + let expected_min = BigDecimal::from(1); + assert_eq!(expected_min, response.result.min_base_vol); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_buy_min_volume() { @@ -5246,17 +5358,16 @@ fn test_buy_min_volume() { "min_volume": min_volume, "order_type": { "type": "GoodTillCancelled" - } + }, + "timeout": 2, }))) .unwrap(); assert!(rc.0.is_success(), "!sell: {}", rc.1); - let rc_json: Json = json::from_str(&rc.1).unwrap(); - let uuid: Uuid = json::from_value(rc_json["result"]["uuid"].clone()).unwrap(); - let min_volume_response: BigDecimal = json::from_value(rc_json["result"]["min_volume"].clone()).unwrap(); - assert_eq!(min_volume, min_volume_response); + let response: BuyOrSellRpcResult = json::from_str(&rc.1).unwrap(); + assert_eq!(min_volume, response.result.min_volume); - log!("Wait for 40 seconds for Bob order to be converted to maker"); - thread::sleep(Duration::from_secs(40)); + log!("Wait for 4 seconds for Bob order to be converted to maker"); + thread::sleep(Duration::from_secs(4)); let rc = block_on(mm_bob.rpc(json! ({ "userpass": mm_bob.userpass, @@ -5264,16 +5375,17 @@ fn test_buy_min_volume() { }))) .unwrap(); assert!(rc.0.is_success(), "!my_orders: {}", rc.1); - let my_orders: Json = json::from_str(&rc.1).unwrap(); - let my_maker_orders: HashMap = json::from_value(my_orders["result"]["maker_orders"].clone()).unwrap(); - let my_taker_orders: HashMap = json::from_value(my_orders["result"]["taker_orders"].clone()).unwrap(); - assert_eq!(1, my_maker_orders.len(), "maker_orders must have exactly 1 order"); - assert!(my_taker_orders.is_empty(), "taker_orders must be empty"); - let maker_order = my_maker_orders.get(&uuid).unwrap(); + let my_orders: MyOrdersRpcResult = json::from_str(&rc.1).unwrap(); + assert_eq!( + 1, + my_orders.result.maker_orders.len(), + "maker_orders must have exactly 1 order" + ); + assert!(my_orders.result.taker_orders.is_empty(), "taker_orders must be empty"); + let maker_order = my_orders.result.maker_orders.get(&response.result.uuid).unwrap(); let expected_min_volume: BigDecimal = "0.2".parse().unwrap(); - let min_volume_maker: BigDecimal = json::from_value(maker_order["min_base_vol"].clone()).unwrap(); - assert_eq!(expected_min_volume, min_volume_maker); + assert_eq!(expected_min_volume, maker_order.min_base_vol); } #[test] @@ -5693,6 +5805,7 @@ mod wasm_bindgen_tests { 1.into(), taker_persistent_pub, uuid, + None, conf_settings.clone(), eth_taker, jst_taker, @@ -5706,6 +5819,7 @@ mod wasm_bindgen_tests { 1.into(), maker_persistent_pub, uuid, + None, conf_settings, eth_maker, jst_maker, diff --git a/mm2src/mm2_tests/structs.rs b/mm2src/mm2_tests/structs.rs index 57d044255a..c101eff859 100644 --- a/mm2src/mm2_tests/structs.rs +++ b/mm2src/mm2_tests/structs.rs @@ -11,7 +11,7 @@ use uuid::Uuid; #[derive(Deserialize)] #[serde(tag = "type", content = "data")] -enum OrderType { +pub enum OrderType { FillOrKill, GoodTillCancelled, } @@ -38,22 +38,30 @@ pub enum MatchBy { Pubkeys(HashSet), } +#[derive(Deserialize)] +pub struct BuyOrSellRpcRes { + pub base: String, + pub rel: String, + pub base_amount: BigDecimal, + pub base_amount_rat: BigRational, + pub rel_amount: BigDecimal, + pub rel_amount_rat: BigRational, + pub min_volume: BigDecimal, + pub min_volume_rat: BigRational, + pub min_volume_fraction: Fraction, + pub action: TakerAction, + pub uuid: Uuid, + pub method: String, + pub sender_pubkey: H256Json, + pub dest_pub_key: H256Json, + pub match_by: MatchBy, + pub conf_settings: OrderConfirmationsSettings, + pub order_type: OrderType, +} + #[derive(Deserialize)] pub struct BuyOrSellRpcResult { - base: String, - rel: String, - base_amount: BigDecimal, - base_amount_rat: BigRational, - rel_amount: BigDecimal, - rel_amount_rat: BigRational, - action: TakerAction, - uuid: Uuid, - method: String, - sender_pubkey: H256Json, - dest_pub_key: H256Json, - match_by: MatchBy, - conf_settings: OrderConfirmationsSettings, - order_type: OrderType, + pub result: BuyOrSellRpcRes, } #[derive(Deserialize)] @@ -155,12 +163,17 @@ pub struct TakerOrderRpcResult { } #[derive(Deserialize)] -pub struct MyOrdersRpcResult { - maker_orders: HashMap, - taker_orders: HashMap, +pub struct MyOrdersRpc { + pub maker_orders: HashMap, + pub taker_orders: HashMap, } #[derive(Deserialize)] +pub struct MyOrdersRpcResult { + pub result: MyOrdersRpc, +} + +#[derive(Debug, Deserialize)] pub struct OrderbookEntry { pub coin: String, pub address: String, @@ -171,6 +184,18 @@ pub struct OrderbookEntry { 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, @@ -186,6 +211,14 @@ pub struct BestOrdersResponse { pub result: HashMap>, } +#[derive(Debug, Deserialize)] +pub struct OrderbookResponse { + #[serde(rename = "askdepth")] + pub ask_depth: usize, + pub asks: Vec, + pub bids: Vec, +} + #[derive(Deserialize)] pub struct PairDepth { pub asks: usize, diff --git a/mm2src/ordermatch_tests.rs b/mm2src/ordermatch_tests.rs index 614747d722..08ad5cea4a 100644 --- a/mm2src/ordermatch_tests.rs +++ b/mm2src/ordermatch_tests.rs @@ -200,28 +200,30 @@ 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 maker_order = MakerOrderBuilder::default() + let coin = MmCoinEnum::Test(TestCoin {}); + + let maker_order = MakerOrderBuilder::new(&coin, &coin) .with_max_base_vol(1.into()) .with_price(1.into()) .build_unchecked(); - // default taker request has empty coins and zero amounts so it should pass to the price calculation stage (division) - let taker_request = TakerRequestBuilder::default() + // default taker order has empty coins and zero amounts so it should pass to the price calculation stage (division) + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_rel_amount(1.into()) .build_unchecked(); let expected = OrderMatchResult::NotMatched; - let actual = maker_order.match_with_request(&taker_request); + let actual = maker_order.match_with_request(&taker_order.request); assert_eq!(expected, actual); - // default taker request has empty coins and zero amounts so it should pass to the price calculation stage (division) - let taker_request = TakerRequestBuilder::default() + // default taker order has empty coins and zero amounts so it should pass to the price calculation stage (division) + let taker_request = TakerOrderBuilder::new(&coin, &coin) .with_base_amount(1.into()) .with_action(TakerAction::Sell) .build_unchecked(); let expected = OrderMatchResult::NotMatched; - let actual = maker_order.match_with_request(&taker_request); + let actual = maker_order.match_with_request(&taker_request.request); assert_eq!(expected, actual); } @@ -324,6 +326,7 @@ fn test_taker_match_reserved() { created_at: now_ms(), order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }; let reserved = MakerReserved { @@ -359,6 +362,7 @@ fn test_taker_match_reserved() { created_at: now_ms(), order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }; let reserved = MakerReserved { @@ -394,6 +398,7 @@ fn test_taker_match_reserved() { created_at: now_ms(), order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }; let reserved = MakerReserved { @@ -429,6 +434,7 @@ fn test_taker_match_reserved() { created_at: now_ms(), order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }; let reserved = MakerReserved { @@ -464,6 +470,7 @@ fn test_taker_match_reserved() { created_at: now_ms(), order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }; let reserved = MakerReserved { @@ -499,6 +506,7 @@ fn test_taker_match_reserved() { created_at: now_ms(), order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }; let reserved = MakerReserved { @@ -534,6 +542,7 @@ fn test_taker_match_reserved() { created_at: now_ms(), order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }; let reserved = MakerReserved { @@ -569,6 +578,7 @@ fn test_taker_match_reserved() { created_at: now_ms(), order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }; let reserved = MakerReserved { @@ -604,6 +614,7 @@ fn test_taker_match_reserved() { matches: HashMap::new(), order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }; let reserved = MakerReserved { @@ -642,6 +653,7 @@ fn test_taker_order_cancellable() { created_at: now_ms(), order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }; assert!(order.is_cancellable()); @@ -665,6 +677,7 @@ fn test_taker_order_cancellable() { created_at: now_ms(), order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }; order.matches.insert(Uuid::new_v4(), TakerMatch { @@ -754,6 +767,7 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { }, order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }); rx } @@ -842,6 +856,7 @@ fn test_taker_order_match_by() { created_at: now_ms(), order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), + timeout: 30, }; let reserved = MakerReserved { @@ -934,12 +949,12 @@ fn should_process_request_only_once() { #[test] fn test_choose_maker_confs_settings() { - // no confs set - let taker_request = TakerRequestBuilder::default().build_unchecked(); let coin = TestCoin {}.into(); + // no confs set + let taker_order = TakerOrderBuilder::new(&coin, &coin).build_unchecked(); TestCoin::requires_notarization.mock_safe(|_| MockResult::Return(true)); TestCoin::required_confirmations.mock_safe(|_| MockResult::Return(8)); - let settings = choose_maker_confs_and_notas(None, &taker_request, &coin, &coin); + let settings = choose_maker_confs_and_notas(None, &taker_order.request, &coin, &coin); // should pick settings from coin configuration assert!(settings.maker_coin_nota); assert_eq!(settings.maker_coin_confs, 8); @@ -953,8 +968,8 @@ fn test_choose_maker_confs_settings() { rel_nota: false, }; // no confs set - let taker_request = TakerRequestBuilder::default().build_unchecked(); - let settings = choose_maker_confs_and_notas(Some(maker_conf_settings), &taker_request, &coin, &coin); + let taker_order = TakerOrderBuilder::new(&coin, &coin).build_unchecked(); + let settings = choose_maker_confs_and_notas(Some(maker_conf_settings), &taker_order.request, &coin, &coin); // should pick settings from maker order assert!(!settings.maker_coin_nota); assert_eq!(settings.maker_coin_confs, 1); @@ -973,10 +988,10 @@ fn test_choose_maker_confs_settings() { rel_confs: 5, rel_nota: false, }; - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_conf_settings(taker_conf_settings) .build_unchecked(); - let settings = choose_maker_confs_and_notas(Some(maker_conf_settings), &taker_request, &coin, &coin); + let settings = choose_maker_confs_and_notas(Some(maker_conf_settings), &taker_order.request, &coin, &coin); // should pick settings from taker request because taker will wait less time for our // payment confirmation assert!(!settings.maker_coin_nota); @@ -996,10 +1011,10 @@ fn test_choose_maker_confs_settings() { rel_confs: 1000, rel_nota: true, }; - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_conf_settings(taker_conf_settings) .build_unchecked(); - let settings = choose_maker_confs_and_notas(Some(maker_conf_settings), &taker_request, &coin, &coin); + let settings = choose_maker_confs_and_notas(Some(maker_conf_settings), &taker_order.request, &coin, &coin); // keep using our settings allowing taker to wait for our payment conf as much as he likes assert!(!settings.maker_coin_nota); assert_eq!(settings.maker_coin_confs, 10); @@ -1019,10 +1034,10 @@ fn test_choose_maker_confs_settings() { base_confs: 1, base_nota: false, }; - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_conf_settings(taker_conf_settings) .build_unchecked(); - let settings = choose_maker_confs_and_notas(Some(maker_conf_settings), &taker_request, &coin, &coin); + let settings = choose_maker_confs_and_notas(Some(maker_conf_settings), &taker_order.request, &coin, &coin); // Taker conf settings should not have any effect on maker conf requirements for taker payment assert!(settings.taker_coin_nota); @@ -1041,11 +1056,11 @@ fn test_choose_maker_confs_settings() { base_confs: 5, base_nota: false, }; - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_conf_settings(taker_conf_settings) .with_action(TakerAction::Sell) .build_unchecked(); - let settings = choose_maker_confs_and_notas(Some(maker_conf_settings), &taker_request, &coin, &coin); + let settings = choose_maker_confs_and_notas(Some(maker_conf_settings), &taker_order.request, &coin, &coin); // should pick settings from taker request because taker will wait less time for our // payment confirmation assert!(!settings.maker_coin_nota); @@ -1056,14 +1071,15 @@ fn test_choose_maker_confs_settings() { #[test] fn test_choose_taker_confs_settings_buy_action() { + let coin = TestCoin {}.into(); + // no confs and notas set - let taker_request = TakerRequestBuilder::default().build_unchecked(); + let taker_order = TakerOrderBuilder::new(&coin, &coin).build_unchecked(); // no confs and notas set let maker_reserved = MakerReserved::default(); - let coin = TestCoin {}.into(); TestCoin::requires_notarization.mock_safe(|_| MockResult::Return(true)); TestCoin::required_confirmations.mock_safe(|_| MockResult::Return(8)); - let settings = choose_taker_confs_and_notas(&taker_request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); // should pick settings from coins assert!(settings.taker_coin_nota); assert_eq!(settings.taker_coin_confs, 8); @@ -1076,12 +1092,12 @@ fn test_choose_taker_confs_settings_buy_action() { rel_confs: 4, rel_nota: false, }; - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_conf_settings(taker_conf_settings) .build_unchecked(); // no confs and notas set let maker_reserved = MakerReserved::default(); - let settings = choose_taker_confs_and_notas(&taker_request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); // should pick settings from taker request // as action is buy my_coin is rel and other coin is base assert!(!settings.taker_coin_nota); @@ -1095,7 +1111,7 @@ fn test_choose_taker_confs_settings_buy_action() { rel_confs: 2, rel_nota: true, }; - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_conf_settings(taker_conf_settings) .build_unchecked(); let mut maker_reserved = MakerReserved::default(); @@ -1106,7 +1122,7 @@ fn test_choose_taker_confs_settings_buy_action() { base_nota: true, }; maker_reserved.conf_settings = Some(maker_conf_settings); - let settings = choose_taker_confs_and_notas(&taker_request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); // should pick settings from maker reserved if he requires less confs // as action is buy my_coin is rel and other coin is base in request assert!(!settings.taker_coin_nota); @@ -1120,7 +1136,7 @@ fn test_choose_taker_confs_settings_buy_action() { rel_confs: 1, rel_nota: false, }; - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_conf_settings(taker_conf_settings) .build_unchecked(); let mut maker_reserved = MakerReserved::default(); @@ -1131,7 +1147,7 @@ fn test_choose_taker_confs_settings_buy_action() { base_nota: true, }; maker_reserved.conf_settings = Some(maker_conf_settings); - let settings = choose_taker_confs_and_notas(&taker_request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); // should allow maker to use more confirmations than we require, but it shouldn't affect our settings // as action is buy my_coin is rel and other coin is base in request assert!(!settings.taker_coin_nota); @@ -1145,7 +1161,7 @@ fn test_choose_taker_confs_settings_buy_action() { rel_confs: 1, rel_nota: false, }; - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_conf_settings(taker_conf_settings) .build_unchecked(); let mut maker_reserved = MakerReserved::default(); @@ -1156,7 +1172,7 @@ fn test_choose_taker_confs_settings_buy_action() { rel_nota: true, }; maker_reserved.conf_settings = Some(maker_conf_settings); - let settings = choose_taker_confs_and_notas(&taker_request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); // maker settings should have no effect on other_coin_confs and other_coin_nota // as action is buy my_coin is rel and other coin is base in request assert!(!settings.taker_coin_nota); @@ -1167,16 +1183,17 @@ fn test_choose_taker_confs_settings_buy_action() { #[test] fn test_choose_taker_confs_settings_sell_action() { + let coin = TestCoin {}.into(); + // no confs and notas set - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_action(TakerAction::Sell) .build_unchecked(); // no confs and notas set let maker_reserved = MakerReserved::default(); - let coin = TestCoin {}.into(); TestCoin::requires_notarization.mock_safe(|_| MockResult::Return(true)); TestCoin::required_confirmations.mock_safe(|_| MockResult::Return(8)); - let settings = choose_taker_confs_and_notas(&taker_request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); // should pick settings from coins assert!(settings.taker_coin_nota); assert_eq!(settings.taker_coin_confs, 8); @@ -1189,13 +1206,13 @@ fn test_choose_taker_confs_settings_sell_action() { rel_confs: 5, rel_nota: true, }; - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_action(TakerAction::Sell) .with_conf_settings(taker_conf_settings) .build_unchecked(); // no confs and notas set let maker_reserved = MakerReserved::default(); - let settings = choose_taker_confs_and_notas(&taker_request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); // should pick settings from taker request // as action is sell my_coin is base and other coin is rel in request assert!(!settings.taker_coin_nota); @@ -1209,7 +1226,7 @@ fn test_choose_taker_confs_settings_sell_action() { rel_confs: 2, rel_nota: true, }; - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_action(TakerAction::Sell) .with_conf_settings(taker_conf_settings) .build_unchecked(); @@ -1221,7 +1238,7 @@ fn test_choose_taker_confs_settings_sell_action() { rel_nota: false, }; maker_reserved.conf_settings = Some(maker_conf_settings); - let settings = choose_taker_confs_and_notas(&taker_request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); // should pick settings from maker reserved if he requires less confs // as action is sell my_coin is base and other coin is rel in request assert!(!settings.taker_coin_nota); @@ -1235,7 +1252,7 @@ fn test_choose_taker_confs_settings_sell_action() { rel_confs: 2, rel_nota: true, }; - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_action(TakerAction::Sell) .with_conf_settings(taker_conf_settings) .build_unchecked(); @@ -1247,7 +1264,7 @@ fn test_choose_taker_confs_settings_sell_action() { base_nota: false, }; maker_reserved.conf_settings = Some(maker_conf_settings); - let settings = choose_taker_confs_and_notas(&taker_request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); // should allow maker to use more confirmations than we require, but it shouldn't affect our settings // as action is sell my_coin is base and other coin is rel in request assert!(!settings.taker_coin_nota); @@ -1261,7 +1278,7 @@ fn test_choose_taker_confs_settings_sell_action() { rel_confs: 2, rel_nota: true, }; - let taker_request = TakerRequestBuilder::default() + let taker_order = TakerOrderBuilder::new(&coin, &coin) .with_action(TakerAction::Sell) .with_conf_settings(taker_conf_settings) .build_unchecked(); @@ -1273,7 +1290,7 @@ fn test_choose_taker_confs_settings_sell_action() { base_nota: false, }; maker_reserved.conf_settings = Some(maker_conf_settings); - let settings = choose_taker_confs_and_notas(&taker_request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); // maker settings should have no effect on other_coin_confs and other_coin_nota // as action is sell my_coin is base and other coin is rel in request assert!(!settings.taker_coin_nota); @@ -1841,44 +1858,47 @@ fn test_subscribe_to_ordermatch_topic_subscribed_filled() { */ #[test] fn test_taker_request_can_match_with_maker_pubkey() { + let coin = TestCoin {}.into(); + let maker_pubkey = H256Json::default(); // default has MatchBy::Any - let mut request = TakerRequestBuilder::default().build_unchecked(); - assert!(request.can_match_with_maker_pubkey(&maker_pubkey)); + let mut order = TakerOrderBuilder::new(&coin, &coin).build_unchecked(); + assert!(order.request.can_match_with_maker_pubkey(&maker_pubkey)); // the uuids of orders is checked in another method - request.match_by = MatchBy::Orders(HashSet::new()); - assert!(request.can_match_with_maker_pubkey(&maker_pubkey)); + order.request.match_by = MatchBy::Orders(HashSet::new()); + assert!(order.request.can_match_with_maker_pubkey(&maker_pubkey)); let mut set = HashSet::new(); set.insert(maker_pubkey.clone()); - request.match_by = MatchBy::Pubkeys(set); - assert!(request.can_match_with_maker_pubkey(&maker_pubkey)); + order.request.match_by = MatchBy::Pubkeys(set); + assert!(order.request.can_match_with_maker_pubkey(&maker_pubkey)); - request.match_by = MatchBy::Pubkeys(HashSet::new()); - assert!(!request.can_match_with_maker_pubkey(&maker_pubkey)); + order.request.match_by = MatchBy::Pubkeys(HashSet::new()); + assert!(!order.request.can_match_with_maker_pubkey(&maker_pubkey)); } #[test] fn test_taker_request_can_match_with_uuid() { let uuid = Uuid::new_v4(); + let coin = MmCoinEnum::Test(TestCoin {}); // default has MatchBy::Any - let mut request = TakerRequestBuilder::default().build_unchecked(); - assert!(request.can_match_with_uuid(&uuid)); + let mut order = TakerOrderBuilder::new(&coin, &coin).build_unchecked(); + assert!(order.request.can_match_with_uuid(&uuid)); // the uuids of orders is checked in another method - request.match_by = MatchBy::Pubkeys(HashSet::new()); - assert!(request.can_match_with_uuid(&uuid)); + order.request.match_by = MatchBy::Pubkeys(HashSet::new()); + assert!(order.request.can_match_with_uuid(&uuid)); let mut set = HashSet::new(); set.insert(uuid); - request.match_by = MatchBy::Orders(set); - assert!(request.can_match_with_uuid(&uuid)); + order.request.match_by = MatchBy::Orders(set); + assert!(order.request.can_match_with_uuid(&uuid)); - request.match_by = MatchBy::Orders(HashSet::new()); - assert!(!request.can_match_with_uuid(&uuid)); + order.request.match_by = MatchBy::Orders(HashSet::new()); + assert!(!order.request.can_match_with_uuid(&uuid)); } #[test]