Skip to content

Commit

Permalink
feat: unify order closing
Browse files Browse the repository at this point in the history
  • Loading branch information
X committed May 21, 2024
1 parent 33cf809 commit 46bbef4
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 65 deletions.
1 change: 1 addition & 0 deletions src/backend/beacon.did
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
146 changes: 81 additions & 65 deletions src/backend/order_book/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TokenId>,
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::<Vec<_>>()
.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;
Expand All @@ -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::<Vec<_>>()
.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!(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -460,22 +481,14 @@ impl State {
fee: Tokens,
decimals: u32,
logo: Option<String>,
) {
) -> 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());
}
}
}
Expand All @@ -495,6 +508,7 @@ impl State {
} else {
self.log(format!("token {} was re-listed", id));
}
Ok(())
}

pub fn create_order(
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions src/backend/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions src/frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export type Backend = {

deposit_liquidity: (tokenId: Principal) => Promise<JsonValue>;

close_all_orders: () => Promise<void>;

close_order: (
tokenId: Principal,
order_type: OrderType,
Expand Down Expand Up @@ -204,6 +206,17 @@ export const ApiGenerator = (
);
},

close_all_orders: async (): Promise<void> => {
const arg = IDL.encode([], []);
const response = await call_raw(
canisterId,
"close_all_orders",
arg,
);

decode(response);
},

close_order: async (
tokenId: Principal,
orderType: OrderType,
Expand Down

0 comments on commit 46bbef4

Please sign in to comment.