diff --git a/src/action/execute/create_spot_order.rs b/src/action/execute/create_spot_order.rs index bc58bd7..ab656f4 100644 --- a/src/action/execute/create_spot_order.rs +++ b/src/action/execute/create_spot_order.rs @@ -1,3 +1,7 @@ +use cosmwasm_std::{to_json_binary, Int128, StdResult, Storage, SubMsg}; + +use crate::msg::ReplyType; + use super::*; pub fn create_spot_order( @@ -18,18 +22,19 @@ pub fn create_spot_order( &order_source_denom, &order_target_denom, &order_price, + &order_type, &info.funds[0].denom, )?; let mut order_vec = SPOT_ORDER.load(deps.storage)?; let new_order: SpotOrder = SpotOrder::new( - order_type, + order_type.clone(), order_price, info.funds[0].clone(), info.sender.clone(), order_target_denom, - order_amm_routes, + order_amm_routes.clone(), &order_vec, ); @@ -40,13 +45,16 @@ pub fn create_spot_order( cw_utils::must_pay(&info, &info.funds[0].denom)?; - let resp = Response::new() - .add_attribute("order_id", new_order.order_id.to_string()) - .add_message(bank_msg); // information message + let resp = create_resp( + env.contract.address.as_str(), + &new_order, + bank_msg, + deps.storage, + )?; order_vec.push(new_order); - SPOT_ORDER.save(deps.storage, &order_vec)?; + Ok(resp) } @@ -54,6 +62,7 @@ fn check_denom_error( order_source_denom: &str, order_target_denom: &str, order_price: &SpotOrderPrice, + order_type: &SpotOrderType, funds_send_denom: &str, ) -> Result<(), ContractError> { if order_source_denom != funds_send_denom { @@ -64,6 +73,10 @@ fn check_denom_error( return Err(ContractError::SpotOrderSameDenom); } + if order_type == &SpotOrderType::Market { + return Ok(()); + } + if (order_price.base_denom != order_source_denom && order_price.base_denom != order_target_denom) || (order_price.quote_denom != order_source_denom @@ -74,3 +87,45 @@ fn check_denom_error( Ok(()) } + +fn create_resp( + sender: &str, + new_order: &SpotOrder, + bank_msg: BankMsg, + storage: &mut dyn Storage, +) -> StdResult> { + let resp = Response::new() + .add_attribute("order_id", new_order.order_id.to_string()) + .add_message(bank_msg); // information message + + if new_order.order_type != SpotOrderType::Market { + return Ok(resp); + } + + let mut reply_infos = REPLY_INFO.load(storage)?; + + let swap_msg = ElysMsg::amm_swap_exact_amount_in( + sender, + &new_order.order_amount, + &new_order.order_amm_routes, + Int128::zero(), + ); + + let info_id = if let Some(max_info) = reply_infos.iter().max_by_key(|info| info.id) { + max_info.id + 1 + } else { + 0 + }; + + reply_infos.push(ReplyInfo { + id: info_id, + reply_type: ReplyType::SpotOrder, + data: Some(to_json_binary(&new_order.order_id)?), + }); + + REPLY_INFO.save(storage, &reply_infos)?; + + let sub_msg = SubMsg::reply_always(swap_msg, info_id); + + Ok(resp.add_submessage(sub_msg)) +} diff --git a/src/action/execute/process_spot_orders.rs b/src/action/execute/process_spot_orders.rs index 78483f0..49de71c 100644 --- a/src/action/execute/process_spot_orders.rs +++ b/src/action/execute/process_spot_orders.rs @@ -70,6 +70,10 @@ fn send_token( } fn check_order(order: &SpotOrder, querier: &ElysQuerier) -> bool { + if order.order_type == SpotOrderType::Market { + return true; + } + let amm_swap_estimation = match querier.amm_swap_estimation(&order.order_amm_routes, &order.order_amount) { Ok(res) => res, @@ -89,6 +93,7 @@ fn check_order(order: &SpotOrder, querier: &ElysQuerier) -> bool { SpotOrderType::LimitSell => order_token_out <= amm_swap_estimation.token_out.amount, SpotOrderType::StopLoss => order_token_out >= amm_swap_estimation.token_out.amount, + _ => false, } } @@ -102,6 +107,7 @@ fn process_order( SpotOrderType::LimitBuy => calculate_token_out_min_amount(order), SpotOrderType::LimitSell => calculate_token_out_min_amount(order), SpotOrderType::StopLoss => Int128::zero(), + SpotOrderType::Market => Int128::zero(), }; let msg = ElysMsg::amm_swap_exact_amount_in( diff --git a/src/tests/create_spot_order/successful_create_market_order.rs b/src/tests/create_spot_order/successful_create_market_order.rs new file mode 100644 index 0000000..266d671 --- /dev/null +++ b/src/tests/create_spot_order/successful_create_market_order.rs @@ -0,0 +1,129 @@ +use crate::tests::get_order_id_from_events::get_order_id_from_events; + +use super::*; + +// This test case verifies the successful creation of a "market" order in the contract. +// A "market" order is designed to execute right after. +#[test] +fn successful_create_stop_loss_order() { + // Create a wallet for the "user" with an initial balance of 2 BTC. + let wallet = vec![("user", coins(2, "btc"))]; + + let prices = vec![ + Price::new( + "btc", + Decimal::from_atomics(Uint128::new(30000), 0).unwrap(), + ), + Price::new("usdc", Decimal::from_atomics(Uint128::new(1), 0).unwrap()), + ]; + + // Initialize the ElysApp instance with the specified wallet. + let mut app = ElysApp::new_with_wallets(wallet); + + // Set the BTC and USDC prices. + app.init_modules(|router, _, store| router.custom.set_prices(store, &prices)) + .unwrap(); + + // Create a mock message to instantiate the contract with no initial orders. + let instantiate_msg = InstantiateMockMsg { + process_order_executor: "owner".to_string(), + orders: vec![], + }; + + // Create a contract wrapper and store its code. + let code = ContractWrapper::new(execute, instantiate, query).with_reply(reply); + let code_id = app.store_code(Box::new(code)); + + // Instantiate the contract with "owner" as the deployer. + let addr = app + .instantiate_contract( + code_id, + Addr::unchecked("owner"), + &instantiate_msg, + &[], + "Contract", + None, + ) + .unwrap(); + + // User "user" creates a "market" order for BTC to USDC. + let resp = app + .execute_contract( + Addr::unchecked("user"), + addr.clone(), + &ExecuteMsg::CreateSpotOrder { + order_type: SpotOrderType::Market, + // Empty order price - not utilized in market orders + order_price: SpotOrderPrice { + base_denom: "".to_string(), + quote_denom: "".to_string(), + rate: Decimal::zero(), + }, + order_amm_routes: vec![SwapAmountInRoute::new(1, "usdc")], + order_source_denom: "btc".to_string(), + order_target_denom: "usdc".to_string(), + }, + &coins(2, "btc"), // User's BTC balance. + ) + .unwrap(); + + // Verify that the "user" no longer has any BTC after creating the order. + assert_eq!( + app.wrap() + .query_balance("user", "btc") + .unwrap() + .amount + .u128(), + 0 + ); + + // Verify that the contract address has swap the 2 BTC for the order (market). + assert_eq!( + app.wrap() + .query_balance(&addr, "btc") + .unwrap() + .amount + .u128(), + 0 + ); + + // Verify that the contract address now holds the 60000 usdc. + assert_eq!( + app.wrap() + .query_balance(&addr, "usdc") + .unwrap() + .amount + .u128(), + 60000 + ); + + // Verify that an order ID is emitted in the contract's events. + assert!(get_order_id_from_events(&resp.events).is_some()); + + app.execute_contract( + Addr::unchecked("owner"), + addr.clone(), + &ExecuteMsg::ProcessSpotOrders {}, + &[], + ) + .unwrap(); + + // Verify that the user got his swaped token + assert_eq!( + app.wrap() + .query_balance("user", "usdc") + .unwrap() + .amount + .u128(), + 60000 + ); + + assert_eq!( + app.wrap() + .query_balance(&addr, "usdc") + .unwrap() + .amount + .u128(), + 0 + ); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index b166053..3313deb 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -21,6 +21,7 @@ mod create_spot_order { mod order_wrong_fund; mod successful_create_limit_buy_order; mod successful_create_limit_sell_order; + mod successful_create_market_order; mod successful_create_stop_loss_order; } diff --git a/src/types/spot_order_type.rs b/src/types/spot_order_type.rs index 7a55699..3550821 100644 --- a/src/types/spot_order_type.rs +++ b/src/types/spot_order_type.rs @@ -5,4 +5,5 @@ pub enum SpotOrderType { StopLoss, LimitSell, LimitBuy, + Market, }