Skip to content

Commit

Permalink
Orders by Owner Pagination (#30)
Browse files Browse the repository at this point in the history
* Added multiindex for #20

* Moved tests to separate module

* Added pagination to orders query

---------

Co-authored-by: Alpo <[email protected]>
  • Loading branch information
crnbarr93 and AlpinYukseloglu authored Feb 17, 2024
1 parent ca96a0e commit 3fc8189
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 31 deletions.
77 changes: 48 additions & 29 deletions contracts/orderbook/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
use crate::types::{FilterOwnerOrders, LimitOrder, Orderbook};
use crate::ContractError;
use cosmwasm_std::{Addr, Order, StdResult, Storage, Uint128};
use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex};
use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, Map, MultiIndex};

pub const MIN_TICK: i64 = -108000000;
pub const MAX_TICK: i64 = 342000000;

// Counters for ID tracking
pub const ORDER_ID: Item<u64> = Item::new("order_id");
pub const ORDERBOOK_ID: Item<u64> = Item::new("orderbook_id");

// Pagination constants for queries
const MAX_PAGE_SIZE: u8 = 100;
const DEFAULT_PAGE_SIZE: u8 = 50;

pub const ORDERBOOKS: Map<&u64, Orderbook> = Map::new("orderbooks");
/// Key: (orderbook_id, tick)
pub const TICK_LIQUIDITY: Map<&(u64, i64), Uint128> = Map::new("tick_liquidity");
Expand Down Expand Up @@ -52,10 +60,6 @@ pub fn orders() -> IndexedMap<'static, &'static (u64, i64, u64), LimitOrder, Ord
)
}

// Counters for ID tracking
pub const ORDER_ID: Item<u64> = Item::new("order_id");
pub const ORDERBOOK_ID: Item<u64> = Item::new("orderbook_id");

pub fn new_orderbook_id(storage: &mut dyn Storage) -> Result<u64, ContractError> {
let id = ORDERBOOK_ID.load(storage).unwrap_or_default();
ORDERBOOK_ID.save(storage, &(id + 1))?;
Expand All @@ -68,38 +72,53 @@ pub fn new_order_id(storage: &mut dyn Storage) -> Result<u64, ContractError> {
Ok(id)
}

// TODO: Add pagination
// TODO: How finite do we need queries?
/// Retrieves a list of `LimitOrder` filtered by the specified `FilterOwnerOrders`.
///
/// This function allows for filtering orders based on the owner's address, optionally further
/// filtering by book ID or tick ID. It supports pagination through `min`, `max`, and `page_size` parameters.
///
/// ## Arguments
///
/// * `storage` - CosmWasm Storage struct
/// * `filter` - Specifies how to filter orders based on the owner. Can be by all orders of the owner,
/// by a specific book, or by a specific tick within a book.
/// * `min` - An optional minimum bound (exclusive) for the order key (orderbook_id, tick, order_id) to start the query.
/// * `max` - An optional maximum bound (exclusive) for the order key to end the query.
/// * `page_size` - An optional maximum number of orders to return. Limited by `MAX_PAGE_SIZE = 100` defaults to `DEFAULT_PAGE_SIZE = 50`.
///
/// ## Returns
///
/// A result containing either a vector of `LimitOrder` matching the criteria or an error.
pub fn get_orders_by_owner(
storage: &dyn Storage,
filter: FilterOwnerOrders,
min: Option<(u64, i64, u64)>,
max: Option<(u64, i64, u64)>,
page_size: Option<u8>,
) -> StdResult<Vec<LimitOrder>> {
let orders: Vec<LimitOrder> = match filter {
FilterOwnerOrders::All(owner) => orders()
.idx
.owner
.prefix(owner)
.range(storage, None, None, Order::Ascending)
.filter_map(|item| item.ok())
.map(|(_, order)| order)
.collect(),
FilterOwnerOrders::ByBook(book_id, owner) => orders()
.idx
.book_and_owner
.prefix((book_id, owner))
.range(storage, None, None, Order::Ascending)
.filter_map(|item| item.ok())
.map(|(_, order)| order)
.collect(),
let page_size = page_size.unwrap_or(DEFAULT_PAGE_SIZE).min(MAX_PAGE_SIZE) as usize;
let min = min.map(Bound::exclusive);
let max = max.map(Bound::exclusive);

// Define the prefix iterator based on the filter
let iter = match filter {
FilterOwnerOrders::All(owner) => orders().idx.owner.prefix(owner),
FilterOwnerOrders::ByBook(book_id, owner) => {
orders().idx.book_and_owner.prefix((book_id, owner))
}
FilterOwnerOrders::ByTick(book_id, tick_id, owner) => orders()
.idx
.tick_and_owner
.prefix((book_id, tick_id, owner))
.range(storage, None, None, Order::Ascending)
.filter_map(|item| item.ok())
.map(|(_, order)| order)
.collect(),
.prefix((book_id, tick_id, owner)),
};

// Get orders based on pagination
let orders: Vec<LimitOrder> = iter
.range(storage, min, max, Order::Ascending)
.take(page_size)
.filter_map(|item| item.ok())
.map(|(_, order)| order)
.collect();

Ok(orders)
}
81 changes: 79 additions & 2 deletions contracts/orderbook/src/tests/test_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,14 @@ fn test_get_orders_by_owner_all() {
.unwrap();
});

let owner_orders: Vec<LimitOrder> =
get_orders_by_owner(&storage, FilterOwnerOrders::All(Addr::unchecked(owner))).unwrap();
let owner_orders: Vec<LimitOrder> = get_orders_by_owner(
&storage,
FilterOwnerOrders::All(Addr::unchecked(owner)),
None,
None,
None,
)
.unwrap();

assert_eq!(owner_orders.len(), order_amount / 2 + 1);
owner_orders.iter().for_each(|order| {
Expand Down Expand Up @@ -152,6 +158,9 @@ fn test_get_orders_by_owner_by_book() {
let owner_orders = get_orders_by_owner(
&storage,
FilterOwnerOrders::ByBook(book_id, Addr::unchecked(owner)),
None,
None,
None,
)
.unwrap();
assert!(!owner_orders.is_empty());
Expand Down Expand Up @@ -193,6 +202,9 @@ fn test_get_orders_by_owner_by_tick() {
let owner_orders = get_orders_by_owner(
&storage,
FilterOwnerOrders::ByTick(book_id, tick, Addr::unchecked(owner)),
None,
None,
None,
)
.unwrap();
assert!(!owner_orders.is_empty());
Expand All @@ -202,3 +214,68 @@ fn test_get_orders_by_owner_by_tick() {
});
});
}

#[test]
fn test_get_orders_by_owner_with_pagination() {
let mut storage = MockStorage::new();
let order_amount = 100;
let ticks = [0, 1, 2];
let owner = "owner1";
let book_id = new_orderbook_id(&mut storage).unwrap();

// Create orders for a single owner across different ticks
(0..order_amount).for_each(|i| {
let order_id = new_order_id(&mut storage).unwrap();
let tick = ticks[i % 3];
let order = LimitOrder::new(
book_id,
tick,
order_id,
OrderDirection::Ask,
Addr::unchecked(owner),
Uint128::new(i as u128),
);
orders()
.save(&mut storage, &(book_id, tick, i as u64), &order)
.unwrap();
});

// Test pagination by fetching orders in batches
let mut start_after = None;
let page_size = 8;
let mut fetched_orders = 0;
let mut total_pages = 0;

loop {
let owner_orders = get_orders_by_owner(
&storage,
FilterOwnerOrders::All(Addr::unchecked(owner)),
start_after,
None,
Some(page_size),
)
.unwrap();

let orders_count = owner_orders.len() as u64;

fetched_orders += orders_count;
if orders_count == 0 {
break;
}

total_pages += 1;

start_after = Some(
owner_orders
.last()
.map(|order| (order.book_id, order.tick_id, order.order_id))
.unwrap(),
);
}

assert_eq!(fetched_orders, order_amount as u64);
assert_eq!(
total_pages,
(order_amount as f64 / page_size as f64).ceil() as u64
);
}
1 change: 1 addition & 0 deletions contracts/orderbook/src/types/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ impl LimitOrder {

// TODO: Unnecessary if finite queries not required
/// Defines the different way an owners orders can be filtered, all enums filter by owner with each getting more finite
#[derive(Clone)]
pub enum FilterOwnerOrders {
All(Addr),
ByBook(u64, Addr),
Expand Down

0 comments on commit 3fc8189

Please sign in to comment.