From 46bbef4acf22aa06b071cad279b4be597f61e9ef Mon Sep 17 00:00:00 2001 From: X Date: Tue, 21 May 2024 09:28:20 +0200 Subject: [PATCH] feat: unify order closing --- src/backend/beacon.did | 1 + src/backend/order_book/mod.rs | 146 +++++++++++++++++++--------------- src/backend/updates.rs | 11 +++ src/frontend/src/api.ts | 13 +++ 4 files changed, 106 insertions(+), 65 deletions(-) diff --git a/src/backend/beacon.did b/src/backend/beacon.did index 7cb2cf5..4f8f219 100644 --- a/src/backend/beacon.did +++ b/src/backend/beacon.did @@ -18,6 +18,7 @@ type OrderType = variant { Buy; Sell }; type Result = variant { Ok; Err : text }; type Result_1 = variant { Ok : nat; Err : text }; service : () -> { + close_all_orders : () -> (); close_order : (principal, OrderType, nat, nat, nat64) -> (); deposit_liquidity : (principal) -> (Result); http_request : (HttpRequest) -> (HttpResponse) query; diff --git a/src/backend/order_book/mod.rs b/src/backend/order_book/mod.rs index a2a4dcb..e7902d9 100644 --- a/src/backend/order_book/mod.rs +++ b/src/backend/order_book/mod.rs @@ -167,6 +167,57 @@ impl State { } } + /// Closes orders satisfying the given condition. + /// + /// The token filter restricts the deletion to the list of tokens if it is not empty. + /// + /// To guarantee that this never runs out + /// of instructions, we need an upper bound on the total number of orders here. + pub fn close_orders_by_condition( + &mut self, + predicate: &dyn Fn(&Order) -> bool, + token_filter: HashSet, + max_chunk: usize, + ) -> usize { + let mut closed_orders = 0; + self.orders + .iter() + .filter(|(token, _)| token_filter.is_empty() || token_filter.contains(token)) + .flat_map(|(token, book)| { + book.buyers + .iter() + .chain(book.sellers.iter()) + .map(move |order| (*token, order.clone())) + }) + .filter(|(_, order)| predicate(order)) + .take(max_chunk) + .collect::>() + .into_iter() + .for_each( + |( + token, + Order { + order_type, + owner, + amount, + price, + timestamp, + .. + }, + )| { + if let Err(err) = + self.close_order(owner, token, amount, price, timestamp, order_type) + { + self.log(format!("failed to close an order: {}", err)) + } else { + closed_orders += 1 + } + }, + ); + + closed_orders + } + pub fn clean_up(&mut self, now: Timestamp) { // Rotate logs let mut deleted_logs = 0; @@ -183,39 +234,12 @@ impl State { deleted_archived_orders += length_before.saturating_sub(archive.len()); } - // To guarantee that this never runs out of instructions, we need an - // upper bound on the total number of orders here. - let max_chunk = 100000; - // Close all orders older than 1 months - let mut closed_orders = 0; - self.orders - .iter() - .flat_map(|(token, book)| { - book.buyers - .iter() - .map(|order| (OrderType::Buy, order)) - .chain(book.sellers.iter().map(|order| (OrderType::Sell, order))) - .map(move |(t, order)| (*token, t, order.clone())) - }) - .filter(|(_, _, order)| order.timestamp + ORDER_EXPIRATION_DAYS * DAY < now) - .take(max_chunk) - .collect::>() - .into_iter() - .for_each(|(token, order_type, order)| { - if let Err(err) = self.close_order( - order.owner, - token, - order.amount, - order.price, - order.timestamp, - order_type, - ) { - self.log(format!("failed to close an expired order: {}", err)) - } else { - closed_orders += 1 - } - }); + let closed_orders = self.close_orders_by_condition( + &|order| order.timestamp + ORDER_EXPIRATION_DAYS * DAY < now, + Default::default(), + 100000, + ); if closed_orders > 0 || deleted_archived_orders > 0 || deleted_logs > 0 { self.log(format!( @@ -251,19 +275,16 @@ impl State { Some(Value::Nat(fee)), Some(Value::Nat(decimals)), logo, - ) => { - self.add_token( - token, - symbol.clone(), - *fee, - *decimals as u32, - match logo { - Some(Value::Text(hex)) => Some(hex.clone()), - _ => None, - }, - ); - Ok(()) - } + ) => self.add_token( + token, + symbol.clone(), + *fee, + *decimals as u32, + match logo { + Some(Value::Text(hex)) => Some(hex.clone()), + _ => None, + }, + ), (symbol, fee, decimals, _) => Err(format!( "one of the required values missing: symbol={:?}, fee={:?}, decimals={:?}", symbol, fee, decimals @@ -460,22 +481,14 @@ impl State { fee: Tokens, decimals: u32, logo: Option, - ) { + ) -> Result<(), String> { if let Some(current_meta) = self.tokens.get(&id) { // If this is a relisting and the fee or the decimals have changed, close all orders first. if current_meta.fee != fee || current_meta.decimals != decimals { - if let Some(order_book) = self.orders.remove(&id) { - for Order { - order_type, - owner, - amount, - price, - timestamp, - .. - } in order_book.sellers.iter().chain(order_book.buyers.iter()) - { - self.close_order(*owner, id, *amount, *price, *timestamp, *order_type) - .expect("couldn't close order") + self.close_orders_by_condition(&|_| true, [id].iter().copied().collect(), 100000); + if let Some(order_book) = self.orders.get(&id) { + if !order_book.buyers.is_empty() || !order_book.sellers.is_empty() { + return Err("couldn't close all orders".into()); } } } @@ -495,6 +508,7 @@ impl State { } else { self.log(format!("token {} was re-listed", id)); } + Ok(()) } pub fn create_order( @@ -965,13 +979,15 @@ mod tests { } fn list_test_token(state: &mut State, token: TokenId, decimals: u32) { - state.add_token( - token, - "TAGGR".into(), - 25, // fee - decimals, - None, - ); + state + .add_token( + token, + "TAGGR".into(), + 25, // fee + decimals, + None, + ) + .unwrap(); } fn list_payment_token(state: &mut State) { diff --git a/src/backend/updates.rs b/src/backend/updates.rs index b419f69..af803bc 100644 --- a/src/backend/updates.rs +++ b/src/backend/updates.rs @@ -38,6 +38,17 @@ fn set_revenue_account(new_address: Principal) { }) } +// Closing of all orders is is needed in order to upgrading the fees or payment token. +// Additionally, it could help in an emergency situation. +#[update] +fn close_all_orders() { + mutate(|state| { + if state.revenue_account == Some(caller()) { + state.close_orders_by_condition(&|_| true, Default::default(), 10000); + } + }) +} + #[update] async fn close_order( token: TokenId, diff --git a/src/frontend/src/api.ts b/src/frontend/src/api.ts index e411e89..db8fd5b 100644 --- a/src/frontend/src/api.ts +++ b/src/frontend/src/api.ts @@ -38,6 +38,8 @@ export type Backend = { deposit_liquidity: (tokenId: Principal) => Promise; + close_all_orders: () => Promise; + close_order: ( tokenId: Principal, order_type: OrderType, @@ -204,6 +206,17 @@ export const ApiGenerator = ( ); }, + close_all_orders: async (): Promise => { + const arg = IDL.encode([], []); + const response = await call_raw( + canisterId, + "close_all_orders", + arg, + ); + + decode(response); + }, + close_order: async ( tokenId: Principal, orderType: OrderType,