diff --git a/packages/types/src/stream/msg.rs b/packages/types/src/stream/msg.rs index c2a2512..9bcf928 100644 --- a/packages/types/src/stream/msg.rs +++ b/packages/types/src/stream/msg.rs @@ -47,12 +47,6 @@ pub enum QueryMsg { /// Returns a stream's current state. #[returns(StreamResponse)] Stream {}, - /// Returns list of streams paginated by `start_after` and `limit`. - // #[returns(StreamsResponse)] - // ListStreams { - // start_after: Option, - // limit: Option, - // }, /// Returns current state of a position. #[returns(PositionResponse)] Position { owner: String }, diff --git a/tests/src/tests/streamswap_tests/finalize_stream.rs b/tests/src/tests/streamswap_tests/finalize_stream.rs index 47a4ff7..4f84abb 100644 --- a/tests/src/tests/streamswap_tests/finalize_stream.rs +++ b/tests/src/tests/streamswap_tests/finalize_stream.rs @@ -652,4 +652,140 @@ mod finalize_stream_tests { .unwrap(); assert_eq!(contract_balance.len(), 0); } + + #[test] + fn out_amount_refund() { + let Suite { + mut app, + test_accounts, + stream_swap_code_id, + stream_swap_controller_code_id, + vesting_code_id, + } = SuiteBuilder::default().build(); + let start_time = app.block_info().time.plus_seconds(100); + let end_time = app.block_info().time.plus_seconds(200); + let bootstrapping_start_time = app.block_info().time.plus_seconds(50); + let msg = get_controller_inst_msg(stream_swap_code_id, vesting_code_id, &test_accounts); + let controller_address = app + .instantiate_contract( + stream_swap_controller_code_id, + test_accounts.admin.clone(), + &msg, + &[], + "Controller".to_string(), + None, + ) + .unwrap(); + let create_stream_msg = CreateStreamMsgBuilder::new( + "Stream Swap tests", + test_accounts.creator_1.as_ref(), + coin(1_000_000, "out_denom"), + "in_denom", + bootstrapping_start_time, + start_time, + end_time, + ) + .build(); + + // This case handles the scenario where the `out_amount` is refunded to the creator. + // For this to happen, the allocated amount for the stream must remain unspent during the stream's lifetime. + // If, at any point during the stream phase, no subscribers are active, + // a portion of the `out_amount` may remain unused and will be left in the contract. + // This unspent amount should be refunded separately to the creator. + + let res = app + .execute_contract( + test_accounts.creator_1.clone(), + controller_address.clone(), + &create_stream_msg, + &[coin(100, "fee_denom"), coin(1_000_000, "out_denom")], + ) + .unwrap(); + let stream_swap_contract_address: String = get_contract_address_from_res(res); + let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; + + app.set_block(BlockInfo { + height: 1_100, + time: start_time, + chain_id: "test".to_string(), + }); + + // First subscription + let _res = app + .execute_contract( + test_accounts.subscriber_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &subscribe_msg, + &[coin(200, "in_denom")], + ) + .unwrap(); + + // Update environment time to end_time + app.set_block(BlockInfo { + height: 1_100, + time: end_time.minus_seconds(1), + chain_id: "test".to_string(), + }); + + // Subscriber 1 withdraws from the stream + let withdraw_msg = StreamSwapExecuteMsg::Withdraw { cap: None }; + let _res = app + .execute_contract( + test_accounts.subscriber_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &withdraw_msg, + &[], + ) + .unwrap(); + + //Update environment time to end_time + app.set_block(BlockInfo { + height: 1_100, + time: end_time, + chain_id: "test".to_string(), + }); + + // Finalize the stream + let finalized_msg = StreamSwapExecuteMsg::FinalizeStream { + new_treasury: None, + create_pool: None, + salt: None, + }; + let res = app + .execute_contract( + test_accounts.creator_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &finalized_msg, + &[], + ) + .unwrap(); + + let stream_swap_funds = get_funds_from_res(res); + assert_eq!( + stream_swap_funds, + vec![ + ( + String::from(test_accounts.creator_1.clone()), + Coin { + denom: "out_denom".to_string(), + amount: Uint128::new(10000) + }, + ), + ( + String::from(test_accounts.creator_1.clone()), + Coin { + denom: "in_denom".to_string(), + amount: Uint128::new(197) + }, + ), + ( + String::from(test_accounts.admin.clone()), + Coin { + denom: "in_denom".to_string(), + amount: Uint128::new(1) + }, + ) + ] + ); + } } diff --git a/tests/src/tests/streamswap_tests/pool.rs b/tests/src/tests/streamswap_tests/pool.rs index 9f26481..df6193d 100644 --- a/tests/src/tests/streamswap_tests/pool.rs +++ b/tests/src/tests/streamswap_tests/pool.rs @@ -8,6 +8,7 @@ mod pool { use cosmwasm_std::{coin, Addr, BlockInfo, Coin, Uint256}; use cw_multi_test::Executor; use cw_utils::NativeBalance; + use streamswap_controller::error::ContractError as ControllerError; use streamswap_types::controller::{CreatePool, PoolConfig}; use streamswap_types::stream::ExecuteMsg as StreamSwapExecuteMsg; use streamswap_types::stream::QueryMsg as StreamSwapQueryMsg; @@ -486,4 +487,101 @@ mod pool { .collect(); assert_eq!(res_funds, expected_res); } + + #[test] + fn pool_validations() { + let Suite { + mut app, + test_accounts, + stream_swap_code_id, + stream_swap_controller_code_id, + vesting_code_id, + } = SuiteBuilder::default().build(); + + let msg = get_controller_inst_msg(stream_swap_code_id, vesting_code_id, &test_accounts); + let controller_address = app + .instantiate_contract( + stream_swap_controller_code_id, + test_accounts.admin.clone(), + &msg, + &[], + "Controller".to_string(), + None, + ) + .unwrap(); + + let start_time = app.block_info().time.plus_seconds(100); + let end_time = app.block_info().time.plus_seconds(200); + let bootstrapping_start_time = app.block_info().time.plus_seconds(50); + let out_supply = 1_000_000u128; + let out_denom = "out_denom"; + + let out_coin = coin(out_supply, out_denom); + let pool_creation_fee = coin(1000000, "fee_denom"); + let stream_creation_fee = coin(100, "fee_denom"); + let in_denom = "in_denom"; + + // Pool amount 0 case + let create_stream_msg = CreateStreamMsgBuilder::new( + "stream", + test_accounts.creator_1.as_ref(), + out_coin.clone(), + in_denom, + bootstrapping_start_time, + start_time, + end_time, + ) + .threshold(Uint256::from(100u128)) + .pool_config(PoolConfig::ConcentratedLiquidity { + out_amount_clp: Uint256::zero(), + }) + .build(); + + let res = app + .execute_contract( + test_accounts.creator_1.clone(), + controller_address.clone(), + &create_stream_msg, + &[ + pool_creation_fee.clone(), + out_coin.clone(), + stream_creation_fee.clone(), + ], + ) + .unwrap_err(); + let err = res.downcast::().unwrap(); + assert_eq!(err, ControllerError::InvalidPoolOutAmount {}); + + // Pool amount greater than out supply case + let create_stream_msg = CreateStreamMsgBuilder::new( + "stream", + test_accounts.creator_1.as_ref(), + out_coin.clone(), + in_denom, + bootstrapping_start_time, + start_time, + end_time, + ) + .threshold(Uint256::from(100u128)) + .pool_config(PoolConfig::ConcentratedLiquidity { + out_amount_clp: Uint256::from(out_supply + 1), + }) + .build(); + + let res = app + .execute_contract( + test_accounts.creator_1.clone(), + controller_address.clone(), + &create_stream_msg, + &[ + pool_creation_fee.clone(), + out_coin.clone(), + stream_creation_fee, + ], + ) + .unwrap_err(); + + let err = res.downcast::().unwrap(); + assert_eq!(err, ControllerError::InvalidPoolOutAmount {}); + } }