diff --git a/contracts/orderbook/src/error.rs b/contracts/orderbook/src/error.rs index d11a231..51ca437 100644 --- a/contracts/orderbook/src/error.rs +++ b/contracts/orderbook/src/error.rs @@ -63,6 +63,17 @@ pub enum ContractError { // Tick out of bounds error #[error("Tick out of bounds: {tick_id:?}")] TickOutOfBounds { tick_id: i64 }, + #[error("Cannot fulfill order. Order ID: {order_id:?}, Book ID: {book_id:?}, Amount Required: {amount_required:?}, Amount Remaining: {amount_remaining:?} {reason:?}")] + InvalidFulfillment { + order_id: u64, + book_id: u64, + amount_required: Uint128, + amount_remaining: Uint128, + reason: Option, + }, + + #[error("Mismatched order direction")] + MismatchedOrderDirection {}, } pub type ContractResult = Result; diff --git a/contracts/orderbook/src/order.rs b/contracts/orderbook/src/order.rs index 5dcfe3a..3e2276c 100644 --- a/contracts/orderbook/src/order.rs +++ b/contracts/orderbook/src/order.rs @@ -2,16 +2,18 @@ use crate::constants::{MAX_TICK, MIN_TICK}; use crate::error::ContractError; use crate::state::ORDERBOOKS; use crate::state::*; -use crate::types::{LimitOrder, OrderDirection, REPLY_ID_REFUND}; +use crate::types::{Fulfillment, LimitOrder, MarketOrder, OrderDirection, REPLY_ID_REFUND}; use cosmwasm_std::{ - coin, ensure, ensure_eq, BankMsg, DepsMut, Env, MessageInfo, Response, SubMsg, Uint128, + coin, ensure, ensure_eq, ensure_ne, BankMsg, Decimal, DepsMut, Env, MessageInfo, Order, + Response, Storage, SubMsg, Uint128, }; +use cw_storage_plus::Bound; use cw_utils::{must_pay, nonpayable}; #[allow(clippy::manual_range_contains)] pub fn place_limit( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, book_id: u64, tick_id: i64, @@ -147,3 +149,168 @@ pub fn place_market( .add_attribute("method", "placeMarket") .add_attribute("owner", info.sender)) } + +#[allow(clippy::manual_range_contains)] +pub fn run_market_order( + storage: &mut dyn Storage, + order: &mut MarketOrder, + tick_bound: Option, +) -> Result<(Vec, BankMsg), ContractError> { + let mut fulfillments: Vec = vec![]; + let mut amount_fulfilled: Uint128 = Uint128::zero(); + let orderbook = ORDERBOOKS.load(storage, &order.book_id)?; + let placed_order_denom = orderbook.get_expected_denom(&order.order_direction); + + let (min_tick, max_tick, ordering) = match order.order_direction { + OrderDirection::Ask => { + if let Some(tick_bound) = tick_bound { + ensure!( + tick_bound <= orderbook.next_bid_tick + && tick_bound <= MAX_TICK + && tick_bound >= MIN_TICK, + ContractError::InvalidTickId { + tick_id: tick_bound + } + ); + } + (tick_bound, Some(orderbook.next_bid_tick), Order::Descending) + } + OrderDirection::Bid => { + if let Some(tick_bound) = tick_bound { + ensure!( + tick_bound >= orderbook.next_ask_tick + && tick_bound <= MAX_TICK + && tick_bound >= MIN_TICK, + ContractError::InvalidTickId { + tick_id: tick_bound + } + ); + } + (Some(orderbook.next_ask_tick), tick_bound, Order::Ascending) + } + }; + + // Create ticks iterator between first tick and requested tick + let ticks = TICK_LIQUIDITY.prefix(order.book_id).range( + storage, + min_tick.map(Bound::inclusive), + max_tick.map(Bound::inclusive), + ordering, + ); + + for maybe_current_tick in ticks { + let current_tick = maybe_current_tick?.0; + + // Create orders iterator for all orders on current tick + let tick_orders = orders().prefix((order.book_id, current_tick)).range( + storage, + None, + None, + Order::Ascending, + ); + + for maybe_current_order in tick_orders { + let current_order = maybe_current_order?.1; + ensure_ne!( + current_order.order_direction, + order.order_direction, + ContractError::MismatchedOrderDirection {} + ); + let fill_quantity = order.quantity.min(current_order.quantity); + // Add to total amount fulfilled from placed order + amount_fulfilled = amount_fulfilled.checked_add(fill_quantity)?; + // Generate fulfillment for current order + let fulfillment = Fulfillment::new(current_order, fill_quantity); + fulfillments.push(fulfillment); + + // Update remaining order quantity + order.quantity = order.quantity.checked_sub(fill_quantity)?; + // TODO: Price detection + if order.quantity.is_zero() { + return Ok(( + fulfillments, + BankMsg::Send { + to_address: order.owner.to_string(), + amount: vec![coin(amount_fulfilled.u128(), placed_order_denom)], + }, + )); + } + } + + // TODO: Price detection + if order.quantity.is_zero() { + return Ok(( + fulfillments, + BankMsg::Send { + to_address: order.owner.to_string(), + amount: vec![coin(amount_fulfilled.u128(), placed_order_denom)], + }, + )); + } + } + + // TODO: Price detection + Ok(( + fulfillments, + BankMsg::Send { + to_address: order.owner.to_string(), + amount: vec![coin(amount_fulfilled.u128(), placed_order_denom)], + }, + )) +} + +pub fn resolve_fulfillments( + storage: &mut dyn Storage, + fulfillments: Vec, +) -> Result, ContractError> { + let mut msgs: Vec = vec![]; + let orderbook = ORDERBOOKS.load(storage, &fulfillments[0].order.book_id)?; + for mut fulfillment in fulfillments { + ensure_eq!( + fulfillment.order.book_id, + orderbook.book_id, + // TODO: Error not expressive + ContractError::InvalidFulfillment { + order_id: fulfillment.order.order_id, + book_id: fulfillment.order.book_id, + amount_required: fulfillment.amount, + amount_remaining: fulfillment.order.quantity, + reason: Some("Fulfillment is part of another order book".to_string()), + } + ); + let denom = orderbook.get_expected_denom(&fulfillment.order.order_direction); + // TODO: Add price detection for tick + let msg = fulfillment + .order + .fill(&denom, fulfillment.amount, Decimal::one())?; + msgs.push(msg); + if fulfillment.order.quantity.is_zero() { + orders().remove( + storage, + &( + fulfillment.order.book_id, + fulfillment.order.tick_id, + fulfillment.order.order_id, + ), + )?; + } else { + orders().save( + storage, + &( + fulfillment.order.book_id, + fulfillment.order.tick_id, + fulfillment.order.order_id, + ), + &fulfillment.order, + )?; + } + // TODO: possible optimization by grouping tick/liquidity and calling this once per tick? + reduce_tick_liquidity( + storage, + fulfillment.order.book_id, + fulfillment.order.tick_id, + fulfillment.amount, + )?; + } + Ok(msgs) +} diff --git a/contracts/orderbook/src/tests/test_order.rs b/contracts/orderbook/src/tests/test_order.rs index e78f5ec..4e7e793 100644 --- a/contracts/orderbook/src/tests/test_order.rs +++ b/contracts/orderbook/src/tests/test_order.rs @@ -1,11 +1,13 @@ -use crate::constants::*; -use crate::error::ContractError; -use crate::order::*; -use crate::orderbook::*; -use crate::state::*; -use crate::types::{LimitOrder, OrderDirection, REPLY_ID_REFUND}; +use crate::{ + constants::{MAX_TICK, MIN_TICK}, + error::ContractError, + order::*, + orderbook::*, + state::*, + types::{Fulfillment, LimitOrder, MarketOrder, OrderDirection, REPLY_ID_REFUND}, +}; use cosmwasm_std::testing::{mock_dependencies_with_balances, mock_env, mock_info}; -use cosmwasm_std::{coin, Addr, BankMsg, Coin, Empty, SubMsg, Uint128}; +use cosmwasm_std::{coin, Addr, BankMsg, Coin, Decimal, Empty, SubMsg, Uint128}; use cw_utils::PaymentError; #[allow(clippy::uninlined_format_args)] @@ -402,7 +404,6 @@ fn test_cancel_limit() { ), ) .unwrap(); - // Update tick liquidity TICK_LIQUIDITY .update( @@ -539,3 +540,1696 @@ fn test_cancel_limit() { assert!(liquidity.is_zero(), "{}", format_test_name(test.name)); } } + +struct ResolveFulfillmentsTestCase { + pub name: &'static str, + pub book_id: u64, + /// bool represents if order is removed + pub fulfillments: Vec<(Fulfillment, bool)>, + // (tick_id, liquidity) + pub expected_liquidity: Vec<(i64, Uint128)>, + pub expected_error: Option, +} + +#[test] +fn test_resolve_fulfillments() { + let valid_book_id = 0; + let test_cases: Vec = vec![ + ResolveFulfillmentsTestCase { + name: "standard fulfillments (single tick) ", + book_id: valid_book_id, + fulfillments: vec![ + ( + Fulfillment::new( + LimitOrder::new( + 0, + 1, + 0, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(100u128), + ), + Uint128::from(100u128), + ), + true, + ), + ( + Fulfillment::new( + LimitOrder::new( + 0, + 1, + 1, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(100u128), + ), + Uint128::from(50u128), + ), + false, + ), + ], + expected_liquidity: vec![(1, Uint128::from(50u128))], + expected_error: None, + }, + ResolveFulfillmentsTestCase { + name: "standard fulfillments (multi tick)", + book_id: valid_book_id, + fulfillments: vec![ + ( + Fulfillment::new( + LimitOrder::new( + 0, + 1, + 0, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(100u128), + ), + Uint128::from(100u128), + ), + true, + ), + ( + Fulfillment::new( + LimitOrder::new( + 0, + 1, + 1, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(100u128), + ), + Uint128::from(100u128), + ), + true, + ), + ( + Fulfillment::new( + LimitOrder::new( + 0, + 2, + 3, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(100u128), + ), + Uint128::from(100u128), + ), + true, + ), + ( + Fulfillment::new( + LimitOrder::new( + 0, + 2, + 4, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(100u128), + ), + Uint128::from(50u128), + ), + false, + ), + ], + expected_liquidity: vec![(1, Uint128::zero()), (2, Uint128::from(50u128))], + expected_error: None, + }, + ResolveFulfillmentsTestCase { + name: "Wrong order book", + book_id: valid_book_id, + fulfillments: vec![ + ( + Fulfillment::new( + LimitOrder::new( + 0, + 1, + 0, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(100u128), + ), + Uint128::from(100u128), + ), + true, + ), + ( + Fulfillment::new( + LimitOrder::new( + 1, + 1, + 1, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(100u128), + ), + Uint128::from(100u128), + ), + true, + ), + ], + expected_liquidity: vec![(1, Uint128::zero())], + expected_error: Some(ContractError::InvalidFulfillment { + order_id: 1, + book_id: 1, + amount_required: Uint128::from(100u128), + amount_remaining: Uint128::from(100u128), + reason: Some("Fulfillment is part of another order book".to_string()), + }), + }, + ResolveFulfillmentsTestCase { + name: "Invalid fulfillment (insufficient funds)", + book_id: valid_book_id, + fulfillments: vec![( + Fulfillment::new( + LimitOrder::new( + 0, + 0, + 0, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(100u128), + ), + Uint128::from(200u128), + ), + true, + )], + expected_liquidity: vec![(1, Uint128::zero())], + expected_error: Some(ContractError::InvalidFulfillment { + order_id: 0, + book_id: 0, + amount_required: Uint128::from(200u128), + amount_remaining: Uint128::from(100u128), + reason: Some("Order does not have enough funds".to_string()), + }), + }, + ]; + + for test in test_cases { + let mut deps = mock_dependencies_with_balances(&[]); + let env = mock_env(); + let info = mock_info("maker", &[]); + + // Create an orderbook to operate on + let quote_denom = "quote".to_string(); + let base_denom = "base".to_string(); + create_orderbook( + deps.as_mut(), + env.clone(), + info.clone(), + quote_denom.clone(), + base_denom.clone(), + ) + .unwrap(); + + let fulfillments = test + .fulfillments + .iter() + .map(|f| f.clone().0) + .collect::>(); + + // Add orders to state + for Fulfillment { order, .. } in fulfillments.clone() { + orders() + .save( + deps.as_mut().storage, + &(order.book_id, order.tick_id, order.order_id), + &order, + ) + .unwrap(); + TICK_LIQUIDITY + .update( + deps.as_mut().storage, + &(order.book_id, order.tick_id), + |l| { + Ok::( + l.unwrap_or_default().checked_add(order.quantity).unwrap(), + ) + }, + ) + .unwrap(); + } + + let response = resolve_fulfillments(deps.as_mut().storage, fulfillments); + + // -- POST STATE -- + + if let Some(expected_error) = &test.expected_error { + let err = response.unwrap_err(); + assert_eq!(err, *expected_error, "{}", format_test_name(test.name)); + // NOTE: We cannot check if orders/tick liquidity were unaltered as changes are made in a for loop that is not rolled back upon error + + continue; + } + + // Check tick liquidity updated as expected + for (tick_id, expected_liquidity) in test.expected_liquidity { + let liquidity = TICK_LIQUIDITY + .may_load(deps.as_ref().storage, &(test.book_id, tick_id)) + .unwrap(); + assert_eq!( + liquidity.is_none(), + expected_liquidity.is_zero(), + "{}", + format_test_name(test.name) + ); + if let Some(post_liquidity) = liquidity { + assert_eq!( + post_liquidity, + expected_liquidity, + "{}", + format_test_name(test.name) + ); + } + } + + let orderbook = ORDERBOOKS + .load(deps.as_ref().storage, &valid_book_id) + .unwrap(); + + let response = response.unwrap(); + + for (idx, (Fulfillment { order, amount }, removed)) in test.fulfillments.iter().enumerate() + { + let saved_order = orders() + .may_load( + deps.as_ref().storage, + &(order.book_id, order.tick_id, order.order_id), + ) + .unwrap(); + // Check order is updated as expected + assert_eq!( + saved_order.is_none(), + *removed, + "{}", + format_test_name(test.name) + ); + // If not removed check quantity updated + if !removed { + assert_eq!( + saved_order.unwrap().quantity, + order.quantity.checked_sub(*amount).unwrap(), + "{}", + format_test_name(test.name) + ); + } + + // Check message is generated as expected + let mut order = order.clone(); + let denom = orderbook.get_expected_denom(&order.order_direction); + let msg = order.fill(denom, *amount, Decimal::one()).unwrap(); + + assert_eq!(response[idx], msg, "{}", format_test_name(test.name)); + } + } +} + +struct RunMarketOrderTestCase { + pub name: &'static str, + pub placed_order: MarketOrder, + pub tick_bound: Option, + pub extra_orders: Vec, + pub expected_fulfillments: Vec, + pub expected_remainder: Uint128, + pub expected_error: Option, +} + +#[test] +fn test_run_market_order() { + let valid_book_id = 0; + let test_cases: Vec = vec![ + RunMarketOrderTestCase { + name: "standard market order (single tick) ASK", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(100u128), + OrderDirection::Ask, + Addr::unchecked("creator"), + ), + tick_bound: None, + extra_orders: vec![], + expected_fulfillments: vec![ + Fulfillment::new( + LimitOrder::new( + valid_book_id, + -1, + 0, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(50u128), + ), + Uint128::from(50u128), + ), + Fulfillment::new( + LimitOrder::new( + valid_book_id, + -1, + 1, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(150u128), + ), + Uint128::from(50u128), + ), + ], + expected_remainder: Uint128::zero(), + expected_error: None, + }, + RunMarketOrderTestCase { + name: "standard market order (multi tick) ASK", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(100u128), + OrderDirection::Ask, + Addr::unchecked("creator"), + ), + tick_bound: None, + extra_orders: vec![], + expected_fulfillments: vec![ + Fulfillment::new( + LimitOrder::new( + valid_book_id, + -1, + 0, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(50u128), + ), + Uint128::from(50u128), + ), + Fulfillment::new( + LimitOrder::new( + valid_book_id, + -2, + 1, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(150u128), + ), + Uint128::from(50u128), + ), + ], + expected_remainder: Uint128::zero(), + expected_error: None, + }, + RunMarketOrderTestCase { + name: "excessive market order (single tick) ASK", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(1000u128), + OrderDirection::Ask, + Addr::unchecked("creator"), + ), + tick_bound: None, + extra_orders: vec![], + expected_fulfillments: vec![ + Fulfillment::new( + LimitOrder::new( + valid_book_id, + -1, + 0, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(50u128), + ), + Uint128::from(50u128), + ), + Fulfillment::new( + LimitOrder::new( + valid_book_id, + -2, + 1, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(150u128), + ), + Uint128::from(150u128), + ), + ], + expected_remainder: Uint128::from(800u128), + expected_error: None, + }, + RunMarketOrderTestCase { + name: "standard market order (no tick) ASK", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(1000u128), + OrderDirection::Ask, + Addr::unchecked("creator"), + ), + tick_bound: None, + extra_orders: vec![], + expected_fulfillments: vec![], + expected_remainder: Uint128::from(1000u128), + expected_error: None, + }, + RunMarketOrderTestCase { + name: "standard market order (multi tick - bound) ASK", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(100u128), + OrderDirection::Ask, + Addr::unchecked("creator"), + ), + tick_bound: Some(-1), + extra_orders: vec![LimitOrder::new( + valid_book_id, + -2, + 1, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(150u128), + )], + expected_fulfillments: vec![Fulfillment::new( + LimitOrder::new( + valid_book_id, + -1, + 0, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(50u128), + ), + Uint128::from(50u128), + )], + expected_remainder: Uint128::from(50u128), + expected_error: None, + }, + RunMarketOrderTestCase { + name: "invalid ASK tick bound", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(100u128), + OrderDirection::Ask, + Addr::unchecked("creator"), + ), + tick_bound: Some(1), + extra_orders: vec![LimitOrder::new( + valid_book_id, + -2, + 1, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(150u128), + )], + expected_fulfillments: vec![Fulfillment::new( + LimitOrder::new( + valid_book_id, + -1, + 0, + OrderDirection::Bid, + Addr::unchecked("creator"), + Uint128::from(50u128), + ), + Uint128::from(50u128), + )], + expected_remainder: Uint128::from(50u128), + expected_error: Some(ContractError::InvalidTickId { tick_id: 1 }), + }, + RunMarketOrderTestCase { + name: "standard market order (single tick) BID", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(100u128), + OrderDirection::Bid, + Addr::unchecked("creator"), + ), + tick_bound: None, + extra_orders: vec![], + expected_fulfillments: vec![ + Fulfillment::new( + LimitOrder::new( + valid_book_id, + 1, + 0, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(50u128), + ), + Uint128::from(50u128), + ), + Fulfillment::new( + LimitOrder::new( + valid_book_id, + 1, + 1, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(150u128), + ), + Uint128::from(50u128), + ), + ], + expected_remainder: Uint128::zero(), + expected_error: None, + }, + RunMarketOrderTestCase { + name: "standard market order (multi tick) BID", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(100u128), + OrderDirection::Bid, + Addr::unchecked("creator"), + ), + tick_bound: None, + extra_orders: vec![], + expected_fulfillments: vec![ + Fulfillment::new( + LimitOrder::new( + valid_book_id, + 1, + 0, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(50u128), + ), + Uint128::from(50u128), + ), + Fulfillment::new( + LimitOrder::new( + valid_book_id, + 2, + 1, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(150u128), + ), + Uint128::from(50u128), + ), + ], + expected_remainder: Uint128::zero(), + expected_error: None, + }, + RunMarketOrderTestCase { + name: "excessive market order (single tick) BID", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(1000u128), + OrderDirection::Bid, + Addr::unchecked("creator"), + ), + tick_bound: None, + extra_orders: vec![], + expected_fulfillments: vec![ + Fulfillment::new( + LimitOrder::new( + valid_book_id, + 1, + 0, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(50u128), + ), + Uint128::from(50u128), + ), + Fulfillment::new( + LimitOrder::new( + valid_book_id, + 2, + 1, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(150u128), + ), + Uint128::from(150u128), + ), + ], + expected_remainder: Uint128::from(800u128), + expected_error: None, + }, + RunMarketOrderTestCase { + name: "standard market order (no tick) BID", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(1000u128), + OrderDirection::Bid, + Addr::unchecked("creator"), + ), + tick_bound: None, + extra_orders: vec![], + expected_fulfillments: vec![], + expected_remainder: Uint128::from(1000u128), + expected_error: None, + }, + RunMarketOrderTestCase { + name: "standard market order (multi tick - bound) BID", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(100u128), + OrderDirection::Bid, + Addr::unchecked("creator"), + ), + extra_orders: vec![LimitOrder::new( + valid_book_id, + 2, + 1, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(150u128), + )], + tick_bound: Some(1), + expected_fulfillments: vec![Fulfillment::new( + LimitOrder::new( + valid_book_id, + 1, + 0, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(50u128), + ), + Uint128::from(50u128), + )], + expected_remainder: Uint128::from(50u128), + expected_error: None, + }, + RunMarketOrderTestCase { + name: "invalid BID tick bound", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(100u128), + OrderDirection::Bid, + Addr::unchecked("creator"), + ), + extra_orders: vec![LimitOrder::new( + valid_book_id, + 2, + 1, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(150u128), + )], + tick_bound: Some(0), + expected_fulfillments: vec![Fulfillment::new( + LimitOrder::new( + valid_book_id, + 1, + 0, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(50u128), + ), + Uint128::from(50u128), + )], + expected_remainder: Uint128::from(50u128), + expected_error: Some(ContractError::InvalidTickId { tick_id: 0 }), + }, + RunMarketOrderTestCase { + name: "tick too large", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(100u128), + OrderDirection::Bid, + Addr::unchecked("creator"), + ), + extra_orders: vec![LimitOrder::new( + valid_book_id, + 2, + 1, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(150u128), + )], + tick_bound: Some(MAX_TICK + 1), + expected_fulfillments: vec![Fulfillment::new( + LimitOrder::new( + valid_book_id, + 1, + 0, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(50u128), + ), + Uint128::from(50u128), + )], + expected_remainder: Uint128::from(50u128), + expected_error: Some(ContractError::InvalidTickId { + tick_id: MAX_TICK + 1, + }), + }, + RunMarketOrderTestCase { + name: "tick too small", + placed_order: MarketOrder::new( + valid_book_id, + Uint128::from(100u128), + OrderDirection::Bid, + Addr::unchecked("creator"), + ), + extra_orders: vec![LimitOrder::new( + valid_book_id, + 2, + 1, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(150u128), + )], + tick_bound: Some(MIN_TICK - 1), + expected_fulfillments: vec![Fulfillment::new( + LimitOrder::new( + valid_book_id, + 1, + 0, + OrderDirection::Ask, + Addr::unchecked("creator"), + Uint128::from(50u128), + ), + Uint128::from(50u128), + )], + expected_remainder: Uint128::from(50u128), + expected_error: Some(ContractError::InvalidTickId { + tick_id: MIN_TICK - 1, + }), + }, + ]; + + for test in test_cases { + let mut deps = mock_dependencies_with_balances(&[]); + let env = mock_env(); + let info = mock_info("maker", &[]); + + // Create an orderbook to operate on + let quote_denom = "quote".to_string(); + let base_denom = "base".to_string(); + create_orderbook( + deps.as_mut(), + env.clone(), + info.clone(), + quote_denom.clone(), + base_denom.clone(), + ) + .unwrap(); + + let fulfillments = test.expected_fulfillments.to_vec(); + let mut all_orders: Vec = fulfillments + .iter() + .map(|Fulfillment { order, .. }| order.clone()) + .collect(); + all_orders.extend(test.extra_orders); + + // Add orders to state + for order in all_orders.clone() { + orders() + .save( + deps.as_mut().storage, + &(order.book_id, order.tick_id, order.order_id), + &order, + ) + .unwrap(); + TICK_LIQUIDITY + .update( + deps.as_mut().storage, + &(order.book_id, order.tick_id), + |l| { + Ok::( + l.unwrap_or_default().checked_add(order.quantity).unwrap(), + ) + }, + ) + .unwrap(); + + let mut orderbook = ORDERBOOKS + .load(deps.as_ref().storage, &valid_book_id) + .unwrap(); + match order.order_direction { + OrderDirection::Ask => { + if order.tick_id < orderbook.next_ask_tick { + orderbook.next_ask_tick = order.tick_id; + } + ORDERBOOKS + .save(deps.as_mut().storage, &valid_book_id, &orderbook) + .unwrap(); + } + OrderDirection::Bid => { + if order.tick_id > orderbook.next_bid_tick { + orderbook.next_bid_tick = order.tick_id; + } + ORDERBOOKS + .save(deps.as_mut().storage, &valid_book_id, &orderbook) + .unwrap(); + } + } + } + + let mut market_order = test.placed_order.clone(); + let response = run_market_order(deps.as_mut().storage, &mut market_order, test.tick_bound); + + // -- POST STATE -- + + if let Some(expected_error) = &test.expected_error { + let err = response.unwrap_err(); + assert_eq!(err, *expected_error, "{}", format_test_name(test.name)); + + continue; + } + + let response = response.unwrap(); + + for (idx, fulfillment) in test.expected_fulfillments.iter().enumerate() { + // Check fulfillment is generated as expected + assert_eq!( + response.0[idx], + *fulfillment, + "{}", + format_test_name(test.name) + ); + } + + assert_eq!( + market_order.quantity, + test.expected_remainder, + "{}", + format_test_name(test.name) + ); + } +} + +// TODO: Merge in to place limit test cases and remove +// struct RunLimitOrderTestCase { +// pub name: &'static str, +// pub order: LimitOrder, +// pub expected_fulfillments: Vec, +// pub expected_bank_msgs: Vec, +// pub expected_liquidity: Vec<(i64, Uint128)>, +// pub expected_remainder: Uint128, +// pub expected_error: Option, +// } + +// #[test] +// fn test_run_limit_order() { +// let valid_book_id = 0; +// let test_cases: Vec = vec![ +// RunLimitOrderTestCase { +// name: "run limit order with single fulfillment ASK", +// order: LimitOrder::new( +// valid_book_id, +// -1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("creator"), +// Uint128::from(50u128), +// ), +// expected_fulfillments: vec![Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// -1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("maker"), +// Uint128::from(50u128), +// ), +// Uint128::from(50u128), +// )], +// expected_bank_msgs: vec![ +// BankMsg::Send { +// to_address: "maker".to_string(), +// amount: vec![coin(50, "quote")], +// }, +// BankMsg::Send { +// to_address: "creator".to_string(), +// amount: vec![coin(50, "base")], +// }, +// ], +// expected_liquidity: vec![(-1, Uint128::zero())], +// expected_remainder: Uint128::zero(), +// expected_error: None, +// }, +// RunLimitOrderTestCase { +// name: "run limit order with multiple fulfillments ASK", +// order: LimitOrder::new( +// valid_book_id, +// -1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("creator"), +// Uint128::from(100u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// -1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// -1, +// 1, +// OrderDirection::Bid, +// Addr::unchecked("maker2"), +// Uint128::from(75u128), +// ), +// Uint128::from(75u128), +// ), +// ], +// expected_bank_msgs: vec![ +// BankMsg::Send { +// to_address: "maker1".to_string(), +// amount: vec![coin(25, "quote")], +// }, +// BankMsg::Send { +// to_address: "maker2".to_string(), +// amount: vec![coin(75, "quote")], +// }, +// BankMsg::Send { +// to_address: "creator".to_string(), +// amount: vec![coin(100, "base")], +// }, +// ], +// expected_liquidity: vec![(-1, Uint128::zero())], +// expected_remainder: Uint128::zero(), +// expected_error: None, +// }, +// RunLimitOrderTestCase { +// name: "run limit order with multiple fulfillments across multiple ticks ASK", +// order: LimitOrder::new( +// valid_book_id, +// -3, +// 2, +// OrderDirection::Ask, +// Addr::unchecked("creator"), +// Uint128::from(100u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// -1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// -2, +// 1, +// OrderDirection::Bid, +// Addr::unchecked("maker2"), +// Uint128::from(75u128), +// ), +// Uint128::from(75u128), +// ), +// ], +// expected_bank_msgs: vec![ +// BankMsg::Send { +// to_address: "maker1".to_string(), +// amount: vec![coin(25, "quote")], +// }, +// BankMsg::Send { +// to_address: "maker2".to_string(), +// amount: vec![coin(75, "quote")], +// }, +// BankMsg::Send { +// to_address: "creator".to_string(), +// amount: vec![coin(100, "base")], +// }, +// ], +// expected_liquidity: vec![(-1, Uint128::zero()), (-2, Uint128::zero())], +// expected_remainder: Uint128::zero(), +// expected_error: None, +// }, +// RunLimitOrderTestCase { +// name: "run limit order with multiple fulfillments w/ partial ASK", +// order: LimitOrder::new( +// valid_book_id, +// -1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("creator"), +// Uint128::from(100u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// -1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// -1, +// 1, +// OrderDirection::Bid, +// Addr::unchecked("maker2"), +// Uint128::from(150u128), +// ), +// Uint128::from(50u128), +// ), +// ], +// expected_bank_msgs: vec![ +// BankMsg::Send { +// to_address: "maker1".to_string(), +// amount: vec![coin(25, "quote")], +// }, +// BankMsg::Send { +// to_address: "maker2".to_string(), +// amount: vec![coin(75, "quote")], +// }, +// BankMsg::Send { +// to_address: "creator".to_string(), +// amount: vec![coin(100, "base")], +// }, +// ], +// expected_liquidity: vec![(-1, Uint128::from(75u128))], +// expected_remainder: Uint128::zero(), +// expected_error: None, +// }, +// RunLimitOrderTestCase { +// name: "run limit order with multiple fulfillments w/ remainder ASK", +// order: LimitOrder::new( +// valid_book_id, +// -1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("creator"), +// Uint128::from(1000u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// -1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// -1, +// 1, +// OrderDirection::Bid, +// Addr::unchecked("maker2"), +// Uint128::from(150u128), +// ), +// Uint128::from(150u128), +// ), +// ], +// expected_bank_msgs: vec![ +// BankMsg::Send { +// to_address: "maker1".to_string(), +// amount: vec![coin(25, "quote")], +// }, +// BankMsg::Send { +// to_address: "maker2".to_string(), +// amount: vec![coin(150, "quote")], +// }, +// BankMsg::Send { +// to_address: "creator".to_string(), +// amount: vec![coin(175, "base")], +// }, +// ], +// expected_liquidity: vec![(-1, Uint128::zero())], +// expected_remainder: Uint128::from(825u128), +// expected_error: None, +// }, +// RunLimitOrderTestCase { +// name: "invalid tick ASK", +// order: LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("creator"), +// Uint128::from(100u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// -1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// -1, +// 1, +// OrderDirection::Bid, +// Addr::unchecked("maker2"), +// Uint128::from(150u128), +// ), +// Uint128::from(50u128), +// ), +// ], +// expected_bank_msgs: vec![], +// expected_liquidity: vec![], +// expected_remainder: Uint128::zero(), +// expected_error: Some(ContractError::InvalidTickId { tick_id: 1 }), +// }, +// RunLimitOrderTestCase { +// name: "run limit order with single fulfillment BID", +// order: LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("creator"), +// Uint128::from(50u128), +// ), +// expected_fulfillments: vec![Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("maker"), +// Uint128::from(50u128), +// ), +// Uint128::from(50u128), +// )], +// expected_bank_msgs: vec![ +// BankMsg::Send { +// to_address: "maker".to_string(), +// amount: vec![coin(50, "base")], +// }, +// BankMsg::Send { +// to_address: "creator".to_string(), +// amount: vec![coin(50, "quote")], +// }, +// ], +// expected_liquidity: vec![(1, Uint128::zero())], +// expected_remainder: Uint128::zero(), +// expected_error: None, +// }, +// RunLimitOrderTestCase { +// name: "run limit order with multiple fulfillments BID", +// order: LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("creator"), +// Uint128::from(100u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 1, +// OrderDirection::Ask, +// Addr::unchecked("maker2"), +// Uint128::from(75u128), +// ), +// Uint128::from(75u128), +// ), +// ], +// expected_bank_msgs: vec![ +// BankMsg::Send { +// to_address: "maker1".to_string(), +// amount: vec![coin(25, "base")], +// }, +// BankMsg::Send { +// to_address: "maker2".to_string(), +// amount: vec![coin(75, "base")], +// }, +// BankMsg::Send { +// to_address: "creator".to_string(), +// amount: vec![coin(100, "quote")], +// }, +// ], +// expected_liquidity: vec![(1, Uint128::zero())], +// expected_remainder: Uint128::zero(), +// expected_error: None, +// }, +// RunLimitOrderTestCase { +// name: "run limit order with multiple fulfillments across multiple ticks BID", +// order: LimitOrder::new( +// valid_book_id, +// 3, +// 2, +// OrderDirection::Bid, +// Addr::unchecked("creator"), +// Uint128::from(100u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 2, +// 1, +// OrderDirection::Ask, +// Addr::unchecked("maker2"), +// Uint128::from(75u128), +// ), +// Uint128::from(75u128), +// ), +// ], +// expected_bank_msgs: vec![ +// BankMsg::Send { +// to_address: "maker1".to_string(), +// amount: vec![coin(25, "base")], +// }, +// BankMsg::Send { +// to_address: "maker2".to_string(), +// amount: vec![coin(75, "base")], +// }, +// BankMsg::Send { +// to_address: "creator".to_string(), +// amount: vec![coin(100, "quote")], +// }, +// ], +// expected_liquidity: vec![(1, Uint128::zero()), (2, Uint128::zero())], +// expected_remainder: Uint128::zero(), +// expected_error: None, +// }, +// RunLimitOrderTestCase { +// name: "run limit order with multiple fulfillments w/ partial BID", +// order: LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("creator"), +// Uint128::from(100u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 1, +// OrderDirection::Ask, +// Addr::unchecked("maker2"), +// Uint128::from(150u128), +// ), +// Uint128::from(50u128), +// ), +// ], +// expected_bank_msgs: vec![ +// BankMsg::Send { +// to_address: "maker1".to_string(), +// amount: vec![coin(25, "base")], +// }, +// BankMsg::Send { +// to_address: "maker2".to_string(), +// amount: vec![coin(75, "base")], +// }, +// BankMsg::Send { +// to_address: "creator".to_string(), +// amount: vec![coin(100, "quote")], +// }, +// ], +// expected_liquidity: vec![(1, Uint128::from(75u128))], +// expected_remainder: Uint128::zero(), +// expected_error: None, +// }, +// RunLimitOrderTestCase { +// name: "run limit order with multiple fulfillments w/ remainder BID", +// order: LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("creator"), +// Uint128::from(1000u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 1, +// OrderDirection::Ask, +// Addr::unchecked("maker2"), +// Uint128::from(150u128), +// ), +// Uint128::from(150u128), +// ), +// ], +// expected_bank_msgs: vec![ +// BankMsg::Send { +// to_address: "maker1".to_string(), +// amount: vec![coin(25, "base")], +// }, +// BankMsg::Send { +// to_address: "maker2".to_string(), +// amount: vec![coin(150, "base")], +// }, +// BankMsg::Send { +// to_address: "creator".to_string(), +// amount: vec![coin(175, "quote")], +// }, +// ], +// expected_liquidity: vec![(1, Uint128::zero())], +// expected_remainder: Uint128::from(825u128), +// expected_error: None, +// }, +// RunLimitOrderTestCase { +// name: "invalid tick BID", +// order: LimitOrder::new( +// valid_book_id, +// -1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("creator"), +// Uint128::from(100u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 1, +// OrderDirection::Ask, +// Addr::unchecked("maker2"), +// Uint128::from(150u128), +// ), +// Uint128::from(50u128), +// ), +// ], +// expected_bank_msgs: vec![], +// expected_liquidity: vec![], +// expected_remainder: Uint128::zero(), +// expected_error: Some(ContractError::InvalidTickId { tick_id: -1 }), +// }, +// RunLimitOrderTestCase { +// name: "mismatched order direction", +// order: LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("creator"), +// Uint128::from(100u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 1, +// OrderDirection::Bid, +// Addr::unchecked("maker2"), +// Uint128::from(150u128), +// ), +// Uint128::from(50u128), +// ), +// ], +// expected_bank_msgs: vec![], +// expected_liquidity: vec![], +// expected_remainder: Uint128::zero(), +// expected_error: Some(ContractError::MismatchedOrderDirection {}), +// }, +// RunLimitOrderTestCase { +// name: "tick too large", +// order: LimitOrder::new( +// valid_book_id, +// MAX_TICK + 1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("creator"), +// Uint128::from(100u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 1, +// OrderDirection::Ask, +// Addr::unchecked("maker2"), +// Uint128::from(150u128), +// ), +// Uint128::from(50u128), +// ), +// ], +// expected_bank_msgs: vec![], +// expected_liquidity: vec![], +// expected_remainder: Uint128::zero(), +// expected_error: Some(ContractError::InvalidTickId { +// tick_id: MAX_TICK + 1, +// }), +// }, +// RunLimitOrderTestCase { +// name: "tick too small", +// order: LimitOrder::new( +// valid_book_id, +// MIN_TICK - 1, +// 0, +// OrderDirection::Bid, +// Addr::unchecked("creator"), +// Uint128::from(100u128), +// ), +// expected_fulfillments: vec![ +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 0, +// OrderDirection::Ask, +// Addr::unchecked("maker1"), +// Uint128::from(25u128), +// ), +// Uint128::from(25u128), +// ), +// Fulfillment::new( +// LimitOrder::new( +// valid_book_id, +// 1, +// 1, +// OrderDirection::Ask, +// Addr::unchecked("maker2"), +// Uint128::from(150u128), +// ), +// Uint128::from(50u128), +// ), +// ], +// expected_bank_msgs: vec![], +// expected_liquidity: vec![], +// expected_remainder: Uint128::zero(), +// expected_error: Some(ContractError::InvalidTickId { +// tick_id: MIN_TICK - 1, +// }), +// }, +// ]; + +// for test in test_cases { +// let mut deps = mock_dependencies_with_balances(&[]); +// let env = mock_env(); +// let info = mock_info("maker", &[]); + +// // Create an orderbook to operate on +// let quote_denom = "quote".to_string(); +// let base_denom = "base".to_string(); +// create_orderbook( +// deps.as_mut(), +// env.clone(), +// info.clone(), +// quote_denom.clone(), +// base_denom.clone(), +// ) +// .unwrap(); + +// let fulfillments = test.expected_fulfillments.to_vec(); +// let all_orders: Vec = fulfillments +// .iter() +// .map(|Fulfillment { order, .. }| order.clone()) +// .collect(); + +// // Add orders to state +// for order in all_orders.clone() { +// orders() +// .save( +// deps.as_mut().storage, +// &(order.book_id, order.tick_id, order.order_id), +// &order, +// ) +// .unwrap(); +// TICK_LIQUIDITY +// .update( +// deps.as_mut().storage, +// &(order.book_id, order.tick_id), +// |l| { +// Ok::( +// l.unwrap_or_default().checked_add(order.quantity).unwrap(), +// ) +// }, +// ) +// .unwrap(); + +// let mut orderbook = ORDERBOOKS +// .load(deps.as_ref().storage, &valid_book_id) +// .unwrap(); +// match order.order_direction { +// OrderDirection::Ask => { +// if order.tick_id < orderbook.next_ask_tick { +// orderbook.next_ask_tick = order.tick_id; +// } +// ORDERBOOKS +// .save(deps.as_mut().storage, &valid_book_id, &orderbook) +// .unwrap(); +// } +// OrderDirection::Bid => { +// if order.tick_id > orderbook.next_bid_tick { +// orderbook.next_bid_tick = order.tick_id; +// } +// ORDERBOOKS +// .save(deps.as_mut().storage, &valid_book_id, &orderbook) +// .unwrap(); +// } +// } +// } + +// let mut order = test.order.clone(); +// let response = run_limit_order(deps.as_mut().storage, &mut order); +// if let Some(expected_error) = &test.expected_error { +// let err = response.unwrap_err(); +// assert_eq!(err, *expected_error, "{}", format_test_name(test.name)); + +// continue; +// } + +// let bank_msgs = response.unwrap(); + +// for (tick_id, expected_liquidity) in test.expected_liquidity { +// let maybe_current_liquidity = TICK_LIQUIDITY +// .may_load(deps.as_ref().storage, &(valid_book_id, tick_id)) +// .unwrap(); + +// if expected_liquidity.is_zero() { +// assert!( +// maybe_current_liquidity.is_none(), +// "{}", +// format_test_name(test.name) +// ); +// } else { +// assert_eq!( +// maybe_current_liquidity.unwrap(), +// expected_liquidity, +// "{}", +// format_test_name(test.name) +// ); +// } +// } + +// for fulfillment in test.expected_fulfillments { +// if fulfillment.amount == fulfillment.order.quantity { +// let maybe_order = orders() +// .may_load( +// deps.as_ref().storage, +// &( +// fulfillment.order.book_id, +// fulfillment.order.tick_id, +// fulfillment.order.order_id, +// ), +// ) +// .unwrap(); +// assert!(maybe_order.is_none(), "{}", format_test_name(test.name)); +// } +// } + +// assert_eq!( +// test.expected_bank_msgs, +// bank_msgs, +// "{}", +// format_test_name(test.name) +// ); + +// assert_eq!( +// order.quantity, +// test.expected_remainder, +// "{}", +// format_test_name(test.name) +// ); +// } +// } diff --git a/contracts/orderbook/src/types/fulfillment.rs b/contracts/orderbook/src/types/fulfillment.rs new file mode 100644 index 0000000..fac98f2 --- /dev/null +++ b/contracts/orderbook/src/types/fulfillment.rs @@ -0,0 +1,16 @@ +use cosmwasm_std::Uint128; + +use super::LimitOrder; + +// Describes orders to be fulfilllled as part of a market order or a converted limit order +#[derive(Clone, Debug, PartialEq)] +pub struct Fulfillment { + pub order: LimitOrder, + pub amount: Uint128, +} + +impl Fulfillment { + pub fn new(order: LimitOrder, amount: Uint128) -> Self { + Self { order, amount } + } +} diff --git a/contracts/orderbook/src/types/mod.rs b/contracts/orderbook/src/types/mod.rs index 30cb5df..6f867ff 100644 --- a/contracts/orderbook/src/types/mod.rs +++ b/contracts/orderbook/src/types/mod.rs @@ -1,7 +1,9 @@ +mod fulfillment; mod order; mod orderbook; mod reply_id; +pub use self::fulfillment::*; pub use self::order::*; pub use self::orderbook::*; pub use self::reply_id::*; diff --git a/contracts/orderbook/src/types/order.rs b/contracts/orderbook/src/types/order.rs index 64c6ae1..2e1c147 100644 --- a/contracts/orderbook/src/types/order.rs +++ b/contracts/orderbook/src/types/order.rs @@ -1,5 +1,7 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::{coin, ensure, Addr, BankMsg, Decimal, Uint128}; + +use crate::ContractError; #[cw_serde] #[derive(Copy)] @@ -36,6 +38,69 @@ impl LimitOrder { quantity, } } + + // Transfers the specified quantity of the order's asset to the owner + pub fn fill( + &mut self, + denom: impl Into, + quantity: Uint128, + price: Decimal, + ) -> Result { + ensure!( + self.quantity >= quantity, + ContractError::InvalidFulfillment { + order_id: self.order_id, + book_id: self.book_id, + amount_required: quantity, + amount_remaining: self.quantity, + reason: Some("Order does not have enough funds".to_string()) + } + ); + self.quantity = self.quantity.checked_sub(quantity)?; + Ok(BankMsg::Send { + to_address: self.owner.to_string(), + amount: vec![coin( + // TODO: Add From for error + quantity.checked_mul_floor(price).unwrap().u128(), + denom.into(), + )], + }) + } +} + +#[cw_serde] +pub struct MarketOrder { + pub book_id: u64, + pub quantity: Uint128, + pub order_direction: OrderDirection, + pub owner: Addr, +} + +impl MarketOrder { + pub fn new( + book_id: u64, + quantity: Uint128, + order_direction: OrderDirection, + owner: Addr, + ) -> Self { + MarketOrder { + book_id, + quantity, + order_direction, + owner, + } + } +} + +impl From for MarketOrder { + fn from(limit_order: LimitOrder) -> Self { + MarketOrder { + book_id: limit_order.book_id, + quantity: limit_order.quantity, + order_direction: limit_order.order_direction, + owner: limit_order.owner, + } + } } /// Defines the different way an owners orders can be filtered, all enums filter by owner with each getting more finite