diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d9636b..bac1604 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,7 @@ { - "rust-analyzer.showUnlinkedFileNotification": false + "rust-analyzer.showUnlinkedFileNotification": false, + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer", // Makes the magic + "editor.formatOnSave": true // Optional + }, } \ No newline at end of file diff --git a/contracts/orderbook/src/contract.rs b/contracts/orderbook/src/contract.rs index 476bd50..9188a60 100644 --- a/contracts/orderbook/src/contract.rs +++ b/contracts/orderbook/src/contract.rs @@ -6,8 +6,8 @@ use cw2::set_contract_version; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -use crate::orderbook; use crate::order; +use crate::orderbook; // version info for migration info const CONTRACT_NAME: &str = "crates.io:orderbook"; @@ -60,13 +60,7 @@ pub fn execute( ExecuteMsg::CreateOrderbook { quote_denom, base_denom, - } => orderbook::create_orderbook( - _deps, - _env, - _info, - quote_denom, - base_denom, - ), + } => orderbook::create_orderbook(_deps, _env, _info, quote_denom, base_denom), // Places limit order on given market ExecuteMsg::PlaceLimit => order::place_limit(_deps, _env, _info), diff --git a/contracts/orderbook/src/orderbook.rs b/contracts/orderbook/src/orderbook.rs index 0c79d56..1677f61 100644 --- a/contracts/orderbook/src/orderbook.rs +++ b/contracts/orderbook/src/orderbook.rs @@ -1,6 +1,6 @@ use crate::error::ContractError; +use crate::state::{new_orderbook_id, MAX_TICK, MIN_TICK, ORDERBOOKS}; use crate::types::Orderbook; -use crate::state::{new_orderbook_id, ORDERBOOKS, MIN_TICK, MAX_TICK}; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; pub fn create_orderbook( @@ -25,7 +25,9 @@ pub fn create_orderbook( ORDERBOOKS.save(deps.storage, &book_id, &_book)?; - Ok(Response::new().add_attribute("method", "createOrderbook").add_attribute("book_id", &book_id.to_string())) + Ok(Response::new() + .add_attribute("method", "createOrderbook") + .add_attribute("book_id", book_id.to_string())) } #[cfg(test)] @@ -42,15 +44,27 @@ mod tests { // Attempt to create an orderbook let quote_denom = "quote".to_string(); let base_denom = "base".to_string(); - let create_response = create_orderbook(deps.as_mut(), env, info, quote_denom.clone(), base_denom.clone()).unwrap(); - + let create_response = create_orderbook( + deps.as_mut(), + env, + info, + quote_denom.clone(), + base_denom.clone(), + ) + .unwrap(); + // Verify response let expected_book_id: u64 = 0; assert_eq!(create_response.attributes[0], ("method", "createOrderbook")); - assert_eq!(create_response.attributes[1], ("book_id", &expected_book_id.to_string())); + assert_eq!( + create_response.attributes[1], + ("book_id", &expected_book_id.to_string()) + ); // Verify orderbook is saved correctly - let orderbook = ORDERBOOKS.load(deps.as_ref().storage, &expected_book_id).unwrap(); + let orderbook = ORDERBOOKS + .load(deps.as_ref().storage, &expected_book_id) + .unwrap(); assert_eq!(orderbook.quote_denom, quote_denom); assert_eq!(orderbook.base_denom, base_denom); assert_eq!(orderbook.current_tick, 0); diff --git a/contracts/orderbook/src/state.rs b/contracts/orderbook/src/state.rs index 69b6ddc..04948e9 100644 --- a/contracts/orderbook/src/state.rs +++ b/contracts/orderbook/src/state.rs @@ -1,17 +1,56 @@ -use crate::types::{LimitOrder, Orderbook}; +use crate::types::{FilterOwnerOrders, LimitOrder, Orderbook}; use crate::ContractError; -use cosmwasm_std::{Storage, Uint128}; -use cw_storage_plus::{Item, Map}; +use cosmwasm_std::{Addr, Order, StdResult, Storage, Uint128}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; pub const MIN_TICK: i64 = -108000000; pub const MAX_TICK: i64 = 342000000; - 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"); + +// TODO: Check additional gas fee for adding more indexes +pub struct OrderIndexes { + // Index by owner; Generic types: MultiIndex + pub owner: MultiIndex<'static, Addr, LimitOrder, (u64, i64, u64)>, + // Index by book and owner; Generic types: MultiIndex + pub book_and_owner: MultiIndex<'static, (u64, Addr), LimitOrder, (u64, i64, u64)>, + // Index by tick and owner; Generic types: MultiIndex + pub tick_and_owner: MultiIndex<'static, (u64, i64, Addr), LimitOrder, (u64, i64, u64)>, +} + +impl IndexList for OrderIndexes { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = + vec![&self.owner, &self.book_and_owner, &self.tick_and_owner]; + Box::new(v.into_iter()) + } +} + /// Key: (orderbook_id, tick, order_id) -pub const ORDERS: Map<&(u64, i64, u64), LimitOrder> = Map::new("tick_orders"); +pub fn orders() -> IndexedMap<'static, &'static (u64, i64, u64), LimitOrder, OrderIndexes> { + IndexedMap::new( + "orders", + OrderIndexes { + owner: MultiIndex::new( + |_, d: &LimitOrder| d.owner.clone(), + "orders", + "orders_owner", + ), + book_and_owner: MultiIndex::new( + |_, d: &LimitOrder| (d.book_id, d.owner.clone()), + "orders", + "orders_book_and_owner", + ), + tick_and_owner: MultiIndex::new( + |_, d: &LimitOrder| (d.book_id, d.tick_id, d.owner.clone()), + "orders", + "orders_tick_and_owner", + ), + }, + ) +} // Counters for ID tracking pub const ORDER_ID: Item = Item::new("order_id"); @@ -29,6 +68,43 @@ pub fn new_order_id(storage: &mut dyn Storage) -> Result { Ok(id) } +// TODO: Add pagination +// TODO: How finite do we need queries? + +/// Retrieves a list of `LimitOrder` filtered by the specified `FilterOwnerOrders`. +pub fn get_orders_by_owner( + storage: &dyn Storage, + filter: FilterOwnerOrders, +) -> StdResult> { + let orders: Vec = 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(), + 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(), + }; + Ok(orders) +} + #[cfg(test)] mod test { use super::*; @@ -91,16 +167,16 @@ mod test { tick_id: tick, book_id, order_id, - owner: Addr::unchecked(format!("maker{}", i)), + owner: Addr::unchecked(format!("maker{i}")), quantity: Uint128::new(i as u128), order_direction: OrderDirection::Ask, }; - ORDERS + orders() .save(&mut storage, &(book_id, tick, i), &order) .unwrap(); } - let tick_orders = ORDERS.prefix((book_id, tick)); + let tick_orders = orders().prefix((book_id, tick)); let orders_desc: Vec = tick_orders .range(&storage, None, None, Order::Descending) .map(|result| result.unwrap().1) @@ -114,4 +190,125 @@ mod test { assert_eq!(orders_asc[i as usize].order_id, i); } } + + #[test] + fn test_get_orders_by_owner_all() { + let mut storage = MockStorage::new(); + let order_amount = 10; + let owner = "owner1"; + + let book_ids: Vec = (0..3) + .map(|_| new_orderbook_id(&mut storage).unwrap()) + .collect(); + + (0..order_amount).for_each(|i| { + let order_id = new_order_id(&mut storage).unwrap(); + let other_owner = &format!("owner{i}"); + let current_owner = Addr::unchecked(if i % 2 == 0 { owner } else { other_owner }); + let order = LimitOrder::new( + book_ids[i % 3], + 0, + order_id, + OrderDirection::Ask, + current_owner, + Uint128::new(i as u128), + ); + orders() + .save(&mut storage, &(order.book_id, 0, i as u64), &order) + .unwrap(); + }); + + let owner_orders: Vec = + get_orders_by_owner(&storage, FilterOwnerOrders::All(Addr::unchecked(owner))).unwrap(); + + assert_eq!(owner_orders.len(), order_amount / 2 + 1); + owner_orders.iter().for_each(|order| { + assert_eq!(order.owner, Addr::unchecked(owner)); + }); + } + + #[test] + fn test_get_orders_by_owner_by_book() { + let mut storage = MockStorage::new(); + let order_amount = 100; + let owner = "owner1"; + + // Generate three new book IDs + let book_ids: Vec = (0..3) + .map(|_| new_orderbook_id(&mut storage).unwrap()) + .collect(); + + // Create orders alternating ownership between `owner` and dynamically generated owners amongst all books evenly + (0..order_amount).for_each(|i| { + let order_id = new_order_id(&mut storage).unwrap(); + let other_owner = &format!("owner{i}"); + let current_owner = Addr::unchecked(if i % 2 == 0 { owner } else { other_owner }); + let order = LimitOrder::new( + book_ids[i % 3], + 0, + order_id, + OrderDirection::Ask, + current_owner, + Uint128::new(i as u128), + ); + orders() + .save(&mut storage, &(order.book_id, 0, i as u64), &order) + .unwrap(); + }); + + // Verify orders by book ID + book_ids.iter().for_each(|&book_id| { + let owner_orders = get_orders_by_owner( + &storage, + FilterOwnerOrders::ByBook(book_id, Addr::unchecked(owner)), + ) + .unwrap(); + assert!(!owner_orders.is_empty()); + owner_orders.iter().for_each(|order| { + assert_eq!(order.owner, Addr::unchecked(owner)); + assert_eq!(order.book_id, book_id); + }); + }); + } + + #[test] + fn test_get_orders_by_owner_by_tick() { + 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 alternating ownership between `owner` and dynamically generated owners amongst all ticks evenly + (0..order_amount).for_each(|i| { + let order_id = new_order_id(&mut storage).unwrap(); + let other_owner = &format!("owner{i}"); + let current_owner = Addr::unchecked(if i % 2 == 0 { owner } else { other_owner }); + let tick = ticks[i % 3]; + let order = LimitOrder::new( + book_id, + tick, + order_id, + OrderDirection::Ask, + current_owner, + Uint128::new(i as u128), + ); + orders() + .save(&mut storage, &(book_id, tick, i as u64), &order) + .unwrap(); + }); + + ticks.iter().for_each(|&tick| { + let owner_orders = get_orders_by_owner( + &storage, + FilterOwnerOrders::ByTick(book_id, tick, Addr::unchecked(owner)), + ) + .unwrap(); + assert!(!owner_orders.is_empty()); + owner_orders.iter().for_each(|order| { + assert_eq!(order.owner, Addr::unchecked(owner)); + assert_eq!(order.tick_id, tick); + }); + }); + } } diff --git a/contracts/orderbook/src/types/mod.rs b/contracts/orderbook/src/types/mod.rs index b99e6b5..fe05fae 100644 --- a/contracts/orderbook/src/types/mod.rs +++ b/contracts/orderbook/src/types/mod.rs @@ -1,5 +1,5 @@ -pub mod orderbook; mod order; +mod orderbook; -pub use orderbook::*; -pub use order::*; \ No newline at end of file +pub use self::order::*; +pub use self::orderbook::*; diff --git a/contracts/orderbook/src/types/order.rs b/contracts/orderbook/src/types/order.rs index 8cf578c..4be802c 100644 --- a/contracts/orderbook/src/types/order.rs +++ b/contracts/orderbook/src/types/order.rs @@ -36,3 +36,25 @@ 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 +pub enum FilterOwnerOrders { + All(Addr), + ByBook(u64, Addr), + ByTick(u64, i64, Addr), +} + +impl FilterOwnerOrders { + pub fn all(owner: Addr) -> Self { + FilterOwnerOrders::All(owner) + } + + pub fn by_book(book_id: u64, owner: Addr) -> Self { + FilterOwnerOrders::ByBook(book_id, owner) + } + + pub fn by_tick(book_id: u64, tick_id: i64, owner: Addr) -> Self { + FilterOwnerOrders::ByTick(book_id, tick_id, owner) + } +}