diff --git a/contracts/orderbook/src/contract.rs b/contracts/orderbook/src/contract.rs index 9188a60..6452678 100644 --- a/contracts/orderbook/src/contract.rs +++ b/contracts/orderbook/src/contract.rs @@ -48,9 +48,9 @@ pub fn migrate(_deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result { match msg { @@ -60,16 +60,21 @@ 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), + ExecuteMsg::PlaceLimit { + book_id, + tick_id, + order_direction, + quantity, + } => order::place_limit(deps, env, info, book_id, tick_id, order_direction, quantity), // Cancels limit order with given ID - ExecuteMsg::CancelLimit => order::cancel_limit(_deps, _env, _info), + ExecuteMsg::CancelLimit => order::cancel_limit(deps, env, info), // Places a market order on the passed in market - ExecuteMsg::PlaceMarket => order::place_market(_deps, _env, _info), + ExecuteMsg::PlaceMarket => order::place_market(deps, env, info), } } diff --git a/contracts/orderbook/src/error.rs b/contracts/orderbook/src/error.rs index dc19f10..d41ad6a 100644 --- a/contracts/orderbook/src/error.rs +++ b/contracts/orderbook/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{StdError, Uint128}; use thiserror::Error; #[derive(Error, Debug)] @@ -8,4 +8,16 @@ pub enum ContractError { #[error("Unauthorized")] Unauthorized {}, + + #[error("Invalid tick ID: {tick_id:?}")] + InvalidTickId { tick_id: i64 }, + + #[error("Invalid quantity: {quantity:?}")] + InvalidQuantity { quantity: Uint128 }, + + #[error("Insufficient funds. Balance: {balance:?}, Required: {required:?}")] + InsufficientFunds { balance: Uint128, required: Uint128 }, + + #[error("Invalid book ID: {book_id:?}")] + InvalidBookId { book_id: u64 }, } diff --git a/contracts/orderbook/src/msg.rs b/contracts/orderbook/src/msg.rs index 732edc5..2033316 100644 --- a/contracts/orderbook/src/msg.rs +++ b/contracts/orderbook/src/msg.rs @@ -1,4 +1,6 @@ +use crate::types::OrderDirection; use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Uint128; /// Message type for `instantiate` entry_point #[cw_serde] @@ -7,14 +9,17 @@ pub struct InstantiateMsg {} /// Message type for `execute` entry_point #[cw_serde] pub enum ExecuteMsg { - // === Orderbook === - - CreateOrderbook{ + CreateOrderbook { quote_denom: String, base_denom: String, }, - PlaceLimit, + PlaceLimit { + book_id: u64, + tick_id: i64, + order_direction: OrderDirection, + quantity: Uint128, + }, CancelLimit, PlaceMarket, } diff --git a/contracts/orderbook/src/order.rs b/contracts/orderbook/src/order.rs index fdb03c9..5978518 100644 --- a/contracts/orderbook/src/order.rs +++ b/contracts/orderbook/src/order.rs @@ -1,16 +1,88 @@ -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use crate::error::ContractError; +use crate::state::*; +use crate::state::{MAX_TICK, MIN_TICK, ORDERBOOKS}; +use crate::types::{LimitOrder, OrderDirection}; +use cosmwasm_std::{ + coin, Addr, BalanceResponse, BankMsg, BankQuery, Coin, DepsMut, Env, MessageInfo, + QuerierWrapper, QueryRequest, Response, StdResult, Uint128, +}; + +// TODO: move this into a balance helper file +pub fn query_balance(querier: &QuerierWrapper, addr: &Addr, denom: &str) -> StdResult { + let res: BalanceResponse = querier.query(&QueryRequest::Bank(BankQuery::Balance { + address: addr.to_string(), + denom: denom.to_string(), + }))?; + Ok(Coin { + denom: denom.to_string(), + amount: res.amount.amount, + }) +} pub fn place_limit( - _deps: DepsMut, - _env: Env, + deps: DepsMut, + env: Env, info: MessageInfo, + book_id: u64, + tick_id: i64, + order_direction: OrderDirection, + quantity: Uint128, ) -> Result { - // TODO: Implement place_limit + // Validate book_id exists + let orderbook = ORDERBOOKS + .load(deps.storage, &book_id) + .map_err(|_| ContractError::InvalidBookId { book_id })?; + + // Validate tick_id is within valid range + if tick_id < MIN_TICK || tick_id > MAX_TICK { + return Err(ContractError::InvalidTickId { tick_id }); + } + + // Validate order_quantity is > 0 + if quantity.is_zero() { + return Err(ContractError::InvalidQuantity { quantity }); + } + + // Verify the sender has `quantity` balance of the correct denom + let denom = match order_direction { + OrderDirection::Bid => orderbook.quote_denom, + OrderDirection::Ask => orderbook.base_denom, + }; + let balance = query_balance(&deps.querier, &info.sender, &denom)?.amount; + if balance < quantity { + return Err(ContractError::InsufficientFunds { + balance, + required: quantity, + }); + } + + // Generate a new order ID + let order_id = new_order_id(deps.storage)?; + + let limit_order = LimitOrder::new( + book_id, + tick_id, + order_id, + order_direction.clone(), + info.sender.clone(), + quantity, + ); + + // Save the order to the orderbook + orders().save(deps.storage, &(book_id, tick_id, order_id), &limit_order)?; Ok(Response::new() + .add_message(BankMsg::Send { + to_address: env.contract.address.to_string(), + amount: vec![coin(quantity.u128(), denom)], + }) .add_attribute("method", "placeLimit") - .add_attribute("owner", info.sender)) + .add_attribute("owner", info.sender.to_string()) + .add_attribute("book_id", book_id.to_string()) + .add_attribute("tick_id", tick_id.to_string()) + .add_attribute("order_id", order_id.to_string()) + .add_attribute("order_direction", format!("{:?}", order_direction)) + .add_attribute("quantity", quantity.to_string())) } pub fn cancel_limit( @@ -35,4 +107,4 @@ pub fn place_market( Ok(Response::new() .add_attribute("method", "placeMarket") .add_attribute("owner", info.sender)) -} \ No newline at end of file +} diff --git a/contracts/orderbook/src/tests/mod.rs b/contracts/orderbook/src/tests/mod.rs index 38f2823..74f51d4 100644 --- a/contracts/orderbook/src/tests/mod.rs +++ b/contracts/orderbook/src/tests/mod.rs @@ -1,2 +1,3 @@ +pub mod test_order; pub mod test_orderbook; pub mod test_state; diff --git a/contracts/orderbook/src/tests/test_order.rs b/contracts/orderbook/src/tests/test_order.rs new file mode 100644 index 0000000..48b5c01 --- /dev/null +++ b/contracts/orderbook/src/tests/test_order.rs @@ -0,0 +1,255 @@ +use crate::error::ContractError; +use crate::order::place_limit; +use crate::orderbook::*; +use crate::state::*; +use crate::types::OrderDirection; +use cosmwasm_std::testing::{ + mock_dependencies, mock_dependencies_with_balances, mock_env, mock_info, +}; +use cosmwasm_std::{coin, Addr, Uint128}; + +#[test] +fn test_place_limit_order() { + let coin_vec = vec![coin(1000, "base")]; + let balances = [("creator", coin_vec.as_slice())]; + let mut deps = mock_dependencies_with_balances(&balances); + let env = mock_env(); + let info = mock_info("creator", &[]); + + // Create an orderbook first + let quote_denom = "quote".to_string(); + let base_denom = "base".to_string(); + let create_response = create_orderbook( + deps.as_mut(), + env.clone(), + info.clone(), + quote_denom, + base_denom, + ) + .unwrap(); + + // Retrieve the book_id from the first attribute of create_response + let book_id: u64 = create_response.attributes[1] + .value + .parse() + .expect("book_id attribute parse error"); + + // Parameters for place_limit call + let tick_id: i64 = 1; + let order_direction = OrderDirection::Ask; + let quantity = Uint128::new(100); + + // Assuming order_id starts at 1 for simplicity + let expected_order_id = 0; + + // Call the place_limit function + let response = place_limit( + deps.as_mut(), + env.clone(), + info.clone(), + book_id, + tick_id, + order_direction.clone(), + quantity, + ) + .unwrap(); + + // Assertions on the response + assert_eq!(response.attributes[0], ("method", "placeLimit")); + assert_eq!(response.attributes[1], ("owner", "creator")); + assert_eq!(response.attributes[2], ("book_id", book_id.to_string())); + assert_eq!(response.attributes[3], ("tick_id", tick_id.to_string())); + assert_eq!( + response.attributes[4], + ("order_id", expected_order_id.to_string()) + ); + assert_eq!( + response.attributes[5], + ("order_direction", format!("{:?}", order_direction)) + ); + assert_eq!(response.attributes[6], ("quantity", quantity.to_string())); + + // Retrieve the order from storage to verify it was saved correctly + let order = orders() + .load(&deps.storage, &(book_id, tick_id, expected_order_id)) + .unwrap(); + + // Verify the order's fields + assert_eq!(order.book_id, book_id); + assert_eq!(order.tick_id, tick_id); + assert_eq!(order.order_id, expected_order_id); + assert_eq!(order.order_direction, order_direction); + assert_eq!(order.owner, Addr::unchecked("creator")); + assert_eq!(order.quantity, Uint128::new(100)); +} + +#[test] +fn test_place_limit_with_invalid_book_id() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + + // Create an orderbook first + let quote_denom = "quote".to_string(); + let base_denom = "base".to_string(); + let create_response = create_orderbook( + deps.as_mut(), + env.clone(), + info.clone(), + quote_denom, + base_denom, + ) + .unwrap(); + + // Retrieve the book_id from the first attribute of create_response + let valid_book_id: u64 = create_response.attributes[1] + .value + .parse() + .expect("book_id attribute parse error"); + + let invalid_book_id = valid_book_id + 1; + + let response = place_limit( + deps.as_mut(), + env, + info, + invalid_book_id, + 1, // tick_id + OrderDirection::Ask, + Uint128::new(100), + ); + + assert!(matches!( + response, + Err(ContractError::InvalidBookId { book_id }) if book_id == invalid_book_id + )); +} + +#[test] +fn test_place_limit_with_invalid_tick_id() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + + // Create an orderbook first + let quote_denom = "quote".to_string(); + let base_denom = "base".to_string(); + let create_response = create_orderbook( + deps.as_mut(), + env.clone(), + info.clone(), + quote_denom, + base_denom, + ) + .unwrap(); + + // Retrieve the book_id from the first attribute of create_response + let book_id: u64 = create_response.attributes[1] + .value + .parse() + .expect("book_id attribute parse error"); + + let invalid_tick_id = MAX_TICK + 1; + + let response = place_limit( + deps.as_mut(), + env, + info, + book_id, + invalid_tick_id, + OrderDirection::Ask, + Uint128::new(100), + ); + + assert!(matches!( + response, + Err(ContractError::InvalidTickId { tick_id }) if tick_id == invalid_tick_id + )); +} + +#[test] +fn test_place_limit_with_invalid_quantity() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + + // Create an orderbook first + let quote_denom = "quote".to_string(); + let base_denom = "base".to_string(); + let create_response = create_orderbook( + deps.as_mut(), + env.clone(), + info.clone(), + quote_denom, + base_denom, + ) + .unwrap(); + + // Retrieve the book_id from the first attribute of create_response + let book_id: u64 = create_response.attributes[1] + .value + .parse() + .expect("book_id attribute parse error"); + + let invalid_quantity = Uint128::zero(); // Invalid quantity + + let response = place_limit( + deps.as_mut(), + env, + info, + book_id, + 1, // tick_id + OrderDirection::Ask, + invalid_quantity, + ); + + assert!(matches!( + response, + Err(ContractError::InvalidQuantity { quantity }) if quantity == invalid_quantity + )); +} + +#[test] +fn test_place_limit_with_insufficient_funds() { + let insufficient_balance = Uint128::new(500); // Mocked balance less than required + let balances = vec![(coin(insufficient_balance.u128(), "base"))]; + let mut deps = mock_dependencies_with_balances(&[("creator", &balances)]); + + let env = mock_env(); + let info = mock_info("creator", &[]); + + // Create an orderbook first + let quote_denom = "quote".to_string(); + let base_denom = "base".to_string(); + let create_response = create_orderbook( + deps.as_mut(), + env.clone(), + info.clone(), + quote_denom, + base_denom, + ) + .unwrap(); + + // Retrieve the book_id from the first attribute of create_response + let book_id: u64 = create_response.attributes[1] + .value + .parse() + .expect("book_id attribute parse error"); + + let required_quantity = Uint128::new(1000); // Quantity greater than the mocked balance + + let response = place_limit( + deps.as_mut(), + env, + info, + book_id, + 1, // tick_id + OrderDirection::Ask, + required_quantity, + ); + + assert!(matches!( + response, + Err(ContractError::InsufficientFunds { balance, required }) if balance == insufficient_balance && required == required_quantity + )); +}