From 65929c41b3511c6dc63c2b5c5e6cfa704b43a8be Mon Sep 17 00:00:00 2001 From: Artem Pikulin Date: Tue, 23 Feb 2021 20:35:11 +0700 Subject: [PATCH] Add min_volume arg to buy and sell RPCs. --- mm2src/lp_ordermatch.rs | 40 ++++++---- mm2src/mm2_tests.rs | 152 +++++++++++++++++++++++++++++++++++++ mm2src/ordermatch_tests.rs | 13 ++++ 3 files changed, 192 insertions(+), 13 deletions(-) diff --git a/mm2src/lp_ordermatch.rs b/mm2src/lp_ordermatch.rs index d7f73a7ef7..eb2850daf1 100644 --- a/mm2src/lp_ordermatch.rs +++ b/mm2src/lp_ordermatch.rs @@ -1100,6 +1100,7 @@ struct TakerOrder { created_at: u64, request: TakerRequest, matches: HashMap, + min_volume: MmNumber, order_type: OrderType, } @@ -1464,7 +1465,7 @@ impl Into for TakerOrder { TakerAction::Sell => MakerOrder { price: (self.request.get_rel_amount() / self.request.get_base_amount()), max_base_vol: self.request.get_base_amount().clone(), - min_base_vol: MIN_TRADING_VOL.into(), + min_base_vol: self.min_volume, created_at: now_ms(), base: self.request.base, rel: self.request.rel, @@ -1474,17 +1475,21 @@ impl Into for TakerOrder { conf_settings: self.request.conf_settings, }, // The "buy" taker order is recreated with reversed pair as Maker order is always considered as "sell" - TakerAction::Buy => MakerOrder { - price: (self.request.get_base_amount() / self.request.get_rel_amount()), - max_base_vol: self.request.get_rel_amount().clone(), - min_base_vol: MIN_TRADING_VOL.into(), - created_at: now_ms(), - base: self.request.rel, - rel: self.request.base, - matches: HashMap::new(), - started_swaps: Vec::new(), - uuid: self.request.uuid, - conf_settings: self.request.conf_settings.map(|s| s.reversed()), + TakerAction::Buy => { + let price = self.request.get_base_amount() / self.request.get_rel_amount(); + let min_base_vol = &self.min_volume / &price; + MakerOrder { + price, + max_base_vol: self.request.get_rel_amount().clone(), + min_base_vol, + created_at: now_ms(), + base: self.request.rel, + rel: self.request.base, + matches: HashMap::new(), + started_swaps: Vec::new(), + uuid: self.request.uuid, + conf_settings: self.request.conf_settings.map(|s| s.reversed()), + } }, } } @@ -2545,6 +2550,8 @@ 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, @@ -2568,6 +2575,8 @@ pub struct AutoBuyInput { base_nota: Option, rel_confs: Option, rel_nota: Option, + #[serde(default = "min_trading_vol")] + min_volume: MmNumber, } pub async fn buy(ctx: MmArc, req: Json) -> Result>, String> { @@ -2672,11 +2681,15 @@ impl<'a> From<&'a TakerRequest> for TakerRequestForRpc<'a> { } } +construct_detailed!(DetailedMinVolume, min_volume); + #[derive(Serialize)] struct LpautobuyResult<'a> { #[serde(flatten)] request: TakerRequestForRpc<'a>, order_type: OrderType, + #[serde(flatten)] + min_volume: DetailedMinVolume, } #[derive(Clone, Debug, Serialize)] @@ -2742,16 +2755,17 @@ pub async fn lp_auto_buy( let result = json!({ "result": LpautobuyResult { request: (&request).into(), order_type: input.order_type, + min_volume: input.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); - drop(my_taker_orders); Ok(result.to_string()) } diff --git a/mm2src/mm2_tests.rs b/mm2src/mm2_tests.rs index 38be5a7c9e..518e9c4895 100644 --- a/mm2src/mm2_tests.rs +++ b/mm2src/mm2_tests.rs @@ -5045,6 +5045,158 @@ fn test_orderbook_is_mine_orders() { ); } +#[test] +fn test_sell_min_volume() { + let bob_passphrase = unwrap!(get_passphrase(&".env.client", "BOB_PASSPHRASE")); + + let coins = json! ([ + {"coin":"RICK","asset":"RICK","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 = unwrap!(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| unwrap! (s.parse::())), + "passphrase": bob_passphrase, + "coins": coins, + "rpc_password": "password", + "i_am_seed": true, + }), + "password".into(), + local_start!("bob") + )); + + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log! ({"Bob log path: {}", mm_bob.log_path.display()}); + unwrap!(block_on( + mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats ")) + )); + log!([block_on(enable_coins_eth_electrum(&mm_bob, vec![ + "http://195.201.0.6:8565" + ]))]); + + let min_volume: BigDecimal = "0.1".parse().unwrap(); + log!("Issue bob ETH/JST sell request"); + let rc = unwrap!(block_on(mm_bob.rpc(json! ({ + "userpass": mm_bob.userpass, + "method": "sell", + "base": "ETH", + "rel": "JST", + "price": "1", + "volume": "1", + "min_volume": min_volume, + "order_type": { + "type": "GoodTillCancelled" + } + })))); + 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); + + log!("Wait for 40 seconds for Bob order to be converted to maker"); + thread::sleep(Duration::from_secs(40)); + + let rc = unwrap!(block_on(mm_bob.rpc(json! ({ + "userpass": mm_bob.userpass, + "method": "my_orders", + })))); + assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + let my_orders: Json = unwrap!(json::from_str(&rc.1)); + let my_maker_orders: HashMap = unwrap!(json::from_value(my_orders["result"]["maker_orders"].clone())); + let my_taker_orders: HashMap = unwrap!(json::from_value(my_orders["result"]["taker_orders"].clone())); + 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 min_volume_maker: BigDecimal = json::from_value(maker_order["min_base_vol"].clone()).unwrap(); + assert_eq!(min_volume, min_volume_maker); +} + +#[test] +fn test_buy_min_volume() { + let bob_passphrase = unwrap!(get_passphrase(&".env.client", "BOB_PASSPHRASE")); + + let coins = json! ([ + {"coin":"RICK","asset":"RICK","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 = unwrap!(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| unwrap! (s.parse::())), + "passphrase": bob_passphrase, + "coins": coins, + "rpc_password": "password", + "i_am_seed": true, + }), + "password".into(), + local_start!("bob") + )); + + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log! ({"Bob log path: {}", mm_bob.log_path.display()}); + unwrap!(block_on( + mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats ")) + )); + log!([block_on(enable_coins_eth_electrum(&mm_bob, vec![ + "http://195.201.0.6:8565" + ]))]); + + let min_volume: BigDecimal = "0.1".parse().unwrap(); + log!("Issue bob ETH/JST sell request"); + let rc = unwrap!(block_on(mm_bob.rpc(json! ({ + "userpass": mm_bob.userpass, + "method": "buy", + "base": "ETH", + "rel": "JST", + "price": "2", + "volume": "1", + "min_volume": min_volume, + "order_type": { + "type": "GoodTillCancelled" + } + })))); + 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); + + log!("Wait for 40 seconds for Bob order to be converted to maker"); + thread::sleep(Duration::from_secs(40)); + + let rc = unwrap!(block_on(mm_bob.rpc(json! ({ + "userpass": mm_bob.userpass, + "method": "my_orders", + })))); + assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + let my_orders: Json = unwrap!(json::from_str(&rc.1)); + let my_maker_orders: HashMap = unwrap!(json::from_value(my_orders["result"]["maker_orders"].clone())); + let my_taker_orders: HashMap = unwrap!(json::from_value(my_orders["result"]["taker_orders"].clone())); + 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 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); +} + // HOWTO // 1. Install Firefox. // 2. Install forked version of wasm-bindgen-cli: cargo install wasm-bindgen-cli --git https://github.com/artemii235/wasm-bindgen.git diff --git a/mm2src/ordermatch_tests.rs b/mm2src/ordermatch_tests.rs index fb89cfacb9..b141ace37a 100644 --- a/mm2src/ordermatch_tests.rs +++ b/mm2src/ordermatch_tests.rs @@ -323,6 +323,7 @@ fn test_taker_match_reserved() { matches: HashMap::new(), created_at: now_ms(), order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }; let reserved = MakerReserved { @@ -357,6 +358,7 @@ fn test_taker_match_reserved() { matches: HashMap::new(), created_at: now_ms(), order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }; let reserved = MakerReserved { @@ -391,6 +393,7 @@ fn test_taker_match_reserved() { matches: HashMap::new(), created_at: now_ms(), order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }; let reserved = MakerReserved { @@ -425,6 +428,7 @@ fn test_taker_match_reserved() { matches: HashMap::new(), created_at: now_ms(), order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }; let reserved = MakerReserved { @@ -459,6 +463,7 @@ fn test_taker_match_reserved() { matches: HashMap::new(), created_at: now_ms(), order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }; let reserved = MakerReserved { @@ -493,6 +498,7 @@ fn test_taker_match_reserved() { matches: HashMap::new(), created_at: now_ms(), order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }; let reserved = MakerReserved { @@ -527,6 +533,7 @@ fn test_taker_match_reserved() { matches: HashMap::new(), created_at: now_ms(), order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }; let reserved = MakerReserved { @@ -561,6 +568,7 @@ fn test_taker_match_reserved() { matches: HashMap::new(), created_at: now_ms(), order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }; let reserved = MakerReserved { @@ -595,6 +603,7 @@ fn test_taker_match_reserved() { }, matches: HashMap::new(), order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }; let reserved = MakerReserved { @@ -632,6 +641,7 @@ fn test_taker_order_cancellable() { matches: HashMap::new(), created_at: now_ms(), order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }; assert!(order.is_cancellable()); @@ -654,6 +664,7 @@ fn test_taker_order_cancellable() { matches: HashMap::new(), created_at: now_ms(), order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }; order.matches.insert(Uuid::new_v4(), TakerMatch { @@ -742,6 +753,7 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { conf_settings: None, }, order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }); rx } @@ -829,6 +841,7 @@ fn test_taker_order_match_by() { matches: HashMap::new(), created_at: now_ms(), order_type: OrderType::GoodTillCancelled, + min_volume: 0.into(), }; let reserved = MakerReserved {