From 7173ae2d5b8229724e3ab2600b10013819efca52 Mon Sep 17 00:00:00 2001 From: Artem Vitae Date: Tue, 14 Sep 2021 18:25:42 +0700 Subject: [PATCH] Support Segwit addresses in orderbooks #826 (#1064) * WIP. * WIP. * WIP. * WIP. Some tests fail. * Fix tests and add more. * Fix WASM tests and clippy. * Review fixes. Return Option<_> from order_getter to avoid unwrap/panic. --- mm2src/coins/eth.rs | 2 +- mm2src/coins/lp_coins.rs | 2 +- mm2src/coins/qrc20.rs | 2 +- mm2src/coins/test_coin.rs | 2 +- mm2src/coins/utxo/qtum.rs | 2 +- mm2src/coins/utxo/slp.rs | 2 +- mm2src/coins/utxo/utxo_common.rs | 4 +- mm2src/coins/utxo/utxo_standard.rs | 2 +- mm2src/coins/utxo/utxo_tests.rs | 21 +- mm2src/lp_ordermatch.rs | 440 +++++++++++++++++++------- mm2src/lp_ordermatch/best_orders.rs | 118 ++++++- mm2src/lp_ordermatch/new_protocol.rs | 48 ++- mm2src/lp_ordermatch/orderbook_rpc.rs | 11 +- mm2src/mm2_tests.rs | 345 ++++++++++++++++++-- mm2src/ordermatch_tests.rs | 260 ++++++++++++++- 15 files changed, 1080 insertions(+), 181 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 82b3df3357..c93fca5de9 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -2963,7 +2963,7 @@ impl MmCoin for EthCoin { fn mature_confirmations(&self) -> Option { None } - fn coin_protocol_info(&self) -> Option> { None } + fn coin_protocol_info(&self) -> Vec { Vec::new() } fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 48921e5ff2..93708a02b7 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -887,7 +887,7 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { fn mature_confirmations(&self) -> Option; /// Get some of the coin config info in serialized format for p2p messaging. - fn coin_protocol_info(&self) -> Option>; + fn coin_protocol_info(&self) -> Vec; /// Check if serialized coin protocol info is supported by current version. fn is_coin_protocol_supported(&self, info: &Option>) -> bool; diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index c4ceefccf7..c4d0b87059 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -1163,7 +1163,7 @@ impl MmCoin for Qrc20Coin { fn mature_confirmations(&self) -> Option { Some(self.utxo.conf.mature_confirmations) } - fn coin_protocol_info(&self) -> Option> { utxo_common::coin_protocol_info(&self.utxo) } + fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(&self.utxo) } fn is_coin_protocol_supported(&self, info: &Option>) -> bool { utxo_common::is_coin_protocol_supported(&self.utxo, info) diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index cc9eba6b51..7562b00a13 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -268,7 +268,7 @@ impl MmCoin for TestCoin { fn mature_confirmations(&self) -> Option { unimplemented!() } - fn coin_protocol_info(&self) -> Option> { None } + fn coin_protocol_info(&self) -> Vec { Vec::new() } fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } } diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index b9240f8e7e..266b97040d 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -635,7 +635,7 @@ impl MmCoin for QtumCoin { fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } - fn coin_protocol_info(&self) -> Option> { utxo_common::coin_protocol_info(&self.utxo_arc) } + fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(&self.utxo_arc) } fn is_coin_protocol_supported(&self, info: &Option>) -> bool { utxo_common::is_coin_protocol_supported(&self.utxo_arc, info) diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 2e0497795a..a7769e7557 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -1216,7 +1216,7 @@ impl MmCoin for SlpToken { fn mature_confirmations(&self) -> Option { self.platform_utxo.mature_confirmations() } - fn coin_protocol_info(&self) -> Option> { unimplemented!() } + fn coin_protocol_info(&self) -> Vec { unimplemented!() } fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { unimplemented!() } } diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 4f11e3b9f5..e3b9f52926 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2314,8 +2314,8 @@ pub fn set_requires_notarization(coin: &UtxoCoinFields, requires_nota: bool) { .store(requires_nota, AtomicOrderding::Relaxed); } -pub fn coin_protocol_info(coin: &UtxoCoinFields) -> Option> { - Some(rmp_serde::to_vec(&coin.my_address.addr_format).unwrap()) +pub fn coin_protocol_info(coin: &UtxoCoinFields) -> Vec { + rmp_serde::to_vec(&coin.my_address.addr_format).expect("Serialization should not fail") } pub fn is_coin_protocol_supported(coin: &UtxoCoinFields, info: &Option>) -> bool { diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index e84a6c8488..f1494537a5 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -499,7 +499,7 @@ impl MmCoin for UtxoStandardCoin { fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } - fn coin_protocol_info(&self) -> Option> { utxo_common::coin_protocol_info(&self.utxo_arc) } + fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(&self.utxo_arc) } fn is_coin_protocol_supported(&self, info: &Option>) -> bool { utxo_common::is_coin_protocol_supported(&self.utxo_arc, info) diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 66419896e4..d7ed27d50e 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -19,6 +19,11 @@ use serialization::{deserialize, CoinVariant}; const TEST_COIN_NAME: &'static str = "RICK"; // Made-up hrp for rick to test p2wpkh script const TEST_COIN_HRP: &'static str = "rck"; +const RICK_ELECTRUM_ADDRS: &[&'static str] = &[ + "electrum1.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum3.cipig.net:10017", +]; pub fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { let ctx = MmCtxBuilder::default().into_mm_arc(); @@ -135,7 +140,7 @@ fn utxo_coin_for_test( #[test] fn test_extract_secret() { - let client = electrum_client_for_test(&["electrum1.cipig.net:10017"]); + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); let coin = utxo_coin_for_test(client.into(), None, false); let tx_hex = hex::decode("0100000001de7aa8d29524906b2b54ee2e0281f3607f75662cbc9080df81d1047b78e21dbc00000000d7473044022079b6c50820040b1fbbe9251ced32ab334d33830f6f8d0bf0a40c7f1336b67d5b0220142ccf723ddabb34e542ed65c395abc1fbf5b6c3e730396f15d25c49b668a1a401209da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365004c6b6304f62b0e5cb175210270e75970bb20029b3879ec76c4acd320a8d0589e003636264d01a7d566504bfbac6782012088a9142fb610d856c19fd57f2d0cffe8dff689074b3d8a882103f368228456c940ac113e53dad5c104cf209f2f102a409207269383b6ab9b03deac68ffffffff01d0dc9800000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac40280e5c").unwrap(); @@ -147,7 +152,7 @@ fn test_extract_secret() { #[test] fn test_generate_transaction() { - let client = electrum_client_for_test(&["electrum1.cipig.net:10017"]); + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); let coin = utxo_coin_for_test(client.into(), None, false); let unspents = vec![UnspentInfo { value: 10000000000, @@ -226,7 +231,7 @@ fn test_generate_transaction() { #[test] fn test_addresses_from_script() { - let client = electrum_client_for_test(&["electrum1.cipig.net:10017", "electrum2.cipig.net:10017"]); + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); let coin = utxo_coin_for_test(client.into(), None, false); // P2PKH let script: Script = "76a91405aab5342166f8594baf17a7d9bef5d56744332788ac".into(); @@ -395,7 +400,7 @@ fn test_wait_for_payment_spend_timeout_electrum() { #[test] fn test_search_for_swap_tx_spend_electrum_was_spent() { let secret = [0; 32]; - let client = electrum_client_for_test(&["electrum1.cipig.net:10017", "electrum2.cipig.net:10017"]); + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); let coin = utxo_coin_for_test( client.into(), Some("spice describe gravity federal blast come thank unfair canal monkey style afraid"), @@ -428,7 +433,7 @@ fn test_search_for_swap_tx_spend_electrum_was_spent() { #[test] fn test_search_for_swap_tx_spend_electrum_was_refunded() { let secret = [0; 20]; - let client = electrum_client_for_test(&["electrum1.cipig.net:10017", "electrum2.cipig.net:10017"]); + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); let coin = utxo_coin_for_test( client.into(), Some("spice describe gravity federal blast come thank unfair canal monkey style afraid"), @@ -827,7 +832,7 @@ fn test_withdraw_rick_rewards_none() { #[test] fn test_ordered_mature_unspents_without_tx_cache() { - let client = electrum_client_for_test(&["electrum1.cipig.net:10017", "electrum2.cipig.net:10017"]); + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); let coin = utxo_coin_for_test( client.into(), Some("spice describe gravity federal blast come thank unfair canal monkey style afraid"), @@ -847,7 +852,7 @@ fn test_ordered_mature_unspents_without_tx_cache() { #[test] fn test_utxo_lock() { // send several transactions concurrently to check that they are not using same inputs - let client = electrum_client_for_test(&["electrum1.cipig.net:10017", "electrum2.cipig.net:10017"]); + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); let coin = utxo_coin_for_test(client.into(), None, false); let output = TransactionOutput { value: 1000000, @@ -1694,7 +1699,7 @@ fn test_ordered_mature_unspents_from_cache_impl( ) { const TX_HASH: &str = "0a0fda88364b960000f445351fe7678317a1e0c80584de0413377ede00ba696f"; let tx_hash: H256Json = hex::decode(TX_HASH).unwrap().as_slice().into(); - let client = electrum_client_for_test(&["electrum1.cipig.net:10017"]); + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); let mut verbose = client.get_verbose_transaction(tx_hash.clone()).wait().unwrap(); verbose.confirmations = cached_confs; verbose.height = cached_height; diff --git a/mm2src/lp_ordermatch.rs b/mm2src/lp_ordermatch.rs index cb1818e9bc..e99be5098d 100644 --- a/mm2src/lp_ordermatch.rs +++ b/mm2src/lp_ordermatch.rs @@ -76,7 +76,7 @@ mod order_requests_tracker; #[path = "lp_ordermatch/orderbook_rpc.rs"] mod orderbook_rpc; #[cfg(all(test, not(target_arch = "wasm32")))] #[path = "ordermatch_tests.rs"] -mod ordermatch_tests; +pub mod ordermatch_tests; pub const ORDERBOOK_PREFIX: TopicPrefix = "orbk"; const MIN_ORDER_KEEP_ALIVE_INTERVAL: u64 = 30; @@ -96,7 +96,7 @@ const TRIE_ORDER_HISTORY_TIMEOUT: u64 = 3; /// Alphabetically ordered orderbook pair type AlbOrderedOrderbookPair = String; -type PubkeyOrders = Vec<(Uuid, OrderbookItem)>; +type PubkeyOrders = Vec<(Uuid, OrderbookP2PItem)>; impl From<(new_protocol::MakerOrderCreated, String)> for OrderbookItem { fn from(tuple: (new_protocol::MakerOrderCreated, String)) -> OrderbookItem { @@ -111,18 +111,16 @@ impl From<(new_protocol::MakerOrderCreated, String)> for OrderbookItem { min_volume: order.min_volume, uuid: order.uuid.into(), created_at: order.created_at, + base_protocol_info: order.base_protocol_info, + rel_protocol_info: order.rel_protocol_info, } } } -#[allow(dead_code)] -pub fn addr_format_from_protocol_info(protocol_info: &Option>) -> AddressFormat { - match protocol_info { - Some(info) => match rmp_serde::from_read_ref::<_, AddressFormat>(info) { - Ok(format) => format, - Err(_) => AddressFormat::Standard, - }, - None => AddressFormat::Standard, +pub fn addr_format_from_protocol_info(protocol_info: &[u8]) -> AddressFormat { + match rmp_serde::from_read_ref::<_, AddressFormat>(protocol_info) { + Ok(format) => format, + Err(_) => AddressFormat::Standard, } } @@ -131,11 +129,15 @@ fn process_pubkey_full_trie( pubkey: &str, alb_pair: &str, new_trie_orders: PubkeyOrders, + protocol_infos: &HashMap, ) -> H64 { remove_pubkey_pair_orders(orderbook, pubkey, alb_pair); - for (_uuid, order) in new_trie_orders { - orderbook.insert_or_update_order_update_trie(order); + for (uuid, order) in new_trie_orders { + orderbook.insert_or_update_order_update_trie(OrderbookItem::from_p2p_and_proto_info( + order, + protocol_infos.get(&uuid).cloned().unwrap_or_default(), + )); } let new_root = pubkey_state_mut(&mut orderbook.pubkeys_state, pubkey) @@ -150,11 +152,15 @@ fn process_trie_delta( orderbook: &mut Orderbook, pubkey: &str, alb_pair: &str, - delta_orders: HashMap>, + delta_orders: HashMap>, + protocol_infos: &HashMap, ) -> H64 { for (uuid, order) in delta_orders { match order { - Some(order) => orderbook.insert_or_update_order_update_trie(order), + Some(order) => orderbook.insert_or_update_order_update_trie(OrderbookItem::from_p2p_and_proto_info( + order, + protocol_infos.get(&uuid).cloned().unwrap_or_default(), + )), None => { orderbook.remove_order_trie_update(uuid); }, @@ -204,8 +210,12 @@ async fn process_orders_keep_alive( let mut orderbook = ordermatch_ctx.orderbook.lock().await; for (pair, diff) in response.pair_orders_diff { let _new_root = match diff { - DeltaOrFullTrie::Delta(delta) => process_trie_delta(&mut orderbook, &from_pubkey, &pair, delta), - DeltaOrFullTrie::FullTrie(values) => process_pubkey_full_trie(&mut orderbook, &from_pubkey, &pair, values), + DeltaOrFullTrie::Delta(delta) => { + process_trie_delta(&mut orderbook, &from_pubkey, &pair, delta, &response.protocol_infos) + }, + DeltaOrFullTrie::FullTrie(values) => { + process_pubkey_full_trie(&mut orderbook, &from_pubkey, &pair, values, &response.protocol_infos) + }, }; } true @@ -263,8 +273,14 @@ async fn request_and_fill_orderbook(ctx: &MmArc, base: &str, rel: &str) -> Resul }; let response = try_s!(request_any_relay::(ctx.clone(), P2PRequest::Ordermatch(request)).await); - let pubkey_orders = match response { - Some((GetOrderbookRes { pubkey_orders }, _peer_id)) => pubkey_orders, + let (pubkey_orders, protocol_infos) = match response { + Some(( + GetOrderbookRes { + pubkey_orders, + protocol_infos, + }, + _peer_id, + )) => (pubkey_orders, protocol_infos), None => return Ok(()), }; @@ -284,7 +300,7 @@ async fn request_and_fill_orderbook(ctx: &MmArc, base: &str, rel: &str) -> Resul log::warn!("Pubkey {} is banned", pubkey); continue; } - let _new_root = process_pubkey_full_trie(&mut orderbook, &pubkey, &alb_pair, orders); + let _new_root = process_pubkey_full_trie(&mut orderbook, &pubkey, &alb_pair, orders, &protocol_infos); } let topic = orderbook_topic_from_base_rel(base, rel); @@ -447,7 +463,7 @@ impl TryFromBytes for String { } } -impl TryFromBytes for OrderbookItem { +impl TryFromBytes for OrderbookP2PItem { fn try_from_bytes(bytes: Vec) -> Result { rmp_serde::from_read(bytes.as_slice()).map_err(|e| ERRL!("{}", e).into()) } @@ -494,41 +510,56 @@ struct GetOrderbookPubkeyItem { orders: PubkeyOrders, } +/// Do not change this struct as it will break backward compatibility +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +struct BaseRelProtocolInfo { + base: Vec, + rel: Vec, +} + #[derive(Debug, Deserialize, Serialize)] struct GetOrderbookRes { /// Asks and bids grouped by pubkey. pubkey_orders: HashMap, + #[serde(default)] + protocol_infos: HashMap, } -async fn process_get_orderbook_request(ctx: MmArc, base: String, rel: String) -> Result>, String> { - fn get_pubkeys_orders(orderbook: &Orderbook, base: String, rel: String) -> (usize, HashMap) { - let asks = orderbook.unordered.get(&(base.clone(), rel.clone())); - let bids = orderbook.unordered.get(&(rel, base)); - - let asks_num = asks.map(|x| x.len()).unwrap_or(0); - let bids_num = bids.map(|x| x.len()).unwrap_or(0); - let total_orders_number = asks_num + bids_num; - - // flatten Option(asks) and Option(bids) to avoid cloning - let orders = asks.iter().chain(bids.iter()).copied().flatten(); - - let mut uuids_by_pubkey = HashMap::new(); - for uuid in orders { - let order = orderbook - .order_set - .get(uuid) - .expect("Orderbook::ordered contains an uuid that is not in Orderbook::order_set"); - let uuids = uuids_by_pubkey.entry(order.pubkey.clone()).or_insert_with(Vec::new); - uuids.push((*uuid, order.clone())) - } +fn get_pubkeys_orders( + orderbook: &Orderbook, + base: String, + rel: String, +) -> (usize, HashMap, HashMap) { + let asks = orderbook.unordered.get(&(base.clone(), rel.clone())); + let bids = orderbook.unordered.get(&(rel, base)); + + let asks_num = asks.map(|x| x.len()).unwrap_or(0); + let bids_num = bids.map(|x| x.len()).unwrap_or(0); + let total_orders_number = asks_num + bids_num; + + // flatten Option(asks) and Option(bids) to avoid cloning + let orders = asks.iter().chain(bids.iter()).copied().flatten(); - (total_orders_number, uuids_by_pubkey) + let mut uuids_by_pubkey = HashMap::new(); + let mut protocol_infos = HashMap::new(); + for uuid in orders { + let order = orderbook + .order_set + .get(uuid) + .expect("Orderbook::ordered contains an uuid that is not in Orderbook::order_set"); + let uuids = uuids_by_pubkey.entry(order.pubkey.clone()).or_insert_with(Vec::new); + protocol_infos.insert(order.uuid, order.base_rel_proto_info()); + uuids.push((*uuid, order.clone().into())) } + (total_orders_number, uuids_by_pubkey, protocol_infos) +} + +async fn process_get_orderbook_request(ctx: MmArc, base: String, rel: String) -> Result>, String> { let ordermatch_ctx = OrdermatchContext::from_ctx(&ctx).unwrap(); let orderbook = ordermatch_ctx.orderbook.lock().await; - let (total_orders_number, orders) = get_pubkeys_orders(&orderbook, base, rel); + let (total_orders_number, orders, protocol_infos) = get_pubkeys_orders(&orderbook, base, rel); if total_orders_number > MAX_ORDERS_NUMBER_IN_ORDERBOOK_RESPONSE { return ERR!("Orderbook too large"); } @@ -553,7 +584,10 @@ async fn process_get_orderbook_request(ctx: MmArc, base: String, rel: String) -> .collect(); let pubkey_orders = orders_to_send?; - let response = GetOrderbookRes { pubkey_orders }; + let response = GetOrderbookRes { + pubkey_orders, + protocol_infos, + }; let encoded = try_s!(encode_message(&response)); Ok(Some(encoded)) } @@ -564,10 +598,31 @@ enum DeltaOrFullTrie { FullTrie(Vec<(Key, Value)>), } +impl DeltaOrFullTrie { + pub fn map_to>(self, mut on_each: impl FnMut(&Key, Option<&V1>)) -> DeltaOrFullTrie { + match self { + DeltaOrFullTrie::Delta(delta) => { + delta.iter().for_each(|(key, val)| on_each(key, val.as_ref())); + let new_map = delta + .into_iter() + .map(|(key, value)| (key, value.map(From::from))) + .collect(); + DeltaOrFullTrie::Delta(new_map) + }, + DeltaOrFullTrie::FullTrie(trie) => { + trie.iter().for_each(|(key, val)| on_each(key, Some(val))); + let new_trie = trie.into_iter().map(|(key, value)| (key, value.into())).collect(); + DeltaOrFullTrie::FullTrie(new_trie) + }, + } + } +} + #[derive(Debug)] enum TrieDiffHistoryError { TrieDbError(Box>), TryFromBytesError(TryFromBytesError), + GetterNoneForKeyFromTrie, } impl std::fmt::Display for TrieDiffHistoryError { @@ -587,28 +642,31 @@ impl From>> for TrieDiffHistoryError fn get_full_trie( trie_root: &H64, db: &MemoryDB, + getter: impl Fn(&Key) -> Option, ) -> Result, TrieDiffHistoryError> where Key: Clone + Eq + std::hash::Hash + TryFromBytes, - Value: Clone + TryFromBytes, { let trie = TrieDB::::new(db, trie_root)?; let trie: Result, TrieDiffHistoryError> = trie .iter()? .map(|key_value| { - let (key, value) = key_value?; - Ok((TryFromBytes::try_from_bytes(key)?, TryFromBytes::try_from_bytes(value)?)) + let (key, _) = key_value?; + let key = TryFromBytes::try_from_bytes(key)?; + let val = getter(&key).ok_or(TrieDiffHistoryError::GetterNoneForKeyFromTrie)?; + Ok((key, val)) }) .collect(); trie } -impl DeltaOrFullTrie { +impl DeltaOrFullTrie { fn from_history( history: &TrieDiffHistory, from_hash: H64, actual_trie_root: H64, db: &MemoryDB, + getter: impl Fn(&Key) -> Option, ) -> Result, TrieDiffHistoryError> { if let Some(delta) = history.get(&from_hash) { let mut current_delta = delta; @@ -629,7 +687,7 @@ impl, - pair_orders_diff: HashMap>, + pair_orders_diff: HashMap>, + #[serde(default)] + protocol_infos: HashMap, } async fn process_sync_pubkey_orderbook_state( @@ -653,7 +713,8 @@ async fn process_sync_pubkey_orderbook_state( None => return Ok(None), }; - let pair_orders_diff: Result<_, _> = trie_roots + let order_getter = |uuid: &Uuid| orderbook.order_set.get(uuid).cloned(); + let pair_orders_diff: Result, _> = trie_roots .into_iter() .map(|(pair, root)| { let actual_pair_root = pubkey_state @@ -662,8 +723,12 @@ async fn process_sync_pubkey_orderbook_state( .ok_or(ERRL!("No pair trie root for {}", pair))?; let delta_result = match pubkey_state.order_pairs_trie_state_history.get(&pair) { - Some(history) => DeltaOrFullTrie::from_history(history, root, *actual_pair_root, &orderbook.memory_db), - None => get_full_trie(actual_pair_root, &orderbook.memory_db).map(DeltaOrFullTrie::FullTrie), + Some(history) => { + DeltaOrFullTrie::from_history(history, root, *actual_pair_root, &orderbook.memory_db, &order_getter) + }, + None => { + get_full_trie(actual_pair_root, &orderbook.memory_db, &order_getter).map(DeltaOrFullTrie::FullTrie) + }, }; let delta = try_s!(delta_result); @@ -672,10 +737,29 @@ async fn process_sync_pubkey_orderbook_state( .collect(); let pair_orders_diff = try_s!(pair_orders_diff); + let mut protocol_infos = HashMap::new(); + let pair_orders_diff = pair_orders_diff + .into_iter() + .map(|(pair, trie)| { + let new_trie = trie.map_to(|uuid, order| match order { + Some(o) => { + protocol_infos.insert(o.uuid, BaseRelProtocolInfo { + base: o.base_protocol_info.clone(), + rel: o.rel_protocol_info.clone(), + }); + }, + None => { + protocol_infos.remove(uuid); + }, + }); + (pair, new_trie) + }) + .collect(); let last_signed_pubkey_payload = vec![]; let result = SyncPubkeyOrderbookStateRes { last_signed_pubkey_payload, pair_orders_diff, + protocol_infos, }; Ok(Some(result)) } @@ -731,7 +815,12 @@ fn test_parse_orderbook_pair_from_topic() { assert_eq!(None, parse_orderbook_pair_from_topic("orbk/BTC:")); } -async fn maker_order_created_p2p_notify(ctx: MmArc, order: &MakerOrder) { +async fn maker_order_created_p2p_notify( + ctx: MmArc, + order: &MakerOrder, + base_protocol_info: Vec, + rel_protocol_info: Vec, +) { let topic = orderbook_topic_from_base_rel(&order.base, &order.rel); let message = new_protocol::MakerOrderCreated { uuid: order.uuid.into(), @@ -744,6 +833,8 @@ async fn maker_order_created_p2p_notify(ctx: MmArc, order: &MakerOrder) { created_at: now_ms() / 1000, timestamp: now_ms() / 1000, pair_trie_root: H64::default(), + base_protocol_info, + rel_protocol_info, }; let key_pair = ctx.secp256k1_key_pair.or(&&|| panic!()); @@ -1157,8 +1248,8 @@ impl<'a> TakerOrderBuilder<'a> { dest_pub_key: Default::default(), match_by: self.match_by, conf_settings: self.conf_settings, - base_protocol_info: self.base_coin.coin_protocol_info(), - rel_protocol_info: self.rel_coin.coin_protocol_info(), + base_protocol_info: Some(self.base_coin.coin_protocol_info()), + rel_protocol_info: Some(self.rel_coin.coin_protocol_info()), }, matches: Default::default(), min_volume, @@ -1184,8 +1275,8 @@ impl<'a> TakerOrderBuilder<'a> { dest_pub_key: Default::default(), match_by: self.match_by, conf_settings: self.conf_settings, - base_protocol_info: self.base_coin.coin_protocol_info(), - rel_protocol_info: self.rel_coin.coin_protocol_info(), + base_protocol_info: Some(self.base_coin.coin_protocol_info()), + rel_protocol_info: Some(self.rel_coin.coin_protocol_info()), }, matches: HashMap::new(), min_volume: Default::default(), @@ -2084,7 +2175,7 @@ impl Orderbook { return; }, }; - let order_bytes = rmp_serde::to_vec(&order).expect("Serialization should never fail"); + let order_bytes = order.trie_state_bytes(); if let Err(e) = pair_trie.insert(order.uuid.as_bytes(), &order_bytes) { log::error!( "Error {:?} on insertion to trie. Key {}, value {:?}", @@ -2487,9 +2578,16 @@ pub async fn lp_ordermatch_loop(ctx: MmArc) { spawn({ let ctx = ctx.clone(); async move { - if let Ok(Some((_, _))) = find_pair(&ctx, &maker_order.base, &maker_order.rel).await + if let Ok(Some((base_coin, rel_coin))) = + find_pair(&ctx, &maker_order.base, &maker_order.rel).await { - maker_order_created_p2p_notify(ctx, &maker_order).await; + maker_order_created_p2p_notify( + ctx, + &maker_order, + base_coin.coin_protocol_info(), + rel_coin.coin_protocol_info(), + ) + .await; } } }); @@ -2580,7 +2678,13 @@ pub async fn lp_ordermatch_loop(ctx: MmArc) { log::error!("Error {} on subscribing to orderbook topic {}", e, topic); } } - maker_order_created_p2p_notify(ctx.clone(), order).await; + maker_order_created_p2p_notify( + ctx.clone(), + order, + base.coin_protocol_info(), + rel.coin_protocol_info(), + ) + .await; } } } @@ -2736,8 +2840,8 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: rel_nota: rel_coin.requires_notarization(), }) }), - base_protocol_info: base_coin.coin_protocol_info(), - rel_protocol_info: rel_coin.coin_protocol_info(), + base_protocol_info: Some(base_coin.coin_protocol_info()), + rel_protocol_info: Some(rel_coin.coin_protocol_info()), }; let topic = orderbook_topic_from_base_rel(&order.base, &order.rel); log::debug!("Request matched sending reserved {:?}", reserved); @@ -3025,8 +3129,10 @@ pub async fn lp_auto_buy( Ok(result.to_string()) } +/// Orderbook Item P2P message +/// DO NOT CHANGE - it will break backwards compatibility #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -struct OrderbookItem { +struct OrderbookP2PItem { pubkey: String, base: String, rel: String, @@ -3037,51 +3143,7 @@ struct OrderbookItem { created_at: u64, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -struct OrderbookItemWithProof { - /// Orderbook item - order: OrderbookItem, - /// Last pubkey message payload that contains most recent pair trie root - last_message_payload: Vec, - /// Proof confirming that orderbook item is in the pair trie - proof: TrieProof, -} - -/// Concrete implementation of Hasher using Blake2b 64-bit hashes -#[derive(Debug)] -pub struct Blake2Hasher64; - -impl Hasher for Blake2Hasher64 { - type Out = [u8; 8]; - type StdHasher = Hash256StdHasher; - const LENGTH: usize = 8; - - fn hash(x: &[u8]) -> Self::Out { - let mut hasher = VarBlake2b::new(8).expect("8 is valid VarBlake2b output_size"); - hasher.update(x); - let mut res: [u8; 8] = Default::default(); - hasher.finalize_variable(|hash| res.copy_from_slice(hash)); - res - } -} - -type Layout = sp_trie::Layout; - -impl OrderbookItem { - fn apply_updated(&mut self, msg: &new_protocol::MakerOrderUpdated) { - if let Some(new_price) = msg.new_price() { - self.price = new_price.into(); - } - - if let Some(new_max_volume) = msg.new_max_volume() { - self.max_volume = new_max_volume.into(); - } - - if let Some(new_min_volume) = msg.new_min_volume() { - self.min_volume = new_min_volume.into(); - } - } - +impl OrderbookP2PItem { fn as_rpc_best_orders_buy(&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()); @@ -3149,6 +3211,105 @@ impl OrderbookItem { rel_min_volume, } } +} + +/// Despite it looks the same as OrderbookItemWithProof it's better to have a separate struct to avoid compatibility +/// breakage if we need to add more fields to the OrderbookItemWithProof +/// DO NOT ADD more fields in this struct as it will break backward compatibility. +/// Add them to the BestOrdersRes instead +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +struct OrderbookP2PItemWithProof { + /// Orderbook item + order: OrderbookP2PItem, + /// Last pubkey message payload that contains most recent pair trie root + last_message_payload: Vec, + /// Proof confirming that orderbook item is in the pair trie + proof: TrieProof, +} + +impl From for OrderbookP2PItemWithProof { + fn from(o: OrderbookItemWithProof) -> Self { + OrderbookP2PItemWithProof { + order: o.order.into(), + last_message_payload: o.last_message_payload, + proof: o.proof, + } + } +} + +impl From for OrderbookP2PItem { + fn from(o: OrderbookItem) -> OrderbookP2PItem { + OrderbookP2PItem { + pubkey: o.pubkey, + base: o.base, + rel: o.rel, + price: o.price, + max_volume: o.max_volume, + min_volume: o.min_volume, + uuid: o.uuid, + created_at: o.created_at, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +struct OrderbookItem { + pubkey: String, + base: String, + rel: String, + price: BigRational, + max_volume: BigRational, + min_volume: BigRational, + uuid: Uuid, + created_at: u64, + base_protocol_info: Vec, + rel_protocol_info: Vec, +} + +#[derive(Clone, Debug, PartialEq)] +struct OrderbookItemWithProof { + /// Orderbook item + order: OrderbookItem, + /// Last pubkey message payload that contains most recent pair trie root + last_message_payload: Vec, + /// Proof confirming that orderbook item is in the pair trie + proof: TrieProof, +} + +/// Concrete implementation of Hasher using Blake2b 64-bit hashes +#[derive(Debug)] +pub struct Blake2Hasher64; + +impl Hasher for Blake2Hasher64 { + type Out = [u8; 8]; + type StdHasher = Hash256StdHasher; + const LENGTH: usize = 8; + + fn hash(x: &[u8]) -> Self::Out { + let mut hasher = VarBlake2b::new(8).expect("8 is valid VarBlake2b output_size"); + hasher.update(x); + let mut res: [u8; 8] = Default::default(); + hasher.finalize_variable(|hash| res.copy_from_slice(hash)); + res + } +} + +type Layout = sp_trie::Layout; + +impl OrderbookItem { + fn apply_updated(&mut self, msg: &new_protocol::MakerOrderUpdated) { + if let Some(new_price) = msg.new_price() { + self.price = new_price.into(); + } + + if let Some(new_max_volume) = msg.new_max_volume() { + self.max_volume = new_max_volume.into(); + } + + if let Some(new_min_volume) = msg.new_min_volume() { + 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()); @@ -3217,6 +3378,57 @@ impl OrderbookItem { rel_min_volume, } } + + fn from_p2p_and_proto_info(o: OrderbookP2PItem, info: BaseRelProtocolInfo) -> Self { + OrderbookItem { + pubkey: o.pubkey, + base: o.base, + rel: o.rel, + price: o.price, + max_volume: o.max_volume, + min_volume: o.min_volume, + uuid: o.uuid, + created_at: o.created_at, + base_protocol_info: info.base, + rel_protocol_info: info.rel, + } + } + + fn base_rel_proto_info(&self) -> BaseRelProtocolInfo { + BaseRelProtocolInfo { + base: self.base_protocol_info.clone(), + rel: self.rel_protocol_info.clone(), + } + } + + /// Serialize order partially to store in the trie + /// AVOID CHANGING THIS as much as possible because it will cause a kind of "hard fork" + fn trie_state_bytes(&self) -> Vec { + #[derive(Serialize)] + struct OrderbookItemHelper<'a> { + pubkey: &'a str, + base: &'a str, + rel: &'a str, + price: &'a BigRational, + max_volume: &'a BigRational, + min_volume: &'a BigRational, + uuid: &'a Uuid, + created_at: &'a u64, + } + + let helper = OrderbookItemHelper { + pubkey: &self.pubkey, + base: &self.base, + rel: &self.rel, + price: &self.price, + max_volume: &self.max_volume, + min_volume: &self.min_volume, + uuid: &self.uuid, + created_at: &self.created_at, + }; + + rmp_serde::to_vec(&helper).expect("Serialization should never fail") + } } fn get_true() -> bool { true } @@ -3535,7 +3747,13 @@ pub async fn set_price(ctx: MmArc, req: Json) -> Result>, Strin let request_orderbook = false; try_s!(subscribe_to_orderbook_topic(&ctx, &new_order.base, &new_order.rel, request_orderbook).await); save_my_new_maker_order(&ctx, &new_order); - maker_order_created_p2p_notify(ctx.clone(), &new_order).await; + maker_order_created_p2p_notify( + ctx.clone(), + &new_order, + base_coin.coin_protocol_info(), + rel_coin.coin_protocol_info(), + ) + .await; let rpc_result = MakerOrderForRpc::from(&new_order); let res = try_s!(json::to_vec(&json!({ "result": rpc_result }))); my_orders.insert(new_order.uuid, new_order); diff --git a/mm2src/lp_ordermatch/best_orders.rs b/mm2src/lp_ordermatch/best_orders.rs index 805d7e2794..4cfb50368b 100644 --- a/mm2src/lp_ordermatch/best_orders.rs +++ b/mm2src/lp_ordermatch/best_orders.rs @@ -1,4 +1,5 @@ -use super::{AddressFormat, OrderbookItemWithProof, OrdermatchContext, OrdermatchRequest}; +use super::{addr_format_from_protocol_info, BaseRelProtocolInfo, OrderbookP2PItemWithProof, OrdermatchContext, + OrdermatchRequest}; use crate::mm2::lp_network::{request_any_relay, P2PRequest}; use coins::{address_by_coin_conf_and_pubkey_str, coin_conf, is_wallet_only_conf, is_wallet_only_ticker}; use common::log; @@ -9,6 +10,7 @@ use num_rational::BigRational; use num_traits::Zero; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; +use uuid::Uuid; #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] @@ -24,9 +26,11 @@ struct BestOrdersRequest { volume: MmNumber, } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Deserialize, Serialize)] struct BestOrdersRes { - orders: HashMap>, + orders: HashMap>, + #[serde(default)] + protocol_infos: HashMap, } pub async fn process_best_orders_p2p_request( @@ -50,6 +54,9 @@ pub async fn process_best_orders_p2p_request( BestOrdersAction::Buy => (coin.clone(), ticker.clone()), BestOrdersAction::Sell => (ticker.clone(), coin.clone()), }); + + let mut protocol_infos = HashMap::new(); + for pair in pairs { let orders = match orderbook.ordered.get(&pair) { Some(orders) => orders, @@ -77,7 +84,8 @@ pub async fn process_best_orders_p2p_request( BestOrdersAction::Sell => &o.max_volume * &o.price, }; let order_w_proof = orderbook.orderbook_item_with_proof(o.clone()); - best_orders.push(order_w_proof); + protocol_infos.insert(order_w_proof.order.uuid, order_w_proof.order.base_rel_proto_info()); + best_orders.push(order_w_proof.into()); collected_volume += max_volume; if collected_volume >= required_volume { @@ -95,7 +103,10 @@ pub async fn process_best_orders_p2p_request( BestOrdersAction::Sell => result.insert(pair.0, best_orders), }; } - let response = BestOrdersRes { orders: result }; + let response = BestOrdersRes { + orders: result, + protocol_infos, + }; let encoded = rmp_serde::to_vec(&response).expect("rmp_serde::to_vec should not fail here"); Ok(Some(encoded)) } @@ -131,13 +142,16 @@ pub async fn best_orders_rpc(ctx: MmArc, req: Json) -> Result>, } for order_w_proof in orders_w_proofs { let order = order_w_proof.order; - // Todo: use the right address format when a solution is found for the problem of protocol_info - let address = match address_by_coin_conf_and_pubkey_str( - &coin, - &coin_conf, - &order.pubkey, - AddressFormat::Standard, - ) { + let empty_proto_info = BaseRelProtocolInfo::default(); + let proto_infos = p2p_response + .protocol_infos + .get(&order.uuid) + .unwrap_or(&empty_proto_info); + let addr_format = match req.action { + BestOrdersAction::Buy => addr_format_from_protocol_info(&proto_infos.rel), + BestOrdersAction::Sell => addr_format_from_protocol_info(&proto_infos.base), + }; + let address = match address_by_coin_conf_and_pubkey_str(&coin, &coin_conf, &order.pubkey, addr_format) { Ok(a) => a, Err(e) => { log::error!("Error {} getting coin {} address from pubkey {}", e, coin, order.pubkey); @@ -158,3 +172,83 @@ pub async fn best_orders_rpc(ctx: MmArc, req: Json) -> Result>, .body(json::to_vec(&res).expect("Serialization failed")) .map_err(|e| ERRL!("{}", e)) } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod best_orders_test { + use super::*; + use crate::mm2::lp_ordermatch::ordermatch_tests::make_random_orders; + use crate::mm2::lp_ordermatch::{OrderbookItem, TrieProof}; + use std::iter::FromIterator; + + #[test] + fn check_best_orders_p2p_res_serde() { + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] + struct BestOrderV1 { + pubkey: String, + base: String, + rel: String, + price: BigRational, + max_volume: BigRational, + min_volume: BigRational, + uuid: Uuid, + created_at: u64, + } + + impl From for BestOrderV1 { + fn from(o: OrderbookItem) -> Self { + BestOrderV1 { + pubkey: o.pubkey, + base: o.base, + rel: o.rel, + price: o.price, + max_volume: o.max_volume, + min_volume: o.min_volume, + uuid: o.uuid, + created_at: o.created_at, + } + } + } + + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] + struct BestOrderWithProofV1 { + /// Orderbook item + order: BestOrderV1, + /// Last pubkey message payload that contains most recent pair trie root + last_message_payload: Vec, + /// Proof confirming that orderbook item is in the pair trie + proof: TrieProof, + } + + #[derive(Debug, Deserialize, PartialEq, Serialize)] + struct BestOrdersResV1 { + orders: HashMap>, + } + + let orders = make_random_orders("".into(), &[1; 32], "RICK".into(), "MORTY".into(), 10); + let orders: Vec<_> = orders + .into_iter() + .map(|order| BestOrderWithProofV1 { + order: order.into(), + last_message_payload: vec![], + proof: vec![], + }) + .collect(); + + let old = BestOrdersResV1 { + orders: HashMap::from_iter(std::iter::once(("RICK".into(), orders))), + }; + + let old_serialized = rmp_serde::to_vec(&old).unwrap(); + + let mut new: BestOrdersRes = rmp_serde::from_read_ref(&old_serialized).unwrap(); + new.protocol_infos.insert(Uuid::new_v4(), BaseRelProtocolInfo { + base: vec![1], + rel: vec![2], + }); + + let new_serialized = rmp_serde::to_vec(&new).unwrap(); + + let old_from_new: BestOrdersResV1 = rmp_serde::from_read_ref(&new_serialized).unwrap(); + assert_eq!(old, old_from_new); + } +} diff --git a/mm2src/lp_ordermatch/new_protocol.rs b/mm2src/lp_ordermatch/new_protocol.rs index c6561fb69a..106af5da20 100644 --- a/mm2src/lp_ordermatch/new_protocol.rs +++ b/mm2src/lp_ordermatch/new_protocol.rs @@ -122,6 +122,10 @@ pub struct MakerOrderCreated { /// This is timestamp of message pub timestamp: u64, pub pair_trie_root: H64, + #[serde(default)] + pub base_protocol_info: Vec, + #[serde(default)] + pub rel_protocol_info: Vec, } #[derive(Debug, Deserialize, Serialize)] @@ -291,7 +295,7 @@ pub struct MakerConnected { pub maker_order_uuid: CompactUuid, } -#[cfg(test)] +#[cfg(all(test, not(target_arch = "wasm32")))] mod new_protocol_tests { use common::new_uuid; @@ -375,4 +379,46 @@ mod new_protocol_tests { assert_eq!(deserialized, v2); } + + #[test] + fn test_maker_order_created_serde() { + #[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] + struct MakerOrderCreatedV1 { + pub uuid: CompactUuid, + pub base: String, + pub rel: String, + pub price: BigRational, + pub max_volume: BigRational, + pub min_volume: BigRational, + /// This is timestamp of order creation + pub created_at: u64, + pub conf_settings: OrderConfirmationsSettings, + /// This is timestamp of message + pub timestamp: u64, + pub pair_trie_root: H64, + } + + let old_msg = MakerOrderCreatedV1 { + uuid: Uuid::new_v4().into(), + base: "RICK".to_string(), + rel: "MORTY".to_string(), + price: BigRational::from_integer(1.into()), + max_volume: BigRational::from_integer(2.into()), + min_volume: BigRational::from_integer(1.into()), + created_at: 0, + conf_settings: Default::default(), + timestamp: 0, + pair_trie_root: H64::default(), + }; + + let old_serialized = rmp_serde::to_vec(&old_msg).unwrap(); + + let mut new: MakerOrderCreated = rmp_serde::from_read_ref(&old_serialized).unwrap(); + + new.base_protocol_info = vec![1, 2, 3]; + new.rel_protocol_info = vec![1, 2, 3, 4]; + + let new_serialized = rmp_serde::to_vec(&new).unwrap(); + let _old_from_new: MakerOrderCreatedV1 = rmp_serde::from_read_ref(&new_serialized).unwrap(); + } } diff --git a/mm2src/lp_ordermatch/orderbook_rpc.rs b/mm2src/lp_ordermatch/orderbook_rpc.rs index 0ad82a3a6a..b2e8d43a4b 100644 --- a/mm2src/lp_ordermatch/orderbook_rpc.rs +++ b/mm2src/lp_ordermatch/orderbook_rpc.rs @@ -1,4 +1,5 @@ -use super::{subscribe_to_orderbook_topic, AddressFormat, OrdermatchContext, RpcOrderbookEntry}; +use super::{subscribe_to_orderbook_topic, OrdermatchContext, RpcOrderbookEntry}; +use crate::mm2::lp_ordermatch::addr_format_from_protocol_info; 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; @@ -106,12 +107,12 @@ pub async fn orderbook_rpc(ctx: MmArc, req: Json) -> Result>, S "Orderbook::unordered contains {:?} uuid that is not in Orderbook::order_set", uuid ))?; - // Todo: use the right address format when a solution is found for the problem of protocol_info + let address_format = addr_format_from_protocol_info(&ask.base_protocol_info); let address = try_s!(address_by_coin_conf_and_pubkey_str( &req.base, &base_coin_conf, &ask.pubkey, - AddressFormat::Standard, + address_format, )); let is_mine = my_pubsecp == ask.pubkey; orderbook_entries.push(ask.as_rpc_entry_ask(address, is_mine)); @@ -132,12 +133,12 @@ pub async fn orderbook_rpc(ctx: MmArc, req: Json) -> Result>, S "Orderbook::unordered contains {:?} uuid that is not in Orderbook::order_set", uuid ))?; - // Todo: use the right address format when a solution is found for the problem of protocol_info + let address_format = addr_format_from_protocol_info(&bid.base_protocol_info); let address = try_s!(address_by_coin_conf_and_pubkey_str( &req.rel, &rel_coin_conf, &bid.pubkey, - AddressFormat::Standard, + address_format, )); let is_mine = my_pubsecp == bid.pubkey; orderbook_entries.push(bid.as_rpc_entry_bid(address, is_mine)); diff --git a/mm2src/mm2_tests.rs b/mm2src/mm2_tests.rs index 287d686ee1..8b6b03a9d3 100644 --- a/mm2src/mm2_tests.rs +++ b/mm2src/mm2_tests.rs @@ -80,20 +80,6 @@ fn rmd160_from_passphrase(passphrase: &str) -> [u8; 20] { key_pair_from_seed(passphrase).unwrap().public().address_hash().take() } -/* -portfolio is removed from dependencies temporary -#[test] -#[ignore] -fn test_autoprice_coingecko() {portfolio::portfolio_tests::test_autoprice_coingecko (local_start())} - -#[test] -#[ignore] -fn test_autoprice_coinmarketcap() {portfolio::portfolio_tests::test_autoprice_coinmarketcap (local_start())} - -#[test] -fn test_fundvalue() {portfolio::portfolio_tests::test_fundvalue (local_start())} -*/ - /// Integration test for RPC server. /// Check that MM doesn't crash in case of invalid RPC requests #[test] @@ -7609,7 +7595,6 @@ fn test_best_orders_filter_response() { } #[test] -#[ignore] #[cfg(not(target_arch = "wasm32"))] fn test_best_orders_segwit() { let bob_passphrase = get_passphrase(&".env.seed", "BOB_PASSPHRASE").unwrap(); @@ -7660,8 +7645,8 @@ fn test_best_orders_segwit() { electrum.1 ); log!({ "enable tBTC: {:?}", electrum }); - let enable_tbtc_res: Json = json::from_str(&electrum.1).unwrap(); - let tbtc_segwit_address = enable_tbtc_res["address"].as_str().unwrap(); + let enable_tbtc_res: EnableElectrumResponse = json::from_str(&electrum.1).unwrap(); + let tbtc_segwit_address = enable_tbtc_res.address; let electrum = block_on(mm_bob.rpc(json!({ "userpass": "pass", @@ -7678,8 +7663,8 @@ fn test_best_orders_segwit() { electrum.1 ); log!({ "enable RICK: {:?}", electrum }); - let enable_rick_res: Json = json::from_str(&electrum.1).unwrap(); - let rick_address = enable_rick_res["address"].as_str().unwrap(); + let enable_rick_res: EnableElectrumResponse = json::from_str(&electrum.1).unwrap(); + let rick_address = enable_rick_res.address; // issue sell request on Bob side by setting base/rel price log!("Issue bob sell requests"); @@ -7794,6 +7779,328 @@ fn test_best_orders_segwit() { block_on(mm_alice.stop()).unwrap(); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_orderbook_segwit() { + 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":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":0,"estimate_fee_mode":"ECONOMICAL","mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"}} + ]); + + let alice_coins_config = json!([ + {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, + {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":0,"estimate_fee_mode":"ECONOMICAL","mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"}} + ]); + + 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 electrum = block_on(mm_bob.rpc(json!({ + "userpass": "pass", + "method": "electrum", + "coin": "tBTC", + "servers": [{"url":"electrum1.cipig.net:10068"},{"url":"electrum2.cipig.net:10068"},{"url":"electrum3.cipig.net:10068"}], + "address_format":{"format":"segwit"}, + "mm2": 1, + }))).unwrap(); + assert_eq!( + electrum.0, + StatusCode::OK, + "RPC «electrum» failed with {} {}", + electrum.0, + electrum.1 + ); + log!({ "enable tBTC: {:?}", electrum }); + let enable_tbtc_res: EnableElectrumResponse = json::from_str(&electrum.1).unwrap(); + let tbtc_segwit_address = enable_tbtc_res.address; + + let electrum = block_on(mm_bob.rpc(json!({ + "userpass": "pass", + "method": "electrum", + "coin": "RICK", + "servers": [{"url":"electrum1.cipig.net:10017"},{"url":"electrum2.cipig.net:10017"},{"url":"electrum3.cipig.net:10017"}], + "mm2": 1, + }))).unwrap(); + assert_eq!( + electrum.0, + StatusCode::OK, + "RPC «electrum» failed with {} {}", + electrum.0, + electrum.1 + ); + log!({ "enable RICK: {:?}", electrum }); + let enable_rick_res: Json = json::from_str(&electrum.1).unwrap(); + let rick_address = enable_rick_res["address"].as_str().unwrap(); + + // 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) + ("tBTC", "RICK", "0.7", "0.0002", Some("0.00015")), + ("RICK", "tBTC", "0.7", "0.0002", Some("0.00015")), + ]; + 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(); + + // checking orderbook on alice side + let rc = block_on(mm_alice.rpc(json! ({ + "userpass": mm_alice.userpass, + "method": "orderbook", + "base": "tBTC", + "rel": "RICK", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + let response: OrderbookResponse = json::from_str(&rc.1).unwrap(); + assert_eq!(response.asks[0].address, tbtc_segwit_address); + assert_eq!(response.bids[0].address, rick_address); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn alice_can_see_the_active_order_after_orderbook_sync_segwit() { + 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":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":0,"estimate_fee_mode":"ECONOMICAL","mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"}} + ]); + + let alice_coins_config = json!([ + {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, + {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":0,"estimate_fee_mode":"ECONOMICAL","mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"}} + ]); + + 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 electrum = block_on(mm_bob.rpc(json!({ + "userpass": "pass", + "method": "electrum", + "coin": "tBTC", + "servers": [{"url":"electrum1.cipig.net:10068"},{"url":"electrum2.cipig.net:10068"},{"url":"electrum3.cipig.net:10068"}], + "address_format":{"format":"segwit"}, + "mm2": 1, + }))).unwrap(); + assert_eq!( + electrum.0, + StatusCode::OK, + "RPC «electrum» failed with {} {}", + electrum.0, + electrum.1 + ); + log!({ "enable tBTC: {:?}", electrum }); + let enable_tbtc_res: EnableElectrumResponse = json::from_str(&electrum.1).unwrap(); + let tbtc_segwit_address = enable_tbtc_res.address; + + let electrum = block_on(mm_bob.rpc(json!({ + "userpass": "pass", + "method": "electrum", + "coin": "RICK", + "servers": [{"url":"electrum1.cipig.net:10017"},{"url":"electrum2.cipig.net:10017"},{"url":"electrum3.cipig.net:10017"}], + "mm2": 1, + }))).unwrap(); + assert_eq!( + electrum.0, + StatusCode::OK, + "RPC «electrum» failed with {} {}", + electrum.0, + electrum.1 + ); + log!({ "enable RICK: {:?}", electrum }); + let enable_rick_res: Json = json::from_str(&electrum.1).unwrap(); + let rick_address = enable_rick_res["address"].as_str().unwrap(); + + // 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) + ("tBTC", "RICK", "0.7", "0.0002", Some("0.00015")), + ("RICK", "tBTC", "0.7", "0.0002", Some("0.00015")), + ]; + 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 electrum = block_on(mm_alice.rpc(json!({ + "userpass": "pass", + "method": "electrum", + "coin": "tBTC", + "servers": [{"url":"electrum1.cipig.net:10068"},{"url":"electrum2.cipig.net:10068"},{"url":"electrum3.cipig.net:10068"}], + "address_format":{"format":"segwit"}, + "mm2": 1, + }))).unwrap(); + assert_eq!( + electrum.0, + StatusCode::OK, + "RPC «electrum» failed with {} {}", + electrum.0, + electrum.1 + ); + log!({ "enable Alice tBTC: {:?}", electrum }); + + let electrum = block_on(mm_alice.rpc(json!({ + "userpass": "pass", + "method": "electrum", + "coin": "RICK", + "servers": [{"url":"electrum1.cipig.net:10017"},{"url":"electrum2.cipig.net:10017"},{"url":"electrum3.cipig.net:10017"}], + "mm2": 1, + }))).unwrap(); + assert_eq!( + electrum.0, + StatusCode::OK, + "RPC «electrum» failed with {} {}", + electrum.0, + electrum.1 + ); + log!({ "enable Alice RICK: {:?}", electrum }); + + // setting the price will trigger Alice's subscription to the orderbook topic + // but won't request the actual orderbook + let rc = block_on(mm_alice.rpc(json! ({ + "userpass": mm_alice.userpass, + "method": "setprice", + "base": "RICK", + "rel": "tBTC", + "price": "1", + "volume": "0.1", + "cancel_previous": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + // Waiting for 62 seconds required for Alice to sync the orderbook + thread::sleep(Duration::from_secs(62)); + + // checking orderbook on alice side + let rc = block_on(mm_alice.rpc(json! ({ + "userpass": mm_alice.userpass, + "method": "orderbook", + "base": "tBTC", + "rel": "RICK", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + let response: OrderbookResponse = json::from_str(&rc.1).unwrap(); + assert_eq!(response.asks[0].address, tbtc_segwit_address); + assert_eq!(response.bids[0].address, rick_address); + + 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, diff --git a/mm2src/ordermatch_tests.rs b/mm2src/ordermatch_tests.rs index 987e5f2935..ac4e025158 100644 --- a/mm2src/ordermatch_tests.rs +++ b/mm2src/ordermatch_tests.rs @@ -1506,7 +1506,13 @@ fn make_ctx_for_tests() -> (MmArc, String, [u8; 32]) { (ctx, pubkey, secret) } -fn make_random_orders(pubkey: String, _secret: &[u8; 32], base: String, rel: String, n: usize) -> Vec { +pub(super) fn make_random_orders( + pubkey: String, + _secret: &[u8; 32], + base: String, + rel: String, + n: usize, +) -> Vec { let mut rng = rand::thread_rng(); let mut orders = Vec::with_capacity(n); for _i in 0..n { @@ -1522,6 +1528,8 @@ fn make_random_orders(pubkey: String, _secret: &[u8; 32], base: String, rel: Str created_at: now_ms() / 1000, timestamp: now_ms() / 1000, pair_trie_root: H64::default(), + base_protocol_info: vec![], + rel_protocol_info: vec![], }; orders.push((order, pubkey.clone()).into()); @@ -1607,7 +1615,11 @@ fn test_process_get_orderbook_request() { .get(&pubkey) .expect(&format!("!best_orders_by_pubkeys is expected to contain {:?}", pubkey)); - let mut actual: Vec = item.orders.iter().map(|(_uuid, order)| order.clone()).collect(); + let mut actual: Vec = item + .orders + .iter() + .map(|(_uuid, order)| OrderbookItem::from_p2p_and_proto_info(order.clone(), BaseRelProtocolInfo::default())) + .collect(); actual.sort_unstable_by(|x, y| x.uuid.cmp(&y.uuid)); log!([pubkey]"-"[actual.len()]); assert_eq!(actual, *expected); @@ -1707,14 +1719,17 @@ fn test_request_and_fill_orderbook() { .into_iter() .map(|(pubkey, orders)| { let item = GetOrderbookPubkeyItem { - orders, + orders: orders.into_iter().map(|(uuid, order)| (uuid, order.into())).collect(), last_keep_alive: now_ms() / 1000, last_signed_pubkey_payload: vec![], }; (pubkey, item) }) .collect(); - let orderbook = GetOrderbookRes { pubkey_orders: result }; + let orderbook = GetOrderbookRes { + pubkey_orders: result, + protocol_infos: HashMap::new(), + }; let encoded = encode_message(&orderbook).unwrap(); // send the response through the response channel @@ -1785,9 +1800,9 @@ fn test_request_and_fill_orderbook() { .iter() .expect("!TrieDB::iter()") .map(|key_value| { - let (key, value) = key_value.expect("Iterator returned an error"); + let (key, _) = key_value.expect("Iterator returned an error"); let key = TryFromBytes::try_from_bytes(key).expect("!try_from_bytes() key"); - let value = TryFromBytes::try_from_bytes(value).expect("!try_from_bytes() val"); + let value = orderbook.order_set.get(&key).cloned().unwrap(); (key, value) }) .collect(); @@ -2169,9 +2184,15 @@ fn test_process_sync_pubkey_orderbook_state_after_new_orders_added() { let actual_root_hash = delta_trie_root::( &mut old_mem_db, pair_trie_root, - delta - .into_iter() - .map(|(uuid, order)| (*uuid.as_bytes(), order.map(|o| encode_message(&o).unwrap()))), + delta.into_iter().map(|(uuid, order)| { + ( + *uuid.as_bytes(), + order.map(|o| { + let o = OrderbookItem::from_p2p_and_proto_info(o, BaseRelProtocolInfo::default()); + o.trie_state_bytes() + }), + ) + }), ) .unwrap(); assert_eq!(expected_root_hash, actual_root_hash); @@ -2409,7 +2430,7 @@ fn test_process_sync_pubkey_orderbook_state_points_to_not_uptodate_trie_root() { .get(&alb_pair) .expect("MORTY:RICK must be in trie_roots"); - let order_bytes = rmp_serde::to_vec(&new_order).expect("Serialization should never fail"); + let order_bytes = new_order.trie_state_bytes(); let mut new_root = old_root; let mut trie = get_trie_mut(&mut orderbook.memory_db, &mut new_root).expect("!get_trie_mut"); trie.insert(new_order.uuid.as_bytes(), &order_bytes) @@ -2424,6 +2445,7 @@ fn test_process_sync_pubkey_orderbook_state_points_to_not_uptodate_trie_root() { .trie_roots .insert(alb_pair.clone(), new_root); + orderbook.order_set.insert(new_order.uuid, new_order.clone()); (old_root, new_root) }; @@ -2442,8 +2464,9 @@ fn test_process_sync_pubkey_orderbook_state_points_to_not_uptodate_trie_root() { DeltaOrFullTrie::FullTrie(full_trie) => full_trie, }; - let mut expected: Vec<_> = orders.into_iter().map(|order| (order.uuid, order)).collect(); - expected.push((new_order.uuid, new_order)); + let mut expected: Vec<(Uuid, OrderbookP2PItem)> = + orders.into_iter().map(|order| (order.uuid, order.into())).collect(); + expected.push((new_order.uuid, new_order.into())); full_trie.sort_by(|x, y| x.0.cmp(&y.0)); expected.sort_by(|x, y| x.0.cmp(&y.0)); assert_eq!(full_trie, expected); @@ -2505,9 +2528,9 @@ fn check_if_orderbook_contains_only(orderbook: &Orderbook, pubkey: &str, orders: .iter() .expect("!TrieDB::iter") .map(|key_value| { - let (key, value) = key_value.expect("Iterator returned an error"); + let (key, _) = key_value.expect("Iterator returned an error"); let key = TryFromBytes::try_from_bytes(key).expect("!try_from_bytes() key"); - let value = TryFromBytes::try_from_bytes(value).expect("!try_from_bytes() val"); + let value = orderbook.order_set.get(&key).cloned().unwrap(); (key, value) }) .collect(); @@ -2589,6 +2612,7 @@ fn test_orderbook_sync_trie_diff_time_cache() { *alice_root, *bob_root, &orderbook_bob.memory_db, + |uuid: &Uuid| orderbook_bob.order_set.get(uuid).cloned(), ) .unwrap(); @@ -2597,7 +2621,16 @@ fn test_orderbook_sync_trie_diff_time_cache() { _ => panic!("Expected DeltaOrFullTrie::FullTrie"), }; - let new_alice_root = process_pubkey_full_trie(&mut orderbook_alice, &pubkey_bob, &rick_morty_pair, full_trie); + let new_alice_root = process_pubkey_full_trie( + &mut orderbook_alice, + &pubkey_bob, + &rick_morty_pair, + full_trie + .into_iter() + .map(|(uuid, order)| (uuid, order.into())) + .collect(), + &HashMap::new(), + ); assert_eq!(new_alice_root, *bob_root); @@ -2626,6 +2659,7 @@ fn test_orderbook_sync_trie_diff_time_cache() { *alice_root, *bob_root, &orderbook_bob.memory_db, + |uuid: &Uuid| orderbook_bob.order_set.get(uuid).cloned(), ) .unwrap(); @@ -2635,7 +2669,16 @@ fn test_orderbook_sync_trie_diff_time_cache() { _ => panic!("Expected DeltaOrFullTrie::Delta"), }; - let new_alice_root = process_trie_delta(&mut orderbook_alice, &pubkey_bob, &rick_morty_pair, trie_delta); + let new_alice_root = process_trie_delta( + &mut orderbook_alice, + &pubkey_bob, + &rick_morty_pair, + trie_delta + .into_iter() + .map(|(uuid, order)| (uuid, order.map(From::from))) + .collect(), + &HashMap::new(), + ); assert_eq!(new_alice_root, *bob_root); } @@ -2712,3 +2755,188 @@ fn test_orderbook_order_pairs_trie_state_history_updates_expiration_on_insert() 10 ); } + +#[test] +fn test_trie_state_bytes() { + let pubkey = "037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5"; + let base = "RICK"; + let rel = "MORTY"; + let price = BigRational::from_integer(1.into()); + let max_volume = BigRational::from_integer(u64::MAX.into()); + let min_volume = BigRational::from_integer(1.into()); + let uuid = Uuid::new_v4(); + let created_at = now_ms() / 1000; + + #[derive(Serialize)] + struct OrderbookItemV1 { + pubkey: String, + base: String, + rel: String, + price: BigRational, + max_volume: BigRational, + min_volume: BigRational, + uuid: Uuid, + created_at: u64, + } + + let old = OrderbookItemV1 { + pubkey: pubkey.to_owned(), + base: base.to_owned(), + rel: rel.to_owned(), + price: price.clone(), + max_volume: max_volume.clone(), + min_volume: min_volume.clone(), + uuid, + created_at, + }; + + let old_bytes = rmp_serde::to_vec(&old).unwrap(); + + let new = OrderbookItem { + pubkey: pubkey.to_owned(), + base: base.to_owned(), + rel: rel.to_owned(), + price, + max_volume, + min_volume, + uuid, + created_at, + base_protocol_info: vec![1, 2, 3], + rel_protocol_info: vec![4, 5, 6], + }; + + let new_bytes = new.trie_state_bytes(); + + assert_eq!(old_bytes, new_bytes); +} + +#[test] +fn check_get_orderbook_p2p_res_serde() { + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] + struct OrderbookItemV1 { + pubkey: String, + base: String, + rel: String, + price: BigRational, + max_volume: BigRational, + min_volume: BigRational, + uuid: Uuid, + created_at: u64, + } + + type PubkeyOrdersV1 = Vec<(Uuid, OrderbookItemV1)>; + + impl From for OrderbookItemV1 { + fn from(o: OrderbookItem) -> Self { + OrderbookItemV1 { + pubkey: o.pubkey, + base: o.base, + rel: o.rel, + price: o.price, + max_volume: o.max_volume, + min_volume: o.min_volume, + uuid: o.uuid, + created_at: o.created_at, + } + } + } + + #[derive(Debug, Deserialize, PartialEq, Serialize)] + struct GetOrderbookPubkeyItemV1 { + /// Timestamp of the latest keep alive message received. + last_keep_alive: u64, + /// last signed OrdermatchMessage payload + last_signed_pubkey_payload: Vec, + /// Requested orders. + orders: PubkeyOrdersV1, + } + + #[derive(Debug, Deserialize, PartialEq, Serialize)] + struct GetOrderbookResV1 { + /// Asks and bids grouped by pubkey. + pubkey_orders: HashMap, + } + + let orders = make_random_orders("".into(), &[1; 32], "RICK".into(), "MORTY".into(), 10); + let item = GetOrderbookPubkeyItemV1 { + last_keep_alive: 100, + last_signed_pubkey_payload: vec![1, 2, 3], + orders: orders.into_iter().map(|order| (order.uuid, order.into())).collect(), + }; + + let old = GetOrderbookResV1 { + pubkey_orders: HashMap::from_iter(std::iter::once(("pubkey".into(), item))), + }; + + let old_serialized = rmp_serde::to_vec(&old).unwrap(); + + let mut new: GetOrderbookRes = rmp_serde::from_read_ref(&old_serialized).unwrap(); + new.protocol_infos.insert(Uuid::new_v4(), BaseRelProtocolInfo { + base: vec![1], + rel: vec![2], + }); + + let new_serialized = rmp_serde::to_vec(&new).unwrap(); + + let old_from_new: GetOrderbookResV1 = rmp_serde::from_read_ref(&new_serialized).unwrap(); + assert_eq!(old, old_from_new); +} + +#[test] +fn check_sync_pubkey_state_p2p_res_serde() { + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] + struct OrderbookItemV1 { + pubkey: String, + base: String, + rel: String, + price: BigRational, + max_volume: BigRational, + min_volume: BigRational, + uuid: Uuid, + created_at: u64, + } + + impl From for OrderbookItemV1 { + fn from(o: OrderbookItem) -> Self { + OrderbookItemV1 { + pubkey: o.pubkey, + base: o.base, + rel: o.rel, + price: o.price, + max_volume: o.max_volume, + min_volume: o.min_volume, + uuid: o.uuid, + created_at: o.created_at, + } + } + } + + #[derive(Debug, Deserialize, Serialize)] + struct SyncPubkeyOrderbookStateResV1 { + /// last signed OrdermatchMessage payload from pubkey + last_signed_pubkey_payload: Vec, + pair_orders_diff: HashMap>, + } + + let orders = make_random_orders("".into(), &[1; 32], "RICK".into(), "MORTY".into(), 10); + + let old = SyncPubkeyOrderbookStateResV1 { + last_signed_pubkey_payload: vec![1, 2, 3, 4], + pair_orders_diff: HashMap::from_iter(iter::once(( + alb_ordered_pair("RICK", "MORTY"), + DeltaOrFullTrie::FullTrie(orders.into_iter().map(|order| (order.uuid, order.into())).collect()), + ))), + }; + + let old_serialized = rmp_serde::to_vec(&old).unwrap(); + + let mut new: SyncPubkeyOrderbookStateRes = rmp_serde::from_read_ref(&old_serialized).unwrap(); + new.protocol_infos.insert(Uuid::new_v4(), BaseRelProtocolInfo { + base: vec![1], + rel: vec![2], + }); + + let new_serialized = rmp_serde::to_vec(&new).unwrap(); + + let _old_from_new: SyncPubkeyOrderbookStateResV1 = rmp_serde::from_read_ref(&new_serialized).unwrap(); +}