diff --git a/Cargo.lock b/Cargo.lock index f63cbfab..f2c19ed5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8619,10 +8619,12 @@ dependencies = [ "pallet-balances", "pallet-timestamp", "parity-scale-codec", + "parking_lot 0.12.1", "scale-info", "sp-core 7.0.0", "sp-io 7.0.0", "sp-runtime 7.0.0", + "sp-std 5.0.0 (git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.43)", ] [[package]] diff --git a/node/tests/data/scale/eth_light_client_brooklyn.scale b/node/tests/data/scale/eth_light_client_brooklyn.scale index 001cdf47..e6780ff8 100644 Binary files a/node/tests/data/scale/eth_light_client_brooklyn.scale and b/node/tests/data/scale/eth_light_client_brooklyn.scale differ diff --git a/node/tests/data/scale/eth_light_client_sydney.scale b/node/tests/data/scale/eth_light_client_sydney.scale index dd25c98e..8689aa0c 100644 Binary files a/node/tests/data/scale/eth_light_client_sydney.scale and b/node/tests/data/scale/eth_light_client_sydney.scale differ diff --git a/pallet/dex/Cargo.toml b/pallet/dex/Cargo.toml index b9e8dfde..b4b96441 100644 --- a/pallet/dex/Cargo.toml +++ b/pallet/dex/Cargo.toml @@ -12,10 +12,13 @@ targets = ["x86_64-unknown-linux-gnu"] frame-support.workspace = true frame-system.workspace = true log.workspace = true +parking_lot = { version = "0.12.1", optional = true } scale-codec = { package = "parity-scale-codec", workspace = true, features = ["max-encoded-len"] } scale-info.workspace = true sp-runtime.workspace = true sp-io.workspace = true +sp-std.workspace = true + [dev-dependencies] pallet-assets.workspace = true @@ -26,6 +29,7 @@ sp-core.workspace = true [features] default = ["std"] std = [ + "parking_lot", "frame-support/std", "frame-system/std", "pallet-assets/std", @@ -34,6 +38,7 @@ std = [ "scale-codec/std", "scale-info/std", "sp-core/std", + "sp-std/std", "sp-runtime/std", ] diff --git a/pallet/dex/src/lib.rs b/pallet/dex/src/lib.rs index 4aeb9dfa..04bbf34e 100644 --- a/pallet/dex/src/lib.rs +++ b/pallet/dex/src/lib.rs @@ -6,7 +6,7 @@ use frame_support::{ pallet_prelude::{ConstU32, DispatchResult}, sp_std::{convert::TryInto, prelude::*}, traits::{Currency, ExistenceRequirement::AllowDeath, Get, ReservableCurrency}, - BoundedBTreeMap, PalletId, RuntimeDebug, + BoundedBTreeMap, BoundedBTreeSet, PalletId, RuntimeDebug, }; use frame_system::offchain::SendTransactionTypes; @@ -183,6 +183,16 @@ pub struct Trade { maker_order: Order, } +#[derive(Encode, Decode, Default, Eq, PartialEq, Clone, RuntimeDebug, TypeInfo)] +pub struct MultipleOrderInfo { + order_id_set: BoundedBTreeSet>, + unfilled_order_id_set: BoundedBTreeSet>, + status: OrderStatus, + reserved_asset_id: u32, + reserved: Balance, + unuse_reserved: Balance, +} + #[allow(clippy::unused_unit)] #[frame_support::pallet] pub mod pallet { @@ -193,6 +203,7 @@ pub mod pallet { Blake2_128Concat, }; use frame_system::offchain::SubmitTransaction; + use sp_std::collections::{btree_map::BTreeMap, btree_set::BTreeSet}; pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; @@ -203,6 +214,15 @@ pub mod pallet { type MapMatchEnginesOf = BoundedBTreeMap<(u32, u32), MatchEngine, BalanceOf>, ConstU32<{ u32::MAX }>>; + type OrderInput = ( + u32, + u32, + BalanceOf, + BalanceOf, + OrderType, + BlockNumberFor, + ); + #[pallet::genesis_config] #[derive(Default)] pub struct GenesisConfig { @@ -339,6 +359,30 @@ pub mod pallet { #[pallet::getter(fn native_asset_id)] pub type NativeAssetId = StorageValue<_, u32, ValueQuery>; + #[pallet::storage] + #[pallet::getter(fn multiple_order_infos)] + pub type MultipleOrderInfos = StorageMap< + _, + Blake2_128Concat, + u64, //multiple order infos id + MultipleOrderInfo>, + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn next_mutiple_order_info_index)] + pub(super) type NextMultipleOrderInfoIndex = StorageValue<_, u64, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn map_mutiple_order_id)] + pub type MapMultipleOrderID = StorageMap< + _, + Blake2_128Concat, + u64, //order index, + u64, //multiple order infos id + ValueQuery, + >; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -398,6 +442,9 @@ pub mod pallet { PriceDoNotMatchOfferedRequestedAmount, DivOverflow, MulOverflow, + MultipleOrderInfoNotFound, + OfferAssetMustBeReservedAsset, + OfferedAmountMustBeLessThanReservedAmount, } #[pallet::hooks] @@ -643,96 +690,15 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - let (asset_id_1, asset_id_2, order_type) = if asset_id_1 > asset_id_2 { - (asset_id_2, asset_id_1, order_type.get_opposite()) - } else { - (asset_id_1, asset_id_2, order_type) - }; - - ensure!( - asset_id_1 != asset_id_2, - Error::::PairAssetIdMustNotEqual - ); - - ensure!( - expiration_block > frame_system::Pallet::::block_number(), - Error::::ExpirationMustBeInFuture - ); - - let (a, b) = match order_type { - OrderType::SELL => (requested_amount, offered_amount), - OrderType::BUY => (offered_amount, requested_amount), - }; - - // because price is an integer, we need to check if the division is exact - // (does not have a remainder) - let price = a - .checked_div(&b) - .ok_or(Error::::PriceDoNotMatchOfferedRequestedAmount)?; - - // do the check - if price - .checked_mul(&b) - .ok_or(Error::::PriceDoNotMatchOfferedRequestedAmount)? - != a - { - return Err(Error::::PriceDoNotMatchOfferedRequestedAmount.into()); - } - - NextOrderIndex::::try_mutate(|index| -> DispatchResult { - let order_index = *index; - - let order = Order { - counter: order_index, - pair: (asset_id_1, asset_id_2), - expiration_block, - order_type, - address: who.clone(), - amount_offered: offered_amount, - amout_requested: requested_amount, - price, - unfilled_offered: offered_amount, - unfilled_requested: requested_amount, - order_status: OrderStatus::Pending, - }; - - let update_asset_id = match order.order_type { - OrderType::SELL => asset_id_1, - OrderType::BUY => asset_id_2, - }; - let mut info = UserTokenInfoes::::get(who.clone(), update_asset_id); - info.amount = info - .amount - .checked_sub(&order.amount_offered) - .ok_or(Error::::NotEnoughBalance)?; - info.reserved = info - .reserved - .checked_add(&order.amount_offered) - .ok_or(Error::::TokenBalanceOverflow)?; - UserTokenInfoes::::insert(who.clone(), update_asset_id, info); - - *index = index - .checked_add(One::one()) - .ok_or(Error::::OrderIndexOverflow)?; - - Orders::::insert(order_index, &order); - UserOrders::::insert(who.clone(), order_index, ()); - - let mut expiration_orders = OrderExpiration::::get(expiration_block); - expiration_orders - .try_push(order_index) - .expect("Max expiration_orders"); - OrderExpiration::::insert(expiration_block, expiration_orders); - - let mut bounded_pair_orders = PairOrders::::get((asset_id_1, asset_id_2)); - bounded_pair_orders - .try_push(order_index) - .expect("Max bounded_pair_orders"); - PairOrders::::insert((asset_id_1, asset_id_2), bounded_pair_orders); - - Self::deposit_event(Event::OrderCreated { order_index, order }); - Ok(()) - })?; + Self::create_order_impl( + who, + asset_id_1, + asset_id_2, + offered_amount, + requested_amount, + order_type, + expiration_block, + )?; Ok(().into()) } @@ -803,6 +769,8 @@ pub mod pallet { } } + Self::update_multiple_order_in_group(order_index, order.amount_offered)?; + Self::deposit_event(Event::OrderTaken { account: who, order_index, @@ -973,6 +941,15 @@ pub mod pallet { )?; } + Self::update_multiple_order_in_group( + taker_order.counter, + taker_order.amount_offered, + )?; + Self::update_multiple_order_in_group( + maker_order.counter, + maker_order.amount_offered, + )?; + Self::deposit_event(Event::OrderMatched { quantity_base: trade.quantity_base, quantity_quote: trade.quantity_quote, @@ -983,6 +960,98 @@ pub mod pallet { Ok(()) } + + #[pallet::weight({9})] + #[pallet::call_index(9)] + pub fn make_multiple_orders( + origin: OriginFor, + orders: Vec>, + reserved_asset_id: u32, + reserved_amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + for order in &orders { + let ( + asset_id_1, + asset_id_2, + offered_amount, + _requested_amount, + order_type, + _expiration_block, + ) = order; + + let offer_asset = match order_type { + OrderType::SELL => asset_id_1, + OrderType::BUY => asset_id_2, + }; + + ensure!( + offer_asset == &reserved_asset_id, + Error::::OfferAssetMustBeReservedAsset + ); + + ensure!( + offered_amount <= &reserved_amount, + Error::::OfferedAmountMustBeLessThanReservedAmount + ); + } + + let mut info = UserTokenInfoes::::get(who.clone(), reserved_asset_id); + info.amount = info + .amount + .checked_sub(&reserved_amount) + .ok_or(Error::::NotEnoughBalance)?; + + info.reserved = info + .reserved + .checked_add(&reserved_amount) + .ok_or(Error::::TokenBalanceOverflow)?; + UserTokenInfoes::::insert(who.clone(), reserved_asset_id, info); + + NextMultipleOrderInfoIndex::::try_mutate(|index| -> DispatchResult { + let next_multiple_order_info_index = *index; + + let mut order_id_set = BoundedBTreeSet::>::new(); + for order in orders { + let order_id = NextOrderIndex::::get(); + + Self::create_multiple_order_impl( + who.clone(), + order.0, + order.1, + order.2, + order.3, + order.4, + order.5, + )?; + + let _ = order_id_set.try_insert(order_id); + + MapMultipleOrderID::::insert(order_id, next_multiple_order_info_index); + } + + MultipleOrderInfos::::insert( + next_multiple_order_info_index, + MultipleOrderInfo { + order_id_set: order_id_set.clone(), + unfilled_order_id_set: order_id_set.clone(), + status: OrderStatus::Pending, + reserved_asset_id, + reserved: reserved_amount, + unuse_reserved: reserved_amount, + }, + ); + + *index = index + .checked_add(One::one()) + .ok_or(Error::::OrderIndexOverflow)?; + + Ok(()) + })?; + + Ok(().into()) + } } impl Pallet { @@ -1096,6 +1165,249 @@ pub mod pallet { }) } + fn cancel_order_impl_no_update_reserved(order_index: u64) -> DispatchResult { + Orders::::try_mutate_exists(order_index, |order| -> DispatchResult { + let order = order.take().ok_or(Error::::InvalidOrderIndex)?; + + UserOrders::::remove(order.address, order_index); + + PairOrders::::try_mutate_exists( + order.pair, + |bounded_pair_orders| -> DispatchResult { + let pair_orders = bounded_pair_orders + .as_mut() + .ok_or(Error::::PairOrderNotFound)?; + let rt = pair_orders.binary_search(&order_index); + if let Ok(rt_index) = rt { + pair_orders.remove(rt_index); + } + + PairOrders::::insert(order.pair, pair_orders); + Ok(()) + }, + )?; + + Self::deposit_event(Event::OrderCanceled { order_index }); + + Ok(()) + }) + } + + fn close_multiple_order(multiple_order_id: u64, owner: T::AccountId) -> DispatchResult { + if MapMultipleOrderID::::contains_key(multiple_order_id) { + let multiple_order_info = MultipleOrderInfos::::get(multiple_order_id); + + let mut info = + UserTokenInfoes::::get(owner.clone(), multiple_order_info.reserved_asset_id); + info.amount = info + .amount + .checked_add(&multiple_order_info.unuse_reserved) + .ok_or(Error::::TokenBalanceOverflow)?; + info.reserved = info + .reserved + .checked_sub(&multiple_order_info.unuse_reserved) + .ok_or(Error::::NotEnoughBalance)?; + UserTokenInfoes::::insert( + owner.clone(), + multiple_order_info.reserved_asset_id, + info, + ); + } + + Ok(()) + } + + fn create_order_impl( + who: T::AccountId, + asset_id_1: u32, + asset_id_2: u32, + offered_amount: BalanceOf, + requested_amount: BalanceOf, + order_type: OrderType, + expiration_block: BlockNumberFor, + ) -> DispatchResult { + let (asset_id_1, asset_id_2, order_type) = if asset_id_1 > asset_id_2 { + (asset_id_2, asset_id_1, order_type.get_opposite()) + } else { + (asset_id_1, asset_id_2, order_type) + }; + + ensure!( + asset_id_1 != asset_id_2, + Error::::PairAssetIdMustNotEqual + ); + + ensure!( + expiration_block > frame_system::Pallet::::block_number(), + Error::::ExpirationMustBeInFuture + ); + + let (a, b) = match order_type { + OrderType::SELL => (requested_amount, offered_amount), + OrderType::BUY => (offered_amount, requested_amount), + }; + + // because price is an integer, we need to check if the division is exact + // (does not have a remainder) + let price = a + .checked_div(&b) + .ok_or(Error::::PriceDoNotMatchOfferedRequestedAmount)?; + + // do the check + if price + .checked_mul(&b) + .ok_or(Error::::PriceDoNotMatchOfferedRequestedAmount)? + != a + { + return Err(Error::::PriceDoNotMatchOfferedRequestedAmount.into()); + } + + NextOrderIndex::::try_mutate(|index| -> DispatchResult { + let order_index = *index; + + let order = Order { + counter: order_index, + pair: (asset_id_1, asset_id_2), + expiration_block, + order_type, + address: who.clone(), + amount_offered: offered_amount, + amout_requested: requested_amount, + price, + unfilled_offered: offered_amount, + unfilled_requested: requested_amount, + order_status: OrderStatus::Pending, + }; + + let update_asset_id = match order.order_type { + OrderType::SELL => asset_id_1, + OrderType::BUY => asset_id_2, + }; + let mut info = UserTokenInfoes::::get(who.clone(), update_asset_id); + info.amount = info + .amount + .checked_sub(&order.amount_offered) + .ok_or(Error::::NotEnoughBalance)?; + info.reserved = info + .reserved + .checked_add(&order.amount_offered) + .ok_or(Error::::TokenBalanceOverflow)?; + UserTokenInfoes::::insert(who.clone(), update_asset_id, info); + + *index = index + .checked_add(One::one()) + .ok_or(Error::::OrderIndexOverflow)?; + + Orders::::insert(order_index, &order); + UserOrders::::insert(who.clone(), order_index, ()); + + let mut expiration_orders = OrderExpiration::::get(expiration_block); + expiration_orders + .try_push(order_index) + .expect("Max expiration_orders"); + OrderExpiration::::insert(expiration_block, expiration_orders); + + let mut bounded_pair_orders = PairOrders::::get((asset_id_1, asset_id_2)); + bounded_pair_orders + .try_push(order_index) + .expect("Max bounded_pair_orders"); + PairOrders::::insert((asset_id_1, asset_id_2), bounded_pair_orders); + + Self::deposit_event(Event::OrderCreated { order_index, order }); + Ok(()) + })?; + + Ok(()) + } + + fn create_multiple_order_impl( + who: T::AccountId, + asset_id_1: u32, + asset_id_2: u32, + offered_amount: BalanceOf, + requested_amount: BalanceOf, + order_type: OrderType, + expiration_block: BlockNumberFor, + ) -> DispatchResult { + let (asset_id_1, asset_id_2, order_type) = if asset_id_1 > asset_id_2 { + (asset_id_2, asset_id_1, order_type.get_opposite()) + } else { + (asset_id_1, asset_id_2, order_type) + }; + + ensure!( + asset_id_1 != asset_id_2, + Error::::PairAssetIdMustNotEqual + ); + + ensure!( + expiration_block > frame_system::Pallet::::block_number(), + Error::::ExpirationMustBeInFuture + ); + + let (a, b) = match order_type { + OrderType::SELL => (requested_amount, offered_amount), + OrderType::BUY => (offered_amount, requested_amount), + }; + + // because price is an integer, we need to check if the division is exact + // (does not have a remainder) + let price = a + .checked_div(&b) + .ok_or(Error::::PriceDoNotMatchOfferedRequestedAmount)?; + + // do the check + if price + .checked_mul(&b) + .ok_or(Error::::PriceDoNotMatchOfferedRequestedAmount)? + != a + { + return Err(Error::::PriceDoNotMatchOfferedRequestedAmount.into()); + } + + NextOrderIndex::::try_mutate(|index| -> DispatchResult { + let order_index = *index; + + let order = Order { + counter: order_index, + pair: (asset_id_1, asset_id_2), + expiration_block, + order_type, + address: who.clone(), + amount_offered: offered_amount, + amout_requested: requested_amount, + price, + unfilled_offered: offered_amount, + unfilled_requested: requested_amount, + order_status: OrderStatus::Pending, + }; + + *index = index + .checked_add(One::one()) + .ok_or(Error::::OrderIndexOverflow)?; + + Orders::::insert(order_index, &order); + UserOrders::::insert(who.clone(), order_index, ()); + + let mut expiration_orders = OrderExpiration::::get(expiration_block); + expiration_orders + .try_push(order_index) + .expect("Max expiration_orders"); + OrderExpiration::::insert(expiration_block, expiration_orders); + + let mut bounded_pair_orders = PairOrders::::get((asset_id_1, asset_id_2)); + bounded_pair_orders + .try_push(order_index) + .expect("Max bounded_pair_orders"); + PairOrders::::insert((asset_id_1, asset_id_2), bounded_pair_orders); + + Self::deposit_event(Event::OrderCreated { order_index, order }); + Ok(()) + })?; + + Ok(()) + } + pub fn update_order_from_trade_order(order: &OrderOf) -> Result<(), DispatchError> { Orders::::insert(order.counter, order); Ok(()) @@ -1164,13 +1476,40 @@ pub mod pallet { taker_order: taker_order.clone(), match_details: vec![], }; + + let mut disable_multiple_order_id_in_group = BTreeSet::::new(); + let mut taker_unfilled_quantity_requested = taker_order.amout_requested; let mut taker_unfilled_quantity_offered = taker_order.amount_offered; - loop { + + let mut multiple_order_group_cache = + BTreeMap::>>::new(); + + let mut skip_book = BoundedBTreeMap::< + OrderBookKey>, + OrderOf, + ConstU32<{ u32::MAX }>, + >::new(); + + let max_loop_step: usize = maker_book.len(); + for _n in 0..max_loop_step { if maker_book.is_empty() { break; } + { + if !disable_multiple_order_id_in_group.is_empty() { + maker_book.retain(|k, _| { + !disable_multiple_order_id_in_group.contains(&k.order_id) + }); + another_book.retain(|k, _| { + !disable_multiple_order_id_in_group.contains(&k.order_id) + }); + + disable_multiple_order_id_in_group.clear(); + } + } + let maker_order_key = match maker_book_type { OrderType::BUY => match maker_book.last_key_value() { Some((k, _)) => k.clone(), @@ -1203,6 +1542,14 @@ pub mod pallet { let (matched_quantity_requested, matched_quantity_offered) = match taker_unfilled_quantity_requested.cmp(&maker_order.unfilled_offered) { Ordering::Greater => { + if MapMultipleOrderID::::contains_key(taker_order.counter) { + // multiple order must be FullyFilled, skip PartialFilled + let _ = skip_book + .try_insert(maker_order_key.clone(), maker_order.clone()); + maker_book.remove(&maker_order_key); + continue; + } + //taker request amout > maker offer amout (maker_order.unfilled_offered, maker_order.unfilled_requested) } @@ -1234,6 +1581,14 @@ pub mod pallet { } } Ordering::Less => { + if MapMultipleOrderID::::contains_key(maker_order.counter) { + // multiple order must be FullyFilled, skip PartialFilled + let _ = skip_book + .try_insert(maker_order_key.clone(), maker_order.clone()); + maker_book.remove(&maker_order_key); + continue; + } + //taker request amout < maker offer amout match taker_order.order_type { OrderType::BUY => { @@ -1287,6 +1642,15 @@ pub mod pallet { || taker_unfilled_quantity_offered == BalanceOf::::default() { taker_order.order_status = OrderStatus::FullyFilled; + + if MapMultipleOrderID::::contains_key(taker_order.counter) { + let mut order_id_set = Self::get_disable_multiple_order_id_in_group( + taker_order.counter, + &mut multiple_order_group_cache, + ); + + disable_multiple_order_id_in_group.append(&mut order_id_set); + } } else if taker_unfilled_quantity_requested != taker_order.amout_requested { taker_order.order_status = OrderStatus::PartialFilled; } @@ -1294,6 +1658,15 @@ pub mod pallet { if maker_unfilled_quantity_offered == BalanceOf::::default() { maker_order.order_status = OrderStatus::FullyFilled; + if MapMultipleOrderID::::contains_key(maker_order.counter) { + let mut order_id_set = Self::get_disable_multiple_order_id_in_group( + maker_order.counter, + &mut multiple_order_group_cache, + ); + + disable_multiple_order_id_in_group.append(&mut order_id_set); + } + match_result.match_details.push(Trade { price: maker_order.price, quantity_base: matched_quantity_base, @@ -1323,6 +1696,10 @@ pub mod pallet { } } + for key in skip_book.keys() { + let _ = maker_book.try_insert(key.clone(), skip_book.get(key).unwrap().clone()); + } + if taker_unfilled_quantity_requested != BalanceOf::::default() && taker_unfilled_quantity_offered != BalanceOf::::default() { @@ -1340,9 +1717,110 @@ pub mod pallet { } } + if !disable_multiple_order_id_in_group.is_empty() { + maker_book.retain(|k, _| !disable_multiple_order_id_in_group.contains(&k.order_id)); + another_book + .retain(|k, _| !disable_multiple_order_id_in_group.contains(&k.order_id)); + + disable_multiple_order_id_in_group.clear(); + } + Ok(match_result) } + fn get_disable_multiple_order_id_in_group( + matched_order_id: u64, + map_infos: &mut BTreeMap>>, + ) -> BTreeSet { + let multiple_order_id = MapMultipleOrderID::::get(matched_order_id); + + map_infos + .entry(multiple_order_id) + .or_insert_with(|| MultipleOrderInfos::::get(multiple_order_id)); + let info = map_infos.get_mut(&multiple_order_id).unwrap(); + let order = Orders::::get(matched_order_id).unwrap(); + + let mut order_id_set = info.order_id_set.clone(); + order_id_set.remove(&matched_order_id); + + let mut new_order_id_set = BTreeSet::::new(); + + if info.unuse_reserved < order.unfilled_offered { + info.unuse_reserved = Default::default(); + } else { + info.unuse_reserved -= order.unfilled_offered; + } + + for id in &order_id_set { + let order = Orders::::get(id).unwrap(); + + if order.unfilled_offered > info.unuse_reserved { + new_order_id_set.insert(*id); + } + } + + new_order_id_set + } + + fn update_multiple_order_in_group( + order_index: u64, + offered_amount: BalanceOf, + ) -> DispatchResult { + if MapMultipleOrderID::::contains_key(order_index) { + let multiple_order_id = MapMultipleOrderID::::get(order_index); + + MultipleOrderInfos::::try_mutate_exists( + multiple_order_id, + |may_multiple_order_info| -> DispatchResult { + let multiple_order_info = may_multiple_order_info + .as_mut() + .ok_or(Error::::MultipleOrderInfoNotFound)?; + + multiple_order_info + .unfilled_order_id_set + .remove(&order_index); + multiple_order_info.unuse_reserved = multiple_order_info + .unuse_reserved + .checked_sub(&offered_amount) + .ok_or(Error::::NotEnoughBalance)?; + + let order_id_set = multiple_order_info.unfilled_order_id_set.clone(); + + for id in &order_id_set { + if !Orders::::contains_key(id) { + multiple_order_info.unfilled_order_id_set.remove(id); + continue; + } + + let order = Orders::::get(id).unwrap(); + + if order.unfilled_offered > multiple_order_info.unuse_reserved { + Self::cancel_order_impl_no_update_reserved(*id)?; + multiple_order_info.unfilled_order_id_set.remove(id); + } + } + + if multiple_order_info.reserved == Default::default() { + for id in &order_id_set { + Self::cancel_order_impl_no_update_reserved(*id)?; + multiple_order_info.unfilled_order_id_set.remove(id); + } + } + + if multiple_order_info.unfilled_order_id_set.is_empty() { + let order = Orders::::get(order_index).unwrap(); + Self::close_multiple_order(order_index, order.address.clone())?; + multiple_order_info.status = OrderStatus::FullyFilled; + } + + Ok(()) + }, + )?; + } + + Ok(()) + } + fn offchain_unsigned_tx( match_result: MatchResult, OrderOf>, ) -> Result<(), Error> { diff --git a/pallet/dex/src/mock.rs b/pallet/dex/src/mock.rs index 57757d14..4ee0cef7 100644 --- a/pallet/dex/src/mock.rs +++ b/pallet/dex/src/mock.rs @@ -1,5 +1,7 @@ use crate as pallet_dex; +use std::{collections::BTreeMap, sync::Arc}; +use crate::{OrderStatus, OrderType, Orders}; use frame_support::{ pallet_prelude::Weight, parameter_types, sp_io, @@ -7,8 +9,10 @@ use frame_support::{ weights::constants::RocksDbWeight, PalletId, }; +use parking_lot::RwLock; +use scale_codec::Decode; use sp_core::{ConstU128, ConstU32, ConstU64, H256}; -use sp_runtime::{testing::Header, traits::IdentityLookup}; +use sp_runtime::{offchain::testing::PoolState, testing::Header, traits::IdentityLookup}; pub type AccountId = u128; pub type Balance = u128; @@ -240,3 +244,67 @@ pub fn add_blocks(blocks: usize) { new_block(); } } + +pub fn call_offchain_worker_function_in_transactions(pool_state: &Arc>) { + let mut txs = vec![]; + while !pool_state.read().transactions.is_empty() { + let tx = pool_state.write().transactions.pop().unwrap(); + let tx = Extrinsic::decode(&mut &*tx).unwrap(); + txs.insert(0, tx); + } + + for tx in txs { + match tx.call { + RuntimeCall::Dex(crate::Call::update_match_order_unsigned { match_result: m }) => { + let _ = Dex::update_match_order_unsigned(RuntimeOrigin::none(), m); + } + _ => { + assert_eq!(2, 3); + } + }; + } +} + +pub fn create_order_book_map_by_price( + sell_order_book: &mut BTreeMap<(Balance, (u32, u32)), (Balance, Balance)>, + buy_order_book: &mut BTreeMap<(Balance, (u32, u32)), (Balance, Balance)>, +) -> (u32, u32) { + let mut sell_order_count = 0; + let mut buy_order_count = 0; + for (_, order) in Orders::::iter() { + if order.order_status != OrderStatus::FullyFilled { + if order.order_type == OrderType::BUY { + buy_order_count += 1; + if !buy_order_book.contains_key(&(order.price, order.pair)) { + buy_order_book.insert( + (order.price, order.pair), + (order.unfilled_offered, order.unfilled_requested), + ); + } else { + let v = buy_order_book.get(&(order.price, order.pair)).unwrap(); + + buy_order_book.insert( + (order.price, order.pair), + (v.0 + order.unfilled_offered, v.1 + order.unfilled_requested), + ); + } + } else { + sell_order_count += 1; + if !sell_order_book.contains_key(&(order.price, order.pair)) { + sell_order_book.insert( + (order.price, order.pair), + (order.unfilled_offered, order.unfilled_requested), + ); + } else { + let v = sell_order_book.get(&(order.price, order.pair)).unwrap(); + sell_order_book.insert( + (order.price, order.pair), + (v.0 + order.unfilled_offered, v.1 + order.unfilled_requested), + ); + } + } + } + } + + (sell_order_count, buy_order_count) +} diff --git a/pallet/dex/src/tests.rs b/pallet/dex/src/tests.rs index bf107f51..69c7bdef 100644 --- a/pallet/dex/src/tests.rs +++ b/pallet/dex/src/tests.rs @@ -901,23 +901,7 @@ fn test_offchain_worker_order_matching() { Dex::offchain_worker(block); - let mut txs = vec![]; - while !pool_state.read().transactions.is_empty() { - let tx = pool_state.write().transactions.pop().unwrap(); - let tx = Extrinsic::decode(&mut &*tx).unwrap(); - txs.insert(0, tx); - } - - for tx in txs { - match tx.call { - RuntimeCall::Dex(crate::Call::update_match_order_unsigned { match_result: m }) => { - let _ = Dex::update_match_order_unsigned(RuntimeOrigin::none(), m); - } - _ => { - assert_eq!(2, 3); - } - }; - } + call_offchain_worker_function_in_transactions(&pool_state); //order_book price=> (total_offered_amount, total_requested_amount) let mut sell_order_book = BTreeMap::new(); @@ -968,3 +952,606 @@ fn test_offchain_worker_order_matching() { assert_eq!(buy_order_book.get(&208111).unwrap(), &(1456777, 7)); }) } + +#[test] +fn test_multiple_orders_buy() { + use frame_support::traits::OffchainWorker; + let mut ext = new_test_ext(); + + ext.execute_with(|| add_blocks(1)); + ext.persist_offchain_overlay(); + + let (offchain, _offchain_state) = TestOffchainExt::new(); + let (pool, pool_state) = TestTransactionPoolExt::new(); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + ext.register_extension(TransactionPoolExt::new(pool)); + ext.execute_with(|| { + let block = 1; + System::set_block_number(block); + + assert_ok!(Dex::deposit(RuntimeOrigin::signed(1), 777, 1000)); + assert_ok!(Dex::deposit(RuntimeOrigin::signed(1), 888, 1000)); + assert_ok!(Dex::deposit(RuntimeOrigin::signed(1), 999, 1000)); + + assert_ok!(Dex::deposit(RuntimeOrigin::signed(2), 777, 1000)); + assert_ok!(Dex::deposit(RuntimeOrigin::signed(2), 888, 1000)); + assert_ok!(Dex::deposit(RuntimeOrigin::signed(2), 999, 1000)); + + assert_ok!(Dex::make_multiple_orders( + RuntimeOrigin::signed(1), + vec![ + (777, 999, 100, 10, OrderType::BUY, 6), + (777, 999, 100, 10, OrderType::BUY, 6), + (777, 999, 200, 20, OrderType::BUY, 6), + ], + 999, + 300, + )); + + assert_eq!( + UserTokenInfoes::::get(1, 999), + TokenInfo { + amount: 700, + reserved: 300, + } + ); + + let mut order_id_set = BoundedBTreeSet::>::new(); + let _ = order_id_set.try_insert(0); + let _ = order_id_set.try_insert(1); + let _ = order_id_set.try_insert(2); + + let mut unfilled_order_id_set = BoundedBTreeSet::>::new(); + let _ = unfilled_order_id_set.try_insert(0); + let _ = unfilled_order_id_set.try_insert(1); + let _ = unfilled_order_id_set.try_insert(2); + + assert_eq!( + MultipleOrderInfos::::get(0), + MultipleOrderInfo { + order_id_set, + unfilled_order_id_set, + status: OrderStatus::Pending, + reserved_asset_id: 999, + reserved: 300, + unuse_reserved: 300, + } + ); + + // not full filled + { + Dex::offchain_worker(block); + + assert_ok!(Dex::make_order( + RuntimeOrigin::signed(2), + 777, + 999, + 5, + 50, + OrderType::SELL, + 6 + )); + + Dex::offchain_worker(block); + + call_offchain_worker_function_in_transactions(&pool_state); + + //order_book price, pair=> (total_offered_amount, total_requested_amount) + let mut sell_order_book = BTreeMap::new(); + let mut buy_order_book = BTreeMap::new(); + + let (sell_order_count, buy_order_count) = + create_order_book_map_by_price(&mut sell_order_book, &mut buy_order_book); + + assert_eq!(sell_order_count, 1); + assert_eq!(buy_order_count, 3); + } + + // full filled + { + Dex::offchain_worker(block); + + assert_ok!(Dex::make_order( + RuntimeOrigin::signed(2), + 777, + 999, + 10, + 100, + OrderType::SELL, + 6 + )); + + Dex::offchain_worker(block); + + call_offchain_worker_function_in_transactions(&pool_state); + + //order_book price, pair=> (total_offered_amount, total_requested_amount) + let mut sell_order_book = BTreeMap::new(); + let mut buy_order_book = BTreeMap::new(); + + let (sell_order_count, buy_order_count) = + create_order_book_map_by_price(&mut sell_order_book, &mut buy_order_book); + + assert_eq!(sell_order_count, 1); + assert_eq!(buy_order_count, 2); + + assert_eq!( + UserTokenInfoes::::get(1, 777), + TokenInfo { + amount: 1010, + reserved: 0, + } + ); + + assert_eq!( + UserTokenInfoes::::get(1, 999), + TokenInfo { + amount: 700, + reserved: 200, + } + ); + + assert_ok!(Dex::make_order( + RuntimeOrigin::signed(2), + 777, + 999, + 20, + 200, + OrderType::SELL, + 6 + )); + + Dex::offchain_worker(block); + + call_offchain_worker_function_in_transactions(&pool_state); + + //order_book price, pair=> (total_offered_amount, total_requested_amount) + let mut sell_order_book = BTreeMap::new(); + let mut buy_order_book = BTreeMap::new(); + + let (sell_order_count, buy_order_count) = + create_order_book_map_by_price(&mut sell_order_book, &mut buy_order_book); + + assert_eq!(sell_order_count, 1); + assert_eq!(buy_order_count, 0); + + assert_eq!( + UserTokenInfoes::::get(1, 777), + TokenInfo { + amount: 1030, + reserved: 0, + } + ); + + assert_eq!( + UserTokenInfoes::::get(1, 999), + TokenInfo { + amount: 700, + reserved: 0, + } + ); + + let mut order_id_set = BoundedBTreeSet::>::new(); + let _ = order_id_set.try_insert(0); + let _ = order_id_set.try_insert(1); + let _ = order_id_set.try_insert(2); + + let unfilled_order_id_set = BoundedBTreeSet::>::new(); + assert_eq!( + MultipleOrderInfos::::get(0), + MultipleOrderInfo { + order_id_set, + unfilled_order_id_set, + status: OrderStatus::FullyFilled, + reserved_asset_id: 999, + reserved: 300, + unuse_reserved: 0, + } + ); + } + }) +} + +#[test] +fn test_multiple_orders_sell() { + use frame_support::traits::OffchainWorker; + let mut ext = new_test_ext(); + + ext.execute_with(|| add_blocks(1)); + ext.persist_offchain_overlay(); + + let (offchain, _offchain_state) = TestOffchainExt::new(); + let (pool, pool_state) = TestTransactionPoolExt::new(); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + ext.register_extension(TransactionPoolExt::new(pool)); + ext.execute_with(|| { + let block = 1; + System::set_block_number(block); + + assert_ok!(Dex::deposit(RuntimeOrigin::signed(1), 777, 1000)); + assert_ok!(Dex::deposit(RuntimeOrigin::signed(1), 888, 1000)); + assert_ok!(Dex::deposit(RuntimeOrigin::signed(1), 999, 1000)); + + assert_ok!(Dex::deposit(RuntimeOrigin::signed(2), 777, 1000)); + assert_ok!(Dex::deposit(RuntimeOrigin::signed(2), 888, 1000)); + assert_ok!(Dex::deposit(RuntimeOrigin::signed(2), 999, 1000)); + + assert_ok!(Dex::make_multiple_orders( + RuntimeOrigin::signed(1), + vec![ + (777, 888, 10, 100, OrderType::SELL, 6), + (777, 999, 10, 100, OrderType::SELL, 6), + (777, 999, 20, 200, OrderType::SELL, 6), + ], + 777, + 30, + )); + + assert_eq!( + UserTokenInfoes::::get(1, 777), + TokenInfo { + amount: 970, + reserved: 30, + } + ); + + let mut order_id_set = BoundedBTreeSet::>::new(); + let _ = order_id_set.try_insert(0); + let _ = order_id_set.try_insert(1); + let _ = order_id_set.try_insert(2); + + let mut unfilled_order_id_set = BoundedBTreeSet::>::new(); + let _ = unfilled_order_id_set.try_insert(0); + let _ = unfilled_order_id_set.try_insert(1); + let _ = unfilled_order_id_set.try_insert(2); + + assert_eq!( + MultipleOrderInfos::::get(0), + MultipleOrderInfo { + order_id_set, + unfilled_order_id_set, + status: OrderStatus::Pending, + reserved_asset_id: 777, + reserved: 30, + unuse_reserved: 30, + } + ); + + // not full filled + { + Dex::offchain_worker(block); + + assert_ok!(Dex::make_order( + RuntimeOrigin::signed(2), + 777, + 999, + 50, + 5, + OrderType::BUY, + 6 + )); + + Dex::offchain_worker(block); + + call_offchain_worker_function_in_transactions(&pool_state); + + //order_book price, pair=> (total_offered_amount, total_requested_amount) + let mut sell_order_book = BTreeMap::new(); + let mut buy_order_book = BTreeMap::new(); + + let (sell_order_count, buy_order_count) = + create_order_book_map_by_price(&mut sell_order_book, &mut buy_order_book); + + assert_eq!(sell_order_count, 3); + assert_eq!(buy_order_count, 1); + } + + // full filled + { + Dex::offchain_worker(block); + + assert_ok!(Dex::make_order( + RuntimeOrigin::signed(2), + 777, + 999, + 100, + 10, + OrderType::BUY, + 6 + )); + + Dex::offchain_worker(block); + + call_offchain_worker_function_in_transactions(&pool_state); + + //order_book price, pair=> (total_offered_amount, total_requested_amount) + let mut sell_order_book = BTreeMap::new(); + let mut buy_order_book = BTreeMap::new(); + + let (sell_order_count, buy_order_count) = + create_order_book_map_by_price(&mut sell_order_book, &mut buy_order_book); + + assert_eq!(sell_order_count, 2); + assert_eq!(buy_order_count, 1); + + assert_eq!( + UserTokenInfoes::::get(1, 777), + TokenInfo { + amount: 970, + reserved: 20, + } + ); + + assert_eq!( + UserTokenInfoes::::get(1, 999), + TokenInfo { + amount: 1100, + reserved: 0, + } + ); + + assert_ok!(Dex::make_order( + RuntimeOrigin::signed(2), + 777, + 999, + 200, + 20, + OrderType::BUY, + 6 + )); + + Dex::offchain_worker(block); + + call_offchain_worker_function_in_transactions(&pool_state); + + //order_book price, pair=> (total_offered_amount, total_requested_amount) + let mut sell_order_book = BTreeMap::new(); + let mut buy_order_book = BTreeMap::new(); + + let (sell_order_count, buy_order_count) = + create_order_book_map_by_price(&mut sell_order_book, &mut buy_order_book); + + assert_eq!(sell_order_count, 0); + assert_eq!(buy_order_count, 1); + + assert_eq!( + UserTokenInfoes::::get(1, 777), + TokenInfo { + amount: 970, + reserved: 0, + } + ); + + assert_eq!( + UserTokenInfoes::::get(1, 999), + TokenInfo { + amount: 1300, + reserved: 0, + } + ); + + let mut order_id_set = BoundedBTreeSet::>::new(); + let _ = order_id_set.try_insert(0); + let _ = order_id_set.try_insert(1); + let _ = order_id_set.try_insert(2); + + let unfilled_order_id_set = BoundedBTreeSet::>::new(); + assert_eq!( + MultipleOrderInfos::::get(0), + MultipleOrderInfo { + order_id_set, + unfilled_order_id_set, + status: OrderStatus::FullyFilled, + reserved_asset_id: 777, + reserved: 30, + unuse_reserved: 0, + } + ); + } + }) +} + +#[test] +fn test_multiple_orders_different_pricess() { + use frame_support::traits::OffchainWorker; + let mut ext = new_test_ext(); + + ext.execute_with(|| add_blocks(1)); + ext.persist_offchain_overlay(); + + let (offchain, _offchain_state) = TestOffchainExt::new(); + let (pool, pool_state) = TestTransactionPoolExt::new(); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + ext.register_extension(TransactionPoolExt::new(pool)); + ext.execute_with(|| { + let block = 1; + System::set_block_number(block); + + assert_ok!(Dex::deposit(RuntimeOrigin::signed(1), 777, 1000)); + assert_ok!(Dex::deposit(RuntimeOrigin::signed(1), 888, 1000)); + assert_ok!(Dex::deposit(RuntimeOrigin::signed(1), 999, 1000)); + + assert_ok!(Dex::deposit(RuntimeOrigin::signed(2), 777, 1000)); + assert_ok!(Dex::deposit(RuntimeOrigin::signed(2), 888, 1000)); + assert_ok!(Dex::deposit(RuntimeOrigin::signed(2), 999, 1000)); + + assert_ok!(Dex::make_multiple_orders( + RuntimeOrigin::signed(1), + vec![ + (777, 888, 10, 100, OrderType::SELL, 6), + (777, 999, 10, 100, OrderType::SELL, 6), + (777, 999, 20, 400, OrderType::SELL, 6), + ], + 777, + 30, + )); + + assert_ok!(Dex::make_multiple_orders( + RuntimeOrigin::signed(1), + vec![ + (777, 888, 10, 200, OrderType::SELL, 6), + (777, 999, 10, 200, OrderType::SELL, 6), + (777, 999, 20, 600, OrderType::SELL, 6), + ], + 777, + 30, + )); + + assert_eq!( + UserTokenInfoes::::get(1, 777), + TokenInfo { + amount: 940, + reserved: 60, + } + ); + + let mut order_id_set = BoundedBTreeSet::>::new(); + let _ = order_id_set.try_insert(0); + let _ = order_id_set.try_insert(1); + let _ = order_id_set.try_insert(2); + + let mut unfilled_order_id_set = BoundedBTreeSet::>::new(); + let _ = unfilled_order_id_set.try_insert(0); + let _ = unfilled_order_id_set.try_insert(1); + let _ = unfilled_order_id_set.try_insert(2); + + assert_eq!( + MultipleOrderInfos::::get(0), + MultipleOrderInfo { + order_id_set, + unfilled_order_id_set, + status: OrderStatus::Pending, + reserved_asset_id: 777, + reserved: 30, + unuse_reserved: 30, + } + ); + + let mut order_id_set = BoundedBTreeSet::>::new(); + let _ = order_id_set.try_insert(3); + let _ = order_id_set.try_insert(4); + let _ = order_id_set.try_insert(5); + + let mut unfilled_order_id_set = BoundedBTreeSet::>::new(); + let _ = unfilled_order_id_set.try_insert(3); + let _ = unfilled_order_id_set.try_insert(4); + let _ = unfilled_order_id_set.try_insert(5); + assert_eq!( + MultipleOrderInfos::::get(1), + MultipleOrderInfo { + order_id_set, + unfilled_order_id_set, + status: OrderStatus::Pending, + reserved_asset_id: 777, + reserved: 30, + unuse_reserved: 30, + } + ); + + // full filled + { + Dex::offchain_worker(block); + + assert_ok!(Dex::make_order( + RuntimeOrigin::signed(2), + 777, + 999, + 100, + 10, + OrderType::BUY, + 6 + )); + + Dex::offchain_worker(block); + + call_offchain_worker_function_in_transactions(&pool_state); + + //order_book price, pair=> (total_offered_amount, total_requested_amount) + let mut sell_order_book = BTreeMap::new(); + let mut buy_order_book = BTreeMap::new(); + + let (sell_order_count, buy_order_count) = + create_order_book_map_by_price(&mut sell_order_book, &mut buy_order_book); + + assert_eq!(sell_order_count, 5); + assert_eq!(buy_order_count, 0); + + assert_eq!( + UserTokenInfoes::::get(1, 777), + TokenInfo { + amount: 940, + reserved: 50, + } + ); + + assert_eq!( + UserTokenInfoes::::get(1, 999), + TokenInfo { + amount: 1100, + reserved: 0, + } + ); + + assert_ok!(Dex::make_order( + RuntimeOrigin::signed(2), + 777, + 999, + 400, + 20, + OrderType::BUY, + 6 + )); + + Dex::offchain_worker(block); + + call_offchain_worker_function_in_transactions(&pool_state); + + //order_book price, pair=> (total_offered_amount, total_requested_amount) + let mut sell_order_book = BTreeMap::new(); + let mut buy_order_book = BTreeMap::new(); + + let (sell_order_count, buy_order_count) = + create_order_book_map_by_price(&mut sell_order_book, &mut buy_order_book); + + assert_eq!(sell_order_count, 3); + assert_eq!(buy_order_count, 0); + + assert_eq!( + UserTokenInfoes::::get(1, 777), + TokenInfo { + amount: 940, + reserved: 30, + } + ); + + assert_eq!( + UserTokenInfoes::::get(1, 999), + TokenInfo { + amount: 1500, + reserved: 0, + } + ); + + let mut order_id_set = BoundedBTreeSet::>::new(); + let _ = order_id_set.try_insert(0); + let _ = order_id_set.try_insert(1); + let _ = order_id_set.try_insert(2); + + let unfilled_order_id_set = BoundedBTreeSet::>::new(); + assert_eq!( + MultipleOrderInfos::::get(0), + MultipleOrderInfo { + order_id_set, + unfilled_order_id_set, + status: OrderStatus::FullyFilled, + reserved_asset_id: 777, + reserved: 30, + unuse_reserved: 0, + } + ); + } + }) +}