diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 808c5f5db1..352adc3757 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -2442,8 +2442,6 @@ impl EthTxFeeDetails { impl MmCoin for EthCoin { fn is_asset_chain(&self) -> bool { false } - fn wallet_only(&self) -> bool { false } - fn withdraw(&self, req: WithdrawRequest) -> Box + Send> { let ctx = try_fus!(MmArc::from_weak(&self.ctx).ok_or("!ctx")); Box::new(Box::pin(withdraw_impl(ctx, self.clone(), req)).compat()) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index d012f5a3c6..e02d07da8e 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -513,7 +513,10 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { fn is_asset_chain(&self) -> bool; /// The coin can be initialized, but it cannot participate in the swaps. - fn wallet_only(&self) -> bool; + fn wallet_only(&self, ctx: &MmArc) -> bool { + let coin_conf = coin_conf(&ctx, &self.ticker()); + coin_conf["wallet_only"].as_bool().unwrap_or(false) + } fn withdraw(&self, req: WithdrawRequest) -> Box + Send>; @@ -828,6 +831,13 @@ pub fn coin_conf(ctx: &MmArc, ticker: &str) -> Json { } } +pub fn is_wallet_only_conf(conf: &Json) -> bool { conf["wallet_only"].as_bool().unwrap_or(false) } + +pub fn is_wallet_only_ticker(ctx: &MmArc, ticker: &str) -> bool { + let coin_conf = coin_conf(ctx, ticker); + coin_conf["wallet_only"].as_bool().unwrap_or(false) +} + /// Adds a new currency into the list of currencies configured. /// /// Returns an error if the currency already exists. Initializing the same currency twice is a bad habit diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 8f66e8b02b..ca2e4794ef 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -925,8 +925,6 @@ impl MarketCoinOps for Qrc20Coin { impl MmCoin for Qrc20Coin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo) } - fn wallet_only(&self) -> bool { false } - fn withdraw(&self, req: WithdrawRequest) -> Box + Send> { Box::new(qrc20_withdraw(self.clone(), req).boxed().compat()) } diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 9ac4d9366e..7bafdcf5ac 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -222,8 +222,6 @@ impl SwapOps for TestCoin { impl MmCoin for TestCoin { fn is_asset_chain(&self) -> bool { unimplemented!() } - fn wallet_only(&self) -> bool { unimplemented!() } - fn withdraw(&self, req: WithdrawRequest) -> Box + Send> { unimplemented!() } diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index eac70c7055..06dbf9d3ef 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -499,8 +499,6 @@ impl MarketCoinOps for QtumCoin { impl MmCoin for QtumCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } - fn wallet_only(&self) -> bool { false } - fn withdraw(&self, req: WithdrawRequest) -> Box + Send> { Box::new(utxo_common::withdraw(self.clone(), req).boxed().compat()) } diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index afe771b28b..b9911f7f1b 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -394,8 +394,6 @@ impl MarketCoinOps for UtxoStandardCoin { impl MmCoin for UtxoStandardCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } - fn wallet_only(&self) -> bool { false } - fn withdraw(&self, req: WithdrawRequest) -> Box + Send> { Box::new(utxo_common::withdraw(self.clone(), req).boxed().compat()) } diff --git a/mm2src/lp_ordermatch.rs b/mm2src/lp_ordermatch.rs index f1bc75ef79..95bd2202b1 100644 --- a/mm2src/lp_ordermatch.rs +++ b/mm2src/lp_ordermatch.rs @@ -2676,11 +2676,11 @@ pub async fn buy(ctx: MmArc, req: Json) -> Result>, String> { let rel_coin = try_s!(rel_coin.ok_or("Rel coin is not found or inactive")); let base_coin = try_s!(lp_coinfind(&ctx, &input.base).await); let base_coin: MmCoinEnum = try_s!(base_coin.ok_or("Base coin is not found or inactive")); - if base_coin.wallet_only() { - return ERR!("Base coin is wallet only"); + if base_coin.wallet_only(&ctx) { + return ERR!("Base coin {} is wallet only", input.base); } - if rel_coin.wallet_only() { - return ERR!("Rel coin is wallet only"); + if rel_coin.wallet_only(&ctx) { + return ERR!("Rel coin {} is wallet only", input.rel); } let my_amount = &input.volume * &input.price; try_s!( @@ -2708,11 +2708,11 @@ pub async fn sell(ctx: MmArc, req: Json) -> Result>, String> { let base_coin = try_s!(base_coin.ok_or("Base coin is not found or inactive")); let rel_coin = try_s!(lp_coinfind(&ctx, &input.rel).await); let rel_coin = try_s!(rel_coin.ok_or("Rel coin is not found or inactive")); - if base_coin.wallet_only() { - return ERR!("Base coin is wallet only"); + if base_coin.wallet_only(&ctx) { + return ERR!("Base coin {} is wallet only", input.base); } - if rel_coin.wallet_only() { - return ERR!("Rel coin is wallet only"); + if rel_coin.wallet_only(&ctx) { + return ERR!("Rel coin {} is wallet only", input.rel); } try_s!( check_balance_for_taker_swap( @@ -3183,11 +3183,11 @@ pub async fn set_price(ctx: MmArc, req: Json) -> Result>, Strin None => return ERR!("Rel coin {} is not found", req.rel), }; - if base_coin.wallet_only() { - return ERR!("Base coin is wallet only"); + if base_coin.wallet_only(&ctx) { + return ERR!("Base coin {} is wallet only", req.base); } - if rel_coin.wallet_only() { - return ERR!("Rel coin is wallet only"); + if rel_coin.wallet_only(&ctx) { + return ERR!("Rel coin {} is wallet only", req.rel); } let my_balance = try_s!( diff --git a/mm2src/lp_ordermatch/best_orders.rs b/mm2src/lp_ordermatch/best_orders.rs index 6b69945282..3743fcd3ab 100644 --- a/mm2src/lp_ordermatch/best_orders.rs +++ b/mm2src/lp_ordermatch/best_orders.rs @@ -1,6 +1,6 @@ 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 coins::{address_by_coin_conf_and_pubkey_str, coin_conf, is_wallet_only_conf, is_wallet_only_ticker}; use common::log; use common::mm_ctx::MmArc; use common::mm_number::MmNumber; @@ -106,6 +106,9 @@ pub async fn process_best_orders_p2p_request( pub async fn best_orders_rpc(ctx: MmArc, req: Json) -> Result>, String> { let req: BestOrdersRequest = try_s!(json::from_value(req)); + if is_wallet_only_ticker(&ctx, &req.coin) { + return ERR!("Coin {} is wallet only", &req.coin); + } let p2p_request = OrdermatchRequest::BestOrders { coin: req.coin, action: req.action, @@ -123,6 +126,13 @@ pub async fn best_orders_rpc(ctx: MmArc, req: Json) -> Result>, log::warn!("Coin {} is not found in config", coin); continue; } + if is_wallet_only_conf(&coin_conf) { + log::warn!( + "Coin {} was removed from best orders because it's defined as wallet only in config", + coin + ); + continue; + } for order_w_proof in orders_w_proofs { let order = order_w_proof.order; let address = match address_by_coin_conf_and_pubkey_str(&coin, &coin_conf, &order.pubkey) { diff --git a/mm2src/lp_ordermatch/orderbook_depth.rs b/mm2src/lp_ordermatch/orderbook_depth.rs index f85b548d03..8a4ad1de7e 100644 --- a/mm2src/lp_ordermatch/orderbook_depth.rs +++ b/mm2src/lp_ordermatch/orderbook_depth.rs @@ -1,5 +1,6 @@ use super::{orderbook_topic_from_base_rel, OrdermatchContext, OrdermatchRequest}; use crate::mm2::lp_network::{request_any_relay, P2PRequest}; +use coins::is_wallet_only_ticker; use common::{log, mm_ctx::MmArc}; use http::Response; use serde_json::{self as json, Value as Json}; @@ -30,6 +31,17 @@ struct PairWithDepth { pub async fn orderbook_depth_rpc(ctx: MmArc, req: Json) -> Result>, String> { let ordermatch_ctx = OrdermatchContext::from_ctx(&ctx).unwrap(); let req: OrderbookDepthReq = try_s!(json::from_value(req)); + + let wallet_only_pairs: Vec<_> = req + .pairs + .iter() + .filter(|pair| is_wallet_only_ticker(&ctx, &pair.0) || is_wallet_only_ticker(&ctx, &pair.1)) + .collect(); + + if !wallet_only_pairs.is_empty() { + return ERR!("Pairs {:?} has wallet only coins", wallet_only_pairs); + } + let mut result = Vec::with_capacity(req.pairs.len()); let orderbook = ordermatch_ctx.orderbook.lock().await; diff --git a/mm2src/lp_ordermatch/orderbook_rpc.rs b/mm2src/lp_ordermatch/orderbook_rpc.rs index 4632b08024..847795dbbf 100644 --- a/mm2src/lp_ordermatch/orderbook_rpc.rs +++ b/mm2src/lp_ordermatch/orderbook_rpc.rs @@ -1,5 +1,5 @@ use super::{subscribe_to_orderbook_topic, OrdermatchContext, RpcOrderbookEntry}; -use coins::{address_by_coin_conf_and_pubkey_str, coin_conf}; +use coins::{address_by_coin_conf_and_pubkey_str, coin_conf, is_wallet_only_conf}; use common::{mm_ctx::MmArc, mm_number::MmNumber, now_ms}; use http::Response; use num_rational::BigRational; @@ -82,10 +82,16 @@ pub async fn orderbook_rpc(ctx: MmArc, req: Json) -> Result>, S if base_coin_conf.is_null() { return ERR!("Coin {} is not found in config", req.base); } + if is_wallet_only_conf(&base_coin_conf) { + return ERR!("Base Coin {} is wallet only", req.base); + } let rel_coin_conf = coin_conf(&ctx, &req.rel); if rel_coin_conf.is_null() { return ERR!("Coin {} is not found in config", req.rel); } + if is_wallet_only_conf(&rel_coin_conf) { + return ERR!("Base Coin {} is wallet only", req.rel); + } let request_orderbook = true; try_s!(subscribe_to_orderbook_topic(&ctx, &req.base, &req.rel, request_orderbook).await); let ordermatch_ctx = try_s!(OrdermatchContext::from_ctx(&ctx)); diff --git a/mm2src/lp_swap.rs b/mm2src/lp_swap.rs index 14215f7e4a..92e6637ff4 100644 --- a/mm2src/lp_swap.rs +++ b/mm2src/lp_swap.rs @@ -58,7 +58,7 @@ use crate::mm2::lp_network::broadcast_p2p_msg; use async_std::sync as async_std_sync; use bigdecimal::BigDecimal; -use coins::{lp_coinfind, MmCoinEnum, TradeFee, TradePreimageError, TransactionEnum}; +use coins::{is_wallet_only_ticker, lp_coinfind, MmCoinEnum, TradeFee, TradePreimageError, TransactionEnum}; use common::{bits256, block_on, calc_total_pages, executor::{spawn, Timer}, log::{error, info}, @@ -1235,6 +1235,12 @@ construct_detailed!(DetailedRequiredBalance, required_balance); pub async fn trade_preimage(ctx: MmArc, req: Json) -> Result>, String> { let req: TradePreimageRequest = try_s!(json::from_value(req)); + if is_wallet_only_ticker(&ctx, &req.base) { + return ERR!("Base Coin {} is wallet only", req.base); + } + if is_wallet_only_ticker(&ctx, &req.rel) { + return ERR!("Rel Coin {} is wallet only", req.rel); + } let result: TradePreimageResponse = match req.swap_method { TradePreimageMethod::SetPrice => try_s!(maker_swap_trade_preimage(&ctx, req).await).into(), TradePreimageMethod::Buy | TradePreimageMethod::Sell => { diff --git a/mm2src/mm2_tests.rs b/mm2src/mm2_tests.rs index c00d371a7c..3a0a7cfdc3 100644 --- a/mm2src/mm2_tests.rs +++ b/mm2src/mm2_tests.rs @@ -5597,6 +5597,123 @@ fn test_best_orders() { block_on(mm_alice.stop()).unwrap(); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_best_orders_filter_response() { + let bob_passphrase = get_passphrase(&".env.seed", "BOB_PASSPHRASE").unwrap(); + + let bob_coins_config = 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"},"rpcport":80}, + {"coin":"JST","name":"jst","protocol":{"type":"ERC20", "protocol_data":{"platform":"ETH","contract_address":"0x2b294F029Fde858b2c62184e8390591755521d8E"}}} + ]); + + // alice defined MORTY as "wallet_only" in config + let alice_coins_config = json!([ + {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, + {"coin":"MORTY","asset":"MORTY","rpcport":11608,"wallet_only": true,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, + {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH"},"rpcport":80}, + {"coin":"JST","name":"jst","protocol":{"type":"ERC20", "protocol_data":{"platform":"ETH","contract_address":"0x2b294F029Fde858b2c62184e8390591755521d8E"}}} + ]); + + // start bob and immediately place the orders + let mut mm_bob = 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": bob_coins_config, + "rpc_password": "pass", + "i_am_seed": true, + }), + "pass".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()}); + + // Enable coins on Bob side. Print the replies in case we need the "address". + let bob_coins = block_on(enable_coins_eth_electrum(&mm_bob, &["http://195.201.0.6:8565"])); + log!({ "enable_coins (bob): {:?}", bob_coins }); + // issue sell request on Bob side by setting base/rel price + log!("Issue bob sell requests"); + + let bob_orders = [ + // (base, rel, price, volume, min_volume) + ("RICK", "MORTY", "0.9", "0.9", None), + ("RICK", "MORTY", "0.8", "0.9", None), + ("RICK", "MORTY", "0.7", "0.9", Some("0.9")), + ("RICK", "ETH", "0.8", "0.9", None), + ("MORTY", "RICK", "0.8", "0.9", None), + ("MORTY", "RICK", "0.9", "0.9", None), + ("ETH", "RICK", "0.8", "0.9", None), + ("MORTY", "ETH", "0.8", "0.8", None), + ("MORTY", "ETH", "0.7", "0.8", Some("0.8")), + ]; + for (base, rel, price, volume, min_volume) in bob_orders.iter() { + let rc = block_on(mm_bob.rpc(json! ({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": base, + "rel": rel, + "price": price, + "volume": volume, + "min_volume": min_volume.unwrap_or("0.00777"), + "cancel_previous": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + } + + let mm_alice = MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 9998, + "myipaddr": env::var ("ALICE_TRADE_IP") .ok(), + "rpcip": env::var ("ALICE_TRADE_IP") .ok(), + "passphrase": "alice passphrase", + "coins": alice_coins_config, + "seednodes": [fomat!((mm_bob.ip))], + "rpc_password": "pass", + }), + "pass".into(), + local_start!("alice"), + ) + .unwrap(); + + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!({ "Alice log path: {}", mm_alice.log_path.display() }); + + block_on(mm_bob.wait_for_log(22., |log| { + log.contains("DEBUG Handling IncludedTorelaysMesh message for peer") + })) + .unwrap(); + + let rc = block_on(mm_alice.rpc(json! ({ + "userpass": mm_alice.userpass, + "method": "best_orders", + "coin": "RICK", + "action": "buy", + "volume": "0.1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!best_orders: {}", rc.1); + let response: BestOrdersResponse = json::from_str(&rc.1).unwrap(); + let empty_vec = Vec::new(); + let best_morty_orders = response.result.get("MORTY").unwrap_or(&empty_vec); + assert_eq!(0, best_morty_orders.len()); + let best_eth_orders = response.result.get("ETH").unwrap(); + assert_eq!(1, best_eth_orders.len()); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + fn request_and_check_orderbook_depth(mm_alice: &MarketMakerIt) { let rc = block_on(mm_alice.rpc(json! ({ "userpass": mm_alice.userpass,